JavaScript异步的实现

前端麦小子
• 阅读 332

JavaScript中的异步

你好,我是麦小子。

正如标题所讲,今天咱们要讨论的点是异步。

如果你对"异步"这个概念没有概念的话——正好,咱们从头开始。

生活中的异步

有个很有意思的问题,来自童年一部不错的动画片。

洗衣服(洗衣机)需要50min

烧水需要10min

吃饭需要20min

请问,做完这三件事一共需要几分钟?

如果你不加思考,张口就来。会不会回答80min呢?我想这是有可能的。因为当年我也是这么回答的,这个回答有错吗?显然是正确的,却不合逻辑。事实上,要做完以上三件事在最理想的状态下仅需50min.

80min和50min

两个答案都是正确的,不同的是思考方式或者生活方式。

前者需等某一件事做完才会去做第二件,后者在洗衣机工作时就已经开始烧水和吃饭了,做完这两件事甚至衣服还没洗完呢。

我们给这两种不同的方式以不同的名字

前者被称为同步

后者被称为异步

看起来,异步让小伙子显得更机灵。

JS中的异步

一颗栗子

吃完上面的栗子~下面来打另外一颗。这次咱们引入一段JS代码。

console.log('hello');
setTimeout(() => { console.log('setTimeout') }, 2000);
console.log('JavaScript');

它的打印结果可能会出乎意料

hello

JavaScript

setTimeout

原因很简单,setTimeout()函数是一个异步函数

我们知道,作为一个定时器设置函数。setTimeout()可以让程序在一段时间后调用某个函数(或执行某段代码)。正如前文所表,我们设置了2000毫秒之后打印 setTimeout

接下来,我们把浏览器想象成一个小伙子。

首先,他看到第一行代码,指令是打印一个 hello ,于是他往控制台打印一个 hello

然后,他看到第二行代码,创建了一个定时器,于是他就在那等着,等时间到了就去执行代码。可是转念一想,老这么等着也不是个事,干脆,我先看看后面还有指令不。

接着,他暂时离开了定时器,发现了第三行代码,指令是打印一个 JavaScript ,于是他往控制台打印一个 JavaScript

接下来,他发现在此之后已经没有可执行的代码,猛然想起,还有一个定时器呢~得回去看看。当他再一次遇到那个定时器,发现时间还没到。

于是,他就又等了一段时间。

后来,时间总算到了。他终于可以执行定时器里的代码了,指令是打印一个 setTimeout 于是它打印了一个 setTimeout

吃完上面的栗子,相信你对JS异步已经有了轮廓。

回调函数

很久以前,JavaScript 仅支持回调函数来实现异步操作。那么,什么是回调函数?

回调函数是一个被传递到另一个函数中的会在适当的时候被调用的函数,如

const callback = function(){
  console.log('this is a callback function');
}
const test = function(callback){
  callback();
}
test(callback);

异步返回值

某些情况下,setTimeout 会返回一个值,我们应该如何使用这个返回值?return 吗?

咱们打颗栗子

const countNum = function (value) {
    setTimeout(() => {
        value *= 2;
    }, 2000);
  return value;
};
console.log(countNum(2));
  • 查看一下打印结果

好想获取异步返回值呀==>2

这样呢?

const countNum = function (value) {
    setTimeout(() => {
        return value *= 2;
    }, 2000);
};
console.log(`好想获取异步返回值呀==>${countNum(2)}`);

抑或是这样?

const countNum = function (value) {
    return setTimeout(() => {
        value *= 2;
    }, 2000);
};
console.log(`好想获取异步返回值呀==>${countNum(2)}`);

事实上,以上代码打印的结果都是不符合我们预想的。

试试看回调吧~

const countNum = function (value, callback) {
  value *= 2;
    setTimeout(() => {
        callback(value);
    }, 2000);
  console.log(`先打印一次计算结果=>${value}`);
};
function printResult(value) {
    console.log(`回调打印计算结果是=>${value}`);
}
countNum(2, printResult);

