阅读 390

watch、computed在实现原理上有什么不同?

背景

watchcomputed在使用上有什么不同,相信大家都很清楚。那么让我们深入来看看,他们在实现原理上有什么不同吧?

watch实现原理

  • 类型{ [key: string]: string | Function | Object | Array }

  • 详细:一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个 property

1. 初始化watch

export function initState(vm) {
  //对vm上的watch做初始化
  if (opts.watch) {
    initWatch(vm, opts.watch);
}

function initWatch(vm, watch) {
//暂时不考虑值是方法名的情况
  for (key in watch) {
    let 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);
    }
  }
}

function createWatcher(vm, key, handler) {
  return vm.$watch(key, handler);
}
//扩展Vue原型上的方法,都通过mixin的方式来进行添加的。
export function stateMixin(Vue) {
    Vue.prototype.$watch = function(key, handler, options = {}) {
        options.user = true; 
        // 标示 this.user 区分渲染 watcher 和用户 watcher
        new Watcher(this, key, handler, options);
    }
}
复制代码

2. watcher实现

  1. 将表达式 key/exprOrFn 转化为函数,方便后面调用 this.getter
  2. this.getter取值调用Object.definePropertyget方法里的dep.depend收集当前的 watcher
  3. 第一次 new Watcher 的时候,调用了 get 方法保存初始 this.value=this.get()。第二次用户更新 run() 又调用 get 方法 保存下新值 newValue=this.get(),并执行回调函数。
class Watcher {
    constructor(vm, exprOrFn, callback, options) {
        // ...
        this.user = !! options.user
        if(typeof exprOrFn === 'function'){
            this.getter = exprOrFn; 
        }else{
            this.getter = function (){ // 将表达式转换成函数
                let path = exprOrFn.split('.');
                let obj = vm;
                for(let i = 0; i < path.length;i++){
                    obj = obj[path[i]];
                }
                return obj;
            }
        }
        this.value = this.get(); // 将初始值记录到value属性上
    }
    get() {
        pushTarget(this); // 把用户定义的watcher存起来  
        const value = this.getter.call(this.vm); // 执行函数 (依赖收集)
        popTarget(); // 移除watcher
        return value;
    }
  
    run(){
        let value = this.get();    // 获取新值
        let oldValue = this.value; // 获取老值
        this.value = value;
        if(this.user){ // 如果是用户watcher 则调用用户传入的callback
            this.callback.call(this.vm,value,oldValue)
        }
    }
}
复制代码

computed实现原理

  • 类型{ [key: string]: Function | { get: Function, set: Function } }
  • 详细:计算属性默认不执行。计算属性的结果会被缓存,除非依赖的响应式 property 变化才会重新计算。注意,如果某个依赖 (比如非响应式 property) 在该实例范畴之外,则计算属性是不会被更新的。

1. 初始化computed

computed 的每个属性 key 创建一个 watcher

export function initState(vm) {
  //对vm上的computed做初始化
  if (opts.computed) {
    initComputed(vm, opts.computed);
  }
}
function initComputed(vm, computed) {

    const watchers = vm._computedWatchers = {}
    for (let key in computed) {
        // 校验 
        const userDef = computed[key];
        // 依赖的属性变化就重新取值 get
        let getter = typeof userDef == 'function' ? userDef : userDef.get;
        // 每个就算属性本质就是watcher   
        // 将watcher和 属性 做一个映射
        watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true }); // 默认不执行
        // 将key 定义在vm上
        defineComputed(vm, key, userDef);
    }
}
复制代码

将 key 定义在 vm 上 ,这样在页面上才能直接取到computed的值

  • this._computedWatchers 包含着所有的计算属性
  • 通过 key 可以拿到对应的 watcher,这个 watcher 中包含了 getter,如果 dirty ture 则调用 evaluate
function defineComputed(vm, key, userDef) {
    let sharedProperty = {};
    if (typeof userDef == 'function') {
       sharedProperty.get = createComputedGetter(key)
    } else {
        sharedProperty.get = createComputedGetter(key);
        sharedProperty.set = userDef.set ;
    }
    Object.defineProperty(vm, key, sharedProperty); // computed就是一个defineProperty
}
复制代码

创建缓存getter,取计算属性的值,走的是这个函数。

function createComputedGetter(key) {
    
    return function computedGetter() { 
        // this._computedWatchers 包含着所有的计算属性
        // 通过key 可以拿到对应watcher,这个watcher中包含了getter
        let watcher = this._computedWatchers[key]
        // 脏就是 要调用用户的getter  不脏就是不要调用getter

        if(watcher.dirty){ // 根据dirty属性 来判断是否需要重新求职
            watcher.evaluate();// this.get()
        }

        // 如果当前取完值后 Dep.target还有值  需要继续向上收集
        if(Dep.target){
            watcher.depend(); // watcher 里 对应了 多个dep
        }
        return watcher.value
    }
}
复制代码

2. watcher实现

class Watcher {
    constructor(vm, exprOrFn, cb, options) {
       //...
        this.lazy = !!options.lazy;
        this.dirty = options.lazy; // 如果是计算属性,那么默认值lazy:true, 
        this.getter = exprOrFn; //  computed[key]/computed[key].get
        this.value = this.lazy ? undefined : this.get(); 
        
    }
    get() { 
        pushTarget(this);
        const value = this.getter.call(this.vm);
        popTarget();
        return value
    }
    update() { 
    if(this.lazy){
            this.dirty = true;
        }else{
            queueWatcher(this);
        }
    }
  
    evaluate(){
        this.dirty = false; // 为false表示取过值了
        this.value = this.get(); // 用户的getter执行
    }
    depend(){
        let i = this.deps.length;
        while(i--){
            this.deps[i].depend(); //lastName,firstName 收集渲染watcher
        }
    }
}



复制代码

computed的实现实在是太绕来绕去了😭😭😭😭😭
我们通过几个问题来理顺一下思路吧
QS1:计算属性如何实现缓存?
通过在watcher上定义了一个dirty属性。当dirty为true时,调用evaluate重新求值。

QS2:计算属性依赖的值变化,怎么让计算属性重新求值?
改变依赖值 --> 触发set --> 触发dep.notify --> watcher.update --> 是计算watcher --> this.dirty = true --> 当dirty为true时,调用evaluate重新求值。

QS3:计算属性依赖的值变化,怎么让视图更新?
目前依赖的值(没有在页面中使用的前提下) 的dep上只有一个计算属性wacher,想让视图更新那么,要将渲染wacher也放入依赖的数据的dep中,这样依赖的属性发生变化也可以让视图进行更新。

总结

watchcomputed在实现原理上的不同:
watch实现是给watch 对象的每一个 key分配了一个watcherthis.get()取值,收集当前的用户watcher,并保存下初始值。当key变化的时候,触发watcher.run(),保存下新值,同时执行回调函数cb
computed的实现通过给computed 对象的每一个 key分配了一个lazy Watcher,默认不执行,取值的时候才执行。Object.definePropertyvm上定义了computed的每个key。通过key所依赖值收集当前的渲染watcher,来实现依赖值变化,视图更新。通过dirty属性来实现缓存效果。

文章分类
前端
文章标签