nextTick源码解读

news/2024/11/15 5:57:06/

📝个人主页:爱吃炫迈
💌系列专栏:Vue
🧑‍💻座右铭:道阻且长,行则将至💗

文章目录

  • nextTick原理
    • nextTick
    • timerFunc
    • flushCallbacks
  • 异步更新流程
    • update
    • queueWatcher
    • flushSchedulerQueue
    • resetSchedulerState


nextTick原理

nextTick

export let isUsingMicroTask = false // 标记 nextTick 最终是否以微任务执行
/*存放异步执行的回调*/
const callbacks = [] 
/*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc/*推送到队列中下一个tick时执行cb 回调函数ctx 上下文
*/
export function nextTick (cb?: Function, ctx?: Object) {let _resolve// 第一步 传入的cb会被push进callbacks中存放起来callbacks.push(() => {if (cb) {      try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})// 第二步:判断用什么方法// 检查上一个异步任务队列(即名为callbacks的任务数组)是否派发和执行完毕了。pending此处相当于一个锁if (!pending) {// 若上一个异步任务队列已经执行完毕,则将pending设定为true(把锁锁上)pending = true// 调用判断Promise,MutationObserver,setTimeout的优先级timerFunc()}// 第三步:nextTick 函数会返回一个Promise对象。该Promise对象在异步任务执行完毕后会resolve,可以让用户在异步任务执行完毕后进行处理。if (!cb && typeof Promise !== 'undefined') {   return new Promise(resolve => {_resolve = resolve})}
}

解释

第二步pending 的作用就是一个锁,防止后续的 nextTick 重复执行 timerFunc(换句话说:当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次)。timerFunc 内部创建会一个微任务或宏任务,等待所有的 nextTick 同步执行完成后,再去执行 callbacks 内的回调。

timerFunc

💡 timerFunc函数,主要通过一些兼容判断来创建合适的 timerFunc,最优先肯定是微任务,其次再到宏任务。 优先级为 promise.then > MutationObserver > setImmediate > setTimeout

// 判断当前环境是否原生支持 promise
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 支持 promiseconst p = Promise.resolve()timerFunc = () => {// 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务p.then(flushCallbacks)if (isIOS) setTimeout(noop)}// 标记当前 nextTick 使用的微任务isUsingMicroTask = true// 如果不支持 promise,就判断是否支持 MutationObserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||MutationObserver.toString() === '[object MutationObserverConstructor]')) {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 // 标记当前 nextTick 使用的微任务// 判断当前环境是否原生支持 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {timerFunc = () => {setImmediate(flushCallbacks)}// 以上三种都不支持就选择 setTimeout
} else {timerFunc = () => {setTimeout(flushCallbacks, 0)}
}

我们发现无论那种timerFunc 最终都会执行flushCallbacks 函数

flushCallbacks

💡 flushCallbacks 里做的事情很简单,它就负责执行 callbacks 里的回调。

// flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
function flushCallbacks() {pending = falseconst copies = callbacks.slice(0) // 拷贝一份 callbackscallbacks.length = 0 // 清空 callbacksfor (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调copies[i]()}
}

image-20230929174246795

异步更新流程

Vue 使用异步更新,等待所有数据同步修改完成后,再去执行更新逻辑。

update

💡
触发某个数据的setter方法后,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象。触发Watch对象的update实现。

