defineReactive给数据添加了 getter 和 setter。
在defineReactive内第一步是实例了Dep
Dep是整个 getter 依赖收集的核⼼
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
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 () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
target 是⼀个全局唯⼀ Watcher ,这是⼀个⾮常巧妙的设计,因为在同⼀时间只能有⼀个全局的 Watcher 被计算,另外它的⾃⾝属性 subs 也是 Watcher 的数组。
Watcher
这个Class的构造函数中定义了一些和Dep相关的属性。
- this.deps 和 this.newDeps 表⽰ Watcher 实例持有的 Dep 实例的数组;
- this.depIds 和 this.newDepIds 分别代表 this.deps 和 this.newDeps 的 id Set。
- Watcher还定义了⼀些原型的⽅法,和依赖收集相关的有 get 、 addDep 和 cleanupDeps⽅法
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
依赖收集
//defineReactive方法内部
let childOb = !shallow && observe(val)//可能还是个obj
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()// 执行Watcher ? Dep.target.addDep(this)
if (childOb) {
childOb.dep.depend()
//收集了依赖
//执⾏ Vue.set 的时候通过 ob.dep.notify() 能够通知到 watcher
//从⽽让添加新的属性到对象也可以检测到变化
if (Array.isArray(value)) {
dependArray(value)//把数组每个元素也去做依赖收集
}
}
}
return value
}
//...
})
在mount过程中,mountComponent函数有这么一段逻辑
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
- 当我们去实例化⼀个渲染 watcher 的时候,⾸先进⼊ watcher 的构造函数逻辑,实际上就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复⽤)。
- 接着调用vm._update,因为这个方法会生成渲染VNode,并且会对vm进行数据访问,因此会触发数据对象的getter,而每个getter都持有一个dep
- 在触发getter的时候会调用dep.depend(),即执行Dep.target.addDep(this),在保证不会被重复添加的情况下,把当前的 watcher 订阅到这个数据持有的 dep 的 subs,this.subs.push(sub),⽬的是为后续数据变化时候能通知到哪些 subs 做准备。
- traverse(value)递归触发子项getter
- Dep.target = targetStack.pop(),把Dep.target恢复成上一个状态
- this.cleanupDeps(),设计了在每次添加完新的订阅,会移除掉旧的订阅。(比如v-if渲染不同子模板a b,渲染a改a通知a的subs,切换渲染b去修改a,若会通知a的subs的回调就是浪费,因此要移除旧的订阅)
派发更新
setter 的逻辑有 2 个关键的点,⼀个是 childOb = !shallow && observe(newVal) ,如果 shallow为 false 的情况,会对新设置的值变成⼀个响应式对象;另⼀个是 dep.notify() ,通知所有的订阅者
//defineReactive方法内部
let childOb = !shallow && observe(val)//可能还是个obj
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
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 = !shallow && observe(newVal)
dep.notify()
}
//...
})
在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最终调用dep.notify(),即遍历subs调用每个watcher的update方法,会对watcher不同状态走不同逻辑,一般情况下会执行queueWatcher逻辑
- 引入一个队列的概念,因为每次数据改变都触发watcher回调,把这些watcher添加到一个队列里,然后异步执行nextTick(flushSchedulerQueue)
- 把传⼊的 cb 压⼊callbacks 数组,最后⼀次性在下一个tick执⾏
- 因此数据的变化到 DOM 的重新渲染是⼀个异步过程,发⽣在下⼀个 tick。
- flushSchedulerQueue会把queue队列做一个id从小到大的排序,原因如下
- 1.组件的更新由⽗到⼦;因为⽗组件的创建过程是先于⼦的,所以 watcher 的创建也是先⽗后⼦,执⾏顺序也应该保持先⽗后⼦。
- 2.⽤户的⾃定义 watcher 要优先于渲染 watcher 执⾏;因为⽤户⾃定义 watcher 是在渲染watcher 之前创建的。
- 3.如果⼀个组件在⽗组件的 watcher 执⾏期间被销毁,那么它对应的 watcher 执⾏都可以被跳过,所以⽗组件的 watcher 应该先执⾏。
- 在排序后遍历拿到对应watcher,执行watcher.run()。在遍历的时候每次都会对queue.length求值,因为在run()的时候queue可能会发生变化。
- run函数传入watcher的回调函数,先this.get()求值,做判断满足条件执行watcher回调,并且传入newVal和oldVal。
- this.get()求值的时候会执行getter,触发组件重新渲染。
- 状态恢复,把流程控制变量设回初始值,把watcher队列情况。
针对特殊情况
给响应式对象添加新属性,使用Vue.set API
set ⽅法接收 3个参数, target 可能是数组或者是普通对象, key 代表的是数组的下标或者是对象的键值, val 代表添加的值。
- ⾸先判断如果 target 是数组且 key 是⼀个合法的下标,则通过 splice 去添加进数组然后返回(splice并非原生splice)
- 若key 已经存在于 target 中,则直接赋值返回
- 获取target.__ob__(Observer的实例),若不存在表明target飞响应式对象直接返回
- 通过defineReactive(ob.value, key, val) 把新添加的属性变成响应式对象,然后再通过 ob.dep.notify() ⼿动的触发依赖通知。(因为在getter过程中有childOb的判断并且调用childOb.dep.depend()收集了依赖,因此能notify()通知)
- 数组情况,在observe方法观察对象的时候对数组作了处理,对数组中所有能改变数组⾃⾝的⽅法,如push、pop 等这些⽅法进⾏重写。重写后的⽅法会先执⾏它们本⾝原有的逻辑,并对能增加数组⻓度的 3 个⽅法 push、unshift、splice ⽅法做了判断,获取到插⼊的值,然后把新添加的值变成⼀个响应式对象,并且再调⽤ ob.dep.notify() ⼿动触发依赖通知
网友评论