前端——正儿八经的浏览器渲染细节向(二)

写在开头:本篇文章主要涉及前端浏览器的基础渲染问题,包括但不仅限于JS执行机制、Rendering Tree、部分操作系统基础概念、上一篇文章中的进程与线程栈内存与堆内存。ps:所有内容均个人总结,只能借鉴,不能保证百分之百正确。——一个八个月大的前端

浏览器中的进程

浏览器主要包括四大进程:

  • 主进程(Browser进程)浏览器只有一个主进程,负责一些浏览器最为基础的工作。例如:负责页面的显示、用户的交互工作、网络资源的下载与管理、各个页面的维护、创建和销毁其他进程。
  • GPU进程,最多一个,负责3D图示绘制
  • 第三方插件进程,负责第三方插件处理。一种插件对应他所相应的进程,即使用即创建
  • 渲染进程(浏览器内核或者也可以说是Renderer进程)主要包含了,GUI渲染线程、Js引擎线程、事件循环线程、定时器线程、http异步线程等。

由此,我们至少可以看出,浏览器是多进程协同工作的。而多进程工作至少保证了,在浏览器其中一个窗口或者第三方插件、或者渲染引擎在出现问题的时候,对于整个浏览器不至于造成太大的麻烦。即提高了用户体验。

渲染进程

本篇文章主要讨论,与我们编程还有最为紧密联系的渲染进程。相信大部分前端代码工程师,都能够随口说出浏览器从输入URL地址到页面呈现在用户面前的所需过程。

  1. DNS 查询
  2. TCP 连接
  3. HTTP 请求即响应
  4. 服务器响应
  5. 客户端渲染

前面四个步骤暂且不去多说,后面将会放到网络协议一篇单独讲,目前我们只需要关注客户端渲染的过程,即页面从下载整合网络资源到以可视化的方式呈现在用户面前的过程。

GUI渲染进程

首先我们应该知道,我们所能够看到的,呈现在我们面前的页面元素,都是由服务端所提供的。所以在浏览器开始干活之前,浏览器需要先通过网络协议从服务端下载,需要呈现给用户看到的HTML文件,再通过HTML文件解析,依次下载CSS文件及JS文件。

在CSS文件及HTML文件下载完成之后,就要涉及到代码解析的问题了,对于底层语言C/C++来说,他们通过编译器转换成机器码,完成最底层的人机交互。HTML与CSS也是需要转换成浏览器可以识别的命令的,而在浏览器完成命令之后,他也需要把“结果”反馈回来,GUI渲染就是专门干这件事情的。

  1. 浏览器中的呈现引擎会去解析HTML文件,将他生成为浏览器可以识别的节点,众多节点组合在一起即DOM Tree(DOM树)
  2. 浏览器中的呈现引擎同时会去解析CSS文件以及样式元素中的样式数据,并将这些带有视觉指令的样式信息构建成另一个树状结构(Style Rules)
  3. 将DOM Tree与Style Rules相结合,随后便可以生成Rendering Tree(渲染树),此时就该GUI渲染引擎就要正式出手了,根据生成的Rendering Tree呈现给用户他现在所看到的页面。
  4. 当然页面的样式不可能是一成不变的,适当的样式变化更有利于人机交互,即用户体验。所以这里就会涉及到另外一个问题:回流与重绘。这里也不多细说,可以放在后面CSS篇再来探讨。
  5. 最后说一点:GUI渲染进程与JS渲染进程是互斥的,即其中一个进程在执行时,必将阻塞另一个进程。其实这也相当好理解,毕竟JS也是可以拥有视觉指令及操作DOM指令的,如果JS渲染进程与GUI渲染进程同时,那这时就相当于出现了两个指挥官,也就会产生一定的冲突。

JS引擎线程

js引擎线程就是js内核,负责解析与执行js代码,也称为主线程。浏览器同时只能有一个JS引擎线程在运行JS程序,所以js是单线程运行的。(第一篇文章有讲)

既然是负责解析和执行js代码的,这时我们就可以来说道说道,好像实际开发当中什么用处,但是频频出现在面试题当中的js预编译问题了。

还回到最初的问题。js是单线程运行的,对于多线程来说,js的运行效率自然会低了一些。那么怎么才能在单线程的特性上,提高运行效率呢?Service Worker便是为解决这一痛点提出的一种不能访问DOM的API,运行在其他线程之上,当然现在也还很不成熟。