/*调度者接口,当依赖发生改变的时候进行回调 */update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true} else if (this.sync) {/*同步则执行run直接渲染视图*/this.run()} else {/*异步推送到观察者队列中,下一个tick时调用。*/queueWatcher(this) // this为当前实例watcher}}

queueWatcher

💡 将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送

export function queueWatcher (watcher: Watcher) {/*获取watcher的id*/const id = watcher.id/*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/if (has[id] == null) {has[id] = true// 不是刷新if (!flushing) {queue.push(watcher)  // 将多个渲染watcher去重后放到队列中} else {// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.let i = queue.length - 1while (i >= 0 && queue[i].id > watcher.id) {i--}queue.splice(Math.max(i, index) + 1, 0, watcher)}// 是刷新if (!waiting) {waiting = truenextTick(flushSchedulerQueue) //这里会产生一个nextTick,队列刷新函数(flushSchedulerQueue)}}
}

从queueWatcher代码中看出Watch对象并不是立即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等到下一个tick运行时将这个队列queue全部拿出来run一遍,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被多次加入到queue中去。这也解释了同一个watcher被多次触发,只会被推入到队列中一次。

flushSchedulerQueue

💡 flushSchedulerQueue 内将刚刚加入 queuewatcher 逐个 run 更新。

function flushSchedulerQueue () {currentFlushTimestamp = getNow()flushing = truelet watcher, id// 在刷新之前对队列进行排序。// 这确保了:// 1. 组件从父级更新到子级。(因为父母总是在子进程之前创建)// 2. 组件的用户观察程序在其渲染观察程序之前运行(因为用户观察者是在渲染观察者之前创建的)// 3. 如果组件在父组件的观察程序运行期间被销毁,可以跳过它的观察者。queue.sort((a, b) => a.id - b.id)// do not cache length because more watchers might be pushed// as we run existing watchersfor (index = 0; index < queue.length; index++) {watcher = queue[index]if (watcher.before) {watcher.before()}id = watcher.idhas[id] = nullwatcher.run()}// keep copies of post queues before resetting stateconst activatedQueue = activatedChildren.slice()const updatedQueue = queue.slice()resetSchedulerState()// call component updated and activated hookscallActivatedHooks(activatedQueue)callUpdatedHooks(updatedQueue)
}

resetSchedulerState

💡 resetSchedulerState 重置状态,等待下一轮的异步更新。

function resetSchedulerState () {index = queue.length = activatedChildren.length = 0has = {}if (process.env.NODE_ENV !== 'production') {circular = {}}waiting = flushing = false
}

要注意此时 flushSchedulerQueue 还未执行,它只是作为回调传入而已。因为用户可能也会调用 nextTick 方法。这种情况下,callbacks 里的内容为 [“flushSchedulerQueue”, “用户的nextTick回调”],当所有同步任务执行完成,才开始执行 callbacks 里面的回调。

由此可见,最先执行的是页面更新的逻辑,其次再到用户的 nextTick 回调执行。这也是为什么我们能在 nextTick 中获取到更新后DOM的原因。

参考文章:

Vue你不得不知道的异步更新机制和nextTick原理 - 掘金

通俗易懂的Vue异步更新策略及 nextTick 原理 - 掘金


http://www.ppmy.cn/news/1129887.html

相关文章

【Linux指令集】---git命令的基本使用

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【Linux专栏】&#x1f388; 本专栏旨在分享学习Linux的一点学习心得&#xff0c;欢迎大家在评论区讨论&#x1f48c; 演示环境&#xff1…

postgresql|数据库|数据库测试工具pgbench之使用

前言&#xff1a; 数据库是项目中的重要组件&#xff0c;也是一个基础的重要组件&#xff0c;其地位说是第一我想应该是没有什么太多问题的。 那么&#xff0c;数据库的设计这些方面是不用多说的&#xff0c;关键的第一步&#xff0c;主要是涉及数据库的部署方式&#xff0c;…

C++:vector 定义,用法,作用,注意点

C 中的 vector 是标准模板库&#xff08;STL&#xff09;提供的一种动态数组容器&#xff0c;它提供了一组强大的方法来管理和操作可变大小的数组。以下是关于 vector 的定义、用法、作用以及一些注意点&#xff1a; 定义&#xff1a; 要使用 vector&#xff0c;首先需要包含 …

3 OpenCV两张图片实现稀疏点云的生成

前文&#xff1a; 1 基于SIFT图像特征识别的匹配方法比较与实现 2 OpenCV实现的F矩阵RANSAC原理与实践 1 E矩阵 1.1 由F到E E K T ∗ F ∗ K E K^T * F * K EKT∗F∗K E 矩阵可以直接通过之前算好的 F 矩阵与相机内参 K 矩阵获得 Mat E K.t() * F * K;相机内参获得的方式…

2023年【安徽省安全员C证】模拟考试题及安徽省安全员C证实操考试视频

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年【安徽省安全员C证】模拟考试题及安徽省安全员C证实操考试视频&#xff0c;包含安徽省安全员C证模拟考试题答案和解析及安徽省安全员C证实操考试视频练习。安全生产模拟考试一点通结合国家安徽省安全员C证考试最…

Scala第六章节

Scala第六章节 scala总目录 章节目标 掌握类和对象的定义掌握访问修饰符和构造器的用法掌握main方法的实现形式掌握伴生对象的使用掌握定义工具类的案例 1. 类和对象 Scala是一种函数式的面向对象语言, 它也是支持面向对象编程思想的&#xff0c;也有类和对象的概念。我们依…

【网络协议】TCP

TCP协议全称为传输控制协议(Transmission Control Protocol).要理解TCP就要从他的特性开始说&#xff0c;这些特性各自之间或多或少各有联结&#xff0c;需要以宏观视角来看待。 目录&#xff1a; 1.TCP报文格式 因为报文解释过于繁琐&#xff0c;具体内容请看这篇文章TCP报文…

前端开发 vs. 后端开发:编程之路的选择

文章目录 前端开发&#xff1a;用户界面的创造者1. HTML/CSS/JavaScript&#xff1a;2. 用户体验设计&#xff1a;3. 响应式设计&#xff1a;4. 前端框架&#xff1a; 后端开发&#xff1a;数据和逻辑的构建者1. 服务器端编程&#xff1a;2. 数据库&#xff1a;3. 安全性&#…