本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Vue 响应式原理 依赖收集
Vue的响应式的核心是利用了Object.defineProperty API(不清楚响应式对象逻辑可以点击这里),这样我们在获取定义的响应式数据的时候,就会触发数据的get方法,改变响应式数据的时候,就会触发数据的set方法。那这两个方法内部又实现了哪些逻辑呢? 本节我们去分析Vue内部,获取响应式数据的值,触发get方法时,Vue的内部做了哪些事情:
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建Dep实例
const dep = new Dep()
// 获取obj描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
// 不可配置则直接返回,不做响应式处理
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 存储预先定义的get方法
const getter = property && property.get
......
// 深层次对象,则递归执行observe
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 计算值
const value = getter ? getter.call(obj) : val
// 如果Dep.target存在,进行依赖收集的操作
if (Dep.target) {
// 收集依赖
dep.depend()
// childOb存在
if (childOb) {
// child收集依赖
childOb.dep.depend()
// 如果是数组,数组中的值也需要收集依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
......
})
}
上面的代码中,有个Dep的类,我们继续来看:
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
* dep是一个可监听对象,可以有多个指令订阅它。
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
// 收集依赖的数组
this.subs = []
}
addSub (sub: Watcher) {
// 将订阅者Watcher推入依赖收集数组
this.subs.push(sub)
}
removeSub (sub: Watcher) {
// 在收集依赖数组移除订阅者Watcher
remove(this.subs, sub)
}
depend () {
// 添加依赖
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
// 派发更新
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// 正在计算的当前目标观察者。这是全局唯一的,因为在任何时候都只能计算一个观察者。
Dep.target = null
// 存放目标观察者的数组
const targetStack = []
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
- Dep.target是Dep上的一个静态属性,起始时候是一个null值,
- pushTarget这个函数,是将当前的目标观察者Watcher推入targetStack栈数组中,并将传入的最新的Watcher,赋值给Dep.target
- popTarget这个函数,取出targetStack栈数组中的最后一个Watcher,赋值给Dep.target
那么这个Dep.target是什么时候赋值的呢? 我们在之前的文章中有过分析,页面渲染的时候会初始化一个updateComponent方法,随后实例化了一个render Watcher,实例化的过程中,执行了Watcher上的get函数(不清楚这段逻辑可以点击这里),这里我们继续以render Watcher来分析:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// this指向当前的render Watcher(后续还会分析自定义Watcher与计算属性Watcher)
pushTarget(this)
let value
// vm指向当前实例
const vm = this.vm
try {
// 获取当前的值,在取值的过程中,就会触发响应式数据的get函数,而这时的Dep.target就是当前的render Watcher
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
// 与深度监听相关
if (this.deep) {
traverse(value)
}
// 取值完毕,依赖收集完成,释放当前render Watcher
popTarget()
// 清除依赖,后面会分析这段逻辑
this.cleanupDeps()
}
return value
}
执行this.get的时候,会将Dep.target赋值为当前的render Watcher,而执行this.getter,就是执行updateComponent函数,会调用render方法,这个过程中会访问到渲染当前页面的数据,有响应式数据的话,就会进入get方法:
get: function reactiveGetter () {
......
// Dep.target为render Watcher进入逻辑
if (Dep.target) {
// 进入逻辑,收集依赖
dep.depend()
// 下面逻辑我们之后的文章分析
if (childOb) {
// child收集依赖
childOb.dep.depend()
// 数组中的值依次收集依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
接下来执行dep.depend()
// Dep类上的方法
depend () {
// 通知依赖更新,Dep.target为render Watcher
if (Dep.target) {
// 执行Watcher上的addDep方法
Dep.target.addDep(this)
}
}
// Watcher类上的方法
/**
* Add a dependency to this directive.
* 添加一个依赖到这个指令
*/
addDep (dep: Dep) {
// 这儿id的作用是防止重复订阅
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
// (上面的逻辑先忽略,之后的文章分析)这儿判断如果这个Watcher没有订阅过这个dep
if (!this.depIds.has(id)) {
// 将Watcher推入这个dep,也就是Watcher订阅这个dep了
dep.addSub(this)
}
}
}
// Dep类上的方法
addSub (sub: Watcher) {
// 将这个Watcher推入Dep的subs中
this.subs.push(sub)
}
通过以上的分析,我们清楚了依赖收集的整个过程了,首先是实例化render Watcher,将当前的Dep.target更改,执行取值方法,获取响应式数据的值,触发响应式数据中的get方法,这样当前的render Watcher就订阅了响应式数据中的dep实例。
收集依赖流程
graph TD
new Watcher(render Watcher) --> Dep.target = render Watcher --> gain value(触发get:reactiveGetter) --> dep.subs.push(Watcher)
那依赖收集完成后,数据改变的时候怎么更新页面呢?下一节我们继续分析派发更新的过程,此外,本节也遗留了几个小问题,之后的文章也会去分析,敬请期待。