前言
js是一个单线程的语言(非阻塞),最初的目的是为了和浏览器交互,也就是事件的输入输出流,计算机根据人类的指令做出不同的反应结果,但是在JS执行的过程环境中 我们有 几个特殊的 “单词” setTimeout
、setInterval
、 Promise
、另外在 Node中还有 process.nextTick
。那么他们的执行顺序到底是怎么样的呢,浏览器不应该是按照他们书写的顺序从上往下执行吗?
那你又有疑问了,既然是单线程的,在某个特定的时刻只有特定的代码能够被执行,并阻塞其它的代码。
那不行啊,我们总不能一直等着啊,前端需要调用后端接口取数据,这个过程是需要响应时间的,那执行这个代码的时候浏览器也等着?答案是否定的。
其实还有其他很多类线程(应该叫做任务队列),比如进行ajax请求、监控用户事件、定时器、读写文件的线程(例如在NodeJS中)等等。
这些我们称之为异步事件,当异步事件发生时,将他们放入执行队列,等待当前代码执行完成。就不会长时间阻塞主线程。
等主线程的代码执行完毕,然后再读取任务队列,返回主线程继续处理。如此循环这就是事件循环机制。
JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为
举个栗子
console.log('0');
setTimeout(() => {
console.log('1');
}, 0);
console.log('2');
//输出 0 , 2 ,1
看起来是setTimeout 设置了时间为0 但是 setTimeout 是一个“异步”的操作,其实真是的情况是 setTimeout 的0 参数是无效的, JS会给他默认一个值为4毫秒。所以结果是 0 2 1 。
我们刚刚说到了“异步” 那么JS是怎么异步的呢 ,其实在JS执行的时候 不同的任务会分配到不同的队列中,每个任务在制定的时候 已经规定了他的基础要素 也就是他属于哪个队列的 ,任务源可以分为2类 微任务 microtask
和宏任务 macrotask
,微任务又称之为JOBS,宏任务称为TASK。
我们在来看看下面这个例子
setTimeout(function() {
console.log(1)
}, 0);
new Promise((resolve)=>{
console.log(2);
for(var i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5)
// 2 3 5 4 1
为什么是这个结果呢 。这就是 Jobs 和 Task的区别 我们下面仔细梳理下
微任务(Jobs)包括
process.nextTick
Promise
Object.observe(已废弃)
MutationObserver (html5 新特性)
宏任务(Task)包括
setTimeout/setInterval
setImmediate
I/O操作
UI rendering
浏览器中新标准中的事件循环机制与 node.js 类似,其中会介绍到几个nodejs有但是浏览器中没有的 API,大家只需要了解就好。
比如process.nextTick
,setImmediate
我们称他们为事件源, 事件源作为任务分发器,他们的回调函数才是被分发到任务队列,而本身会立即执行。
例如,setTimeout
第一个参数被分发到任务队列,Promise
的 then 方法的回调函数被分发到任务队列(catch方法同理)。
不同源的事件被分发到不同的任务队列,其中 setTimeout
和 setInterval
属于同源
整体代码开始第一次循环。全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的job。
当所有可执行的 job 执行完毕之后。循环再次从task开始,找到其中一个任务队列执行完毕,然后再执行所有的 job,这样一直循环下去。
无论是 task 还是 job,都是通过函数调用栈来完成。
这个时候我们是不是有一个大发现,除了首次整体代码的执行,其他的都有规律,先执行task任务队列,再执行所有的 job 并清空 job 队列。
再执行 task—job—task—job……,往复循环直到没有可执行代码。
那我们可不可以这么理解,第一次 script 代码的执行也算是一个task任务呢,如果这么理解那整个事件循环就很容易理解了。
UI rendering是在Task执行之后就运行的 那么我们只要把DOM操作放入Job中就可以提高渲染的性能了
下面我们说说 vue的 nextTick
还是举个栗子
-
{{li.name}}
new Vue({
el: '#app',
data: {
list: []
},
mounted() {
this.init()
},
methods: {
init() {
this.list = [{name:"lxl",age:18},{name:"kobe",age:19}]
this.$refs.list.getElementsByTagName('li')[0].style.color = 'red'
},
}
})
我们会发现 这样会报错
如下修改:
new Vue({
el: '#app',
data: {
list: []
},
mounted() {
this.init()
},
methods: {
init() {
this.list = [{name:"lxl",age:18},{name:"kobe",age:19}]
this.$nextTick(()=>{
this.$refs.list.getElementsByTagName('li')[0].style.color = 'red'
})
},
}
})
我在获取到数据后赋值给 data 对象的 list 属性,然后我想引用ul元素找到第一个li把它的颜色变为红色,但是事实上,这个要报错的。
我们知道,在执行这句话时,ul 下面并没有 li,也就是说刚刚进行的赋值操作,当前并没有引起视图层的更新。
因为 Vue 的数据驱动视图更新,是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
因此,在这样的情况下,vue 给我们提供了 nextTick 方法,vue 在更新完视图后就会执行我们的函数帮我们做事情。
nextTick
可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。
var callbacks = [];
var pending = false;
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire 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 microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
var microTimerFunc;
var macroTimerFunc;
var 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 = function () {
setImmediate(flushCallbacks);
};
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = flushCallbacks;
macroTimerFunc = function () {
port.postMessage(1);
};
} else {
/* istanbul ignore next */
macroTimerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
microTimerFunc = function () {
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 (macro) task instead of a microtask.
*/
function withMacroTask (fn) {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true;
var res = fn.apply(null, arguments);
useMacroTask = false;
return res
})
}
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
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(function (resolve) {
_resolve = resolve;
})
}
}
综合上面的代码我们可以知道
在 Vue 2.4 之前都是使用的 microtasks
,但是 microtasks
的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macrotasks
又可能会出现渲染的性能问题。所以在新版本中,会默认使用 microtasks
,但在特殊情况下会使用 macrotasks
,比如 v-on。
对于实现 macrotasks
,会先判断是否能使用 setImmediate
,不能的话降级为 MessageChannel
,以上都不行的话就使用 setTimeout
setImmediate传送门
MessageChannel传送门
event-loops传送门
总结一下今天的知识
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步