【deep JS】Javascript异步动画图解手册 ——解析时间的秘密

442 阅读6分钟

写在前面——本篇文章的使用手册

我思考了很久为什么异步总是难以掌握的问题? 得到的答案有两点:

  1. 内部的构造,我未必都掌握了,觉得自己理解了,其实一直在黑箱操作而已,所以出了bug,不知道该怎么办。
  2. 时间,异步之所以复杂,是因为它有了时间维度。而我自己在学习异步的时候,学得异常吃力,是因为大部分的学习资料在用语言来阐述这个高纬度的过程。 所以我决定把异步全部GIF化,用动画来阐释时间,希望帮助大家更有效率的理解异步。
  • part1 setTimeout和eventloop
  • part2 promise和miscrotask queue
  • part3 generator...yield和async...await,做动画的时候快肝死了,如果大家觉得还可以,麻烦给我点个赞(卑微)

Part1 setTimeout的工作原理真的懂吗?

I.什么是异步?

  • 异步的定义

理论上来说,一个人割麦子肯定要慢于多人劳作,比如网络请求,委托给另外一个线程去处理,不要占着主线程,线程多力量大,主要目的就是为了快。同时异步还有着不可控的含义,在前言里面提过,异步要管理时间,同步虽然慢,但是顺序执行非常可靠,然而异步要思考多出来的线程怎么管理,什么时候接受结果?如果线程罢工了后面的程序怎么办?这就是快的代价。

II. setTimeout到底发生了什么

1)setTimeout构造原理——它不是原生JS

Javascript是单线程的语言,它只有一条线程或者说它只有一个callback stack,原生JS里没有异步

其实:

  1. 异步是JS调用浏览器(browser)提供的web API实现的功能。
  2. V8引擎的结构模型。下图是我简化之后的版本,大家需要知道每个区域的功能是什么:
  1. Javascript source的runtime区域,负责存储数据和运算数据
  2. Call Stack和Javascript source同属V8引擎,callback储存函数执行所需的上下文,主要管理运行顺序
  3. WEB API是browser提供的,它包含很多异步功能,比如setTimeout的timer,还有网络请求(xhr)和事件等等。

3)setTimeout的运行原理——eventloop和task queue

这段代码很基础,讲解的目的是理清楚JS是怎么运行的。

  1. JS最开始会创建一个global函数,这是默认的平时不会出现。碰到setTimeout,把函数cb传给了browser(或web API),这段代码屏蔽掉(变红)跳转执行sayHello
  2. sayHello执行完毕后(变绿),sayHello出栈
  3. 1000ms之后,browser把函数cb丢进了callstack中,执行cb(),全部执行完默认的global也出栈

3)event loop的引入

如果setTimeout的时间是0,那么到底谁先谁后呢?如果回答不清楚,那就是还缺少一片拼图,才能完成setTimeout的运行全貌。

我们看一下代码的执行过程

1.我们直接从global已经进栈开始。setTimeout进入browser,屏蔽掉之后继续执行。

2. sayHello函数先在memory(heap)里面注册,然后再执行。但此时setTimeout也返回了,这时候谁应该先进栈?
3.引入eventloop,在browser的函数进入call stack前,要进入task queue中也有叫macro queue(水蓝色)。evenloop是个逻辑,当callstack为空的时候它才会把队列中的函数推入栈中。此时栈内有global,不能入栈,先执行sayHello("First!")
global()依旧在,继续执行console.log("second!")。
4. 终于,call stack为空。callback()函数进栈执行

Part2 等待结果的promise

I. promise的运行原理——满足条件后“它”自动运行

这部分的原理,是弄清楚async await的拼图之一,所以promise对象的工作机制还是很有必要理解清晰的。

  1. promise执行后,立刻返回一个Object给fetchPromise占位。并向browser发送调用xhr网络的请求。这部分代码结束。
  2. promise.then为这个占位的object的onfulfilled属性赋值,当value有值的时候才会触发,目前不会触发。
  3. 加入200s后返回了value:“hi”,它传递回promise对象,value更新为"hi",此时onfullfill里面的display函数触发。

II. promise和setTimeout的竞争——miscro task queue

下面这段代码,我们会发现在执行到setTimeout和promise这两个返回的时候,又出现了谁先谁后的问题,这时候引入新的概念micro task queue来控制顺序。

  1. 有了前面的基础我们快速的过一遍setTimeout和fetchPromise的流程如下图所示,setTimout执行完进入task queue等待,fetchPromise向网络发送异步请求,在JS的部分也已经执行完毕,接下来执行要阻塞主线程500ms的blockFor500ms。
  2. 如果promise在200ms后得到返回值,触发执行的函数display,但是call stack里还有函数正在执行。没有办法执行那么至少让函数有地方等待。我们可能以为display会进栈task queue, 但其实display会进入一个新的队列micro task queue等待。
  3. task queuemicro task queue都遵循call stack为空的时候才进栈的规则。但是micro task queue的优先级更高,display先执行,最后执行setTimeout里面的值。
"Me first!"
"Hi" //promise
"Hello" //setTimeout

Part3 async await的pull与push

I. generator的工作原理——yield的暂停

下面这段代码,大家可以不看答案猜一猜,结果是多少?

element2 = 7 !!!

步骤如下:

  1. generator函数createFlow()执行完后返回一个对象,里面的next的指针指向createFlow(linkedlist?)
  2. element1执行,returnNextElement.next()指向createFlow,执行createFlow函数的内部,到newNum赋值运算的时候碰到yield,yield是非常强的指令,级别高于=,newNum没有完成赋值,num就return了出去给了element1,此外yield还完成了另外一个功能就是给目前的执行上下文按下了暂停键
  3. element2执行,这时候next(2)里面传了个值,继续调用createFlow,此时函数不会重新开启新的执行上下文而是在原来的基础上继续执行,上回的newNum并没有完成赋值,传入的值会自动填补为赋值的变量,newNum=2,执行yield newNum+5,遵循同样的规律进行。

II. async await只是generator+promise的语法糖

我们先看一段 async await的代码

实际上它跟下面这段代码是一回事儿
我们看一下加入异步之后的执行顺序:

1.futureData在yield出一个promise,createFlow的执行暂停,此时createFlow内部的data还没有被赋值,为undefined。

2.promise调用web API执行,比如200s后返回一个结果‘Hi’,根据promise的机制,返回结果后触发doWhenBack的执行,doWhenBack进入micro task queue等待执行。
3.event loop 等待到函数栈为空的时候推doWhenBack进栈执行(当然200s的时候函数栈早就为空了,图中为示例event loop),又调用了createFlow,继续执行此时,data自动被promise的返回值'Hi'填补上,打印出结果。

//执行结果
“Me First!”
“Me second!”
“Hi”