JavaScript执行运行时 再解(一)

浏览器拿到一段 JavaScript 代码,是如何运行的?

设想一下,如果你是chrome 浏览器的开发者(这些人是真的666),如何使用Java

Script 引擎执行 JavaScript?

其实当浏览器收到一段 JavaScript 的代码的时候,浏览器首先做的,就是传递给 JavaScript 引擎,让它去执行。

然而,执行 JavaScript 并不是一锤子的买卖。宿主环境会把 JavaScript 传给 JavaScript 引擎去运行,此外,还有可能会通过 API 把 JavaScript 代码传递给 JavaScript 引擎,比如 setTimeOut,比如 new promise。它会允许 JavaScript 在特定的时机执行。

所以,我们首先应该形成一个感性的认知:一个 JavaScript 引擎会常驻在内存里面,等待我们(宿主)把 JavaScript 传递给引擎来执行。

我们都知道,JavaScript 是单线程的语言,在早起的版本里面没有异步执行代码的能力,这就意味着,宿主环境传递给 JavaScript 引擎一段代码,引擎就把代码直接顺序执行了,倘若遇到一个 alert(),那代码岂不就是卡死在 alert 那里。

但是在 ES5 之后,JavaScript 加入了 Promise,这个时候,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务了。

在这里,我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。

宏观任务和微观任务

在 winter 老师前端进阶训练营第四周里面,就用了 OC 语言来模拟一个 JavaScript 引擎,自己不会 OC,看的都不是太懂。

不过,

while(TRUE) {
	r = wait;
	execute(r);
}

整个循环做的事情就是反复的“等待-执行”,一直循环。

我们大概这么理解:宏观任务的队列就相当于事件循环。

在宏观任务当中,JavaScript 的 Promise 还会产生异步的代码,JavaScript 必须保证这些异步的代码在一个宏观任务里面完成。因此,每个宏观任务中又包含了一个微观任务队列。

img 有了宏观任务和微观任务机制,我们就可以实现 JavaScript 引擎级和宿主级的任务了。比如:Promise 永远在队列尾部添加微观任务。setTimeout等宿主 API,则会添加宏观任务。

Promise 之前博客里面有写过详细的用法。

直接看代码

    var r = new Promise(function(resolve, reject){
        console.log("a");
        resolve()
    });
    r.then(() => console.log("c"));
    console.log("b")
		// a b c
    var r = new Promise(function(resolve, reject){
        console.log("a");
        resolve()
    });
    setTimeout(()=>console.log("d"), 0)
    r.then(() => console.log("c"));
    console.log("b")
		// a b c d

这段代码里面,无论代码的顺序是什么样子的,d 一定发生在 c 之后,因为 Promise 产生的是 JavaScript 引擎内部的微任务,而setTimeout 是浏览器的 API,它产生的是宏任务。

为了理解微任务始终先于宏任务,我们设计一个实验,执行一个耗时1 秒的 Promise

    setTimeout(()=>console.log("d"), 0)
    var r = new Promise(function(resolve, reject){
        resolve()
    });
    r.then(() => { 
        var begin = Date.now();
        while(Date.now() - begin < 1000);
        console.log("c1") 
        new Promise(function(resolve, reject){
            resolve()
        }).then(() => console.log("c2"))
    });

这里我们强制了 1 秒的执行耗时,这样,我们就可以确保任务 c2 是在 d 之后被添加到任务队列的。

我们可以看到,即使耗时一秒的 c1 执行完毕,再 enque 的 c2,仍然在 d 之前执行了,这很好的解释了微任务优先的原理。

666,以后什么 Promise 题不会做呀。。。

通过一系列的实现,我们可以总结一下如何分析异步执行的顺序:

  1. 首先我们分析有多少个宏任务
  2. 在每个宏任务里面有多少个微任务
  3. 根据调用次序,确定宏任务中的微任务执行次序
  4. 根据宏任务的触发规则和调用次序,确定宏任务的执行次序
  5. 确定整个次序

再夸一遍,666,以后什么 Promise 题不会做呀。。。(winter 666)

我们再来看一个稍微复杂的例子:

    function sleep(duration) {
        return new Promise(function(resolve, reject) {
            console.log("b");
            setTimeout(resolve,duration);
        })
    }
    console.log("a");
    sleep(5000).then(()=>console.log("c"));

这是一段非常常用的封装方法,利用 Promise 把 setTimeout 封装成可以用于异步的函数。

首先我们来看,setTimeout 把整个代码分成了两个宏观任务,这里无论是 5 秒还是 50 秒还是 50min 还是 0秒,都是一样的。(写夸张点 2333)。

第一个宏观任务当中,包含了先后同步执行的 console.log(“a”) 和 console.log(“b”)。

setTimeout后,第二个宏观任务调用了 resolve,然后 then 中的代码异步得到执行,所以调用了 console.log(“c”),最后输出的顺序是 a b c。

Promise 是 JavaScript 中的一个定义,但是实际编写代码的时候,我们可以发现,它似乎并不比回调的方式书写更加简单。但是从 ES6 开始,我们有了 async/await,这个语法改进跟 Promise 配合,能够有效地改善代码结构。

新特性:async/await

要好好看下 async/await,因为自己就用过 yield

async/await 是 ES2016 新加入的特性(ES2015 就是我们所说的 ES6,ES2016 就是 ES6 更新后的一个版本,不知道正确与否,我是这么理解的。)

它提供了 for、if 等代码结构来编写异步的方式。它的运行时基础是 Promise,面对这种比较新的特性,我们先来看一下基本用法。

async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数。

async 函数时一种特殊语法,特征是在 function 关键字之前加上 async 关键字,这样就定义了一个 async 函数,我们可以使用 await 来等待一个 Promise。

function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}
async function foo(){
    console.log("a")
    await sleep(2000)
    console.log("b")
}

这段代码利用了我们之定义的 sleep 函数。在异步函数 foo 中,我们调用 sleep。

async 函数的强大之处在于,它是可以嵌套的。我们在定义了一大批原子操作的情况下,可以使用 async 函数组合出新的 async 函数。

function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}
async function foo(name){
    await sleep(2000)
    console.log(name)
}
async function foo2(){
    await foo("a");
    await foo("b");
}

这里 foo2 用 await 调用了两次异步函数 foo,可以看到,如果我们把 sleep 这样的异步操作放入某一个框架或者库中,使用者几乎不需要了解到 Promise 的概念即可进行异步编程操作了。

此外,generator/iterator 也常常跟被拿来跟异步一起来讲,然而实质上这并非异步代码,只是在缺少 async/await 的时候,一些框架用这样的特性来模拟 async/await。

有了 async/iterator,generator/iterator模拟的用法也应该被飞起。

作业:实现一个红绿灯,把一个圆形 div 按照绿色 3 秒、黄色 1 秒、红色 2 秒循环改变背景色。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div
      class="cycle"
      style="
        background-color: red;
        height: 500px;
         500px;
        border-radius: 100%;
        margin: 0 auto;
      "
    >
      666
    </div>
    <script>
      var x = document.querySelector('div');
      x.style.backgroundColor = 'black';
      function sleep(duration) {
        return new Promise((resolve, reject) => {
          setTimeout(resolve, duration);
        });
      }
      async function changeCircleColor(duration, color) {
        x.style.backgroundColor = color;
        await sleep(duration);
      }
      (async function main() {
        while (true) {
          await changeCircleColor(300, 'green');
          await changeCircleColor(200, 'red');
          await changeCircleColor(100, 'yellow');
        }
      })();
    </script>
  </body>
</html>
原文地址:https://www.cnblogs.com/ssaylo/p/13152858.html