Vue2.0源码阅读笔记(四):nextTick

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

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

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

一、GUI 渲染线程

二、JavaScript引擎线程

三、定时触发器线程

四、事件触发线程

五、异步http请求线程
vue

  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 引擎线程读取。
java

2、Event Loop

  Javascript 任务分为同步任务异步任务,同步任务是指调用以后马上获得结果的任务;异步任务是指调用以后没法马上获得结果,须要进行额外操做的任务。

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

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

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

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

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


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

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

  流畅的页面中通常任务执行状况以下所示:


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


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

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

3、nextTick的实现

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

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

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

一、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 数组中的状况发生。

二、老版本实现方式

  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 的使用。

如需转载,烦请注明出处:http://www.javashuo.com/article/p-awyfqhpx-cm.html

相关文章
相关标签/搜索