Vue响应式之依赖收集-vue2

2/16/2022, 2:20:42 AM

前言

Vue3对比Vue2,我认为最明显的区别在于Vue3有一套独立的响应式单元,不依赖组件。这允许我们可以创建局部的状态,多个组件共享状态数据,数据变更将会驱动所有依赖组件更新视图。而Vue2中多个组件若想共享同一个状态,只能通过创建共同父组件使用props传递或者使用Vuex。依赖收集正是数据变动到视图更新的桥梁,Vue3和Vue2的实现完全不同。

正文

Vue的响应式原理很简单,Vue2通过Object.defineProperty监听数据变化,Vue3通过Proxyj监听数据变化,数据变更会使dom重新渲染,那么Vue是怎么知道哪些模板依赖变化数据,并重新渲染呢?原因就在依赖收集,即为每个响应式数据存储一个回调队列,数据更新(触发setter时),遍历执行更新回调,触发组件重新渲染。

Vue2(2.6.11)

之前有做过Vue2的响应式原理源码分析,组件在实例化的时候,会调用 initState 对数据进行响应式处理

export function initState (vm) {
  // ...
  if (opts.data) {
    initData(vm)
  }
 // ...
}

export function initData() {
  // ...
  observe(data, true /* asRootData */)
}

function observe() {
  // ...
  new Observer(value)
}

export class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.walk(value)
  }
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

最终会走到 walk 方法,遍历data 的属性,并调用 defineReactive 处理每个属性,defineReactive 方法就是经常说的通过Object.defineProperty 设置响应式的逻辑

export function defineReactive (obj, key, val) {
  const dep = new Dep()  // 该数据的所有依赖的容器
  // ...
  val = obj[key]
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        // ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      val = newVal
      dep.notify() // 通知依赖更新
    }
  })
}

dep 就是响应式数据收集的依赖的容器,组件实例化 initData 阶段保存在闭包中。当执行到处理模板阶段时,读取data对应属性会触发get,此时通过dep.dpend()方法将对应的回调保存到 dep 中。属性更新时触发set,调用 dep.notify 调用所有依赖回调。

下面才进入依赖收集的核心,也就是 dep 的逻辑

  • new Dep() 做了什么
  • dep.depend() 怎么知道依赖回调是什么,是如何保存对应的回调的
  • dep.notify() 如何通知模板更新

Dep是一个构造函数,用来创建 dep 实例,并提供一些方法

export default class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    const subs = this.subs.slice()
    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]
}

这里的 Dep.target 指向的是一个 watcher,具体是什么 watcher ,会在不同阶段不断地更改指向,每次通过 get 触发 dep.depend() 时,Dep.target 都会对应不同的watcher

当数据更新触发 set 时,调用 dep.notify() 遍历 subs 并调用每个watcher 的 update 方法

搞清楚以下几个问题,Vue2 的依赖收集的基本流程就理清楚了

  • Dep.target 的指向是什么,何时更新
  • watcher 是什么,watcher.update 方法做了什么

Dep.target 是通过导出的 pushTarget 方法更新的,全局搜一下 pushTarget 方法就可以

pushTarget 基本是通过Watcher类的 get 方法触发的

export default class Watcher {
  constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    this.getter = expOrFn
    this.get()
    // ...
  }
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    popTarget()
    return value
  }
  // ...
  update () {
     // ...
    this.get()
  }
  addDep (dep) {
    dep.addSub(this)
  }
}

watcher 实例保存着对应的组件实例 vm,新建watcher 的时候会立即 执行 get 方法,修改 Dep.target 指向到当前 watcher 实例,并执行watcher 的 getter,也就是 new Watcher()  时传入的回调函数 exOrFn。

dep.depend调用的 addDep 最终会将当前 watcher 实例 push 到 dep 实例的 subs 中。

dep.notify 遍历subs 执行保存的 watcher 实例的 update 方法,也就是 watcher 实例的 get 方法,最终执行new Watcher() 时传入的回调函数。

那么 watcher 什么时候被创建的呢?首先在组件实例化阶段执行到 $mount 方法时,会创建 watcher 

export function mountComponent (vm, el) {
  // ...
  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent)
  // ...
}

$mount 阶段,传入的回调最终会执行组件的 _render 方法,并将返回值作为参数执行 _update 方法。到这一步就很明显了,_render 方法会执行 render 方法创建 vnode,_update 最终会经历 patch 阶段生成dom并挂载到页面。

以上就是 Vue2 整个依赖收集和更新的过程,整个响应式系统依赖具体组件

  • initData 阶段,Object.defineProperty 为 data 每个属性设置 getter,setter,并为每个属性闭包保存一个 dep。
  • mount 阶段,生成 watcher,并将 watcher.update 方法设置为组件的创建vnode并渲染页面的逻辑,然后立即执行一次该逻辑,并触发一次模板引用到的 data 的属性的 getter,将该逻辑收集到对应属性的 dep.subs 中
  • 组件活跃阶段,data 属性更新触发 setter,通过 dep.notify 遍历执行 mount 阶段收集到的重新创建vnode 并渲染页面的逻辑,完成响应式更新。