查看一下打印结果

先打印一次计算结果=>4

回调打印计算结果是=>4

显然,回调函数可以实现我们的需求。

失败处理

在异步函数中使用成功与失败的不同回调

const workout = function(value,success,fail){
  setTimeout(() => {
    try {
      if (typeof value !== 'number') {
        throw '欸嘿~怎么不是数字呀~~';
      }
      success(value * 200);
    } catch (exception) {
      fail(value);
    }
  }, 1000);
}
const success = function(value){
  console.log(`算出结果了=>${value}`);
}
const fail = function(value){
  console.log(`${value}不是数字,算不了啊~~`);
}

回调的嵌套

function double(value,callback){
  setTimeout(() => {
    callback(value*2);
  }, 1000);
}
function doubleCallback(value){
  double(value,(result)=>console.log(`打印下结果=>${result}`))
}
double(1,doubleCallback);

仔细看下上面的代码,或许你会觉得可读性并不高,事实也正是如此。当逻辑更加复杂,回调的嵌套会更多。比如,MDN的厄运金字塔实例

期约

期约promise 的中文翻译。当然,你可以把它翻译成 约定 。需要保证的是,大家理解你的说法。

如果可能的话,我更愿意把它翻译成老板,他会告诉你将来会给你一个结果,却不知道什么时候给到,甚至也不确认的这结果的好坏。

比较好的应对方案是, 不给 rejectionFunc

初见 Promise

Promise 是 ES6 新增的引用类型。可通过 new 进行实例化

const pro = new Promise(()=>{});

打印一下 pro ,但见

Promise { <pending> }

以上的 pending 是 Promise 的一种状态——待定

一个 Promise 会处于以下三种状态之一

  • 待定

    • pending
  • 兑现

    • fulfilled OR 解决 -- resolved
  • 拒绝

    • rejected

pending 是 Promise 的初始状态,它可以 settled 为另一种状态,或者是 fulfilled 或者是 rejected。这种 settled 操作是不可逆的。

理解状态

  • pending -- 尚未开始或正在执行

  • fulfilled -- 已经完成

  • rejected -- 没有成功

当 Promise 状态转换为 fulfilled,会生成一个解决值 value ;

当 Promise 状态转换为 rejected,会生成一个拒绝理由 reason 。

二者可选,默认值为 undefined

关于构造

  1. Promise 构造接收一个执行器函数 executor

  2. executor 长这样

function(resolutionFunc, rejectionFunc){
   // 通常是一些异步操作
}
  1. resolutionFunc 和 rejectionFunc 长这样
resolutionFunc(value) // 当被敲定时调用
rejectionFunc(reason) // 当被拒绝时调用
// 二者参数可为任意类型

可以通过调用 resolutionFunc 和 rejectionFunc 为 Promise 切换状态

调用 resolutionFunc 可以切换到 fulfilled ,反之切换到 rejected

const one = new Promise((resolve,reject)=>resolve())
const two = new Promise((resolve,reject)=>reject())
console.log('one的状态是',one);
console.log('two的状态是',two);

JavaScript异步的实现

实例化非待定状态的 promise

  • Promise.resolve()

  • Promise.rejuect()

二者均可以接收一个参数,前者表示成功的结果,后者表示拒绝的理由

Promise 实例方法

  • Promise.prototype.then()

  • Promise.prototype.catch()

  • Promise.prototype.finally()

  1. then()

    可最多接收两个函数作为参数。

    第一个参数处理兑现状态,第二个参数处理拒绝状态

    返回一个新 Promise 实例

    根据 then 的回调返回值决定 新Promise 的状态
    1. 返回fulfilled的promise,返回一个值和不返回值。新promise状态为fulfilled

    2. 返回rejected的promise,抛出错误。新promise状态为rejected

    3. 返回pending的promise。新promise状态为pending

    const excutor = function (resolve, reject) {
        setTimeout(reject, 2000);
    };
    const pro = new Promise(excutor);
    const newPro = pro.then(
        () => {
            console.log('resolve');
        },
        () => {
            console.log('reject');
        },
    );
    console.log(newPro);
  2. catch()

    为 promise 添加拒绝处理程序

    接收一个参数,即

    promise.catch(onReject) 等价于

    promise.then(null,onReject)

  3. finally()

