(转)Vue中$nextTick源码解析
在做项目的时候,我们经常会用到nextTick,简单的理解就是它就是一个setTimeout函数,将函数放到异步后去处理;将它替换成setTimeout好像也能跑起来,但它仅仅这么简单吗?那为什么我们不直接用setTimeout呢?让我们深入剖析一下。
发现问题
记得之前有一个需求,就是根据文字的行数来显示展开更多的一个按钮,因此我们在Vue中给数据赋值之后需要获取文字高度。
<div id="app">
<div class="msg">
{{msg}}
</div>
</div>
new Vue({
el: '#app',
data: function(){
return {
msg: ''
}
},
mounted(){
this.msg = '我是测试文字'
console.log(document.querySelector('.msg').offsetHeight) //0
}
})
复制代码
这时不管怎么获取,文字的Div高度都是0;但是直接获取却是有值:
同样的情况也发生在给子组件传参上;我们给子组件传参数后,在子组件中调用函数查看参数。
<div id="app">
<div class="msg">
<form-report ref="child" :name="childName"></form-report>
</div>
</div>
Vue.component('form-report', {
props: ['name'],
methods: {
showName(){
console.log('子组件name:'+this.name)
}
},
template: '<div>{{name}}</div>'
})
new Vue({
el: '#app',
data: function(){
return {
childName: '',
}
},
mounted(){
this.childName = '我是子组件名字'
this.$refs.child.showName()
}
})
复制代码
虽然页面上展示了子组件的name,但是打印出来却是空值:
异步更新
我们发现上述两个问题的发生,不管子组件还是父组件,都是在给data
中赋值后立马去查看数据导致的。由于“查看数据”这个动作是同步操作的,而且都是在赋值之后;因此我们猜测一下,给数据赋值操作是一个异步操作,并没有马上执行,Vue官网对数据操作是这么描述的:
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
也就是说我们在设置this.msg = 'some thing'
的时候,Vue并没有马上去更新DOM数据,而是将这个操作放进一个队列中;如果我们重复执行的话,队列还会进行去重操作;等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿出来处理。
这样做主要是为了提升性能,因为如果在主线程中更新DOM,循环100次就要更新100次DOM;但是如果等事件循环完成之后更新DOM,只需要更新1次。还不了解事件循环的童鞋,可以看我的另一篇文章从一道面试题来理解JS事件循环
为了在数据更新操作之后操作DOM,我们可以在数据变化之后立即使用Vue.nextTick(callback)
;这样回调函数会在DOM更新完成后被调用,就可以拿到最新的DOM元素了。
//第一个demo
this.msg = '我是测试文字'
this.$nextTick(()=>{
//20
console.log(document.querySelector('.msg').offsetHeight)
})
//第二个demo
this.childName = '我是子组件名字'
this.$nextTick(()=>{
//子组件name:我是子组件名字
this.$refs.child.showName()
})
复制代码
nextTick源码分析
了解了nextTick的用法和原理之后,我们就来看一下Vue是怎么来实现这波“操作”的。
Vue把nextTick的源码单独抽到一个文件中,/src/core/util/next-tick.js
,删掉注释也就大概六七十行的样子,让我们逐段来分析。
const callbacks = []
let pending = false
let timerFunc
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
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
复制代码
我们首先找到nextTick
这个函数定义的地方,看看它具体做了什么操作;看到它在外层定义了三个变量,有一个变量看名字就很熟悉:callbacks,就是我们上面说的队列;在nextTick的外层定义变量就形成了一个闭包,所以我们每次调用$nextTick的过程其实就是在向callbacks新增回调函数的过程。
callbacks新增回调函数后又执行了timerFunc函数,pending
用来标识同一个时间只能执行一次。那么这个timerFunc函数是做什么用的呢,我们继续来看代码:
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//判断1:是否原生支持Promise
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
//判断2:是否原生支持MutationObserver
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)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
//判断3:是否原生支持setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//判断4:上面都不行,直接用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
复制代码
这里出现了好几个isNative
函数,这是用来判断所传参数是否在当前环境原生就支持;例如某些浏览器不支持Promise,虽然我们使用了垫片(polify),但是isNative(Promise)还是会返回false。
可以看出这边代码其实是做了四个判断,对当前环境进行不断的降级处理,尝试使用原生的Promise.then
、MutationObserver
和setImmediate
,上述三个都不支持最后使用setTimeout;降级处理的目的都是将flushCallbacks
函数放入微任务(判断1和判断2)或者宏任务(判断3和判断4),等待下一次事件循环时来执行。MutationObserver
是Html5的一个新特性,用来监听目标DOM结构是否改变,也就是代码中新建的textNode;如果改变了就执行MutationObserver构造函数中的回调函数,不过是它是在微任务中执行的。
那么最终我们顺藤摸瓜找到了最终的大boss:flushCallbacks;nextTick不顾一切的要把它放入微任务或者宏任务中去执行,它究竟是何方神圣呢?让我们来一睹它的真容:
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
复制代码
本来以为有多复杂的flushCallbacks,居然不过短短的8行。它所做的事情也非常的简单,把callbacks数组复制一份,然后把callbacks置为空,最后把复制出来的数组中的每个函数依次执行一遍;所以它的作用仅仅是用来执行callbacks中的回调函数。
总结
到这里,整体nextTick的代码都分析完毕了,总结一下它的流程就是:
- 把回调函数放入callbacks等待执行
- 将执行函数放到微任务或者宏任务中
- 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
再回到我们开头说的setTimeout,可以看出来nextTick是对setTimeout进行了多种兼容性的处理,宽泛的也可以理解为将回调函数放入setTimeout中执行;不过nextTick优先放入微任务执行,而setTimeout是宏任务,因此nextTick一般情况下总是先于setTimeout执行,我们可以在浏览器中尝试一下:
setTimeout(()=>{
console.log(1)
}, 0)
this.$nextTick(()=>{
console.log(2)
})
this.$nextTick(()=>{
console.log(3)
})
//运行结果 2 3 1
复制代码
最后验证猜想,当前宏任务执行完成后,优先执行两个微任务,最后再执行宏任务。
作者:谢小飞
链接:https://juejin.cn/post/6844904147804749832
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
The grading was as follows grade I in six cases, grade II in 19 cases, and grade III in three cases priligy 30mg tablets By Sunday the temp started coming down
Not all cysts need to be removed, and if the cyst is removed, the surgeon must be careful to preserve as much of the normal surrounding ovarian tissue as possible buy priligy reddit Histologically composed of closely packed glands lined by actively secreting cuboidal or hobnail shaped cells