马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
本帖最后由 不二如是 于 2018-12-16 11:23 编辑
在上一讲我们介绍了JS的回调,并提到了JS是单线程,这次好好科普一下JS中的执行机制(Event Loop)。
为什么 JS 是单线程的?
作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM 。
这决定了它只能是单线程,否则会带来很复杂的同步问题。
比如:
假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点。
这时浏览器应该以哪个线程为准?
很明显,多线作战会带来很多麻烦,那咋办?
将JS执行机制设计为单线程好了,这就意味着:
所有任务都需要排队,前一个任务结束,才能执行后一个任务。
一个一个的执行,也带了一个潜在风险:
如果前一个任务耗时很长,那么后一个任务就不得不一直等待。
于是乎,JS 设计者们把所有任务分成两类:
同步:只有前一个任务执行完毕,才能执行后一个任务。
异步:当同步任务执行到某个 WebAPI 时,就会触发异步操作,此时浏览器会单独开线程去处理这些异步任务。
这两个概念,小甲鱼老师有讲过,再发一次传送门:006 - 为网页注入灵魂
稍微解释下WebAPI。
浏览器事件、定时器、ajax,这些操作不会阻塞 JS 的执行,JS 会跳过当前代码,执行后续代码。
任务队列( Task Queue ):主线程执行完毕后所触发的异步任务( WebAPIs ),叫任务队列。
回调队列( Callback Queue ):这些异步 WebAPI 执行完成后得到的结果,会添加到 callback queue 中。
事件循环( Event Loop ):只要主线程的同步任务执行完毕,就会不断的读取 "回调队列" 中的回调函数,到主线程中执行,这个过程不断循环往复。
如何知道主线程执行执行完毕?
JS引擎存在 monitoring process 进程,会持续不断的检查主线程执行为空。
一旦为空,就会去 callback queue 中检查是否有等待被调用的函数。
没办法,必须铺垫一些概念,先来上代码:console.log("1");
// 定时器
setTimeout(function () {
console.log("2");
},1111);
console.log("3");
输出结果:
先打印1,浏览器正常的开始流程。
遇到 WebAPI( setTimeout ) ,浏览器新开定时器线程处理,执行完成后把回调函数存放到回调队列中。
专业一点的说发:
JS 引擎遇到异步任务后不会一直等待其返回结果,而是将这个任务挂起交給其他浏览器线程处理,自己继续执行主线程中的其他任务。
这个异步任务执行完毕后,把结果返回给回调队列。
被放入的代码不会被立即执行。
而是当主线程所有同步任务执行完毕, monitoring process 进程就会把 "回调队列" 中的第一个回调代码放入主线程。
然后主线程执行代码,如此反复。
打印3异步 setTimeout 不会阻塞同步代码,因此会首先打印3。
主线程执行完毕后,执行 Callback Queue 打印2。
上面这个流程意味着异步执行任务是有优先级滴!
宏任务与微任务
异步任务的执行优先级并不相同,它们被分为两类:微任务( micro task ) 和 宏任务( macro task ) 。
宏任务( macro-task ):整体 script、setTimeout、setInterval、UI交互事件、I/O。
微任务( micro-task ):process.nextTick、Promise、MutaionObserver。
根据异步事件的类型,这些事件实际上会被派发对应的宏任务和微任务中。
在当前主线程执行完毕后,会先查看微任务中是否有事件存在,如果不存在,则再去找宏任务。
如果存在,则会依次执行队列中的参数,直到微任务列表为空,让后去宏任务中一次读取事件到主线程中执行,如此反复。
当前主线程执行完毕后,会首先处理微任务队列中的事件,让后再去读取宏任务队列的事件。
在同一次事件循环中,微任务永远在宏任务之前执行。
概念讲完,我们拿上面的代码领悟下。
setTimeout是宏任务,正常的console.log()是微任务,所以在执行输出1后,setTimeout不会立即执行,等待第二个微任务完成,即打印3。
主线程执行完毕后,执行 Callback Queue 最终打印2。
还不过瘾?
再来一段代码之前,再来科普一个很厉害的方法。
Promise的含义
Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
从语法上说,Promise是一个对象,从它可以获取异步操作的消息。
Promise提供统一的API,各种异步操作都可以用同样的方法进行处理,是ES6中新加的一个方法。
实例:var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。
它们是两个函数,由JavaScript引擎提供,不用自己定义。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数,实例:promise.then(function(value) {
// success
}, function(value) {
// failure
});
then方法可以接受两个回调函数作为参数。
第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。
其中,第二个函数是可选的,不一定要提供。
这两个函数都接受Promise对象传出的值作为参数。
实例:let promise = new Promise(function(resolve, reject) {
console.log('promise-Promise');
resolve();
});
promise.then(function() {
console.log('then-Resolved.');
});
console.log('微任务');
输出结果:
上面代码中,Promise新建后立即执行,所以首先输出的是“promise-Promise”。
然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以“then-Resolved.”最后输出。
目前我们只需要了解到这个层级,深入了解请自行搜索哈。
实战
好了,知识点都有了,我们来个稍微难一点的例子。
代码:(function fishc(){
setTimeout(function(){console.log(4)},1111);
new Promise(function(resolve,reject){
console.log(1);
for(var i = 0 ; i < 88 ; i++){
if(i == 33) resolve();
}
console.log(2);
}).then(function () {
console.log(5);
});
console.log(3);
})()
如果理解了上面的概念,看到上面的结果,应该很开心。
setTimeout,宏任务,存入宏任务队列,所以4不会立即执行。
Promise,函数本身是同步执行的,所以输出1。
执行完Promise,输出2。
then,异步执行,因此打印完1,2,then存入微任务中,然后打印3。
第一次主线程执行完毕。
执行微任务中的回调函数,输出then中的5.
然后执行宏任务中的setTimeout,输出4。
最后告诉一个本人总结的心法:
只要主线程执行完毕,就立马执行setTimeout中的回调代码。
如果喜欢,别忘了评分 :
这位鱼油,如果喜欢本系列Js帖子,请订阅 专辑☞( 传送门)( 不喜欢更要订阅 )
|