browser-thread

参考:https://juejin.im/post/5a6547d0f265da3e283a1df7#heading-6

进程与线程

理解

进程 (process)

程序的一次执行, 它占有一片独有的内存空间。

进程是 cpu 资源分配的最小单位。系统会给它分配内存,是能拥有资源和独立运行的最小单位

线程 (thread)

是进程内独立执行的单元,线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程

线程是 cpu 调度的最小单位

1547106772414

进程与线程

线程池 (thread pool):保存多个线程对象的容器,实现线程对象的反复调用

多进程:一个应用程序可以同时启动多个进程运行

多线程:单个进程内,同时运行多个线程

看一个形象化的例子

工厂的资源 -> 系统分配的内存(独立的一块内存)

工厂之间的相互独立 -> 进程之间相互独立

多个工人协作完成任务 -> 多个线程在进程中协作完成任务

工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成

工人之间共享空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)

不同进程之间也可以通信,不过代价较大

现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)

优缺点

多线程优点:有效提高 CPU 的利用率

缺点:

单线程优点:顺序编程简单易懂

缺点:效率低

JS 是单线程执行的

如何证明 Js 执行是单线程的?

setTimeout() 的回调函数是在主线程执行的

定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行

alert() 之后,暂停了主线程,不点确定,定时器的回调函数一直不会执行

为什么 Js 要用单线程模式, 而不用多线程模式?

JavaScript 的单线程,与它的用途有关。

作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。

这决定了它只能是单线程,否则会带来很复杂的同步问题: 比如,你在网页上有若干个操作,也就是在主线程中有多个任务,一个线程任务是在某个 DOM 节点上添加内容,另一个线程任务是删除这个节点,这时浏览器应该以哪个线程为准?

即使是支持多线程的 JAVA,在开发安卓应用也只使用一个主线程操作 UI,称作 UI 线程

https://www.cnblogs.com/Jacky-MYD/p/7743532.html

浏览器是多进程的

浏览器是单进程还是多进程?

有的是单进程

有的是多进程

如何查看浏览器是否是多进程运行的呢?

注意:在这里浏览器应该也有自己的优化机制,有时候打开多个 tab 页后,可以在 Chrome 任务管理器中看到,有些进程被合并了 (所以每一个 Tab 标签对应一个进程并不一定是绝对的)

浏览器都包含哪些进程

Browser 进程

浏览器的主进程(负责协调、主控),只有一个

主要的作用是:

第三方插件进程

每种类型的插件对应一个进程,仅当使用该插件时才创建

GPU 进程

最多一个,用于 3D 绘制等

浏览器渲染进程

等价于 浏览器内核、Renderer 进程,内部是多线程

默认每个 Tab 页面一个进程,互不影响。

主要作用为:

浏览器多进程的优势

相比于单进程浏览器,多进程有如下优点:

当然,内存等资源消耗也会更大

浏览器内核(渲染进程)

什么是浏览器内核?

支持浏览器运行的最核心的进程,页面的渲染,JS 的执行,事件的循环,都在这个进程内进行。

不同的浏览器可能不太一样

渲染进程有哪些线程?

GUI 渲染线程

负责渲染浏览器界面,解析HTML,CSS,构建 DOM 树和 RenderObject 树,布局和渲染等。

当界面需要重绘(Repaint)或由于某种操作引发回流(reflow) 时,该线程就会执行

注意,GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。

GUI 线程渲染结果多数情况下是一个位图 (Bitmap),然后交给 Browser 进程显示

JS 引擎线程

也称为 JS 内核,负责处理 Javascript 脚本程序。(例如 V8 引擎)

JS 引擎线程负责解析 Javascript 脚本,运行代码。

JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序

同样注意,GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

可以看到 JS 引擎线程的优先级是更高的,一旦遇到需要执行的脚本,GUI 渲染线程就会被挂起

事件触发线程

归属于浏览器而不是 JS 引擎,用来控制事件循环, 可以理解, JS 引擎自己都忙不过来,需要浏览器另开线程协助

当 JS 引擎执行代码块如 setTimeout 时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件线程

当对应的事件符合触发条件被触发时,该线程会把事件添加到任务队列的队尾,等待 JS 引擎的处理

注意,由于 JS 的单线程关系,所以这些任务队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)

定时触发器线程

传说中的 setIntervalsetTimeout 所在线程

