异步更新队列 - Vue2 响应式

news2024/9/24 17:08:21

前言

这篇文章分析了 Vue 更新过程中使用的异步更新队列的相关代码。通过对异步更新队列的研究和学习,加深对 Vue 更新机制的理解

什么是异步更新队列

先看看下面的例子:

<div id="app">
        <div id="div" v-if="isShow">被隐藏的内容</div>
        <input @click="getDiv" value="按钮" type="button">
    </div>
  <script>

    let vm = new Vue({
        el: '#app',
        data: {
        //控制是否显示#div
        isShow: false
        },
        methods:{
        getDiv: function () {
            this.isShow=true
            var content = document.getElementById('div').innerHTML;
            console.log('content',content)
        }
        }
    })
</script>
  • 上面的例子是,点击按钮显示被隐藏的 div,同时打印 div 内部 html 的内容。
  • 按照我们一般的认知,应该是点击按钮能够显示 div 并且在控制台看到 div 的内部 html 的内容。

但是实际执行的结果确是,div 可以显示出来,但是打印结果的时候会报错,错误原因就是 innerHTML 为 null,也就是 div 不存在。
只有当我们再次点击按钮的时候才会打印出 div 里面的内容。这就是 Vue 的异步更新队列的结果

异步更新队列的概念

Vue 的 dom 更新是异步的,当数据发生变化时 Vue 不是立刻去更新 dom,而是开启一个队列,并缓冲在同一个事件中循环发生的所有数据变化。
在缓冲时,会去除重复的数据,避免多余的计算和 dom 操作。在下一个事件循环 tick 中,刷新队列并执行已去重的工作。

  • 所以上面的代码报错是因为当执行 this.isShow=true 时,div 还未被创建出来,知道下次 Vue 事件循环时才开始创建

  • 查重机制降低了 Vue 的开销

  • 异步更新队列实现的选择:由于浏览器的差异,Vue 会根据当前环境选择 Promise.then 或者 MuMutationObserver,如果两者都不支持,则会用 setImmediate 或者 setTimeout 代替

异步更新队列解析

异步队列源码入口

通过之前对 Vue 数据响应式的分析我们知道,当 Vue 数据发生变化时,会触发 dep 的 notify() 方法,该方法通知观察者 watcher 去更新 dom,我们先看一下这的源码

  • from src/core/observer/dep.js
//直接看核心代码
    notify () {
        //这是Dep的notify方法,Vue的会对data数据进行数据劫持,该方法被放到data数据的set方法中最后执行
        //也就是通知更新操作
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
        subs.sort((a, b) => a.id - b.id)
        }
        for (let i = 0, l = subs.length; i < l; i++) {
        // !!!核心:通知watcher进行数据更新
        //这里的subs[i]其实是Dep维护的一个watcher数组,所以我们下面是执行的watcher中的update方法
        subs[i].update()
        }
  }
  • 上面的代码简单来说就是 dep 通知 watcher 尽心更新操作,我们看一下 watcher 相关的代码
    from :src/core/observer/watcher.js
//这里只展示部分核心代码
    //watcher的update方法
    update () {
        /* istanbul ignore else */
        //判断是否存在lazy和sync属性
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
        this.run()
        } else {
        //核心:将当前的watcher放到一个队列中
        queueWatcher(this)
        }
    }
  • 上面 watcher 的 update 更新方法简单来说就是调用了一个 queueWatcher 方法,这个方法其实是将当前的 watcher 实例放入到一个队列中,以便完成后面的异步更新队列操作

异步队列入队

下面看看 queueWatcher 的逻辑 from src/core/observer/scheduler.js

