一、前情回顾 & 背景
上一篇的小作文详细讨论了 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 />
组件接收的someKey
prop 绑定的数据也是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
方法进行异步的队列更新;
-