持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
一、前情回顾 & 背景
上一篇小作文文是 nextTick 的姊妹篇,同时又是深入理解 nextTick 精妙设计所在的过程。另外又解答了何为合并多次修改的性能优化,其核心实现如下:
watcher被push到queue之前有一步判断重复处理,即has[id] == null;watcher被成功推入queue之后has[id] = true,可保证下次push这个watcher时不会通过上一步的判重,因而不会重复加入queue;- 当
queue中的watcher被重新求值前,会按id进行升序,用户 wacher的id小于渲染 watcher的id,所以用户 watcher放心大胆的改响应式数据,同样是由于has[渲染watcher id] = true故而不会将渲染 watcher多次加入queue; - 最后,每个
watcher从queue取出并重新求值后has[被取出 watcher id]被置为null,这就保证下个tick中修改响应式数据时,这些watcher又可以被重新添加到queue;
二、示例代码
首先我们准备下面一个例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue</title>
</head>
<body>
<div id="app">
<--! 点击这个按钮会修改 forProp 中 a 属性值,使之累加-->
<button @click="goForPatch">使 forProp.a++ ===> {{forProp.a}}</button>
<some-com :some-key="forProp"></some-com>
</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: 'hhhhhhh' }
}
}
},
inject: ['foo']
}
new Vue({
el: '#app',
data: {
forProp: {
a: 100
},
},
props: {
someProp: {
type: Object,
default: function () { return { sp: 'NO 996' } }
}
},
methods: {
goForPatch () {
this.forProp.a++;
}
},
computed: {
someComputed () {
return this.msg + this.someProp.sp
},
forPatchComputed () {
return this.forProp.a + ' reject 996'
}
},
components: {
someCom: sub
}
})
</script>
</body>
</html>
从上面的示例代码可以看出,当点击 button 按钮时,触发点击事件执行 goForPatch 方法,在该方法内修改 forProp.a 属性,使之累加。
forProp 在模板中两次被引用:
<button>元素中的文案:forProp.a- 作为
prop属性传递给子组件:<some-com :some-key="forProp" />
初次渲染时使用的是 forProp 的初始值,渲染完成后 forProp 被修改势必会触发重新渲染,这个过程就包含 DOM diff 过程;
三、触发渲染 Watcher 更新
触发 渲染 Watcher 更新的前提是 forProp.a 被更新,那么 forProp.a 被更新为什么会带来 渲染 Watcher 的更新?
带着这个问题我们回顾一下前面的全部过程:
3.1 触发点击事件
首先点击 button 触发点击事件处理函数 goForPatch,如下图所示:
在 goForPatch 方法中修改 forProp.a 属性,使之累加,forProp.a 的初始值为 100,累加后为 100;
3.2 触发响应式数据 setter
在数据响应式初始化的时候通过 defineReactive 方法把 forProp.a 属性变成 getter 和 setter,当被修改时就会触发 setter,setter 函数的参数接收 forProp.a 的新值 101:
在 setter 的后面会调用 dep.notify 方法,dep 是 Dep 类型的实例,每个响应式数据都与自己的 dep 实例,dep 的作用就是把它对应的响应式数据的 watcher 收集起来,当响应式数据被修改时也是由 dep.notify 通知 watcher 更新。
在 dep 收集的 watcher 中就包含了视图对应的 渲染 watcher
上面截图上的框中的 expression 并不是 Watcher 实例本身,而是 Watcher 实例的 get 函数,当 watcher 求值时会执行这个函数,这个函数中 vm._update/vm._render 都是大家熟悉的处理视图更新的方法;
当然,通过前面的小作文的讲解,notify 并不会直接调用这些 watcher 求值,而是通过 queueWatcher 方法合并当前事件循环中的更新,把要更新的 watcher 添加到一个队列中,在下个事件循环中再执行 watcher 的求值动作;
3.3 渲染 watcher 更新
经过 queueWatcher 合并后,在下个事件循环执行 flushSchedulerQueue 方法,队列中的 watcher 实例被逐个调用 watcher.run 方法进行重新求值:
下面是 Watcher.prototype.run 方法,在 run 方法内部会调用 Watcher.prototype.get 方法:
这个 get 方法其实就是调用创建 渲染 Watcher 实例的时候传入的 updateComponent 方法:
传入 updateComponent,创建渲染 Watcher(mountComponent 方法部分代码):
export function mountComponent () {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
}
}, true)
}
3.3 updateComponent 方法执行
从上面的代码块可以看出 updateComponent 就是调用 vm._render() 获取虚拟 DOM(VNode 节点)然后把虚拟DOM 传递给 vm._update() 方法,而 vm._update 则根据有没有旧的节点(vm._vnode 属性)决定是进行初次渲染还是进行 patch,部分代码如下:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// 页面的挂载点,真实元素
const prevEl = vm.$el
// 旧的 VNode
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
// 新的 VNode
// 如果执行过初次渲染后,vm._vnode 就不在是 undefined 了
// 所以响应式数据更新时上面的 preVnode 就有值了
vm._vnode = vnode
if (!prevVnode) {
// 老的 VNode 不存在,标识首次渲染,即初始化页面时走这里
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
} else {
// 响应式数据更新时,即更新页面时走这里
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
四、总结
好久不写这个系列的小作文了,好多内容都忘记了😂😂😂,前一阵子写 thread-loader 源码了,真的都想断更了。
但是想来想去善始善终吧,不然就彻底有遗憾了,重新捡起来和开始写第一篇一样困难。
本篇小作文相当于给自己复习了,如果是一路读过来的小伙伴到这里也就当复习了吧,本篇并没有开始写 DOM diff 的过程,而是 DOM diff 的前奏部分——触发渲染函数更新:
-
通过修改响应式数据(UI操作、代码修改均可)触发响应式数据的
setter函数; -
setter函数接收新的值,然后通过被修改的响应式数据的dep实例派发依赖该数据的Watcher更新; -
dep.notify会把这些watcher添加到队列中,然后下个事件循环再集中更新这些watcher; -
这些
watcher中包含了渲染 watcher,对渲染 watcher求值就会执行创建渲染 watcher时传入的updateComponent方法,这个方法就会执行vm._update()进而执行vm.__patch__方法,进入patch阶段;