VueStudyNote
VueStudyNote copied to clipboard
22 实现消息队列
参考通过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
处理机制
- 一个事件循环有一个或者多个任务队列;
- 每个事件循环都有一个microtask队列
- macrotask队列就是我们常说的任务队列,microtask队列不是任务队列
- 一个任务可以被放入到macrotask队列,也可以放入microtask队列
- 当一个任务被放入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
})
}
}