export function queueWatcher (watcher: Watcher) {
    const id = watcher.id
    //去重的操作,先判断是否在当前队列中存在,避免重复操作
    if (has[id] == null) {
        has[id] = true
        if (!flushing) {
        queue.push(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 - 1
        while (i > index && queue[i].id > watcher.id) {
            i--
        }
        queue.splice(i + 1, 0, watcher)
        }
        // queue the flush
        if (!waiting) {
        waiting = true

        if (process.env.NODE_ENV !== 'production' && !config.async) {
            flushSchedulerQueue()
            return
        }
        // 启动异步任务(刷新当前的计划任务)
        nextTick(flushSchedulerQueue)
        }
    }
    }
  • 上面这段 queueWatcher 的代码的主要作用就是对任务去重,然后启动异步任务,进行跟新操作。接下来我们看一线 nextTick 里面的操作

from src/core/util/next-tick.js

//cb:
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  //callbacks:这个方法维护了一个回调函数的数组,将回调函数添家进数组
  callbacks.push(() => {
      //添加错误处理
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    //启动异步函数
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
  • 这里的核心,其实就在 timerFunc 的函数上,该函数根据不同的运行时环境,调用不同的异步更新队列,下面看一下代码

from src/core/util/next-tick.js

/**这部分逻辑就是根据环境来判断timerFunc到底是使用什么样的异步队列**/
    let timerFunc
    //首选微任务执行异步操作:Promise、MutationObserver
    //次选setImmediate最后选择setTimeout
    // 根据当前浏览器环境选择用什么方法来执行异步任务
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
        //如果当前环境支持Promise,则使用Promise执行异步任务
        const p = Promise.resolve()
        timerFunc = () => {
            //最终是执行的flushCallbacks方法
            p.then(flushCallbacks)
            //如果是IOS则回退,因为IOS不支持Promise
            if (isIOS) setTimeout(noop)
        }
        //当前使用微任务执行
        isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
        //如果当前浏览器支持MutationObserver则使用MutationObserver
        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
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
        //如果支持setImmediate,则使用setImmediate
        timerFunc = () => {
            setImmediate(flushCallbacks)
        }
    } else {
        //如果上面的条件都不满足,那么最后选择setTimeout方法来完成异步更新队列
        timerFunc = () => {
            setTimeout(flushCallbacks, 0)
        }
    }
  • 从上面代码可以看出,不论 timerFunc 使用的是什么样的异步更新队列,最终执行的函数还是落在了 flushCallbacks 上面,那么我们来看一看,这个方法到底是什么

from src/core/util/next-tick.js

function flushCallbacks () {
        pending = false
        //拷贝callbacks数组内容
        const copies = callbacks.slice(0)
        //清空callbacks
        callbacks.length = 0
        //遍历执行
        for (let i = 0; i < copies.length; i++) {
            //执行回调方法
            copies[i]()
        }
    }
  • 上面的这个方法就是遍历执行了我们 nextTick 维护的那个回调函数数组, 其实就是将数组的方法依次添加进异步队列进行执行。同时清空 callbacks 数组为下次更新作准备。

上面这几段代码其实都是 watcher 的异步队列更新中的入队操作,通过 queueWatcher 方法中调用的 nextTick(flushSchedulerQueue), 我们知道,其实是将 flushSchedulerQueue 这个方法入队

异步队列的具体更新方法

所以下面我们看一下 flushSchedulerQueue 这个方法到底执行了什么操作

from src/core/observer/scheduler.js

/**我们这里只粘贴跟本次异步队列更新相关的核心代码**/
    //具体的更新操作
function flushSchedulerQueue () {
    currentFlushTimestamp = getNow()
    flushing = true
    let watcher, id
    //重新排列queue数组,是为了确保:
    //更新顺序是从父组件到子组件
    //用户的watcher先于render 的watcher执行(因为用户watcher先于render watcher创建)
    //当子组件的watcher在父组件的watcher执行时被销毁,则跳过该子组件的watcher
    queue.sort((a, b) => a.id - b.id)
    //queue数组维护的一个watcher数组
    //遍历queue数组,在queueWatcher方法中我们将传入的watcher实例push到了该数组中
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
            watcher.before()
        }
        id = watcher.id
        //清空has对象里面的"id"属性(这个id属性之前在queueWatcher方法里面查重的时候用到了)
        has[id] = null
        //核心:最终执行的其实是watcher的run方法
        watcher.run()
        //下面是一些警告提示,可以先忽略
        if (process.env.NODE_ENV !== 'production' && has[id] != null) {
            circular[id] = (circular[id] || 0) + 1
            if (circular[id] > MAX_UPDATE_COUNT) {
                warn(
                    'You may have an infinite update loop ' + (
                        watcher.user
                        ? `in watcher with expression "${watcher.expression}"`
                        : `in a component render function.`
                    ),
                    watcher.vm
                )
                break
            }
        }
    }
    //调用组件updated生命周期钩子相关,先跳过
    const activatedQueue = activatedChildren.slice()
    const updatedQueue = queue.slice()

    resetSchedulerState()

    callActivatedHooks(activatedQueue)
    callUpdatedHooks(updatedQueue)
        if (devtools && config.devtools) {
            devtools.emit('flush')
        }
}
  • 上面的一堆 flushSchedulerQueue 代码,简单来说就是排列了 queue 数组,然后遍历该数组,执行 watcher.run 方法。所以,异步队列更新当我们入队完以后,真正执行的方法其实是 watcher.run 方法

