ES6中的异步

163 阅读8分钟

  在ES6中,主要涉及到的异步有Promise对象,Generator函数和async函数。我主要会从各个的定义、特点以及涉及到的一些方法、原理等进行梳理。

Promise对象

定义:Promise是一个对象,里面保存着未来可能发生的事件。值得注意的是,定义Promise对象里的函数会立即执行。

 let promise = new Promise(function(resolve, reject){
        if(异步成功){
            resolve(value)
        }
        else{
            reject(error)
        }
    }
 )

在上述例子中,在定义promise对象的过程中,if-else语句会理解执行。

特点:Promise对象有三种状态,分别是:pending, resolved, rejected;
    状态只能由异步操作改变;
    状态一旦发生改变就不会再发生变化。

  创建Promise对象接收一个函数作为参数,这个函数接收两个方法作为参数,一个为resolve,调用时表示事件的状态由pending变为resolved。另一个是reject函数,调用时表示对象的状态由pending转换为rejected。两个方法都可以传递参数。

实例方法
1. then( )
  接收两个函数作为参数,一个是resolve( )后执行的回调函数;一个是reject( )后执行的回调函数(可选),两个函数的参数都是定义Promise对象时resolve('参数')及reject函数传过来的。
  返回的还是一个Promise对象,意味着Promise.resolve( ).then( ).then( )....then( )函数后面还可以再跟then()函数。
  上个回调函数的返回值(return)可以在下个then( )里的回调函数参数中拿到。

2. catch( )
  接收一个函数作为参数,主要处理catch( )方法之前异步过程的错误。回调函数的参数一般为error。
原理上,Promise.catch( function(err) { } ) 等价于 Promise.then( null, function(err){ } )。
  reject('error')实际上等价于throw new Error( ),抛出一个错误。
  Promise.then( ).then( ).catch( ),catch能捕获前面所有的错误。

3. finally( )
  不管promise最后的状态,在执行完then或者catch指定的回调函数以后,都会执行finally方法指定的回调函数。
  finally方法的回调函数不接受任何参数。
  实现原理:

  Promise.prototype.finally = function(callback){
      let P = this.constructor;
      return this.then(
        value => P.resolve(callback()).then(() => value),
        reason => P.resolve(callback()).then(() => {throw reason})
      )
  }

实例方法

1. Promise.all( )
  将多个promise实例,包装成一个新的promise实例;接受一个promise数组作为参数(const promise = Promise.all( [ p1, p2, ... ] ));当所有实例都变为resolved时,状态为resolved;当其中一个实例为rejected时,状态为rejected。
  实现原理:

  Promise.all = function(promises){
      return new Promise(function(resolve, reject){
          if(!isArray(promises)){
              return reject(new Error('arguments must be an array'));
          }
          let resolvedNum = 0;
          let length = promise.length;
          let resolvedValue = new Array(length);
          for(let i = 0; i < length; i++){  //使用let构建独立作用域,确保每个i是正确的值
              Promise.resolve(promises[i]).then(function(value){  //给每个实例建立resolve和reject方法的监听
                  resolvedNum ++;
                  resolvedValue[i] = value;
                  if(resolvedNum === length){
                      resolve(resolvedValue);
                  }
              },function(reason){
                  reject(reason);
              })
          }
      })
  }

2. Promise.race( )
  与Promise.all类似,只要P1, P2, P3....之中有一个实例改变状态,则状态改变,率先返回的promise实例的值就传给回调函数。
  实现原理和Promise.all( )类似。

3. Promise.resolve( )
  将现有对象转为promise对象。
  如果参数是一个thenable对象(thenable对象指的是具有then方法的对象),将这个对象转为Promise对象,立即执行then方法,状态转为resolved。
  如果参数是一个promise对象,原封不动地返回这个实例。
  如果参数不是具有then方法的对象,或者根本不是对象,则是如下情况:

  let P = Promise.resolve('Hello');
  P.then(function(s){
      console.log(s);   //'Hello'
  })

  如果不带任何参数,直接返回一个resolved状态的Promise对象。

