diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..86cf8936f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ts-node", + "type": "node", + "request": "launch", + "args": [ + "${relativeFile}" + ], + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "cwd": "${workspaceRoot}", + "protocol": "inspector", + "internalConsoleOptions": "openOnSessionStart" + } + ] +} \ No newline at end of file diff --git "a/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/01-\344\272\213\344\273\266\345\276\252\347\216\257.md" "b/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/01-\344\272\213\344\273\266\345\276\252\347\216\257.md" new file mode 100644 index 000000000..6b6e08b8a --- /dev/null +++ "b/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/01-\344\272\213\344\273\266\345\276\252\347\216\257.md" @@ -0,0 +1,115 @@ +# 事件循环 + +事件循环,在W3C中描述为 `Event Loop`,Chrome的实现源码中定义为 `Message Loop`,因此事件循环也称为消息循环,是**浏览器渲染主线程**的工作方式。 + + + +## 1. 浏览器的进程模型 + +浏览器是一个多进程多线程的应用程序,核心的进程是**浏览器进程**、**网络进程**、**渲染进程** + +**浏览器进程**:负责浏览器软件的UI界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同任务。 + +**网络进程**:负责加载网络资源。 + +**渲染进程**:(重点) 渲染进程启动后,会开启一个**渲染主线程**,主线程负责执行 HTML、CSS、JS。默认情况下,浏览器会为每个标签页开启一个**新的渲染进程**,以保证标签页隔离 (未来可能改为一个站点一个进程)。 + + + +## 2. 渲染主线程的异步处理 + +渲染主线程是浏览器中最繁忙的线程,处理的任务包括但不限于: + ++ 解析 HTML ++ 解析 CSS ++ 计算样式 ++ 布局 ++ 处理图层 ++ 帧绘制 ++ 执行全局 JS 代码 ++ 执行事件处理函数 ++ 执行计时器的回调函数 ++ ...... + +JS运行在浏览器的单个渲染进程的渲染主线程中,而渲染主线程只有一个!注意,js的单线程,是因为执行在浏览器的渲染主线程,并不代表浏览器是单进程/线程的。 + +因此,JS是一门 **单线程** 的语言,浏览器采用异步而非同步的方式来避免阻塞,如计时器、网络、事件监听。 + +主线程将任务交给其他线程处理,完成后将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。 + +**总结**: + +1. **单线程是异步产生的原因** + +2. **事件循环是异步的实现方式** + + + +**异步的场景**:`setTimeout`、`setInterval`、`XHR`、`Fetch`、`addEventListener` 等 + + + + + +## 3. JS 阻塞渲染 + +如下面的案例,虽然 h1 已经设置了 text 内容,但会在 3s 后才显示,因为 **JS执行和渲染都在浏览器的渲染主线程上执行**,在执行了 h1 内容设置后,向消息队列(message queue)中插入了新的渲染任务,但需要在 delay 完成后,渲染主线程才会执行渲染。 + +```javascript +var h1 = document.querySelector(...); +var btn = document.querySelector(...); + +// 死循环一段时间 +function delay(duration) { + var start = Date.now(); + while(Date.now() - start < duration) {} +} + +btn.onclick = function() { + h1.textContent = 'xxx'; + delay(3000); +} +``` + + + +## 4. 任务没有优先级,但消息队列有优先级 + +每个任务都有一个任务类型,同一类型的任务必须在一个队列,一个队列可能有多种类型的任务,在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。 + +浏览器必须准备好一个**微队列**(microTask),微队列中的任务优先于所有其他任务执行。 + +随着浏览器复杂度急剧提升,W3C不再使用宏队列的说法。 + +目前 chrome 中至少包含了以下队列: + ++ **微队列**:[最高] 存放需要最快执行的任务 ++ **交互队列**:[高] 存放用户交互后产生的事件处理任务 ++ **延时队列**:[中] 存放定时器回调 + +添加任务到微队列的方式主要为:`Promise`、`MutationObserver`,如 + +```javascript +// 立即添加函数到微队列 +Promise.resolve().then(函数) +``` + +案例: + +```javascript +function delay(duration) { + var start = Date.now(); + while(Date.now() - start < duration) {} +} +setTimeout(function() { // 添加到延迟队列 + console.log(1); +}, 0); +Promise.resolve().then(function() { // 添加到微队列 + console.log(2); +}); +delay(1000); // 死循环1s +console.log(3); // 当前任务中执行 +// 1s 后输出 3 2 1 +``` + + diff --git "a/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/02-\346\265\217\350\247\210\345\231\250\346\270\262\346\237\223\345\216\237\347\220\206.md" "b/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/02-\346\265\217\350\247\210\345\231\250\346\270\262\346\237\223\345\216\237\347\220\206.md" new file mode 100644 index 000000000..fe6725d8e --- /dev/null +++ "b/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/02-\346\265\217\350\247\210\345\231\250\346\270\262\346\237\223\345\216\237\347\220\206.md" @@ -0,0 +1,78 @@ +# 浏览器渲染原理 + +## 1. 渲染过程 + + + +### 1.1 【解析 HTML】 为 DOM 和 CSSOM + +首先解析 HTML,解析过程中遇到 CSS 解析 CSS,遇到 JS 执行 JS。为提高解析效率,浏览器在开始解析前,会启动一个预解析线程,率先下载 HTML 中的外部 CSS 文件和 JS 文件。 + +若主线程解析到 link 位置时 CSS 文件还未下载解析完成则会跳过。因为下载和解析 CSS 是在预解析线程中进行的,所以 **CSS 不会阻塞渲染主线程对 HTML 的解析**。 + +主线程解析到 script 位置时会停止解析 HTML,等待 JS 文件下载结束,并将全局代码解析执行完成后,才继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停,所以 **JS 会阻塞渲染主线程对 HTML 的解析**。 + +解析 HTML 完成后,会得到 **DOM 树** 和 **CSSOM 树**,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。 + +### 1.2 【样式计算】得到带样式的 DOM + +主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出最终样式,称之为 **Computed Style**。在此过程中,很多预设值会变成绝对值,比如 red 会变成 rgb(255,0,0); 相对单位会变成绝对单位,比如 em 会变成 px。 + +样式计算完成后,会得到一棵带有样式的 DOM 树。 + +> js 操作 stylesheet +> +> ```javascript +> document.styleSheets +> getComputedStyle(document.body) +> ``` + +### 1.3 【布局】生成 layout 布局树 + +布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息,如节点的宽高、相对包含块的位置。 + +大部分时候,DOM 树和布局树并非一一对应。比如 `display:none` 节点不会生成到布局树;比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中;还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。 + + + +### 1.4 【分层】 + +主线程会使用一套复杂的策略对整个布局树进行分层。分层会影响后续的布局树变化的处理,当某层变动只改某层能提升性能,但层次过多也会消耗性能。滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,可以通过 `will-change` 属性更大程度的影响分层结果,如在css中添加 `will-change: transform` 属性告诉浏览器将来 transform 可能变化,由浏览器决定是否分层。 + +### 1.5 【绘制】生成绘制指令集传递给合成线程 + +主线程会为每个层单独生成绘制指令集,完成后主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。 + +### 1.6 【分块】合成线程分块 + +合成线程首先将每个图层划分为更多的小区域,它会从线程池中拿取多个线程来完成分块工作。 + +### 1.7 【光栅化】GPU光栅化 + +合成线程会将块信息交给 GPU 进程,GPU 进程会开启多个线程极速完成光栅化,得到一块一块的位图,默认会优先处理靠近视口区域的块。 + +### 1.8 【draw】合成线程根据位图生成指引并由GPU绘制到屏幕 + +合成线程拿到每个层、每个块的位图后,生成一个个"指引(quad)"信息,用于标识出每个位图在屏幕的绘制位置,以及旋转、缩放等变形的处理。 + +由于 `transform` 变形发生在合成线程,与渲染主线程无关,所以效率高。 + +最后合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。 + + + +## 2. transform 高效的原因 + +如前面说述,transform 既不影响布局也不影响绘制指令,只影响渲染流程最后的绘制阶段。由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响忙碌的渲染主线程。 + + + +## 3. reflow(重排) & repaint(重绘) + +`reflow`:当进行了影响布局树的操作后,需要重新计算 layout 布局树。浏览器会合并布局属性的修改操作,当 JS 代码全部完成后再异步统一计算。但为了让 JS 获取布局属性时取到最新的布局信息,在获取属性时会立即 reflow。 + +`repaint`:当修改了可见样式后需要重新计算绘制指令。 + +由于元素的布局信息也属于可见样式,所以 **reflow 一定会引起 repaint**,要尽量减少 reflow。 + + diff --git "a/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/03-\345\261\236\346\200\247\346\217\217\350\277\260\347\254\246.md" "b/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/03-\345\261\236\346\200\247\346\217\217\350\277\260\347\254\246.md" new file mode 100755 index 000000000..19db7be31 --- /dev/null +++ "b/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/03-\345\261\236\346\200\247\346\217\217\350\277\260\347\254\246.md" @@ -0,0 +1,62 @@ +# 属性描述符 + +## 1. 属性描述符的获取与设置 + +```javascript +var obj = { a: 1, b: 2 }; + +// 获取属性描述符 +Object.getOwnPropertyDescriptor(obj, 'a'); +// { value: 1, writable: true, enumerable: true, configurable: true } + +// 设置属性描述符 +Object.defineProperty(obj, 'a', { + // value: 3, // Cannot both specify accessors and a value or writable attribute + // writable: false, // 不可重写 + enumerable: false, // 不可遍历 + configurable: false, // 属性描述符不可再修改 + get: function() { // getter + return 4; + }, + set: function() { // setter + throw new Error('属性只读'); + }, +}); +``` + +## 2. 案例:属性描述符的应用 + +```javascript +class Demo { + constructor (g) { + g = { ...g }; + Object.freeze(g); //【1】clone后冻结,避免改变原对象 + Object.defineProperty(this, 'data', { + get: function() { + return g; + }, + set: function() { + throw new Error('data 属性只读'); + }, + configurable: false, // 属性描述符不可再修改 + }); + + this.testForFreeze = 1; + // Object.freeze(this); //【2】冻结,但也会影响现有的属性方法 + Object.seal(this); //【3】密封,不能给对象添加/删除属性和方法,不能修改现有属性和方法的配置 + } +} + +var g = new Demo({ + price: 1, +}); + +g.data.price = 2; +g.testForFreeze = 2; // Object.freeze 后不会生效,但 Object.seal 不影响 +g.testForSeal = 2; // Object.freeze 和 Object.seal 后都不会生效 + +Object.freeze(Demo.prototype); //【4】冻结原型链 +Demo.prototype.testForPrototype = 1; +console.log(g.testForPrototype); // undefined +``` + diff --git "a/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/04-CSS\345\214\205\345\220\253\345\235\227.md" "b/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/04-CSS\345\214\205\345\220\253\345\235\227.md" new file mode 100644 index 000000000..330dde7dc --- /dev/null +++ "b/01-JS\350\257\255\350\250\200\345\237\272\347\241\200/2023\345\274\272\345\214\226/04-CSS\345\214\205\345\220\253\345\235\227.md" @@ -0,0 +1,132 @@ +# CSS包含块 + +元素的尺寸和位置,会受它的包含块影响。对于一些属性,例如 width, height, padding, margin,当绝对定位元素(position: absolute/fixed)的偏移值为百分比值时,由元素的包含块计算得来。 + +包含块分两种: + ++ **初始包含块(initial containing block)**,即根元素(HTML元素)所在的包含块,浏览器中等于视口 viewport 大小,定位基点在画布原点(视口左上角)。 + ++ **非根元素包含块**,判定方式分以下规则: + +1. 元素 `position: relative/static`,包含块为最近的**块容器** (block container) 的**内容区域**(content area) + +2. 元素 `position: fixed`,包含块为视口 + +3. 元素 `position: absolute`,包含块为最近的 `position` 的值非 `static` (即值为 `fixed`/`absolute`/`relative`/`sticky`) 的祖先元素的内边距区域 + +案例: + +```html +
+This is text in the first paragraph...
++ This is text + + in the + second + paragraph. + +
+