浏览器事件循环机制与Vue nextTick的实现

栏目: 编程语言 · 发布时间: 6年前

内容简介:先上一段简单的代码执行结果总是如下:为什么呢?为什么同样是异步,Promise.then 就是 比 setTimeout 先执行呢。

先上一段简单的代码

console.log('aa');
setTimeout(() => { 
    console.log('bb')}, 
0);
Promise.resolve().then(() => console.log('cc'));
复制代码

执行结果总是如下:

aa
cc
bb
复制代码

为什么呢?为什么同样是异步,Promise.then 就是 比 setTimeout 先执行呢。

这就涉及到浏览器事件循环机制了。

  1. 以前浏览器只有一类事件循环,都是基于当前执行环境上下文, 官方用语叫 browsing-context 链接在此。我们可以理解为一个 window 就是一个执行环境上下文,如果有 iframe , 那么 iframe 内就是另一个执行环境了。
  2. 2017年新版的HTML规范新增了一个事件循环,就是web workers。这个暂时先不讨论。

事件循环机制涉及到两个知识点 macroTaskmicroTask ,一般我们会称之为 宏任务微任务 。不管是 macroTask 还是 microTask ,他们都是以一种 任务队列 的形式存在。

macroTask

script (整体代码), setTimeout , setIntervalsetImmediate (仅IE支持), I/O , UI-rendering

注:此处的 I/O 是一个抽象的概念,并不是说一定指输入/输出,应该包括DOM事件的触发,例如click事件,mouseover事件等等。这是我的理解,如果有误,还请指出。

microTask

包括: Promises , process.nextTick , Object.observe (已废弃), MutationObserver ( 监听DOM改变 )

以下内容摘抄于知乎何幻的回答

一个浏览器环境(unit of related similar-origin browsing contexts.)只能有一个事件循环(Event loop),而一个事件循环可以多个任务队列(Task queue),每个任务都有一个任务源(Task source)。

相同任务源的任务,只能放到一个任务队列中。

不同任务源的任务,可以放到不同任务队列中。

对上面的几句话进行总结:事件循环只有一个,围绕着调用栈, macroTaskmicroTaskmacroTaskmicroTask 是一个大的任务容器,里面可以有多个任务队列。不同的 任务源 ,任务会被放置到 不同的任务队列 。那任务源是什么呢,比如 setTimeoutsetIntervalsetImmediate ,这都是不同的任务源,虽然都是在 macroTask 中,但肯定是放置在不同的任务队列中的。 最后,具体浏览器内部怎么对不同任务源的任务队列进行 排序 和取数,这个目前我还不清楚,如果正在看文章的你知道的话,请告诉下我。

接下来我们继续分析 macroTaskmicroTask 的执行顺序,这两个队列的行为与浏览器具体的实现有关,这里只讨论被业界广泛认同和接受的队列执行行为。

macroTaskmicroTask 的循环顺序如下:

注意: 整体代码算一个 macroTask

  1. 先执行一个 macroTask 任务(例如执行整个js文件内的代码)
  2. 执行完 macroTask 任务后,找到 microTask 队列内的 所有 任务,按先后顺序取出并执行
  3. 执行完 microTask 内的所有任务后,再从 macroTask 取出 一个 任务,执行。
  4. 重复:2,3 步骤。

现在,我们来解释文章开始时的那串代码,为什么 Promise 总是优先于 setTimeout

console.log('aa');
setTimeout(() => { 
    console.log('bb')}, 
0);
Promise.resolve().then(() => console.log('cc'));
复制代码
  1. 浏览器加载整体代码并执行算一个 macroTask
  2. 在执行这段代码的过程中,解析到 setTimeout 时,会将 setTimeout内的代码 添加到 macroTask 队列中。
  3. 接下来,又解析到 Promise , 于是将 Promise.then()内的代码 添加到 microTask 队列中。
  4. 代码执行完毕,也就是第一个 macroTask 完成后,去 microTask 任务队列中,找出所有任务并执行, 此时执行了 console.log('cc') ;
  5. microTask 任务队列执行完毕后,又取出下一个 macroTask 任务并执行,也就是执行 setTimeout 内的代码 console.log('bb')

