模拟实现 JS 引擎:深入了解 JS机制 以及 Microtask and Macrotask

news/2024/7/5 1:07:58

如果JavaScript是单线程的,那么我们如何像在Java中那样创建和运行线程?

很简单,我们使用events或设定一段代码在给定时间执行,这种异步性在 JavaScript 中称为 event loop

在这篇文章中,主要想分析两个点:

  • Javascript 中的 event loop 系统是如何工作;
  • 实现自定义 Javascript 引擎来解释 event loop 系统的工作原理并演示其任务队列、执行周期。

JavaScript 中的 Event Loop 机制

JavaScript 是由 Stack 栈、Heap 堆、Task Queue 任务队列组成的:

  • Stack:用来是一种类似于数组的结构,用于跟踪当前正在执行的函数;
  • Heap :用来分配 new 创建的对象;
  • Task Queue :是用来处理异步任务的,当该任务完成时,会指定对应回调进入队列。

运行以下同步任务时

console.log('script start');
console.log('script end');
复制代码

JavaScript 会依次执行代码,首先执行该脚本,具体分为以下几步

  1. 获取该脚本、或输入文件的内容 ;

  2. 将上述内容包裹在函数内;

  3. 作为与程序关联的“start”或“launch”事件的事件处理程序;

  4. 执行其他初始化;

  5. 发出程序启动事件;

  6. 事件被添加到事件队列中;

  7. Javascript引擎将该事件从队列中拉出并执行注册的处理程序,然后运行!— “Asynchronous Programming in Javascript CSCI 5828: Foundations of Software Engineering Lectures 18–10/20/2016” by Kenneth M. Anderson

总结一下就是,Javascript 引擎会将脚本内容包裹在 Main 函数内,并将其关联为程序 startlaunch 事件的对应处理程序,然后 Main 函数进入 Stack ,然后遇到 console.log('script start') ,将其入栈,输出 log('script start'),待其运行完毕之后出栈,直到所有代码运行完。

如果存在异步任务时

console.log('script start');
setTimeout(function callback() {
    console.log('setTimeout');
}, 0);
console.log('script end');
复制代码