Generator函数

  语法上,Generator函数是一个状态机,封装多个内部状态;形式上,function关键字与函数名之间有一个星号( * ),函数体内部使用yield表达式,定义不同的状态。

  Generator函数有几个特点:
  1. 函数运行后,并不会立即执行,而是返回一个遍历器接口。用next( )函数执行代码。

  2. yield(暂停标志): 用next方法执行代码时,遇到yield则暂停,并将后面的表达式的值作为返回对象的value值;如果没有遇到新的yield表达式,则执行到return语句,并将return后的表达式作为返回对象的value值;如果没有return语句,则返回undefined。

  3. next()方法:可以向函数内部传值,next( )的参数作为上一个yield表达式的返回值。这样,Generator函数的妙处就在于你可以通过yield返回的对象取得一系列的值(这个也是‘generator’的含义),还可以通过next向内部传值。因此你可以从函数内部取值,也可以向函数内部传值。

  4. yield*:用来在一个Generator函数里执行另一个Generator函数。

  5. 应用场景:异步操作的同步化表达;控制流管理;部署Iterator接口(如 Object, 这样还可以使用for of)。

  Generator函数 + yield语句可以实现异步应用。但是如何判断异步完成,将控制权再交还到Generator函数,需要有一个机制
  基于promise对象的自动执行:将异步操作包装成promise对象,用then方法交回执行权。因此yield语句后只能是一个promise对象。
  例如:读取文件:

  var fs = require('fs');
  var readFile = function(fileName){
      return new Promise (function(resolve, reject){   //将异步操作包装成一个promise对象
          fs.read(fileName, function(err, data){
              if(err){
                  return reject(err);
              }
              resolve(data);
          })
      })
  }
  
  var gen =  function* (){
      var f1 = yield readFile('./1.txt');    //保证yield语句后面是一个promise对象
      var f2 = yield readFile('./2.txt');
      console.log(f1.toString());
      console.log(f2.toString());
  }
  
  //根据异步操作的结果自动执行gen函数
  var g = gen();
  g.netx().value.then(function(data){
      g.next(data).value.then(function(data){
          g.next(data);
      })
  })                   //这种方式没有一般性,会形成回调地狱
  
  //使用递归实现一般性的自动执行函数
  function run(gen){
      var g = gen();
      function next(data){
          var result = g.next(data);
          if(result.done){
              return result.value;
          }
          result.value.then(function(data){
              next(data);                      //data的值最后会返回给gen函数里的f1和f2变量
          })
      }
      next();
  }
  
  run(gen);

async函数

  本质上是Generator函数的语法糖。但是两者也存在区别:
  1.内置执行器:async函数自带执行器,不需要像Generator函数一样需要next()函数或者机制。
  2.更好的语义:async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  3.更广的适用性:await命令后面可以是Promise对象和原始类型的值。
  4.返回的值是Promise对象:async函数的返回值是Promise对象,可以用then方法指定下一步操作。

基本用法:当函数执行的时候,遇到await就会先返回,等到异步操作完成,再接着执行函数体后面的语句。

返回的Promise对象状态:必须等到内部所有的await命令后面的Promise对象执行完,才会发生状态改变。但只要一个出错,函数就会终止执行,在此之后的代码都不会执行,可以将代码代入try catch中或者在await后面加上一个catch函数。

  async function f(){
      try{
          await Promise.reject('error');     //将await语句放入try代码块中,避免发生错误后影响后续代码的执行
      }catach(e){
      }
      return await Promise.resolve('Hello');     //可以继续执行
  }
  
  或者
  
  async function f(){
      await Promise.reject('error')
      .catch(function(error){            //使用catch函数捕捉前面的错误
          console.log(error);
      })
       return await Promise.resolve('Hello');
  }

await命令:
1.一般后面跟的Promise对象,如果不是,会先转换为Promise对象。

2.最好放在try catach代码块中,防止出错导致终止。

3.多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时出发。

假设 getFoo和getBar是两个异步操作
let foo = await getFoo();
let bar = await getBar();   //这样会先执行getFoo,再执行getBar

改成同时触发:
let [foo, bar] = await Prmoise.all([getFoo(), getBar()]);

或者
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;    //只有在这里才可以拿到resolve里的值
let bar = await barPromise;   

4.await只能在async函数中使用,在普通函数中使用会报错。

5.async函数的实现原理:将Generator函数和span函数包装在一个函数中。

async function fn(args){
    // do something
}

等价于

function fn(args){
    return span(function *(){
        // do something
    })
}

span函数的实现:
function span(genF){
   return new Promise(function(resolve, reject){         //async函数返回的是一个promise对象
       const gen = genF();                              //剩下的跟前面的run函数类似
       function next(data){
           let result;
           try{
               result = gen.next(data);
               if(result.done){
                   return resolve(result.value);
               }
           }catch(e){
               reject(e);
           }
           Promise.resolve(result.value).then(function(data){    //result.value可能不是Promise对象,将其转换为Promise对象
               next(data);
           },function(err){
               gen.throw(err);
           })
       }
       next();
   }) 
}
  1. 与其它异步处理方法的比较
    假设某个DOM元素上面部署了一系列的动画,前一个动画结束,下一个动画才能开始。如果动画中有个出错,就不再往下执行,返回上一个成功执行的动画的返回值。
    下面使用Promise对象、Generator函数和async函数三种方式来实现:
Promise: 
function chainAnimations(elem, animations){
    let ret = null;
    let p = Promise.resolve();  //构建一个Promise对象
    
    for(let anim of animations){
        p = p.then(function(data){       //构建链式操作
            ret = data;
            return anim(elem);          //执行异步操作
        })
    }
    
    return p.catch(function(e){
        
    }).then(function(){
        return ret;
    })
}

Generator:
function *chainAnimation(elem, animations){
    return span(function *(){
        let ret = null;
        try{
            for(let anim of animations){
                ret = yield anim(elem);      //执行异步操作,结果保存在ret中  
            }
        }catch(e){
            
        }
        return ret;
    })
}

async:
async function chainAnimations(elem, animations){
    let ret = null;
    try{
        for(let anim of animations){
            ret = await anim(elem);
        }
    }catch(e){
        
    }
    return ret;
}

从以上可以看出来它们之间的区别是:

  1. 因为Promise是链式的,所以有自己的catch函数。
  2. Generator函数必须使用执行器span,用户的代码写在span的参数中。
  3. async函数不需要执行器,并且更加同步化。
  4. Generator和async函数使用try catch函数更好,没有自己的catch函数。