nextTick 什么时候用?
出现的前提
修改数据时,视图不会即时更新;
这是因为vue采用的是异步更新策略; 修改数据后并不会立即更新dom,dom的更新是异步的。
什么是nextTick
官方文档解释:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
nextTick的使用场景
- 更改数据后,进行节点dom操作。
- 在created,mounted生命周期中进行DOM操作。
```javascript
<template>
<div class="box" ref="list">{{msg}}</div>
</template>
export default {
data () {
return {
msg: 'hello'
}
},
created() {
console.log(this.$refs.msg);
// 打印undefined,这是因为在当前生命周期,dom还没挂载
this.$nextTick(() => {
console.log(this.$refs.msg);
});
},
mounted () {
//在mounted阶段其实也无法保证全部挂载完毕,所以最好还是加上nextTick
this.$nextTick(() => {
console.log(this.$refs.msg);
});
//当我们修改this.msg数据后,想要操作dom;这个时候拿到的是旧值
this.msg = 'world'
let box = document.getElementsByClassName('box')[0]
console.log(box.innerHTML) // hello
}
}
//还有输入框当前是隐藏的;我们修改v-show让其显示的同时让 输入框 获取焦点
//获取元素 宽高 等
nextTick的本质
nextTick.js源码
//平时的写法
//this.$nextTick(() => {
// console.log(this.$refs.msg);
// });
//() => {
// console.log(this.$refs.msg);
// }就是传入的cb;
let callbacks = [];
let pending = false;
let timerFun
function nextTick (cb, ctx) {
var _resolve;
// 把传进来的回调函数cb放到callbacks队列里
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
// pending代表一个等待状态 等这个tick执行
if (!pending) {// 同一个事件循环(tick) 这里面只会执行一次
pending = true;
timerFunc();//重点看这个
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
/**----- timerFunc ----*/降级策略
// 1、优先考虑Promise实现
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
} 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)
}
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 3、降级到setImmediate实现 setImmediate在宏任务中优先级比setTimeout高
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 4、如果以上都不支持就用setTimeout来兜底了
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 将callbacks中的cb依次执行
function flushCallbacks () {
//设置 pending 为 false, 说明该 函数已经被推入到任务队列或主线程中。
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()// 执行回调函数
}
}
//总结 本质 往任务队列 追加微任务或者宏任务
MutationObserver API(监视DOM变动的接口)
上面提到了MutationObserver API 是浏览器自带api,还可以用它做兴趣埋点
let mo = new MutationObserver(callback) //传入一个回调 ,得到实例
var domTarget = 你想要监听的dom节点
mo.observe(domTarget, {
characterData: true //说明监听文本内容的修改。
childList:true//子节点变动
})
分析
let counter = 1
const observer = new MutationObserver(flushCallbacks) //创建实例
const textNode = document.createTextNode(String(counter)) //创建一个TextNode节点
observer.observe(textNode, {
characterData: true
})//监听
//当我们调用timerFunc counter 改变了,
//导致TextNode节点文本改变,所以触发observe,执行回调flushCallbacks![在这里插入图片描述](https://img-blog.csdnimg.cn/2021071614481365.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2JpZ19Db2NreQ==,size_16,color_FFFFFF,t_70#pic_center)
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
讲到这里,大家就知道nextTick 其实就是利用了事件循环机制。
nextTick,下一个tick?
- JS分为同步任务和异步任务。
- 同步任务都在主线程上执行,形成一个执行栈。
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
-
一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
总结:每执行一个宏任务,就是一个tick。那么可理解为“在下一个宏任务执行之前的回调函数”。
问题:为什么优先宏任务?
microtasks(微任务)的执行速度是快于宏任务的;并且宏任务之间还穿插着UI渲染。因此优先选择microtasks。
那这和dom更新有啥关系呢?nextTick如何知道DOM什么时候更新呢?带着疑惑,继续往下看。
nextTick如何知道DOM什么时候更新呢?
```javascript
<template>
<div class="box" ref="list">{{msg}}</div>
</template>
export default {
data () {
return {
msg: 'hello'
}
},
mounted () {
this.msg = 'world'
let box = document.getElementsByClassName('box')[0]
console.log(box.innerHTML) // hello
this.$nextTick(() => {
console.log(box.innerHTML) // world
});
}
}
分析这个代码;this.msg = ‘world’执行后;直接打印是hello;而nextTick下是最新的值。如何做到的呢?
我们可以从this.msg = ‘world’ 开始说起。msg是响应式数据,在vue中当某个响应式数据发生变化的时候,他的setter函数会通知dep,dep会调用他管理的所有的watch对象。触发watch对象的update实现。这部分的知识可以后续了解下。反正最终this.msg = ‘world’这个代码,触发watch对象的update。
<!--观察者Watcher类-->
class Watcher {
constructor () {
Dep.target = this // new Watcher的时候把观察者存放到Dep.target里面
}
update () {
queueWatcher(this) // 异步更新策略
}
run () {
// dom在这里执行真正的更新
}
}
/** queueWatcher函数*/
let has = {};
let queue = [];
let waiting = false;
function queueWatcher (watcher: Watcher) {
/*获取watcher的id*/
const id = watcher.id
// 防止queue队列wachter对象重复
if (has[id] == null) {
has[id] = true
queue.push(watcher)
// 传递本次的更新任务
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
/** flushSchedulerQueue函数 */
function flushSchedulerQueue () {
let watcher, id;
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
// 执行更新
watcher.run();
}
// 更新完毕恢复标志位
waiting = false;
}
1.queue里面存放着我们本次要更新的watcher对象,queueWatcher函数做了一个判重操作,相同的watcher对象只会被加入到queue队列一次。就比如写个for循环去改变msg变量。虽然会出发update100次,但是之后进入queue一次。
2.flushSchedulerQueue函数依次调用了wacther对象的run方法执行更新。并作为回调传递给了nextTick函数。
3.waiting这个标记位代表我们是否已经向nextTick函数传递了更新任务,nextTick会在当前task结束后再去处理传入的回掉,只需要传递一次,更新完毕再重置这个标志位。
也就是说:
this.msg=‘hello’ =>update=>queueWatcher=>quene=>nextTick(flushSchedulerQueue );
最终调用nextTick,并将quene中的watch的run作为回调传入。当然只是传入,没调用哈。
再看上面写的代码
this.msg='hello' //执行后 callbacks里面【flushSchedulerQueue 】
this.$nextTick(() => {
console.log(box.innerHTML) // world
});
//执行后 callbacks里面【flushSchedulerQueue,() => {
console.log(box.innerHTML) // world
} 】
由此可见,先执行flushSchedulerQueue后,dom更新了,这个时候去执行用户写的回调,当然是能够获取到更新后的DOM。
demo
<template>
<div class="hello">
<div class='test' >{{msg}}</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
mounted () {
let x = document.querySelector('.test')
this.$nextTick(() => {
console.log(x.innerHTML, 1111)//Welcome to Your Vue.js App
})
this.msg = '新数据'
this.$nextTick(() => {
console.log(x.innerHTML, 222)//新数据111
})
this.msg = '新数据1111'
}
}
</script>
那我这样写,打印出来的是什么呢?
解释
1
this.$nextTick(() => {
console.log(x.innerHTML, 1111)//Welcome to Your Vue.js App
})
callbacks 【() => {
console.log(x.innerHTML, 1111)//Welcome to Your Vue.js App
}】
2
this.msg = '新数据'
callbacks =[
() => {
console.log(x.innerHTML, 1111)//Welcome to Your Vue.js App
},
flushSchedulerQueue
]
3
this.$nextTick(() => {
console.log(x.innerHTML, 222)//新数据111
})
执行后如下:
callbacks =[
() => {
console.log(x.innerHTML, 1111)//Welcome to Your Vue.js App
},
flushSchedulerQueue ,
() => {
console.log(x.innerHTML, 222)//新数据111
}
]
4.
this.msg = '新数据1111'
是同一个watch;判重,所以调用了update方法,但是没有进入quene数组中。
但是为啥打印的是新数据111,而不是新数据呢。因为watch实例的value是已经更新完毕的。
也就是说:
第二步执行后:watch实例的value是'新数据'。然后向nextTick抛出一个回调。
第三步执行后:向nextTick抛出一个回调。
第四步执行后:watch实例的value是'新数据111'。
然后代码(第一个宏任务)跑完,开始执行所有微任务(现在的nextTick中的回调)。当执行第二个回调的时候,watch.run方法拿到的就是watch实例的value'新数据111'。