异步编程解决方案

840 阅读10分钟

为何产生异步

  • 程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。
  • js中常见的场景:定时器、鼠标点击、Ajax请求相应,创建了将来执行的代码创建了一个将来执行的块,引入了异步机制。

js处理异步的机制

事件循环

// eventLoop是一个队列的数组,先进先出
var eventLoop = [];
var event;
while(true) {    
    if(eventLoop.length > 0){
        event = eventLoop.shift();
        try{
            event();
        } catch (err) {
            reportError(err);
        }
    }
}
  • 循环的每一轮称作一个tick,对每个tick而言,如果在队列中有等待事件,那么会从事件中摘下一个事件执行。这个事件就是你的回调函数。
  • 每循环一次都会清空一次当前的事件队列

事件队列的特点

1 先同步 - 2再微任务 -3 再宏任务

  • 微任务promise mutationObserverprocess.nextTick

  • 宏任务script ui 渲染、setTimeoutsetIntervalpostMessageMessageChannelSetImmediate

案例分析

async function async1() {
  console.log("async1 start");
  await async2();
  console.log('after-async2')
}
async function async2() { 
  console.log("async2");
}
async1();
setTimeout(() => {
  console.log('timer1')
}, 0)
console.log("start")
  • 为了方便理解本案例只写了一轮事件循环, 解析执行过程如下
// 第一个事件循环
// 1 先执行同步代码
"async1 start"  "async2" "start"
// 2再执行微任务
'after-async2'
// 3 再执行宏任务
'timer1'

45题让你搞懂异步执行顺序

异步的解决方案有哪些

回调函数

  • 将函数作为参数传递给另一个函数,当主函数恰当的时机调用回调函数

回调函数的应用场景

  • 创建dom元素,并对dom元素进行操作
var appendDiv = function(){ 
     for ( var i = 0; i < 100; i++ ){ 
         var div = document.createElement( 'div' ); 
         div.innerHTML = i; 
         document.body.appendChild( div ); 
         div.style.display = 'none'; 
     } 
}; 
appendDiv();
  • 上面的对元素的操作太过于定制,如果希望不是隐藏而是改背景颜色呢?
  • 将控制元素的代码抽出为一个函数,再将函数作为参数传递,在恰当的时机调用
var appendDiv = function(cb){ 
     for ( var i = 0; i < 100; i++ ){ 
         var div = document.createElement( 'div' ); 
         div.innerHTML = i; 
         document.body.appendChild( div ); 
         cb(div)
     } 
}; 
// 通过该函数控制无论是元素隐藏还是改背景颜色实现了具体的操作由用户控制
function operateDiv(node){
    //node.style.display = 'none'; 
    node.style.backgroundColor = 'red';
}
appendDiv(operateDiv);
  • 假如下列函数为同步的,下面的执行顺序如何?
doA(function(){
    doC()
    doD(function(){
        doF()
    })
    doE()
})
doB()
// 因为假设为同步代码,代码都是从上至下依次执行 doA doC doD doF doE doB
  • 上面还是假设全部为同步代码时的情况,如果全部为异步代码呢?执行顺序如何呢?

栗子1;

listen("click",function handler() {
    setTimeout(() => {
        ajax("http://some.url.1",function response(test){
            if(test == "hello") {
                handler();
            } else  {
                request();
            }
        })
    },500)
})

上例得到了一个3个异步嵌套的链,每个函数代表异步序列(任务、进程)中的一个步骤,这种代码被称为回调地狱

  • 栗子1 里面嵌套的层数过多,功能不够单一,重写栗子1
listen("click",handler);
function handler() {
    setTimeout(request,500);
}
function request() {
    ajax("http://some.url.1",response);
}
function response(text) {
    if(text == 'hello') {
       handler(); 
    }esle {
        request();
    }
}
  • 发现我们想要知道执行顺序需要来回跳跃阅读代码

回调的缺点

  • 查看代码时要跳来跳去,不方便阅读
  • 1 -> 2 -> 3 保证顺序执行,只用回调,将3硬编码至2中,2硬编码至1中, 会导致代码更脆弱
  • 考虑场景一多,可能的事件与路径就很多,代码就会变得非常复杂

