一、前情回顾 & 背景
上一篇的小作文详细讨论了 createElm 这个方法的逻辑,这个方法根据 VNode 创建真实的元素,其中包含两种场景:
- 如果
VNode是自定义组件,则调用createComponent方法处理; - 如果是普通元素,则通过
nodeOps.createElement创建原生HTML元素;
在上一篇的最后梳理了整个从 new Vue 到 insert 的全过程执行栈流程,这一个过程就是整个 Vue 从 new Vue 到渲染到页面上的全过程。
但是 Vue 是个响应式的系统,前面的过程只完成了一半,而剩下的一半就是当响应式数据发生变化时,Vue 的渲染 watcher 收到通知触发重新渲染,也就是大家常说的 patch 阶段了。
对于 Vue 自身内部设计来说,只要是 VNode 渲染到页面的过程都叫做 patch,并没有拆成两个大的流程,虽然内部是区分还是第一次渲染你还是响应式数据发生变更而渲染。但是这个对于初学 Vue 源码的人来说不友好,所以我自作聪明的把它分成两个过程还起了名字;
- 第一次渲染我们叫他
初次渲染,也就是前面的挂载阶段,第一次把模板变成DOM渲染到页面; - 后面的这一部分叫做
patch 阶段,后面的这个阶段就是响应式数据更新触发重新渲染,你最爱的dom diff就是这个阶段的小可爱了(她虐你千百遍,你待她如初恋);
二、过程分析 + 示例代码
为了适应 patch 阶段需要响应式数据的更新,我们在模板中加入了一个按钮 button#btn,这个按钮点击事件的 handler 会修改 data.forProp.a 属性:
forPatchComputed这个计算属性依赖data.forProp;<some-com />组件接收的someKeyprop 绑定的数据也是data.forProp
test.html 代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue</title>
</head>
<body>
<div id="app">
<button id="btn" @click="goForPatch">使 forProp.a++</button>
<some-com :some-key="forProp"><div slot="namedSlot">SLOT-CONTENT</div></some-com>
<div>forPatchComputed = {{forPatchComputed}}</div>
<div class="static-div">静态节点</div>
</div>
<script src="./dist1/vue.js"></script>
<script>
const sub = {
template: `
<div style="color: red;background: #5cb85c;display: inline-block">
<slot name="namedSlot">slot-fallback-content</slot>
{{ someKey.a + ' foo' }}
</div>`,
props: {
someKey: {
type: Object,
default () {
return { a: '996-rejected!!' }
}
}
};
debugger
new Vue({
el: '#app',
data: {
forProp: {
a: 100
}
},
methods: {
goForPatch () {
this.forProp.a++
}
},
computed: {
// 计算属性
forPatchComputed () {
return this.forProp.a + ' reject 996'
}
},
components: {
someCom: sub
}
})
</script>
</body>
</html>
所以当 data.forProp 被修改时,现在有三个 watcher 要触发更新了:
forPatchComputed计算属性对应的watcher,注意计算属性的watcher是lazy的;some-com对应的渲染 watcher;div#app这个根实例的模板对应的渲染 watcher;
三、触发响应式更新
在响应式系统中是一个明显的观察者模式,这就要求我们搞清楚谁是观察者,谁是被观察者,谁负责这二者间的通信;
- 观察者就是
Watcher实例,watcher从字面量已经看出是观察者(watch英文翻译不是注视、看吗~); - 被观察者自然就是数据本身了,比如
data.forProp这个对象; - 负责二者的通信就是
Dep实例了,Dep是依赖,注意是名词不是动词(想表达的是被 watcher 依赖,如果变成动词就是依赖别人了);每个响应式数据都有dep,dep负责收集用到这个数据的watcher,然后数据改变了则派发通知,让watcher行动起来;
3.1 复习响应式的实现
数据的响应式一共有三个组成部分:
- 在初始化响应式数据的时候将
data通过defineReactive方法(核心是Object.defineProperty)将data.forProp变成getter和setter,当被访问时通过getter被触发收集依赖,当被需改时触发setter通知依赖这个数据的watcher们更新; - 初始化响应式数据时处理
computed的逻辑:- 2.1 给每个计算属性创建一个
watcher,而创建Watcher类的实例时传入的expOrFn即要求值的函数,就是定义计算属性时声明的方法例如上面的例子:forPatchComputed () { return this.forProp.a + 'reject 996!!!' }; - 2.2 计算属性是
lazy watcher,即这个计算属性被访问的时候才会求值; - 2.3 什么时候求值呢?被访问到时候,
forPatchComputed是根实例的模板对应的render 函数执行的时候就会拿forPatchComputed对应的值,这时求值;
- 2.1 给每个计算属性创建一个
- 修改
data.forProp.a触发setter,而前面getter已经知道有三个watcher依赖了这个forProp,此时通知他们三个更新;
谁来负责收集依赖 watcher 们和通知 watcher 们更新呢?Dep 类,在数据响应式初始化的时候给每个数据都创建一个 Dep 的实例,dep.denpend 收集依赖 watcher,dep.notify 通知 watcher 更新。watcher 更新则是通过 watcher.update() 方法实现的;
function defineRective () {
const dep = new Dep();
Object.defineProperty(target, key {
get () {
if (Dep.target) dep.depend()
}
set () {
dep.notify()
}
})
}
下图是点击 button#btn 后因修改 this.forProp.a 而触发 setter 进入到 setter :
3.2 Dep.prototype.notify
export default class Dep {
// 把 watcher 放大响应式数据的依赖列表中
depend () {
if (Dep.target) {
// Dep.target 是响应式数据 getter 时设置的,值是 watcher 实例
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
// 遍历 dep 中存储的 watcher,执行 watcher.update()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
下图是点击按钮后断点调试进入到 dep.notify,你会发现 this.subs 中有三个 watcher:
- 第一个是 forPatchComputed 这个
计算属性 watcher,如图
2. 第二个是
div#app 这个根实例模板对应的渲染 watcher:
- 第三个则是
<some-com />自定义组件的渲染 watcher:
3.3 Wather.prototype.update
export default class Watcher {
constructor (...) {}
update () {
if (this.lazy) {
// 懒执行的 watcher 走这里
// 将 dirty 置为 true,
// 就可以让 计算属性对应的 getter 被访问到的时候
// 再触发重新计算 computed 回调函数的执行结果
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 更新时一般都在这里,
// 将 watcher 放入到 watcher 队列,
// 然后异步更新队列
queueWatcher(this)
}
}
}
3.3.1 计算属性的 update
3.3.2 渲染 watcher 的 update
queueWatcher 是一个重点,我们为它单独开一篇;
四、总结
本篇小作文作为 patch 阶段的第一篇主要做了以下工作:
- 重新修改
test.html加入了可以修改响应式数据的button#btn元素,以及绑定点击事件修改data.forProp.a; - 重新梳理了完整的响应式流程,包含依赖收集、修改数据、派发更新的过程;并且明确了
Watcher、Dep以及响应式数据间的依赖和被依赖关系以及三者协作过程; - 通过修改
this.forProp.a进入到了dep.notify(),接着看到了作为计算属性的lazy watcher和普通 watcher在watcher.update()方法中的不同处理方式:-
3.1 lazy watcher 把 this.dirty 置为 true;这就可以使得计算属性的缓存失效,当计算属性再次被访问到的时候,就会重新求值,这个过程我们在说 Watcher 的时候详细介绍过 computed 的缓存原理;
-
3.2 普通
watcher包括渲染 watcher和用户 watcher都会执行queueWatcher方法进行异步的队列更新;
-