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 <value>
语句,然后暂停,并返回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–摸鱼开始 | |
// 主线程: 摸鱼完毕 |