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–摸鱼开始 | |
| // 主线程: 摸鱼完毕 |
