vue3、vue2中nextTick源码解析

04-19 9541阅读 0评论

nexttick是啥

nextTick是Vue提供的一个全局API,由于Vue的异步更新策略导致我们对数据的修改不会更新,如果此时想要获取更新后的Dom,就需要使用这个方法.

vue3、vue2中nextTick源码解析 第1张
(图片来源网络,侵删)

vue的异步更新策略意思是如果数据变化,vue不会立刻更新dom,而是开启一个队列,把组件更新函数保存在队列里,在统一事件循环中发生的所有数据变更会异步的批量更新,这一策略导致我们对数据的修改不会立即体现在dom上,此时如果想要获取更新后的dom状态,就要使用nexttick

nextTick所指定的回调会在浏览器更新DOM完毕之后再执行。即在一次事件循环中,更新了数据,把更新Dom的操作放入队列中,使用了nextTick,则把nextTick里的回调放入队列中,执行完所有的同步代码后,去执行微任务,即依次调用队列里的函数。

函数签名:

nextTick(this: T, fn?: (this: T) => R): Promise>:

this:不是参数,是ts中的一个语法,给 this 定义类型。给用于绑定回调函数中的 this 上下文,可以省略。

fn:要异步执行的回调函数,是一个函数,可以省略。

vue3、vue2中nextTick源码解析 第2张
(图片来源网络,侵删)

函数返回一个 Promise,Promise 的泛型为 Awaited,表示回调函数执行后的返回值。

