摘要:浅析的特点之一就是响应式,但数据更新时,并不会立即更新。尽管已经更新,但新增的元素并不立即插入到中。实际在中,执行了,这也是自动绑定到执行上下文的原因。在内,使用数组保存回调函数,表示当前状态,使用函数来执行回调队列。
Vue.nextTick 浅析
Vue 的特点之一就是响应式,但数据更新时,DOM 并不会立即更新。当我们有一个业务场景,需要在 DOM 更新之后再执行一段代码时,可以借助nextTick实现。以下是来自官方文档的介绍:
将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
具体的使用场景和底层代码实现在后面的段落说明和解释。
用途Vue.nextTick([callback, context]) 与 vm.$nextTick([callback])
前者是全局方法,可以显式指定执行上下文,而后者是实例方法,执行时自动绑定this到当前实例上。
此外,在 2.1.0 版本还新增了不传入回调的使用方式,这种调用会返回一个 Promise,在 then 的回调执行目标操作即可,如vm.$nextTick().then(cb)。
以下是一个nextTick使用例子:
{{count}}
- {{item}}
new Vue({ el: "#app", data: { count: 0, list: [] }, methods: { add() { this.count += 1; this.list.push(1); let li = this.$refs.ul.querySelectorAll("li"); li.forEach(item => { item.style.color = "red"; }); } } });
以上的代码,期望在每次新增一个列表项时都使得列表项的字体是红色的,但实际上新增的列表项字体仍是黑色的。尽管data已经更新,但新增的 li 元素并不立即插入到 DOM 中。如果希望在 DOM 更新后再更新样式,可以在nextTick的回调中执行更新样式的操作。
new Vue({ el: "#app", data: { count: 0, list: [] }, methods: { add() { this.count += 1; this.list.push(1); this.$nextTick(() => { let li = this.$refs.ul.querySelectorAll("li"); li.forEach(item => { item.style.color = "red"; }); }); } } });解释
数据更新时,并不会立即更新 DOM。如果在更新数据之后的代码执行另一段代码,有可能达不到预想效果。将视图更新后的操作放在nextTick的回调中执行,其底层通过微任务的方式执行回调,可以保证 DOM 更新后才执行代码。
源码在/src/core/instance/index.js,执行方法renderMixin(Vue)为Vue.prototype添加了$nextTick方法。实际在Vue.prototype.$nextTick中,执行了nextTick(fn, this),这也是vm.$nextTick( [callback] )自动绑定this到执行上下文的原因。
nextTick函数在/scr/core/util/next-tick.js声明。在next-tick.js内,使用数组callbacks保存回调函数,pending表示当前状态,使用函数flushCallbacks来执行回调队列。在该方法内,先通过slice(0)保存了回调队列的一个副本,通过设置callbacks.length = 0清空回调队列,最后使用循环执行在副本里的所有函数。
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](); } }
接着定义函数marcoTimerFunc、microTimerFunc。
先判断是否支持setImmediate,如果支持,使用setImmediate执行回调队列;如果不支持,判断是否支持MessageChannel,支持时,在port1监听message,将flushCallbacks作为回调;如果仍不支持MessageChannel,使用setTimeout(flushCallbacks, 0)执行回调队列。不管使用哪种方式,macroTimerFunc最终目的都是在一个宏任务里执行回调队列。
if (typeof setImmediate !== "undefined" && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks); }; } else if ( typeof MessageChannel !== "undefined" && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === "[object MessageChannelConstructor]") ) { const channel = new MessageChannel(); const port = channel.port2; channel.port1.onmessage = flushCallbacks; macroTimerFunc = () => { port.postMessage(1); }; } else { /* istanbul ignore next */ macroTimerFunc = () => { setTimeout(flushCallbacks, 0); }; }
然后判断是否支持Promise,支持时,新建一个状态为resolved的Promise对象,并在then回调里执行回调队列,如此,便在一个微任务中执行回调,在 IOS 的 UIWebViews 组件中,尽管能创建一个微任务,但这个队列并不会执行,除非浏览器需要执行其他任务;所以使用setTimeout添加一个不执行任何操作的回调,使得微任务队列被执行。如果不支持Promise,使用降级方案,将microTimerFunc指向macroTimerFunc。
if (typeof Promise !== "undefined" && isNative(Promise)) { const p = Promise.resolve(); microTimerFunc = () => { p.then(flushCallbacks); // in problematic UIWebViews, Promise.then doesn"t completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn"t being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) setTimeout(noop); }; } else { // fallback to macro microTimerFunc = macroTimerFunc; }
在函数nextTick内,先将函数cb使用箭头函数包装起来并添加到回调队列callbacks,转入的回调cb会在callbacks被遍历执行的时候执行。如果没有传入cb,则是形如this.$nextTick().then(cb)的使用方式,所以要返回一个fulfilled的 Promise,在箭头函数内则需要执行resolved,令返回的 Promise 状态变为fulfilled。接着判断当前是否正在执行回调,如果不是,将pengding设置为真。判断回调执行是宏任务还是微任务,分别通过marcoTimerFunc、microTimerFunc来触发回调队列。最后,如果,没有传入cb,则需要创建一个Promise实例并返回以支持链式调用,并且将_resolve指向返回 Promise 的resolve函数。
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; if (useMacroTask) { macroTimerFunc(); } else { microTimerFunc(); } } // $flow-disable-line if (!cb && typeof Promise !== "undefined") { return new Promise(resolve => { _resolve = resolve; }); } }
而全局方法Vue.nextTick在/src/core/global-api/index.js中声明,是对函数nextTick的引用,所以使用时可以显式指定执行上下文。
Vue.nextTick = nextTick;小结
本文关于nextTick的使用场景和源码做了简单的介绍,如果想深入了解这部分的知识,可以去了解一下微任务mircotask和宏任务marcotask。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/108549.html
摘要:核心的异步延迟函数,用于异步延迟调用函数优先使用原生原本支持更广,但在的中,触摸事件处理程序中触发会产生严重错误的,回调被推入队列但是队列可能不会如期执行。 浅析 Vue 2.6 中的 nextTick 方法。 事件循环 JS 的 事件循环 和 任务队列 其实是理解 nextTick 概念的关键。这个网上其实有很多优质的文章做了详细介绍,我就简单过过了。 以下内容适用于浏览器端 JS,...
摘要:哪吒别人的看法都是狗屁,你是谁只有你自己说了才算,这是爹教我的道理。哪吒去他个鸟命我命由我,不由天是魔是仙,我自己决定哪吒白白搭上一条人命,你傻不傻敖丙不傻谁和你做朋友太乙真人人是否能够改变命运,我不晓得。我只晓得,不认命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出处 查看github最新的Vue...
摘要:因为平时使用都是传回调的,所以很好奇什么情况下会为,去翻看官方文档发现起新增如果没有提供回调且在支持的环境中,则返回一个。这就对了,函数体内最后的判断很明显就是这个意思没有回调支持。 Firstly, this paper is based on Vue 2.6.8刚开始接触Vue的时候,哇nextTick好强,咋就在这里面写就是dom更新之后,当时连什么macrotask、micro...
摘要:在查询了各种资料后,总结了一下其原理和用途,如有错误,请不吝赐教。截取关键部分如下具体来说,异步执行的运行机制如下。知乎上的例子改变数据想要立即使用更新后的。需要注意的是,在和阶段,如果需要操作渲染后的试图,也要使用方法。 对于 Vue.nextTick 方法,自己有些疑惑。在查询了各种资料后,总结了一下其原理和用途,如有错误,请不吝赐教。 概览 官方文档说明: 用法: 在下次 DO...
阅读 2568·2021-09-06 15:02
阅读 3198·2021-09-02 10:18
阅读 2819·2019-08-30 15:44
阅读 683·2019-08-30 15:43
阅读 1946·2019-08-30 14:08
阅读 2756·2019-08-30 13:16
阅读 1393·2019-08-26 13:52
阅读 930·2019-08-26 12:21