在阅读 nextTick 的源码之前,要先弄明白 JS 执行环境运行机制,介绍 JS 执行环境的事件循环机制的文章很多,大部分都阐述的比较笼统,甚至有些文章说的是错误的,以下为个人理解,如有错误,欢迎指正。

一、浏览器中的进程与线程

  以 chorme 浏览器为例,浏览器中的每个页面都是一个独立的进程,在该进程中拥有多个线程,通常有以下几个常驻线程:

1、GUI 渲染线程

2、JavaScript引擎线程

3、定时触发器线程

4、事件触发线程

5、异步http请求线程

  GUI 渲染线程解析 html 生成 DOM 树,解析 css 生成 CSSOM 树,然后将两棵树合并成渲染树,最后根据渲染树画出界面。当 DOM 的修改导致了样式非几何属性的变化时,渲染线程重新绘制新的样式,称为“重绘”;当 DOM 的修改导致了样式几何属性的变化,渲染线程会重新计算元素的集合属性,然后将结果绘制出来,称为“回流”。

  JS 引擎线程负责处理Javascript脚本程序,且与GUI 渲染线程是互斥的,因为 js 是可以操控 DOM 的,如果这两个线程并行会导致错误。JS 引擎线程与其他可以并行的线程配合来实现称为Event Loop的 javaScript 执行环境运行机制。

  JS 的运行环境是单线程的,在代码中如果调用形如 setTimeout() 这样的计时功能的 API ,JS 引擎线程会将该任务交给定时触发器线程。定时触发器线程在定时结束之后会将任务放入任务队列中,等待 JS 引擎线程读取。

  JS 与 HTML 之间的交互是通过事件来实现的。在 JS 代码中使用侦听器来预定事件,以便事件发生时执行相应的代码,该代码称为事件处理程序或者事件侦听器。例如点击事件的事件侦听器是 onclick 。JS 引擎线程在执行侦听 DOM 元素的代码时,会将该任务交给事件触发线程处理,当事件被触发时,事件触发线程会将任务放入任务队列中,等待 JS 引擎线程读取。

  JS 代码中通过 XMLHttpRequest 发起 ajax 请求时,会使用异步http请求线程来管理,在状态改变时,该线程会将对应的回调放入任务队列中,等待 JS 引擎线程读取。

二、Event Loop

  Javascript 任务分为同步任务异步任务,同步任务是指调用之后立刻得到结果的任务;异步任务是指调用之后无法立刻得到结果,需要进行额外操作的任务。

  JS 引擎线程顺序执行执行栈中的任务,执行栈中只有同步任务,遇到异步任务就交给相应的线程处理。例如在代码块中有 setTimeout() 方法的调用,则将其交由定时触发器线程处理,定时结束之后定时触发器线程将方法的回调放入自身的任务队列中,当执行栈中的任务处理完之后会读取各线程中任务队列中的事件。

  前面是从同步异步的角度来划分任务的,从执行顺序来说,任务也分为两种:macrotask(宏任务)、microtask(微任务)。异步的 macrotask 执行完之后返回的事件会放在各线程的任务队列中,microtask 执行完之后返回的事件会放在微任务队列中。

macrotask包括:script(JS文件)、MessageChannel、setTimeout、setInterval、setImmediate、I/O、ajax、eventListener、UI rendering。

microtask包括:Promise、MutationObserver、已废弃的Object.observe()、Node中的process.nextTick

  其中需要注意的是GUI 渲染线程去渲染页面也是以 macrotask 的形式进行的,这个之后详谈。



  JS 执行环境运行机制——Event Loop(事件循环)的过程如上图所示:

1、JS 引擎线程顺序执行执行栈中的任务,以一个 macrotask 为单位,在单个宏任务没有处理完之前,JS 引擎线程不会将程序交由GUI 渲染线程接管。也就是说耗时的任务会阻塞渲染,导致页面卡顿的情况发生。典型浏览器一般1秒钟插入60个渲染帧,也就是说16ms进行一次渲染,单个任务超过16ms,如果渲染树发生改变将得不到及时更新渲染。

  流畅的页面中一般任务执行情况如下所示:



  单个任务耗时较多,则会发生丢帧的情况:



2、JS 引擎线程在执行 macrotask 时,会将遇到的异步任务交给指定的线程处理。当异步任务为 macrotask 时,对应线程处理完毕之后放入线程自身的任务队列中;若异步任务为 microtask 时,对应线程处理完毕之后放入微任务队列中。macrotask 执行完之后会遍历微任务队列中的任务加以执行,清空微任务队列。

3、当执行栈中的任务执行完毕后,会读取各个线程中的任务队列,将各任务队列中的事件添加到执行栈中开始执行。从读取各任务队列中的事件放入执行栈中到清空微任务队列的过程称为一个“tick”。JS引擎线程会循环不断地读取任务、处理任务,这个就称为Event Loop(事件循环)机制。

