JavaScript 只有一个主线程,是单线程模式。但是为了解决长时间排队的问题,CPU可以不管IO操作,挂起处于等待中的任务,先运行排在后面的任务。等到IO操作返回了结果,再回过头,把挂起的任务继续执行下去。

程序中的任务可以分成两个:同步任务和异步任务

同步任务:没有被引擎挂起、在主线程上排队执行的任务。

异步任务:是那些被引擎放在一边,不进入主线程、而进入任务队列里的任务。

只有引擎认为某个异步任务可以执行了(例如Ajax操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入到主线程。

异步任务的模式

1、回调函数 => 异步操作最基本的方法

  //f2 必须等到 f1 执行完成才能执行
  
  function f1(){
    //...
  }
  function f2(){
    //...
  }
  
  f1()
  f2()
  

此时,如果 f1 是异步操作, f2 会立即执行,不会等到 f1 结束再执行

使用回调函数:将 f2 写成 f1 的回调函数

  function f1(callback){
      //...
      callback()
  }
  function f2(){
      //...
  }
  
  f1(f2)

回调函数的优点是简单、容易理解和实现。缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪,并且每个任务只能指定一个回调函数。

2、事件监听 => 采用事件驱动模式

  f1.on('done',f2) //jQuery的写法
  

当 f1 发生 done 事件,就执行 f2 对 f1 进行改写

  function f1(){
    setTimeout(function(){
        //...
        f1.trigger('done') //执行完成后,立刻触发 done 事件
    },1000);
  }

事件监听的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而去可以去耦合,有利于实现模块胡啊。缺点是整个程序都是变成事件驱动型,运行流程会变得很不清晰。

3、发布/订阅 => 观察者模式

事件可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就像信号中心“发布”一个信号,其他任务也可以想信号中心“订阅”这个信号,从而知道什么时候自己可以开始执行。

  jQuery.subscribe('done',f2);// Ben Alman 的 Tiny Pub/Sub jQuery 的一个插件

f2 向信号中心 jQuery 订阅 done 信号,对 f1 进行改写

  function f1(){
    setTimeout(function(){
      //...
      jQuery.publish('done')// f1 执行完成后,向信号中心 jQuery 发布 done 信号
    },1000);
  }

Promise

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

Promise => 一个保存着某个未来才会结束的事件(通常是一个异步操作)结果的容器。

Promise 对象有两个特点:

1、对象的状态不受外界影响 三种状态: pending(进行中)、 fulfilled(已成功)、 rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。 状态变化只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。

  const promise = new Promise(function(resolve,reject){
    if(/*异步操作成功*/){
      resolve(value)//将Promise对象的状态由pending变为fulfilled
    }else{
      reject(error)//将Promise对象的状态由pending变为rejected
    }
  })

Promise 实例生成以后,可以调用then方法分别指定 fulfilled 状态和 rejected 状态的回调函数。

   promise.then(function(value){
     //success
   },function(error){
     //failure
   })

then 方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。

Promise 新建之后就会立即执行

  let promise = new Promise(function(resolve,reject){
    console.log('Premise')
    resolve();
  });
  promise.then(function(){
    console.log('resolved.')
  });
  conlose.log('Hi!')
  
  //Promise  Promise立即执行 打印
  //Hi! 
  //resolved  then 方法指定的回调函数,将在当前脚本所有的同步任务执行完才会执行,所以resolved 最后输出
  

如果调用resolve函数和reject函数时 带有参数,那么他们的参数会被传递给回调函数。reject 函数的参数通常是Error对象的实例,表示抛出的错;resolve 函数的参数除了正常的值以外,还可能是另一个 Promise 实例

  const p1 = new Promise(function(resolve,reject){
    //...
  })
  const p2 = new Promise(function(resolve,reject){
    //...
    resolve(p1); // 此时 p1 的状态就会传递给p2,p1 的状态决定了 p2 的状态。如果 p1 的状态是 pending ,那么 p2 的回调函数就会等待 p1 的状态改变;如果 p1 的状态已经是 fulfilled 或者 rejected ,那么 p2 的回调函数将会立刻执行
  })
  
  const p1 = new Promise(function(resolve,reject){
    setTimeout(()=> reject(new Error('fail')),3000)
  })
  
  const p2 = new Promise(function(resolve,reject){
    setTimeout(()=> resolve(p1),1000)
  })
  
  p2.then(result =>console.log(result)).catch(error=>console.log(error))
  //Error:fail

在上面代码中,p1是Promise,3秒之后变为 rejected。 p2 的状态在1秒之后改变,resolve 方法返回的是p1。 由于 p2 返回的是另一个Promise,导致 p2 自己的状态无效了,由 p1 的状态决定 p2 的状态。所以,后面的 then 语句都变成针对后者(p1)。又过了 2 秒,p1 变为 rejected ,导致触发 catch 方法指定的回调函数。(当p2 内部是一个异步任务的时候,p2 的then方法会比resolve方法先执行)

Promise 指定多个回调 => 当Promise对象状态改变为对应状态时都会调用,如果Promise对象状态没有改变时就不会调用

  let p = new Promise((resolve,reject)=>{
    resolve('OK')
  })
  //指定回调  - 1
  p.then(value=>{
    console.log(value)
  })
  //指定回调  - 2
  p.then(value=>{
    alert(value)
  })
  
  //OK
  //弹框 OK

改变 Promise 对象状态和指定回调函数谁先执行?

1、都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调

2、如何先改变状态再指定回调?

1、再执行器中直接调用 resolve/reject

2、延迟更长时间才调用then

3、什么时候才能得到数据?

1、如果先制定的回调,那当状态发生改变时,回调函数就会调用,得到数据

2、如果先改变的状态,那当指定回调时,回调函数就会调用,得到数据。

promise.then() 返回新的 promise 的结果状态由什么决定?

1、简单表述:由then() 指定的回调函数执行的结果决定

2、详细表达:

  1. 如果抛出异常,新 promise 变为 rejected,reason 为抛出的异常

  2. 如果返回的是非 promise 的任意值,新 promise 变为 fulfilled ,value为返回的值

  3. 如果返回的是另一个新promise,此 promise的结果就会成为新 promise 的结果

     let p = new Promise((resolve,reject)=>{
       resolve('')
     });
     //执行 then 方法
     let result = p.then(value=>{
       //console.log(value)
       //1、抛出错误
       //throw "出了问题"
       //2、返回结果是非 Promise 类型的对象
       // return 521;
       //3、返回结果是 Promise 对象
       return new Promise((resolve,reject){
         //resolve('success') //success
         //reject('error')  //error
       })
       
     },reason=>{
       console.warn(reason)
     })
     
     console.log(result)
    

promise 如何串连多个操作任务?

1、promise 的then() 返回一个新的promise ,可以开成 then() 的链式调用

2、通过 then() 的链式调用串连多个同步/异步任务

    let p = new Promise((resolve,reject)=>{
      setTimeout(()=>{
        resolve('ok')
      },1000)
    })
    
    p.then(value=>{
      return new Promise((resolve,reject)=>{
        resolve('success')
      })
    }).then(value=>{
      console.log(value)  //success
    }).then(value=>{
      console.log(value)  //undifined
    })

promise 异常穿透 1、当使用promise 的then 链式调用时,可以在最后指定失败的回调 2、前面任何操作出了异常,都会传到最后失败的回调中处理

   let p = new Promise((resolve,reject) =>{
     setTimeout(()=>{
       //resolve('ok')
       reject('Error)
     },1000)
   })
   p.then(value=>{
     console.log(111);
     //中断promise链的唯一方法
     return new Promise(()=>{})
   }).then(value=>{
     console.log(222)
   }).then(value=>{
     console.log(333)
   }).catch(reason=>{
     console.warn(reason)
   })
   

Promise.prototype.then()

为Promise 实例添加状态改变时的回调函数

Promise.prototype.catch()

指定发生错误时的回调函数

Promise.prototype.finally()

指定不管 Promise 对象最后状态如何,都会执行的操作

Promise.all()

将多个 Promise 实例,包装成一个新的 Promise 实例

    const p = Promise.all([p1,p2,p3])

p的状态由 p1、p2、p3 决定

1、只有 p1、p2、p3 的状态都变成 fulfilled, p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。

2、只要 p1、p2、p3 之中有一个被 rejected ,p的状态就变成 rejected ,此时第一个被 reject 的实例的返回值,会传递给p的回调函数

Promise.race()

  const p = Promise.race([p1,p2,p3])

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

Promise.any()

  Promise.any([
    fetch('https://v8.dev/').then(()=>'home'),
    fetch('https://v8.dev/blog').then(()=>'blog'),
    fetch('https://v8.dev/docs').then(()=>'docs'),
  ]).then((first) =>{ //只要有一个 fetch() 请求成功
    console.log(first);
  }).catch((error) =>{ //所有三个 fetch() 全部请求失败
    console.log(error);
  })

async函数

函数的返回值是一个promise对象

promise 对象的结果由async 函数执行的返回值决定

  async function main(){
    //1、如果返回值是一个非Promise类型的数据
    //return 521;
    //打印的promise 状态 fulfilled 值 521
    //2、如果返回的是一个Promise 对象 
    //return new Promise ((resolve,reject) => {
    //  //resolve('ok') //状态fulfilled 值 ok
    //  //reject('error') //状态rejected 值 error
    //})
    //打印的promise的状态和结果都根据返回的promise 的状态,结果一致
    //3、抛出异常
    throw "oh NO";
    //打印的promise 状态rejected 值 抛出的异常值 oh NO 
  }
  
  let result = main()
  
  console.log(result)
  

await 表达式

await 右侧的表达式一般为 promise对象,但也可以是其他的值

如果表达式时promise 对象,await 返回的是 promise 成功的值

如果表达式时其他值,直接将此值作为 await 的返回值

注意:

await 必须写在 async 函数中, 但是 async 函数中可以没有await

如果 await 的promise 失败了,就会抛出异常,需要通过 try ... catch 捕获处理

  async function main(){
    let p = new Promise((resolve,reject)=>{
      //resolve('ok')
      reject('error')
    })
    //1、右侧为promise的情况
    //let res = await p;
    //console.log(res) //await 返回 promise对象成功的那个值  ok
    //2、右侧为其他类型的数据
    //let res2 = await 20;
    //console.log(res) //await 右侧的值是什么await 返回的值就是什么 20 
    //3、如果promise 是失败的状态
    try{
      let res3 = await p;
    }catch(e){
      console.log(e) //error
    }
  }
  main()

async 和 await 结合实践

  /**
  * resource 读取 1.html 2.html 3.html 文件内容 并且拼接输出
  */
  
  const fs = require('fs')
  const ytil = require('util')
  const mineReadFile = util.promisify(fs.readFile);
  
  //普通回调函数方法
  fs.readFile('./resource/1.html',(err,data1 =>{
    if(err) throw err;
    fs.readFile('./resource/2.html',(err,data2)=>{
      if(err) throw err
      fs.readFile('./resource/3.html',(err,data3)=>{
        if(err) throw err
        console.log(data1+data2+data3)
      })
    })
  })
  
  //async 和 await 
  async function main(){
    try{
      //读取第一个文件的内容
      let data1 = await mineReadFile('./resource/1.html')
      //读取第二个文件的内容
      let data2 = await mineReadFile('./resource/2.html')
      //读取第三个文件的内容
      let data3 = await mineReadFile('./resource/3.html')

      console.log(data1+data2+data3)
    }catch(e){
      console.log(e)
    }
  }
  
  main()