@frank-shaw
2019-11-30T10:18:07.000000Z
字数 4004
阅读 1358
vue
源码
异步更新
关于数据响应化,问一个常见的问题:
下面示例代码中的两个输出console.log(p1.innerHTML)
,分别是什么?为什么?
<!DOCTYPE html>
<html>
<body>
<div id="demo">
<h1>异步更新</h1>
<p id="p1">{{foo}}</p>
</div>
<script>
const app = new Vue({
el: '#demo',
data: { foo: '' },
mounted() {
setInterval(() => {
this.foo = 't1'
this.foo = 't2'
this.foo = 't3'
console.log(p1.innerHTML) //此时,页面的展示值?
this.$nextTick(() => {
console.log(p1.innerHTML) //此时,页面的展示值?
})
}, 1000);
}
});
</script>
</body>
</html>
这个问题的第一问“是什么”,并不复杂。难的是"为什么"。该问题的本质涉及到Vue的异步更新问题。
首先,需要明确的是:Vue的更新DOM的操作是异步的,批量的。之所以这么做的缘由也很简单:更新DOM的操作是昂贵的,消耗较大。如上面的展示例子所示,Vue内部会连续更新三次DOM么?那显然是不合理的。批量、异步的操作才更优雅。
我们想要去源码看看,Vue更新DOM的批量与异步操作,到底是如何做的呢?
首先界定一个界限:我们不会立马深入到虚拟DOM的生成与页面更新的patch算法中去,只是想要看看这个批量与异步的过程,解决刚刚提到的问题。
从之前的笔记内容可知:数据响应的核心方法defineReactive()
中,当数据发生变化的时候,会调用Dep.notify()
方法,通知对应的Watcher执行updateComponent()
操作,继而重新渲染执行更新页面。
让我们从Dep的notify()
方法说起。
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
...//省略
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
可知,其内部是执行的是相关联的Watcher的update()
方法。
import { queueWatcher } from './scheduler'
export default class Watcher {
...//省略
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {//如果是同步
this.run()
} else {
queueWatcher(this) //Watcher的入队操作
}
}
//实际执行的更新方法,会被scheduler调用
run () {
if (this.active) {
//this.get()是挂载时传入的updateComponent()方法
const value = this.get()
//如果是组件的Watcher,不会有返回值value,不会执行下一步
//只有用户自定义Watcher才会进入if
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
看到这里,提问一哈:如果在同一时刻,组件实例中的data修改了多次,其对应的Watcher也会执行queueWatcher(this)
多次,那么是否会在当前队列中存在多个同样的Watcher呢?
带着这个问题,查看同一文件夹下schedule.js
的queueWatcher()
方法:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
//去重
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
//异步刷新队列
nextTick(flushSchedulerQueue)
}
}
}
代码中看到:每个Watcher都会有一个id标识,只有全新的Watcher才会入队。批量的过程我们看到了,将是将Watcher放入到队列里面去,然后批量操作更新。
看了这个批量更新的操作,有人会问:多次数据响应化,只有第一次更新的Watcher才会进入队列,是不是意味着只有第一次的数据响应化才生效,而后几次的数据响应化无效了呢?
回答:并不是这样的,数据响应化一直都在进行,变化的数据也一直在变。需要明确其和批量更新队列之间的关联,发生在Watcher的run()方法上。当执行run()方法的时候,其获取的data是最新的data。
讲了批量,那么异步的过程是怎样的呢?让我们来看看nextTick()
函数内部,了解一些关于异步操作的知识点:
export let isUsingMicroTask = false
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
//关于timerFunc的选取过程
let timerFunc
//优先选择Promise,因为Promise是基于微任务的
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
//次优选择MutationObserver,MutationObserver也是基于微任务的
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
//如果以上两者都不行,那么选择setImmediate(),它是基于宏任务的
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最无奈的选择,选择setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
//nextTick: 按照特定异步策略timerFunc() 执行队列操作
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
}
关于宏任务与微任务,可以查看更多有意思的页面:
https://juejin.im/post/5b498d245188251b193d4059
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/?utm_source=html5weekly