文章目录
老规矩 先放链接致敬作者
同步任务和异步任务详解
浏览器的线程有哪些
宏任务和微任务到底是什么?
js中的同步和异步的个人理解
简单理解Vue中的nextTick
async 、await、Generator 原理实现
es6的generator解决异步编程
Promise 值穿透 特性
红 色 字 \color{#FF0000}{红色字}红色字
浏览器线程
浏览器的渲染进程是多线程的。js是阻塞单线程的
GUI线程
GUI线程就是渲染页面的,他解析HTML和CSS,然后将他们构建成DOM树和渲染树就是这个线程负责的。
JS引擎线程
这个线程就是负责执行JS的主线程,前面说的"JS是单线程的"就是指的这个线程。大名鼎鼎的Chrome V8引擎就是在这个线程运行的。需要注意的是,这个线程跟GUI线程是互斥的。互斥的原因是JS也可以操作DOM,如果JS线程和GUI线程同时操作DOM,结果就混乱了,不知道到底渲染哪个结果。这带来的后果就是如果JS长时间运行,GUI线程就不能执行,整个页面就感觉卡死了。所以我们最开始例子的while(true)这样长时间的同步代码在真正开发时是绝对不允许的。
定时器线程
前面异步例子的setTimeout其实就运行在这里,他跟JS主线程根本不在同一个地方,所以“单线程的JS”能够实现异步。JS的定时器方法还有setInterval,也是在这个线程。
事件触发线程
定时器线程其实只是一个计时的作用,他并不会真正执行时间到了的回调,真正执行这个回调的还是JS主线程。所以当时间到了定时器线程会将这个回调事件给到事件触发线程,然后事件触发线程将它加到任务队列里面去。最终JS主线程从任务队列取出这个回调执行。事件触发线程不仅会将定时器事件放入任务队列,其他满足条件的事件也是由他负责放进任务队列。
异步HTTP请求线程
这个线程负责处理异步的ajax请求,当请求完成后,他也会通知事件触发线程,然后事件触发线程将这个事件放入任务队列给主线程执行。
所以JS异步的实现靠的就是浏览器的多线程,当他遇到异步API时,就将这个任务交给对应的线程,当这个异步API满足回调条件时,对应的线程又通过事件触发线程将这个事件放入任务队列,然后主线程从任务队列取出事件继续执行。
线 程 和 进 程 的 区 别 有 : \color{#FF0000}{线程和进程的区别有:}线程和进程的区别有:
1、定义不一样,进程是执行中的一段程序,而一个进程中执行中的每个任务即为一个线程。
2、一个线程只可以属于一个进程,但一个进程能包含多个线程。
3、线程无地址空间,它包括在进程的地址空间里。
4、线程的开销或代价比进程的小。
宏任务(macrotask) 微任务(microtask)
ES6 规范中,microtask 称为 jobs,macrotask 称为 task
宏任务是由宿主发起的,而微任务由JavaScript自身发起。
在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。

同步异步
其实同步和异步,无论如何,做事情的时候都是只有一条流水线(单线程),同步和异步的差别就在于这条流水线上各个流程的执行顺序不同。最基础的异步是setTimeout和setInterval函数,很常见,但是很少人有人知道其实这就是异步,因为它们可以控制js的执行顺序。我们也可以简单地理解为:可 以 改 变 程 序 正 常 执 行 顺 序 的 操 作 就 可 以 看 成 是 异 步 操 作 \color{#FF0000}{可以改变程序正常执行顺序的操作就可以看成是异步操作}可以改变程序正常执行顺序的操作就可以看成是异步操作。
具体来说,异步运行机制如下:
(1)所有同步任务都在主线程( j s 引 擎 线 程 ) \color{#FF0000}{(js引擎线程)}(js引擎线程)上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行( 还 是 在 j s 引 擎 线 程 执 行 的 ) \color{#FF0000}{(还是在js引擎线程执行的)}(还是在js引擎线程执行的)。
(4)主线程不断重复上面的第三步。
注 意 : 任 务 分 为 了 同 步 任 务 和 异 步 任 务 ; 而 异 步 任 务 又 可 以 分 为 微 任 务 和 宏 任 务 \color{#FF0000}{注意:任务分为了同步任务和异步任务;而异步任务又可以分为微任务和宏任务}注意:任务分为了同步任务和异步任务;而异步任务又可以分为微任务和宏任务
同步异步的任务重点是要放入浏览器的不同线程运行、宏任务微任务主要关注于运行顺序,setTimeout是异步、是宏任务,Promise是微任务、本身是同步执行,但Promise 的回调函数属于异步任务,会在同步任务之后执行(比如说then、catch、finally)。 Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。
回调
解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)
回调函数的目的 传入一个函数,在一定时机调用
有promise之前 获取函数中异步操作的结果,则必须通过回调函数获取
生命周期函数其实就是是回调函数
Promise (值穿透看链接文章即可)
Promise就是为了解决callback的问题而产生的。
优点:解决了回调地狱的问题
缺点:无法取消 Promise ,错误需要通过回调函数来捕获
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装
promise构造函数是同步执行的,then方法是异步执行的
Promise说明:Promise是一个容器,Promise是立即执行的,但是往往里面放异步任务.
pending 初始状态,既不是成功,也不是失败
fulfilled 操作成功完成
rejected 操作失败
处于 pending 状态的 Promise 对象可能会变为fulfilled 状态并传递一个值给相应的状态处理方法,也可能变为失败状态(rejected)并传递失败信息。
当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)
generator
next方法会执行函数体,直到遇到第一个yield语句,然后挂起函数执行,等待后续调用。但是next会返回一个对象,这个对象有2个key其中value表示yield语句后面的表达式的值,done是个布尔值,表示函数体是否已经执行结束。再次调用g。next时,执行流在挂起的地方继续执行,直到遇到第2个yield,依次类推。
function asyncfun(v) {
setTimeout(function(){
let r=v+20;
//请求返回后最为next()的参数
console.log(g.next(r));
},500)
}
function* generate() {
//执行请求
let x=yield asyncfun(1);
return x;
}
let g=generate();
g.next();
0.5秒后输出如下
{value: 21, done: true}
done: true
value: 21
[[Prototype]]: Object
setTimeout setInterval
遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒
ES6 async和await
async和await是如何处理异步任务的?简单说,async是通过Promise包装异步任务其实
await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator
比如有如下代码:
总 结 : a s y n c 是 通 过 P r o m i s e 包 装 异 步 任 务 , a s y n c 2 在 p r o m i s e 里 、 a s y n c 1 自 身 的 代 码 在 t h e n 里 , 所 以 先 打 印 2 再 是 1 \color{#FF0000}{总结:async是通过Promise包装异步任务,async2在promise里、async1自身的代码在then里 ,所以先打印2再是1}总结:async是通过Promise包装异步任务,async2在promise里、async1自身的代码在then里,所以先打印2再是1
async function async1() {// 在执行到await之后会让线程给async2
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
改为ES5的写法:
new Promise((resolve, reject) => {
// console.log('async2 end')
async2()
...
}).then(() => {
// 执行async1()函数await之后的语句
console.log('async1 end')
})
nextTick
Promise,process.nextTick谁先执行?
因为process.nextTick为Node环境下的方法,所以后续的分析依旧基于Node。
process.nextTick() 是一个特殊的异步API,其不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。
所以,nextTick和Promise同时出现时,肯定是nextTick先执行,原因是nextTick的队列比Promise队列优先级更高。
vue中的nextTick
在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中
在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted()钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。
看实际情况,一般在 created(或beforeRouter) 里面就可以,如果涉及到需要页面加载完成之后的话就用 mounted。
在created的时候,视图中的html并没有渲染出来,所以此时如果直接去操作html的dom节点,一定找不到相关的元素
而在mounted中,由于此时html已经渲染出来了,所以可以直接操作dom节点,(此时document.getelementById 即可生效了)。
vm.$nextTick 接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。
这个API就是基于事件循环实现的。
“下次DOM更新周期”的意思就是下次微任务执行时更新DOM,而vm.$nextTick就是将回调函数添加到微任务中(在特殊情况下会降级为宏任务)。
因为微任务优先级太高,Vue 2.4版本之后,提供了强制使用宏任务的方法。
vm.$nextTick优先使用Promise,创建微任务。
如果不支持Promise或者强制开启宏任务,那么,会按照如下顺序发起宏任务:
优先检测是否支持原生 setImmediate(这是一个高版本 IE 和 Edge 才支持的特性)
如果不支持,再去检测是否支持原生的MessageChannel
如果也不支持的话就会降级为 setTimeout。
例题
console.log("script start");//1.1
async function async1() {
await async2();
console.log("async1 end");//2.1
}
async function async2() {
console.log("async2 end");//1.2
}
async1();
setTimeout(function () {
console.log("setTimeout");//3
}, 0);
new Promise((resolve) => {// promose本身同步执行 回调是微任务
console.log("Promise");//1.3
resolve();
})
.then(function () {
console.log("promise1");//2.2
})
.then(function () {
console.log("promise2");//2.3
});
console.log("script end");//1.4
解答:
宏任务先运行,没有宏任务(js代码块),哪来的微任务(这题里的微任务都是promise的then,promise是立即执行的 在第一轮宏任务里)。
第一轮宏任务打印:script start 、async2 end、 Promise 、script end
产生微任务后 微任务优先级高于宏任务 所以:
第一轮微任务打印:async1 end 、promise1、promise2
(此时微任务队列清空,且存在其他宏任务,进入下一轮事件循环)
第二轮宏任务: setTimeout
注:
Event Loop中,每一次循环称为tick,每一次tick的任务如下:
执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
检查是否存在微任务,有则会执行至微任务队列为空;
如果宿主为浏览器,可能会渲染页面;
开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。