如果调用第三方的支付函数,传入自己的支付成功后的函数

analytics.trackPurchase(purchaseData,function() {
    chargeCreditCard();
    displayThankyouPage();
})
  • 回调问题1:如果由于某些特殊的情况,第三方库analytics将传入trackPurchase的回调函数多次执行
// 解决:为了防止第三方多次调用自己的回调,你想到了:创建一个开关来处理对回调的多个并发调用
var tracked = false;
analytics.trackPurchase(purchaseData,function(){
    if(!tracked){
        chargeCreditCard();
        displayThankyouPage();
    }
})

  • 回调问题2:调用回调过早,在追踪之前
  • 回调问题3:调用回调过晚(或没有调用)
  • 回调问题4:调用回调的次数太少或太多(就像你遇到过的问题!);
  • 回调问题5:没有把所需的环境 / 参数成功传给你的回调函数;
  • 回调问题6:吞掉可能出现的错误或异常;

现在你应该更加明白回调地狱是多像地狱了吧。

回调的一些补救措施

分离回调,即一个成功的通知一个失败的通知,err-first风格,node的设计风格

function success(data) { console.log( data ); }
function failure(err) { console.error( err ); }
ajax( "http://some.url.1", success, failure );

如果即没有调用成功也没有调用失败呢?

  • 设置一个超时取消事件
function timeoutify(fn,delay){
    var intv = setTimeout(function(){
         // 如果超时了,抛出错误
         fn(new Error('Timeout'));
         intv = null;
    },delay);
    
    return function() {
        // 如果定时器还存在即没有执行上面的报错代码
        if(intv){
            // 清空定时器以及调用函数
            clearTimeout(intv);
            fn.apply(this,arguments);
        }
    }
}

Promise

Promise有哪些特点?

  • promise是一种异步解决方案, 解决回调函数以及回调嵌套的难读懂难维护的问题
  • Promise的优点是可以像书写同步代码的方式书写异步代码

有哪些特点

  • Promise是一个立即执行函数
  • Promise有三种状态,Pending、Fulfiled、Rejected。状态一经改变就不能再次改变
  • 成功后调用resolve,失败时调用reject,将上次一次Promise的返回值返回给下一个promise
  • Promise有一个then方法,可以传入2个回调函数,第一个为成功的回调,第二个参数为失败的回调
  • 每次then都会返回一个新的Promise
  • 如果上一次promise中resolve的为Promise,且为普通值,则直接将此普通值返回给成功的回调
  • 如果Promise.then中返回的为普通值(非promise 非抛错)的情况,会发生值的穿透

Promise实际运用场景

案例

案例1 :

  • 执行步骤1 -> 2 -> 3 , 将步骤1 结果传递给步骤2,将步骤2结果传递给步骤3, 获取最终步骤3的结果
// 获取接口数据
function request(params){
    return new Promise((resolve,reject) =>{
       // 假设此处为异步接口请求
       setTimeout(() => {
            resolve(params)
       },500)
    })
}
request(1)
    .then((step1Result) =>{
        // 步骤1的结果 + 1
        let params = step1Result + 1
        // 返回步骤2的执行
        return request(params)
    })
    .then((step2Result) =>{
        // 步骤2的结果 + 1
        let params = step2Result + 1
        return request(params)
    })
    .then((step3Result) => {
         // 步骤3的结果为
        console.log('step3Result',step3Result)
    })

案例1 :

  • 间隔1s 亮绿灯 2s亮黄灯 3s亮红灯
const lights = [{type:'绿灯',time:1},{type:'黄灯',time:2},{type:'红灯',time:3}]
lights.reduce((prev,cur) => {
   return new Promise((resolve,reject) => {
       prev.then((res) => {
           setTimeout(() => {
               console.log('亮灯',cur.type)
               resolve(cur.time)
           },cur.time * 1000)
       })
   }) 
},Promise.resolve())

案例3 :

  • 间隔1s 亮绿灯 2s亮黄灯 3s亮红灯 增加可以循环亮灯
