js从异步锤到event Loop(个人笔记)

166 阅读10分钟

我们要了解异步必须从js的执行机制开始讲起,首先我们都知道js是单线程的执行机制,单线程是指一个时间内只能执行一件事,即并发式的执行。又因为是单线程的执行机制,所以很多时候,我们做接口请求的时候需要等待,这样可能会导致页面白屏,这给用户一个很不好的体验,所以我们让接口请求异步执行,异步即让其他同步事件先执行,等我们的异步事件加载完成后再去执行我们的异步任务。这个涉及到js中的事件循环eventloop,我们到下面再扯一扯,先来彻底的整整异步。

回调函数

最早的解决异步的方法便是回调函数。什么是回调函数呢?回调函数是函数作为参数传递给另一个函数并在其父函数完成后执行的函数。

function  foo(msg,callback) {    setTimeout ( function (){        console.log(msg);        callback()    },2000)
console.log('先打印');}function callback (){    console.log('回调')}foo('lsj',callback)

如上代码。callback作为参数传递给foo函数,foo中的setTimeout是异步的所以先执行‘先答应’,2秒之后在执行回调:


这就是一个回调函数,回调函数我们一般用来处理接口请求,

ajax(url,() => {    // 处理逻辑})

这样我们的线程就不用一直等待接口得到数据以后再去执行别的任务,我们可以让他先请求着,然后其他同步任务一起执行。

但是回调函数也有他的缺点:容易产生回调地狱,即回调函数里面嵌套一个回调函数,然后嵌套的回调函数里面又有回调函数:

ajax(url,() => {    // 处理逻辑    ajax(url,() => {        // 处理逻辑        ajax(url,() => {// 处理逻辑        })    })})

这段代码中如果我们更改某些代码,那么将会对整个代码块产生巨大的影响,一旦出现错误,整个代码块很容易瘫痪,而且这段代码一直写下去严重影响我们的阅读感。为了解决回调地狱这个问题,后续ES6出现了Generator函数 Promise函数 Async用来处理回调地狱

Generator函数

generator函数还是先上一段代码来讲一下比较好理解:

function *foo(x) {    let y = 2 * (yield(x+1))    let z = yield(y/3)    return (x + y + z)}let it = foo(5)console.log(it.next());   //(5+1)=6 {value: 6, done: false}console.log(it.next(12)); //,(y=2*12)/3 等于8 暂停 {value: 8, done: false}console.log(it.next(13)); // return= 13(z) + 24(y) + 5(x) {value: 42, done: true}

generator函数有一个标志性的符号*,和一个yield关键字。其中yield翻译成中文是产出的意思。我们要执行generator函数必须使用next才能使用,返回值是一个可以遍历的对象。如上的代码,我们第一次使用it.next()方法启动函数,代码从右向左执行 到第一个yield暂停,返回此时的结果(5+1),即输出6.第二次使用it.next(12),此时打开第一个yield函数,但是传了参数12把上次的结果覆盖,(y=2*12)/3 等于8 暂停,第三次使用it.next(13),这里最后打开最后一个yield z=13 return= 13(z) + 24(y) + 5(x) 值为 42。 在generator函数就像是yield就类似一个门,平时都是关闭的,然后next就是一把钥匙,一把钥匙开一扇门,然后next带的参数就像是拿了这把钥匙的人,它会进入门内,done表示后面还有门还可以next去执行。所以会覆盖原来的数,如果没有带参数的话,就使用上一次传递下来的值。这里需要注意,第一个next是不能带参数的,没有任何意义。 (这里generator我也只是大概的讲了一下用法,其实他还有很多知识点,迭代器什么的,这里不详讲了,大家可以去看看阮一峰大佬的博客解释的挺清楚的)

因为generator这种钥匙和门的机制,我们非常适合用来处理异步操作,同时也不会产生回调地狱:

function *fetch() {    yield ajax(url, ()=> {})    yield ajax(url, ()=> {})    yield ajax(url, ()=> {})}let a = fetch()a.next()a.next()a.next()

这样的代码看起来就非常的舒服和简洁。说完generator就不得不说说Promise async 和await。特别是await,他算是generator的语法糖。

Promise

首先来说说promise把,它翻译成英文是“承诺”,他一般配合着then方法一起使用。其实感觉翻译成中文后还挺好理解的,你向你女朋友发起一个承诺,未来你会娶她,然后你一直为这个承诺准备着,赚钱,让自己变得更好,最后结果他被你感动,嫁给了你,但是也有可能他等不了你,嫁给了别人(也别难受你会遇见更好的)。这其中你的准备和两种结果就对应着promise的三种状态:

1. 等待中(pending)
2. 完成了(resolved)
3. 拒绝了(rejected)

上代码来看看吧:

 new Promise ((resolve,reject) => {     resolve('结婚')           // reject ('滴滴好人卡') })

这个箭头函数就是你说的话  (resolve,reject) => {
resolve('结婚') 
// reject ('滴滴好人卡')
}    

我们用Promise包裹就变成了一个承诺,他会有两种返回结果,成功和失败,并且是不可逆的,成功了就成功了失败了就失败了。

好现在我们基本了解了Promise,现在用它来处理异步。

function A () {      return new Promise((resolve,reject)=>{        setTimeout (()=> {            resolve (1)            console.log('模拟请求数据');        },1500)    })}function B () {    return new Promise((resolve,reject)=>{         setTimeout (()=> {             resolve (2)             console.log('B');         },500)     }) }function name() {    console.log('lsj');    }A().then(B).then(name) 
//模拟请求数据
//B
//lsj

Promise.resolve(1).then(res => {    console.log(res); //1    return 2   //使用return的话,会被Promise.resolve(2)包裹}).then((res) => {    console.log(res); //2})

我们通多.than关键字配合Promise使用来解决异步问题。

为了让大家更清楚的了解Promise我们来手写一个Promise函数。

const PENDING = 'pending'const RESOLVED = 'resolved'const REJECTED = 'rejected'function myPromise(fn) {    const that = this    that.state = PENDING    that.value = null    that.resolvedCallbacks = []    that.rejectedCallbacks = []    // 完善reject 和resolve函数    function resolve(value){        if(that.state===PENDING){            that.state ===RESOLVED            that.value = value            that.resolvedCallbacks.map(cb => cb(that.value))        }    }    function reject(value){        if(that.state===PENDING){            that.state === REJECTED            that.value = value            that.rejectedCallbacks.map(cb => cb(that.value))        }    }    //完善fn    try{        fn(resolve,reject)    } catch(error){        reject(error)    }}myPromise.prototype.then = function(onFulfilled,onRejected) {    const that = this    // 首先判断两个参数是否为函数类型, 因为这两个参数是可选的    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => {return v}    onRejected = typeof onRejected === 'function' ? onRejected : r => { throw r }    if( that.state === PENDING ) {  //如果状态为准备是要返回执行完才能执行下去        that.resolvedCallbacks.push(onFulfilled)        that.rejectedCallbacks.push(onRejected)              }    if( that.state === RESOLVED) {        onFulfilled(that.value)    }    if(that.state === REJECTED) {        onRejected(that.value)    }}var test = new myPromise((resolve, reject) => {    setTimeout(() => {      resolve(1)      console.log('1');    }, 3000)  })    function test2() {    return new myPromise((resolve, reject) => {      setTimeout(() => {        resolve(2)        console.log('2');      }, 500)    })  }    test.then(test2)   // 三秒后输出1 然后再过0.5秒输出2  

Promis我们最主要要注意的是三种状态的转变,一开始准备阶段,然后一直等到变成成功或者失败后才能继续.then执行,否则他会一直等待状态的改变,即使是个需要等待的计时器我们也要等这个计时器执行,状态改变,我们才能去执行.then 里面的函数。

现在我们来聊一聊最后一个异步的解决方案:

Async/await(终极解决方案)


其实Async就是promise的语法糖,一个函数如果加上了Async,那么该函数就会返回一个Promise。


上面的代码和下面的这个其实本质上是一样的:

function test() {    return new Promise ((resolve,reject) => {        resolve('1')    })}

你看两者的对比,代码量一下就上来了。所以Async可以很大的简化我们的代码。

而await则是generator 加上Promise后的语法糖,内部实现了自执行generator。

let a = 0 let b = async () => {    a = a+ await 10   //await 内部实现了generator,generato会保留堆栈中的东西    console.log('2',a);  //(2 ,10)}b()

Async/await使用如下

async function async1() {await async2() console.log('async1 end')  }async function async2() {console.log('async2 end')  }async1()   //async2 end     async1 end

相较于promise,虽然promise解决了回调地狱的问题,但是疯狂的.then导致Promise链,使得代码不雅观,语义不明显,不能很好的表示代码的执行过程。async则能在函数内部就调用await,使得代码看起来很优雅。但是async/await也有他的缺点,在代码没有依赖性的情况下(函数A不依赖函数B的返回结果)使用async/await会增加性能消耗。如果他们之间没有依赖性的话建议使用Promise.all的方法来执行。

以上就是处理异步的方法。说完异步真好理一理event loop那将绝杀。

event loop

我们在一开始就提到过js是单线程的执行任务,所以任务都是一个个执行的。所以在js的执行中有个执行栈。这个执行栈你可以理解成一个放羽毛球的球筒子,我们将任务一个一个的放进去,然后最后放进去的最先出结果然后以此类推:

function foo(b) {    var a = 5;    return a*b+10}function bar(x) {    var y= 3    return foo(x*y)}bar(6)   

这个函数bar先执行在执行foo() 即先存入bar() ,再存入foo()栈中, 然后先输出foo的返回值,再输入bar的返回值(执行栈)可以认为 执行栈 是一个存储函数调用的栈结构。

然后我们了解这个执行栈以后再来给大家讲一下宏任务和微任务。

宏任务:(还包括同步代码)

微任务:


我们先记住上面的宏任务和微任务。然后接下来我来讲一讲他们的执行顺序

首先我么我们代码分为同步代码和异步代码。当我们的js代码开始执行时我们先跑的是同步代码(宏任务)。js首先回将我们所以的同步代码执行完毕。然后就开始执行异步代码,但是异步代码里面有宏任务和微任务,我们先执行那个呢。首先我们先把所有的微任务执行完毕,执行完所有的微任务后,如果有必要的话会渲染页面,开始下一轮的Event loop 执行宏任务当中的异步代码,也就是setTimeout中的回调。



我们可以把执行栈看做是一个车厢,这节车厢只有一个出口,然后大家有买坐票的和站票的,其中我们把买坐票的看作是同步任务,当火车当了终点站以后大家下车,肯定是让站在火车中间同步任务先下车,等同步任务的兄弟们都走完了,然后就让靠近走道的那一列的微任务下车,然后再让靠里面的宏任务下车。通过一个火车车厢下车的顺序我们就更好的理解event loop的原理。在上个代码解释一下

console.log('script start')   //1   async function async1() {      await async2()   //await看出是让出线程的标志 所以看成同步这里  但是下面的还是异步的  console.log('async1 end')  //5   // 第五步的时候因为同步任务执行完毕,开始执行所以的微任务}async function async2() {  console.log('async2 end')  //2}async1()setTimeout(function() {  console.log('setTimeout') //8   宏任务挂起等所有的微任务执行完以后在执行}, 0)new Promise(resolve => {  console.log('Promise')  //3   这里也是同步,立即执行的即状态还是pending  resolve()})  .then(function() {    console.log('promise1')  //6  })  .then(function() {    console.log('promise2')  //7  })console.log('script end')  //4

以上就是eventloop在浏览器下执行的方式,但是在node.js中eventloop执行方式是不同的。我也还不是很明白,等下次研究好了,在更新,就不误人子弟。

最后:

能看到这里的呀,都是人才。