了解一下Vue - [Vue是怎么实现响应式的(二)]

写在前面:本文为个人在日常工作和学习中的一些总结,便于后来查漏补缺,非权威性资料,请带着自己的思考^-^。
前文链接:了解一下Vue - [Vue是怎么实现响应式的(二)]
前言:上一篇文章简单介绍了基于data的Vue响应式的实现,这篇将进行一点扩展,data变更会自动触发computed进行重新计算,从而反映到视图层面,那这个过程又是怎么做到的呢?

从Vue实例生成过程的initComputed说起

顾名思义,在initComputed中主要进行的工作是对computed进行初始化,上代码:

function initComputed (vm, computed) {
  const watchers = vm._computedWatchers = Object.create(null) // 用于存放computed 相关的watcher
  
  for (const key in computed) { // 遍历computed,为每一个computed属性创建watcher实例,这个watcher实例的作用后面会体现
    // 这里可以看出,平时我们的computed一般都是函数形式的,但很多时候我们也可以写成{get() {}, set() {}},这种对象形式
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop, // 此函数参数会在watcher实例的get方法中进行执行
        noop,
        computedWatcherOptions // {dirty: true},可以翻一下之前的class Watcher代码或者找源码看一下,这个options其中的一个作用就在于控制实例化watcher的时候是否先执行一次get() 方法,这个get方法内部会对参数传进来的getter进行执行
      )
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

// function defineComputed

function defineComputed ( // 整体来说此函数的作用就是通过defineProperty定义getter/setter将computed中的属性代理到vm上
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering() // 非服务端渲染,则为true
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key) // computed的计算结果会被缓存,不需要每次访问computed属性时都重新进行计算
      : userDef // computed 不使用缓存的情况
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

实例化Vue期间,对computed的处理,做了:

  1. 将computed[key]的值作为getter参数,实例化一个Watcher对象(至于Watcher对象的作用,后面会提到);
  2. 将computed[key]的值(其实是经过包装的)作为getter,通过defineProperty将computed[key]代理到vm[key];

说到$mount的执行过程

$mount包含了模板编译、render函数生成... 不再赘述
和响应式相关的是在$mount中实例化了一个render Watcher,前文已经有过标注,在实例化Watcher中会执行get()函数,从而执行render函数,在render函数的执行过程中势必会读取页面渲染使用到的computed属性,触发其getter:

// computed属性的getter
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key] // watcher就是initComputed时传入computed[key]作为getter参数实例化的watcher
    if (watcher) {
      if (watcher.dirty) { // 如果是首次读取computed[key],一般watcher.dirty为true
        watcher.evaluate()
        /**
        evaluate () {
            this.value = this.get()
            this.dirty = false // 执行完get之后就会将dirty置为false,这样下次读取computed[key]时就不会再重新计算
        }
        执行watcher.get(),在这个函数内部会做几个事情:
        执行pushTarget函数,将当前的Dep.target置为当前watcher实例
        执行computed[key],计算得到结果赋值给watcher.value
        如果computed[key]函数内容是通过几个data计算得到值,则将会触发data的getter,
        这将会把这个几个data的dep对象作为依赖添加至watcher的deps列表,同时将当前watcher添加至这些dep的subs列表中,
        通俗一点说,这个过程对于当前watcher来说就是依赖收集过程,将其依赖的项添加至deps中
        对于某一个data的dep来说,就是将当前watcher添加至其观察者列表subs中
        执行完以上过程,就会将Dep.target重置为之前的值,
        */
      }
      if (Dep.target) {
        watcher.depend() // 这一步也很重要,此处是将当前watcher的依赖也加入到Dep.target的依赖列表中
        /**
        为什么要有这一步呢?
        因为当前的Dep.target在执行完watcher.evaluate之后就被重置回了上一个Dep.target,一般来说当前的Dep.target
        就是render Watcher
        设想有这种情况:某一个data的属性x并没有直接用于render,那么在render执行过程的依赖收集x就不会被添加
        到render Watcher的deps中,x的dep.subs中也没有render Watcher 也就是说之后如果对x进行重新赋值,则不会
        通知render Watcher,此时还没有问题,但时如果x被computed用到,由于computed没有setter,则x被重新赋值
        通知到computed Watcher去重新计算,但是computed并没有直接通知render Watcher的方法,这个时候render就不会
        重新执行,页面也就不会进行更新。。。
        */
      }
      return watcher.value
    }
  }
}

总之,详见代码注释。。。

当computed依赖的data更新时

这里已经知道对data重新赋值,会触发其对应setter

// setter
set: function reactiveSetter (newVal) {
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      
      val = newVal
      childOb = !shallow && observe(newVal)
      dep.notify() // 这里会通知到所有watcher,让它们进行update
    }
    
// dep.notify
notify () { // 在render阶段进行依赖收集时会将watcher加入subs列表,computed在进行计算的时候会收集依赖的data,
// 与此同时会将computed的watcher对象添加至data的dep.subs列表
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

render watcher和computed watcher执行update是不同的,computed watcher的update方法只会将watcher.dirty置为true,这代表该computed依赖的data发生了更新,需要重新计算;这样在render函数再次执行的时候会读取computed,触发computed的getter,在getter中会重新计算得出computed的新值,并且将dirty置为false,代表只需计算一次,在同一个render loop中多次引用该computed将不会重新计算。

THE END...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章