1. JavaScript 单线程
我们通常说,javascript是单线程,指的是解释和执行js代码的引擎是单线程。
而对于浏览器来说,浏览器并不是单线程的,浏览器的线程通常包括:渲染引擎线程(负责页面的渲染), js引擎线程(解释和执行js代码),定时器处理线程(处理setTimeOut,setInterval),事件处理线程(比如点击事件,键盘事件等),ajax线程(处理ajax请求)。2.关于setTimeout和setInterval
定时器仅仅是计划在未来某个时间执行,执行的时机并不能保证。因为在页面的生命周期中,不同时间可能有其他代码控制javascript执行。在页面下载完成后的代码运行、事件处理程序,ajax回调函数必须使用同样的线程运行。可以把事件处理程序和ajax回调函数,想象成一段 javascript待处理的消息队列。
setTimeout(function () { console.log("setTimeOut")}, 100);//这样一段代码并不是 100ms之后执行,而是100ms 之后加入到消息队列中 //执行代码的间隔的时间一般会大于等于指定的时间setInterval(function () { console.log("setInterval")},100);//每隔100ms就将代码加入到消息队列setTimeout(function () { setTimeout(arguments.callee,100);}, 100);//用这种方法 代替 重复定时器复制代码
setInterval创建的定时器是重复定时器,确保代码每个一段时间就将代码加入到消息队列。这种重复的定时器规则有两个问题:1.某些间隔会被跳过 2.多个定时器的代码执行之间的间隔可能比预期小。
(具体原因,可以参考 javascript高级程序设计第三版中的22.3)3.消息队列,事件循环
刚才提到,对于一些事件处理程序,ajax回调函数会被加入到 js待处理的消息队列中,同样的定时器处理程序,也会被添加到消息队列中,实际上一些异步的处理,都会被放入消息队列中。js会从消息队列里面取出待处理的程序执行。
(function () { console.log('start'); setTimeout(function cb() { console.log('setTimeout'); }); new Promise(function (resolve, reject) { if (true) { resolve(); } }).then(function () { console.log("promise"); }); console.log('end');})();复制代码
这段代码,看起来是promise.then()先加入了消息队列,然后才是setTimeout加入了消息队列,预期的输出结果应该是start,end, setTimeout、promise,但是实际上,输出结果却是start,end, promise, setTimeout,注意到了吗,promise在setTimeout之前就输出了。
原因是这样的:首先,一个浏览器环境,只能有一个事件循环,一个时间循环可以有多个任务队列。有一个事件循环,但是任务队列可以有多个。setTimeOut 属于 macrotask(宏任务) 而 promise.then 属于microtask(),整个script也属于macrotask,执行的时候,遇到setTimeOut,把它放入了macrotask ,遇到promise.then 把它放入了microtask, 执行完一个macrotask的时候,接下来 的执行顺序是,执行microtask队列的所有任务,microtask队列的所有任务,执行完之后,在执行macrotask 队列的任务,之后再检查,microtask队列的任务,并执行。依次循环。具体参考
4.关于microtasks和macrotasks
microtasks包括: process.nextTick,promise,Object.observe,MutationObserver
macrotasks包括: setTimeout,setInterval,setImmediate,I/O,UI渲染在vue.js中有这样一个方法,nextTick, 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。怎么能在下次DOM更新循环之后执行延迟回调呢?实际上在Vue.$nextTick的源码中,是通过promise将回调函数,放入了microtask中,在当前栈内的任务执行完之后,优先执行microtask中的任务。
具体参考