当我们在用$nextTick时,我们到底在用什么?

537 阅读1分钟

前言:去年刷知乎的时候,看到一篇面经里有个问题是问vue的nextTick原理。猛然想到,我虽然经常使用这个方法,但一直知其然不知其所以然,于是扒了一下源码并着手写这篇文章作为记录。不过,因工作和生活上接连有事,一直到今天才修改整理出来。如有错漏,欢迎在评论区指正

一、 nextTick的作用

this.$nextTick(cb)将回调延迟到下次 DOM 更新之后执行。

首先,我们知道vue的一大特点是响应式,它的更新方式是异步进行dom更新,如果一个 watcher 被多次触发,只有最后一次有效。这种方式可以去除重复数据和不必要的dom操作,本是一个很好的优化,但某些时候如果我们需要获取修改后的数据或获取更新后的dom时就会出现一些问题。比如前几天群里有个同学问:“为什么用refs取不到dom?”。截图只有一行代码,信息极少,但如果代码不出错,大概率就是上述原因导致。遂推荐其套个nextTick,解决。

二、 nextTick的原理

nextTick源码较为简单,这里按源码结构一步步分析。

nextTick.js文件主要成员有六个,其中三个数据:isUsingMicroTask ,callbacks ,pending ,三个方法:flushCallbacks,timerFunc,nextTick 。

布尔值isUsingMicroTask 作为导出数据这里只修改未使用;数组callbacks 顾名思义,就是回调函数的集合;布尔值pending 为等待状态。

flushCallbacks为刷新执行所有回调函数的方法。

function flushCallbacks () {
  pending = false							//放掉等待状态
  const copies = callbacks.slice(0)					//复制回调数组
  callbacks.length = 0							//重置回调数组
  for (let i = 0; i < copies.length; i++) {				//循环执行回调
    copies[i]()
  }
}

timerFunc为异步延迟包装方法。定义触发flushCallbacks方法的任务方式(根据运行环境选择宏任务或者微任务)。如果当前环境支持微任务Promise或者MutationObserver ,优先使用微任务。如果不支持则改用宏任务setImmediate 或setTimeout实现。这与event loop的运行机制有关,在我的理解中,event loop的每轮tick结束后(微任务队列执行完),浏览器即执行render,然后开始下一个宏任务。如果使用宏任务,才真正代表着“在dom更新后”。

最核心的nextTIck方法

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve					//定义一个resolve指针
  callbacks.push(() => {
    if (cb) {					//如果有回调,则使用ctx上下文执行回调
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {			//如果没有回调且当前环境可使用promise,
      _resolve(ctx)				//则执行resolve指针
    }
  })
  if (!pending) {				//如果当前不处于等待状态,
    pending = true				//挂起状态并执行timerFunc
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {	//如果没有回调且当前环境可使用promise,
    return new Promise(resolve => {		//返回一个执行promise,并保存resolve
      _resolve = resolve
    })
  }
}

第一次看的时候我很奇怪,为什么已经在timerFunc里推微任务了,还要在最后没有cb的时候返回一个promise?

直到我看到了nextTick的另一种使用方法:await this.$nextTick()。