// 异步事件队列循环执行,则可以理解为递归调用
function operateLight(){
    lights.reduce((prev,next) => {
        return new Promise((resolve,reject) => {
            prev.then(() => {
                setTimeout(() => {
                    console.log('亮',cur.type)
                    // 当执行到红灯的时候递归调用
                    if(cur.time === 3){
                       operateLight()
                    }
                    resolve()
               },cur.time * 1000))
           })
        })
    },Promise.resolve())
}

案例4 :

  • 间隔1s 亮绿灯 2s亮黄灯 3s亮红灯 可以控制循环亮灯次数
// 可以控制循环次数,即可以控制递归终止条件
const lights = [{ type: '绿灯', time: 1 }, { type: '黄灯', time: 2 }, { type: '红灯', time: 3 }]
function operateLight(count) {
  // 第一次亮灯也需要减去一次次数
  count--
  lights.reduce((prev, cur) => {
    return new Promise((resolve, reject) => {
      prev.then(() => {
        setTimeout(() => {
          console.log('亮', cur.type)
          // 当执行到红灯的时候递归调用
          // 每循环一次就减一次次数
          if (cur.time === 3 && count >= 0) {
            count--
            operateLight(count)
          }
          resolve()
        }, cur.time * 1000)
      })
    })
  }, Promise.resolve())
}
// 参数2 为循环亮灯的次数
operateLight(2)
promisify:
  • 将某个错误错误优先的函数改装成可以通过promise调用
// node 的 util模块提供了将某个err-first 的函数改成promise返回
// let read  = util.promisify(fs.readFileSync)
function promisify(fn){
    (...args) => {
       return new Promise((resolve,reject) =>{
            fn(...args,(err,data) => {
                resolve(data)
            })
       })
    }
}
CO库
  • 如果想实现yield函数的调用该如何处理?
let fs = require("fs").promises;
function* read() {
    try {
        let name = yield fs.readFile('name.txt', 'utf8');
        let age = yield fs.readFile(name, 'utf8');
    } catch (err) {
        console.log(err)
    }
}
const it = read()
let { value, done } = it.next()
value.then(data => {
    let { value, done } = it.next(data);
    value.then(data => {
        let { value, done } = it.next(data);
        console.log("data", data)
    })
})
//上面的调用方式嵌套太深,不易于阅读及调用,`实现思路是异步回调嵌套`
// 每次都值执行了 let { value, done } = it.next(data);,如果没有done则继续递归调用function myco(it){
    return new Promise((resolve,reject) => {
        let next = function(data){
            let {value, done } = it.next(data)
            if(!done){
                //将上次next 结果通过promise.resole(value),包装成promise
                Promise.resolve(value).then(res => {
                    // 将上次执行的结果传递给下一次函数
                    next(res)
                })
           } else {
               return resolve(data)
           }
        }
        next()
    })
}

Promise如何实现

手摸手实现promise

promise优缺点

优点

  • promsise解决了回调函数的回调地狱问题,只有上一个步骤执行成功或失败了才会执行下一个步骤
  • 确保上一个步骤执行完毕的结果无论成功失败都可以在下一个步骤中获取到

缺点

  • promise还是基于异步的回调的嵌套解决的问题

异步终极解决方案 async + await

async + await应用场景

  • 将上面使用promise实现的案例用async + await 实现一次

案例1 :

  • 执行步骤1 -> 2 -> 3 , 将步骤1 结果传递给步骤2,将步骤2结果传递给步骤3, 获取最终步骤3的结果
function request(count){
    return new Promise((resolve,reject) => {
        setTimeout(() =>{
            resolve(count)
        },500)
    })
}
// 封装一个获取步骤3结果的函数 
async function getStep3Result(callback){
    try{
        // 1 等待步骤1的执行结果
        const step1Result  = await request(1)    
        // 2 等待步骤2的执行结果
        const step2Result  = await request(step1Result + 1)
         // 3 等待步骤3的执行结果
        const step3Result  = await request(step2Result + 1)
        callback(step3Result)
    } catch(err){
        console.log('执行失败',err)
    }
}

