复习computed和watch的区别时,重撸computed源码实现

131 阅读5分钟

computed和watch的区别

相同点:两者都是观察页面数据变化的

不同点:

  • computed只有当依赖的数据变化时才回去计算,否则只会从缓存中去取
  • watch每次都需要执行函数,更适用于数据变化的异步操作

computed的基本原理和源码实现

computed的设计初衷是:为了使模板中的逻辑运算更加简单

  • 可以使模板中的逻辑更加简单清晰,方便代码管理
  • 可以使用缓存,只有当依赖的数据发生变化的时候,才会重新计算

computed的初始化

寻找初始化函数(定义)

通常在我们的项目中的main.js中,通过 new Vue({})来实例化代码,

  • 调用的就是vue/src/core/instance/index.js 里面的_init()方法,_init()是在initMixin()方法中的
  • _init()中调用了initState() 进而在存在opts.computed调用了initComputed(vm, opts.computed)方法,这里我们就找到了初始化computed的方法了
初始化函数中都写了啥
  1. 首先使用 Object.create(null); 创建一个空对象, 分别赋值给 watchersvm._computedWatchers
  • 这是在实例上定义 _computedWatchers 对象,用于存储 ’计算属性Watcher‘
  1. 因为computed可以写成一个函数或者一个对象,所以,在接下来我们是使用for in循环遍历,computed中的每一个值,同时判断是函数声明还是对象声明,来获取每个计算属性的getter

  2. 接下来是为每个计算属性,根据key来实例化对应的Watcher,存在上面创建的watchers中,getter作为其中的一个参数

    • 创建计算属性Watcher getter作为参数传入,他会在依赖属性更新的时候调用,并对计算属性重新取值,需要注意里面的lazy配置,那是实现缓存的标识

    • 可以理解为computed中的每一个key对应的值,都是一个Watcher的实例,通过发布订阅模式来监听的

  3. 说到了lazy配置和用到了Watcher构造函数,那么我们就要去查看Watcher的实现

    • 在第三步中lazy是通过第四个参数传入的

      const computedWatcherOptions = { lazy: true };
      
    • Watcherconstructor一共有五个参数,第三部传入了四个,这里我们关注点在第四个参数我们传入了 options = {lazy: true};

      this.vm = vm	
      // 这是Watcher构造函数中,接下来的代码
      
      		vm._watchers.push(this) // 这是把当前的	watcher 添加到vue实例上
      		// 上面的this 就是 state.js 对应的watchers[key],就是第三步中创建的
      		// 这时候vm._watchers.vm 就是我们的computed所在的组件的实例,就是整个组件的vm
      
          // options
          if (options) {
            this.deep = !!options.deep
            this.user = !!options.user
            this.lazy = !!options.lazy  // 这里根据传参的值来给`Watcher`对应的静态属性赋值
            // lazy用于标记watcher是否为懒执行,该属性是给 computed data 用的,当 data 中的值更改的时候,不会立即计算 getter 
            // 获取新的数值,而是给该 watcher 标记为dirty,当该 computed data 被引用的时候才会执行从而返回新的 computed 
            // data,从而减少计算量。
            this.sync = !!options.sync
            this.before = options.before
          } else {
            this.deep = this.user = this.lazy = this.sync = false
          }
          this.cb = cb
          this.id = ++uid // uid for batching
          this.active = true
          this.dirty = this.lazy // for lazy watchers  
          this.deps = []
          this.newDeps = []
          this.depIds = new Set()
          this.newDepIds = new Set()
          this.expression = process.env.NODE_ENV !== 'production'
            ? expOrFn.toString()
            : ''
      

      后面还有不是懒加载类型会调用this.get()方法,这里我们不去扩展

    • 执行到这里state.js中的initComputed中的下面这段代码算是执行完毕了

      watchers[key] = new Watcher(
              vm,
              getter || noop,
              noop,
              computedWatcherOptions
            )	
      
  4. 接下来判断,如果 computed中的key没有在vm中, 则通过defineComputed挂载上去。在初始化阶段,因为是第一次执行, 所以vm中是没有该属性的,所以初始化的时候,必然会调用defineComputed对数据进行劫持(就是变为响应式)

    • defineComputed这个函数逻辑很简单,就是判断是不是服务端渲染,不是的话则为true,赋值给shouldCache变量,作用就是判断是否需要被缓存,意思是只要不是服务端渲染都是默认要缓存的

    • 通过传过来的值userDef来判断这个computed[key]是函数式声明还是对象式声明,使用不同的方法 来个state.js最上方声明的变量sharedPropertyDefinition的set和get赋值,然后通过

      // 数据劫持 使之变为响应式  
        Object.defineProperty(target, key, sharedPropertyDefinition)
      

      使之变为响应式

  5. 客户端渲染这边赋值的方法是通过createComputedGetter

    因此我们给sharedPropertyDefinition.get赋值的就是createComputedGetter函数return出来的computedGetter函数

    function createComputedGetter (key) {   // 核心
      return function computedGetter () {
        // 上面的 initComputed 函数中 ’计算属性Watcher‘就存储在 实例的 _computedWatchers 中,这里相当于是取出来
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          if (watcher.dirty) {  // 这个就是缓存的触发,这里就是不能用缓存的了  要重新计算
            watcher.evaluate()  //  计算属性重新求值
          }
          if (Dep.target) { // Dep对象有,就要收集依赖
            watcher.depend()
          }
          return watcher.value  // 返回的计算属性的值
        }
      }
    }
    
最后我们来总结一下,在页面第一次初始化的时候,我们是如何初始化执行的呢:
  1. 当我们执行_init()函数时,会调用vm.$mount,这段代码的作用就是对页面的模板进行编译操作,这不是我们这里关注的点,暂时略过,我们只关注下面这段代码
 if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
  1. vm.$mount的定义是在vue/src/platforms/web/entry-runtime-with-compiler.js

    这里因为有一段代码const mount = Vue.prototype.$mount把之前的Vue.prototype.$mount方法赋值给了mount,然后在这个文件中重写了Vue.prototype.$mount方法,并在这个重写方法的最后调用了mount.call(this, el, hydrating)

  2. 而初始的Vue.prototype.$mount是定义在vue/src/platforms/web/runtime/index.js中的,那么上一步的调用mount.call(this, el, hydrating)到最后就是调用了mountComponent方法

  3. mountComponent中,通过new WatcherWatcher实例化了,这里我们又要去到watcher.js

      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
    
  4. 因为实例化的时候第四个参数没有传入lazy,所以需要调用this.get()方法,这个时候this.lazy = false

this.value = this.lazy ? undefined : this.get();
  1. this.get()中会执行value = this.getter.call(vm, vm) ,这里的 this.getter就是我们定义的computed[key]对应的值,那就走到了下面
function createComputedGetter (key) {   // 核心
  return function computedGetter () {
    // 上面的 initComputed 函数中 ’计算属性Watcher‘就存储在 实例的 _computedWatchers 中,这里相当于是取出来
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {  // 这个就是缓存的触发,这里就是不能用缓存的了  要重新计算
        watcher.evaluate()  //  计算属性重新求值
      }
      if (Dep.target) { // Dep对象有,就要收集依赖
        watcher.depend()
      }
      return watcher.value  // 返回的计算属性的值
    }
  }
}
  1. 最后返回的就是watcher.value的值,就是计算属性的值了,整个computed的执行过程就是通过事件的发布订阅模式来监听对象数据变化实现的