非重入

non-reentrancy

一句话结论:

promise 转换状态,处理程序被排期而非立即执行。

换句话说就是,

调用 then() 会立即把程序推入消息队列。等到之后的同步代码全部执行完毕再来执行之前被排期的部分,会按照被排期的顺序执行即先排期先执行。

打一颗栗子:

console.log('1.程序开始执行了~');
const pro = new Promise((resolve,reject)=>{
  console.log('2.Promise执行函数resolve之前');
  resolve();
  console.log('3.Promise执行函数resolve之后');
})
const newPro = pro.then(()=>console.log('5.pro--then'));
newPro.then(()=>console.log('6.newPro--then'));
console.log('4.同步代码结束了~');
1.程序开始执行了~
2.Promise执行函数resolve之前
3.Promise执行函数resolve之后
4.同步代码结束了~
5.pro--then
6.newPro--then

连锁与合成

  • promise 的连锁也就是链式调用

由于 Promise 的实例方法都会返回新的 promise 实例,所以我们可以一直调用 实例方法

pro.then(()=>console.log('5.pro--then'))
.then(()=>console.log('6.newPro--then'));
  • Promise.all() 和 Promise.race()
  1. all()

    接收一个可迭代对象作为参数,返回一个新 promise 实例

    关于返回值:

    0. 参数为空或参数不含 promise ,则返回的 promise 为 fulfilled
    
    1.  其他情况,返回的 promise 为 pending。改状态会根据参数的 promise 状态异步变成 fulfilled 或 rejected。(全部完成转为前者,有一个失败转为后者)

    promise.all 返回的 promise 的完成状态的结果是一个数组,它包含所有的传入迭代参数对象的值(也包括非 promise 值)

  2. race()

    接收一个可迭代对象作为参数,返回一个新 promise 实例

    关于返回值:

    0. 参数集合中最先解决或拒绝的 promise 的镜像

这俩方法本身很简单,这里不再赘述,值得一提是。

call() 和 race() 都具有异步性。

当且仅当传入的可迭代对象为空时为同步。

异步函数

即 async/await 为 ES8 新增

async

用于声明异步函数

异步函数始终返回一个 promise 对象

它的返回值:

可以是常规值或特别对象(实现thenable接口的对象)

若是前者情况,则被当作被解决的 promise

后者,则由  then() 的处理程序解包

打颗栗子:

async function one(){
  return Promise.resolve('one');
}
async function two(){
  return 'two'
}
console.log(one());
console.log(two());

JavaScript异步的实现

await

await 后跟一个常规值或特别对象(实现thenable接口的对象)

若是前者情况,则被当作被解决的 promise

或者,则由 await 解包

async function one(){
  const p = new Promise((resolve,reject)=>resolve(3))
  console.log(await p);
}
async function two(){
  console.log(await "await");
}
one()
two()

JavaScript异步的实现

注意事项

  • await 必须在异步函数中使用

  • await 只能直接出现在异步函数的定义中

停止和恢复执行

一颗栗子:

async function one(){
  console.log(await Promise.resolve('1'));
}
async function two(){
  console.log(await '2');
}
async function three(){
  console.log('3');
}
one()
two()
three()
3
1
2

另一颗栗子:

async function foo() {
  console.log(2);
  console.log(await Promise.resolve(6));
  console.log(7);
}
async function bar(){
  console.log(4);
  console.log(await 8);
  console.log(9);
}
console.log(1);
foo();
console.log(3);
bar();
console.log(5);
1
2
3
4
5
6
7
8
9

