vue源码解析-响应式原理

在前两篇文章中,我们介绍了vue初始化的工作 和 挂载的总体流程。
下面,我们基于 前面的基础上,重点探讨一下vue中的响应式原理实现。

先来看个简单的:


  
    
  

  
    
{{ a }}

运行结果:

image.png

点击之后:

image.png

嗯,这很简单:

image.png

那么这一切背后,背后发生了什么?下面,我们来一探究竟。

一. initState (new Vue时发生)

在vue实例化的时候,会调用initState方法。其核心代码如下:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // ...
   if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
}

由此,我们看到,我们在实例化vue时,传入了data,里面只有1条信息, a = "这是根节点"。那么将执行initData方法。

initData 方法核心代码:

function initData (vm: Component) {
  data = vm._data = typeof data === 'function' ?  getData(data, vm) : data || {}
  // 此处省略 ... 校验 data, props, methods的 key不能重复的校验
  // 此处省略 ... 设置data对象的访问代理,访问this.a,实际上访问 的是this._data.a,而data上值赋值给了this._data
  observe(data, true)
}

值得注意的是:这里的data,实际上是vm._data

observe 方法核心代码:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 省略... 判断,如果是虚拟dom对象,是不能够被Observer的
  
  let ob: Observer | void
   if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    // ...
  ) {
    ob = new Observer(value)
  }
  return ob;
}

observe方法,先判断需要观察的对象,是不是VNode类型,如果是,直接return。
接着,判断我们传入的data对象,是否有__ob__属性,这里 是为了防止重复 观察。
下面,进入Observer类的实例化。

Observer类 核心代码如下:

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

  constructor(value: any) {
      this.value = value
      this.dep = new Dep()
      this.vmCount = 0
      def(value, '__ob__', this)

      // 省略... 数组相关的响应式实现, 这里数组和对象是不一样的,后面的文章中,我们会详细聊到

     // 如果不是数组,将执行walk方法
      this.walk(value)
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}


// defineReactive 核心代码如下:
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
   const dep = new Dep()

   Object.defineProperty(obj, key, {
      // ...
      get: function reactiveGetter() {
         if (Dep.target) {
            dep.depend()
         }
        // ...
        return value;
      },
      set: function reactiveSetter (newVal) {
         // ...
         dep.notify()
      }
   })
}

// Dep类 核心代码
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  // ...下面的方法可以暂时不看,一会触发数据的 get set 我们再补上
}

我们可以 看到,Observer类实例化时:

    1. 调用def方法,给vm._data上设置__ob__属性,而__ob__属性值 是Observer类。这个时候vm._data 数据结构变成如下结构:
vm._data = {
  a: "这是根节点",
  __ob__: {
    value: {
       a: "这是跟节点" 
    },
    vmCount: 1,
    // dep  是 Dep类 
    dep: {
      id: '这里是一个dep id,每次Dep类被实例化一次,就累加1',
      subs: Array = []
    }
  }
}
    1. 对vm._data对象进行 遍历 调用 defineReactive方法
    1. defineReactive方法,实际上 对vm._data上每个key都定义了 get set,做了个数据劫持。 注意,这里只是定义,还没有触发调用。(记住,记住,记住:这里对对象上每个key做了数据劫持,下面会用到)

ok,到这里,我们已经了解初始化中 和 响应式相关的 工作。

下面,我们将进行响应式实现的 下一个阶段 - mount。准备开车喽~

image.png

二. mount

在上一篇文章中,我们介绍了mount执行总流程。实际上是调用了 mountComponent,这里不再赘述,不清楚的同学,可以看我的上一篇文章:vue源码解析-$mount。

mountComponent 核心代码如下:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 此处省略n行...
  callHook(vm, 'beforeMount')

  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true)

  // ...
  callHook(vm, 'mounted')
  return vm
}

这里,我们可以看到,先调用beforeMount钩子,然后定义updateComponent方法。
随之,实例化Watch类。

Watcher 核心代码如下:

export default class Watcher {
  vm: Component;
  // ...
  deps: Array;
  depIds: SimpleSet;
  getter: Function;
  // ...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean   
  ) {
     this.vm = vm
     if (isRenderWatcher) {
      vm._watcher = this
    }
    // ...
    this.value = this.lazy ? undefined : this.get()
  }

  get() {
     pushTarget(this)
     // 此处省略  try  catch ..., 我们直接看核心调用
     value = this.getter.call(vm, vm) 

     popTarget()
     return value
  }

  addDep(dep: Dep) {
    // ... 
    dep.addSub(this)
  }
  
  update() {
     // ... 省略lazy,和 sync
    queueWatcher(this)
  }
}

实例化Watcher时,Watcher中保存了 vm变量,这个vm变量就是当前 组件的实例。
这里需要指出的是: 我们这里的例子,只有一个div,里面没有嵌套组件。如果有子组件,当子组件被实例化的时候,这里的vm就是子组件,而不是Vue。

这里先记住: vue实际上是个Vue构造函数,而template中写的 子组件,他实际上不是Vue构造函数,而是VueComponent构造函数,这个后面,我会专门开章节 分享 vue源码-组件化。

