重读 vue 文档 --- nextTick

1,086 阅读4分钟

引言

看过多遍 vue.js 文档,但是对一些知识点始终似懂非懂。于是便有了重读 Vue.js 文档的计划。此篇对 Vue 中 nextTick 进行剖析并结合源码,希望对您有所帮助。

聊聊变化侦测

前端三大框架中,React、Angular的变化侦测都有一个共同点,就是都不知道哪些状态 (state) 发生了改变,于是就需要进行比较暴力的比对。React 采用了虚拟 DOM 比对,Angular 采用了脏检查机制。而 Vue在一定程度上知道哪些状态发生了改变,哪些节点依赖了这个状态,从而对这些节点进行更新操作,事实上 Vue1.0 就是这么干的。

Vue1.0 实现的好处是简单、粗暴。但也为此付出了一定的代价。原因是对每个状态进行侦测,每个绑定都会对应一个 watcher实例,来观察状态的变化,粒度太细。对一个大型项目来说,会造成很大的开销。

Vue2.0 既没有采用类似React、Angular一样的粗粒度侦测,也没有继续采用Vue1.0 的细粒度侦测,而是采取了则中方案。引入了虚拟DOM, 每个组件对应一个 watcher实例。即便组件中有多个节点依赖于某个状态,也只会生成单个 watcher 实例观察该状态。当该状态改变时,只能通知到该组件,然后通过虚拟DOM的 diff 算法去比对、更新、渲染。如果每次都同步更新视图,会非常耗性能的,于是异步更新是大势所趋了。

异步更新队列

Vue 在更新 DOM 时是异步执行的,当侦测到状态发生变化,Vue会开启一个状态更新队列,将此事件循环中的所有数据变更缓冲起来,在下一个的事件循环 tick 中,刷新队列并执行去重后的更新操作。即便一个 watcher 被多次触发,也只会被推入队列中一次

<template>
    <div class="example">
        {{ count }}
    </div>
</template>

<script>
export default {
    data () {
        return {
            count: 0
        }
    },
    mounted () {
        this.count++;
        this.count++;
        this.count++;
    },
    watch: {
        count () {
            console.log(this.count); // 只会执行一次,结果为 3
        }
    }
}

多次改变count值,本应监听到多次变化,可只监听到了最后一个操作的结果。

Vue.js 异步更新策略实现

let timerFunc;

if (typeof Promise !== 'undefined' && isNative(Promise)) { // native Promise 存在
    const p = Promise.resolve();
    timerFunc = () => {
        p.then(flushCallbacks); // flushCallbacks 作用是将回调队列清空,遍历并执行每个回调。
    };
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) 
|| MutationObserver.toString() === '[object MutationObserverConstructor]')) {
    // Use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    // (#6466 MutationObserver is unreliable in IE11)
    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);
    };
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate. 
    // Techinically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = () => {
        setImmediate(flushCallbacks);
    };
} else {
    // Fallback to setTimeout.
    timerFunc = () => {
        setTimeout(flushCallbacks, 0);
    };
}
// 以上代码就是在不同环境下,timerFunc 的不同实现

Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。而之所以优先采用microtask,然后macrotask,是因为想让回调异步且尽早调用。setTimeout(fn, 0)最快为4ms,而setImmediate 延迟小于 setTimeout。

nextTick

function nextTick(cb, ctx) { // cb 回调函数,ctx 回调执行上下文,均为可选参数
    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; // 标记是否已经向任务队列添加了一个任务,这就是多次触发一个watcher,只会被推入更新队列一次的原因
        timerFunc();
    }
    // 如果未提供回调,且存在 Promise,即返回一个 Promise
    // 于是支持 this.$nextTick().then(function () {});
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve;
        });
    }
}

nextTick 的使用情景

当改变一个值时,如 this.message = 'World',虽然 data 里的状态会立刻改变,但该状态的更新并不会立即反映到DOM上。多数情况我们不需要关心这个过程,但是如果想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。此时,我们就可以使用 nextTick(callback),这样回调函数将在 DOM 更新完成后被调用。

<template>
    <div class="example" ref="example">
        {{ message }}
    </div>
</template>

<script>
export default {
    data () {
        return {
            message: 'Hello'
        }
    },
    mounted () {
        this.message = 'World';
        console.log(this.$refs.example.innerText); // Hello
        this.$nextTick(function () {
            console.log(this.$refs.example.innerText); // World
        });
    }
}

改变了message 的值之后,界面上message 值并没有立即发生改变。而是在下一个 tick 才改变。

总结

以上是个人对 nextTick 的一些理解,希望能对大家有所帮助。如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞。您的鼓励,是对笔者的最大支持。

参考