1.js的事件循环
在事件循环中有两个比较重要的概念,分别叫做宏任务和微任务。宏任务和微任务都是指代异步任务。 我们都知道JavaScript是自上而下执行的,在执行过程中涉及到执行栈和任务队列两个东西。执行中的代码会放在执行栈中执行,宏任务和微任务会放在任务队列中等待执行。
js首先自上而下执行,当遇到异步任务会将任务加入到异步任务栈中,当异步任务完毕(达到触发条件,例如定时时间到了),推入任务队列等待执行。当js栈执行完毕,去检查微任务队列中是否存在可以被执行的任务,如果存在就把任务从队列中取出来放入到执行栈中执行,微任务队列被清空之后再开始检查宏任务队列,存在宏任务取出执行。从任务队列取出任务(清空任务队列,即有很多等待执行的任务的时候会将他们全部取出,可能不止一个),执行栈执行任务,取出任务,执行任务,就是一次又一次的事件循环。
总结一句话就是: 先执行同步代码,再执行微任务,再检查宏任务是否到达时间,到达时间再执行。
2.nextTick有什么用?
Vue.nextTick( [callback, context] )
参数:
{Function} [callback]
{Object} [context]
用法:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
什么是nextTick,next是下一个,tick是任务,这里解释为事件循环,也就是下一个事件循环。简单来说,nexttick作用就是拿一个队列存储所有要执行的任务,在下一个tick(异步)执行这些任务。(放在一个回调函数中,当进入下一个事件循环时触发这个回调,而不是立即触发这个函数)
3.nextTick源码实现
nextTickHandler
export const nextTick = (function () {
/* 存放异步执行的回调*/
const callbacks = []
/* 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/* 一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc
/* 下一个tick时的回调*/
function nextTickHandler () {
/* 一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
pending = false
// 拷贝存储所有回调的数组
const copies = callbacks.slice(0)
// 清空数组
callbacks.length = 0
/* 执行所有callback*/
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
这里的nextTick是一个立即执行函数,所以当加载vue.js的时候就已经执行了,声明了一些全局变量,定义了一个函数nextTickHandler,他的作用就是去执行存储在callbacks这个数组中的所有回调函数。
timerFunc
下面是对当前执行vue.js的环境做一个判断,根据支持程度决定当前环境下nexttick推迟一个任务用何种异步方式执行它。根据代码可知,若支持setImmediate,则采用setImmediate,若支持MessageChannel,采用MessageChannel,若支持Promise,采用Promise,若以上都不支持,采用setTimeout。所以优先级为setImmediate > MessageChannel >Promise >setTimeout
// An asynchronous deferring mechanism.
// In pre 2.4, we used to use microtasks (Promise/MutationObserver)
// but microtasks actually has too high a priority and fires in between
// supposedly sequential events (e.g. #4521, #6690) or even between
// bubbling of the same event (#6566). Technically setImmediate should be
// the ideal choice, but it's not available everywhere; and 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)) {
timerFunc = () => {
setImmediate(nextTickHandler)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = nextTickHandler
timerFunc = () => {
port.postMessage(1)
}
} else
/* istanbul ignore next */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// use microtask in non-DOM environments, e.g. Weex
const p = Promise.resolve()
timerFunc = () => {
p.then(nextTickHandler)
}
} else {
// fallback to setTimeout
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
注意注意,这里最开头有一段注释,这是尤大大写的注释,大概的意思是
// 一个异步的延迟机制。
// 在2.4之前,我们曾经使用微任务(Promise/MutationObserver)。但是微任务实际上具有太高的优先级,并且会在所谓的连续事件之间(例如#4521、#6690)甚至在同一事件的冒泡之间(#6566)触发。 从技术上讲,setImmediate 应该是理想的选择,但它并非随处可用;在同一循环中触发的所有 DOM 事件之后,唯一一致地将回调排队的 polyfill 是使用 MessageChannel。
所以说现在的nexttick使用的异步任务类型优先级和之前是不一样的,因此可以看到网上有很多人对nexttick的源码做解析,但是不同年份时间点,大家对这个优先级的说法不一样,不是当时那个作者写错了,而是这部分内容更新调整过了!!!
queueNextTick
关键的queueNextTick来了,还记得我上面说的源码中nexttick是一个立即执行函数嘛,这个立即执行函数的返回值就是这个queueNextTick函数,所以说我们平常使用的this.$nexttick 其实调用的就是这个queueNextTick函数。他接收2个参数,一个cb 回调函数,一个ctx 上下文。
是不是和vue官方给出的$nexttick参数一模一样 ^_^
ok我们来看这个函数做了什么,首先在callbacks这个回调集合数组中push回调函数,若cb存在,将回调的this指向指向vue实例,ctx默认vue实例。
pending为最开始定义的全局变量,是一个标记位,表示现在是否函数被推送到任务队列等待执行栈去执行。若pending为false ,则先将标志改为true,然后执行timerFunc函数(注意此时nextTickHandler函数就被推进异步任务中去了,之前callbacks这个数组中所有的回调将被等待执行,但是注意此时新的回调仍然可以被push到callbacks这个变量中。开始执行时callbacks变量会被清空),timerFunc就是之前上面定义的,想不起了可以再回过头上去看下。若pending为true,表示上一个事件循环在执行中,不会执行timerFunc(),这样做是为了避免timerFunc多次推入任务队列。当任务队列中的事件被执行栈开始执行,pending将变回false。
//推送到队列中下一个tick时执行
return function queueNextTick (cb?: Function, ctx?: Object) {
let _resolve
/* cb存到callbacks中*/
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
4.完整nexttick函数代码
github.com/vuejs/vue
源码位置在src/core/util/env.js
/**
* Defer a task to execute it asynchronously.
*/
export const nextTick = (function () {
/* 存放异步执行的回调*/
const callbacks = []
/* 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/* 一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc
/* 下一个tick时的回调*/
function nextTickHandler () {
/* 一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
pending = false
// 拷贝存储所有回调的数组
const copies = callbacks.slice(0)
callbacks.length = 0
/* 执行所有callback*/
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// An asynchronous deferring mechanism.
// In pre 2.4, we used to use microtasks (Promise/MutationObserver)
// but microtasks actually has too high a priority and fires in between
// supposedly sequential events (e.g. #4521, #6690) or even between
// bubbling of the same event (#6566). Technically setImmediate should be
// the ideal choice, but it's not available everywhere; and 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)) {
timerFunc = () => {
setImmediate(nextTickHandler)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = nextTickHandler
timerFunc = () => {
port.postMessage(1)
}
} else
/* istanbul ignore next */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// use microtask in non-DOM environments, e.g. Weex
const p = Promise.resolve()
timerFunc = () => {
p.then(nextTickHandler)
}
} else {
// fallback to setTimeout
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
/*
推送到队列中下一个tick时执行
cb 回调函数
ctx 上下文
*/
return function queueNextTick (cb?: Function, ctx?: Object) {
let _resolve
/* cb存到callbacks中*/
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()
此文仅为个人学习记录,若有错误欢迎随时指出。