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实现各种“异步”功能的基础。比如赫赫有名的延时方法setTimeoutsetTimeout(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请求 等。
  • 顺序轮询执行:不断轮询队列并执行:
  1. 执行任务时,永远不会进行渲染(render),与GUI线程互斥。
  2. 浏览器为了让JS内部宏任务与DOM操作能够有序的执行,会在一个宏任务执行结束后,下一个宏任务执行开始前,对页面进行重新渲染。
  3. 如果在执行一个长耗时任务,页面会“无响应”,可以拆分为多个子任务通过延时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))会自动运行。参数resolvereject是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() 添加一个不管状态如何都会执行的回调,没有参数也没返回值,后面继续thencatch

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...catchpromise中的所有函数都包含一个隐式的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语法糖

awaitasync 是使用Promise更简易的一种语法糖,就像写同步代码,完全可以代替Promise的语法形式。

  • async functionasync在函数申明前使用,让函数返回一个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() 消息解析错误的异常事件

??操作步骤

  1. 创建处理worker线程任务的JS文件,在JS文件中订阅消息事件用于接收消息指令。
  2. 基于该指定的JS文件创建worker线程,该JS文件代码会被执行。
  3. 订阅worder的消息通知,接收worker发送过来的消息。
  4. 给worker发送消息(指令),让他干活。
  5. 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

??版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀

相关