JavaScript 的 同步,异步,Event-Loop,线程

3,124 阅读6分钟

JavaScript的单线程

JavaScript是一个单线程语言,浏览器只会分配一个线程给js用于执行同步代码。但是由于以下原因所以将JavaScript设计为单线程。

假设两个JS线程同时操作DOM或者争夺唯一性资源,那么浏览器需要引入锁的概念来决定这两个JS的操作有效性,所以为了避免引入锁带来的复杂性,JS就被设计为单线程。

另外JS当初是以i/o为目标而设计的语言,所以单线程也是为了避免资源的浪费。所以在现代浏览器中,html5标准引入了Worker语法用于多线程执行(Worker无法访问DOM)。

同步

console.log('同步一');
console.log('同步二');
// 结果
同步一
同步二

同步就是一步一步执行代码,每一行代码执行完成之后才执行下一步。

异步

console.log('log1');
setTimeout(()=>console.log('log2'),0);
console.log('log3');
// 结果
log1
log3
log2

为什么要异步?因为在执行一些耗时任务的时候(如:ajax,事件监听,定时器等)如果还是采用同步代码,将会造成浏览器假死现象。所以浏览器会为异步事件开辟单独的线程来执行。

浏览器会将异步任务加入任务队列(Task Queue)中,浏览器执行完同步任务之后会进入任务队列执行已经完成的任务。并且在任务队列中进行Event-Loop

Alt text

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

为什么要异步?

如果在执行某个非常耗时的任务时候,同步执行的逻辑会锁死后面所有的代码,而有了异步,那么浏览器将异步代码抽离出同步执行线程,放入异步中执行,首先避免的浏览器假死,第二个就是利用了异步线程加快了整体代码执行速度。

Alt text

(图片来自Soham Kaman)

异步的流程

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

Event-Loop

Event Loop(事件循环)

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

macrotasks与microtasks的区别

macrotasks: setTimeout setInterval setImmediate I/O UI渲染

microtasks: Promise process.nextTick Object.observe MutationObserver

主线程在执行完成同步的代码之后,会去读取任务队列的事件,而Event-loop中如果产生了macrotask那么将会将这个macrotask放入下一次的执行中,而microtask则直接在此次循环中完成,而不论你是否加入了多个microtask。

简单的说法就是:macrotask排入下一次执行,microtask此次执行,新的microtask排入末尾执行。

任务队列(消息队列)

"任务队列"是一个事件的队列(也可以理解成消息的队列),工作线程完成一项任务,就在"任务队列"中添加一个事件(也可以理解为发送一条消息),表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

浏览器拥有哪些线程

  1. JavaScript引擎线程
  2. 界面(GUI)渲染线程
  3. 浏览器事件触发线程
  4. Http请求线程
  5. 定时触发线程

JavaScript引擎线程

Javascript引擎,也可以称为JS内核,主要负责处理Javascript脚本程序,例如V8引擎。Javascript引擎线程理所当然是负责解析Javascript脚本,运行代码。

界面渲染线程

GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被”冻结”了。

GUI 渲染线程 与 JavaScript引擎线程互斥!

由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JavaScript引擎为互斥的关系,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。

定时触发器线程

浏览器定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。

事件触发线程

当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

异步http请求线程

在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。

备忘知识

HTML5标准中设置setTimeout最小值是4ms,但是通过测试发现,最小值在webkit中是1ms。 以下是测试代码

setTimeout(()=>console.log('log1'),1);
setTimeout(()=>console.log('log2'),0);

我们想输出的是 log2,log1。但是实际上是log1,log2.这是由于webkit中setTimeout的最小值是1ms造成的。 这个设想乃一家之言,如有更好的见解,请告诉我。tks git.webkit.org/?p=WebKit.g… 源码中设置的是4_ms,现在我不清楚是何种原因造成的。 它的代码中也有设置对比,取最小值。

static Seconds defaultMinimumInterval() { return 4_ms; }

Chrome有两种时间段,一种是4ms,一种是1ms chromium.googlesource.com/chromium/sr…

static const int kMaxIntervalForUserGestureForwarding =
    1000;  // One second matches Gecko.
static const int kMaxTimerNestingLevel = 5;
static const double kOneMillisecond = 0.001;
// Chromium uses a minimum timer interval of 4ms. We'd like to go
// lower; however, there are poorly coded websites out there which do
// create CPU-spinning loops.  Using 4ms prevents the CPU from
// spinning too busily and provides a balance between CPU spinning and
// the smallest possible interval timer.
static const double kMinimumInterval = 0.004;
  double interval_milliseconds =
      std::max(kOneMillisecond, interval * kOneMillisecond);
  if (interval_milliseconds < kMinimumInterval &&
      nesting_level_ >= kMaxTimerNestingLevel)
    interval_milliseconds = kMinimumInterval;
  if (single_shot)
    StartOneShot(interval_milliseconds, BLINK_FROM_HERE);
  else
    StartRepeating(interval_milliseconds, BLINK_FROM_HERE);