1. Javascript 的运行时模型 -- 事件循环
JS 的运行时是个单线程的运行时, 它不像其他编程语言, 比如 C++,Java,C# 这些可以进行多线程操作的语言. 当它执行一个函数时, 它只会一条路走到黑, 不会在当前函数结束之前去调用其他的函数(除非当前函数主动调用其他函数). 它也不用担心会有其他线程打扰它, 因为它的运行时只有一个线程. 如果你还记得一些计算机原理的话, 这种运行时只有一个栈, 设计起来相当的简单.
一条路走到黑的设计很棒, 因为它足够简单, 但是又是谁决定哪个函数从开始进入栈内执行呢? 答案是 JS 的运行时还有一个事件等待队列与栈搭配, 每当运行栈为空时(也就是当前函数运行结束),JS 的运行时就从当前的事件队列中取出一个消息处理, 执行与这个消息相关联的函数. 这种行为可以用以下代码来说明:
- while (eventQueue.waitForMessage()) {
- let event = eventQueue.pop();
- let handler = event.handler;
- handler(); // 执行事件关联的函数
- context.scheduler.schedule(); // 让调度器处理一下其他事务
- }
有了运行栈和事件队列之后, 我们的 Javascript 运行时已经初具雏形. 不过 Javascript 中的变量都是对象, 它们的大小通常很大, 可不是一个小小的栈能放下的, 如果我们熟悉 C++, 就会知道一般在 C++ 中我们只在栈中存储基本类型 (int, bool 等) 和指针, 而指针所指的位置是内存堆中的一个地址, 这也是 JS 的对象的存储地点. 下面这张图可以形象地解释一下 JS 运行时的模型.
2. 事件循环模型的优点和缺点
先说优点. 除了实现上的简单, Javascript 的最大优点就是完全异步, 永不阻塞. 这句话可能有点令人迷糊, 一个单线程的运行时怎么完全异步, 永不阻塞? 实际上虽然 JS 运行时单线程, 但是浏览器是个多进程多线程的环境, 这一个点在后端也一样, 虽然 Node 是个单线程 JS 运行时, 但是后端还有其他进程和线程配合 Node 一起完成响应操作.
以浏览器打开 IndexedDB 为例, 当你执行 indexedDB.open()的之后, 当前的 Javascript 运行栈就结束了, JS 可以处理其他事件的关联函数, 所以 JS 不会阻塞. 那原来的 open 操作交给谁了呢? 浏览器会调用其他线程接管这个打开数据库的过程, 当返回时, 浏览器会在 JS 运行时的事件队列中添加一个打开成功或者打开失败的事件, 同时将你当时添加的回调函数关联到事件.
再说缺点. 我们都知道 JS 的调用函数只会一条路走到黑, 而且没有正常的方法能打断这一过程, 如果这一路恰好比较长(比如进行了大量的数学运算), 就会使 JS 进入一种类阻塞的状态, 页面会无法响应. 等等! 刚说 JS 永不阻塞, 这里怎么又冒出一个类阻塞呢? 这时因为我们所说的阻塞一般都是指 IO 阻塞, 也就是 CPU 等 IO 结束的过程, 这种情况在 JS 中可以永远不会发生(注意这里是可以, 不是一定, 某些 IO 操作是有同步的 API 可以调用的). 所谓类阻塞状态呢, 就是在执行 CPU 密集型任务, 这是一种不可避免的过程. 那为什么这种情况下页面会没有响应呢? 这时因为浏览器虽然会把事件放入事件队列里, 但是由于前一个函数还没执行完, 页面响应事件关联的函数得不到执行, 自然页面会表现出不响应的状态.
3. Promise 的实现
Promise 是 JS 处理回调的一种方式, 也是利用 JS 的事件循环模型的一个编程范式包装, 也是 ES7 中 await async 的基础. 如果说回调是对 JS 事件模型最直接最拙劣的实现的话, Promise 至少给回调加了件衣服, 使它不再那么难看. 但本质上讲, Promise 还是描述 JS 事件循环模型的一种工具. 下面给个例子来说明它和 JS 事件模型的联系.
- let getUrlAsync = (url) => {
- let promise = new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.open('GET', url);
- xhr.onload = () => resolve(xhr.responseText);
- xhr.onerror = () => reject(xhr.statusText);
- });
- return promise;
- };
- getUrlAsync('http://exaple.com/text/11111')
- .then(res => console.log(res))
- .catch(error => console.log(error));
当调用 getUrlAsync 时, JS 运行时做以下事情:
1. 会创建一个 Promise 对象, 此时还是在 getUrlAsync 的栈帧里;
2. 然后创建一个 XMLHttpRequest 对象, 此时还是在 getUrlAsync 的栈帧里;
3. 调用 XMLHttpRequest 的 open 方法, 此时浏览器其他线程接管 open 过程, JS 无需等待 open 结束;
4. 给 xhr 的 onload 事件关联一个处理函数(委托), 注意此时该事件并没有进入事件队列;
5. 给 xhr 的 onerror 事件关联一个处理函数(委托), 同样此时该事件没有进入运行时的事件队列;
6. 传入
res => console.log(res)
来具体化第 4 步中的委托;
7. 传入
error => console.log(error)
来具体化第 5 部中的委托, 此时当前的运行栈就退出了, 运行时将处理其他事件.
在某一个时刻, 浏览器控制的 open 方法返回, 它会在 JS 运行时的事件队列中添加一个事件, 比如 onload
8. JS 运行时循环到 onload 事件, 并找到它的关联处理函数, 在这个例子中就是 res => console.log(res), 并运行这个函数.
最后吐槽一个博客园, 还不支持 Markdown 吗? 默认编辑器真的好难用....
来源: https://www.cnblogs.com/bill-shooting/p/9301817.html