Vue.nextTick浅析

Vue的特点之一就是响应式,但数据更新时,DOM并不会立即更新。当我们有一个业务场景,需要在DOM更新之后再执行一段代码时,可以借助nextTick实现。以下是来自官方文档的介绍:

将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

具体的使用场景和底层代码实现在后面的段落说明和解释。

用途

Vue.nextTick( [callback, context] )vm.$nextTick( [callback] )

前者是全局方法,可以显式指定执行上下文,而后者是实例方法,执行时自动绑定this到当前实例上。

以下是一个nextTick使用例子:

```<div id="app">
<button @click="add">add</button>
{{count}}
<ul ref="ul">
<li v-for="item in list">
{{item}}
</li>
</ul>
</div>
```


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=&gt;{
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(()=&gt;{
let li = this.$refs.ul.querySelectorAll('li')
li.forEach(item=&gt;{
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 &lt; copies.length; i++) {
copies[i]()
}
}

接着定义函数marcoTimerFuncmicroTimerFunc

先判断是否支持setImmediate,如果支持,使用setImmediate执行回调队列;如果不支持,判断是否支持MessageChannel,支持时,在port1监听message,将flushCallbacks作为回调;如果仍不支持MessageChannel,使用setTimeout(flushCallbacks, 0)执行回调队列。不管使用哪种方式,macroTimerFunc最终目的都是在一个宏任务里执行回调队列。


if (typeof setImmediate !== 'undefined' &amp;&amp; isNative(setImmediate)) {
macroTimerFunc = () =&gt; {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' &amp;&amp; (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () =&gt; {
port.postMessage(1)
}
} else {
/* istanbul ignore next */
macroTimerFunc = () =&gt; {
setTimeout(flushCallbacks, 0)
}
}

然后判断是否支持Promise,支持时,新建一个状态为resolvedPromise对象,并在then回调里执行回调队列,如此,便在一个微任务中执行回调,在IOS的UIWebViews组件中,尽管能创建一个微任务,但这个队列并不会执行,除非浏览器需要执行其他任务;所以使用setTimeout添加一个不执行任何操作的回调,使得微任务队列被执行。如果不支持Promise,使用降级方案,将microTimerFunc指向macroTimerFunc


if (typeof Promise !== 'undefined' &amp;&amp; isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () =&gt; {
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。接着判断当前是否正在执行回调,如果不是,将pengding设置为真。判断回调执行是宏任务还是微任务,分别通过marcoTimerFuncmicroTimerFunc来触发回调队列。最后返回一个Promise实例以支持链式调用。


export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() =&gt; {
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 &amp;&amp; typeof Promise !== 'undefined') {
return new Promise(resolve =&gt; {
_resolve = resolve
})
}
}

而全局方法Vue.nextTick/src/core/global-api/index.js中声明,是对函数nextTick的引用,所以使用时可以显示指定执行上下文。


Vue.nextTick = nextTick

小结

本文关于nextTick的使用场景和源码做了简单的介绍,如果想深入了解这部分的知识,可以去了解一下微任务mircotask和宏任务marcotask

来源:https://segmentfault.com/a/1190000016495892

最新文章

  1. Fis3的前端模块化之路[基础篇]
  2. Visual Studio 默认保存为UTF8编码
  3. Bash shell的内建命令:type
  4. 2.4 ARM寻址方式
  5. 二叉树节点个数题目[n0,n1,n2]
  6. mono+jexus 验证码不显示:System.Drawing
  7. Spring AOP 实现原理与 CGLIB 应用
  8. jdk 1.5
  9. C#中的Collection 3
  10. C# 使用Code First迁移更新数据库
  11. 能用存储过程的DBHelper类
  12. Brackets - 强大免费的开源跨平台Web前端开发工具IDE (HTML/CSS/Javascript代码编辑器)
  13. c3p0获取连接Connection后的Close()---释疑
  14. 关系型数据库工作原理-数据库整体框架(翻译自Coding-Geek文章)
  15. Servlet中的jsp内置对象
  16. 1--Postman使用token进行批量测试
  17. 【数据结构】约瑟夫问题 C语言链表实现
  18. Selenium Grid的Java调用方法
  19. win10 设置
  20. 【转】 H.264编码原理以及I帧B帧P帧

热门文章

  1. 11. ClustrixDB 管理文件空间和数据库容量
  2. Task的用法
  3. Android 通过应用设置系统日期和时间的方法
  4. Spring Boot教程(十)异步方法测试
  5. JS中集合对象(Array、Map、Set)及类数组对象的使用与对比(转载)
  6. Jquery EasyUI tree 的异步加载(遍历指定文件夹,根据文件夹内的文件生成tree)
  7. What is &#39;typeof define === &#39;function&#39; &amp;&amp; define[&#39;amd&#39;]&#39; used for?
  8. 【C++进阶】getline
  9. 在windows下使用Mingw搭建模拟Linux
  10. JS调用服务器端方法