而我们要讲的则是已经沿用了很久,让我们经历了回调地狱、Promise、及Promise改进版async/await的同步与异步概念。

js的运行机制将需要执行的任务分为同步和异步两个部分,对于属于同步的任务,毫无疑问的直接会被丢到主线程当中排队执行即可,而对于处于异步的任务,并不会直接进入主线程,他们会先去一个叫做任务队列(task queue)的地方,在那里乖乖地等着,只有处于任务队列中的异步任务有着落了,才会通知主线程,告诉主线程自己已经ok了,这才能够跟在主线程的最后面排队执行。

当然还有第二种理解方式,主线程在执行完全部的同步方法之后,他会去任务队列中看上一看,看看那些异步任务已经结束等待状态了,再把他们带到主线程当中,排队执行。

任务队列中会不停地有异步任务通知主线程,自己ok了,让我去排队吧,而主线程中也不停排在前面的任务事件执行完毕,任务队列中也不停地有新的“成员”(异步任务)加入,这也就形成了一个事件循环。

而第二种理解的解释,也是类似的,主线程不断地清空所有需要执行的任务事件,不断地轮询任务队列,不断地有任务队列中准备就绪的异步任务进入主线程,不断地有新的异步任务进入任务队列。事件循环有就形成了。

那么好,我们现在通过代码来更深刻的理解,并通过代码来依次解读一下。

console.log(1);//  (bbb)
setTimeout(() => {
  console.log(2);
}, 100); //   (ccc)
setTimeout(() => {
  console.log(4);
}, 0); //(ddd)
setTimeout(() => {
  console.log(5);
}, 0); // (eee) 
function aaa() {
  console.log(6);
  new Promise(function(resolve, reject) {
    console.log(7);
    resolve();
  }).then(() => {
    console.log(8);
  }); // (fff) 
}
aaa();
console.log(9); (ggg)
复制代码

上面的代码输出结果,涉及到了三个要点,只要明白了这三点,也就能够很好的理解了。

  1. setTimeout类异步方法WEB API接口
  2. Promise对象
  3. 同步与异步,任务队列与事件循环

对于第三点在上面的讲解中应该已经有了一个大概的概念了,现在我们将三个要点分别带入到解析代码当中,一边解析代码一边解读三个要点。

首先是同步与异步,任务队列与事件循环:

  • 同步任务有aaa、bbb、ggg
  • 异步方法有ccc、ddd、eee
  • 这里还有个特殊的异步方法:fff

现在我们已经把同步与异步区分开来了,那么就让我们把他们分别放到主线程及任务队列当中,并开始依次去运行程序吧。

首先根据代码的出现顺序,主线程中bbb、aaa、(fff)、ggg依次排好队,任务队列当中在结合了定时器的延迟时间后,ddd、eee、ccc也依次排好队。

此时就可以开始我们的事件执行了,bbb、aaa,分别打印出1、6,随后便遇到了fff(Promise对象),此时再对Promise对象进行较为展开的说明了。Promise对象:将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。

当然他不是单纯地将异步转同步,还对js的执行顺序造成了较大的改变,即引入了宏任务(macrotask)与微任务(microtask)的概念。

宏任务与微任务

  • 宏任务:可以简单的理解为,暂未从任务队列中引入异步方法,仅执行主线程(执行栈)事件循环中的一次循环
  • 微任务:可以简单的理解为Promise实例生成后,所调用的then方法里面的任务。注:在检查到微任务时,微任务将会在主线程的

了解了宏任务和微任务后我们再回头看fff(Promise)对象,在遇到fff时,先将匿名函数Promise对象中的7打印出来,随后将then方法中的微任务直接丢到主线程(事件队列)的最后排队去了。随后遇到ggg打印9,此时再次遇到刚刚才被丢到最后then方法里面的函数,并执行里面的方法,将8打印出来。

此时便完成了一次宏任务,打印结果分别是:1、6、7、9、8;

完成了一次宏任务之后,任务队列也就可以往主线程中添加已经结束了等待状态的异步任务了,ddd、eee、ccc将依次进入,并依次将结果打印出来,分别为4、5、2;于是上面的代码打印结果连接起来,依次是:1、6、7、9、8、4、5、2;

当然了,程序员们所定义的延迟时间并不是真正的,用户所要等待的时间,因为这个延迟时间,只是该方法延迟进入任务队列当中的时间,我们还需要考虑此时主线程中的执行情况。且官方也规定了最小延迟时间:4ms。当然这些只需了解即可。

