js异步

632 阅读12分钟

WechatIMG20.jpeg

一、异步出现的原因

js是单线程的,一次只能干一件事,所以,所有的事情都像排队一样,等着被执行,但是如果其中一件事情在队列中需要耗费很长的时间,那下一个事件就会一直在等待。比如我们打开一个网页,接口请求时间是5秒,那页面等待5秒之后,再渲染页面,页面就会长时间留白,影响用户体验。于是,js在同步机制的缺陷下设计出了异步模式

举个例子

  • 1.不论你是先切菜还是先用电饭煲煮饭,都要等一个事情完毕后才能进行下一项,这就是一个同步的例子
  • 2.切菜的时候你也可以用电饭煲煮饭 (不用等你切完菜)这就是一个异步的例子。下面用代码演示一下异步
function a() {
  console.log("a");
  setTimeout(() => {
    console.log("a1");
  }, 1000);
}
function b() {
  console.log("b");
}
a();
b();
//打印结果 a,b,a1
//这里b不用等待一秒后a1打印完再打印,这里可以结合js的事件循环机制一起看,更加细节
// 附上js的事件循环机制文章:https://segmentfault.com/a/1190000039866826

二、异步的解决方案

1.回调函数

回调函数的概念:被作为实参传入另一函数,并在该外部函数内被调用,用以来完成某些任务的函数,称为回调函数

function b(value) {
  var bb = "everyone";
  console.log(value + bb);
}
function a(callback) {
  let value = "hello ";
  setTimeout(() => {
    callback(value);
  }, 1000);
}
a(b);
//这是一个异步回调,1秒钟之后才会执行b函数

回调函数的缺点:容易写出回调地狱(Callback hell)

  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  • 嵌套函数一多,就很难处理错误(不能用try catch,不能直接return)
let a = setTimeout(function a() {
  var name1 = "hello ";
  try {
    setTimeout(function b() {
      var name2 = "everyone " + name1; //如果这里的b函数,name2改动了,下面的c函数打印的值就会受影响,牵一发而动全身
      setTimeout(function c() {
        var name3 = "yeah!";
        return name3; // 在这里return只是为了演示,return name3 不能被接收到
        console.log(name2 + name3);
      }, 1000);
    }, 1000);
  } catch (e) {
    console.log(e, "不能捕获错误");  //这个try,catch只是为了演示,这里不能捕获到错误,因为try catch不能捕获异步错误,当执行到try时,try里面的代码放到到异步的任务队列里,没有try到任何内容,所以catch里面不打印。让同步代码执行完,去执行异步的任务队列时,这时候会报错。
  }
}, 1000);
console.log(a); //这里a不是name3,所以不能直接return

正是由于回调函数有缺陷,所以,es2015诞生了promise

2.Promise

Promise的概念:Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值

手写Promise(会围绕下图的方法展开)

WechatIMG371.jpeg

Promise的使用

WechatIMG368.jpeg

Promise一直是pending的状态,只有当调用resolve或者reject方法之后,状态才会改变,这时候会根据是成功还是失败的状态去执行相应的回调函数
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("成功");
    // reject("失败");
  }, 1000);
}).then(
  (value) => {
    console.log(value, "这是成功返回的值");
  },
  (reason) => {
    console.log(reason, "这是失败的原因");
  }
);

以下是手写Promise类核心逻辑的实现

  • Promise就是一个类(或者构造函数,这里只讲类),在执行这个类的时候,需要传递一个执行器(executor)进去,执行器会立即执行
  • executor里面有两个参数,一个叫resolve(成功),一个叫reject(失败)
  • 由于resolve和reject可执行,所以都是函数
