javascript单线程,异步与执行机制

js的单线程模型与游览器的进程/线程息息相关,在了解js单线程与异步的时候,建议先看看这篇文章

为什么是单线程

  • 由于js是可操作dom的,如果js是多线程,在多线程的交互下,处于界面中的dom节点就可能成为一个临界资源。
  • 这个时候,如果两个线程同时操作一个dom,一个负责修改,一个负责删除,这时就会出现问题。
  • 虽然可以通过锁来解决上面的问题,但为了避免因为引入了锁而带来更大的复杂性,js在最初就选择了单线程。

为什么需要异步

  • 由于js是可操纵dom的,如果在修改这些dom的同时渲染界面(即js线程和gui线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
  • 为了防止渲染出现不可预期的结果,浏览器将gui线程与js引擎线程设置为互斥关系,当js引擎执行时,gui线程会被挂起,等到js引擎线程空闲时才会被执行。
  • 所以,如果js执行时间过长(同步ajax),就会让页面卡死,造成渲染阻塞。因此,js的异步特性就显得很有必要了。

如何实现异步

  • 通过事件驱动机制,来实现异步任务等待,同步任务先执行。
  • 当js主线程执行完同步任务后,再自动去拿留待的异步任务去执行。

异步编程模型

  • 传统异步回调的问题
    • 代码可读性
    • 流程控制
    • 异常和错误处理
  • 异步编程的变革
    • Promise
    • Generator
    • Async/await

执行机制

  • js执行涉及主线程和执行栈,所有的程序任务都会被放到执行栈中被主线程执行。
  • js执行采用后进先出的原则。当函数执行的时候,会被添加到栈的顶部;当执行栈执行完后,就会从栈顶被移出,直到栈内被清空。
  • 主线程执行,由js引擎线程负责;事件队列,由事件触发线程管理。

事件驱动机制

  • 事件驱动机制(event driven)通过事件队列(event queue)和事件循环(event loop)来实现。
  • 事件队列(event queue),也称消息队列/任务队列,由异步I/O操作发起,里面存放着各种事件消息,这些消息都关联着回调函数。
  • 事件循环(event loop),是指js主线程重复从消息队列中取消息、执行的过程。
  • 模型图示

1516738-20181023204907352-916047888.png

任务类型

  • 从执行时机的角度
    • 同步任务,存放在执行栈中,会被主线程依次执行的任务
    • 异步任务,存放在事件队列中,会在异步操作有了结果后,将注册回调放入这个队列,等待主线程空闲时,被拉取到执行栈中执行。(空闲时,意味着同步任务已被执行完,执行栈为空了)
  • 从提供者的角度
    • 宏任务(macrotask),由宿主环境提供——全局script,setTimeout,setInterval,setImmediate,I/O,UI rendering,postMessage,MessageChannel
    • 微任务(microtask),由语言标准提供——Promise.then,process.nextTick,Object.observe(已废弃),MutationObserver

任务机制

  • 所有同步任务在执行完之前,任何的异步任务是不会执行的。
console.log("A");
setTimeout(function(){
   console.log("B");
},0);
while(true){}
// 结果为A。因为同步任务被死循环卡住了,任务队列里的任务不会被主线程拉取进执行栈
  • 每执行一个宏任务后,就会执行所有微任务。

js_macrotask_microtask.png

  • 为了使js任务与dom任务能够有序执行,会在一个task执行结束后,在下一个task执行开始前,对页面进行重新渲染 (task(宏->微)->render->task(宏->微)-->...)

宏任务/微任务拓展

  • 1个事件循环中,宏任务可以有多个,微任务只有1个。
  • 以银行排号为例,1个柜台对应多个用户,每个用户都是1个宏任务,当用户办完(宏)主任务后,突然想到要办理很多(微)次任务,银行柜员会一次帮他解决所有需求,而不是让他重新排队
  • 程序模型图示

1516738-20190414151028626-1357637309.png

  • 执行机制详述
  1. 执行一个宏任务,主栈中没有就从事件队列中获取。
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中。
  3. 宏任务执行完毕后,立即依次执行当前微任务队列中的所有微任务。
  4. 当微任务执行完毕,开始检查渲染,然后gui线程接管渲染。
  5. 渲染完毕后,js线程继续接管,开始下一个宏任务。
console.log('1');
setTimeout(function() {
    console.log('5');
    Promise.resolve().then(function() {
        console.log('6');
    })
}, 0);
Promise.resolve().then(function() {
    console.log('3');
}).then(function() {
    console.log('4');
});
console.log('2');

// 1,2,3,4,5,6
// 第一轮任务中,宏任务为全局script(刚好处于执行栈内,不用在事件队列中取),所以先是1,2;
// 同时,由于执行过程中遇到了setTimeout,将其再放入宏任务队列,遇到了promise,将其放入微任务队列;
// 该轮宏任务执行完毕后,开始执行微任务,将微任务全部取出,一次执行,因此再是3,4;
// 开始第二轮任务,取出的宏任务为setTimeout回调,因此结果是5;
// 同时执行这轮宏任务回调时,又遇到promise,再将其放入微任务队列;
// 当这轮宏任务setTimeout回调结束后,立即刚才加入的微任务取出执行,因此结果为6;
  • api优先级顺序
    • html5新特性MutationObserver属于微任务,优先级小于Promise
    • html5新特性MessageChannel属于宏任务,优先级是:setImmediate->MessageChannel->setTimeout。
    • 在node环境的微任务执行中,process.nextTick的优先级高于promise。
    • 在node环境的宏任务执行中,setImmediate的优先级高于setTimeout。
  • Vue.nextTick实现
    • 2.4版本时,Vue通过利用MutationObserver来模拟nextTick(MutationObserver为H5新特性,用于监听一个dom变动, 当dom对象树发生任何变动时,Mutation Observer会得到通知)
    • 2.5版本开始,nextTick实现移除了MutationObserver的方式(兼容性原因), 取而代之的是使用MessageChannel (当然,默认情况仍然是Promise,不支持才兼容的)
    • 由于,js执行是单线程,在一个tick的过程中,可能会存在多次修改数据,vue会把这些数据修改先统一push到一个队列里,然后内部调用1次nextTick去更新视图。
    • 因此,vue从数据改变到dom视图变化是需要在下一个tick才能完成的,这种数据驱动变化的原理符合游览器的原理(js引擎线程和gui渲染互斥)和处理策略(task(宏->微)->render->task(宏->微)-->...)
    • 最终,Vue.nextTick采取的策略是默认走 microtask,对于一些dom交互事件,如v-on绑定的事件回调函数的处理,会强制走macrotask。对于macrotask的执行,vue优先检测是否支持原生setImmediate(高版本游览器支持),不支持的话再去检测是否支持原生的MessageChannel,如果也不支持的话就会降级为setTimeout 0。

参考

转载于:https://www.cnblogs.com/kanyu/p/9838976.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值