参考文档: Timer

1. js为什么是单线程的?

javascript是单线程语言,单线程就是所执行的代码必须按照顺序,同一时间只能做一件事。
作为浏览器脚本语言,JS的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JS脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JS单线程的本质。


2. 浏览器的线程

浏览器是多线程的,它们在内核控制下相互配合以保持同步,一个浏览器至少实现三个常驻线程: JavaScript引擎线程、GUI渲染线程、浏览器事件触发线程。JS引擎是单线程的,但是浏览器却可以是多线程的,定时器计时、网络请求、浏览器渲染等等都是由不同的线程去完成的

(1) javascript引擎是基于事件驱动单线程执行的(可以修改DOM,简单化处理了),必须符合ECMAScript规范。JS引擎一直等待着event loop中任务的到来,然后加以处理(只有当前函数执行栈执行完毕,才会去任务队列中取任务执行)。浏览器无论什么时候都只有一个JS线程在运行JS程序。

(2) UI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但是 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,JS对页面的操作即GUI的更新也会被保存在一个队列中,等到JS引擎空闲时才有机会被执行。这就是JS阻塞页面加载

(3) 事件触发线程,当一个事件被触发时该线程会把事件添加到任务队列的队尾,等待JS引擎的处理。这些事件可以来自JavaScript引擎当前执行的代码块调用setTimeout/ajax添加一个任务,也可以来自浏览器其他线程如鼠标点击添加的任务。但由于JS的单线程关系,所有这些事件都得排队等待JS引擎处理


注:

浏览器在1s中渲染页面60次,每16ms就会往Render queue中添加一个UI render任务。但是浏览器只有在stack为空时才有机会执行该任务。通过setTimeout(cb,0)将任务分割,就是增加UI render 任务被执行的机会,改善用户体验。


可以看下图加深理解:

javascript如何处理并发 javascript 并发_js实现并行


3.Event loop
Dom树是非线程安全的,因为这个限制,js代码应该单线程执行,然而同时必须确保js引擎处理所有的事件,以及不遗漏任何函数调用。因此我们谈及Event loop.

Event loop可以用下面图片简单描述:

javascript如何处理并发 javascript 并发_单线程_02

  • Call stack,是一个调用栈,包含所有即将调用的函数
  • Event handlers queue, 是一个将要执行的事件处理程序队列,根据优先级使用不同算法排列
  • Event loop, 检查事件处理程序队列是否为空,如果不空,则调用顶部然后移除它。
  • JavaScript Web APIs, 这些都是浏览器提供的API,例如AJAX请求等等,实际上是浏览器的线程,而不是JS的。
    整个过程类似下面代码:
    当call stack为空,浏览器检查是否该渲染当前页面,然后才会去检查Event handler queue.