好了,我们继续往下走,vm._watch上 挂载 Watcher的实例,这里lazy不存在,会调用get方法。下面我们先看下 pushTarget,popTarget方法

Dep类核心代码:

class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

 notify() {
   // ...
   for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
 }
}

Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
到这里,有的同学 已经明白了:这实际上就是个发布订阅模式。

可以看到,Watcher类get方法中,调用pushTarget方法,将设置 Dep类的target属性 为 当前的Watcher对象,targetStack数组中push当前的 Watcher类。

记住:多层组件嵌套的情况下,Dep.target 值不会混乱,他永远指向目前被实例化的Watcher。

后面的章节,我会详细 分析vue实现组件化过程,届时会再次提到dep, watcher, observer。

好了,到这里,有的同学会问:
通过上面的分析,我已经知道vm._data的数据结构以及实例化Watcher了, 但是vm._data.__ob__.dep下的subs 数据还是空的,Watcher是什么时候关联的呢?

image.png

莫急,重点来了:

image.png

watcher实例化之后,调用this.getter.call方法,实际上是调用mountComponent函数中的updateComponent方法。此方法的核心:vm._update(vm._render(), hydrating)。

这里,我们可以看到 分了2步:

    1. 先执行render函数,获取虚拟DOM
    1. 执行update, 实际上是patch过程

在上一篇文章中,我们知道,虚拟DOM,实际上是一个对象,这个对象上定义了一些属性,用来描述dom节点。而生成虚拟DOM过程中,会访问 data对象,也就是访问 vm._data对象,这个时候,就会触发 前面 所说的 get 方法 (本文前部分 defineReactive函数中 get)。

在get方法中, 会执行如下代码:

if (Dep.target) {
   dep.depend()
}

dep类实际上 是个依赖收集器, 他是 数据 Observe 和 Watcher的中间桥梁。dep.depend()方法如下:

depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

而这个时候Dep.target 指向了当前组件 实例化的Watcher, watcher类中 addDep方法如下:

// ...
dep.addSub(this)

dep中的addSub 方法如下:

addSub (sub: Watcher) {
  this.subs.push(sub)
}

这个时候,dep类的 subs数组中就 添加了 对应watcher, 而watcher中的 vm属性,就是指向 当前使用了这个数据的 组件 实例。

到此,我们就可以看到 vm._data对象的 数据结构变成这样:

vm._data = {
  a: "这是根节点",
  __ob__: {
    value: {
       a: "这是跟节点" 
    },
    vmCount: 1,
    // dep  是 Dep类 
    dep: {
      id: '这里是一个dep id,每次Dep类被实例化一次,就累加1',
      subs: [
        {
          //  Watcher类
          // ... 省略watcher类的各种属性
         update() {...},
          vm: "用到这个数据的vue组件实例对象"
        }
      ]
    }
  }
}

当data中数据变化时, 也就是vm._data中数据变化时,会调用对应的set (本文前部分 defineReactive函数中 set),其核心代码如下:

dep.notify()

// dep 中的 notify 核心方法
notify() {
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

这里的subs,就是 dep依赖收集器,数组中的watcher类,而watcher类中保留了 依赖这些数据的 组件的实例对象vm。 那么就能调用对应的组件实例的 patch方法,做diff 更新DOM.

需要注意的是:

    1. 根据vm._data的数据结构,vm._data下面的值变化时,我们就能通过 vm._data.__ob__属性,获取该属性上面的 dep.subs数据,进而循环调用Watcher的update方法
    1. watcher的update方法,是个更新队列,我们将在后面的章节重点分析详细过程, 这里我们可以简单理解,每个watcher中都保存了 使用该数据 的 组件实例vm,那么可以就对 使用到这些数据的组件进行patch diff更新, 而不是 刷新整个页面DOM。

ok ,到这里,响应式就实现啦~

三. 总结

vue实现响应式原理步骤:

    1. vue实例化时,调用initState方法
    1. 设置数据访问代理,this.xx 实际访问 this._data.xx
    1. 判断数据不能重复observer
    1. vm._data添加__ob__属性,该属性值是Observer类
    1. Observer类,包含value, dep 核心属性
    1. Observer类,dep属性,是Dep类的实例对象
    1. 对vm._data下所有数据循环 defineReactive
    1. defineReactive 方法,对vm._data所有数据进行 Object.defineProperty 劫持get set
    1. 实例化vue后,进行挂载时,会实例化一个Watcher
    1. 实例化Watcher时, 会设置当前Dep类的target属性,指向当前被 实例化的 watcher
    1. 执行updateComponent方法
    1. 执行当前的vm实例的render方法,生成虚拟DOM
    1. 获取vm._data数据,触发 defineReactive 方法中定义的 get
    1. dep类收集vm._data数据,vm._data.__ob__.dep.subs push对应的watcher
    1. 收集的watcher,有个vm属性,指向了使用vm._data.xx的组件实例
    1. vm._data上的数据发生变化时,对所有的watcher信息循环 update
    1. 进入update队列,执行patch过程,更新dom

下一篇,我将分享,vue 如何实现组件化。 码字不易,多多关注,点赞 ~

你可能感兴趣的:(vue源码解析-响应式原理)