浏览器定时计数器并不是由 JavaScript 引擎计数的,(因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)

因此通过单独线程来计时并触发定时(任务发起后,添加到事件队列中,等待 JS 引擎空闲后执行)

注意,W3C 在 HTML 标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms

异步 Http 请求线程

在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求

将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入任务队列中。再由 JavaScript 引擎执行。

https://juejin.im/post/5a6547d0f265da3e283a1df7#heading-6

Browser 进程 和 Renderer 进程的通信过程

如果自己打开任务管理器,然后打开一个浏览器,就可以看到:任务管理器中出现了两个进程(一个是主控进程,一个则是打开 Tab 页的渲染进程), 然后在这前提下,看下整个的过程:(简化了很多)

浏览器内核中线程之间的关系

GUI 渲染线程与 JS 引擎线程互斥

由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JS 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JS 引擎为互斥的关系,当 JS 引擎执行时 GUI 线程会被挂起, GUI 更新则会被保存在一个队列中等到 JS 引擎线程空闲时立即被执行。

JS 阻塞页面加载

从上述的互斥关系,可以推导出,JS 如果执行时间过长就会阻塞页面。

譬如,假设 JS 引擎正在进行巨量的计算,此时就算 GUI 有更新,也会被保存到队列中,等待 JS 引擎空闲后执行。 然后,由于巨量计算,所以 JS 引擎很可能很久很久后才能空闲,自然会感觉到巨卡无比。

所以,要尽量避免 JS 执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉

浏览器的事件循环(轮询)模型

event loop 可视化 https://www.jsv9000.app/

理解

所有代码分类

js 引擎执行代码的基本流程:初始化代码===>回调代码

模型的重要组成部分:

模型的运转流程

  1. 执行初始化代码, 将事件回调函数交给事件触发线程管理,产生执行栈
  2. 当事件发生时, 事件触发线程将回调函数及其上数据添加到回调列队中
  3. 只有当初始化代码执行完后 (可能要一定时间), 才会遍历读取任务队列中的回调函数执行

模型概念图

1547107412345

1547107426240

一个完整异步过程

  1. JS 引擎线程发一起一个异步请求
  2. 相关线程接收请求并告知主线程已收到通知
  3. 主线程可以继续执行后面的代码,同时工作线程执行异步任务,并管理着回调函数
  4. 事件发生,工作线程完成工作后,通知主线程
  5. 主线程收到通知后,如果已经执行完了初始化的代码,会遍历读取回调队列(消息队列)中的回调函数执行

异步函数

通常具有以下的形式:A(args..., callbackFn);

它可以叫做异步过程的发起函数,或者叫做异步任务注册函数

所以,从主线程的角度看,一个异步过程包括下面两个要素:

  1. 发起函数(或叫注册函数)A;
  2. 回调函数 callbackFn;

它们都是主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。

setTimeout(()=>{}),1000);
//其中setTimeout就是异步过程的发起函数,function是回调函数。
//注:前面说得形式A(args..., callbackFn)只是一种抽象的表示,并不代表回调函数一定要作为发起函数的参数,例如:
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = xxx;     // 添加回调函数
  xhr.open('GET', url);
  xhr.send();   // 发起函数  
//发起函数和回调函数是分离的。

任务队列和事件循环

工作线程将任务放到任务队列, 主线程通过事件循环过程去取任务。

任务队列:任务队列是一个先进先出的队列,它里面存着各种任务。

事件循环:事件循环是指主线程重复从任务队列中取任务,执行的过程

//事件循环代码表示大概是这样的:
while(true){
  var message = queue.get();
  execute(message);
}

任务队列中放的任务具体是什么?

//再次以异步AJAX为例,假设存在如下的代码:
  $.ajax('http://baidu.com',function(res){
    console.log('我是响应',res);
  })
  // 其他代码
  ...
  ...

主线程在发起 AJAX 请求后,会继续执行其他代码,AJAX 线程负责请求 http://baidu.com,拿到响应后,它会把响应封装成一个 JavaScript 对象,然后构造一条消息:

var message = function(){
  callbackFn(response);
}
//其中callbackFn就是就是前面代码中得到成功响应时的回调函数。

主线程在执行完当前循环中的所有代码后,就会到任务队列取出这条任务 (也就是 message 函数),并执行它。

到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。

