一文了解Vue中的nextTick
vue中的 nexttick 涉及到vue中dom的异步更新,感觉很有意思,特意了解了一下。其中关于 nexttick 的源码涉及到不少知识,很多不太理解,暂且根据自己的一些感悟介绍下 nexttick 。
一、示例
先来一个示例了解下关于vue中的dom更新以及 nexttick 的作用。
模板
<div class="app"> <div ref="msgdiv">{{msg}}</div> <div v-if="msg1">message got outside $nexttick: {{msg1}}</div> <div v-if="msg2">message got inside $nexttick: {{msg2}}</div> <div v-if="msg3">message got outside $nexttick: {{msg3}}</div> <button @click="changemsg"> change the message </button> </div>
vue实例
new vue({ el: '.app', data: { msg: 'hello vue.', msg1: '', msg2: '', msg3: '' }, methods: { changemsg() { this.msg = "hello world." this.msg1 = this.$refs.msgdiv.innerhtml this.$nexttick(() => { this.msg2 = this.$refs.msgdiv.innerhtml }) this.msg3 = this.$refs.msgdiv.innerhtml } } })
点击前
点击后
从图中可以得知:msg1和msg3显示的内容还是变换之前的,而msg2显示的内容是变换之后的。其根本原因是因为vue中dom更新是异步的(详细解释在后面)。
二、应用场景
下面了解下 nexttick 的主要应用的场景及原因。
在vue生命周期的 created() 钩子函数进行的dom操作一定要放在 vue.nexttick() 的回调函数中
在 created() 钩子函数执行的时候dom 其实并未进行任何渲染,而此时进行dom操作无异于徒劳,所以此处一定要将dom操作的js代码放进 vue.nexttick()
的回调函数中。与之对应的就是 mounted() 钩子函数,因为该钩子函数执行时所有的dom挂载和渲染都已完成,此时在该钩子函数中进行任何dom操作都不会有问题 。
在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的dom结构的时候,这个操作都应该放进 vue.nexttick() 的回调函数中。
具体原因在vue的官方文档中详细解释:
vue 异步执行 dom 更新。只要观察到数据变化,vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 dom 操作上非常重要。然后,在下一个的事件循环“tick”中,vue 刷新队列并执行实际 (已去重的) 工作。vue 在内部尝试对异步队列使用原生的 promise.then 和 messagechannel ,如果执行环境不支持,会采用 settimeout(fn, 0)
代替。
例如,当你设置 vm.somedata = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 dom 状态更新后做点什么,这就可能会有些棘手。虽然 vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 dom,但是有时我们确实要这么做。为了在数据变化之后等待 vue 完成更新 dom ,可以在数据变化之后立即使用 vue.nexttick(callback) 。这样回调函数在 dom 更新完成后就会调用。
三、 nexttick 源码浅析
作用
vue.nexttick 用于延迟执行一段代码,它接受2个参数(回调函数和执行回调函数的上下文环境),如果没有提供回调函数,那么将返回 promise 对象。
源码
/** * defer a task to execute it asynchronously. */ export const nexttick = (function () { const callbacks = [] let pending = false let timerfunc function nexttickhandler () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } // the nexttick behavior leverages the microtask queue, which can be accessed // via either native promise.then or mutationobserver. // mutationobserver has wider support, however it is seriously bugged in // uiwebview in ios >= 9.3.3 when triggered in touch event handlers. it // completely stops working after triggering a few times... so, if native // promise is available, we will use it: /* istanbul ignore if */ if (typeof promise !== 'undefined' && isnative(promise)) { var p = promise.resolve() var logerror = err => { console.error(err) } timerfunc = () => { p.then(nexttickhandler).catch(logerror) // 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 if (!isie && typeof mutationobserver !== 'undefined' && ( isnative(mutationobserver) || // phantomjs and ios 7.x mutationobserver.tostring() === '[object mutationobserverconstructor]' )) { // use mutationobserver where native promise is not available, // e.g. phantomjs, ios7, android 4.4 var counter = 1 var observer = new mutationobserver(nexttickhandler) var textnode = document.createtextnode(string(counter)) observer.observe(textnode, { characterdata: true }) timerfunc = () => { counter = (counter + 1) % 2 textnode.data = string(counter) } } else { // fallback to settimeout /* istanbul ignore next */ timerfunc = () => { settimeout(nexttickhandler, 0) } } return function queuenexttick (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() } if (!cb && typeof promise !== 'undefined') { return new promise((resolve, reject) => { _resolve = resolve }) } } })()
首先,先了解 nexttick 中定义的三个重要变量。
callbacks
用来存储所有需要执行的回调函数
pending
用来标志是否正在执行回调函数
timerfunc
用来触发执行回调函数
接下来,了解 nexttickhandler()
函数。
function nexttickhandler () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
这个函数用来执行 callbacks 里存储的所有回调函数。
接下来是将触发方式赋值给 timerfunc 。
- 先判断是否原生支持promise,如果支持,则利用promise来触发执行回调函数;
- 否则,如果支持mutationobserver,则实例化一个观察者对象,观察文本节点发生变化时,触发执行所有回调函数。
- 如果都不支持,则利用settimeout设置延时为0。
最后是 queuenexttick 函数。因为 nexttick 是一个即时函数,所以 queuenexttick 函数是返回的函数,接受用户传入的参数,用来往callbacks里存入回调函数。
上图是整个执行流程,关键在于 timefunc() ,该函数起到延迟执行的作用。
从上面的介绍,可以得知 timefunc() 一共有三种实现方式。
- promise
- mutationobserver
- settimeout
其中 promise 和 settimeout 很好理解,是一个异步任务,会在同步任务以及更新dom的异步任务之后回调具体函数。
下面着重介绍一下 mutationobserver 。
mutationobserver 是html5中的新api,是个用来监视dom变动的接口。他能监听一个dom对象上发生的子节点删除、属性修改、文本内容修改等等。
调用过程很简单,但是有点不太寻常:你需要先给他绑回调:
var mo = new mutationobserver(callback)
通过给 mutationobserver 的构造函数传入一个回调,能得到一个 mutationobserver 实例,这个回调就会在 mutationobserver 实例监听到变动时触发。
这个时候你只是给 mutationobserver 实例绑定好了回调,他具体监听哪个dom、监听节点删除还是监听属性修改,还没有设置。而调用他的 observer 方法就可以完成这一步:
var domtarget = 你想要监听的dom节点 mo.observe(domtarget, { characterdata: true //说明监听文本内容的修改。 })
在 nexttick 中 mutationobserver 的作用就如上图所示。在监听到dom更新后,调用回调函数。
其实使用 mutationobserver 的原因就是 nexttick 想要一个异步api,用来在当前的同步代码执行完毕后,执行我想执行的异步回调,包括 promise 和 settimeout 都是基于这个原因。其中深入还涉及到 microtask 等内容,暂时不理解,就不深入介绍了。
总结
以上所述是小编给大家介绍的vue中的nexttick,希望对大家有所帮助