本文介绍了JS中实现异步的几种方式:分别为 1.回调函数 2.Promise对象 3.Generator函数 4.async(语法糖)

先说下js的单线程模式,Javascript语言的执行环境是”单线程”(single thread)所谓”单线程”,就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。”同步模式”就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;”异步模式”则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。”异步模式”非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,”异步模式”甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

第一,回调函数模式:

1
2
3
4
5
6
7
8
9
const fs = require('fs');
fs.readFile('./files/test01.json',(err,data)=>{
if (err) {
console.log('读取失败');
} else {
console.log(JSON.parse(data));
}
});
console.log('后续代码会先执行的');//这一行代码会先输出
  • 上述代码定义了读取文件结束后的回调函数,但不会阻塞代码的继续运行,会先执行 console.log(‘后续代码会先执行的’); 当读取文件操作结束后再去执行其回调函数

第二,Promise 对象

  • 关于Promise相关信息请到这来: Promise

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const fs = require('fs');
    let p = new Promise((resolve,reject)=>{
    fs.readFile('./files/test01.json',(err,data)=>{
    if (err) {
    reject(`error mes:${err}`);
    } else {
    resolve(data);
    }
    });
    });
    p.then(msg=>{
    console.log(`成功的回调:${msg}`);
    },msg=>{
    console.log(`失败的回调:${msg}`);
    });
    console.log('后续代码');//这行代码会先执行,最先输出
  • 以上代码,我们将耗时操作放在new Promise()构造函数的参数(参数是一个函数)里面,当耗时操作成功后,我们去调用resolve()并传入成功的数据,当耗时操作失败后,我们去调用reject()并传入失败的信息。p.then()函数有两个参数(都是函数),其中第一个参数是成功的回调(也就是resolve()调用后马上去调用p.then()的第一个参数),第二个参数是失败的回调(也就是reject()调用后马上去调用p.then()的第二个参数)

Promise.all() 方法并行执行多个异步操作

  • Promise.all() 表示其中的Promise异步操作都执行完毕了才会去掉用.then方法
  • Promise.all([p1,p2]).then((data)=>{},(errdata)=>{}).catch()里面的多个异步p1,p2是并行执行的,所耗时间将是多个异步操作中耗时最长的耗时。不是按照实例化Promise对象的先后顺序实现的,但是返回的结果却是按照顺序返回的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const fs = require('fs');
    function createPromise(url){
    return new Promise((resolve,reject)=>{
    fs.readFile(url,(err,data)=>{
    if (err) {
    reject(`error mes:${err}`);
    } else {
    resolve(JSON.parse(data));
    }
    });
    });
    }
    Promise.all(
    [createPromise('./files/test01.json'),
    createPromise('./files/test002.json')]
    ).then((data)=>{
    console.log(data);
    },(err)=>{
    console.log(err);
    });

    console.log('后续代码');//这行代码会先执行,最先输出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    //以下代码:虽然是先实例化的写操作,后实例化的读操作,但是会先执行读操作
    var a = new Promise(function (resolve,reject) {
    setTimeout(function () {
    fs.writeFile('./files/test002.txt','66666',(err)=>{
    if (err) {
    reject('写入失败');
    } else {
    resolve('写入成功');
    }
    });
    },10000);
    });

    var b = new Promise(function (resolve,reject) {
    setTimeout(function () {
    fs.readFile('./files/test002.txt',(err,data)=>{
    if (err) {
    reject(err);
    } else {
    resolve(data);
    }
    });
    },1000);
    });

    var p = Promise.all([a,b]);
    p.then(function(val) {
    console.log(val);
    });

Promise.race() 方法

  • 和promise.all() 对应的是promise.race(),promise.race([p1,p2]).then(()=>{},()=>{}); 表示竞速(也就是多个异步操作只要有一个完成了就去执行.then方法)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    var a = new Promise(function  (resolve,reject) {
    setTimeout(function () {
    fs.writeFile('./files/test002.txt','aaaaa',(err)=>{
    if (err) {
    reject('写入失败');
    } else {
    resolve('写入成功');
    }
    });
    },10000);
    });

    var b = new Promise(function (resolve,reject) {
    setTimeout(function () {
    fs.readFile('./files/test002.txt',(err,data)=>{
    if (err) {
    reject(err);
    } else {
    resolve(data);
    }
    });
    },1000);
    });

    Promise.race([a,b]).then(function(val) {
    console.log(val);
    });
    //以上代码会在 读操作执行完毕后立即去执行 .then()方法,并输出数据,但是10s后 文件test002.txt的数据又会改变

Promise按顺序执行多个异步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//使用then做链式操作,按顺序依次执行多个异步操作
const FS = require('fs');
let readFilePromise = (url)=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{FS.readFile(url,(err,data)=>{if(err){reject(err)}else{resolve(JSON.parse(data))}})},1000);
});
};
let readFilePromise1 = (url)=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{FS.readFile(url,(err,data)=>{if(err){reject(err)}else{resolve(JSON.parse(data))}})},10000);
});
};