使用场景:

  • created中想要获取dom时
  • 响应式数据变化后获取dom更新后的状态,比如希望获取列表更新后的高度
    vm.name = 'changed'
    vm.$nextTick(()=>{ // 要在更新数据的后面使用
      console.log(app.innerHTML)
    })
    

    vue2、vue3中nexttick源码

    简单来讲就是nexttick回调函数使用promise.then方式放入了异步,在所有dom都更新完成后才调用

    先上一个小例子,(来自https://b23.tv/AmCLtgx),

    async increment() {
    	this.count++;
    	//dom还未更新
    	console.log(document.getElementById('counter').textContent)//0
    	await nextTick();
    	//dom已经更新
    	conosle.log(document.getElementById('counter').textContent)//1
    

    我们先看,响应式数据count改变之后,会发生什么. (具体代码讲解放在代码注释里了,以下为vue3源码)

    1. 让与响应式数据相关连的函数去排队,调用queueJob()

      源码位置:(太长了,只放一部分)

      https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts

    // 为组件创建一个响应式效果,以便在组件的依赖项发生变化时触发重新渲染,换句话说,就是一个响应式数据改变后, 与它相关联的函数用什么方式去执行
    // create reactive effect for rendering
        const effect = (instance.effect = new ReactiveEffect(
          componentUpdateFn,//与响应式数据相关联的函数
          NOOP,//空函数,,表示没有特定的调度逻辑
          () => queueJob(update),//响应式数据改变后,不会立刻执行componentUpdateFn,而是让componentUpdateFn去排队,在未来某一时刻执行、
          instance.scope, // track it in component's effect scope
        ))
    
    1. 排队函数具体内容

      源码位置:

      https://github.com/vuejs/core/blob/main/packages/runtime-core/src/scheduler.ts

    queueJob方法: 把上一步中输入的参数update(也就是job)按特定规则推到queue任务队列里, 调用queueFlush()

    export function queueJob(job: SchedulerJob) {
      // the dedupe search uses the startIndex argument of Array.includes()
      // by default the search index includes the current job that is being run
      // so it cannot recursively trigger itself again.
      // if the job is a watch() callback, the search will start with a +1 index to
      // allow it recursively trigger itself - it is the user's responsibility to
      // ensure it doesn't end up in an infinite loop.
    //先检查queue数组是否为空,或者job是否已经包含在queue数组中。这个检查确保相同的job不会被多次排队。
      if (
        !queue.length ||
        !queue.includes(
          job,
          isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
        )
      ) {
        if (job.id == null) {//如果job无id属性,把job插入到queue数组末尾
          queue.push(job)
        } else {//如果job有id属性,就按照id把job插入到queue数组中特定位置
          queue.splice(findInsertionIndex(job.id), 0, job)
        }
        queueFlush()//刷新队列
      }
    }
    

    queueFlush方法: 把flushJobs(真正刷新队列的函数)放在promise.then里,等同步任务都完成后才真的刷新队列,再执行里面的更新函数

    function queueFlush() {//刷新队列方法
      if (!isFlushing && !isFlushPending) {//当前没有正在进行的刷新操作,并且没有待处理(被挂起)的刷新操作
        isFlushPending = true//表示有一个刷新操作待处理(被挂起)
        currentFlushPromise = resolvedPromise.then(flushJobs)
      }//注意这里是promise,属于微任务,所以会在未来某一时刻异步的执行,因此,当flushjob这个方法真正执行时,其实其他所有的同步代码都已经执行完了
    }
    

    flushjob方法:按特定顺序(从父组件更新到子组件遍历并执行任务队列queue中的任务, 并处理执行过程中的任何错误。

    function flushJobs(seen?: CountMap) {//循环遍历所有的组件更新函数,去更新所有的组件
      isFlushPending = false
      isFlushing = true
      if (__DEV__) {
        seen = seen || new Map()
      }
      // Sort queue before flush.
      // This ensures that:
      // 1. Components are updated from parent to child. (because parent is always
      //    created before the child so its render effect will have smaller
      //    priority number)
      // 2. If a component is unmounted during a parent component's update,
      //    its update can be skipped.
      queue.sort(comparator)//排序,确保组件从父组件更新到子组件,并允许跳过已卸载组件的更新。
      // conditional usage of checkRecursiveUpdate must be determined out of
      // try ... catch block since Rollup by default de-optimizes treeshaking
      // inside try-catch. This can leave all warning code unshaked. Although
      // they would get eventually shaken by a minifier like terser, some minifiers
      // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
      const check = __DEV__
        ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
        : NOOP
      try {//遍历作业队列。
        for (flushIndex = 0; flushIndex  
    

    nextTick: 获取queueFlush中用到的resolvedPromise, 用then方法执行nexttick的回调函数,

    export function nextTick(
      this: T,//确保回调函数fn在执行时具有正确的上下文
      fn?: (this: T) => R,
    ): Promise {
      const p = currentFlushPromise || resolvedPromise
      return fn ? p.then(this ? fn.bind(this) : fn) : p
    }//如果提供了回调函数 fn,则在 p 的 promise 对象上调用 then 方法。如果有可用的 this 上下文,则使用 bind 方法将 fn 函数绑定到 this 上下文。否则,直接使用 fn。
    //如果未提供回调函数(fn 为假值),则直接返回 p 的 promise 对象。
    

    回看一开始的例子,

    async increment() {
    	this.count++;//导致组件更新函数入队
    	//dom还未更新
    	console.log(document.getElementById('counter').textContent)//0
    	await nextTick();//导致下面所有代码封装成一个匿名函数,并放到刚才的组件更新函数后面,
    	//因此清空队列的时候,会先把所有的组件全清空后,才会执行nexttick后的延迟的语句或回调函数
    	//dom已经更新
    	conosle.log(document.getElementById('counter').textContent)//1
    

    vue2中netxtick源码位置:

    https://github.com/vuejs/vue/blob/main/src/core/util/next-tick.ts

    里面用到了优雅降级,可以看看,不多写了

    /* globals MutationObserver */
    import { noop } from 'shared/util'
    import { handleError } from './error'
    import { isIE, isIOS, isNative } from './env'
    export let isUsingMicroTask = false
    const callbacks: Array = []
    let pending = false
    function flushCallbacks() {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i = 9.3.3 when triggered in touch event handlers. It
    // completely stops working after triggering a few times... so, if native
    // Promise is available, we will use it:
    /* istanbul ignore next, $flow-disable-line */
    if (typeof Promise !== 'undefined' && isNative(Promise)) {//首先,判断是否原生支持Promise
      const p = Promise.resolve()
      timerFunc = () => {
        p.then(flushCallbacks)
        // In problematic UIWebViews, Promise.then doesn't completely break, but
        // it can get stuck in a weird state where callbacks are pushed into the
        // microtask queue but the queue isn't being flushed, until the browser
        // needs to do some other work, e.g. handle a timer. Therefore we can
        // "force" the microtask queue to be flushed by adding an empty timer.
        if (isIOS) setTimeout(noop)
      }
      isUsingMicroTask = true
    } else if (//其次 判断是否原生支持MutationObserver
      !isIE &&
      typeof MutationObserver !== 'undefined' &&
      (isNative(MutationObserver) ||
        // PhantomJS and iOS 7.x
        MutationObserver.toString() === '[object MutationObserverConstructor]')
    ) {
      // Use MutationObserver where native Promise is not available,
      // e.g. PhantomJS, iOS7, Android 4.4
      // (#6466 MutationObserver is unreliable in IE11)
      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 
      // Fallback to setImmediate.
      // Technically it leverages the (macro) task queue,
      // but it is still a better choice than setTimeout.
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {// 最后 都不支持的情况下 则使用setTimeout来兜底
      // Fallback to setTimeout.
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    // 将回调函数cb包装成一个箭头函数push到事件队列callbacks中
    export function nextTick(): Promise
    export function nextTick(this: T, cb: (this: T, ...args: any[]) => any): void
    export function nextTick(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、本网站所刊发、转载的文章,其版权均归原作者所有,如其他媒体、网站或个人从本网下载使用,请在
转载有关文章时务必尊重该文章的著作权,保留本网注明的“稿件来源”,并白负版权等法律责任。

手机扫描二维码访问

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
评论列表 (暂无评论,9541人围观)

还没有评论,来说两句吧...

目录[+]