两种机制分别指:
- 在 1次 回调里触发 多次
computed/effect,其只会执行 1次 - 在 1次 回调里 多次 更新与视图相关的
ref,视图更新也只执行 1次
举两个例子:
js
// effectA
effect(() => {
console.log('A:', a.value)
if (a.value > 5) {
// 2、再触发第2次 effectB
b.value = a.value * 2
}
})
// effectB
effect(() => {
console.log('B:', b.value)
})
// 1、更新dep, 触发1次 effectA 和 1次 effectB
a.value = 6
// 实际 effectB 只会执行 1 次,而不是 2 次
js
btn.onclick = () => {
count.value++
count.value++
count.value++
count.value++
}
return () => {
console.log('render call')
return h('div', `count: ${count.value}`)
}
// 点击按钮,实际 'render call' 只会被打印1次
如何实现的?简写源码 来说明: PS: 不熟悉响应式基础的,请移步往期文章
effect 的批处理
修改响应式变量的值后,会触发 setter 里的函数 trigger,之后会沿着链表通知所有 effect:
js
export function traggerRef(dep: RefImpl) {
......
propagate(dep.subs)
......
}
/** 传播更新 */
export function propagate(subs: Link) {
// 链表节点
let link = subs
// 收集 effect
const queuedEffect = []
while (link) {
const sub = link.sub
// 标记 dirty,防止重复触发 effect
if (!sub.tracking && !sub.dirty) {
sub.dirty = true
// ......
queuedEffect.push(sub)
// ......
}
// 遍历链表
link = link.nextSub
}
// 通知 effect 执行
queuedEffect.forEach(effect => effect.notify())
}
可以看到 sub 里有一个 dirty 属性,如果同一次回调函数中,多次触发 sub,它只会被放入待执行列表 1 次,也就是不会多次执行。
注意,dirty 标志位会等 Effect 真正执行完成后才重置。
异步渲染 render
mount组件的流程是:使用VNode创建组件实例instance -> 挂载到DOM -> 更新,组件实际上就是创建了一个 Effect 来订阅更新:
js
const mountComponent = (vnode, container, anchor) => {
/**
* 1、创建组件实例
* 2、初始化状态
* 3、挂载到DOM
*/
// 1 实例化
const instance = createComponentInstance(vnode)
// 2 初始化
setupComponent(instance)
const componentUpdateFn = () => {
// 首次挂载
if (!instance.isMounted) {
// 得到 Virtual DOM
const subTree = instance.render()
// 3 挂载
patch(null, subTree, container, anchor)
// 保存当前 V-DOM
instance.subTree = subTree
// 修改标志位
instance.isMounted = true
} else {
// 更新
const preSubTree = instance.subTree
// 获取新的 V-DOM
const subTree = instance.render()
// 对比新旧 VNode,更新
patch(preSubTree, subTree, container, anchor)
instance.subTree = subTree
}
}
const effect = new ReactiveEffect(componentUpdateFn)
const update = effect.run.bind(effect)
instance.update = update
effect.scheduler = () => {
queueJob(update)
}
effect.run()
}
但假如有如下例子,假如点击 1次 按钮,将打印 4次 effect execute 和 1次 render call:
js
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<div id="app"></div>
<button id="btn">+++</button>
<script type="module">
import { createApp, h, ref, effect, computed } from 'vue'
const rootComp = {
setup() {
const count = ref(0)
btn.onclick = () => {
count.value++
count.value++
count.value++
count.value++
}
effect(() => {
console.log('effect execute', count.value)
})
return () => {
console.log('render call')
return h('div', `count1: ${count.value}`)
}
},
}
createApp(rootComp).mount('#app')
</script>
</body>
</html>
按理说 render call 也应该打印 4次,Why? 因为代码里利用 Effect.scheduler 做了 异步更新,即重写了scheduler:
js
const componentUpdateFn = () => {
//......
const update = effect.run.bind(effect)
instance.update = update
effect.scheduler = () => {
queueJob(update)
}
//......
}
function queueJob(job) {
Promise.resolve().then(() => {
job()
})
}
此时,每次 ref 更新后,不立即重置 dirty,而是等所有同步任务执行完后,再执行渲染,BINGO