readFilePromise('./files/test01.json').then(data => {
console.log(data); // 打印第 1 个文件内容
return readFilePromise1('./files/test002.json');
}).then(data => {
console.log(data) // 打印第 2 个文件内容
return readFilePromise('./files/test03.json');
}).then(data => {
console.log(data); // 打印第 3 个文件内容
return readFilePromise('./files/test04.json');
}).then(data=> {
console.log(data); // 打印第 4 个文件内容
});

第三,generator 函数

  • 之前的总结连接:generator
  • 这里讲的比较深入:深入generator
  • 形式上,Generator 函数是一个普通函数,但是有两个特征
    • 一是,function关键字与函数名之间有一个星号
    • 二是,函数体内部使用yield语句,定义不同的内部状态
  • Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,我们可以通过调用 next 方法,使得指针移向下一个状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function* f() {
    for(var i = 0; true; i++) {
    var reset = yield i;
    console.log(reset);
    if(reset) { i = -1; }
    }
    }
    var g = f();
    console.log(g.next(true)); // { value: 0, done: false }
    console.log(g.next(true)); // { value: 0, done: false }
    console.log(g.next(true)); // { value: 0, done: false }
    //以上代码的输出结果如下
    { value: 0, done: false }
    true
    { value: 0, done: false }
    true
    { value: 0, done: false }
pic
说明

第四,async 配合 await

  • 据说是异步编程终级解决方案的 async/await 是非常甜的语法糖(是generator的高级封装) JavaScript 的 async/await 实现离不开 Promise

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    let sleep = (time)=>{
    return new Promise((resolve,reject)=>{
    setTimeout(()=>{resolve('ok');},time);
    });
    };
    (async function fn(){
    try{
    console.log(new Date());
    let time = await sleep(10000);
    console.log(time);
    console.log(new Date());
    } catch (err) {
    console.log(err);
    }
    })();
    console.log("1234");//由于async是异步的 故这行代码不会被阻塞
    //上面的代码输出如下:
    2018-04-07T07:29:51.741Z
    1234
    ok
    2018-04-07T07:30:01.747Z
    //可以看到,1234 提前输出了,代表代码没有被阻塞,而是异步执行的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    const FS = require('fs');
    //该函数返回一个Promise对象
    let getFiles = (url,time)=>{
    return new Promise((resolve,reject)=>{
    setTimeout(()=>{
    FS.readFile(url,(err,data)=>{if (err){reject(err);}else{resolve(JSON.parse(data))}});
    },time);
    });
    };
    //立即执行一个async函数,里面await等待一个Promise resolve
    //里面的每一个await是按照顺序执行的
    (async function(){
    console.log('start开始时间:'+ new Date());
    let file1 = await getFiles('./files/test01.json',5000);
    console.log('第一个文件读取操作结束时间:'+ new Date());
    let file2 = await getFiles('./files/test002.json',4000);
    console.log('第二个文件读取操作结束时间:'+ new Date());
    let file3 = await getFiles('./files/test03.json',3000);
    console.log('第三个文件读取操作结束时间:'+ new Date());
    let file4 = await getFiles('./files/test04.json',2000);
    console.log('第四个文件读取操作结束时间:'+ new Date());
    return [file1,file2,file3,file4];
    })().then((data)=>{
    console.log('异步操作全部成功时间:'+ new Date());
    console.log(data)}
    );
    console.log('1');//由于是异步的,故该行代码不会被阻塞
    //以上代码的输出如下:
    start开始时间:Sat Apr 07 2018 15:43:10 GMT+0800 (中国标准时间)
    1
    第一个文件读取操作结束时间:Sat Apr 07 2018 15:43:15 GMT+0800 (中国标准时间)
    第二个文件读取操作结束时间:Sat Apr 07 2018 15:43:19 GMT+0800 (中国标准时间)
    第三个文件读取操作结束时间:Sat Apr 07 2018 15:43:22 GMT+0800 (中国标准时间)
    第四个文件读取操作结束时间:Sat Apr 07 2018 15:43:24 GMT+0800 (中国标准时间)
    异步操作全部成功时间:Sat Apr 07 2018 15:43:24 GMT+0800 (中国标准时间)
    [ { name: 'zhangsan', age: 24, height: 172 },
    { name: 'lisi', age: 34 },
    { name: 'xiaoming', age: 3098 },
    { name: 'liuhaooo', age: 27 } ]
  • 看上面的结果:1是提前输出了的,说明是异步执行的,并且async里面的各个异步操作是按照顺序执行的

  • 注意几点:
    • 正常情况下,await 命令后面跟着的是 Promise ,如果不是的话,也会被转换成一个 立即 resolve 的 Promise
    • await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中
    • async 函数内部 return 返回的值。会成为 then 方法回调函数的参数
    • await 命令只能用在 async 函数之中,如果用在普通函数,就会报错