值得一提的是,曾经这段代码的打印结果是

1

2

3

4

5

6

7

8

9

后来,TC39 对 await 后面 promise 的情况做了修改。修改之后栗子中的

Promise.resolve(6)

只生成一个异步任务,较之前更简单一点。

异步函数策略

实现 sleep()
async function sleep(delay){
  return new Promise((resolve)=>setTimeout(resolve,delay));
}
async function test(){
  const t0 = Date.now();
  await sleep(2000);
  console.log(Date.now()-t0);
}
test();

值得一提的是,打印的结果是会大于 2000 的。

至于为什么,我想咱们后面可以讨论一番。

好了,今天就写到这里吧,关于异步函数和约定,MDN有更多知识。

点赞
收藏
评论区
推荐文章
Souleigh ✨ Souleigh ✨
3年前
理解 Javascript 中的 Async / Await
在本文中,我们将探讨async/await,对于每个Javascript开发人员来说,是异步编程的首选工具。如果您不熟悉javascript,请不要担心,本文将帮助您async/await从头开始理解。介绍async/await是javascript中的一种模式,可使您的代码以同步方式执行,但又不影响javascript的异步行为。定义异步功能要定义一
Chase620 Chase620
3年前
Promise从入门到拿Offer之手写Promise
1、Promise构造函数的实现Promise构造函数用来声明示例对象,需要传入一个执行器函数。其中包括resolve函数和reject函数,以及几个重要的属性:状态属性、结果属性和回调函数队列。构造函数的基本框架resolve函数用于异步处理成功后调用的函数。其中包括验证对象状态修改次数,修改promise实例对象状态,异步调用成功的回调函数
菜园前端 菜园前端
1年前
什么是JavaScript异步模式
什么是异步模式?不会等待当前任务执行完毕,才会去执行下一个任务,这就是异步模式(Asynchronous)。开启异步后,就会跳过本任务,开始执行下一个任务,后续的逻辑一般会通过回调函数的方式定义。异步模式执行中,涉及到调用栈(Callstack)、消息队列
Wesley13 Wesley13
3年前
C# 1.0 新特性之异步委托(AP、APM)
Ø前言C异步委托也是属于异步编程中的一种,可以称为AsynchronousProgramming(异步编程)或者AsynchronousProgrammingModel(异步编程模型),因为这是实现异步编程的模式。委托是C1.0就有的特性,并且.NETv1.0同时也伴随有AsyncCallback、IAsyncResult
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
3年前
JavaScript回调函数的高手指南
摘要:本文将会解释回调函数的概念,同时帮你区分两种回调:同步和异步。回调函数是每个前端程序员都应该知道的概念之一。回调可用于数组、计时器函数、promise、事件处理中。本文将会解释回调函数的概念,同时帮你区分两种回调:同步和异步。1.回调函数首先写一个向人打招呼的函数。只需要创建一个接受name参数的函数gree
Stella981 Stella981
3年前
ES6 Promise 对象扯谈
newPromise(/executor/function(resolve,reject){...});Promise的构造函数接收一个函数作为参数,函数里面传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。其实这里用“成功”和“失败”来描述并不准确,按照标准来
Stella981 Stella981
3年前
ES6 Promise
Promisepromise是异步编程的一种解决方案1什么是异步?异步模式,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。"异步模式"非常重要。在浏
Stella981 Stella981
3年前
Noark入门之异步事件
引入异步事件主要是为了各模块的解耦,每当完成一个动作时,向系统发布一个事件,由关心的模块自己监听处理,可选择同步处理,异步处理,延迟处理。何时发布事件,当其他模块关心此动作时<br比如获得道具时,任务系统模块要判定完成进度,BI模块需要上报等等都可以监听此事件,已达模块解耦0x00事件源一个实现xyz.noark.core.event
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
前端麦小子
前端麦小子
Lv1
一起学前端吧,每天进步一点点。
文章
1
粉丝
1
获赞
1
热门文章

暂无数据