@frank-shaw
2019-11-30T02:18:07.000000Z
字数 4004
阅读 1606
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 firstconst 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才会进入ifif (value !== this.value ||isObject(value) ||this.deep) {const oldValue = this.valuethis.value = valueif (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] = trueif (!flushing) {queue.push(watcher)} else {let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}if (!waiting) {waiting = trueif (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 = falseconst callbacks = []let pending = falsefunction flushCallbacks () {pending = falseconst copies = callbacks.slice(0)callbacks.length = 0for (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 = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true//如果以上两者都不行,那么选择setImmediate(),它是基于宏任务的} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {timerFunc = () => {setImmediate(flushCallbacks)}} else {// 最无奈的选择,选择setTimeouttimerFunc = () => {setTimeout(flushCallbacks, 0)}}//nextTick: 按照特定异步策略timerFunc() 执行队列操作export function nextTick (cb?: Function, ctx?: Object) {let _resolvecallbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending = truetimerFunc()}}
关于宏任务与微任务,可以查看更多有意思的页面:
https://juejin.im/post/5b498d245188251b193d4059
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/?utm_source=html5weekly