可以这样理解: 一个 宏任务 执行完后,会执行完所有的 微任务 ,再又执行一个 宏任务 。依此循环,这也就是事件循环。

如果对事件循环机制还是不怎么理解的话,可以看下这篇文章,图文并茂,讲的挺细的。

Vue nextTick函数的实现

首先我们运用 nextTick 的方式有两种

// 第一种,Vue全局方法调用
Vue.nextTick(fn, context);

// 第二种,在实例化vue时,内部调用
this.$nextTick(fn);
复制代码

其实这两种方式都是调用的 Vue 内部提供的一个nextTick 方法,Vue内部对这个方法做了些简单的封装

// src/core/instance/render.js --- line 57
// 这里调用 nextTick 时自动把当前vue实例对象作为第二个参数传入,所以我们调用 this.$nextTick时,不需要传第二个参数
Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};

// src/core/global-api/index.js --- line 45
// 直接将 nextTick 暴露出去,作为Vue全局方法
Vue.nextTick = nextTick;
复制代码

也就是说,这两种调用方式,都是执行的Vue内部提供的 nextTick 方法。这个 nextTick 方法,Vue用了一个单独的文件维护。如下

代码来源:vue项目下 src/core/util/next-tick.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 内的函数。那么 callbacks 数组是存放什么的?其实就是存放我们调用 this.$nextTick(fn) 是传入的 fn ,只不过对它做了一层作用域包装和异常捕获。

nextTick 函数 定义在文件的末尾,代码如下。注意看我加的注释。

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 将传入的函数包装一层,绑定作用域,并try-catch捕获错误
  // 如果没传入函数,且浏览器原生支持 Promise 的情况下,让 Promise resolve;
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // pending 是一个开关,每次执行 flushCallbacks 后,会将 pending 重置为 fasle
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  // 这里返回一个 Promise, 所以我们可以这样调用,$this.nextTick().then(xxx)
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
复制代码

上面的代码的 pending 有点意思, 它是为什么处理同时调用多个 nextTick 的业务场景, 例如

new Vue({
    // 省略
    created() {
        // 执行第一个时 , pending 为 false, 所以会进入 if (!pending),然后 pending 被设为false
        this.$nextTick(fn1);
        // 
        this.$nextTick(fn2);
        this.$nextTick(fn3);
    }
})
复制代码

如果是这样调用, 那么Vue会怎么做呢,

看到这里的同学估计会有个疑问点, useMacroTask 是什么, macroTimerFunc 是什么, microTimerFunc 又是什么。接下来会一一解开。

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
复制代码

接下来, macroTimerFunc 的定义

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
复制代码

再是 microTimerFunc 的定义

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    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)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}
复制代码

最后上一段代码,出自Google 2018GDD大会,欢迎探讨并说出原因。

button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('microtask 1'))
  console.log('listener 1')
})

button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('microtask 2'))
  console.log('listener 2')
})

1. 手动点击,输出结果
2. 用测试代码 button.click() 触发,输出结果
复制代码

答案在这篇文章

参考并推荐几篇好文:

event-loop

vue技术内幕

深入浏览器的事件循环 (GDD@2018)

【Vue源码】Vue中DOM的异步更新策略以及nextTick机制

前端基础进阶(十二):深入核心,详解事件循环机制


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Dive Into Python 3

Dive Into Python 3

Mark Pilgrim / Apress / 2009-11-6 / USD 44.99

Mark Pilgrim's Dive Into Python 3 is a hands-on guide to Python 3 (the latest version of the Python language) and its differences from Python 2. As in the original book, Dive Into Python, each chapter......一起来看看 《Dive Into Python 3》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具