三、nextTick的实现

  Vue的数据更新采用的是异步更新的方式,这样的好处是数据属性多次求值只不用重复调用渲染函数,能够大幅提高性能。其中,异步更新队列是通过调用 nextTick 方法完成的。

  Vue是数据驱动的框架,最好的情况是在页面重新渲染前完成数据的更新。从前面的讲述中可以知道,浏览器的运行机制是首先执行 macrotask,然后执行 microtask ,清空微任务队列后,再从各线程的任务队列中读取新的事件之前,GUI 渲染线程有可能接管程序,完成页面重新渲染。

  nextTick() 在2.5版本之后被单独提取到一个 js 文件中,并且改变了其实现方式。下面分别介绍两种具体实现情况:

1、Vue2.5+ 版本实现方式

  Vue2.5.22 版本的 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
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

  首先说明其中三个变量,callbacks 是存储异步更新回调的任务队列、pending 标识任务队列是否正在刷新、useMacroTask 变量表明是否强制使用 macrotask 方式执行回调。

  nextTick() 注册一个执行传入回调的函数放入到 callbacks 数组中,如果没有传入回调则返回 Promise 对象。如果队列没有开始刷新,则将等待刷新标识设为 true,开始刷新任务。如果没有强制指明需要使用 macrotask 的方式刷新,则默认调用 microTimerFunc 方法来执行。

  microTimerFunc 方法的实现如下代码所示:

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => { setImmediate(flushCallbacks) }
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => { port.postMessage(1) }
} else {
macroTimerFunc = () => { setTimeout(flushCallbacks, 0) }
} if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
} else {
microTimerFunc = macroTimerFunc
}

  microTimerFunc 方法实质就是将 flushCallbacks 方法注册成异步任务加以执行。

  优先使用 Promise 的方式将 flushCallbacks() 的执行注册成 microtask;其中需要注意的是在有的ios环境下,即使将任务推到微任务队列中,队列也不会马上刷新,直到浏览器需要做一些其它的工作,因此在此处添加一个空的计时器来使微任务队列刷新。

  如果环境不兼容 Promise,则将 flushCallbacks() 的执行注册成 macrotask。优先使用 setImmediate 注册任务,setImmediate() 性能好、优先级高,但是兼容性很差,目前只有 IE 浏览器支持。其次使用 MessageChannel 实现,如果都不支持,则调用 setTimeout() 实现。

  flushCallbacks() 的实现方式如下所示:

function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

  首先将是否刷新的标识设为 false ,然后复制 callbacks 数组到 copies ,再清空 callbacks 数组,遍历 copies 执行每一个回调。这里将 callbacks 清空、遍历复制数组 copies 的原因是为了防止在遍历执行回调的过程中,不断有新的回调添加到 callbacks 数组中的情况发生。

2、老版本实现方式

  Vue2.4.4 版本的 nextTick() 实现与2.5+ 版本的差异主要是下面这段代码:

  if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
if (isIOS) setTimeout(noop)
}
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, { characterData: true })
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
timerFunc = () => {setTimeout(nextTickHandler, 0)}
}

  老版本的 nextTick() 与2.5+ 版本的最主要区别是将任务注册成异步队列的方式不同。优先使用 Promise 将任务注册成 microtask,其次使用 MutationObserver 将任务注册成 microtask。如果环境不允许将任务注册成 microtask,则直接使用 setTimeout() 将任务注册成 macrotask。

  可以看出老版本的 nextTick() 对性能的追求特别高,基本上都是采用 microtask 来实现异步更新的,macrotask 没有区分层级,直接使用 setTimeout() 来最后兜底。

  MutationObserver 的优先级特别高,在某些场景下它甚至要比事件冒泡还要快,会导致很多问题。如果全部使用 macrotask 则对一些有重绘和动画的场景也会有性能影响。所以 Vue2.5+ 版本删除了对 MutationObserver 的使用,增强了 macrotask 的使用。

如需转载,烦请注明出处:https://www.cnblogs.com/lidengfeng/p/10856352.html