class Promise {//类
  constructor(executor) { // 构造函数
    // 成功
    let resolve = () => { };
    // 失败
    let reject = () => { };
    //执行executor可能会报错,把错误捕获,传递到reject里面去
    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
}
  • Promise有三种状态,分别为成功Fulfilled,失败Rejected,等待Pending。
    • Pending--->Fulfilled
    • Pending--->Rejected
    • 一但状态确定就不可更改
  • resolve和reject就是用来更改状态的
    • resolve()--->Fulfilled
    • reject()--->Rejected
const PENDING = "pending"; //等待
const FULFILLED = "fulfilled"; // 成功
const REJECTED = "rejected"; // 失败
class Promise {
  constructor(exector) {
    const resolve = (value) => {
    // 一但状态确定就不可更改
      if (this.status !== PENDING) return;
      //更改状态,状态变成成功
      this.status = FUlFILLED;
       //把成功的值保持下来
      this.value = value;
    };
   const reject = (reason) => {
      if (this.status !== PENDING) return;
      this.status = REJECTED;
      this.reason = reason;
    };
    try {
      exector(resolve, reject); //立即执行,resolve
    } catch (e) {
      reject(e);
    }
  }
  status = PENDING;
  value = undefined;
  reason = undefined;
  fulfilledCallBack = undefined;
  rejectedCallBack = undefined;
}

  • then方法内部做的事就是判断状态,如果状态是成功,就调用成功的回调函数;状态失败,就调用失败的回调函数。
class Promise{
  constructor(executor){...}
  status = PENDING;
  value = undefined;
  reason = undefined;
  // then 方法 有两个参数onFulfilled onRejected
  then(successCallback, failCallback) {
    // 状态为fulfilled,successCallback,传入成功的值
    if (this.status === FULFILLED) {
    //用try,catch捕获在then方法里面抛出的错误,传入reject中
      try {
        successCallback(this.value);
      } catch (error) {
        reject(error);
      }
    }
    // 状态为rejected,执行failCallback,传入失败的原因
    if (this.status === REJECTED) {
      try {
        failCallback(this.reason);
      } catch (error) {
        this.reject(error);
      }
    }
}
  • 现在基本可以实现简单的同步代码,但是当resolve在setTomeout内执行,then时status还是pending等待状态,所以这时候要把successCallback,failCallback存储起来,一旦resolve或者reject就调用他
  • 现在基本可以实现简单的同步代码,但是同一个promsie多次调用then,如果resolve或者reject写在setTomeout内执行,只会执行最后一个then你方法,因为failCallback或者failCallback是以一个变量的方式保存的,应该以数组的方式保存

WechatIMG25.jpeg

const PENDING = "pending"; 
const FULFILLED = "fulfilled"; 
const REJECTED = "rejected"; 
class MyPromise {
constructor(exector) {
    const resolve = (value) => {
    // 一但状态确定就不可更改
      if (this.status !== PENDING) return;
      //更改状态,状态变成成功
      this.status = FUlFILLED;
       //把成功的值保持下来
      this.value = value;
      // if (this.fulfilledCallBack) this.fulfilledCallBack(value);
      if (this.fulfilledCallBack) {
        this.fulfilledCallBack.map((item) => {
          item(value);
          return;
        });
      }
    };
   const reject = (reason) => {
      if (this.status !== PENDING) return;
      this.status = REJECTED;
      this.reason = reason;
      // if (this.rejectedCallBack) this.rejectedCallBack(reason);
      if (this.rejectedCallBack) {
        this.rejectedCallBack.map((item) => {
          item(reason);
          return;
        });
      }
    };
    try {
      exector(resolve, reject); 
    } catch (e) {
      reject(e);
    }
  }
  status = PENDING;
  value = undefined;
  reason = undefined;
  fulfilledCallBack = [];
  rejectedCallBack = [];
  then(successCallback, failCallback) {
    if (this.status === FULFILLED) {
      successCallback(this.value);
    } else if (this.status === REJECTED) {
      failCallback(this.reason); 
    } else {  
      // this.fulfilledCallBack=successCallback;
      // this.rejectedCallBack=failCallback;
       // 这里是pending的状态
      this.fulfilledCallBack.push(successCallback);  
      // 用数组把多个成功或者失败回调存储起来,等到resolve或者reject的时候,依次执行
      this.rejectedCallBack.push(failCallback);
    }
  }
}
  • then方法的参数是可选的
    • then() 相当于----> then(value=>value,(reason)=>{throw reason})

WechatIMG24.jpeg

const PENDING = "pending"; 
const FULFILLED = "fulfilled"; 
const REJECTED = "rejected"; 
class MyPromise {
  constructor(executor){...}
  status = PENDING;
  value = undefined;
  reason = undefined;
  fulfilledCallBack = [];
  rejectedCallBack = [];
  then(successCallback, failCallback) {
  // 这里是如果then没有传参数,对应的
  // then() 相当于----> then(value=>value,(reason)=>{throw reason})
  const successCallback = successCallback?successCallback:(value)=>value
  const failCallback = failCallback?failCallback:(error)=>{throw error}
  
    if (this.status === FULFILLED) {
      successCallback(this.value);
    } else if (this.status === REJECTED) {
      failCallback(this.reason); 
    } else {  
      this.fulfilledCallBack.push(successCallback);  
      this.rejectedCallBack.push(failCallback);
    }
  }

