我们都知道JS有一个处理事件的机制 也就是事件循环机制
以及同步和异步
事件循环的顺序,决定js代码的执行顺序。事件循环如下
在执行完微任务之后才会去进行下一个宏任务
vue中关于dom实际上也是异步的
如何理解呢?
写给代码你就好理解了
<template>
<div>
<div>{{count}}</div>
<div @click="handleClick">click</div>
</div>
</template>
<script>
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 10000; i++) {
this.count++;
}
}
}
}
</script>
如上 想想看Vue中数据驱动视图,本质上就是数据的变化会引起DOM操作改变浏览器的显示
上面的count数据不断变化 变化了1W次 那么岂不是要执行1W次的DOM操作吗?显然这会让浏览器十分的奔溃,浪费了很多的操作
于是vue就决定让这些数据变化完 等到count已经加完1w次了之后再执行DOM 那么这样的话DOM只更新一次也同样达到了效果 显示会变成1W
具体是如何呢
我们来看Vue官方文档的解释
并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列queueWatcher里,然后在 nextTick 后执行 flushSchedulerQueue处理当 count 增加 10000 次时,vue内部会先将对应的 Watcher 对象给 push 进一个队列 queue 中去,等下一个 tick 的时候再去执行。并不需要在下一个 tick 的时候执行 10000 个同样的 Watcher 对象去修改界面,而是只需要执行一个 Watcher 对象,使其将界面上的 0 变成 10000 即可。
而有的时候我们就需要在DOM更新完成之后再执行操作 但是因为DOM更新是异步的 我们的写的代码又是同步的 就会出现错乱 我们的代码并不会执行!那么我们把操作变成异步的不就好了吗?
这当然是一个办法 最简单的莫过于使用setTimout把时间调成0 这样就完美的实现了 setTimout内的回调函数执行是异步的 会在等DOM执行完毕之后执行
但是这个问题vue官方也了解到了 并给出了解决方案
也就是我们要说nextTick
我们还是先看官方文档上面是怎么讲的
作用:在下一次DOM更新结束后执行其指定的回调
什么时候用:在改变数据后,要基于更新后的DOM进行某些操作时,要在nextTick所指定的回调函数中执行
代码应用<template>
<div class="test">
<p ref='msg' id="msg">{{msg}}</p>
</div>
</template>
<script>
export default {
name: 'Test',
data () {
return {
msg:"hello world",
}
},
methods: {
changeMsg() {
this.msg = "hello Vue" // vue数据改变,改变了DOM里的innerText
let msgEle = this.$refs.msg.innerText //后续js对dom的操作
console.log(msgEle) // hello world
// 输出可以看到data里的数据修改后DOM并没有立即更新,后续的DOM不是最新的
this.$nextTick(() => {
console.log(this.$refs.msg.innerText) // hello Vue
})
this.$nextTick().then(() => {
console.log(this.$refs.msg.innerText) // hello Vue
})
},
changeMsg2() {
this.$nextTick(() => {
console.log(this.$refs.msg.innerText) // 1.hello world
})
this.msg = "hello Vue" // 2.
console.log(this.$refs.msg.innerText) // hello world
this.$nextTick().then(() => {
console.log(this.$refs.msg.innerText) // hello Vue
})
// nextTick中先添加的先执行,执行1后,才会执行2(Vue操作Dom的异步)
}
}
}
</script>
可能大家对changeMsg2并不是那么理解
我这边先说一下他们在控制台输出的顺序、
hello world 34行代码
hello world 31行代码
hello vue 36行代码
有人不是说next Tick不是异步吗?为什么会这样
nextTick1:注意其虽然是放在$nextTick的回调中,在下一个tick执行,但是他的位置是在this.name = 'hellow world'的前。也就是说,他的cb会比App组件的派发更新(flushSchedulerQueue)更先进入队列,当nextTick1打印时,App组件还未派发更新,所以拿到的还是旧的DOM值。
那么nextTick的原理是什么?
由上一节我们知道,Vue中 数据变化 => DOM变化 是异步过程,一旦观察到数据变化,Vue就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察到数据变化的 Watcher(Vue源码中的Wacher类是用来更新Dep类收集到的依赖的)推送进这个队列。
如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
nextTick的作用是为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback),JS是单线程的,拥有事件循环机制,nextTick的实现就是利用了事件循环的宏任务和微任务。
因此对next cilk的源码解读如下
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
// 上面三行与核心代码关系不大,了解即可
// noop 表示一个无操作空函数,用作函数默认值,防止传入 undefined 导致报错
// handleError 错误处理函数
// isIE, isIOS, isNative 环境判断函数,
// isNative 判断某个属性或方法是否原生支持,如果不支持或通过第三方实现支持都会返回 false
export let isUsingMicroTask = false // 标记 nextTick 最终是否以微任务执行
const callbacks = [] // 存放调用 nextTick 时传入的回调函数
let pending = false // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
// 当向任务队列中添加了任务时,将 pending 置为 true,当任务被执行时将 pending 置为 false
//
// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
// 回调的 this 自动绑定到调用它的实例上
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
// 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
callbacks.push(() => {
if (cb) { // 对传入的回调进行 try catch 错误捕获
try {
cb.call(ctx)
} catch (e) { // 进行统一的错误处理
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 如果当前没有在 pending 的回调,
// 就执行 timeFunc 函数选择当前环境优先支持的异步方法
if (!pending) {
pending = true
timerFunc()
}
// 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
// 在返回的这个 promise.then 中 DOM 已经更新好了,
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
// 判断当前环境优先支持的异步方法,优先选择微任务
// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
// setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行
// setImmediate 在 IE10 和 node 中支持
// 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次
let timerFunc
// 判断当前环境是否原生支持 promise
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 支持 promise
const p = Promise.resolve()
timerFunc = () => {
// 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
// 标记当前 nextTick 使用的微任务
isUsingMicroTask = true
// 如果不支持 promise,就判断是否支持 MutationObserver
// 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
// new 一个 MutationObserver 类
const observer = new MutationObserver(flushCallbacks)
// 创建一个文本节点
const textNode = document.createTextNode(String(counter))
// 监听这个文本节点,当数据发生变化就执行 flushCallbacks
observer.observe(textNode, { characterData: true })
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter) // 数据更新
}
isUsingMicroTask = true // 标记当前 nextTick 使用的微任务
// 判断当前环境是否原生支持 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => { setImmediate(flushCallbacks) }
} else {
// 以上三种都不支持就选择 setTimeout
timerFunc = () => { setTimeout(flushCallbacks, 0) }
}
// 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
// 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0) // 拷贝一份 callbacks
callbacks.length = 0 // 清空 callbacks
for (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调
copies[i]()
}
}
// 为什么要拷贝一份 callbacks
// 用 callbacks.slice(0) 将 callbacks 拷贝出来一份,
// 是因为考虑到在 nextTick 回调中可能还会调用 nextTick 的情况,
// 如果在 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
// 而 nextTick 回调中的 nextTick 应该放在下一轮执行,
// 否则就可能出现一直循环的情况,
// 所以需要将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调
目前浏览器平台并没有实现 nextTick 方法,所以 Vue.js 源码中分别用 Promise、setTimeout、setImmediate 等方式在 microtask(或是task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。
nextTick的调用方式
- 回调函数方式:Vue.nextTick(callback)
- Promise方式:Vue.nextTick().then(callback)
- 实例方式:vm.$nextTick(callback)
标签:nextTick,Vue,DOM,回调,callbacks,原理,执行 From: https://www.cnblogs.com/tomxiao/p/16587739.html