美文网首页🐋成员文章 菲麦前端让前端飞
刺破vue的心脏之——响应式源码分析

刺破vue的心脏之——响应式源码分析

作者: 小虫巨蟹 | 来源:发表于2017-07-23 23:41 被阅读179次

    之前发刺破 vue 的心脏之——详解 render function code的时候,承诺过会对 Vue 的核心过程的各个部分通过源码解析的方式进行抽丝剥茧的探索,今天就来进入第二部分响应式原理部分的源码解析,承诺兑现得有些晚,求轻拍

    一、先分析工作原理

    还是之前的套路,在读源码之前,先分析原理


    上图来自 Vue 官网深入响应式原理,建议先看看,这里主要说说我的理解:在初始化的时候,首先通过 Object.defineProperty 改写 getter/setter 为 Data 注入观察者能力,在数据被调用的时候,getter 函数触发,调用方(会为调用方创建一个 Watcher)将会被加入到数据的订阅者序列,当数据被改写的时候,setter 函数触发,变更将会通知到订阅者(Watcher)序列中,并由 Watcher 触发 re-render,后续的事情就是通过 render function code 生成虚拟 dom,进行 diff 比对,将不同反应到真实的 dom 中

    二、源码分析

    记住一个实例

    读源码是一件枯燥的事情,带着问题去找答案,要更容易读得进去

    <template>
    ...
    </template>
    <script>
      export default {
        data () {
          return {
            name: 'hello'
          }
        },
        computed: {
          cname: function () {
            return this.name + 'world'
          }
        }
      }
    </script>
    <style>
    ...
    </style>
    

    为了减少干扰,例子已经剥离得只剩下两个关键的要素,数据属性 name,以及调用了该属性的计算属性 cname,这其中 cname 跟 name 就是订阅者跟被订阅者的关系。我们现在需要带着这样的疑问去阅读源码,cname 是如何成为 name 的订阅者的,并且当 name 发生了变更的时候,如何通知到 cname 更新自己的数据

    初始化数据,注入观察者能力

    响应式处理的源码在 src/core/observer 目录下,见名之意,这使用了观察者模式,先不用着急进入这个目录,在 Vue 实例初始化的时候,会执行到 src/core/instance/state.js 中相关的状态初始化逻辑,先到这个文件来看看:

    export function initState (vm: Component) {
      ...
      if (opts.data) {
        // 初始化数据
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      // 初始化计算属性
      if (opts.computed) initComputed(vm, opts.computed)
      ...
    }
    

    我们所关注的初始化数据和初始化计算属性在这里都会被执行到,先来分析下 initData, 沿着方法跟下去,发现最终 initData 要做的事情是:

      // observe data
      observe(data, true /* asRootData */)
    

    调用 observe 方法为 data 注入观察者能力,这个时候我们可以正式进入 observer/index.js 文件了,在这个文件我们可以找到 observe 方法的定义,跟着方法读下去,找到下一步的关键信号:

    ob = new Observer(value)
    

    这一步通过传入的 data,创建一个 Observer 实例,再跟到 Observer 的构造函数中会发现,构造函数会为 data 的各个元素(当 data 为数组的时候)或者各个属性(当 data 为对象的时候)递归的创建 Observer 对象,最终起作用的方法是 defineReactive

    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: Function
    ) {
      const dep = new Dep()
    
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
    
      // cater for pre-defined getter/setters
      const getter = property && property.get
      const setter = property && property.set
    
      let childOb = observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          // 当前的 Watcher
          if (Dep.target) {
            // 将当前的 watcher 加入到该数据的订阅者序列
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
            }
            if (Array.isArray(value)) {
              dependArray(value)
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = observe(newVal)
          // 当数据发生变更的时候,通知订阅方进行数据变更
          dep.notify()
        }
      })
    }
    

    代码贴得有点多,但着实不是为了凑字数,因为在这里部分省略可能会带来一些疑惑,就没有进行缩减,见谅。这里主要是通过 Object.defineProperty 方法,重写数据的 set 和 get 方法,当数据被调用时,set 方法会将当前的 Watcher Dep.target 也就是当前的调用方加入到该数据的订阅者序列中,当数据变更,set 方法发通知到所有订阅者,让大家重新计算。这其中定义在 observer/dep.js 文件中的 Dep 定义了数据订阅者的订阅、取消订阅等行为,在这里就不贴代码了。

    回忆一下我们实例中的 name 和计算属性 cname,当 cname 的方法执行的时候,name 被调用,就会触发它的 get 方法,这个时候 cname 所对应的 watcher (computed 初始化的时候会为每个计算属性创建一个 watcher)。当 name 发生了变更,set 方法被触发,cname 所对应的 watcher 作为订阅者就会被通知到,从而重新计算 cname 的值

    初始化计算属性,创建 Watcher

    回到 src/core/instance/state.js 文件的计算属性初始化逻辑 initComputed,这个方法不负众望的为计算属性创建了 Watcher 对象

      // create internal watcher for the computed property.
      watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
    

    于是乎我们的视线需要转移到 observer/watcher.js 中,Watcher 的构造函数中最为关键的是,this.get 方法的调用

    this.value = this.lazy
          ? undefined
          : this.get()
    

    在 this.get 方法中有两步尤为关键(对于计算属性来说,会进行延迟计算,这就是 this.lazy 标志的意义所在):

     get () {
        // 将当前的调用者 watcher 置为 Dep.target
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          // 调用方法,计算依赖方的值
          value = this.getter.call(vm, vm)
        } catch (e) {
          ...
        } finally {
          ...
        }
        return value
      }
    
    1. pushTarget(this) 将 watcher 置为 Dep.target,当所依赖的数据的 get 方法被调用的时候,就可以根据 Dep.target 把当前的 watcher 加入到订阅者序列中。这么做的目的是,当 watcher 依赖于多个数据的时候,可以共享 Dep.target
    2. 执行 this.getter.call(vm, vm) 方法计算值,例如计算属性 cname 的 getter 就是它的定义函数function(){this.name + 'world'}。此时依赖方的 get 方法被触发,整个流程就能串起来,说得通了

    对于计算属性,还有一个细节,需要将视线再转移到 initComputed 中:

    export function defineComputed (target: any, key: string, userDef: Object | Function) {
      ...
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    

    它所调用的 defineComputed 方法会为计算属性在当前的组件实例中创建一个同名的属性,这也就是为何计算属性的定义上是方法,但是在实际的使用当中却是属性的原因。只有在它所依赖的数据更新的时候,数据通过 set 方法通知到它,它才会重新计算并把值赋给这个新建的代理属性。计算属性高效就高效在这里

    三、总结

    写源码分析难以覆盖到方方面面,毕竟不能一直贴代码,如何在贴最少量代码的情况下把问题说清楚,这仍然还是努力的方向。在达到这个目标之前,只能通过提问的方式了,有问题欢迎评论,尽力解答

    此文还在公众号菲麦前端中发布:


    相关文章

      网友评论

        本文标题:刺破vue的心脏之——响应式源码分析

        本文链接:https://www.haomeiwen.com/subject/pqdvkxtx.html