看promise教你如何优雅的写js异步代码

众所周知,函数是js的一等公民。何为一等公民,也就是说函数是js中最重要的组成部分。其中一个很重要的体现点就是函数可以是一个函数的参数或者返回值。当一个函数的参数或者返回值是函数时,我们称这个函数为高阶函数。高阶函数在js的发展历程中起了很重要的作用。例如js的模块化,js的异步编程等等。那么今天我们就来聊聊js的异步编程

js 异步发展史

1.简单粗暴的回调函数
2.相对优雅的Promise
3. 远看像同步的Generator
4. 同步编程的干儿子之async

回调函数

回调函数是javascript中最原始的异步编程写法,该写法正是利用了js语法中函数可以作为参数传递这一特性,下面我们来看下通过回调函数来实现js异步的一个小例子。

// node js 中的文件读取
fs.readFile('./a.txt','utf8',(err,data)=>{
  if(err) return err
  console.log(data)
});
复制代码

通过上面传入回调函数的方式我们可以在文件读取完毕后打印出data,这种情况下完全没问题,而且也很好理解。但如果我们想要实现一个在文件读取完毕后再写入到另外个文件,写完之后再进行其他操作呢,依然通过回调函数来实现。

fs.readFile('./a.txt','utf8',(err,data)=>{
  if(err) return err
  fs.writeFile('./b.txt', data, function(err, data) {
      if (err) return err
      
      ///操作data
  })
});
复制代码

可以看到,要想进行一系列异步操作的话可以简单粗暴的在一个异步操作中嵌套另外的操作,一直到你的异步操作完毕。但这样下去就会出现传说中的回调金字塔问题,不利于咱们代码的维护。于是在千呼万唤中Promise终于诞生了。

Promise

promise翻译成中文就是承诺的意思。何为承诺,即答应过了就不会再改变。在promise中的体现就是promise的状态从pending转换成resolve或者reject就不会再改变。promise的简单用法是

let promise = function (path) {
    
    return new Promise(function(resolve,reject){
        
        fs.readFile(path, 'uft8', function(err, data){
            if (err) reject(err)
            resolve(data)
        })
    })
}

promise('./a.txt')
.then(data => {
    // 成功后的回调
    console.log(data)
}, err => {
    // 失败后的回调
    console.log(err)
})
复制代码

promise有个重要的特性就是promise实例会有个then方法,此方法就类似原始的回调函数,可以将异步操作成功或者失败后的操作写在then方法中,当然异步操作成功后再进行的其他异步操作也是写在then方法中

promise('./a.txt')
.then(data => {
    处理data
    return new Promise((resolve, reject) => {
        fs.writeFile('./b.txt', data, function(err, data){
            if (err) reject(err)
            resolve(data)
        })
        
    })
}, err => {
    // 失败后的回调
    console.log(err)
})
.then(data => {
    // 接收到写操作成功后的data
    console.log(data)
}, err => {
    console.log(err)
})
复制代码

promise的另一个重要特性,也是解决回调地狱的一个关键点。那就是链式调用。那么如何才能实现链式调用,那就是让then方法的返回值是另外一个promise,但我们平常在编码中很多时候手动返回的不是一个promise,这个时候promise内部通过调用Promise.resolve()方法将该值转化成了一个promise,这个时候下一个then方法中的第一个函数接受到data就是resolve时传递的值。所以咱们可以通过在then方法中一直返回一个新的promise实例这样一直调用then方法去避免恶心的回调地狱问题。

另外promise实例上还有个catch方法,该方法用于捕获promise调用过程中的错误。内部实现为

catch(onRejected) {
    // catch就是then的没有成功的简写
    return this.then(null, onRejected);
}
复制代码
另外Promise的几个静态方法也特别有用。
1.Promise.resolve()

有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。该实例状态为resolve。

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

// 内部实现
Promise.resolve = function (val) {
    return new Promise((resolve, reject) => resolve(val))
}
复制代码
2.Promise.reject()

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了

// 内部实现
Promise.reject = function (val) {
    return new Promise((resolve, reject) => reject(val));
}
复制代码
3.Promise.race()

Promise.race方法是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);
复制代码

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

// 内部实现
Promise.race = function (promises) {
    return new Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
            promises[i].then(resolve, reject);
        }
    });
}
复制代码
4.Promise.all()
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);
复制代码

上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)

// 内部实现
Promise.all = function (promises) {
    return new Promise((resolve,reject)=>{
        let arr = [];
        let i = 0; // i的目的是为了保证获取全部成功,来设置的索引
        function processData(index,data) {
            arr[index] = data;
            i++;
            if (i === promises.length){
                resolve(arr);
            }
        }
        for(let i = 0;i<promises.length;i++){
             promises[i].then(data=>{
                processData(i,data);
            }, reject);
        }
     })
}
复制代码

Generator

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

// * 和 yield一起使用,yield产出
function * gen() { // 可以暂停,调用next才会继续走
  let a = yield '买菜'; // a的结果是买回来的菜
  let b = yield a; // b的结果是做好的菜
  return b; // 返回做好的菜
}
let a = gen('菜'); // 执行后返回的是迭代器
console.log(a.next());
console.log(a.next('买好的菜'));
复制代码

generator通常是和co一起搭配使用,co可以自动去调用generator的next方法,不在手动跳用。下面大概列下自己实现的一个co方法

function co(it) {
  // 异步递归怎么实现
  return new Promise((resolve,reject)=>{
    function next(data){ // next是为了实现异步迭代
      let { value, done } = it.next(data);
      if(!done){
        value.then((data=>{
          // 当第一个promise执行完再继续执行下一个next
          next(data);
        }), reject); // 有一个失败了就失败了
      }else{ // 迭代成功后将成功的结果返回
        resolve(value);
      }
    }
    next();
  });
}

co(gen()).then(data=>{
  console.log(data);
});
复制代码

async

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

generator写法

const fs = require('fs');

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

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
复制代码

再来看咱们的async的写法

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
复制代码

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

async函数对 Generator 函数的改进,体现在以下四点。

(1)内置执行器。 Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

(2)更好的语义。 async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

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

(4)返回值是 Promise。 async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

综上

Promise是javascript异步编程的发展的一个里程碑。在后面的不管是generator抑或是async都是对promise的进一步封装。他们的终极目的都是为了让js的异步编程看着更加同步。方便咱们在平常的开发中维护代码。以上是对js异步编程的一点点总结,欢迎吐槽。

转载于:https://juejin.im/post/5b0197aff265da0ba567d253

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值