前言
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 并渲染页面的逻辑,完成响应式更新。