SegmentFault JS精读最新的文章
2017-09-16T11:45:58+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
JavaScript 异步、栈、事件循环、任务队列
https://segmentfault.com/a/1190000011198232
2017-09-16T11:45:58+08:00
2017-09-16T11:45:58+08:00
谷雨
https://segmentfault.com/u/1024dylan
126
<h2>概览</h2>
<p><img src="/img/bVU9kG?w=922&h=706" alt="图片描述" title="图片描述"><br>我们经常会听到引擎和runtime,它们的区别是什么呢?</p>
<ul>
<li>引擎:解释并编译代码,让它变成能交给机器运行的代码(runnable commands)。</li>
<li>runtime:就是运行环境,它提供一些对外接口供Js调用,以跟外界打交道,比如,浏览器环境、Node.js环境。不同的runtime,会提供不同的接口,比如,在 Node.js 环境中,我们可以通过 <code>require</code> 来引入模块;而在浏览器中,我们有 <code>window</code>、 DOM。</li>
</ul>
<p>Js引擎是单线程的,如上图中,它负责维护任务队列,并通过 Event Loop 的机制,按顺序把任务放入栈中执行。而图中的异步处理模块,就是 runtime 提供的,拥有和Js引擎互不干扰的线程。接下来,我们会细说图中的:栈和任务队列。</p>
<h2>栈</h2>
<p>现在,我们要运行下面这段代码:</p>
<pre><code>function bar() {
console.log(1);
}
function foo() {
console.log(2);
far();
}
setTimeout(() => {
console.log(3)
});
foo();</code></pre>
<p>它在栈中的入栈、出栈过程,如下图:<br><img src="/img/bVU9kK?w=1181&h=993" alt="图片描述" title="图片描述"></p>
<h2>任务队列</h2>
<p>Js 中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)。宏任务队列可以有多个,微任务队列只有一个。那么什么任务,会分到哪个队列呢?</p>
<ul>
<li>宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.</li>
<li>微任务:process.nextTick, Promise, Object.observer, MutationObserver.</li>
</ul>
<h3>浏览器的 Event Loop</h3>
<p><strong>浏览器的 Event Loop 遵循的是 HTML5 标准,而 NodeJs 的 Event Loop 遵循的是 libuv。</strong> 区别较大,分开讲。</p>
<p>我们上面讲到,当stack空的时候,就会从任务队列中,取任务来执行。浏览器这边,共分3步:</p>
<ol>
<li>取一个宏任务来执行。执行完毕后,下一步。</li>
<li>取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。</li>
<li>更新UI渲染。</li>
</ol>
<p>Event Loop 会无限循环执行上面3步,这就是Event Loop的主要控制逻辑。其中,第3步(更新UI渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新UI成本大,所以,一般都会比较长的时间间隔,执行一次更新。</p>
<p>从执行步骤来看,我们发现微任务,受到了特殊待遇!我们代码开始执行都是从script(全局任务)开始,所以,一旦我们的全局任务(属于宏任务)执行完,就马上执行完整个微任务队列。看个例子:</p>
<pre><code>console.log('script start');
// 微任务
Promise.resolve().then(() => {
console.log('p 1');
});
// 宏任务
setTimeout(() => {
console.log('setTimeout');
}, 0);
var s = new Date();
while(new Date() - s < 50); // 阻塞50ms
// 微任务
Promise.resolve().then(() => {
console.log('p 2');
});
console.log('script ent');
/*** output ***/
// one macro task
script start
script ent
// all micro tasks
p 1
p 2
// one macro task again
setTimeout
</code></pre>
<p>上面之所以加50ms的阻塞,是因为 <code>setTimeout</code> 的 delayTime 最少是 4ms. 为了避免认为 <code>setTimeout</code> 是因为4ms的延迟而后面才被执行的,我们加了50ms阻塞。</p>
<h3>NodeJs 的 Event Loop</h3>
<p>NodeJs 的运行是这样的:</p>
<ul>
<li>初始化 Event Loop</li>
<li>执行您的主代码。这里同样,遇到异步处理,就会分配给对应的队列。直到主代码执行完毕。</li>
<li>执行主代码中出现的所有微任务:<strong>先执行完所有nextTick(),然后在执行其它所有微任务。</strong>
</li>
<li>开始 Event Loop</li>
</ul>
<p>NodeJs 的 Event Loop 分6个阶段执行:</p>
<pre><code> ┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘</code></pre>
<p>以上的6个阶段,具体处理的任务如下:</p>
<ul>
<li>timers: 这个阶段执行<code>setTimeout()</code>和<code>setInterval()</code>设定的回调。</li>
<li>pending callbacks: 上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行。</li>
<li>idle, prepare: 仅内部使用。</li>
<li>poll: 执行 I/O callback,在适当的条件下会阻塞在这个阶段</li>
<li>check: 执行<code>setImmediate()</code>设定的回调。</li>
<li>close callbacks: 执行比如<code>socket.on('close', ...)</code>的回调。</li>
</ul>
<p><strong>每个阶段执行完毕后,都会执行所有微任务(先 nextTick,后其它),然后再进入下一个阶段。</strong></p>
<h2>Links</h2>
<ul>
<li><a href="https://link.segmentfault.com/?enc=IicO6TIcMuvafPnSgEDuLg%3D%3D.VznTkd%2BfaRHCxlINg1ghrkFiXxscJkAcE%2FXSllBJ3Rwh%2Bm41JscWGQVzJ0CA2GuyurthZnlJM1aWyVFKaSGV8%2Bm2N%2FSd0dmUYRyCiml8DyA%3D" rel="nofollow">Event loops</a></li>
<li><a href="https://link.segmentfault.com/?enc=U0HIueeKgf5aDANfcNfwHg%3D%3D.ADmukVFaScuaZ6r2vZqPYRVw5HxRQDaNnBL6ny3FAkvVEUczBB8a8kMaaimGnBuAzEYnd%2FNifc5YCbs7lNLIykkvtiIxVOn26UOp%2B3XLzaQ%3D" rel="nofollow">NodeJs 的 Event Loop 官方文档</a></li>
<li><a href="https://link.segmentfault.com/?enc=udw7jzu8GsF6Xrdb3bvo1A%3D%3D.PKM%2BUEasIe81qZByqgwJISmtrJh2rUE4H2dz1Mm425wnLewL4W6EJDr3ijr6HSBMZn4tR%2F4aLxJEc%2B%2FkN8%2FWW7MtZAiOxY2n8BMyRkSV3zA%3D" rel="nofollow">并发模型与事件循环</a></li>
<li><a href="https://link.segmentfault.com/?enc=A0gNH9QUxpDUCFU92VpaFw%3D%3D.Xe%2BS7liJUSBieu8ZTqZI5bw5LtbsH75EXzsJlAYtMuI%3D" rel="nofollow">Philip Roberts: Help, I’m stuck in an event-loop.</a></li>
<li><a href="https://link.segmentfault.com/?enc=bN4%2Fycc%2F%2FavFOQ%2FUVkMVJg%3D%3D.GVkUizT1fHkyg7JbYUjnKhGSPwSCPTdG1JEfFn%2BVUzkdhnmA43uyPml%2FzDFT6JP0" rel="nofollow">Promise的队列与setTimeout的队列有何关联?</a></li>
<li><a href="https://segmentfault.com/a/1190000004322358">JavaScript:彻底理解同步、异步和事件循环(Event Loop)</a></li>
<li><a href="https://link.segmentfault.com/?enc=%2FJg%2FXpm221lZLeQoaXlPDg%3D%3D.CXFIqv9OUxYizgnZykKnaMmazmvIsMcWpd%2BOUIOkgWxzSWry1Gmg5VjE9XoCXKIh" rel="nofollow">从event loop规范探究javaScript异步及浏览器更新渲染时机</a></li>
<li><a href="https://link.segmentfault.com/?enc=vCrJE0XJOvOx6tw05zZONw%3D%3D.6W84omZR%2B7f8bNY1Rdqv2JUc0K2B%2B1M66ygWuvDjvV3saaE6Crj2XRmIeo5VIJZtpSIPFox9JVoAhkUOPbhFhA%3D%3D" rel="nofollow">JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志</a></li>
<li><a href="https://link.segmentfault.com/?enc=9Yu6TX4sU6cGALZuC72ZtQ%3D%3D.sDR%2BBXpG3%2FSlw%2FigCeq6V3yoFn0HIUcPJW%2FVk0jjr4ifouc%2B0iMDO84FH8LLfTzzH%2BanPmI0g1SlJkeYraa0UULqhbCK%2BRVIRObzhmBFSug%3D" rel="nofollow">Tasks, microtasks, queues and schedules</a></li>
<li><a href="https://link.segmentfault.com/?enc=o1HyIH61PgEsUIuwmurGuA%3D%3D.0JFxQWkAtL1%2BgxMsnDyeAl9sFMO30IgZ2kX5e6m938WO59qZj2CdV5n9zeGM14qFCKswayFgDPElbagiL%2FBTQwRTX1IlW9eZo71a81ZIlkGZQX6c5cvS0P9Iorv0KEcm" rel="nofollow">WindowOrWorkerGlobalScope.setTimeout()</a></li>
<li><a href="https://link.segmentfault.com/?enc=z9rW7KzoD5aX0OgrsU4Meg%3D%3D.ebPA0lUpkSMF6JLDk%2Ffk1fCK9VMo4ILbDEL9ZD67y4j8dvZlmRPxyFGhvHGjak6YQEUq7I%2F8jeJpgV4N%2FATathCpqjKE62wDV2l6uySZzTIMJ5nBjGDbtj%2FZeXkNUv87n%2Fw%2FS5uPkTB4RuUBI%2FNqS22FV7X5b1nuABlgg%2BQBSpY%3D" rel="nofollow">What is the difference between JavaScript Engine and JavaScript Runtime Environment</a></li>
</ul>
简述JavaScript的垃圾回收机制
https://segmentfault.com/a/1190000011098241
2017-09-10T12:28:54+08:00
2017-09-10T12:28:54+08:00
谷雨
https://segmentfault.com/u/1024dylan
9
<p>不管是高级语言,还是低级语言。内存的管理都是:</p>
<ol>
<li>分配内存</li>
<li>使用内存(读或写)</li>
<li>释放内存</li>
</ol>
<p>前两步,大家都没有太大异议。关键是释放内存这一步,各种语言都有自己的垃圾回收(garbage collection, 简称GC)机制。做GC的第一步是判断堆中存的是数据还是指针,是指针的话,说明它被指向活跃的对象。有3种判断方法:</p>
<ol>
<li>Conservative:如果存储格式是地址,就认为是。C/C++有用到这种算法。</li>
<li>Compiler hints:对于静态语言,比如Java,编译器是知道它是不是指针的,所以可以用这种。</li>
<li>Tagged pointers:JavaScript用的是这种,在字末位进行标识,1为指针。</li>
</ol>
<p>对于JavaScript而言,最初的垃圾回收机制,是基于引用计次来做的。后来升级为标记清除。</p>
<h3>引用计次</h3>
<p>当对象被引用次数为0时,就被回收。潜在的一个问题是:循环引用时,两个对象都至少被引用了一次,将不能自动被回收。所以导致,我们常讲的内存泄露。</p>
<pre><code>// 引用计次
var a = {t: 1}; // 对象 `{t: 1}` (以下简称obj)被引用一次
var b = a; // obj 被引用两次
a = null; // obj 现在为1次
b = null; // obj 现在为0次,可回收
// 循环引用
function fn() {
var a = {};
var b = {};
a.b = b;
b.a = a;
}
fn();</code></pre>
<h3>标记清除</h3>
<p>这是当前主流的GC算法,V8里面就是用这种。当对象,无法从根对象沿着引用遍历到,即不可达(unreachable),进行清除。对于上面的例子,<code>fn()</code> 里面的 <code>a</code> 和 <code>b</code> 在函数执行完毕后,就不能通过外面的上下文进行访问了,所以就可以清除了。</p>
<p>下面,我们简述下V8的GC机制:</p>
<h4>V8的GC机制</h4>
<p>在大部分的应用场景:一个新创建的对象,生命周期通常很短。所以,V8里面,GC处理分为两大类:新生代和老生代。</p>
<p>新生代的堆空间为1M~8M,而且被平分成两份(to-space和from-space),通常一个新创建的对象,内存被分配在新生代。当to-space满的时候,to-space和form-space交换位置(此时,to空,from满),并执行GC.如果一个对象被断定为,未被引用,就清除;有被引用,逃逸次数+1(如果此时逃逸次数为2,就移入老生代,否则移入to-space)。</p>
<p>老生代的堆空间大,GC不适合像新生代那样,用平分成两个space这种空间换时间的方式。老生代的垃圾回收,分两个阶段:标记、清理(有Sweeping和Compacting这两种方式)。</p>
<p>标记,采用3色标记:黑、白、灰。步骤如下:</p>
<ol>
<li>GC开始,所以对象标记为白色。</li>
<li>根对象标记为黑色,并开始遍历其子节点(引用的对象)。</li>
<li>当前被遍历的节点,标记为灰色,被放入一个叫 marking bitmap 的栈。<strong>在栈中,把当前被遍历的节点,标记为黑色,并出栈,同时,把它的子节点(如果有的话)标记为灰色,并压入栈</strong>。(大对象比较特殊,这里不展开)</li>
<li>当所有对象被遍历完后,就只剩下黑和白。通过Sweeping或Compacting的方式,清理掉白色,完成GC。</li>
</ol>
<h4>小补充:JavaScript的根对象</h4>
<p>GC的时候,从根对象开始遍历。在浏览器,根对象是 <code>window</code>;在 Node.js 中,是 <code>global</code>(或称为<code>root</code>).</p>
<p><img src="/img/bVUJjT?w=698&h=266" alt="root.png" title="root.png"></p>
<p>Node.js中,每个文件被当做一个模块,所以,当你用 <code>var/let/const</code> 在文件的全局,声明变量的时候,作用域是当前文件(模块)。因此,图中 <code>root.a</code> 是 <code>undefined</code>.</p>
<h3>Links:</h3>
<ul>
<li><a href="https://link.segmentfault.com/?enc=kXvuzwWdJ%2FJSa%2B1NpSFuzA%3D%3D.Uj9EguQbVmC1e6PTeNG5fq0EiAZKeFn9aHZfb0K6JzHEZzvA%2F1kaUImi0fxeZpmat7%2Bk2pV41ZbjuCaJ1sslo31OadAIPryVhPLfst7hJk4%3D" rel="nofollow">Memory Management</a></li>
<li><a href="https://link.segmentfault.com/?enc=gj4FMZeKLfmY7WvgNau4Gg%3D%3D.Af3f31JoY4xlX5eETtBBx%2Fw6hzRdNWQAYNDKg7OyuoiSUnmYOgbj1RG0i93d%2BQMMS3tO8yvkChT3Xcz95cArehj3afFcC%2FzalgBOQX7cMPo%3D" rel="nofollow">What is the root object in Node.js</a></li>
<li><a href="https://link.segmentfault.com/?enc=tyyW1m7DnsM34YKCISWKtQ%3D%3D.CnikWPpr4S1GOPY4m4aUlqD6gnu4xA03PX7NxI8%2FVJX0WVYbzCcViLhNWkJGIxi3" rel="nofollow">解读 V8 GC Log(一): Node.js 应用背景与 GC 基础知识</a></li>
<li><a href="https://link.segmentfault.com/?enc=YpcigGkkCJhjWNl4U%2BS9Bg%3D%3D.EYW0SK2L7Y89HHs9czF%2B2Bs2qXZAOgEAxhZ6T%2B6ZUVWgELSQSyWOslOfh%2FnSV8Or" rel="nofollow">解读 V8 GC Log(二): 堆内外内存的划分与 GC 算法</a></li>
<li><a href="https://link.segmentfault.com/?enc=DvpJDJ7afsy4wB6sSO4dWQ%3D%3D.sjz4gI80Epqf9JpRt8%2FDPGwmfgLUDiXHWUnHFvwqqDLfn0oKCfV6GiUUxBmDANNxmcAcW8ZACo9iGjpr0sB8Dg%3D%3D" rel="nofollow">A tour of V8: Garbage Collection</a></li>
</ul>