VueStudyNote icon indicating copy to clipboard operation
VueStudyNote copied to clipboard

22 实现消息队列

Open xwjie opened this issue 6 years ago • 0 comments

参考通过microtasks和macrotasks看JavaScript异步任务执行顺序【朴灵评注】JavaScript 运行机制详解:再谈Event Loop 和 vue的源码next-tick.js。

macrotasks和microtasks的划分:

macrotasks:

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI rendering

microtasks:

  • process.nextTick
  • Promises
  • Object.observe
  • MutationObserver

处理机制

  1. 一个事件循环有一个或者多个任务队列;
  2. 每个事件循环都有一个microtask队列
  3. macrotask队列就是我们常说的任务队列,microtask队列不是任务队列
  4. 一个任务可以被放入到macrotask队列,也可以放入microtask队列
  5. 当一个任务被放入microtask或者macrotask队列后,准备工作就已经结束,这时候可以开始执行任务了。

通俗的解释一下,microtasks的作用是用来调度应在当前执行的脚本执行结束后立即执行的任务。 例如响应事件、或者异步操作,以避免付出额外的一个task的费用。

microtask会在两种情况下执行:

任务队列(macrotask = task queue)回调后执行,前提条件是当前没有其他执行中的代码。 每个task末尾执行。 另外在处理microtask期间,如果有新添加的microtasks,也会被添加到队列的末尾并执行。

也就是说执行顺序是:

开始 -> ** 取task queue第一个task执行 -> 取microtask全部任务依次执行 -> 取task queue下一个任务执行 -> 再次取出microtask全部任务执行 ->** … 这样循环往复

以上文本都引自上面的文章。

之前的测试代码问题

<div id="demo1">
{{message}} + {{message2}}  + {{message3}}
</div>

<script>
var vm = new Xiao({
  el: '#demo1',
  data: {
    message: '123',
    message2: '456',
    message3: '789'
  }
})

vm.message = 'new message1' // 更改数据
console.log(vm.$el.textContent)

setTimeout(function(){
	vm.message = 'new message2' // 更改数据
	vm.message2 = '89';
	vm.message3 = '222';
	console.log(vm.$el.textContent)
}, 1000);

</script>

之前的代码里面,更新了三次属性,那么就会render三次,使用了event loop的异步队列之后,只会更新一次。

实现

wathcer 类update的时候,放入队列。

export default class Watcher {
  update() {
    log(`[Watcher${this.id}] update`)

    // fixme
    // this.get();
    // 放队列里面执行
    queueWatcher(this)
  }
}

队列实现

import Watcher from './watcher'
import { log, logstart, logend, nextTick } from '../util'

// 当前已有的id队列
let has: { [key: number]: ?true } = {}

// 队列
const queue: Array<Watcher> = []

// 是否正在执行队列刷新
let flushing = false

// 是否等待刷新
let waiting = false

function flushSchedulerQueue() {
  logstart(`flushSchedulerQueue, queue size: ${queue.length}`)
  let watcher, id
  flushing = true

  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (let index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.get()
  }

  // 清空
  queue.length = 0

  flushing = false
  waiting = false

  logend()
}

export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  log('queueWatcher', id)

  // 队列里面没有
  if (has[id] == null) {
    has[id] = true
    queue.push(watcher)
  }

  // 防止重复提交
  if (!waiting) {
    waiting = true
    nextTick(flushSchedulerQueue)
  }
}

nextTick实现

代码来自 vue

使用的队列是 微任务队列 MicroTask,实现是用 Promise.resolve() 的回调函数会加入 microtask 来实现的。microtask的优先级会更加高(准确的说更加快的执行)。

setImmediate 是ie才有的。MessageChannel是h5有的,这2个的任务都只能放到 `macrotask‘ (大任务队列?),只有 Promise的才能放到 microtask 里面。

  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
  }

如果不是h5,那只能用 setTimeout(flushCallbacks, 0) 了。默认有4毫秒延迟,而且是异步的。

import { isNative } from './typeutil'
import {log} from './debug'

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 microTimerFunc
let macroTimerFunc
let 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)
  }
} 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) {
  log('nextTick' , cb)

  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        console.log('nextTick', e)
      }
    } 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
    })
  }
}

xwjie avatar Jan 24 '18 16:01 xwjie