如果一开始主线程就没有提供回调函数,AJAX 线程在收到 HTTP 响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。

用图表示这个过程就是:

img

从上文中我们也可以得到这样一个明显的结论,就是:异步过程的回调函数,一定不在当前这一轮事件循环中执行。

异步与事件

任务队列中的每个任务实际上都对应着一个事件。

上文中一直没有提到一类很重要的异步过程:DOM 事件。

var button = document.getElement('#btn');
	button.addEventListener('click', function(e) {
	console.log();
});

从事件的角度来看,上述代码表示:

从异步过程的角度看:

事件的概念实际上并不是必须的,事件机制实际上就是异步过程的通知机制。

我觉得它的存在是为了编程接口对开发者更友好。

另一方面,所有的异步过程也都可以用事件来描述。

例如:setTimeout 可以看成对应一个时间到了!的事件。

前文的 setTimeout(fn, 1000); 可以看成:timer.addEventListener('timeout', 1000, fn);

定时触发器线程

上述事件循环机制的核心是:JS 引擎线程和事件触发线程

为什么要单独的定时器线程?因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确,因此很有必要单独开一个线程用来计时。

什么时候会用到定时器线程?当使用 setTimeout 或 setInterval 时,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。

setTimeout(function(){
    console.log('hello!');
}, 1000);

这段代码的作用是当 1000 毫秒计时完毕后(由定时器线程计时),将回调函数推入事件队列中,等待主线程执行

setTimeout(function(){
    console.log('hello!');
}, 0);

console.log('begin');

这段代码的效果是最快的时间内将回调函数推入事件队列中,等待主线程执行

注意:执行结果是:先 beginhello!

虽然代码的本意是 0 毫秒后就推入事件队列,但是 W3C 在 HTML 标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms。 (不过也有一说是不同浏览器有不同的最小时间设定)

就算不等待 4ms,就算假设 0 毫秒就推入事件队列,也会先执行 begin(因为只有可执行栈内空了后才会主动读取事件队列)

定时器和消息队列

Macrotask 与 Microtask

在 ES6 中,Promise 里有了一个新的概念:microtask

进一步,JS 中分为两种任务类型:macrotask 和 microtask,在 ECMAScript 中,microtask 称为 jobs,macrotask 可称为 task

Macrotask

又称之为宏任务,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

每一个 task 会从头到尾将这个任务执行完毕,不会执行其它

浏览器为了能够使得 JS 内部 task 与 DOM 任务能够有序的执行,会在一个 task 执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->...

Microtask

又称为微任务,可以理解是在当前 task 执行结束后立即执行的任务

常见的宏任务和微任务

macrotask:

宏任务的优先级?

microtask:

补充:在 node 环境下,process.nextTick 的优先级高于 Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的 nextTickQueue部分,然后才会执行微任务中的 Promise部分。

再根据线程来理解下:

所以,总结下运行机制:

promise.all 和 单独的 promise一样,只不过多个 primise的逻辑在 all方法内实现,最终都是一个micro-task挂载到队列上。 @@@

小知识:babel中对于微任务的polyfill,如果是拥有 setImmediate 函数平台,则使用之,若没有则自定义则利用各种比如 nodejs中的 process.nextTick,浏览器中支持 postMessage 的,或者是通过 create一个 script来实现微任务 (microtask)。最终的最终,是使用 setTimeout,不过这个就和微任务无关了,promise变成了宏任务的一员。

nextTick

FAQ

#faq/js

参考:谷歌 73以上:https://v8.js.cn/blog/fast-async/

基本概念

执行一个宏任务(栈中没有就从事件队列中获取)

执行过程中如果遇到微任务,就将它添加到微任务的 Job Queues 中

微任务也是一个队列,先加入的先执行

宏任务执行完毕后,执行这个 task宏任务期间发生的所有 job微任务

resolve()是微任务还是算同步代码?算作微任务

微任务产生的微任务如何处理?在同一个微任务队列中处理,直到微任务为空,才执行下一个宏任务

如果产生无限的微任务,就会发生死循环

setTimeout(()=>{
  console.log(1) 
},0)
/* Promise.resolve().then(()=>{
  console.log(2) 
}) */

let a = new Promise((resolve,reject) => {
  resolve()
})
let target = 50
for (let i = 2; i < target; i++) {
  a = a.then(() => {
    console.log(i)
  })
}
console.log(3)  // 直到50全部输出完才打印1

