image.png
image.png

浏览器及其相关进程

image.png

  1. 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  2. 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  3. GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了GPU 进程。
  4. 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  5. 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

image.png
提问:即使是如今的多进程架构,我偶尔还会碰到一些由于单个页面卡死最终崩溃导致所有页面崩溃的情况,请问这是什么原因呢?
是这样的,通常情况下是一个页面使用一个进程,但是,有一种情况,叫“同一站点(same-site)”,具体地讲,我们将“同一站点”定义为根域名(例如,geekbang.org)加上协议(例如,https:// 或者http://),还包含了该根域名下的所有子域名和不同的端口,比如下面这三个:https://time.geekbang.org https://www.geekbang.org https://www.geekbang.org:8080
都是属于同一站点,因为它们的协议都是https,而根域名也都是geekbang.org。
你也许了解同源策略,但是同一站点和同源策略还是存在一些不同地方,在这里你需要了解它们不是同一件事就行了。
Chrome的默认策略是,每个标签对应一个渲染进程。但是如果从一个页面打开了新页面,而新页面和当前页面属于同一站点时,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫process-per-site-instance。
直白的讲,就是如果几个页面符合同一站点,那么他们将被分配到一个渲染进程里面去。所以,这种情况下,一个页面崩溃了,会导致同一站点的页面同时崩溃,因为他们使用了同一个渲染进程。
为什么要让他们跑在一个进程里面呢?因为在一个渲染进程里面,他们就会共享JS的执行环境,也就是说A页面可以直接在B页面中执行脚本。因为是同一家的站点,所以是有这个需求的。

感觉挺好奇的,单进程浏览器开多个页面,渲染线程也只有一个吗?感觉一个页面开一个线程不是更合理吗?
作者回复: 之前回答的有点笼统,下面是我整理过后的回答:
首先这个问题提的很好,我们从IE6开始讲起,IE6时代,浏览器是单进程的,所有页面也都是运行在一个主线程中的,当时IE6就是这样设计,而且此时的IE6是单标签,也就是说一个页面一个窗口。这时候,国内有很多国产浏览器,都是基于IE6来二次开发的,而IE6原生架构就是所有页面跑在单线程里面的,意味着,所有的页面都共享着同一套JavaScript运行环境,同样,对于存储Cookie也都是在一个线程里面操作的。而且这些国产浏览器由于需要,都采用多标签的形式,所以其中的一个标签页面的卡顿都会影响到整个浏览器。基于卡顿的原因,国内浏览器就开始尝试支持页面多线程,也就是让部分页面运行在单独的线程之中,运行在单独的线程之中,意味着每个线程拥有单独的JavaScript执行环境,和Cookie环境,这时候问题就来了:比如A站点页面登陆一个网站,保存了一些Cookie数据到磁盘上,再在当前线程环境中保存部分Session数据,由于Session是不需要保存到硬盘上的,所以Session只会保存在当前的线程环境中。这时候再打开另外一个A站点的页面,假设这个页面在另外一个线程中里面,那么它首先读取硬盘上的Cookie信息,但是,由于Session信息是保存在另外一个线程里面的,无法直接读取,这样就要实现一个Session同步的问题,由于IE并没有源代码,所以实现起来非常空难,国内浏览器花了好长一点时间才解决这个问题的。Session问题解决了,但是假死的问题依然有,因为进程内使用了一个窗口,这个窗口是依附到浏览器主窗口之上的,所以他们公用一套消息循环机制,消息循环我们后面会详细地讲,这也就意味这一个窗口如果卡死了。也会导致整个浏览器的卡死。国产浏览器又出了一招,就是把页面做成一个单独的弹窗,如果这个页面卡死了,就把这个弹窗给隐藏掉。这里还要提一下为什么Chrome中的一个页面假死不会影响到主窗口呢?这是因为chrome输出的实际上图片,然后浏览器端把图片贴到自己的窗口上去,在Chrome的渲染进程内,并没有一个渲染窗口,输出的只是图片,如果卡住了,顶多图片不更新了。国产浏览器这一套技术花了四五年时间,等这套技术差不多成熟时,Chrome发布了 :(

渲染流水线

image.png
image.png
image.png

解析dom与阻塞问题

image.png

JS执行上下文

image.png
image.png

词法作用域

作用域链是由词法作用域决定的。词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。

JS闭包原因

image.png
image.png
产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中的closure对象中。

JS变量提升

image.png
下面是关于同名变量和函数的两点处理原则:
1. 如果是同名的函数,JavaScript编译阶段会选择最后声明的那个
2. 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略
image.png

在块级作用域(ES6+)中声明变量

image.png
所以具体结果受ES版本和严格模式以及浏览器具体实现影响,会有不同的结果。
E6之前的版本都没有块级作用域的概念,只有全局作用域和函数作用域。!!
image.png
如果启用了严格模式(开启后,3与5版本冲突则按5算)则上述var处应该为let,即函数声明被当做let xxx=func
但无论怎样,普通变量依旧按照var声明(不使用真正的let前提下哈)

若没启用严格模式(冲突了按3.0算),则上述var处,就应是var。

ES规定函数绝对不能在块级作用域中声明(理论上,实践上不是这样),但是变量却是可以呀!

  1. 普通块,即直接一对 大括号{}。
  2. ifelse这种判断性块:

凎!最初if-else中的变量的声明都有。因为不存在块级作用域,也不是严格模式。

  1. // "use strict";
  2. foo();
  3. function foo() {
  4. console.log(c, g, w, n);
  5. if (true) {
  6. var c = 2;
  7. console.log(111, c, g, n);
  8. var g = function () {
  9. return true;
  10. }
  11. function n() {
  12. return 'n';
  13. }
  14. } else {
  15. var w = 'w';
  16. return false
  17. }
  18. console.log(c, g ,n, w);
  19. };
  20. //非严格模式下,结果如下
  21. undefined undefined undefined undefined
  22. index.html:20 111 2 undefined ƒ n() {
  23. return 'n';
  24. } undefined
  25. index.html:32 2 ƒ () {
  26. return true;
  27. } ƒ n() {
  28. return 'n';
  29. } undefined
  30. // // console.log(a, c, t,)
  31. // console.log(a, c, t)
  32. // {
  33. // var c = 2
  34. // console.log(t, m)
  35. // var t = function t(){
  36. // return 't'
  37. // }
  38. // function m(){
  39. // return 'm'
  40. // }
  41. // }
  42. // var a = 1;
  43. // console.log(window.a, window.c, window.t, window.m)

JS调用栈

image.png
image.png

var的缺陷以及为何引入let const

变量提升所带来的问题

  1. 变量容易在不被察觉的情况下被覆盖掉
  2. 本应销毁的变量没有被销毁

    ☆JavaScript 是如何支持块级作用域的

    “在同一段代码中,ES6 是如何做到既要支持变量提升的特性,又要支持块 级作用域的呢?”
    那么接下来,我们就要站在执行上下文的角度来揭开答案
  1. function foo(){
  2. var a = 1
  3. let b = 2
  4. {
  5. let b = 3
  6. var c = 4
  7. let d = 5
  8. console.log(a)
  9. console.log(b)
  10. }
  11. console.log(b)
  12. console.log(c)
  13. console.log(d)
  14. }
  15. foo()

image.png
image.png
image.png

image.png

精髓

image.png
image.png

image.png
let分为创建和初始化,创建会被提升到块级作用域最上面,然后放入暂时性死区,只有初始化后才会拿出来,放到词法环境的小型栈中。

如果站在语言层面来谈,每种语言其实都是在相互借鉴对方的优势,协同进化,比如 JavaScript 引进了作用域、迭代器和协程,其底层虚拟机的实现和 Java、Python 又是非 常相似,也就是说如果你理解了 JavaScript 协程和 JavaScript 中的虚拟机,其实你也就理 解了 Java、Python 中的协程和虚拟机的实现机制。

this 的设计缺陷以及应对方案

  1. 嵌套函数中的 this 不会从外层函数中继承 =》 箭头函数可解决,或者另用变量保存
  2. 普通函数中的 this 默认指向全局对象 window =》严格模式下默认指向undefined

    v8的垃圾回收

    代际假说:朝生夕死、
    新老生代 <===> 副主回收器
    新生代有对象区和空闲区
    标记整理、并行回收、增量回收、并发回收

消息队列与事件循环

image.png
image.png
个人认为:
宿主发起的任务是宏任务 如点击事件,settimeout 进消息队列;js引擎发起的任务是微任务如promise

页面使用单线程的缺点

第一个问题是如何处理高优先级的任务。
第二个是如何解决单个任务执行时长过久的问题。

setTimeout是如何实现的?

原来浏览器真的没有计时器线程。。
为了支持定时器的实现,浏览器增加了延时队列 ,setTimeout也真的还有很多陷阱啊,得看chromium源码。
image.png
image.png
image.png

注意事项!

  1. 如果当前任务执行时间过久,会影延迟到期定时器任务的执行
  2. 如果 setTimeout 存在5次及以上的嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
    1. image.png
  3. 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒

    也就是说,如果标签不是当前的激活标签,那么定时器最小的时间 间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量。这一点你在使用 定时器的时候要注意。

  4. 延时执行时间有最大值

image.png

  1. 使用 setTimeout 设置的回调函数中的 this 不符合直觉

    如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字 将指向全局环境,而不是定义时所在的那个对象。

    XMLHttpRequest是如何实现的?

    不过在 XMLHttpRequest 出现之前,如果服务器数据有更新,依然需要重新刷新整个页 面。而 XMLHttpRequest 提供了从 Web 服务器获取数据的能力,如果你想要更新某条数 据,只需要通过XMLHttpRequest 请求服务器提供的接口,就可以获取到服务器的数据, 然后再操作 DOM 来更新页面内容,整个过程只需要更新网页的一部分就可以了,而不用 像之前那样还得刷新整个页面,这样既有效率又不会打扰到用户。

image.png

  1. function GetWebData(URL){
  2. /**
  3. * 1: 新建 XMLHttpRequest 请求对象
  4. */
  5. let xhr = new XMLHttpRequest()
  6. /**
  7. * 2: 注册相关事件回调处理函数
  8. */
  9. xhr.onreadystatechange = function () {
  10. switch(xhr.readyState){
  11. case 0: // 请求未初始化
  12. console.log(" 请求未初始化 ")
  13. break;
  14. case 1://OPENED
  15. console.log("OPENED")
  16. break;
  17. case 2://HEADERS_RECEIVED
  18. console.log("HEADERS_RECEIVED")
  19. break;
  20. case 3://LOADING
  21. console.log("LOADING")
  22. break;
  23. case 4://DONE
  24. if(this.status == 200||this.status == 304){
  25. console.log(this.responseText);
  26. }
  27. console.log("DONE")
  28. break;
  29. }
  30. }
  31. xhr.ontimeout = function(e) { console.log('ontimeout') }
  32. xhr.onerror = function(e) { console.log('onerror') }
  33. /**
  34. * 3: 打开请求
  35. */
  36. xhr.open('Get', URL, true);// 创建一个 Get 请求, 采用异步
  37. /**
  38. * 4: 配置参数
  39. */
  40. xhr.timeout = 3000 // 设置 xhr 请求的超时时间
  41. xhr.responseType = "text" // 设置响应返回的数据格式
  42. xhr.setRequestHeader("X_TEST","time.geekbang")
  43. /**
  44. * 5: 发送请求
  45. */
  46. xhr.send();
  47. }
  1. 跨域问题
  2. HTTPS 混合内容的问题,如果用xmlrequesthttp请求了https但包含了的http资源,则会报错,加载不了

Promise 消灭嵌套调用 和 多次错误处理

使用 Promise 来重构 XFetch 的代码

  1. //makeRequest 用来构造 request 对象
  2. function makeRequest(request_url) {
  3. let request = {
  4. method: 'Get',
  5. url: request_url,
  6. headers: '',
  7. body: '',
  8. credentials: false,
  9. sync: true,
  10. responseType: 'text',
  11. referrer: ''
  12. }
  13. return request
  14. }
  15. function XFetch(request) {
  16. function executor(resolve, reject) {
  17. let xhr = new XMLHttpRequest()
  18. xhr.open('GET', request.url, true)
  19. xhr.ontimeout = function (e) { reject(e) }
  20. xhr.onerror = function (e) { reject(e) }
  21. xhr.onreadystatechange = function () {
  22. if (this.readyState === 4) {
  23. if (this.status === 200) {
  24. resolve(this.responseText, this)
  25. } else {
  26. let error = {
  27. code: this.status,
  28. response: this.response
  29. }
  30. reject(error, this)
  31. }
  32. }
  33. }
  34. xhr.send()
  35. }
  36. return new Promise(executor)
  37. }

image.png

  1. Promise 中为什么要引入微任务?
  2. Promise 中是如何实现回调函数返回值穿透的?
  3. Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获异常的函数?

    async&await

    可以看我写的那篇async await文章。
    async&await = Generator(底层用协程实现, 协程在用户态) + 执行器(自动迭代器) + Promise
    image.png
    第一点:gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切 换是通过 yield 和 gen.next 来配合完成的。

第二点:当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调 用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时, JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。

为了直观理解父协程和 gen 协程是如何切换调用栈的,你可以参考下图:
image.png

安全沙箱:页面和系统之间的隔离墙

image.png
image.png
image.png
image.png
image.png
那安全沙箱是如何影响到各个模块功能的呢?
image.png
image.png

iframe级别的站点隔离:
image.png
安全沙箱是不能防止 XSS 或者 CSRF 一类的攻击, 安全沙箱的目的是隔离渲染进程和操作系统,让渲染进程没有访问操作系统的权利。 XSS 或者 CSRF 主要是利用网络资源获取用户的信息,这和操作系统没有关系的

HTTPS:让数据传输更安全

image.png
image.png
image.png
image.png
image.png
image.png
这个都在我脑子里了。。凡是我没好好写的东西,都在我脑中了,就算忘了也挺好,毕竟提醒我忘了,该复习了