watcher和computed实现原理

284 阅读3分钟

watcher和computed有何不同是面试中经常出现的一道题目,但是很少有文章从原理上剖析两者的差异

1. watch实现原理

watch和computed的实现都依赖了watcher类,通过options传入的标记判断是渲染watcher、用户watcher或者计算watcher。对于这两个方法源码的解读,应该关注何时调用watcher.get方法,以及update方法对这两个属性的判断逻辑。

  1. initWatch

vue在初始化阶段,调用initWatch,为每一个watch建立一个watcher实理,这个watcher实例接受4个参数, vm, 属性名,handler函数,以及options ;options内部传入user: true属性标记这是一个用户watcher

function initWatch(vm,watch) {
	for(let key in watch) {
		const handler = watch[key]
		const options = {user: true,deep: false, immediate: true}
		options.deep = watch[key][deep]
		options.immediate = watch[key][immediate]
		createWatcher(vm,key,handler,options)
	}
}

function createWatcher(vm, key, handler, options) {
	options.user = true
	return vm.$watch(vm,key,handler,options)
}

Vue.prototype.$watch = function (vm,exporfn, cb, options) {
	new Watcher(this, exporfn, cb, options)
}
  1. watcher依赖的值变化后,如何通知到这个监测watcher?

watcher类在初始化阶段会对传入的exporfn进行判断,如果是一个表达式则处理为函数,然后调用这个函数获取其对应的值,在读值时触发依赖收集逻辑,收集到这个用户watcher,当值更新时,触发更新逻辑,判断这是一个用户watcher, 通过run方法调用其传入的回调函数

class Watcher {
  constuctor() {
    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属性上
  }
  update() {
    if(this.immediate) {
      this.run()
    }else {
        queueWatcher(this)
    }
  }

  run(){
    let value = this.get();    // 获取新值
    let oldValue = this.value; // 获取老值
    this.value = value;
    if(this.user){ // 如果是用户watcher 则调用用户传入的callback
        this.callback.call(this.vm,value,oldValue)
    }
  }
}
  1. 如何实现immediate
if(this.immediate === true) {
  this.run()
}

2. computed实现原理

每个计算属性也都是一个watcher,计算属性需要表示lazy:true,这样在初始化watcher时不会立即调用计算属性方法

  1. initWatch
function initComputed(vm, computed) {
    // 存放计算属性的watcher
    const watchers = vm._computedWatchers = {};
    for (const key in computed) {
        const userDef = computed[key];
        // 获取get方法
        const getter = typeof userDef === 'function' ? userDef : userDef.get;
        // 创建计算属性watcher
        watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true });
        defineComputed(vm, key, userDef)
    }
}

  1. 通过defineProperty把计算属性代理到vm上方便我们获取, 当读取值时,调用getter, 在getter中获取调用其对应的watcher.evaluate方法获取值,取值操作会触发依赖搜集逻辑
function defineComputed(target, key, userDef) {
    if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = createComputedGetter(key)
    } else {
        sharedPropertyDefinition.get = createComputedGetter(userDef.get);
        sharedPropertyDefinition.set = userDef.set;
    }
    // 使用defineProperty定义
    Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = this._computedWatchers[key];
        if (watcher) {
            if (watcher.dirty) { // 如果dirty为true
                watcher.evaluate();// 计算出新值,并将dirty 更新为false
            }
            // 如果依赖的值不发生变化,则返回上次计算的结果
            return watcher.value
        }
    }
}

为了实现缓存,在get中注入额外的操作,通过判断dirty是否为true,决定是否需要更新,每次更新后把dirty置为false,由于evaluate调用了get方法,在这个过程中读取到了数据,触发依赖收集逻辑,把当前watcher收集起来了,当数据更新时,调用watcher的update方法,update方法中判断这个数据是一个computed watcher,就会把dirty置为true

class Watcher {
    constructor(vm, exprOrFn, callback, options) {
        this.vm = vm;
        this.dirty = this.lazy
        // ...
        this.value = this.lazy ? undefined : this.get(); // 调用get方法 会让渲染watcher执行
    }
    get() {
      pushTarget(this)
      let value = this.getter.call(this.vm)
      popTarget()
      return value
    }
    evaluate() {
      this.value = this.get()
      this.dirty = false
    }
    update() {
        if (this.lazy) {
            this.dirty = true;
        } else {
            queueWatcher(this);
        }
    }
}

3. 结论

computed是一个数据,通过object.defineProperty代理到vm实例上,我们可以在模板中作为数据直接使用

computed属性具有缓存特性,只有其以依赖的数据变化了才会重新计算最新的值

watcher本质上是一个监测到数据变化后的执行的回调函数,当监测的数据变化后会执行