JavaScript入门⑨-异步编程●异世界之旅
JavaScript入门系列目录
- JavaScript入门⑩-ES6归纳总结
01、JS线程与事件循环
JavaScript的是单线程的语言,按顺序执行。事件循环(Event loop)是JS的运行机制,也是JS实现各种“异步”功能的基础。
1.1、浏览器进程
浏览器本身是多进程的(Edge/Chrome),在系统的的任务管理器中可以看到,只打开了一个页面,却有多个进程。其中渲染进程(浏览器内核)就是页面的管家,负责页面的渲染、脚本执行、事件等,每个页面(浏览器页签)会有一个独立的管家——渲染进程。
而在渲染进程中,又有多个线程,具有不同的职责,负责不同的事务。比如有定时器线程、HTTP请求线程、事件触发线程、渲染线程、JS引擎线程等,除了HTTP线程基本都是单线程。
- 定时器线程,就是用于管理
setTimeout
/setInterval
定时任务的,当到达指定时间了就把要执行的任务(函数)放到一个任务队列中,等待JS引擎去执行。so,定时器一般都不准,有一点延迟。也不能这么说,定时器并没有错,应该是队列和JS引擎的问题。 - HTTP请求线程,负责执行HTTP请求,包括各种资源加载。当请求完成、或请求的状态变化时,把触发的回调函数放入事件队列,交给JS线程去执行。
- GUI线程:负责浏览器页面的渲染,如解析HTML、CSS,构建DOM树,布局计算和绘制等。GUI线程和JS线程是互斥的,所以当JS执行一个长任务时,会造成页面UI的卡顿。
- 事件触发线程:用来控制事件的循环,各种事件首先是会在事件触发线程里处理,当满足条件触发事件执行时,把待执行的事件处理任务(函数)添加到JS的任务队列中。
- ??JS引擎线程(主线程):用于页面JavaScript代码的解析和执行。
1.2、JS的单线程与异步
JavaScript的是单线程的语言,一个页面渲染进程中只有一个JS线程,意味着同一时刻只能干一件事情,那么多的JS代码都必须顺序执行。
?为什么采用单线程呢?核心目的是为保障DOM操作的一致性,避免同时操作DOM引起的渲染混乱,也可能就是为了便于实现??。同时缺点也很明显,如果有长耗时操作时就会引起阻塞,导致页面卡顿。为了解决这个问题,JS执行代码有同步、异步两种模式,浏览器提供了多种异步编程方案。
异步编程方案 | 描述 |
---|---|
回调函数 | 最常用的一种异步模式,也是异步的基础。缺点是容易形成地狱回调 |
事件监听 | 绑定事件处理程序,触发执行 |
发布订阅模式 | 自定义(或第三方)一个消息中心,基于消息的发布、订阅来驱动 |
Promise(async/awit) | ES6支持的异步编程API,好用!async /awit 是其语法糖 |
生成器Generator/ yield | 基于生成器Generator的可暂停、恢复函数执行的特性 |
worker线程 | 正式的线程,可创建一个独立上下文环境的线程,和主线程采用消息通信 |
1.3、事件循环(Event Loop)
JS线程解析代码时,把一些异步任务交给其他工作线程去处理,如HTTP请求、事件、setTimeout
,这些工作线程处理完会把回调任务(函数)放到任务队列中给JS线程来执行。JS线程会一直轮询任务队列并进行处理,这就是JS的“事件循环(Event Loop)”。
- 任务(事件)队列:任务队列是一个先进先出的队列,它里面存放着各种待处理任务(代码/函数)。
- 事件循环:事件循环是指JS主线程循环从任务队列中获取任务、执行任务的过程。
事件循环(Event loop)是JS的运行机制,也是JS实现各种“异步”功能的基础。比如赫赫有名的延时方法setTimeout
,setTimeout(func,0)
(参数delay
=0)并不是真正立即执行,而是把func
放到一个任务队列里。如果JS引擎刚好有空,就会立即召唤他,否则就老老实实排队等候被轮。
??一段setTimeout
代码:
setTimeout(function () {
console.log(1 + 1);
}, 3000);
- ① JS线程:
setTimeout(callback,timeout)
函数会调用定时器线程,把这个定时任务交给他。 - ② 定时器线程:我只是一个计时器,
timeout
时间到了,就把回调函数放入任务队列,由JS线程来执行。 - ③ JS线程:轮询任务队列(有空的时候),执行回调函数。
1.4、?宏/微?任务队列
上面说的JS任务队列,大致分为宏任务(macro task)队列、微任务(micro task)队列。
??宏任务队列:JS引擎的任务队列,按顺序轮询排队执行,没有优先级,发生在渲染之前。
- 哪些任务:script整体代码、各种UI/IO事件任务、延时任务
setTimeout
/setInterval
、网络HTTP请求 等。 - 顺序轮询执行:不断轮询队列并执行:
- 执行任务时,永远不会进行渲染(render),与GUI线程互斥。
- 浏览器为了让JS内部宏任务与DOM操作能够有序的执行,会在一个宏任务执行结束后,下一个宏任务执行开始前,对页面进行重新渲染。
- 如果在执行一个长耗时任务,页面会“无响应”,可以拆分为多个子任务通过延时
setTimeout
执行。
??微任务(Microtask):比宏任务优先级高的微任务,发生在宏任务之后、渲染之前:当执行完一个宏任务后优先执行(清空)所有微任务。
- 哪些任务:异步
Promise
创建的待执行.then/catch/finally
会成为微任务;通过方法queueMicrotask(func)
添加的任务(优先UI任务执行)。 - 优先执行:微任务会在执行任何其他事件处理、或渲染、或执行任何其他宏任务之前完成。微任务会被优先执行,不愧是超级无敌VIP会员。
console.log("sync-1");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve()
.then(() => console.log("promise.then"))
.finally(() => console.log("promise.finally"));
queueMicrotask(() => console.log("queueMicrotask"));
console.log("sync-2");
// sync-1
// sync-2
// promise.then
// queueMicrotask
// promise.finally
// timeout
02、Promise异步编程
Promise(IE??)是现代 JavaScript 中异步编程的基础,比传统的回调异步更强大、更简洁。Promise是一个对象,可以理解为一个容器,保存了一些异步操作(及执行的状态信息),并管理和调度这些异步操作。(Promise /?pr?m?s/ 承诺)
2.1、基础语法
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),状态是由异步操作执行结果决定的。
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled):意味着操作成功成功。(fulfilled /f?l?f?ld/ 完成)
- 已拒绝(rejected):意味着操作失败。(rejected /r??d?ekt?d/被拒绝)
??Promise 的标准语法:
// 标准语法
let promise = new Promise(function (resolve, reject) {
//some code
if (true) //根据需要返回成功、失败的状态。
resolve("OK");
else
reject("failure"); //如果发生异常,会自动捕获并进行拒绝reject(error)处理
});
//链式操作
promise.then(successCallback, ?failureCallback)
.catch(func)
.finally(func);
Promise的构造函数参数 excutor (function(resolve, reject))
会自动运行。参数resolve
、reject
是Promise提供的回调,在合适的地方调用他们即可,不过每次只有一个有效,他决定了promise的状态。
- resolve(value) :任务成功并带有结果
value
,该结果值value
可传输给后续的异步函数。promise
状态为“已兑现(fulfilled)” - reject(error) :任务失败,返回错误信息。
promise
状态为“已拒绝(rejected)”
??异步&微任务:
- Promise 的参数
excutor
是同步的,会立即执行。 - then()、catch()、finally()都是异步的,他们是在一个
promise
准备就绪后,被放到一个微任务队列里(microtask queue),等当前JS的任务执行完毕后,才开始执行这个队列中的任务。注意不是多线程,还是在JS线程里。
2.2、Promise方法-链式调用
Promise的属性方法:
?静态属性/方法 | 描述 |
---|---|
Promise.all(iterable) | 执行一个promise集合,都成功或有一个失败返回一个新promise对象(返回值的数组) |
Promise.allSettled(iterable) | 等到所有 promise 都已敲定,返回的promise包含返回值数组 |
Promise.any(iterable) | 任意一个 promise 成功,就返回那个成功的 promise 的值。 |
Promise.race(iterable) | 任意一个 promise 敲定,返回那个promise对象 |
Promise.reject(reason) | 返回一个状态为已拒绝的 Promise 对象,并将给定的失败信息传递给对应的处理函数 |
Promise.resolve(value) | 如参数为普通数据,返回一个状态为成功的 Promise 对象。其他参数promise、thenable |
?构造函数 | |
Promise(func(resolve, reject)) | 创建一个 Promise 对象,用于包装一个不支持异步的普通函数,这里的函数是同步执行的 |
?实例属性 | 内置属性,不可访问 |
[[PromiseState]] | promise 的状态 |
[[PromiseResult]] | resolve 返回的值,或reject 返回的错误error 。 |
?实例方法 | |
then(resolveFunc ,rejectFunc?) | 添加成功和失败的回调函数,实例promise 的状态决定调用哪个回调函数,参数2可空 |
catch(func(e)) | 添加一个被rejected 拒绝状态的回调函数,是then(null,rejectFunc) 的简写版本 |
finally() | 添加一个不管状态如何都会执行的回调,没有参数也没返回值,后面继续then 、catch |
Promise的then()
方法才是他的精髓,用于给promise
实例添加状态改变的回调函数,支持链式调用。
- 链式调用:
then()
、catch()
、finally()
都会返回新的promise
对象,所以它们可以被链式调用。 - 参数传递:
resolve
返回的值value
会作为参数传递给下一个then(func(value))
。
let p1 = new Promise((resolve, reject) => { resolve("0") });
p1=Promise.resolve('0'); //同上
p1.then(v => v + '1')
.then(v => v + '2')
.then(v => console.log(v)) //012
.catch(e => console.log(e))
.finally(() => console.log('end')); //end
2.3、异常处理
??隐式的try...catch
:promise
中的所有函数都包含一个隐式的try...catch
,并对捕获的异常进行拒绝(rejection )处理。错误信息会一直“冒泡”传递,直到一个rejection
处理程序来处理。
- 被拒绝的
promise
会找最近的rejection
处理程序进行处理,如catch()
、then()
的的第二个参数,推荐专门的catch()
来捕获处理。 - 如果后面一直都没人处理这个异常,会触发一个全局window的
error
—— unhandledrejection。 catch()
处理完后可以继续链式执行then()
。
let p = Promise.resolve(1);
p.then(value => { return value + 1; })
.finally(() => console.log("休息一下!"))
.then(value => { return value + 1; })
.then(value => { console.log(value) })
.then(value => { throw new Error("着火了"); })
.catch(err => { console.log(err) })
.then(value => { console.log(value) }) //undefined,没有参数了
.finally(() => console.log("end"))
// 休息一下!
// 3
// Error: 着火了
// undefined
// end
2.4、async/await语法糖
await、async 是使用Promise更简易的一种语法糖,就像写同步代码,完全可以代替Promise
的语法形式。
- async function:
async
在函数申明前使用,让函数返回一个promise
异步对象,同时允许函数内使用await
。 - await promise(func),只在
async
内部工作,awit
会等待一个promise(func)
执行完成,再继续往下。类似promise.then
,他并不会阻塞CPU,是异步的。
//async 修饰的函数返回值包装成一个promise
async function doAsync() {
return 1;
return Promise.resolve(1); //效果同上
}
doAsync().then(console.log);
console.log('a');
//输出:a 1
async function doAsync2(num) {
try { //异常处理,同promise.catch
let n = await ((num) => { return num + 1; })(num)
n = await new Promise((resolve, reject) => {
setTimeout(() => { resolve(n + 1); }, 1000);
}) //等待返回结果,再往下执行
console.log(n); //3
await (() => { console.log("result:" + n) })(n); //result:3
}
catch (error)
{ console.log(error) }
}
doAsync2(1)
console.log('doAsync');
//输出:doAsync 3 result:3
03、生成器*Generator
??什么是Generator?
- 她是一个迭代器,返回一个遍历器对象,符合可迭代协议和迭代器协议,可用
next()
、for(of)
迭代。 - 她是可控函数:内部代码可以自由控制暂停和继续执行。标准的函数是一次性执行完毕,直到末尾或
return
语句。而生成器的函数可以由yield
暂停执行(交出控制权),next()
恢复执行。 - 她是一个状态机,封装了多个内部状态。
- 她是异步任务管理容器,提供一种异步的实现方案。
3.1、基础语法
Generator 使用一个特殊的函数语法function*
(带星*
号)创建生成器generator
,调用生成器函数获得一个生成器对象,该对象的实例方法:
实例方法 | 描述 |
---|---|
next() | 恢复执行,返回一个由 yield 表达式生成的值:{value: 1, done: false} |
return(value?) | 返回给定的值并结束生成器,可提前中止生成器。 |
throw() | 向生成器抛出一个错误,生成器内部如没处理则会中止 |
//定义生成器
function* GeneratorN(s) {
console.log('yield-1');
yield s + 1;
console.log('yield-2');
yield s + 2;
console.log('yield-3');
return s + 3;
}
//创建生成器对象
var gn = GeneratorN(1);
//next()调用
console.log(gn.next()); //yield-1 {value: 2, done: false}
console.log(gn.next()); //yield-2 {value: 3, done: false}
console.log(gn.next()); //yield-3 {value: 4, done: true}
console.log(gn.next()); //{value: undefined, done: true}
??定义生成器:function* generatorName(s) { }
- yield:在
generator
(仅在)内部,用yield
表达式申明一个需要返回的值。(yield /ji?ld/ 收益) - return:非必须!作用是指定最后一次
next()
函数调用时的value
值,并标识迭代器状态完成done: true
。
??使用:在外部调用迭代器函数,并不是执行函数,而是返回一个生成器对象generator object
。
- 生成器对象
generator
的主要方法就是next()
,当调用next()
方法时,执行代码到最近的yield
语句,然后暂停,并返回yield
表达式的值。 next()
方法返回一个对象,表示当前阶段的信息:{value: 2, done: false}
- value 属性是
yield
语句后面表达式的值,表示当前阶段的值。- done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。已全部执行完成则为 true,否则为 false。
??指针:generator
对象内部存在一个“指针”,指向代码暂停的地方,调用next()
方法时,从指针位置执行代码直到下一个yield
语句,指针会移到到该yield
语句末尾。
??一个切换CSS状态的示例:
// 创建一个可无限循环的列表list
function* loop(list) {
let i = 0;
while (true) {
if (i >= list.length) i = 0;
yield list[i++];
}
}
function toggle(...actions) {
let gen = loop(actions);
return function (...args) {
return gen.next().value.apply(this, args);
}
}
// 绑定状态切换事件
switcher.addEventListener('click', toggle(
e => e.target.className = 'off',
e => e.target.className = 'warn',
e => e.target.className = 'on'
));
? 其他使用示例:
function* genAll() {
yield* [1, 2, 3]; //yield*:嵌套一个迭代对象,进入另一个生成器函数
let data = yield 'data:'; //申明接收一个next()参数,下次next(arg)的参数赋值给data
console.log(data);
}
let g = genAll();
console.log(g.next('a1')); //{value: 1, done: false}
console.log(g.next('a2')); //{value: 2, done: false}
console.log(g.next('a3')); //{value: 3, done: false}
console.log(g.next('a4')); //{value: 'data:', done: false}
console.log(g.next('a5')); //参数传入了:a5 {value: undefined, done: true}
console.log(g.next('a6')); //{value: undefined, done: true}
3.2、可迭代for(of)
迭代器对象是可迭代的,执行迭代时,自动调用next()
获取值,并判断状态done
。
- 可使用
for(of)
迭代获取所有next()
的值value
。 - 用展开操作符,展开所有所有
next()
的值value
。
function* GeneratorN(s) {
yield s + 1;
yield s + 2;
return s + 3;
}
//用for(of)遍历
for (let item of GeneratorN(100)) {
console.log(item); //101,102
}
//展开运算符,也没有return的值
console.log(...GeneratorN(10)); //11 12
?? 忽略return值:当
done: true
时,for..of
循环会忽略最后一个 value,即只有yield
表达式返回的值才有效。
3.3、async异步迭代器*
由于迭代器的暂停、恢复执行的特点,让他成为了实现异步编程的一种方案。比如常用的ajax
请求处理数据:
function* addUserPoints(id, points) {
yield ajax('/api/points/', { id: id, points: points });
let user = yield ajax('/api/userinfo/', { id: id });
user.points += points;
yield ajax('/api/user/', user);
yield 'end';
}
结合异步async
/await
,就可以像同步代码一样写异步代码了。可以把整个 Generator 函数看作异步任务的容器,异步操作需要暂停的地方,都用 yield 语句注明。
- async:迭代器函数加
async
,申明为异步generator
,此时内部可以用await
了。 - await:用
await
去等待一个promise
操作,比如ajax请求。此时调用next()
返回的就是一个promise
了。
async function* genN(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 2000)); //一个等待2s的异步操作
yield i;
}
}
let g = genN(1, 5);
console.log('start');
for(let i=1;i<=5;i++){
g.next().then(v => console.log(v.value)); //注意g.next()返回的是一个异步promise,可用await
}
console.log('end');
//输出:
//start
//end
//1 2 3 4 5 6 //间隔2s输出
04、worker线程
Worker(IE10)可以创建独立的线程,前端终于有了正式的线程了,所以JS也就不再是单纯的“单线程”了。可以充分分担主线程的压力,也能发挥多CPU多核的优势了。可以把一些非UI操作的任务放在worker中来执行,再也不用担心UI卡顿了。
Worker 基于一个JS文件创建一个独立的线程,该JS文件代码运行在这个新线程中。该线程是独立于主线程的,其全局上下文不是window了,一般情况就是DedicatedWorkerGlobalScope,可用self
来访问。需要注意一些事项:
- 同源:和主线程遵循同源策略。
- 不可操作UI:包括窗口、文档DOM、页面元素,
alert()
、confirm()
也是不可用的。 - 消息通信:和主线程不在一个上下文环境,只能通过消息进行通信。
??构造函数 | 描述 |
---|---|
Worker(jsUrl,options) | 创建一个专用 Web worker |
??实例方法 | |
postMessage(message) | 向worker发生一个消息 |
terminate() | 立即终止 worker,是立即 |
??事件 | |
onmessage=func(message) | 订阅worker的消息,消息在message.data 上 |
onerror=func(e) | 发生错误的异常事件 |
onmessageerror=func() | 消息解析错误的异常事件 |
??操作步骤:
- 创建处理
worker
线程任务的JS文件,在JS文件中订阅消息事件用于接收消息指令。 - 基于该指定的JS文件创建worker线程,该JS文件代码会被执行。
- 订阅worder的消息通知,接收worker发送过来的消息。
- 给worker发送消息(指令),让他干活。
- worker收到的指令,并根据指令干活,干完后发送消息回去。
//********************** worker线程脚本 **********************//
console.log("kworker.js")
//接收消息,//这里的self是该工作线程的全局对象DedicatedWorkerGlobalScope,可以省略
self.addEventListener("message", message => {
self.receiveMessage(message.data);
//判断消息指令,开始干活
if (message.data == "摸鱼") {
console.log("kworker.js--摸鱼开始");
//do something
//发送消息到主线程
postMessage("摸鱼完毕");
}
})
function receiveMessage(mes) {
console.log('kworker.js--收到消息:', mes);
}
//********************** 主脚本 **********************//
console.log('创建worker线程');
//2基于指定的JS文件创建worker线程,该JS文件代码会被执行。
let thread = new Worker('../js/kworker.js');
//订阅发回来的消息
thread.onmessage = (mes) => console.log('主线程:',mes.data);
// 给wokder线程发送消息
thread.postMessage('呼叫');
thread.postMessage('摸鱼');
//********************** 输出 **********************//
// 创建worker线程
// kworker.js
// kworker.js--收到消息: 呼叫
// kworker.js--收到消息: 摸鱼
// kworker.js--摸鱼开始
// 主线程: 摸鱼完毕
参考资料
- Generator 详解
- 异步迭代和 generator
??版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