Vue的计算属性和初次渲染原理

283 阅读3分钟

本系列为vue 2.5 的原理解析,分为几个方面,这是这系列的第二篇

  1. 数据的响应式原理
  2. Vue的计算属性和初次渲染原理
  3. Vue的异步更新机制
  4. Vue的nextTick的原理
  5. 手动实现响应式原理和虚拟dom,异步更新

1. computed的作用

1.1 computed的创建

vue源码中只有三个地方使用了new Watcher来创建watch,computed的使用就是一个地方

  1. 使用

    computed: {
      reversedMessage: function () {
        return this.message.split('').reverse().join('')
      }
    }
    
  2. 初始化computed

    export function initState (vm: Component) {
      if (opts.computed) initComputed(vm, opts.computed)
    }
    
  3. 利用watcher来创建computed (传递了{lazy: true})

    for (const key in computed) {
      const userDef = computed[key];
    	const getter = typeof userDef === 'function' ? userDef : userDef.get
    	vm._computedWatchers = watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            {lazy: true}
          )
      // 如果计算属性不在vm实例中,定义
      if (!(key in vm)) {
         defineComputed(vm, key, userDef)
      }
    }
    
    // 由于传递了lazy:true 所以不会触发watcher的get()方法去获取值,触发依赖收集
    class Watcher {
      constructor() {
          this.dirty = this.lazy // for lazy watchers
          this.value = this.lazy ? undefined : this.get()
      }
    }
    
  4. defineComputed给vm实例挂载属性

    function defineComputed (target,key,userDef) {
      if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = createComputedGetter(key)
        sharedPropertyDefinition.set = function() {}
      }
    }
    
    // 获取计算属性的值的时候,会触发对应的get,然后触发这个函数
    function createComputedGetter (key) {
      return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          // 计算属性的时候,会为true直接获取到值
          if (watcher.dirty) {
            watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend()
          }
          return watcher.value
        }
      }
    }
    

2. 初始化渲染

具体例子

<section class="todoapp">
  <h1>{{c}}</h1>
</section>
new Vue({
  el: '.todoapp',
  data() {
    return {
      a: 1,
      b: 2,
    };
  },
  mounted() {
    setTimeout(() => {
      this.a = this.a + 1;
    }, 2000);
  },
  computed: {
    c() {
      console.log('computed_C');
      return this.a + this.b;
    }
  }
  1. 初始化的工作

      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++
        // 初始化parent, root children等数据
        initLifecycle(vm)
        // 初始化父类的监听事件
        initEvents(vm)
        // 定义vm.$createElement和vm._c
        initRender(vm)
        callHook(vm, 'beforeCreate')
        // inject的使用
        initInjections(vm) // resolve injections before data/props
        // 数据的响应式原理
        initState(vm)
        // provide的使用
        initProvide(vm) // resolve provide after data/props
        callHook(vm, 'created')
        
        /* 挂载 */
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    
  2. 初始化computed

    • 包装computed的get函数,然后绑定在实例上
    • 由于lazy懒求值,初始化的值为undefined
  3. 执行挂载顺序vm.$mount -> Vue.prototype.$mount -> mountComponent

    Vue.prototype.$mount = function (el) {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el)
    }
    
    function mountComponent (vm, el) {
      vm.$el = el
      callHook(vm, 'beforeMount')
    
      let updateComponent = () => {
          vm._update(vm._render(), hydrating)
      }
    
      // 创建 渲染watcher
      vm._watcher = new Watcher(vm, updateComponent, noop)
    
      // 初次渲染的时候触发mounted钩子函数
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm
    }
    
  4. mountComponent执行是进入Watcher,然后创建渲染watcher

    • 渲染watcher会触发虚拟dom生成(vm._render)和视图的更新(vm._update)
    • 虚拟dom的渲染的时候,模板中值的获取会触发相应的get
    class Watcher {
      constructor(vm,expOrFn,cb,options) {
          // getter函数式渲染视图和触发依赖收集
          this.getter = function updateComponent {
          	vm._update(vm._render(), hydrating)
          }
          this.cb = function() {}
          // 触发watcher的get函数,Dep 和 watch的相互收集
          this.value = this.lazy ? undefined : this.get()
      }
      get() {
        // 是Dep.target = this -> 绑定渲染watcher
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          // 触发虚拟dom的生成和新旧dom的对比,然后渲染到视图
          value = this.getter.call(vm, vm)
        } catch (e) {
          if (this.user) {
            handleError(e, vm, `getter for watcher "${this.expression}"`)
          } else {
            throw e
          }
        } finally {
          if (this.deep) {
            traverse(value)
          }
          // Dep.target = 上一个watcher
          popTarget()
          this.cleanupDeps()
        }
        return value
      }
    }
    
  5. vm._render()中会触发依赖收集

    • 如果存在计算属性并在模板中使用的话,此时会触发computed在初始化的时候,绑定的computedGetter
    • 然后进入计算属性的watcher去获取到对应的值
     function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          // 计算属性的时候,会为true直接获取到值
          if (watcher.dirty) {
            watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend()
          }
          return watcher.value
        }
     }
    // computedGetter 返回了数字3给渲染使用
    
    
    // Watcher 部分
     // 获取值,然后给computedGetter
     evaluate () {
        this.value = this.get()
        this.dirty = false
     }
    
     get () {
        // 把Dep.target 设置为计算属性的watcher
        pushTarget(this)  
        let value
        const vm = this.vm
        /** 会触发计算属性定义时候的函数,然后触发a,b的依赖收集
     		function () {
          console.log('computed_C');
          return this.a + this.b;
        } 
        **/
        value = this.getter.call(vm, vm)
        // 把Dep.target重新复制为渲染watcher
        popTarget()
        return value
      }
    
  6. 执行完渲染后,返回到渲染watcher的函数执行尾部

    • 此时页面已经渲染,做一些清除工作

       finally {
            if (this.deep) {
              traverse(value)
            }
            // Dep.target = 上一个watcher
            popTarget()
            this.cleanupDeps()
       }
      
  7. 渲染watcher执行完毕后,返回mountComponent

    • 触发mounted钩子函数
    • 返回实例本身
    function mountComponent (vm, el) {
      vm.$el = el
      callHook(vm, 'beforeMount')
    
      let updateComponent = () => {
          vm._update(vm._render(), hydrating)
      }
    
      // 创建 渲染watcher 执行完毕  
      vm._watcher = new Watcher(vm, updateComponent, noop)
    
      // 初次渲染的时候触发mounted钩子函数
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm
    }