当前宏任务执行完毕,开始检查渲染,然后 GUI线程接管渲染

渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

promise 的 excutor 是同步函数,resolve 不会阻塞同步代码的执行

注意的是,立即 resolve 的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。

setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three

上面代码中,setTimeout(fn, 0) 在下一轮“事件循环”开始时执行,Promise.resolve() 在本轮“事件循环”结束时执行,console.log('one') 则是立即执行,因此最先输出。

等级一

setTimeout(()=>{
   console.log(1) 
},0)
Promise.resolve().then(()=>{
   console.log(2) 
})
console.log(3) 
3 2 1

这个是属于 Eventloop的问题。main script运行结束后,会有微任务队列和宏任务队列。微任务先执行,之后是宏任务。

等级二

setTimeout(()=>{
   console.log(1) 
},0)
let a=new Promise((resolve)=>{
    console.log(2)
    resolve()
}).then(()=>{
   console.log(3) 
}).then(()=>{
   console.log(4) 
})
console.log(4) 
2 4 3 4 1

这个要从 Promise的实现来说:

等级三

new Promise((resolve,reject)=>{
  console.log("promise1")
  resolve()
}).then(()=>{
  console.log("then11")
  new Promise((resolve,reject)=>{
    console.log("promise2")
    resolve()
  }).then(()=>{
    console.log("then21")
  }).then(()=>{
    console.log("then23")
  })
}).then(()=>{
  console.log("then12")
})
'promise1'
'then11'
'promise2'
'then21'
'then12'
'then23'

第一轮

第二轮

第三轮

第四轮

关键是微任务被加入队列的顺序

等级三变种

Promise中 then返回一个 Promise呢??

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    return new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})
'promise1'
'then11'
'promise2'
'then21'
'then23'
'then12'

这里 Promise的第二个 then相当于是挂在新 Promise的最后一个 then的返回值上。

等级三变种 2

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})
new Promise((resolve,reject)=>{
    console.log("promise3")
    resolve()
}).then(()=>{
    console.log("then31")
})
'promise1'
'promise3'
'then11'
'promise2'
'then31'
'then21'
'then12'
'then23'

第一轮

第二轮

第三轮

第四轮

最终输出:[promise1,promise3,then11,promise2,then31,then21,then12,then23]

等级四

在 async/await之下,对 Eventloop的影响。

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}

async  function async2() {
    console.log( 'async2');
}

console.log("script start");

setTimeout(function () {
    console.log("settimeout");
},0);

async1();

new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
console.log('script end'); 
'script start'
'async1 start'
'async2'
'promise1'
'script end'
'async1 end'
'promise2'
'settimeout'

async/await仅仅影响的是函数内的执行,而不会影响到函数体外的执行顺序。也就是说 async1()并不会阻塞后续程序的执行,await async2() 相当于一个 Promise,console.log("async1 end"); 相当于前方 Promise的 then之后执行的函数。

按照上章节的解法,最终输出结果:[script start,async1 start,async2,promise1,script end,async1 end,promise2,settimeout]

此处唯一有争议的就是 async的 then和 promise的 then的优先级的问题,请看下方详解。*

async/await与 Promise的优先级详解

全家福

  const ch = new MessageChannel()
  const port1 = ch.port1
  const port2 = ch.port2
  
  port1.onmessage = function() {
      console.log(8)
  }
  
  
  var callback = function (){
    console.log(6)
  };
  
  const mo = new MutationObserver(callback)
  const option = {
    characterData:true
  }
  mo.observe(test.childNodes[0],option)
  
  setTimeout(() => {
    console.log(1)
  }, 0)
  
  new Promise((resolve) => {
    console.log(2)
    for(let i = 0; i < 100000; i++) {
        (i === 99999) && resolve()
    }
    console.log(3)
  }).then(() => {
    console.log(4)
  })
  
  console.log(5)
  
  port2.postMessage(8)
  
  test.childNodes[0].data = 456
  
  console.log(7)
  
  // 发送消息
  
  
  
  // 2
  // 3
  // 5
  // 7
  // 4
  // 6
  // 8
  // 1

为什么 JS 要是异步的,能解决什么问题

因为 JS 是单线程的, 说说 UI 单线程

所以要异步处理一些耗时的操作