这是我参与更文挑战的第1天,活动详情查看: 更文挑战
注:以下是个人理解、如有不对还望指正!
nextTick使用?
方法描述:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM、也就意味着你在data初始化数据进行页面渲染成功后走的一个回调钩子、它支持在生命周期created、mounted或者任意方法内部使用
nextTick案例?
需求、进入页面我们通过一系列操作成功后显示一个滚动元素、然后对显示的元素进行滚动事件监听
//html
<div id="app">
<div id="name" v-if="show">滚动条内容测试</div>
<button @click="changeStatus">操作成功按钮</button>
</div>
//js
data(){
return{
show:false
}
},
methods:{
changeStatus(){
this.show = true;
//获取不到节点而报错
let dom = document.getElementById('name')
dom.addEventListener('scroll',(e) =>{ console.log(e) })
//使用nextTick方式获取dom进行监听
this.$nextTick( () =>{
let dom = document.getElementById('name')
dom.addEventListener('scroll',(e) =>{ console.log(e) })
})
}
}
复制代码
上面案例我们知道当我们去修改data的show状态值、立刻在同步代码执行获取dom、发现我们并不能马上获取、这是因为vue不会即时进行更新dom、而是把需要更新dom的操作存放在异步更新的队列、试想如果同步每次一修改就去更新操作、堵塞同步代码、页面进行多次渲染、很浪费性能 当然异步也会有其他问题的引入、vue也给我们提供了dom更新后的钩子$nextTick为了就是解决异步更新获取dom的操作
nextTick思考?
这里我们引入一段nextTick实现的思考、为什么他这个API就能实现dom更新后进行触发呢??有没有其他的原生方法呢、答案是:有的
setTimeout( () =>{
console.log('setTimeout' ,document.getElementById('name') )
let dom = document.getElementById('name')
dom.addEventListener('scroll',(e) =>{ console.log(e) })
})
复制代码
发现也能正常获取到dom、这是什么原理呢?下面我们开始nextTick的源码分析
nextTick源码分析?
定位到源码的/src/core/util/next-tick.js 我们把核心的粘贴出来
- 保存nextTick里面的回调方法集合、每次页面渲染后去清空执行这些回调
const callbacks = []
//定义一个状态标记、确保每次更新后只会有一个flushCallbacks函数在运行
let pending = false
复制代码
- 执行清空操作
function flushCallbacks () {
//初始化标记
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
复制代码
- 核心、利用eventLoop原理去判断环境、执行清空方法
//优先判断是否存在Promise、利用微任务去执行清空回调
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
//没有promsie的情况、利用h5提供给我们dom变化后的回调去实现
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
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
//还没有就判断 setImmediate是否有、作者说setImmediate依然比setTimeout更好
//该方法用于分解长时间运行的操作,并在浏览器“完成其他操作“(后立即运行回调函数
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
// 最后才去用setTimeout
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
复制代码
通过源码我们发现、vue利用js事件循环机制“eventLoop“原理去做的这一个操作、所以为什么我们在代码里面加入setTimeout也能获取到dom的原因
疑惑?
那为什么vue能过准确的知道什么时候更新好了呢、顺着源码往下看、
在/src/core/observer/scheduler.js有一段
// queue 队列
export function queueWatcher (watcher: Watcher) {
//...忽略
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
//执行了nextTick、那么就有可能触发flushCallbacks回调
nextTick(flushSchedulerQueue)
}
}
}
复制代码
这里他把需要更新的组件watcher队列方法丢到nextTick、利用队列先进先出原理、等组件执行更新完后在执行nextTick剩下开发者定义的钩子
总结
大概流程是、组件里面执行方法、第一行代码去修改data变量、修改后会进入vue的Set方法触发dep.notify方法、通知收集的watcher去执行update方法、update方法会把自己放入watcher队列、然后把队列放入nextTick钩子这个时候会去判断nextTick里面是否已经有在执行的flushCallbacks、如果没有则会触发next-tick.js的timerFunc执行(此时执行会把callbacks放到微任务或者宏任务、然后回到我们的代码第一行结尾,继续往下执行、微或宏暂时放入队列等待方法同步执行完后调用、这个时候就是执行更改data数据下一行代码、this.$nextTick(…)、又往callbacks添加了一个我们自己定义的回调方法、记住此时里面还有一个组件更新的方法还没开始执行、他是比我们先进队列所以先执行、所以为什么我们能在下一个回调里面拿到dom元素、因为这个时候vue已经把dom更新上去了才去执行剩下的callback方法、所以利用这个规则其实我们在自己代码里面写Promise.resolve().then( () => { …获取dom } ,也能拿到dom!