异步编程方案

866 阅读7分钟

前言

我们知道Javascript语言的执行环境是"单线程"。也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。

但是如果有一个任务执行的时间过长,那么后面的任务都必须等待他执行完毕才可以继续执行程序,就会造成堵塞,影响用户体验,导致整个页面卡在某个地方。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步和异步。本文主要介绍异步编程几种办法,并通过比较,得到最佳异步编程的解决方案!

事件队列

了解异步任务前,我们先补充一下事件队列的概念。 事件队列分为宏任务以及微任务

  • 宏任务(队列):macroTask,计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列
  • 微任务(队列):microTask,Promise.then, MutationObserver 微任务的优先级比宏任务高

如果执行的宏队列的任务有微队列任务的话 会添加到微队列中然后立马执行它 微队列没有任务才能继续执行宏任务

所以可以得到的结论就是, JS主线程(同步)-->微任务-->宏任务按这个顺序执行。

至于为什么要区分宏任务和微任务而不把两者放在一起可能是为了优先级吧,微任务要尽快完成,宏任务按时完成这样,个人理解X.X

异步运行机制

  • 所有同步任务都在主线程上执行,形成一个执行栈。
  • 主线程之外,还存在一个"任务队列"。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  • 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  • 主线程不断重复上面的第三步。

同步和异步

了解了事件队列就对同步和异步特别清晰了,我用自己理解的话来说把QAQ

  • 同步: 主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
  • 异步: 不进入主线程、而进入"任务队列"(task queue)的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行
  • 同步为主线任务,异步为支线任务,主线你必须一步一步往下走,而支线你可以走走停停,我瞎说的😆理解下即可

回调函数(Callback)

  • 优点: 代码简单明了
  • 缺点: 回调地狱,代码不易维护,不能try-catch,不能直接 return
ajax('XXX1', () => {
    // callback 函数体
    ajax('XXX2', () => {
        // callback 函数体
        ajax('XXX3', () => {
            // callback 函数体
        })
    })
})

代码层层嵌套,看着就刺激,回调多了后可读性很差,流程会很混乱。

这里补充下同步回调和异步回调

  • 同步回调:
    • 理解: 立即执行, 完全执行完了才结束, 不会放入回调队列中
    • 例子: 数组遍历相关的回调函数 / Promise的excutor函数
  • 异步回调:
    • 理解: 不会立即执行, 会放入回调队列中将来执行
    • 例子: 定时器回调 / ajax回调 / Promise的成功|失败的回调
// 同步回调, 不会放入回调队列, 而是  立即执行
const arr = [1, 2, 3]
arr.forEach(item => console.log(item))

// 异步回调, 会放入回调队列, 所有同步执行完后才可能执行,放入队列等待同步玩在执行
setTimeout(() => {
 console.log('timout 回调')
}, 0)

事件监听

任务的执行不取决于代码的顺序,而取决于某个事件是否发生

  • 优点: 好理解,能绑定多事件,每个事件可以指定回调函数
  • 缺点: 整个程序都要变成事件驱动型,运行流程会变得很不清晰

实现原理也是利用定时器的原理去把func放入事件队列里,等全部执行完毕之后,才会执行事件队列里的方法

f1.on('done', fn);
//当func执行时,触发监听,执行了fn,进行代码的改写

function func(){
    setTimeout(function () {
        // f1的逻辑代码
        func.trigger('done');
    }, 1000);
}

发布/订阅

Vue中也用了这种模式,订阅者可以订阅某个任务,某个任务完成时,会对订阅者进行通知,从而知道什么时候自己可以开始执行。

