• Vue2.0 —— Vue.nextTick(this.$nextTick)源码探秘


    Vue2.0 —— Vue.nextTick(this.$nextTick)源码探秘

    《工欲善其事,必先利其器》

    一、知识储备

    在学习这个 API 之前,我们需要进行一定量的知识储备,并且是从最基础的开始:

    1. nextTick,译为:下一个刻度,可理解为下一个事件,下一个要去做的事情;
    2. 浏览器的进程和线程,进程包含着线程,需理解多进程的概念;
    3. 了解 JavaScript 的事件循环机制,包括宏任务和微任务的区别。

    浏览器工作原理与实践
    (截图来自极客时间网站)

    首先说明,这里不是广告,也不是盗图。只是这位老师的课程讲的实在是特别好!强烈建议还没学习的小伙伴可以去学习这门课程,我希望看到的是大家一起进步,而不是得过且过。

    • 现代的浏览器分为多个模块的进程,它们之间互不干扰又部分通信共享,这种底层的技术称为 IPC (Inter Process Communication) ,译为:进程间通信,属于半双工通信,例如我们现实生活中的对讲机。
    • 现代浏览器属于多进程架构,分别有主进程、网络进程、渲染进程、GPU 进程和插件进程。其中,渲染进程运行在沙箱模式下,即:一个 Tabs 就代表一个渲染进程。我们熟知的 JavaScript 线程就运行在这个进程之中。
    • JavaScript 线程又将执行任务分为宏任务和微任务。在 JS 引擎工作的过程中,会产生一个 执行栈,里面用于执行宏任务;如果在宏任务执行的过程中遇见微任务,JS 引擎会将微任务提炼到 任务队列 中,当执行栈栈顶的宏任务执行完之后,在 GPU 渲染之前,执行任务队列中属于该宏任务的微任务。如此循环以往,称之为 事件循环机制
    • 宏任务有:主代码块、setTimeoutsetIntervalsetImmidiate以及 I/O流requestAnimationFrame
    • 微任务有:PromiseObject.observeMutationObserver 以及 process.nextTick(node)。

    好了,下面开始进入正题,话不多说,上号!

    上号

    二、为什么会有这个 API?

    由官方的解释引入:

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

    在之前我写过一篇文章 —— 《Vue2.0 —— 关于虚拟节点和 Diff 算法的浅析》 一文中提到,开发者们为了提升 SPA 的性能可谓是绞尽脑汁,不仅应用了虚拟节点的技术,还实现了 Diff 算法,目的就是提升更为优异的性能。最后我们得出的结论是:Vue 只会在执行完 Diff 算法 之后渲染一次 DOM。

    但你有没有想过,这与我们上面做的知识储备是否背道而驰?明明每次执行完宏任务,就会进行一次 GPU 渲染,那为什么官网还倡导我们在数据修改之后立即使用这个方法去获取更新后的 DOM 呢?

    那我们不妨大胆猜想,Vue 如果没有指定立即刷新视图(sync 关键字),那么他的 render 调用视图更新方法,极有可能就是异步的,而且是属于 微任务(事实上也的确如此,后面的源码会分析)。Fine,事情开始变的有趣了起来。

    搞笑

    三、使用方式

    • Vue.nextTick
    vm.msg = "Hello";
    // DOM 还没更新
    /**
     * {Function} callback
     * {Object} context
    */
    Vue.nextTick(function() {
    	// DOM 更新了
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这个方法属于全局应用,值得注意的是,如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不是原生支持 Promise (IE:你们都看我干嘛),你得自行 polyfill。

    • vm.$set
    new Vue({
      // ...
      methods: {
        // ...
        example: function () {
          // 修改数据
          this.message = 'changed'
          // DOM 还没有更新
          /**
    	   * {Function} callback
    	   * {Object} context
    	  */
          this.$nextTick(function () {
            // DOM 现在更新了
            // `this` 绑定到当前实例
            this.doSomethingElse()
          })
        }
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这个方法是应用在组件内的方式,与上面的全局方法在本质上实现并无二致,后者仅仅是前者的一个别名。

    四、源码探秘

    接下来是官方的原话:

    可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

    皇天不负有心人,我们上面的猜想被验证了。无独有偶,在 JavaScript 的发展历程中,鉴于 JS 单线程引擎的工作原理,我们的前辈也想要在浏览器主代码执行完之后、浏览器渲染之前,可以做一些操作。因此 JavaScript 的 事件循环机制 以及 宏任务微任务 的概念就诞生了。

    而我们的 Vue 框架,为此也向程序猿们提供了本文这个 API (Vue.nextTick)。并且,这个方法最终也成为了 Vue 渲染视图的主要手段,造成了异步更新 DOM 的现象,间接的提升了 Vue 框架的性能。

    • 第一,我们修改响应式数据之后,触发 dep.notify

    触发更新

    • 第二,Dep 利用观察者模式通知已经收集好的 Watcher 进行视图更新;

    执行更新

    • 第三,每个 Watcher 将自己推入到任务队列里面;

    推入队列

    • 第四,ShedulerWatcher 进行识别判断,如果属于同一 Watcher 则会被忽略;

    推入队列具体实现

    • 第五,重点来了:Vue.config.async 默认是 true,如果没有设置,那么 Vue 会利用 nextTick 方法,将 flushSchedulerQueue() 处理流函数,提取到异步任务队列之中。如果设置了,那么就是立即同步执行,这就是典型的,“异步渲染机制”。

    flushScheduleQueue函数
    flushScheduleQueue() 处理流函数用于执行 Watcherrun 回调方法,以及部分生命周期的更新,重置 Schedule 实例等。

    • 第六,才是我们今天的主题,Vue.nextTick
    // 执行微任务标识
    export let isUsingMicroTask = false
    // 储存任务数组
    const callbacks: Array<Function> = []
    // 执行标识,默认为false
    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]()
      }
    }
    // 声明回调函数集
    let timerFunc
    // Vue 默认使用 Promise 作为异步任务,上面分析过浏览器的差异,所以下面要判断其他方法
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      // 允许使用 Promise 浏览器情况下的回调函数赋值
      timerFunc = () => {
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
      }
      isUsingMicroTask = true
    } 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
      })
      // 允许使用 MO 浏览器情况下的回调函数赋值
      timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
      }
      isUsingMicroTask = true
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // 允许使用 setImmediate 浏览器情况下的回调函数赋值
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {
      // Fallback to setTimeout.
      // 摆烂情况下的赋值
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    
    export function nextTick(): Promise<void>
    export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
    export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
    /**
     * @internal
     */
    export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
      let _resolve
      // 这里就没啥了,回调函数加入数组
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e: any) {
            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
        })
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    捋一下吧:Vue 内部默认使用异步渲染机制,最终调用 nextTick 方法,这个 API 的作用就是,先是把所有的回调函数加入“回调函数集合”数组,再把不同浏览器可用的微任务做了一个判断、适配和循环赋值(flushCallbacks),最终添加完了之后,执行回调函数集合。

    当然了,这个 API 也是对外暴露的,任何开发者都可以使用。

    五、用例测试

    测试1
    输出:40,这个应该是没有什么问题对吧,我们再看一组。

    测试2
    这里即使 agenextTick 函数后面,但你前面已经执行修改 gender 触发了收集依赖,所以,微任务就会等主代码执行完之后,再执行回调,所以这里打印出来的,依然是更新之后的 DOM ,输出: 40。

    测试3
    同样,这里也会输出:40;即便你多次执行同一个 Watcher 的更新,Vue 会对其进行去重操作,并不会修改一次属性就更新一次视图,这部分是为了性能做的优化。

    测试4
    这个输出:20。因为 nextTick 的回调在异步渲染的回调之前执行,所以获取不到更新后的 DOM。

    最后,感谢你的阅读,愿你的未来一片光明~

  • 相关阅读:
    下载安装Microsoft ODBC Driver for SQL Server和配置SQL Server ODBC数据源
    docker模拟mysql 主从
    谷歌学术高级搜索输入框的含义
    小话 Spring AOP 源码
    分布式服务一篇概览
    【Vue2.x源码系列04】依赖收集原理(Dep、Watcher、Observer)
    K8S之Ingress 对外暴露应用(十四)
    java计算机毕业设计高校开放式实验室管理系统MyBatis+系统+LW文档+源码+调试部署
    【PowerQuery】使用PowerQuery实现DirectQuery模式的数据库刷新
    为什么说Java不适合做游戏开发,劣势在哪里?
  • 原文地址:https://blog.csdn.net/LizequaNNN/article/details/126957312