源码层面解读Vue响应式原理
在 Vue(以下均指的是 2.x 版本) 中,数据模型仅仅是普通的 JavaScript 对象,当你修改它们时,视图会进行更新,这就是 Vue 的响应式系统,其设计模式就是观察者模式。
思路梳理
以下为阅读源码时,对响应式实现原理的理解,为了思路更清晰,仅考虑最简单的情况,并且可能会省略或修改部分代码。
首先,顺着 vue 的生命周期开始:
beforeCreate 与 created
首先 在 _init 方法中执行 beforeCreate 与 created :
Vue.prototype._init = function (options?: Object) {
// ...
callHook(vm, 'beforeCreate')
// ...
initState(vm)
// ...
callHook(vm, 'created')
// ...
}
记住这里执行了一个方法: initState
#####initState
export function initState (vm: Component) {
// 略:...初始化 props,methods 等
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 略:...初始化 computed,watch 等
}
这里的 initData 代码如下:
function initData (vm: Component) {
// 略:...将 data 的每个属性都挂在 vm 上,这样可以通过 this.xx 直接访问到
// observe data
observe(data, true /* asRootData */)
}
记住 observe 这个方法。
observe
observe 用于创建一个 observer 实例:
export function observe (value: any, asRootData: ?boolean): Observer | void {
let ob: Observer | void
// 判断是否已经 observe 过,是的话直接返回
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// observer 最终会挂在 value 的 __ob__ 属性上,因此可以这么判断是否 observe 过,详情看 Observer 的代码
ob = value.__ob__
} else if (...) {
// 否则满足一系列条件的情况下就会重新实例化一个 observer
ob = new Observer(value)
}
return ob
}
实例化 observer 的过程中实际上会将传入对象的每个属性通过 defineReactive 方法转成响应式:
export class Observer {
constructor (value: any) {
this.value = value
// 初始化数据模型的 dep 实例
this.dep = new Dep()
// observer 最终会挂在 value 的 __ob__ 属性上
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 略:...重写 value 的一些数组方法
// 略:...遍历数组元素并 observe
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 遍历传入的每个属性,并用 defineReactive 转成响应式
defineReactive(obj, keys[i])
}
}
}
defineReactive
上面代码中最核心的一个函数是 defineReactive,做了以下几件事情:
- 初始化目标属性的 dep 实例,这实际上是一个发布者
- 对该属性的子对象递归调用 observe
- 通过
Object.defineProperty劫持该属性的 getter 与 setter:
export function defineReactive (
obj: Object,
key: string,
val: any,
) {
// 初始化目标属性的 dep 实例
const dep = new Dep()
// 略:... 一些容错判断
// 略:... 一些初始化
// 递归 observe
let childOb = !shallow && observe(val)
// 劫持 get 与 set
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ...一旦获取该属性就会进行依赖收集
},
set: function reactiveSetter (newVal) {
// ...一旦设置了新值就会派发更新
}
})
}
这里就用到了观察者模式:当这个属性更新时,会通过发布者通知到观察者,这些观察者收到通知后更新视图。这里发布者实际上是Dep 实例,观察者实际上是 Watcher 实例,它的 update 方法可以更新视图。
所以,依赖收集实际上就是将Wathcer 实例添加到 Dep 实例的观察者列表。
reactiveGetter 和 reactiveSetter 这两个方法的具体实现我们先放一边,这里只需要知道大概的作用即可,后面会详细介绍。我们现在来看看什么时候会触发 get 呢?答案是在挂载组件的时候。
beforeMount 与 mounted
然后是在 mountComponent 中执行 beforeMount 以及 mounted:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
callHook(vm, 'beforeMount')
// 略:...这里有个非生产环境的判断,这里不考虑简化成如下:
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 这里实例化 Watcher:可以看作是更新视图用的观察者,这里省略一些参数
new Watcher(vm, updateComponent)
// ...仅考虑 new Vue 的情况,代码简化如下
callHook(vm, 'mounted')
return vm
}
挂载组件的时候就会实例化一个 Watcher
Watcher
当我们实例化 Watcher 的时候:
let uid = 0
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
) {
// 略:...一些初始化
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
// 略:...一些初始化
// 略:...对 this.getter 的一些初始化
if (this.computed) {
// computed 的情况,本文暂不考虑
} else {
// 初始化时就会执行 get 方法
this.value = this.get()
}
}
get () {
// pushTarget 用于将 watcher 实例挂到 Dep 的 target 这个静态属性上
pushTarget(this)
let value
const vm = this.vm
try {
// 此时的 this.getter 实际上就是 updateComponent
value = this.getter.call(vm, vm)
} catch (e) {
// 略:...错误处理
} finally {
// 略:...递归访问 value,触发它所有子项的 getter
this.cleanupDeps()
}
return value
}
// ...
}
pushTarget 将 watcher 实例挂到 Dep 的 target 这个静态属性, 用于 Dep 内部可以访问到:
function pushTarget (_target: ?Watcher) {
// ...
Dep.target = _target
}
接着调用 this.getter, 这里的 this.getter 实际上就是 updateComponent:
vm._update(vm._render(), hydrating)
vm._render() 会生成 VNode,这个过程会访问 vm 上的数据,至此就会触发数据对象的 getter,进行依赖收集。
思路小结
至此我们先梳理一下这一整个过程,以免遗忘:
beforeCreate =>
initState -> observe -> new Observer
-> defineReactive:实例化 Dep,劫持对象属性 getter 与 setter
=> created
=> beforeMount
实例化 Watcher -> 将 watcher 实例绑定到 Dep.target,并触发对象属性的 getter,收集依赖
=> mounted
=> 当数据模型变更时,触发 setter,更新视图
在 defineReactive 中有个关键的步骤是实例化 Dep。
Dep
上面说过 Dep 实例是一个发布者,其代码其实很简单:
export default class Dep {
constructor () {
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
// 这里将 Dep.target 即 watcher 添加到 subs 里
Dep.target.addDep(this)
}
}
notify () {
// 发布通知
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
我们最终调用的是 depend方法来添加观察者,即Dep.target.addDep,我们知道此时 Dep.target 实际上就是 watcher 实例。
// ...
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// 将发布者 id 添加到 this.newDepIds
this.newDepIds.add(id)
// 将发布者 添加到 this.newDeps
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 如果旧的 发布者id 列表中没有这个发布者 id,那么就添加到发布者的观察者列表中
dep.addSub(this)
}
}
}
// ...
而在 watcher 中,最终还是调用了传入的 dep 实例的 addSub 来添加到 dep 的观察者列表中。之所以这么设计,其原因是为了能在 watcher 中维护一个自己订阅的发布者列表。
那么 watcher 中的发布者列表有什么用呢?还记得 Watcher 实例化的最后会调用 this.cleanupDeps() 吗?
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
这个函数实际上的作用是:
如果无需再观察,则从发布者的观察者列表中移除
将本次的发布者 id 列表记录在 this.depsIds 中,清空 this.newDepIds
将本次的发布者列表记录在 this.deps 中,清空 this.newDeps
那么具体是如何将对象属性转为响应式的呢,我们看看其具体实现:
reactiveGetter
function reactiveGetter () {
// ...
// 此时的 Dep.target 就是 watcher,关于 Dep 后面会详细解释
if (Dep.target) {
// 这里会将 watcher 实例添加进 dep 的 subs 中维护,即收集依赖
dep.depend()
if (childOb) {
// 子对象的依赖收集
childOb.dep.depend()
// 略:...对数组的一些处理
}
}
// 返回属性值
return value
}
reactiveSetter
function reactiveSetter (newVal) {
// value:属性值
const value = ...
// 略:...一些判断是否触发 setter 的条件
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 设置新值后需要重新 observe
childOb = !shallow && observe(newVal)
// 发布通知
dep.notify()
}
总结
本文简化了一些细节,但是 Vue 的响应式原理大致如此。当理解了原理之后,如果后面遗忘了的话,以下为重要的记忆点:
- observe:实例化 Observe,参数为数据模型,实例化过程会调用遍历对象属性调用 defineReactive
- defineReactive:实例化 Dep,劫持对象属性的 getter 与 setter,getter 时进行依赖收集
- 在挂载组件的时候,会实例化 Watcher,此时会触发 getter
- 当更新对象属性时,会调用 Dep 实例的 notify,触发 watcher 的 update