  • then方法的链式调用
    • then()返回的也是一个promise对象
    • 把上一个then方法的返回的值传递给下一个then。
    • then() 返回的结果有两种类型,一种是promise对象,这时候在promise的成功或者失败回调里resolve(value)或者reject(reason)
    • 一种是非promise对象的普通值,直接返回resolve(value)
  • then方法里面不能返回它自己,报“循环引用”错误

image.png

const PENDING = "pending"; 
const FULFILLED = "fulfilled"; 
const REJECTED = "rejected"; 
class MyPromise {
  constructor(executor){...}
  status = PENDING;
  value = undefined;
  reason = undefined;
  fulfilledCallBack = [];
  rejectedCallBack = [];
  then(successCallback, failCallback) {
    successCallback = successCallback ? successCallback : (value) => value;
    failCallback = failCallback
      ? failCallback
      : (reason) => {
          throw reason;
        };
    let p = new MyPromise((resolve, reject) => {
      //then是链式调用的,1.所以返回promsie对象
      // 2.把上一个then返回的值传递下去(resolve(value)或者reject(reason)),如果是promise,判断paromsie的状态,成功还是失败,调用resolve或者reject把状态告知下一个then方法
      if (this.status === FULFILLED) {
        setTimeout(() => {
        // 这里的定时器是为了得到p, 因为这里p要在new promise()执行完才得到,现在在执行过程中,没法得到,要使用定时器,变成异步,得到 p
          try {
           // 这里用try catch 是把then()里面的错误捕获,传给reject
            const x = successCallback(this.value);
            getValueType(p, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      } else if (this.status === REJECTED) {
      // 类似成功时候的回调
        setTimeout(() => {      
          try {
            const x = failCallback(this.reason);
            getValueType(p, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      } else {
          this.fulfilledCallBack.push((value) =>
            setTimeout(() =>{
                    try{
                        getValueType(p, successCallback(value), resolve, reject)
                    }catch(e){
                        reject(e)
                    }
                }
            )
          );
          this.rejectedCallBack.push((reason) =>
            setTimeout(() =>
                    try{
                        getValueType(p, failCallback(reason), resolve, reject)
                    }catch(e){
                        reject(e)
                    }
            )
          );
      }
    });
    return p;
  }
}
// 单独抽出一个方法,判断then返回;
// 返回的如果是普通值,直接resolve(value);
// 如果返回的是promise对象,先执行then方法,判断状态,在成功的回调里resolve(value),失败的回调里reject(reason);

const getValueType = (p, x, resolve, reject) => {
// 判断x 和p 是否相等,相等就是自己调用自己,报“循环引用”错误
  if (p === x) {
    return reject(
      new TypeError("Chaining cycle detected for promise #<Promise>")
    );
  }
  // 用instanceof判断x是不是MyPromise的实例对象
  if (x instanceof MyPromise) {
    return x.then(resolve, reject); //简写
  } else {
    return resolve(x);
  }
};
  • catch捕获错误
    • 返回一个promise错误
    • 接受一个回调函数,捕获错误
⚠️注意:这里链式then的失败回调和catch都能捕获错误,但是then的失败回调只能捕获当前promises的错误,不能捕获当前then的成功回调函数里面的错误。但是catch能捕获到所有的错误,所以,链式调用的时候,用catch捕获错误
class MyPromise {
  constructor(executor){...}
  status = PENDING;
  value = undefined;
  reason = undefined;
  fulfilledCallBack = [];
  rejectedCallBack = [];
  then(){...}
  catch(failCallBack) { 
  // 相当于then()第一个参数是undefined
    return this.then(undefined, failCallBack); 
  }
}
const getValueType =()=>{...}
  • Promise.all(),Promise.race方法
    • Promise.all()允许我们按照异步代码调用的顺序,得到异步代码执行的结果
    • Promise.race()异步代码调用哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态
class MyPromise {
  constructor(executor){...}
  status = PENDING;
  value = undefined;
  reason = undefined;
  fulfilledCallBack = [];
  rejectedCallBack = [];
  then(){...}
  catch() {...}
  static all(array) {
    let result = [];
    let key = 0;
    return new MyPromise((resolve, reject) => {
      function addData(i, value) {   
        result[i] = value;
        key++;
        // 判断key === array.length,这时候,所以的异步promise才执行完毕
        if (key === array.length) {   
        // 之所以不在for下面执行resolve,在这里执行,因为如果数组里面是异步的promise对象的话,要等它执行完在返回再resolv出去
          resolve(result); 
        }
      }
      for (let i = 0; i < array.length; i++) {
        if (array[i] instanceof MyPromise) { 
        //是promise对象就把成功的值push进数组,失败的值reject出去
          array[i].then(
            (value) => addData(i, value),
            (reason) => reject(reason)
          );
        } else {
          addData(i, array[i]); //是普通值就push进数组
        }
      }
      //   resolve(result); // 如果在这里执行resolve的话,不会等promise对象执行完毕
    });
  }
  static race(array) {
    return new MyPromise((resolve, reject) => {
      for (let i = 0; i < array.length; i++) {
        if (array[i] instanceof MyPromise) {
        // 成功返回第一个resolve,失败返回第一个reject
          array[i].then(resolve, reject); 
        } else {
        // 会返回第一个resolve,因为状态已经改变了,后面resolve不会再执行
          resolve(array[i]); 
        }
      }
    });
  }
}
const getValueType =()=>{...}


  • Promise.resolve(),Promise.reject()方法
    • 如果传入的是promise对象,原封不动的返回
    • 如果传入的是普通值,包装成promise对象返回
class MyPromise {
  constructor(executor){...}
  status = PENDING;
  value = undefined;
  reason = undefined;
  fulfilledCallBack = [];
  rejectedCallBack = [];
  then(){...}
  catch(){...}
  static resolve(value) {
    if (value instanceof MyPromise) return value;
    return new MyPromise((resolve, reject) => {
      resolve(value);
    });
  }
  static reject(value) {
    if (value instanceof MyPromise) return value;
    return new MyPromise((resolve, reject) => {
      reject(value);
    });
  }
}
const getValueType =()=>{...}
  • finally无论失败还是成功都会执行
    • finally无论结果是成功或者失败,都会被执行一次
    • finally后面可以链式调用then方法,来拿到当前这个promise最终返回的结果
    • ⚠️注意:这里finally的回调函数可能返回一个promsie对象,那后面的then()要等finally执行完毕再执行,这里就要借助resolve方法
class MyPromise {
  constructor(executor){...}
  status = PENDING;
  value = undefined;
  reason = undefined;
  fulfilledCallBack = [];
  rejectedCallBack = [];
  then(){...}
  catch(){...}
  static resolve(value){...}
  finally(callBack) {
    // 1.首先要知道状态,那调用then()就能知道状态
    return this.then(
      // 2.return出去,是因为要返回一个promsie去链式调用
      (value) => {
        // callBack();
        // return value; // 3.还有把值return出去,便于下一个then()接收
        return MyPromise.resolve(callBack()).then(() => value); // 这里是finally的回调函数可能返回一个promsie对象,那就要等这个promsie对象执行完毕,再执行后面的then方法,于是,不管是返回普通值还是promsie对象,直接调用resolve()转成promise对象
      },
      (reason) => {
        // callBack();
        // throw reason;
        return MyPromise.resolve(callBack()).then(() => {
          throw reason;
        });
      }
    );
  }
}
const getValueType =()=>{...}

至此,promise的基本功能已大致实现。其实promise.then也是类似回调函数的思想,只是,区分了成功和失败的回调函数,而且then方法的链式调用,把回调函数的嵌套地狱解决了,扁平化了。但是还是没有达到我们传统的同步代码的可读性,于是Generator出现了。

3.Generator

  • generator函数2个特点,一个*(一般写在function后,function*),一个yield。
  • generator函数返回的是一个生成器对象,直到我们通过.next调用,这个函数的函数体才会执行。
  • 可以利用yield暂停生成器函数这一特点。使用生成器函数,去完成更优的异步体验。
function* fun1() {
  console.log("start");
  try{
      // 在函数内部,可以随时通过yield向外返回一个值
      // yield关键词不会向return一样结束这个生成器函数的执行,只是暂停这个生成器函数的执行。直到外界再去执行yield方法时,从yield这个位置继续往下执行
      let aa = yield "foo";   
      // 这里"bar",会作为yield "foo"的返回值,即 aa = "foo"
  }catch(error){
      console.log(error,"error")
  }
}
const generator = fun1();
// 调用fun1函数不会并不会立即去执行这个函数,而是得到一个生成器对象
console.log(generator,"generator") 
const result = generator.next();
// 直到我们手动调用这个对象的next方法,这个函数的函数体才会开始执行
// 在next方法返回的对象{value: "foo", done: false},去拿到yeild返回的值
// next方法返回的对象里面的done属性表示这个生成器是否已经全部执行完了
console.log(result,"result")
// 调用next传入了参数的话,传入的参数会作为yield语句的返回值
 generator.next("bar") 
// throw方法也是让函数继续向下执行,只不过生成器函数内部抛出一个异常,要用try,catch去捕获
generator.throw("报错啦")

下面看一个例子

//ajax("http://www.baidu.com")函数这里是一个伪代码,假设返回一个promise对象
function ajax(){
  return new Promise(...)
}
function* main(){
  const data1 = yeild ajax("http://www.baidu1.com")  
  console.log(data1)
  const data2 = yeild ajax("http://www.baidu2.com")  
  console.log(data2)
}
const g = main()
const result = g.next()  //这里result就是一个生成器对象,value值是yeild ajax("http://www.baidu.com")返回的promise对象
result.value.then((value)=>{
  const result1 = g.next(value)  // 把promise执行完返回的值,传给data1
  if(result1.done) return
  result1.value.then((value)=>{
      let result2 = g.next(value)  // 把promise执行完返回的值,传给data2
      if(result3.done) return
       // ...如此往复,可以使用递归的方式
  })
})

故在Generator函数体中可以拿到result, 这样就可以用同步的写法解决异步问题

封装一个生成器函数执行器(github.com/tj/co)

function co(generator){
    const g = generator()
    function handleResult(result){
        if(result.done) return
        result.value.then((data)=>{
            handleResult(g.next(data))
        },(error)=>{
            g.throw(error) // 外部用try catch去捕获
        })
    }
    handleResult(g.next())
}
// 调用 co(main)


async await

  • async await是generator的语法糖,相比于generator,async await不需要再去配合一个类似于co这样的执行器,内部的执行过程和generator是完全一样的
  • async函数返回一个promise对象
  • await只能在async函数里面使用
function* fun() {
  let a = yield setTimeout(() => console.log("111", 0));
  let b = yield setTimeout(() => console.log("222", 0));
}
/**
 * 等价于
 */
async function fun() {
  let a = await setTimeout(() => console.log("111", 0));
  let b = await setTimeout(() => console.log("222", 0));
}

所以现在我们大部分情况下会使用async await,它是以几乎同步的方式来实现异步