getStep3Result(function(step3Result){
  console.log('步骤3的结果为',step3Result)
})

  • async + await 执行异步任务可以像写同步代码一样,更加容易理解 1执行完后执行2,2执行完后执行3
  • 可以通过try catch 捕获失败的错误

案例2 :

  • 间隔1s 亮绿灯 2s亮黄灯 3s亮红灯
function lightOn(type,time){
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            console.log("亮",type)
            resolve()
        },time * 1000)
    })
}
async function opeerate(){
    await lightOn('绿灯',1)
    await lightOn('黄灯',2)
    await lightOn('红灯',3)
}
opeerate()

案例3 :

  • 间隔1s 亮绿灯 2s亮黄灯 3s亮红灯 增加可以循环亮灯
// 循环亮灯即一个回合之后重新调用操作函数即可
async function opeerate(){
    await lightOn('绿灯',1)
    await lightOn('黄灯',2)
    await lightOn('红灯',3)
    opeerate()
}
opeerate()

案例4:

  • 间隔1s 亮绿灯 2s亮黄灯 3s亮红灯 可以控制循环亮灯次数
// 可以控制次数即每循环一次就减去一次
async function opeerate(time){
    time--
    await lightOn('绿灯',1)
    await lightOn('黄灯',2)
    await lightOn('红灯',3)    
    if(time-- >= 0){
        opeerate()
    }
}
opeerate(2)

案例5:

  • 如何实现createFlow,使createFlow数组里面的任务按照索引顺序依次执行

await async 优缺点

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const subFlow = createFlow([() => delay(1000).then(() => console.log("c"))]);

createFlow([
  () => console.log("a"),
  () => console.log("b"),
  subFlow,
  [() => delay(1000).then(() => console.log("d")), () => console.log("e")],
]).run(() => {
  console.log("done");
});

分析:由上面的测试用例可以看出createFlow需要返回一个函数run,且需要按照索引上一个执行完毕后,才可执行下一个

function createFlow(flow){
    const run = async function(cb) {
     flow  = flow.slice().flat(); // 最后一个参数为数组,需要拍平
      for(let i = 0; i < flow.length; i++) {
          if(flow[i].run){
              await flow[i].run()
          } else {
              await flow[i]()
          }
      }
      if(cb){
          cb()
      }
    }
    return {
        run
    }
}

优点

  • 像写同步代码一样写异步代码,更容易理解

缺点

  • 兼容性不够好

利用异步特性解决哪些问题?

切割大任务防止阻塞主线程

  • 需要执行大量的同步计算的大任务
  • 大量的同步操作会阻塞主线程,可以通过将任务拆分成一个个小任务异步执行

假设后台返回的数据量巨大

function handleManyData(data) {
  return new Promise((resolve, reject) => {
    let result = [];  // 大任务执行完毕后的结果
    let next = (data) => {
     //将大数据处理,拆分成小任务,每次取出数据的5项
      var chunk = data.splice(0, 5)
      // 假设此处为大量的同步计算需要大的计算开销
      result = result.concat(chunk.map((item) => item * 2))
      if (data.length > 0) {
       // 开启一个异步进程,让其余的计算在下一个事件环去执行
        setTimeout(() => {
          next(data)
        }, 200)
      } else {
        resolve(result);
      }
    }
    next(data)
  })
}

nextTick的实现

  • 不知道任务的具体执行事件,但是希望在当前任务的下一个tick执行
  • 实现一个简单的nextTick
let callback = []; // 放需要在下一个tick执行的事件
let lock = false; // 当前队列状态
function nextTick(cb){
    // 当前队列未被执行
    if(!lock){
        callback.push(cb)
        // 将当前的队形状态改为执行中
        lock = true
        // // 开启一个异步进程,让函数在下一个tick去执行
        setTimeout(()=>{
            executeCallback()
        })
    }
}

// 执行异步队列数据
function executeCallback(){
    // 只有在执行中的队列才可以执行清空操作
    if(lock){
    // 执行任务队列中的所有任务
        callback.map((cb) => cb())
    }
    callback = []
    lock = false    
}