第一步,同上图,运行 console.log('script start'),然后遇到**WebAPIs **(DOMajaxsetTimeout

执行setTimeout(function callback() {}) 得到结果是在得到一个 Timer ,继续执行 console.log('end')

此时如果 timer 运行完成,会让其对应 callback 进入Task Queue

然后当 Stack 中函数全部运行完成之后(也就是 Event Loop 的关键:如果 Stack 为空的话,按照先入先出的顺序读取 Task Queue 里面的任务),将 callback 推入 Stack 中执行。

所以上述代码的结果如下

console.log('script start');
setTimeout(function callback() {
	console.log('setTimeout');
}, 0);
console.log('script end');
// log script start
// log script end
// setTimeout
复制代码

以上是游览器利用 Event Loop 执行异步任务时的机制。

Microtask 和 Macrotask 以及实现 JS 引擎

Microtask 以及 Macrotask 都属于异步任务,它们各自包括如下api:

  • Microtask:process.nextTickPromisesMutationObserver
  • Macrotask:setTimeoutsetIntervalsetImmediate 等。

其中 Macrotask 队列就是任务队列,而 Microtasks 则通常安排在当前正在执行的同步任务之后执行,并且需要与当前队列中所有 Microtask 都在同一周期内处理,具体如下

for (macroTask of macroTaskQueue) {
    // 1. 处理 macroTask
    handleMacroTask();
      
    // 2. 处理当前 microTaskQueue 所有 microTask
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}
复制代码

执行如下代码

// 1. 首先进入 Stack log "script start"
console.log("script start");
// 2. 执行webAPi,完成后 anonymous function 进入 task queue
setTimeout(function() { 
    console.log("setTimeout");
}, 0);
new Promise(function(resolve) {
    // 3. 立即执行 log "promise1"
    console.log("promise1");
    resolve();
}).then(function() {
    // 4. microTask 安排在当前正在执行的同步任务之后
    console.log("promise2");
}).then(function() {
    // 5. 同上 
    console.log("promise3");
});
// 6. log "script end"
console.log("script end");
/*
script start
promise1
script end
promise2
promise3
setTimeout
*/
复制代码

所以输出结果是 1 -> 3 -> 6 -> 4 -> 5 -> 2。

接下来,利用 Javascript模拟 JS Engine,这一部分可以优先查看Microtask and Macrotask: A Hands-on Approach,这篇文章,然后来给如下代码挑错。

首先在 JSEngine 内部维护宏任务、微任务两个队列macroTaskQueuemicroTaskQueue 以及对应的 jsStack 执行栈,并定义相关操作。

class JsEngine {
      macroTaskQueue = [];
      microTaskQueue = [];
      jsStack = [];

      setMicro(task) {
        this.microTaskQueue.push(task);
      }
      setMacro(task) {
        this.macroTaskQueue.push(task);
      }
      setStack(task) {
        this.jsStack.push(task);
      }
	  setTimeout(task, milli) {
        this.macroTaskQueue.push(task);
      }
}
复制代码

接下来定义相关运行机制以及初始化操作

class JsEngine {
    ...
    // 与event-loop中的初始化对应
    constructor(tasks) {
        this.jsStack = tasks;
        this.runScript(this.runScriptHandler);
    }
    runScript(task) {
    	this.macroTaskQueue.push(task);
    }
	runScriptHandler = () => {
        let curTask = this.jsStack.shift();
        while (curTask) {
          	this.runTask(curTask);
          	curTask = this.jsStack.shift();
        }
    }
    runMacroTask() {
        const { microTaskQueue, macroTaskQueue } = this;
		// 根据上述规律,定义macroTaskQueue与microTaskQueue执行的先后顺序
        macroTaskQueue.forEach(macrotask => {
        	macrotask();
          	if (microTaskQueue.length) {
            	let curMicroTask = microTaskQueue.pop();
            	while (curMicroTask) {
              		this.runTask(microTaskQueue);
             		curMicroTask = microTaskQueue.pop();
            	}
        	}
        });
    }
	// 运行task
    runTask(task) {
    	new Function(task)();
    }
}
复制代码

利用上述 Js Engine 运行如下代码

const scriptTasks = [
      `console.log('start')`,
      `console.log("Hi, I'm running in a custom JS 	engine")`,
      `console.log('end')`
    ];
const customJsEngine = new JsEngine(scriptTasks);
customJsEngine.setTimeout(() => console.log("setTimeout"));
customJsEngine.setMicro(`console.log('Micro1')`);
customJsEngine.setMicro(`console.log('Micro2')`);
customJsEngine.runMacroTask();
复制代码

最终得到结果

start
Hi, I'm running in a custom JS engine
end
Micro1
setTimeout
复制代码

总结

查了些资料,翻了一些视频,把这个上述问题重新梳理了一下。

参考

  • www.youtube.com/watch?v=8aG…
  • blog.bitsrc.io/microtask-a…
  • juejin.im/entry/58d4d…

http://www.niftyadmin.cn/n/4223545.html

相关文章

centos 中文man手册安装

中文man手册安装包地址https://src.fedoraproject.org/repo/pkgs/man-pages-zh-CN/ wget https://src.fedoraproject.org/repo/pkgs/man-pages-zh-CN/manpages-zh-1.5.1.tar.gz/13275fd039de8788b15151c896150bc4/manpages-zh-1.5.1.tar.gz tar fx manpages-zh-1.5.1.tar.gz …

nexus学习 一、nexus介绍及手动安装

一、Neuxs介绍 ​ Nexus是一个强大的Maven仓库管理器,它极大的简化了本地内部仓库的维护和外部仓库的访问。 ​ Nexus是一个强大的Maven仓库管理器,它极大的简化了本地内部仓库的维护和外部仓库的访问。 ​ 如果使用了公共的Maven仓库服务器&#xff0c…

python-web自动化环境安装

web自动化环境安装 1、安装selenium 命令行使用以下命令安装selenium:pip install -U selenium 2、安装chrome浏览器 3、chromedriver的下载 : chromedriver放到python安装根目录下面即可 chromedriver下载地址:http://npm.taobao.org/…

GlobeControl 做鹰眼图

之前在网上看的都是mapcontrol做鹰眼的例子,还一个是globe作为主视图,mapcontrol作为鹰眼的例子,自己根据鹰眼的原理,写了一个,大致说下我的思路:1、添加mapcontrol的OnExtentUpdated方法2、在globecontrol…

1.5.1 Python函数初识

一、为什么要使用函数? 1,避免代码重用 2,提高代码的可读性 二、函数的定义与调用 1,函数定义: def func(): 函数注释print(函数体)return 返回值 复制代码 定义:def关键字开头,空格之后接函数名和圆括号,…

k8s介绍性文章索引

2019独角兽企业重金招聘Python工程师标准>>> containered替代了docker engine吗? - Kubernetes Containerd集成进入GA阶段转载于:https://my.oschina.net/u/2475751/blog/3003690

关于自定义异常的层次

纵观众多的开源框架,甚至是jdk自身,异常几乎总是按一定的类层次结构组织起来。那种认为一个系统只需要提供一个异常基类,其余所有异常都是只需要继承这一个基类的观点是武断和片面的。我们至少可以从两个方面来认识异常层次的重要性&#xff…

nexus学习 二、nexus 说明

nexus 说明 component name的说明 maven-central:maven中央仓库,默认从https://repo1.maven.org/maven2/拉取jarmaven-releases:私库发行版jarmaven-snapshots:私库快照(调试版本)jarmaven-public&#x…