while(true){
    if(renderEngine.isItTimeToRender(){
        renderEngine.render();
    }
 
    if(eventHandlersQueue.isNotEmpty()){
        eventHandlersQueue.processTopEventHandler();
    }
}

4. 如何在JS中实现并发?

有两种方法,一种是基于setTimeout()方法,一种是使用WebWorkers.

setTimeout():用于在指定的毫秒数后调用函数或计算表达式
实际上是使用的是浏览器的定时器线程,在指定时间内,将任务放入事件队列,等待js引擎空闲后执行。
它的方法是将执行分解成简单任务,使得浏览器渲染线程可以有机会执行。
eg:
下面是一种无限逼近PI值的方法:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="results">results</div>
    <script>
    var results = document.getElementById("results");
    function appendResult(result) {
        var div = document.createElement('div');
        div.innerText = result;
        results.appendChild(div);
    }

    // function calculatePi(numberOfIterations) {
    //     var pi = 0,
    //     n = 1;
    //     for (var i = 1; i < numberOfIterations; i++) {
    //         console.log("我被调用了");
    //         appendResult(pi);
    //         pi = pi + (4 / n) - (4 / (n + 2));
    //         n += 4
    //     }
    
    //     return pi;
    // };
 
    function calculatePi(pi, n) {;
        for (; ; n += 4) {
            pi = pi + (4 / n) - (4 / (n + 2));
            if ((n - 1) % 20000000 == 0) {
                console.log("我被调用了");
                appendResult(pi);
                setTimeout(function () {
                    calculatePi(pi, n + 4);
                });
                return;
            }
        }
    }
 
calculatePi(0, 1);
</script>
</body>
</html>

可以看到未注释部分执行后,浏览器及时显示接近PI的值,浏览器定时触发器不断将新的任务加入到js引擎线程队列中,js引擎执行完成后,浏览器GUI渲染线程开始渲染,显示PI值,之后不断重复。
当使用注释部分代码时,因为js引擎线程一直不为空,GUI渲染线程不能执行(他们是互斥的),所以页面一直是空白的


Web Worker
参考文档:

至 2008 年 W3C 制定出第一个 HTML5 草案开始,HTML5 承载了越来越多崭新的特性和功能。它不但强化了 Web 系统或网页的表现性能,而且还增加了对本地数据库等 Web 应用功能的支持。其中,最重要的一个便是对多线程的支持。在 HTML5 中提出了工作线程(Web Worker)的概念,并且规范出 Web Worker 的三大主要特征:能够长时间运行(响应),理想的启动性能以及理想的内存消耗。Web Worker 允许开发人员编写能够长时间运行而不被用户所中断的后台程序,去执行事务或者逻辑,并同时保证页面对用户的及时响应。

在 HTML5 中,工作线程的出现使得在 Web 页面中进行多线程编程成为可能。众所周知,传统页面中(HTML5 之前)的 JavaScript 的运行都是以单线程的方式工作的,虽然有多种方式实现了对多线程的模拟(例如:JavaScript 中的 setinterval 方法,setTimeout 方法等),但是在本质上程序的运行仍然是由 JavaScript 引擎以单线程调度的方式进行的。在 HTML5 中引入的工作线程使得浏览器端的 JavaScript 引擎可以并发地执行 JavaScript 代码,从而实现了对浏览器端多线程编程的良好支持。

注: 不能对DOM进行操作,workers线程主要用途是繁重的后台运算。

eg:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <input type="button" value="开始" id = "startButton">
    <div id="output">test</div>
    <script>
var output = document.getElementById("output");
var startButton = document.getElementById("startButton");
// Some magic to run function as WebWorker
var workerBlob = new Blob(["(" + worker.toString() + ")()"], { type: 'text/javascript' }),
    workerInstance;
 
startButton.onclick = function () {
    if (workerInstance) {
        workerInstance.terminate(); //如果存在工作线程实例,先中断
    }
 
    output.innerHTML = ""; 
    //参数是worker将执行的脚本的URL,它必须遵守同源策略
    workerInstance = new Worker(URL.createObjectURL(workerBlob));
    // 从worker接受消息
    workerInstance.onmessage = function (e) {
        output.innerHTML += "<div>" + e.data + '</div>';
    };
    //发送消息来start the worker
    workerInstance.postMessage(null);
};
 
function worker() {
    onmessage = function (e) {
        var pi = 0,
              n = 1;
        for (var n = 1; ; n += 4) {
            pi = pi + (4 / n) - (4 / (n + 2));
 
            if ((n - 1) % 200000000 == 0) {
                postMessage(pi);
 
            }
        }
    };
}
    </script>
</body>
</html>

5. 总结

Answering the question, JavaScript is single-threaded in the same context, but browser Web APIs are not. Also we have possibility of simulating parallelism by using setTimeout function or, with some limitations, by the use of the real parallelism provided by WebWorkers.(可以看到JS是在相同的上下文中是单线程的,但是浏览器的Web APIS 不是。同样我们可以使用setTimeout()模拟并行,或者可以使用有限制的WebWorkers真的并行)