//订阅done事件
$('#app').on('done',function(data){
  console.log(data)
})
//发布事件
$('#app').trigger('done,'haha')

Promise

Promise本身并不是异步的,它只是实现了对异步回调的统一封装

  • 优点:
    • 一旦状态改变,就不会再变,任何时候都可以得到这个结果 .then避免了层层嵌套的回调函数,可读性比callback回调函数好,可以进行try-catch
  • 缺点:
    • 无法取消 Promise ,错误需要通过回调函数来捕获

Promise 对象有三种状态,pending(进行中)、fulfilled(已成功)和rejected(已失败)。

Promise 的状态一旦改变之后,就不会在发生任何变化,将回调函数变成了链式调用。

let p = new Promise((resolve, reject) => {
    console.log('pending(进行中)')
})

let p = new Promise((resolve, reject) => {
  reject('rejected(已失败') // 抛出错误也会被后面失败回调捕获到
})

let p = new Promise((resolve, reject) => {
  resolve('fulfilled(已成功)')
})

使用.then获取结果

let p = new Promise((resolve, reject) => {
    resolve('1')
}).then(res=>console.log(res))//1

使用.catch错误捕获

let p = new Promise((resolve, reject) => {
    throw new Error('Error')
}).catch(console.log(error))

return

Promise.resolve(1)
    .then(res => {
        console.log(res)
        return 2 //包装成 Promise.resolve(2)
    })
    .then(res => console.log(res))
//等同于
let p = new Promise((resolve, reject) => {
        resolve('1')
    }).then(res => {
        console.log(res)
        return 2 //包装成 Promise.resolve(2)
    }).then(res => console.log(res))

then链式

let p = new Promise((resolve, reject) => {
    resolve('1')
}).then(res=>{
    console.log(res)//1
    return 20
}).then(res=>{
    console.log(res)//20
    return 30
})

p.then(res=>{
    console.log(res)//30
    return 40
})

p.finally(res=>{
    console.log('无论如何都会执行')
    throw new Error('接招吧 catch')
}).catch(err=>console.log(err))

生成器Generators/ yield

生成器一句话 交出函数的执行权

  • 优点: 可以控制函数的执行 (自然的同步 / 顺序方式表达任务的一系列步骤)
  • 缺点: 调用太频繁,不能一次性得到结果,async.await出来后基本不用了,只有在react-sage中有见过了-.-

在函数名上加上*

 function* CreateGenerator() {
    console.log('one')
    let resilt =yield 1
    console.log('two')
    resilt =yield 2
    console.log('three')
    resilt =yield 3
}
let generator = CreateGenerator();//交出执行权

console.log(generator.next())//one        { value: 1, done: false }
console.log(generator.next())//two        { value: 2, done: false }
console.log(generator.next())//three      { value: 3, done: true }
console.log(generator.next())//           { value: undefined, done: true }
  • Generator 函数除了状态机,还是一个遍历器对象生成函数。
  • 可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。
  • yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

解决回调地狱

function *fetch() {
    yield ajax(url, () => {})
    yield ajax(url1, () => {})
    yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()

Async/Await

  • 优点: 代码清晰,避免了promise多次.then
  • 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

生成器语法糖 async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await。

单独使用async 可以发现默认返回的就是Promise对象

async function func(){}
console.log(func())//Promise { undefined }

await 无法单独使用 需要配合async

function fn2() {
    return Promise.resolve(123)
  }

  async function fn3() {
    const result = await fn2();//得到的值为 Promise成功/失败的值  失败用try catch捕获即可
    console.log(result)
  }
  fn3()//123

注意点

  • async函数返回的是 Promise 对象。
  • async函数执行的时候,一旦遇到await会先执行该语句,执行完后再接着执行函数体内await后面的语句。
  • await执行完才执行后面的代码意味着他们会被放到为队列中

##Promise练习题

async function async1() {
    console.log('async1 start');
    await async2();//立即执行该语句
    console.log('async1 end');//await后面的代码放到微任务队列
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function () {
    console.log('promise2');
});
console.log('script end');
// 同步 'script start'   async1 start  async2  promise1  script end
// 宏队列  setTimeout
// 微队列  async1 end   promise2
// 同-微-宏

over总结

  • JS 异步编程进化史:callback -> promise -> generator -> async + await
  • 用async+await完事
  • 端午节安康 粽子太香啦~

本文使用 mdnice 排版