当前位置: 代码迷 >> 综合 >> 【Vue 源码学习笔记】-- $nextTick
  详细解决方案

【Vue 源码学习笔记】-- $nextTick

热度:20   发布时间:2023-12-13 07:38:54.0

我们都知道Vue在视图更新采用的是异步更新策略,简单来说,Vue 在修改数据后,视图不会立刻更新,而是等同一事件中的所有数据变化完成之后,再统一进行视图更新。

官网给出的$nextTick的用法:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

说到更新,那肯定要提到 Watcher,我们来看源码:

Watcher:

// 以下是我精简过的代码
// 初始化时
this.deep = this.user = this.lazy = this.sync = false
...update () {if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {// vue是异步更新的所以正常是执行这一步queueWatcher(this)}}
...

queueWatcher

也就是当触发update更新的时候,会去执行queueWatcher方法:


let has = {};
let queue = [];
let waiting = false;
let flushing= false;// queueWatcher函数 部分代码
export function queueWatcher (watcher: Watcher) {// 防止queue队列wachter对象重复// 意思就是 去重 实现同一个数据多次修改,只触发一个Watcherconst id = watcher.idif (has[id] == null) {has[id] = trueif (!flushing) {queue.push(watcher)} else {let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}// queue the flushif (!waiting) {waiting = trueif (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue()return}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对象,queueWatche r函数做了一个去重操作,相同的 watcher 对象只会被加入到queue队列一次
  2. flushSchedulerQueue 函数依次调用了wacther对象的run方法执行更新,并作为回调传递给了nextTick函数。
  3. waiting 这个标记位代表我们是否已经向 nextTick 函数传递了更新任务,nextTick 会在当前更新任务结束后再去处理传入的回掉,只需要传递一次,更新完毕再重置这个标志位。
  4. waiting 变量,这是很重要的一个标志位,它保证 flushSchedulerQueue 回调只允许被置入callbacks一次。

$nextTick实现:


let callbacks = [];
let pending = false;
let timerFunc;/**----- nextTick -----*/
function nextTick (cb) {// 把传进来的回调函数放到callbacks队列里callbacks.push(cb);// pending代表一个等待状态 等这个tick执行if (!pending) {pending = truetimerFunc()}// 如果没传递回调 提供一个Promise化的调用if (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}
}/**----- timerFunc ----*/// 1、优先考虑Promise实现
if (typeof Promise !== 'undefined' && isNative(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 = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 3、降级到setImmediate实现timerFunc = () => {setImmediate(flushCallbacks)}
} else {
// 4、如果以上都不支持就用setTimeout来兜底了timerFunc = () => {setTimeout(flushCallbacks, 0)}
}function flushCallbacks () {// 将callbacks中的cb依次执行pending = falseconst copies = callbacks.slice(0)callbacks.length = 0for (let i = 0; i < copies.length; i++) {copies[i]()}
}
  1. 首先Vue通过callback数组来模拟事件队列,事件队里的事件,通过nextTickHandler方法来执行调用,而何时进行执行,是由timerFunc来决定的。
  2. timerFunc 是实现的核心,它会优先使用Promise等microtask,保证在同一个事件循环里面执行,这样页面只需要渲染一次。
  3. Promise --> MutationObserver--> setImmediate --> setTimeout,vue在这里用了降级处理的策略。
  相关解决方案