下面我们来继续看一下 watcher.run 方法,到底执行了什么操作

from src/core/observer/watcher.js

/**
   * Scheduler job interface.
   * Will be called by the scheduler.
   * 上面这段英文注释 是官方注释,从这我们看出该方法最终会被scheduler调用
   */
  run () {
    if (this.active) {
        //这里调用了watcher的get方法
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
  • 上述 run 方法最终要的操作就是调用了 watcher 的 get 方法,该方法我们在之前的源码分析有讲过,主要实现的功能是调用了 data 数据的 get 方法,获取最新数据。

至此,Vue 异步更新队列的核心代码我们就分析完了,为了便于理清思路,我们来一张图总结一下

关于 Vue.$nextTick

我们都知道 . n e x t T i c k 方 法 , 其 实 这 个 ∗ ∗ .nextTick 方法,其实这个 ** .nextTick 方法,其实这个∗∗nextTick** 方法就是直接调用的上面的 nextTick 方法

from src/core/instance/render.js

Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  • 由上面的代码我们可以看出,$nextTick 是将我们传入的回调函数加入到了异步更新队列,所以它才能实现 dom 更新后回调

注意,$nextTick() 是会将我们传入的函数加入到异步更新队列中的,但是这里有个问题,如果我们想获得 dom 更新后的数据,我们应该把该逻辑放到更新操作之后
因为加入异步队列先后的问题,如果我们在更新数据之前入队的话 ,是获取不到更新之后的数据的

总结

总结起来就是,当触发数据更新通知时,dep 通知 watcher 进行数据更新,这时 watcher 会将自己加入到一个异步的更新队列中。然后更新队列会将传入的更新操作进行批量处理。
这样就达到了多次更新同时完成,提升了用户体验,减少了浏览器的开销,增强了性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/890356.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

有哪些好用的设计图工具?

设计图纸制作软件是高级学习数字设计的最佳选择&#xff0c;无论你是想通过设计图纸制作软件创建一个明亮的设计&#xff0c;还是与其他设计师分享和交流。本文将介绍十个易于使用的设计图纸制作软件&#xff0c;其中大多数是初学者和高级艺术家&#xff0c;具有完整的绘图、照…

科研论文配图绘制指南——基于Python—第一章

目录 第一章1.1科研论文配图的绘制基础1.2科研论文配图的配色基础1.2.1 色轮配色原理1.2.3 颜色主题1.2.4 配色工具 总结 第一章 1.1科研论文配图的绘制基础 科研配图包括线性图、灰度图、照片彩图和综合配图四种类型&#xff0c;最经常使用为线型图。 建议使用EPS、PDF等矢量…

C语言:每日一练(选择+编程)

目录 选择题&#xff1a; 题一&#xff1a; 题二&#xff1a; 题三&#xff1a; 题四&#xff1a; 题五&#xff1a; 编程题&#xff1a; 题一&#xff1a;打印1到最大的n位数 示例1 思路一&#xff1a; 题二&#xff1a;计算日期到天数转换 示例1 思路一&#xf…

SwiftUI 动画进阶:实现行星绕圆周轨道运动

0. 概览 SwiftUI 动画对于优秀 App 可以说是布帛菽粟。利用美妙的动画我们不仅可以活跃界面元素,更可以单独打造出一整套生动有机的世界,激活无限可能。 如上图所示,我们用动画粗略实现了一个小太阳系:8大行星围绕太阳旋转,而卫星们围绕各个行星旋转。 在本篇博文中,您将…

华为网络篇 RIP路由标记-31

难度2复杂度2 目录 一、实验原理 二、实验拓扑 三、实验步骤 四、实验过程 总结 一、实验原理 路由标记tag是用于进行路由过滤的&#xff0c;它给相关的路由打标记&#xff0c;然后应用于路由策略中&#xff0c;路由器会根据策略进行路由过滤。比如&#xff0c;给静态路由…

题目:售货员的难题(状压dp)

售货员的难题 题目描述输入输出格式输入格式&#xff1a;输出格式&#xff1a; 输入输出样例输入样例#1&#xff1a;输出样例#1&#xff1a; 思路AC代码&#xff1a; 题目描述 某乡有n个村庄( 1 < n < 16 )&#xff0c;有一个售货员&#xff0c;他要到各个村庄去售货&am…

Smartbi 李代:人尽其才、数尽其用,Smartbi Eagle智慧数据运营平台全新亮相

数据是企业数字化转型的基石&#xff0c;也是赢得未来的核心资产和竞争力。数字化转型的关键&#xff0c;是在全公司建立一种数据驱动的组织和机制&#xff0c;营造数据文化的氛围&#xff0c;让更多的用户、在更多的场景中&#xff0c;有意愿、有能力使用数据&#xff0c;从而…

【C++】面向对象编程引入 ③ ( 面向过程编程的结构化程序设计方法 | 结构化程序设计方法概念 / 特点 / 优缺点 | 面向对象编程引入 )

文章目录 一、面向过程编程的结构化程序设计方法1、结构化程序设计方法概念2、结构化程序设计方法特点3、结构化程序设计方法优缺点 二、面向对象编程引入 一、面向过程编程的结构化程序设计方法 如果使用 面向过程语言 ( 如 : C 语言 ) , 开发 大型 项目 , 一般使用 结构化程序…

Apache SeaTunnel社区迎来新Committer!

采访&编辑 | Debra Chen 个人简介 姓名&#xff1a;马骋原公司&#xff1a;恒生电子 GitHub ID&#xff1a;rewerma个人擅长研究领域&#xff1a;java中间件、微服务、大数据等 您为社区提交了什么贡献&#xff1f;具体方案可以描述一下吗&#xff1f; 为SeatTunnel提交…

案例21 基于Spring Boot+Redis实现图书信息按书号存储案例

1. 案例需求 基于Spring BootRedis实现图书信息按书号存储和取出功能&#xff0c;数据存储至Redis。 2. 创建Spring Boot项目 创建Spring Boot项目&#xff0c;项目名称为springboot-redis02。 3. 选择依赖 ​ pom.xml文件内容如下所示&#xff1a; <?xml version&quo…

入门Web自动化测试之元素定位的配置管理

之前我们讲过Selenium使用教程&#xff0c;这一篇我们来学习元素定位的配置管理。 目的 Web自动化测试作为软件自动化测试领域中绕不过去的一个“香饽饽”&#xff0c;通常都会作为广大测试从业者的首选学习对象&#xff0c;相较于C/S架构的自动化来说&#xff0c;B/S有着其无…

AI巨浪下,数据技术如何驱动智能未来?

引言 数据技术是大数据时代的核心驱动力&#xff0c;也是推动各行各业数字化转型和智能化升级的关键因素。随着云计算、人工智能、区块链等新兴技术的不断发展和融合&#xff0c;数据技术也呈现出多模态、混合处理、自动化管理等新的趋势和特点。 8 月 19 日&#xff08;周六&…

UniApp 制作高德地图插件

1、下载Uni插件项目 在Uni官网下载Uni插件项目&#xff0c;并参考官网插件项目创建插件项目. 开发者须知 | uni小程序SDK 如果下载下来项目运行不了可以参考下面链接进行处理 UniApp原生插件制作_wangdaoyin2010的博客-CSDN博客 2、引入高德SDK 2.1 在高德官网下载对应SD…

e6zzseo:跨境独立站还能做起来吗?

跨境独立站指的是在其他国家或地区创建和运营自己的电子商务网站。虽然跨境独立站在理论上是可行的&#xff0c;但成功实施和运营它可能面临一些挑战。以下是e6zzseo分析的一些考虑因素和建议&#xff0c;以帮助你更好地评估是否可以成功运营跨境独立站&#xff1a; 做跨境独立…

【0基础入门Python笔记】python 之基础语法、基础数据类型、复合数据类型及基本操作

python 基础&#xff08;一&#xff09; 基础语法规则基础数据类型数字类型&#xff08;Numbers&#xff09;字符串类型&#xff08;String&#xff09;布尔类型&#xff08;Boolean&#xff09; 复合数据类型List&#xff08;列表&#xff09;Tuple&#xff08;元组&#xff0…

excel常见的数学函数篇2

二、数学函数 1、ABS(number)&#xff1a;返回数字的绝对值 语法&#xff1a;ABS(数字)&#xff1b;返回数字的绝对值&#xff1b;若引用单元格&#xff0c;把数字换为单元格地址即可 2、INT(number)&#xff1a;向小取整 语法&#xff1a;INT(数字)&#xff1b;若引用单元格…

自夹持P型屏蔽型碳化硅沟槽型绝缘栅双极晶体管,用于低开通电压和开关损耗

目录 标题&#xff1a;Self-Clamped P-shield SiC Trench IGBT for Low On-State Voltage and Switching LossProceedings of the 35st International Symposium on Power Semiconductor Devices & ICs摘要信息解释研究了什么文章的创新点文章的研究方法文章的结论 标题&am…

为什么要报11月份的PMP考试?一篇说清楚!

各位PMP考生即将迎来8.19的考试&#xff0c;现在心里难免会有点焦虑&#xff0c;相信大家在系统的学习完PMP课程之后&#xff0c;都能顺利上岸&#xff0c;3A通过&#xff01; 另外PMP11月份的考试正在报名当中&#xff01;大家尽量提前报名&#xff0c;给自己留充足的时间备考…

夏威夷等全球多地深陷「末日狂烧」,关键时刻 AI 监测能否跑赢野火?

内容一览&#xff1a;当地时间 8 月 8 日&#xff0c;美国夏威夷州突发野火&#xff0c;当地居民和游客不得不跳入太平洋中躲避火势。截至 8 月 17 日&#xff0c;这场野火已经造成110 人死亡&#xff0c;超过 1000人失踪。与此同时&#xff0c;美国、加拿大、法国等地也正遭遇…

GPU短缺:人工智能行业的可持续发展问题

原创 | 文 BFT机器人 2023年8月&#xff0c;人工智能似乎会受到GPU供应的瓶颈。 “人工智能热潮被低估的一个原因是GPU/TPU短缺。这种短缺导致了产品推出和模型培训的各种限制&#xff0c;但这些都不明显。相反&#xff0c;我们看到的是英伟达的股价飙升。一旦供给满足需求&am…