vue源码手写实现(五):实现计算属性和watch

  1. 1. 计算属性
    1. 1.1. 计算属性的形式
    2. 1.2. 特性
    3. 1.3. 初始化computed
    4. 1.4. 为计算属性添加响应式
    5. 1.5. 兼容watcher
  2. 2. Watch
    1. 2.1. 简介
    2. 2.2. 初始化
    3. 2.3. 处理watch
    4. 2.4. $watch实现与watcher调整

计算属性

计算属性的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 计算属性有两种形式

computed:{
// 1.函数
fullname(){
return this.firstname + this.lastname
},
// 2.对象
fullname:{
get(){
return this.firstname + this.lastname
},
set(newVal){
console.log(newVal)
}
}
}

特性

计算属性具有缓存,当多次调用时,如果值没有改变则只会计算一次。只有依赖值变了才会重新执行用户的方法。

计算属性是一个defineProperty,计算属性也是一个watcher。

初始化computed

计算属性也是配置在新建Vue实例的参数中,所以我们需要在state.js中中取到,再进行处理。

我们额外新建一个initComputed方法,在state.js中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// state.js
// 在initSate中,initData之后

if(opts.computed){
initComputed(vm);
}


function initComputed(vm){
const computed = vm.$options.computed;
const watchers = vm._computedWatchers = {};

for(let key in computed){
let userDef = computed[key];
// 将属性和watcher对应起来
let fn = typeof userDef === 'function' ? userDef : userDef.get;
watchers[key] = new Watcher(vm,fn,{lazy:true});
defineComputed(vm,key,userDef);
}
}

为计算属性添加响应式

计算属性本质上也是一个响应式数据,能在页面中使用花括号表达式直接使用。所以这里也是使用Object.defineProperty()绑定响应式。

其中的set无须处理,本质是依赖的data中的值,data本身set会触发赋值重新渲染逻辑,我们只考虑重新渲染时加上计算属性watcher即可。

get函数需要单独处理,计算属性具有缓存功能,当计算值未发生变化时,多次使用也只会调用一次get,我们需要通过维护一个dirty属性去判断是否需要重新计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function defineComputed(target,key,userDef){
const setter = userDef.set || (()=>{});

Object.defineProperty(target,key,{
// get: typeof userDef === 'function' ? userDef : userDef.get,
get: createComputedGetter(key),
set: setter,
})
}


// 计算属性根本不会收集依赖,只有让自己依赖属性去收集依赖
function createComputedGetter(key){
// 需要监测是否执行这个getter
return function(){
const watcher = this._computedWatchers[key];
if(watcher.dirty){
// 如果是脏,就去调用用户的函数
watcher.evaluate();
}

// 计算属性出栈后,还要渲染watcher
// 让计算属性watcher里的属性,也去收集上一层的依赖
if(Dep.target){
watcher.depend();
}
return watcher.value;
}
}

兼容watcher

从前边代码可以看出,new Watcher时传入了一个lazy属性。我们在watcher内部新增一个dirty属性,取lazy值,默认第一次watcher则为脏,需要重新计算。

新增evaluate方法,用于重新计算值。计算后将dirty属性维护为false,当值未发生变动时不会重新计算。

那么更新处的代码则需要判断,通过lazy去判断是否为计算属性watcher,若为真则将dirty维护为true。这里不会执行渲染,当计算属性更新时,更新栈里会有两个watcher,一个是计算属性watcher,另一个是渲染watcher。而且渲染watcher会在计算属性之后执行(观察代码顺序可以看出)。

下边是部分watcher.js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
constructor(vm,fn,options){
this.id = id++;

this.renderWatcher = options;// 标识是一个渲染watcher
this.getter = fn;// getter会发生取值操作

this.deps = [];
this.depsId = new Set();

this.lazy = options.lazy;
this.dirty = this.lazy;// 缓存值
this.vm = vm;

this.lazy ? undefined : this.get();
}

depend(){
let i = this.deps.length;
while(i--){
this.deps[i].depend();
}
}

evaluate(){
this.value = this.get();// 获取到用户的返回值,并且标识为脏
this.dirty = false;
}

update(){// 执行更新
// this.get();// 重新渲染
if(this.lazy){
// 如果是计算属性,依赖的值变化了,就标识计算属性更新了
this.dirty = true
}else{
queueWatcher(this);// 把当前的watcher保存起来
}
}

depend()是让dep记住watcher,下次执行渲染时才能将计算属性一并渲染。

Watch

简介

watch就是一个更加直白的观察者模式了:当某个值发生变化,则执行回调。同理是存在一个对于watch的watcher。

那么watch的形式在vue2中主要有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
watch:{
// 函数形式
firstname(newValue,oldValue){
console.log(newVlaue,oldValue);
},
// 数组形式
firstname:[
(newValue,oldValue)=>{
console.log(newVlaue,oldValue);
},
(newValue,oldValue)=>{
console.log(newVlaue,oldValue);
},
],
// 使用methods中函数
firstname: 'fn',
}

// 使用$watch
vm.$watch(()=>vm.firstname,(newVal,oldVal)=>{
console.log(newVlaue,oldValue);
})

在底层不论哪种形式,最终都会转成$watch()。

初始化

与计算属性同理,需要先获取到所有的watch选项,再循环处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(opts.watch){
initWatch(vm);
}

function initWatch(vm){
let watch = vm.$options.watch;

for(let key in watch){
const handler = watch[key];

if(Array.isArray(handler)){
for(let i=0;i<handler.length;i++){
createWatcher(vm,key,handler[i]);
}
}else{
createWatcher(vm,key,handler);
}
}
}

处理watch

watch处理比较简单,因为在底层都是调用$watch钩子,那么只要处理好形式,再统一调用这个钩子即可。

1
2
3
4
5
6
function createWatcher(vm,key,handler){
if(typeof handler === 'string'){
handler = vm[handler]
}
return vm.$watch(key,handler);
}

$watch实现与watcher调整

watch只需要监听到变化再执行一个回调即可,同理要建一个专门处理watch的watcher,options传入user:true进行标识。

1
2
3
4
Vue.prototype.$watch = function(expOrfn,cb,options={}){
// 值一变化,执行cb回调即可
new Watcher(this,expOrfn,{user:true},cb)
}

剩下的处理都在watcher中。

在watcher.js里,我们在构造函数需要额外新增一个cb用于接受回调函数。在构造函数内部接收user和cb。

在run函数中判断user,为true则执行cb()回调。

watch的老值和新值如何处理?

在构造函数中取出一次get值,此时是新建watcher时执行的一次,作为初始值。当后续数值变化更新时,会由Dep触发更新再次执行get。我们分别取到两次get返回值,作为新值和老值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
constructor(vm,exprOrFn,options,cb){
this.id = id++;

this.renderWatcher = options;// 标识是一个渲染watcher

if(typeof exprOrFn === 'string'){
this.getter = function(){
return vm[exprOrFn];
}
}else{
this.getter = exprOrFn;// getter会发生取值操作
}

this.deps = [];
this.depsId = new Set();

this.lazy = options.lazy;
this.dirty = this.lazy;// 缓存值
this.vm = vm;

this.cb = cb;
this.user = options.user;// 标识是否是用户watcher

this.value = this.lazy ? undefined : this.get();
}

run(){
let oldValue = this.value;// 计算之前的值,为老值
// 重新执行get获取的值为新值
let newValue = this.get();// 真正执行渲染
console.log('this',this);
if(this.user){
this.cb.call(this.vm,oldValue,newValue);// 如果是watch,执行回调
}
}

至此,已能实现watch的基本功能。