Vue源码解析系列——响应式原理篇:nextTick

准备

vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。
要完全理解nextTick的实现需要有以下知识储备:

  • JS的事件循环机制
  • ES6中的Promise的使用方法及原理
  • 事件循环中的微任务宏任务

回顾

如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》

nextTick的两种使用方法

在Vue中,nextTick有两种风格的使用方法,一种是回调函数风格:

this.$nextTick(()=>console.log("nextTick"))

另外一种是Promise风格的使用方法:

this.$nextTick().then(()=>console.log("nextTick"))

Tick

在上一篇我们了解到在执行queueWatcher的时候,最终是用nextTick来调用flushSchedulerQueue方法的,那么这个nextTick是何方神圣呢?先不要急,我们先了解下Tick的概念:
JS是单线程执行的,当执行完当前线程时,会去事件按顺序先取出微任务执行,再取出宏任务执行。
一个Tick的流程就是:主线程执行完毕后再将当前的所有微任务执行完。所以说宏任务是在下一个Tick执行的。
所以见名知意,nextTick(flushSchedulerQueue)意思就是指watcher渲染DOM是在下一个Tick执行的,也就是运行在宏任务中。
但其实为了渲染效率,尽快去渲染DOM,如果执行环境支持的话还是会运行在微任务中。
进入nextTick.js看看Vue是怎么实现nextTick方法的。

nextTick.js

const callbacks = []
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]()
  }
}

第一部分,声明一个数组callbacks,用于存放所有需要在下一个Tick中执行的任务;声明一个变量pending,默认为false
定义了一个方法flushCallbacks,用于遍历callbacks挨个执行里面的任务。
继续往下看

let timerFunc

if (typeof Promise !== 'undefined' && isNative(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 (!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)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }

这一段很长,但是我们不用一行一行看,只要大概看一下就知道这边其实就是对执行环境支持的一些判断,一开始定义的timerFunc就是决定需要用哪种方式、哪种风格去执行下一个Tick的任务。这边就能看到我刚刚说的如果执行环境允许的话还是会放在微任务中去执行,如果执行环境不允许的话才会降级在宏任务中运行。
这边我就以浏览器为例,首选Promise,以Promise风格去实现nextTick
继续向下看:

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  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
    })
  }
}

这边就是nextTick的核心了。传入两个参数(注:如果是以Promise去使用nextTick的话,两个参数都不用传入:this.$nextTick().then(() => console.log("next tick")) )。
定义一个_resolve变量,这个方法的最后我们可以看到,_resolve就是一个Promise实例的resolve回调。
然后是向callbacks列表去添加一个匿名函数,这个匿名函数有一个判断:

  • 如果有第一个参数(“普通风格调用”),就执行回调函数。
  • 如果没有第一个参数(“Promise风格调用”),就执行他的resolve回调。
    然后判断当前有没有正在执行的任务,如果没有就将pending至为true,并调用timerFunc
    timerFunc前面说过了,这边以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)
  }

获取一个已经resolvePromise实例,然后调用flushCallbacks在下一个Tick中执行所有的任务队列。

总结

经过上面的源码我们可以得知,Vue的DOM渲染过程是异步的,JS在主线程执行过程中会一个一个的收集nextTick的任务,然后放在下一个Tick中全部执行完毕,其中DOM渲染也在nextTick的任务列表中。

附录

经过以上对nextTick的学习,看看对nextTick的理解是不是很透彻。
这边有一段有趣的代码,猜猜最后的输出是什么?为什么会是这样的结果?:

<template>
  <div>
    <div ref="div">{{ message }}</div>
    <button @click="handler">toggle</button>
  </div>
</template>

<script>
export default {
  data: () => ({
    message: 'hello'
  }),
  methods: {
    handler() {
      this.$nextTick(() => console.log(this.$refs.div.innerHTML));
      this.message = 'world';
      console.log(this.$refs.div.innerHTML);
      this.$nextTick().then(() => console.log(this.$refs.div.innerHTML));
    }
  }
};
</script>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的前端小黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值