Vue2.0源码阅读笔记(四):nextTick的更多相关文章

  1. Vue2.0源码阅读笔记--双向绑定实现原理

    上一篇 文章 了解了Vue.js的生命周期.这篇分析Observe Data过程,了解Vue.js的双向数据绑定实现原理. 一.实现双向绑定的做法 前端MVVM最令人激动的就是双向绑定机制了,实现双向 ...

  2. Vue2.0源码阅读笔记--生命周期

    一.Vue2.0的生命周期 Vue2.0的整个生命周期有八个:分别是 1.beforeCreate,2.created,3.beforeMount,4.mounted,5.beforeUpdate,6 ...

  3. Vue2.0源码阅读笔记(二):响应式原理

      Vue是数据驱动的框架,在修改数据时,视图会进行更新.数据响应式系统使得状态管理变的简单直接,在开发过程中减少与DOM元素的接触.而深入学习其中的原理十分有必要,能够回避一些常见的问题,使开发变的 ...

  4. Vue2.0源码阅读笔记(一):选项合并

      Vue本质是上来说是一个函数,在其通过new关键字构造调用时,会完成一系列初始化过程.通过Vue框架进行开发,基本上是通过向Vue函数中传入不同的参数选项来完成的.参数选项往往需要加以合并,主要有 ...

  5. Vue2.0源码阅读笔记(三):计算属性

      计算属性是基于响应式依赖进行缓存的,只有在相关响应式依赖发生改变时才会重新求值,这种缓存机制在求值消耗比较大的情况下能够显著提高性能. 一.计算属性初始化   Vue 在做数据初始化时,通过 in ...

  6. Werkzeug源码阅读笔记(四)

    今天主要讲一下werkzeug中的routing模块.这个模块是werkzeug中的重点模块,Flask中的路由相关的操作使用的都是这个模块 routing模块的用法 在讲解模块的源码之前,先讲讲这个 ...

  7. Mina源码阅读笔记(四)—Mina的连接IoConnector2

    接着Mina源码阅读笔记(四)-Mina的连接IoConnector1,,我们继续: AbstractIoAcceptor: 001 package org.apache.mina.core.rewr ...

  8. Linux 0.11源码阅读笔记-文件管理

    Linux 0.11源码阅读笔记-文件管理 文件系统 生磁盘 未安装文件系统的磁盘称之为生磁盘,生磁盘也可以作为文件读写,linux中一切皆文件. 磁盘分区 生磁盘可以被分区,分区中可以安装文件系统, ...

  9. Linux 0.11源码阅读笔记-中断过程

    Linux 0.11源码阅读笔记-中断过程 是什么中断 中断发生时,计算机会停止当前运行的程序,转而执行中断处理程序,然后再返回原被中断的程序继续运行.中断包括硬件中断和软件中断,硬中断是由外设自动产 ...

随机推荐

  1. KNN算法项目实战——改进约会网站的配对效果

    KNN项目实战——改进约会网站的配对效果 1.项目背景: 海伦女士一直使用在线约会网站寻找适合自己的约会对象.尽管约会网站会推荐不同的人选,但她并不是喜欢每一个人.经过一番总结,她发现自己交往过的人可 ...

  2. SSM中前台传数组。后台接受的问题

    当时写得时候,忘记考虑json的jar,做个记录. 第一步:先带入jar <dependency> <groupId>com.fasterxml.jackson.core< ...

  3. 23飞机大战__pygame 快速入门

      1. 使用 pygame 创建图形窗口 小节目标 游戏的初始化和退出 理解游戏中的坐标系 创建游戏主窗口 简单的游戏循环 可以将图片素材 绘制 到 游戏的窗口 上, 开发游戏之前需要先知道 如何建 ...

  4. [Luogu2015]二叉苹果树(树形dp)

    [Luogu2015] 二叉苹果树 题目描述 有一棵苹果树,如果树枝有分叉,一定是分2叉(就是说没有只有1个儿子的结点) 这棵树共有N个结点(叶子点或者树枝分叉点),编号为1-N,树根编号一定是1. ...

  5. Ubuntu16.04 重新安装误删的某个*.so文件

    在使用Ubuntu系统时,如果不小心将某个*.so文件删除,该如何重新安装呢? 如果直接使用命令:sudo  apt-get  install  *.so 可能会报错或者找不到这个*.so文件. 正确 ...

  6. 树形dp专栏

    前言 自己树形dp太菜了,要重点搞 219D Choosing Capital for Treeland 终于自己做了一道不算那么毒瘤的换根dp 令 \(f[u]\) 表示以 \(u\) 为根,子树内 ...

  7. canvas一个简单粗暴的中奖转盘

    最近在学canvas做动画,于是就写个转盘练下手.上个简陋的成果图(中间那个是转的指针,外面的圈是图片,懒得写了哈哈哈) 代码很简单,都注释了,直接上代码吧,嘤嘤嘤 html <body> ...

  8. vue-cli 2.0搭建vue脚手架步骤

    1.安装node 检测版本node -v 2.安装webpack npm install webpack -g 检测版本 webpack -v 3.安装vue-cli npm install vue- ...

  9. Mardown加上目录

    适合Jekyll+Github模式下post.html 中加入如下代码,会在页面加载时生成目录结构: 有两种方案: 方案一效果

  10. Vue----项目增加百度统计

    到百度统计->注册账号->新增网址->获取代码 在Vue单页面开发中接入百度统计代码时,如果直接按照官网的走会出现错误,就是_hmt找不到,这是因为在一个js文件里声明的变量在另一个 ...