从JS事件循环(Event Loop)机制到vue.nextTick的实现

众所周知,为了与浏览器进行交互,Javascript是一门非阻塞单线程脚本语言。vue

  1. 为什么单线程? 由于若是在DOM操做中,有两个线程一个添加节点,一个删除节点,浏览器并不知道以哪一个为准,因此只能选择一个主线程来执行代码,以防止冲突。虽然现在添加了webworker等新技术,但其依然只是主线程的子线程,并不能执行诸如I/O类的操做。长期来看,JS将一直是单线程。
  2. 为什么非阻塞?由于单线程意味着任务须要排队,任务按顺序执行,若是一个任务很耗时,下一个任务不得不等待。因此为了不这种阻塞,咱们须要一种非阻塞机制。这种非阻塞机制是一种异步机制,即须要等待的任务不会阻塞主执行栈中同步任务的执行。这种机制是以下运行的:
  • 全部同步任务都在主线程上执行,造成一个执行栈(execution context stack 
  • 等待任务的回调结果进入一种任务队列(task queue)
  • 当主执行栈中的同步任务执行完毕后才会读取任务队列,任务队列中的异步任务(即以前等待任务的回调结果)会塞入主执行栈,
  • 异步任务执行完毕后会再次进入下一个循环。此即为今天文章的主角事件循环(Event Loop) 

用一张图展现这个过程:ios

1.macro task与micro task

在实际状况中,上述的任务队列(task queue)中的异步任务分为两种:微任务(micro task)宏任务(macro task)git

  • micro task事件:Promises(浏览器实现的原生Promise)MutationObserverprocess.nextTick 
    <br />
  • macro task事件:setTimeoutsetIntervalsetImmediateI/OUI rendering
    这里注意:script(总体代码)即一开始在主执行栈中的同步代码本质上也属于macrotask,属于第一个执行的task 

microtask和macotask执行规则:github

    • macrotask按顺序执行,浏览器的ui绘制会插在每一个macrotask之间
    • microtask按顺序执行,会在以下状况下执行:
      • 每一个callback以后,只要没有其余的JS在主执行栈中
      • 每一个macrotask结束时

下面来个简单例子:web

console.log(1);promise

 

setTimeout(function() {浏览器

  console.log(2);app

}, 0);dom

new Promise(function(resolve,reject){异步

    console.log(3)

    resolve()

}).then(function() {

  console.log(4);

}).then(function() {

  console.log(5);

});

console.log(6);


一步一步分析以下:

 

1.同步代码做为第一个macrotask,按顺序输出:1 3 6

2.microtask按顺序执行:4 5

 

3.microtask清空后执行下一个macrotask:2

再来一个复杂的例子:

// Let's get hold of those elements

var outer = document.querySelector('.outer');

var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the

// outer element

new MutationObserver(function() {

  console.log('mutate');

}).observe(outer, {

  attributes: true

});

// Here's a click listener…

function onClick() {

  console.log('click');

 

  setTimeout(function() {

    console.log('timeout');

  }, 0);

 

  Promise.resolve().then(function() {

    console.log('promise');

  });

 

  outer.setAttribute('data-random', Math.random());

}

// …which we'll attach to both elements

inner.addEventListener('click', onClick);

outer.addEventListener('click', onClick);

假设咱们建立一个有里外两部分的正方形盒子,里外都绑定了点击事件,此时点击内部,代码会如何执行?一步一步分析以下:

  • 1.触发内部click事件,同步输出:click
  • 2.setTimeout回调结果放入macrotask队列
  • 3.promise回调结果放入microtask
  • 4.Mutation observers放入microtask队列,主执行栈中onclick事件结束,主执行栈清空
  • 5.依序执行microtask队列中任务,输出:promise mutate
  • 6.注意此时事件冒泡,外部元素再次触发onclick回调,因此按照前5步再次输出:click promise mutate(咱们能够注意到事件冒泡甚至会在microtask中的任务执行以后,microtask优先级很是高) 
  • 7.macrotask中第一个任务执行完毕,依次执行macrotask中剩下的任务输出:timeout timeout

 

2.vue.nextTick实现

 

Vue.js 里是数据驱动视图变化,因为 JS 执行是单线程的,在一个 tick 的过程当中,它可能会屡次修改数据,但 Vue.js 并不会傻到每修改一次数据就去驱动一次视图变化,它会把这些数据的修改所有 push 到一个队列里,而后内部调用 一次 nextTick 去更新视图,因此数据到 DOM 视图的变化是须要在下一个 tick 才能完成。这即是咱们为何须要vue.nextTick.

 

这样一个功能和事件循环很是类似,在每一个 task 运行完之后,UI 都会重渲染,那么很容易想到在 microtask 中就完成数据更新,当前 task 结束就能够获得最新的 UI 了。反之若是新建一个 task 来作数据更新,那么渲染就会进行两次。

 

因此在vue 2.4以前使用microtask实现nextTick,直接上源码

 

var counter = 1var observer = new MutationObserver(nextTickHandler)var textNode = document.createTextNode(String(counter))

 

observer.observe(textNode, {

 

    characterData: true

 

})

 

timerFunc = () => {

 

    counter = (counter + 1) % 2

 

    textNode.data = String(counter)

 

}

能够看到使用了MutationObserver

然而到了vue 2.4以后却混合�使用microtask macrotask来实现,源码以下

/* @flow *//* globals MessageChannel */

import { noop } from 'shared/util'import { handleError } from './error'import { isIOS, isNative } from './env'

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]()

  }

}

// Here we have async deferring wrappers using both micro and macro tasks.// In < 2.4 we used micro tasks everywhere, but there are some scenarios where// micro tasks have too high a priority and fires 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 micro task by default, but expose a way to force macro task when// needed (e.g. in event handlers attached by v-on).let microTimerFunclet macroTimerFunclet useMacroTask = false

// 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)

  }

}

// 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

}

/**

 * Wrap a function so that if any code inside triggers state change,

 * the changes are queued using a Task instead of a MicroTask.

 */export function withMacroTask (fn: Function): Function {

  return fn._withTask || (fn._withTask = function () {

    useMacroTask = true

    const res = fn.apply(null, arguments)

    useMacroTask = false

    return res

  })

}

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()

    }

  }

  // $flow-disable-line

  if (!cb && typeof Promise !== 'undefined') {

    return new Promise(resolve => {

      _resolve = resolve

    })

  }

}

能够看到使用setImmediateMessageChannelmascrotask事件来实现nextTick

为何会如此修改,其实看以前的事件冒泡例子就能够知道,因为microtask优先级过高,甚至会比冒泡快,因此会形成一些诡异的bug。如 issue #4521#6690#6556;可是若是所有都改为 macro task,对一些有重绘和动画的场景也会有性能影响,如 issue #6813。因此最终 nextTick 采起的策略是默认走 micro task,对于一些 DOM 交互事件,如 v-on 绑定的事件回调函数的处理,会强制走 macro task

相关文章
相关标签/搜索