刺破vue的心脏之——响应式源码分析
之前发刺破 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
}
- pushTarget(this) 将 watcher 置为 Dep.target,当所依赖的数据的 get 方法被调用的时候,就可以根据 Dep.target 把当前的 watcher 加入到订阅者序列中。这么做的目的是,当 watcher 依赖于多个数据的时候,可以共享 Dep.target
- 执行 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 方法通知到它,它才会重新计算并把值赋给这个新建的代理属性。计算属性高效就高效在这里
三、总结
写源码分析难以覆盖到方方面面,毕竟不能一直贴代码,如何在贴最少量代码的情况下把问题说清楚,这仍然还是努力的方向。在达到这个目标之前,只能通过提问的方式了,有问题欢迎评论,尽力解答
此文还在公众号菲麦前端中发布: