浅说 Javascript 异步

什么是异步?

“异步”,从字面来讲,好像是在不同的(异)的ways上do something,那首先想到的词可能是“一边…一边…”,比如‘小明一边吃雪糕一边写作业’,这完全没毛病,雪糕吃完了,作业也写完了,这就是异步?那就大错特错了!

其实同步和异步,无论如何,做事情的时候都是只有一条流水线(单线程),

同步和异步的差别就在于这条流水线上各个流程的执行顺序不同

最基础的异步是setTimeout和setInterval函数,很常见,但是很少人有人知道其实这就是异步,因为它们可以控制js的执行顺序。我们也可以简单地理解为:可以改变程序正常执行顺序的操作就可以看成是异步操作。

异步产生根源

根源----“Javascript是单线程” 即一次只能做一件事。

单线程设计原因:js渲染在浏览器上,包含了许多与用户的交互,如果是多线程,那么试想一个场景:一个线程在某个DOM上添加内容,而另一个线程删除这个DOM,那么浏览器要如何反应呢?这就乱套了。

单线程下所有的任务都是需要排队的,而这些任务分为两种:

  • 同步任务 -- 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务

  • 异步任务 -- 不进入主线程、而进入任务队列(task queue)的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

综上同步执行其实也是一种只有主线程的异步执行。一个视频关于异步操作是如何被执行的,讲得非常好《what the hack is event loop》。

不同的异步操作添加到任务队列的时机不同,如 onclick, setTimeout, ajax 处理的方式都不同。这些异步操作是由浏览器内核的 webcore 来执行的。

webcore 包含上面提到的3种 webAPI:

  • DOM Binding、

  • timer、

  • network

onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。

setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。

ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。

JavaScript 异步进化史

callback --> Promise --> Generator函数 --> async/await

最基础的异步调用方式是callback,它将回调函数 callback 传给异步 API,由浏览器或 Node 在异步完成后,通知 JS 引擎调用 callback。对于简单的异步操作,用 callback 实现,是够用的。但随着负责交互页面和 Node 出现,callback 方案的弊端开始浮现出来。 Promise 规范孕育而生,并被纳入 ES6 的规范中。后来 ES7 又在 Promise 的基础上将 async 函数纳入标准。

为什么要用异步操作

JS 是单线程的语言,所谓“单线程”就是一根筋,对于拿到的程序,一行一行的执行,上面的执行为完成,就傻傻的等着。例如

var i, t = Date.now()
for (i = 0; i < 100000000; i++) {
}
console.log(Date.now() - t)  // 250 (chrome浏览器)
上面的程序花费 250ms 的时间执行完成,执行过程中就会有卡顿,其他的事儿就先撂一边不管了。

执行程序这样没有问题,但是对于 JS 最初使用的环境 ———— 浏览器客户端 ———— 就不一样了。因此在浏览器端运行的 js ,可能会有大量的网络请求,而一个网络资源啥时候返回,这个时间是不可预估的。这种情况也要傻傻的等着、卡顿着、啥都不做吗?———— 那肯定不行。

因此,JS 对于这种场景就设计了异步 ———— 即,发起一个网络请求,就先不管这边了,先干其他事儿,网络请求啥时候返回结果,到时候再说。这样就能保证一个网页的流程运行。

异步的实现原理

先看一段比较常见的代码

var ajax = $.ajax({
    url: '/data/data1.json',
    success: function () {
        console.log('success')
    }
})

上面代码中$.ajax()需要传入两个参数进去,

  • url — 请求的路由

  • success – 是一个函数。这个函数传递过去不会立即执行,而是等着请求成功之后才能执行。

success对于这种传递过去不执行,等出来结果之后再执行的函数,叫做callback,即回调函数。

再看一段更加能说明回调函数的 nodejs 代码。和上面代码基本一样,唯一区别就是:上面代码时网络请求,而下面代码时 IO 操作。


var fs = require('fs')
fs.readFile('data1.json', (err, data) => {
    console.log(data.toString())
})

从上面两个 demo 看来, 实现异步的最核心原理,就是将callback作为参数传递给异步执行函数,当有结果返回之后再触发 callback执行,就是如此简单!

常用的异步操作

开发中比较常用的异步操作有:

  • 网络请求,如ajax http.get

  • IO 操作,如readFile readdir

  • 定时函数,如setTimeout setInterval

如何实现异步

可以通过

  • 回调函数、

  • 事件监听

  • 发布/订阅

  • Promise、

  • Generator、

  • Async/Await等来实现异步。

1. 回调函数(CallBack)

这是异步编程最基本的方法。 所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。它的英语名字 callback,直译过来就是”重新调用”。 读取文件进行处理,是这样写的。


fs.readFile('/etc/passwd', function (err, data) {
  if (err) throw err;
  console.log(data);
});

上面代码中,readFile函数的第二个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了 /etc/passwd 这个文件以后,回调函数才会执行。

回调本身是比较好用的,但是随着Javascript越来越成熟,对于异步编程领域的发展,回调已经不够用了,体现在以下几点:

  • 大脑处理程序是顺序的,对于复杂的回调函数会不易理解,我们需要一种更同步、更顺序的方式来表达异步。

  • 回调一般会把控制权交给第三方,从而带来信任问题

  • 调用回调过早

  • 调用回调过晚(或未调用)

  • 调用回调次数过多或过少

  • 未能传递所需的环境和参数

  • 吞掉可能出现的错误和异常

  • 回调地狱

假设我们有一个 getData 方法,用于异步获取数据,第一个参数为请求的 url 地址,第二个参数是回调函数,如下:


function getData (url, callBack) {
      // 模拟发送网络请求
      setTimeout(() => {
          // 假设 res 就是返回的数据
          var res = {
              url: url,
              data: Math.random()
          }
          // 执行回调,将数据作为参数传递
          callBack(res)
      }, 1000)
  }

我们预先设定一个场景,假设我们要请求三次服务器,每一次的请求依赖上一次请求的结果,如下:

\[object Object\]



 ``通过上面的代码可以看出,第一次请求的 url 地址为:`/page/1?param=123`,  
  
返回结果为 `res1`。

第二个请求的 url 地址为:`/page/2?param=${res1.data}`,  
  
依赖第一次请求的`res1.data`,返回结果为`res2`。

第三次请求的 url地址为:`/page/3?param=${res2.data}`,  
  
依赖第二次请求的 `res2.data`,返回结果为 `res3`。

由于后续请求依赖前一个请求的结果,所以我们只能把下一次请求写到上一次请求的回调  
  
函数内部,这样就形成了常说的:回调地狱。``  



2. 事件监听

在DOM监听中比较常见。

3. 发布/订阅

也就是常说的观察者模式。

观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

一些前端MVVM框架就是用的观察者模式实现是双向绑定

发布/订阅模式(Pub/Sub)是一种消息模式,它有 两个参与者 : 发布者和订阅者 。发布者向 某个信道发布一条消息,订阅者绑定这个信道,当有消息发布至信道时就会 接收到一个通知。最重要的一点是, 发布者和订阅者是完全解耦的,彼此并不知晓对方 的存在。两者仅仅共享一个信道名称。

理解起来很简单: 我去书报亭订了一份报纸,当他把报纸送给我了,我就去领了看.

这里,我就变成了 订阅者 ,报亭就是 发布者 ,当报纸送到的时候(状态发生改变,通知订阅者),我就去领了看(做一些操作)

一个发布者应该有三个主要的方法:

  • 订阅

  • 发布

  • 退订

example

订阅:


var PubSub = {};  
var eventObj = {};  
PubSub.subscribe = function(event, fn) {  
     eventObj\[event\] = fn;
}

发布:


PubSub.publish = function(event) {  
    if (eventObj\[event\]) {
      eventObj\[event\]();
    }
}

退订:


PubSub.off = function(event, fn) {  
    if (eventObj\[event\]) {
      eventObj\[event\] = null;
    }
}

我们来整理一下代码用闭包隐藏eventObj这个对象:


var PubSub = (function() {  
    var eventObj = {};
    return {
        subscribe: function(event, fn) {
            eventObj\[event\] = fn;
        },
        publish: function(event) {
            if (eventObj\[event\]) eventObj\[event\]();
        },
        off: function(event) {
            if (eventObj\[event\]) delete eventObj\[event\];
        }
    }
}());

执行:


PubSub.subscribe('event', function() {  
    console.log('event release');
});
PubSub.publish('event'); // 'event release'  

OK it work!!

这绝对是最简单无脑的观察者模式的实现了,你以为这就完了吗?

这样…这个一个事件只能绑定一个操作,并且取消订阅把整个事件都删除掉了,这样就不是很好了,我们应该写一个支持一个事件绑定多个操作的,并且退订时是退订一个事件上的一个操作,而不是删除整个事件

再来:

一个事件绑定多个操作,我们应该用一个数组把操作保存起来,发布时按订阅顺序执行,退订时删除对应的数组元素就好.


var PubSub = (function() {  
    var queue = {};
    var subscribe = function(event, fn) {
        if (!queue\[event\]) queue\[event\] = \[\];
        queue\[event\].push(fn);
    }
    var publish = function(event) {
        var eventQueue = queue\[event\],
            len = eventQueue.length;
        if (eventQueue) {
            eventQueue.forEach(function(item, index) {
                item();
            });
        }
    }
    var off = function(event, fn) {
        var eventQueue = queue\[event\];
        if (eventQueue) {
            queue\[event\] = eventQueue.filter(function(item) {
                return item !== fn;
            });
        }
    }
    return {
        subscribe: on,
        publish: emit,
        off: off
    }
}());

以上就是一个简单的观察者模式的实现了.

example:


function first() {  
    console.log('event a publish first');
}
PubSub.subscribe('a', first);  
PubSub.subscribe('a', function() {  
    console.log('event a publish second');
});
PubSub.publish('a'); // event a emit first, event a emit second

PubSub.off('a', first);  
PubSub.publish('a');  //event a emit second

4. Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

  • 首先 Promise 和异步接口签订一个协议,

  • 成功时,调用resolve函数通知 Promise,

  • 异常时,调用reject通知 Promise。

  • 另一方面 Promise 和 callback 也签订一个协议,由 Promise 在将来返回可信任的值给then和catch中注册的 callback。

    // 创建一个 Promise 实例(异步接口和 Promise 签订协议)
    var promise = new Promise(function (resolve,reject) {
      ajax('url',resolve,reject);
    });
    
    // 调用实例的 then catch 方法 (成功回调、异常回调与 Promise 签订协议)
    promise.then(function(value) {
      // success
    }).catch(function (error) {
      // error
    })

Promise 是个非常不错的中介,它只返回可信的信息给 callback。它对第三方异步库的结果进行了一些加工,保证了 callback 一定会被异步调用,且只会被调用一次。

var promise1 = new Promise(function (resolve) {
  // 可能由于某些原因导致同步调用
  resolve('B');
});
// promise依旧会异步执行
promise1.then(function(value){
    console.log(value)
});
console.log('A');
// A B (先 A 后 B)
var promise2 = new Promise(function (resolve) {
  // 成功回调被通知了2次
  setTimeout(function(){
    resolve();
  },0)
});
// promise只会调用一次
promise2.then(function(){
    console.log('A')
});
// A (只有一个)
var promise3 = new Promise(function (resolve,reject) {
  // 成功回调先被通知,又通知了失败回调
  setTimeout(function(){
    resolve();
    reject();
  },0)

});
// promise只会调用成功回调
promise3.then(function(){
    console.log('A')
}).catch(function(){
    console.log('B')
});
// A(只有A)

介绍完 Promise 的特性后,来看看它如何利用链式调用,解决异步代码可读性的问题的。


var fetch = function(url){
    // 返回一个新的 Promise 实例
    return new Promise(function (resolve,reject) {
        ajax(url,resolve,reject);
    });
}

A();
fetch('url1').then(function(){
    B();
    // 返回一个新的 Promise 实例
    return fetch('url2');
}).catch(function(){
    // 异常的时候也可以返回一个新的 Promise 实例
    return fetch('url2');
    // 使用链式写法调用这个新的 Promise 实例的 then 方法    
}).then(function() {
    C();
    // 继续返回一个新的 Promise 实例...
})
// A B C ...
function getDataAsync (url) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            var res = {
                url: url,
                data: Math.random()
            }
            resolve(res)
        }, 1000)
    })
}

getDataAsync('/page/1?param=123')
.then(res1 => {
    console.log(res1)
    return getDataAsync(`/page/2?param=${res1.data}`)
})
.then(res2 => {
    console.log(res2)
    return getDataAsync(`/page/3?param=${res2.data}`)
})
.then(res3 => {
    console.log(res3)
})

如此反复,不断返回一个 Promise 对象,再采用链式调用的方式不断地调用。使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。

then 方法返回一个新的 Promise 对象,then 方法的链式调用避免了 CallBack 回调地狱。

但也并不是完美,比如我们要添加很多 then 语句, 每一个 then 还是要写一个回调。

如果场景再复杂一点,比如后边的每一个请求依赖前面所有请求的结果,而不仅仅依赖上一次请求的结果,那会更复杂。

Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。

5. Generator的方式

ECMAScript 6 (简称 ES6 )作为下一代 JavaScript 语言,将 JavaScript 异步编程带入了一个全新的阶段。关于异步编程可以查看下图:

而下面这种连续的执行过程叫做同步的。Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。

除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:

  • 函数体内外的数据交换

  • 错误处理机制。

next 方法返回值的 value 属性,是 Generator 函数向外输出数据;
next 方法还可以接受参数,这是向 Generator 函数体内输入数据。

如下例:

  • 特性1:暂停执行与恢复执行
    function* gen(x){
      var y = yield x + 2;
      return y;
    }
也就是通过yield来暂停执行,通过next来恢复执行
  • 特性2:函数体内外的数据交换
    function* gen(x){
      var y = yield x + 2;
      return y;
    }
    
    var g = gen(1);
    g.next() // { value: 3, done: false }
    g.next(2) // { value: 2, done: true }
通过调用next方法获取到的value代表函数体向外输出的数据,而调用next方法传入的参数本身代表向Generator传入数据。
  • 特性3:错误处理机制
    function* gen(x){
      try {
        var y = yield x + 2;
      } catch (e){
        console.log(e);
      }
      return y;
    }
    
    var g = gen(1);
    g.next();
    g.throw('出错了');
    // 出错了
上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try … catch 代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

下面是Generator处理实际任务的一个例子:


var fetch = require('node-fetch');
function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

具体的执行过程如下:


var g = gen();
var result = g.next();
result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

thunk函数

编译器的”传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。


function f(m){
  return m * 2;     
}
f(x + 5);

// 等同于
var thunk = function () {
  return x + 5;
};
function f(thunk){
  return thunk() * 2;
}

上面代码中,函数 f 的参数 x + 5 被一个函数替换了。凡是用到原参数的地方,对 Thunk 函数求值即可。这就是 Thunk 函数的定义,它是”传名调用”的一种实现策略,用来替换某个表达式。

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数

thunk函数的实现机制还是通过闭包来完成的。其调用分为三步,

  1. 首先是传入一个函数,

  2. 接着是传入该函数的所有除了callback以外的参数,

  3. 最后是传入回调函数callback。

    function thunkify(fn){ //第一步:传入函数 return function(){ //第二步:传入除了callback以外的参数 var args = new Array(arguments.length); var ctx = this; for(var i = 0; i < args.length; ++i) { args[i] = arguments[i]; } return function(done){ //第三步:传入回调函数 var called; args.push(function(){ if (called) return; //回调函数只会运行一次 called = true; done.apply(null, arguments); }); try { fn.apply(ctx, args); } catch (err) { done(err); } } } };

thunk与Generator强强联手将程序执行权交还给Generator函数
  • Generator的yield返回的必须是thunkify的函数才能递归
    Thunk 函数有什么用?回答是以前确实没什么用,但是 ES6 有了 Generator 函数,Thunk 函数现在可以用于 Generator 函数的自动流程管理。

    以读取文件为例。下面的 Generator 函数封装了两个异步操作。

    var fs = require('fs');
    var thunkify = require('thunkify');
    var readFile = thunkify(fs.readFile);
    var gen = function* (){
      var r1 = yield readFile('/etc/fstab');
      //1.交出执行权
      console.log(r1.toString());
      var r2 = yield readFile('/etc/shells');
       //1.交出执行权
      console.log(r2.toString());
    };
上面代码中,yield 命令用于将程序的执行权移出 Generator 函数,那么就需要一种方法,将执行权再交还给 Generator 函数。这种方法就是 Thunk 函数,因为它可以在回调函数里,将执行权交还给 Generator 函数。为了便于理解,我们先看如何手动执行上面这个 Generator 函数。




* * *

var g = gen();
var r1 = g.next();
//2.查看这里的程序你可以清楚的看到,这里是将同一个回调函数反复的传入到  
g.next返回的value中。
但是这个返回的value必须是thunkify过后的函数,这样它只会接  
受一个参数,那么就满足这里的定义了
r1.value(function(err, data){
  if (err) throw err;
  var r2 = g.next(data);
  r2.value(function(err, data){
    //2.value必须是thunkify的函数才会只接受一个callback参数
    if (err) throw err;
    g.next(data);
  });
});

* * *



上面代码中,变量 g 是 Generator 函数的内部指针,表示目前执行到哪一步。next 方法负责将指针移动到下一步,并返回该步的信息(value 属性和 done 属性)。  
仔细查看上面的代码,可以发现 Generator 函数的执行过程,其实是将同一个回调函数,反复传入 next 方法的 value 属性。这使得我们可以用`递归`来自动完成这个过程。
使用thunkify来自动执行Generator函数从而将执行权交还给Generator

function run(fn) {
  var gen = fn();
  //获取到generator内部指针,这里的next就是thunk函数的回调函数
  function next(err, data) {
    var result = gen.next(data);
    //获取generator内部状态
    if (result.done) return;
    result.value(next);
    //Gnerator的value必须是thunkify函数,此时才会只接受一个回调函数
  }
  next();
}
run(gen);

下面是一个读取多个文件的例子:


var gen = function* (){
  var f1 = yield readFile('fileA');
  var f2 = yield readFile('fileB');
  // ...
  var fn = yield readFile('fileN');
};
run(gen);

上面代码中,函数 gen 封装了 n 个异步的读取文件操作,只要执行 run 函数,这些操作就会自动完成。这样一来,异步操作不仅可以写得像同步操作,而且一行代码就可以执行。

Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。

6. async/await对于异步的终极解决方案

async 函数就是 Generator 函数的语法糖。

前文有一个 Generator 函数,依次读取两个文件。


var fs = require('fs');
var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

写成 async 函数,就是下面这样。


var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab');
  var f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比较就会发现,async 函数就是将 Generator 函数的星号( * )替换成 async,将 yield 替换成 await,仅此而已。

async函数的优点
  • 内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
    var result = asyncReadFile();
  • 更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。

  • 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

async的用法

同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

下面的例子,指定多少毫秒后输出一个值。


function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}
async function asyncPrint(value, ms) {
  await timeout(ms);
  //遇到await了,所有先返回,得到异步操作完成,执行后面的代码
  console.log(value)
}
asyncPrint('hello world', 50);

上面代码指定50毫秒以后,输出”hello world”。

async自动执行器的实现

//genF是Generator函数
function spawn(genF) {
    //返回promise和co一样,但是co只能是promise和thunk函数
  return new Promise(function(resolve, reject) {
    var gen = genF();
    //得到Generator内部指针
    function step(nextF) {
      try {
        var next = nextF();
       //next获取到第一个await返回的结果
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      //如果done为true那么我们直接resolve
      Promise.resolve(next.value).then(function(v) {
        //第一个await返回的对象的value表示结果{value:'',done:false}
        step(function() { return gen.next(v); });  
        //调用gen.next()获取到下一个await的结果并传入上一次的await调  
        用后得到的value    
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
    //首次执行的时候传入第一个await的data为undefined
  });
}

async注意点
  • await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。
    async function myFunction() {
      try {
        await somethingThatReturnsAPromise();
      } catch (err) {
        console.log(err);
      }
    }
    
    // 另一种写法
    
    async function myFunction() {
      await somethingThatReturnsAPromise().catch(function (err){
        console.log(err);
      });
    }
  • await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
    async function dbFuc(db) {
      let docs = \[{}, {}, {}\];
    
      // 报错
      docs.forEach(function (doc) {
        await db.post(doc);
      });
    }

上面代码会报错,因为 await 用在普通函数之中了。但是,如果将 forEach 方法的参数改成 async 函数,也有问题。


async function dbFuc(db) {
      let docs = \[{}, {}, {}\];

      // 可能得到错误结果
      docs.forEach(async function (doc) {
        await db.post(doc);
      });
}

上面代码可能不会正常工作,原因是这时三个 db.post 操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。

async function dbFuc(db) {
    let docs = \[{}, {}, {}\];

    for (let doc of docs) {
      await db.post(doc);
    }
}


参考资料

  1. js异步处理(一)——理解异步

  2. JavaScript 异步进化史

  3. 深入理解 JavaScript 异步系列(1)——基础

  4. JS异步处理方案总结

  5. JavaScript实现的发布/订阅(Pub/Sub)模式

  6. async 函数的含义和用法

  7. js中的同步和异步的个人理解

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端布道人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值