是不是感觉自己已经基本掌握了js的执行机制了?那么我们再稍微作一丢丢的修改,将async/await也加入进来,暂时先不去探讨async/await与Promise的区别,本篇只需要知道async/await只是Promise对象与Generator 函数相结合的语法糖即可,我们只用讨论加入async/await后,JS的执行机制又会如何?

console.log(1);
setTimeout(() => {
  console.log("ccc");
}, 0);
new Promise(function(resolve, reject) {
  console.log(2);
  resolve();
}).then(() => {
  console.log(3);
});
async function aaa() {
  console.log(4);
  let res = await bbb();
  console.log(res);
  await console.log(8);
  console.log(9);
}
aaa();
async function bbb() {
  console.log(5);
  await console.log(6);
  console.log(7);
  await console.log(10);
  return 11;
}
new Promise(function(resolve, reject) {
  console.log(12);
  resolve();
})
  .then(() => {
    console.log(13);
  })
  .then(() => {
    console.log(14);
  })
  .then(() => {
    console.log("aaa");
  })
  .then(() => {
    console.log("bbb");
  });
console.log(15);
复制代码

既然加入了async/await,那么就不得对其的用法简单地说明一下,至少我们需要明白两点:

  1. async/await是同时出现的,缺一不可。await下面的执行代码需要等到被await“标记”的代码拥有执行结果之后再执行。但是也并不是在死等,因为在遇到await“标记”的时候,函数会让出线程重新在队列最后排队。
  2. async/await虽然用法上看上去只是与Generator 函数相似,但是其执行之后的返回值确确实实是一个Promise对象。

好了,在明白了这两点之后,我们再回头来看这个问题,首先继续按照同步与异步,先将执行代码进行区分。

  • 由于大部分代码解析已经有了较清晰的说明,我们现在只着重说明async/await部分,在主线程当中根据事件队列,我们依次打印了1、2、4、5、6、12、15。
  • 这时我们可以看做他完成了第一次循环,随即我们开始第二次循环,在分别依次打印3、7、10、13。
  • 现在我们进行第三次循环,分别是bbb函数返回值11,打印14
  • 我们继续进行第四次循环,此时bbb函数已经执行完成,返回值虽然是11,但是这也是一个Promise对象,且没有then方法,于是跳过了打印,随后打印aaa
  • 开始进行第五次循环,aaa函数终于可以继续执行 打印11、8、bbb
  • 开始第六次循环,打印aaa函数中的9
  • 经过千辛万苦,主线程中的事件终于“执行完成”,任务队列中干巴巴等着的定时器异步方法终于进入了主线程当中,打印ccc
  • 最终结果依次为:1、2、4、5、6、12、15、3、7、10、13、14、aaa、11、8、bbb、9

好了,经过千辛万苦js的执行机制相信已经很清晰了。但是~有一点必须进行说明,其实V8引擎总是随着版本改动的,甚至有几个版本下的node.js与谷歌浏览器的V8引擎版本都是不一样的,这样有时会导致async/await与Promise对象的执行顺序有所不同。

所以除了面试题,js的执行机制问题虽然有用但肯定不值得花费太多太多的时间。后面的几个渲染进程中的线程,都是一些概念性问题,了解即可。毕竟他们之间的关系逻辑,通过两个代码?应该已经较为清晰了

事件循环线程

事件循环线程用来管理控制事件循环,并且管理着一个事件队列(task queue),当js执行碰到事件绑定和一些异步操作时,会把对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待js引擎线程空闲时来处理。

定时器线程

由于js是单线程运行,所以不能抽出时间来计时,只能另开辟一个线程来处理定时器任务,等计时完成,把定时器要执行的操作添加到事件任务队列尾,等待js引擎线程来处理。这个线程就是定时器线程。

异步请求线程

当执行到一个http异步请求时,便把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),把回调函数添加到事件队列,等待js引擎线程来执行。

最后说两句:对于文章里面的内容,如果说学术性和权威性有多强,肯定是没有的。但是其实际理解应该是没有太大偏差的。我个人认为作为前端工程师与后端及运维肯定还是有着一定的“地位”差距的,但是这不代表前端不如后端及运维。一来,前端技术一直一来并没有很统一且不够成熟。二来,我们前端工程师的门槛比较低。但是随着前端数据库的出现,数据可视化的实用价值展现。做一个优秀的前端工程师,大家共勉。

转载于:https://juejin.im/post/5c7de0885188251b8354fd9e

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值