@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
|
||||
- it’s also important to realize 认识到这一点也很重要 |
||||
- some other 其他一些 |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
- 正则表达式匹配所有中文:`^[\u4e00-\u9fa5]{2}$` |
@ -1,10 +0,0 @@
@@ -1,10 +0,0 @@
|
||||
## 工作流程 |
||||
|
||||
1. 浏览器首先使用 HTTP 协议或者 HTTPS 协议,向服务端请求页面; |
||||
2. 把请求回来的 HTML 代码经过解析,构建成 DOM 树; |
||||
3. 计算 DOM 树上的 CSS 属性; |
||||
4. 最后根据 CSS 属性对元素逐个进行渲染,得到内存中的位图; |
||||
5. 一个可选的步骤是对位图进行合成,这会极大地增加后续绘制的速度; |
||||
6. 合成之后,再绘制到界面上。 |
||||
|
||||
从 HTTP 请求回来,就产生了流式的数据,后续的 DOM 树构建、CSS 计算、渲染、合成、绘制,都是尽可能地流式处理前一步的产出:即不需要等到上一步骤完全结束,就开始处理上一步的输出,这样我们在浏览网页时,才会看到逐步出现的页面。 |
@ -1,4 +0,0 @@
@@ -1,4 +0,0 @@
|
||||
/var/log/message 一般信息和系统信息 |
||||
/var/log/secure 登陆信息 |
||||
/var/log/maillog mail记录 |
||||
/var/log/wtmp 登陆记录信息(last命令即读取此日志) |
@ -1,8 +0,0 @@
@@ -1,8 +0,0 @@
|
||||
# 这是一篇使用 Obsidian 写的笔记 |
||||
|
||||
其实我没有自建个人知识库的打算,我只是想找一款趁手的界面美观的笔记软件,最好支持私有化部署,前前后后我总共试过不少笔记软件,最近一个最深得我心的是 Trilium,它支持私有化部署,还是用我吃饭的语言 JavaScript 写的,但是很遗憾,它的界面并不能让我满意,在经过一两个小时的新鲜体验之后就从我的服务器上消失了,我又开始了寻找一款优秀的还正好戳中我审美的笔记软件的路程,其实在这之前我一直都在使用 OneNote 写笔记,直到现在我也觉得没有什么问题,相当强大的云同步和全平台优势,至于为什么要寻找一个新的笔记软件,那可能就是刻在DNA里面的那一丝丝“折腾”的精神,现在决定选中 Obsidian + Git 的方式管理笔记,也正好最近在服务器上搭建了私有的 Gitea 服务,这不得用起来? |
||||
不得不说 Markdown 写笔记确实是舒服,学习成本低,排版相对容易,用 OneNote 的时候总会考虑对齐的问题,但是这不代表以后就会用 OneNote,在它的优势下依然会是我首选的笔记工具,只是在一些场景会更细化,两两结合 |
||||
最后按照惯例 |
||||
|
||||
|
||||
# Obsidian 牛逼!! |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
- useMemo 除了用于减少子组件的重复渲染,还可以用于避免重复计算 |
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
- 父组件命中性能优化,子组件才有可能命中性能优化 |
||||
- 没有传递 props 的时候,子组件接收到的 props 实际上是一个空对象 |
||||
- React Context 内的数据发生变化时,只有使用了 useContext 的组件会发生 render |
||||
- 当父组件的状态发生变化 使用了 useContext 的组件会发生 render,即使它的 props 并没有发生变化 |
||||
- 组件的状态尽可能的下放,粒度尽可能的精细,可以尽量减少组件的重复 render |
||||
- 有时候使用控制反转也就是直接使用 children props 传递子组件可以减少重复 render |
@ -1,5 +0,0 @@
@@ -1,5 +0,0 @@
|
||||
- [ ] 为什么说父组件直接调用子组件的函数,会让执行过程处于生命周期以外 |
||||
- [ ] React.memo 的作用和实际发生了什么? |
||||
- [ ] useMemo 的前提是当前组件必须是浅比较,为什么? |
||||
- [ ] 当使用了组件反转之后,children 传递的组件属于谁的子组件,React 源码中又是如何处理的 |
||||
- [ ] React Context Get 和 Set 分离是否有用? |
@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
|
||||
- [ ] 什么是排序算法的稳定性 |
||||
- [ ] 什么是原地排序算法 |
@ -1,21 +0,0 @@
@@ -1,21 +0,0 @@
|
||||
用前中后序遍历二叉树 |
||||
|
||||
## 二叉搜索树 BST |
||||
|
||||
> Binary Search Tree |
||||
|
||||
left (包括其后代) value <= root value |
||||
right (包括其后代) value >= root value |
||||
|
||||
### 求二叉搜索树的第 K 小值 |
||||
|
||||
## 平衡二叉搜索树 BBST |
||||
|
||||
## 红黑树 / 自平衡二叉搜索树 |
||||
|
||||
>通过红黑颜色转换来维持树的平衡 |
||||
>低成本快速维持平衡的平衡二叉搜索树 |
||||
|
||||
## B 树 |
||||
>物理上是多叉树,但逻辑上是二叉树 |
||||
>一般用于高效I/O,关系型数据库常用 B 树来组织数据 |
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
>1. 完全二叉树 |
||||
>2. 最大堆:父节点 >= 子节点 |
||||
>3. 最小堆:子节点 <= 父节点 |
||||
>4. 逻辑结构是一颗二叉树,物理结构上是一个数组 |
||||
|
||||
## 堆栈模型 |
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
>栈是一种逻辑结构,从栈的操作特性来看,它是一种“操作受限”的线性表。 |
||||
>后进者先出,先进者后出,这就是典型的“栈”结构。 |
||||
|
||||
当某个数据集合只涉及在一端插入和删除,并且满足后进先出、先进后出的特性,我们就应该首选“栈”这种数据结构。 |
||||
|
||||
由于栈只是一个逻辑结构,所以它既可以用数组实现,也可以用链表来实现,数组实现的栈,叫做**顺序栈**,而链表实现的栈,叫做**顺序栈**。 |
@ -1,91 +0,0 @@
@@ -1,91 +0,0 @@
|
||||
## 数组转换成链表 |
||||
|
||||
```typescript |
||||
interface LinkedList { |
||||
value: Number | null; |
||||
next: LinkedList | null; |
||||
} |
||||
|
||||
const toLinkedList = (array: Number[]): LinkedList => { |
||||
let linkedList: LinkedList = { |
||||
value: null, |
||||
next: null, |
||||
}; |
||||
|
||||
const endLength = array.length - 1; |
||||
for (let index = endLength; index >= 0; index--) { |
||||
linkedList = { |
||||
value: array[index], |
||||
next: index === endLength ? null : linkedList, |
||||
}; |
||||
} |
||||
|
||||
return linkedList; |
||||
}; |
||||
``` |
||||
|
||||
## 反转单向链表 |
||||
|
||||
```typescript |
||||
interface LinkedList { |
||||
value: Number | null; |
||||
next: LinkedList | null; |
||||
} |
||||
|
||||
const reverseLinkedList = (LinkedList: LinkedList) => { |
||||
let preNode: LinkedList | null = null; |
||||
let curNode: LinkedList | null = LinkedList; |
||||
let nexNode: LinkedList | null = LinkedList.next; |
||||
|
||||
while (curNode) { |
||||
curNode.next = preNode; |
||||
preNode = curNode; |
||||
if (nexNode == null) break; |
||||
curNode = nexNode; |
||||
nexNode = nexNode.next; |
||||
} |
||||
|
||||
return curNode; |
||||
}; |
||||
``` |
||||
|
||||
## 使用链表实现队列 |
||||
|
||||
```typescript |
||||
interface LinkedList { |
||||
value: number | null; |
||||
next: LinkedList | null; |
||||
} |
||||
|
||||
class LinkedListQueue { |
||||
private head: LinkedList | null = null; |
||||
private tail: LinkedList | null = null; |
||||
private len: number = 0; |
||||
|
||||
add(value: number) { |
||||
if (this.len === 0) { |
||||
this.head = { value, next: null }; |
||||
this.tail = this.head; |
||||
this.len++; |
||||
return; |
||||
} |
||||
this.tail.next = { value, next: null }; |
||||
this.tail = this.tail.next; |
||||
this.len++; |
||||
} |
||||
|
||||
pop(): number { |
||||
if (!this.head || !this.tail) return; |
||||
const popValue = this.head.value; |
||||
this.head = this.head.next; |
||||
this.len--; |
||||
return popValue; |
||||
} |
||||
|
||||
get length() { |
||||
return this.len; |
||||
} |
||||
} |
||||
``` |
||||
|
||||
## 实现一个简单的 LRU 缓存淘汰策略 |
@ -1,37 +0,0 @@
@@ -1,37 +0,0 @@
|
||||
>队列跟栈一样,也是一种**操作受限的线性表数据结构** |
||||
|
||||
用数组实现的队列叫作**顺序队列**,用链表实现的队列叫作**链式队列**。 |
||||
|
||||
## 用两个栈实现一个队列 |
||||
|
||||
```javascript |
||||
class StackQueue { |
||||
private stack1: number[] = []; |
||||
private stack2: number[] = []; |
||||
|
||||
add(value: number) { |
||||
this.stack1.push(value); |
||||
} |
||||
|
||||
popup() { |
||||
if (!this.stack1.length) return; |
||||
while (this.stack1.length) { |
||||
const stackValue = this.stack1.pop(); |
||||
this.stack2.push(stackValue!); |
||||
} |
||||
|
||||
const popValue = this.stack2.pop(); |
||||
|
||||
while (this.stack2.length) { |
||||
const stackValue = this.stack2.pop(); |
||||
this.stack1.push(stackValue!); |
||||
} |
||||
|
||||
return popValue; |
||||
} |
||||
|
||||
get length() { |
||||
return this.stack1.length; |
||||
} |
||||
} |
||||
``` |
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
>数据结构就是数据的存储形式,算法就是对数据的一系列操作 |
||||
|
||||
一般而言,我们遇到一个算法问题,可以大致分为如下三个步骤 |
||||
- 第一步是建立算法模型 |
||||
- 第二步则是根据算法模型分析我们需要对数据进行哪些操作(增删改) |
||||
- 第三步,我们需要分析哪些操作最频繁,对算法的影响最大。最后,我们需要根据第三步的分析结果选择合适的数据结构 |
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
- 要有对时间复杂度的敏感性,如 length 不能遍历查找 |
||||
- 数据结构的选择,要比算法优化更重要 |
||||
- 凡有序,必二分 |
||||
- 凡二分,时间复杂度必包含 O(logn) |
||||
- 优化嵌套循环,可以考虑双指针 |
||||
- 二叉搜素树可以使用二分法快速查找 |
||||
- 分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧 |
@ -1,22 +0,0 @@
@@ -1,22 +0,0 @@
|
||||
复杂度的计算是算法中至关重要的一个知识点,它指的是一个数量级 |
||||
|
||||
## 时间复杂度 |
||||
|
||||
O(1) 无论输入的量级有多大,始终都保持相同计算量 |
||||
O(n) 计算量和输入的量级成正比,即输入量级越大计算量越大 |
||||
O(n^2) 计算量是输入量级的平方 |
||||
O(logn) 计算量是数据量的对数 |
||||
O(n * logn) 数据量 * 数据量的对数 |
||||
|
||||
O(n) 常见于循环遍历的算法中 |
||||
O(n^2) 则会经常出现在嵌套循环的算法中 |
||||
O(logn) 常见于二分的算法中 |
||||
O(n * logn) 常见于循环嵌套二分的算法中 |
||||
|
||||
## 空间复杂度 |
||||
|
||||
O(1) 无论输入的量级有多大,始终都保持相同空间量 |
||||
O(n) 空间量和输入的量级成正比,即输入量级越大空间量越大 |
||||
O(n^2) 空间量是输入量级的平方 |
||||
O(logn) 空间量是数据量的对数 |
||||
O(n * logn) 数据量 * 数据量的对数 |
@ -1,25 +0,0 @@
@@ -1,25 +0,0 @@
|
||||
## 实现一个基于二分查找的整形数组查找 |
||||
|
||||
```typescript |
||||
const indexOf_BinarySearch = (dataArr: number[], num: number): number => { |
||||
let n1 = 0; |
||||
let n2 = dataArr.length - 1; |
||||
if (n1 === num) return n1; |
||||
if (n2 === num) return n2; |
||||
|
||||
while (n1 <= n2) { |
||||
const mid = Math.floor((n1 + n2) / 2); |
||||
if (num > dataArr[mid]) { |
||||
n1 = mid + 1; |
||||
} else if (num < dataArr[mid]) { |
||||
n2 = mid - 1; |
||||
} else { |
||||
return mid; |
||||
} |
||||
} |
||||
|
||||
return -1; |
||||
}; |
||||
``` |
||||
|
||||
## 两数之和 |
@ -1,30 +0,0 @@
@@ -1,30 +0,0 @@
|
||||
>大问题拆解为小问题,逐级向下拆解 |
||||
|
||||
## 用循环实现斐波那契数列 |
||||
|
||||
>实际上是 (n - 1) + (n - 2) |
||||
|
||||
```typescript |
||||
const Fibonacci_Cycle = (n: number): number => { |
||||
if (n <= 0) return 0; |
||||
if (n === 1) return n; |
||||
|
||||
let n1 = 0; |
||||
let n2 = 0; |
||||
let n3 = 0; |
||||
|
||||
for (let index = 0; index <= n; index++) { |
||||
if (index === 1) { |
||||
n1 = 0; |
||||
n2 = 1; |
||||
continue; |
||||
} |
||||
|
||||
n3 = n1 + n2; |
||||
n1 = n2; |
||||
n2 = n3; |
||||
} |
||||
|
||||
return n3; |
||||
}; |
||||
``` |
@ -1,14 +0,0 @@
@@ -1,14 +0,0 @@
|
||||
对于排序算法执行效率的分析,我们一般会从这几个方面来衡量: |
||||
1. 最好情况、最坏情况、平均情况时间复杂度 |
||||
2. 时间复杂度的系数、常数 、低阶 |
||||
3. 比较次数和交换(或移动)次数 |
||||
|
||||
## 冒泡排序 |
||||
|
||||
## 插入排序 |
||||
|
||||
## 选择排序 |
||||
|
||||
## 归并排序 (Merge Sort) |
||||
|
||||
## 快速排序 (Quick Sort) |
@ -1,17 +0,0 @@
@@ -1,17 +0,0 @@
|
||||
>深度优先遍历: DFS(英语:Depth-First-Search,DFS) 是一种用于遍历或搜索树或图的算法 |
||||
|
||||
```javascript |
||||
const node = { |
||||
name: "node 1", |
||||
children: [] |
||||
}; |
||||
|
||||
function DFS(node) { |
||||
console.log("探寻", node.name); |
||||
node.children.forEach(child => { |
||||
DFS(child); |
||||
}); |
||||
console.log("回溯", node.name); |
||||
} |
||||
``` |
||||
|
@ -1,10 +0,0 @@
@@ -1,10 +0,0 @@
|
||||
只要同时满足以下三个条件,就可以用递归来解决: |
||||
1. 一个问题的解可以分解为几个子问题的解 |
||||
2. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样 |
||||
3. 存在递归终止条件 |
||||
|
||||
写递归代码最关键的是写出递推公式,找到终止条件 |
||||
|
||||
编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤 |
||||
|
||||
递归代码虽然简洁高效,但是,递归代码也有很多弊端。比如,堆栈溢出、重复计算、函数调用耗时多、空间复杂度高等,所以,在编写递归代码的时候,一定要控制好这些副作用。 |
@ -1,32 +0,0 @@
@@ -1,32 +0,0 @@
|
||||
>在浏览器环境中,我们也无法单纯依靠 JavaScript 代码实现 div 对象,只能靠 document.createElement 来创建,也说明了 JavaScript 的对象机制并非简单的属性集合 + 原型 |
||||
|
||||
## 对象分类 |
||||
|
||||
- 宿主对象 (host Object):由 JavaScript 宿主环境提供的对象,它们的行为完全由宿 主环境决定 |
||||
- 内置对象 (Built-in Objects):由 JavaScript 语言提供的对象 |
||||
- 固有对象 (Intrinsic Objects):由标准规定,随着 JavaScript 运行时创建而自动创 建的对象实例。 |
||||
- 原生对象 (Native Objects):可以由用户通过 Array、RegExp 等内置构造器或者特 殊语法创建的对象。 |
||||
- 普通对象 (Ordinary Objects):由{}语法、Object 构造器或者 class 关键字定义类 创建的对象,它能够被原型继承。 |
||||
|
||||
## 固有对象 |
||||
|
||||
>固有对象在任何 JS 代码执行前就已经被创建出来了,它们通常扮演着类似基础库的角色,“类”其实就是固有对象的一种。 |
||||
|
||||
固有对象构造器创建的对象多数使用了私有字段,这些字段使得原型继承方法无法正常工作,这里的无法正常工作指的是无法继承这一些私有字段,所以我们可以认为所有这些原生对象都是为了特定能力或者性能而设计出来的 “特权对象”。 |
||||
|
||||
函数对象的定义是:具有 \[\[call\]\] 私有字段的对象,该必须是一个引擎中定义的函数,需要接受 this 值和调用参数,并且会产生域的切换。 |
||||
构造器对象的定义是:具有 \[\[construct\]\] 私有字段的对象。 |
||||
|
||||
JavaScript 使用对象模拟函数的的设计代替了一般编程语言中的函数,它们可以像其他语言的函数一样被调用、传参,只需要实现了上面函数对象的定义要求,就能被 JavaScript 函数调用语法支持。 |
||||
|
||||
用户用 function 关键词创建的函数必定同时是函数和构造器,但是它们表现出来的行为效果却并不相同:比如 Number 、String 等构造器在被当作函数调用时则产生类型转换的效果。在 ES6 之后使用 `=>` 语法创建出来的函数仅仅是函数,它们无法被当作构造器来使用。 |
||||
|
||||
但是对于用户使用 `function` 语法或者 Function 构造器创建的对象来说,\[\[call\]\] 和 \[\[construct\]\] 的行为却是相似的 |
||||
|
||||
\[\[construct\]\] 的执行过程实际上和 new 关键词的执行过程是一致的: |
||||
1. 创建一个空的简单JavaScript对象 (即 **{}**,以 Object.prototype 为原型) |
||||
2. 为步骤1新创建的对象添加属性 **\_\_proto\_\_**,将该属性链接至构造函数的原型对象 |
||||
3. 以新对象为 this,执行函数的 \[\[call\]\] |
||||
4. 如果 \[\[call\]\] 的返回值是对象则返回该对象,否则返回第一步创建的对象 |
||||
|
||||
以上也是出现闭包的背景原因,因为如果构造函数返回一个对象,那么步骤一创建的新对象就会变成一个除了构造函数之外完全无法访问的对象。 |
@ -1,94 +0,0 @@
@@ -1,94 +0,0 @@
|
||||
>JavaScript 引擎会常驻于内存中,等待着宿主将 JavaScript 代码或者函数传递给它执行。 |
||||
|
||||
在 ES3 和更早的版本中,JavaScript 本身还没有异步执行代码的能力,在 ES5 之后,JavaScript 引入了 Promise,这样不需要浏览器的安排,引擎本身也可以发起任务了。 |
||||
|
||||
## 宏观和微观任务 |
||||
|
||||
采纳 JSC 引擎的术语,把宿主发起的任务称为**宏观任务**,将引擎发起的任务称作**微观任务** |
||||
|
||||
用伪代码表示事件循环大概如下: |
||||
|
||||
```javascript |
||||
while(true) { |
||||
r = wait(); |
||||
execute(r); |
||||
} |
||||
``` |
||||
|
||||
其中每次的循环过程,其实就是一个宏观任务,大致可以理解为宏观任务的队列就相当于事件循环。在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列。 |
||||
|
||||
有了宏观任务和微观任务机制,我们就可以实现 JavaScript 引擎级和宿主级的任务了,例如:Promise 永远在队列尾部添加微观任务。setTimeout 等宿主 API,则会添加宏观任务。 |
||||
|
||||
## Promise |
||||
|
||||
Promise 是 JavaScript 语言提供的一种标准化的异步管理方式,它的总体思想是,需要进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过 Promise 的 then 方法的回调)。 |
||||
|
||||
JavaScript 引擎会在下一次执行宿主发起的宏观任务之前先执行由引擎自身发起的微观任务,这也就是为什么微观任务始终会在宏观任务之前执行。 |
||||
|
||||
## this 关键字 |
||||
|
||||
普通函数的 this 的最终指向的是那个调用它的对象,举一个例子说明 |
||||
|
||||
```javascript |
||||
function showThis(){ |
||||
console.log(this); |
||||
} |
||||
var o = { |
||||
showThis: showThis |
||||
} |
||||
showThis(); // global |
||||
o.showThis(); // o |
||||
``` |
||||
|
||||
|
||||
在以上代码中 `o.showThis()` 表达式返回的并非是函数本身,而是一个 Reference 类型,该类型由两部分组成:一个对象和一个属性值,在调用时时被解引用,获取到真正的信息,在上面的代码中 Reference 类型中的对象被当作 this 值,传入了执行函数时的上下文当中,这也就是为什么 this 会指向调用它的那个对象。 |
||||
|
||||
如果将例子中的 showThis 函数改为箭头函数,那么结果会发生变化,这是因为箭头函数并不会产生新的执行上下文,其 this 始终与外层函数保持一致。 |
||||
|
||||
如果将例子中的 showThis 函数改写为类中的方法,那么直接调用 showThis 方法的 this 的结果也会不同,这是因为 JavaScript 的 Class 被设计成了默认在严格模式下(use strict)执行,而严格模式下 this 指向会发生一些改变。 |
||||
|
||||
this 是一个复杂的机制,JavaScript 标准定义了 \[\[ thisMode \]\] 私有属性,这个属性有以下三种取值: |
||||
- lexical - 表示从上下文中找 this,对应箭头函数。 |
||||
- global - 表示当 this 为 underfined 时,取全局对象,对应普通函数。 |
||||
- strict - 当严格模式时使用,this 严格按照调用时传入的值,可能为 underfined 或 null |
||||
|
||||
实际上 this 也可以看作是在调用时被确认的词法环境,普通函数被调用时会将 this 指向调用者压入执行上下文栈,但箭头函数不会创建新的词法环境,因此箭头函数中的 this 将从词法环境中查找并继承(复用)其定义时外部函数的 this。 |
||||
|
||||
JavaScript 提供了可以操作 this 的内置函数,如 Function.prototype.call 和 Function.prototype.apply,它们可以指定函数调用时传入的 this,除此之外还有 Function.prototype.bind,它可以返回一个更改了 this 的函数,它们甚至可以应用在箭头函数中,但是它并不会修改 this,仅仅实现了传参的能力。 |
||||
|
||||
Kyle Simpson 关于 this 的总结: |
||||
1. 如果由new调用 - 绑定到新创建的对象 |
||||
2. 如果由 call、apply 或 bind调用 - 绑定到指定的对象 |
||||
3. 如果由上下文对象调用 - 绑定到那个上下文对象 |
||||
4. 其他调用情况 - 严格模式下会绑定到 undefined,非严格模式绑定到全局对象 |
||||
5. 以上情况对于箭头函数都不适用,它会继承外层函数的 this 绑定 |
||||
|
||||
## 上下文栈 |
||||
|
||||
>JavaScript 引擎并非一行一行分析执行代码,而是一段一段的分析执行,当执行一段代码的时候会进行一些准备工作 |
||||
|
||||
函数能够引用定义时的变量,函数也能记住定义时的 this,因此,函数内部必然有一个机制来保存这些信息,这个用来保存定义时上下文的机制就是私有属性 \[\[Environment\]\]。 |
||||
|
||||
在函数执行时,会创建一条执行环境记录,也就是函数定义时的上下文设置为函数的 \[\[Environment\]\],这个动作就是切换上下文,无论函数以何种形式被调用,变量都会依照定义时的环境被查找出来。 |
||||
|
||||
JavaScript 用一个栈来管理执行上下文,当函数调用时,会入栈一个新的执行上下文,函数调用结束之后,执行上下文被出栈 |
||||
|
||||
this 的值存放在私有属性 \[\[ThisBindingStatus\]\] 中 |
||||
|
||||
函数创建新的执行上下文中的词法环境记录时,会根据 \[\[thisMode\]\] 来标记新纪录的 \[\[ThisBindingStatus\]\] 私有属性。代码执行遇到 this 时,会逐层检查当前词法环境记录中的 \[\[ThisBindingStatus\]\],当找到有 this 的环境记录时获取 this 的值。 |
||||
|
||||
## Completion Record / 完成标记 |
||||
|
||||
>Completion Record 是 JavaScript 中的一个规范类型,用于描述异常、跳出等语句执行过程 |
||||
|
||||
Completion Record 是语言实现者才需要关心的内容,但是我们可以从中看出一些 JavaScript 更加底层的实现逻辑,它有着三个字段分别是: |
||||
- \[\[type\]\] - 表示完成的类型,有 break continue return throw 和 normal 几种类型 |
||||
- \[\[value\]\] - 表示语句的返回值,如果语句没有,则是 empty |
||||
- \[\[target\]\] - 表示语句的目标,通常是一个 JavaScript 标签 |
||||
|
||||
JavaScript 正是依靠语句的 Completion Record 类型,方才可以在语句的复杂嵌套结构中,实现各种控制,普通语句执行后,会得到 \[\[type\]\] 为 normal 的 Completion Record,JavaScript 引擎遇到这样的 Completion Record,会继续执行下一条语句。 |
||||
|
||||
这些语句中,只有表达式语句会产生 \[\[value\]\],当然,从引擎控制的角度,这个 value 并没有什么用处,Chrome 控制台显示的正是语句的 Completion Record 的 \[\[value\]\] |
||||
|
||||
参考文章: |
||||
- http://www.ecma-international.org/ecma-262/#sec-ecmascript-specification-types |
@ -1,88 +0,0 @@
@@ -1,88 +0,0 @@
|
||||
## Undefined |
||||
|
||||
- underfined 实际上是一个变量而并非是一个关键字,这是 JavaScript 的一个设计失误,为了避免无形中被修改,建议使用 void 0 |
||||
- 在 ES5 后,underfined 在全局作用域中被设置为 read-only,但是在局部作用域中,还是会被修改 |
||||
|
||||
## String |
||||
|
||||
- JavaScript 中的字符串是永远无法变更的,一旦字符被构建出来,无法用任何方式改变字符串的内容 |
||||
- Example: |
||||
```javascript |
||||
let testString = "Hello"; |
||||
testString[0] = "X"; |
||||
console.log(testString); |
||||
``` |
||||
|
||||
## Number |
||||
|
||||
- JavaScript 中的 Number 类型有 18437736874454810627(即 2^64-2^53+3) 个值 |
||||
- NaN其实是 2^53-2 个特殊数字的合集,NaN并不是一个数,而是一堆数据合集,所以NaN ! == NaN |
||||
- 在 JavaScript 中 `0.1 + 0.2 !== 0.3` ,这是因为对于硬件系统来说,由高低电平表示1和0,这样我们使用的高级语言都要用二进制进行编码,不同的进制数在转换的时候都会有精度的问题,所以相加之后因为一些误差就导致值不相等 |
||||
- 在比较浮点数的时候,不应该使用等于或者全等,应该使用 JavaScript 提供的最小精度值 : `(Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON)` |
||||
|
||||
## Object |
||||
|
||||
- JavaScript 中的几个基本类型,都在对象类型中有一个对应的对象类型,这些基本类型是 `Number`、`String`、`Boolean`、`Symbol` |
||||
- 所以 3 与 new Number(3) 是完全不同的值,一个是 Number 类型,一个是对象类型 |
||||
- `Number`、`String`、`Boolean`,三个构造器是两用的,当和 new 搭配时会产生对象,直接调用时表示强制类型转换 |
||||
- Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器 |
||||
- JavaScript 语言在设计上视图模糊对象和基本类型之间的关系,代码中可以把对象的方法放在基本类型上使用比如 `"Hello".charAt(0)` |
||||
- 这是因为 JavaScript 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对象类型的方法 |
||||
- JavaScript高级程序设计中的解释: 为了方便操作原始值,ES提供了3种特殊的引用类型:Boolean、Number和String。这些类型具有其他引用类型一样的特点,但也具有与各自原始类型对应的特殊行为。每当用到某个原始值的方法和属性时,后台都会创建一个相应的原始包装类型的对象,从而暴露出操作原始值的各种方法。 |
||||
- 围绕原始数据类型创建一个显式包装器对象从 ECMAScript 6 开始不再被支持。 然而,现有的原始包装器对象,如 new Boolean、new String以及new Number,因为遗留原因仍可被创建 |
||||
|
||||
## 类型转换 |
||||
|
||||
- 在使用 parseInt 的时候,建议传入 parseInt 的第二个参数 |
||||
- parseFloat 则直接把原字符串作为十进制来解析,它不会引入任何的其他进制 |
||||
- 多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择 |
||||
- Object.prototype.toString.call() 能识别类型是因为其内部发生了装箱转换, |
||||
|
||||
## 装箱转换 |
||||
|
||||
- 每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类 |
||||
- 全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的 call 方法来强迫产生装箱 |
||||
|
||||
```javascript |
||||
var symbolObject = (function(){ return this; }).call(Symbol("a")); |
||||
|
||||
console.log(typeof symbolObject); //object |
||||
object console.log(symbolObject instanceof Symbol); //true |
||||
console.log(symbolObject.constructor == Symbol); //true |
||||
``` |
||||
|
||||
- 装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换 |
||||
- call 本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型,因 call 当传入值为基本类型时,会触发这个基本类型的对应的类,所以用`object.prototype.tostring` 不正确,如果是对象则可以用此方法得出该实例的类 |
||||
|
||||
## 拆箱转换 |
||||
|
||||
- 在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换) |
||||
- 拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError |
||||
|
||||
```javascript |
||||
var o = { |
||||
valueOf : () => {console.log("valueOf"); return {}}, |
||||
toString : () => {console.log("toString"); return {}} |
||||
} |
||||
|
||||
o * 2 |
||||
// valueOf |
||||
// toString |
||||
// TypeError 拆箱失败 |
||||
``` |
||||
|
||||
- 到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从 o x 2 换成 String(o),那么你会看到调用顺序就变了 |
||||
- 在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。 |
||||
|
||||
```javascript |
||||
var o = { |
||||
valueOf : () => {console.log("valueOf"); return {}}, |
||||
toString : () => {console.log("toString"); return {}} |
||||
} |
||||
|
||||
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"} |
||||
|
||||
console.log(o + "") |
||||
// toPrimitive |
||||
// hello |
||||
``` |
@ -1,57 +0,0 @@
@@ -1,57 +0,0 @@
|
||||
## import 声明 |
||||
|
||||
import 声明有两种用法,一个是直接 import 一个模块,另一个是带 from 的 import,它能引入模块里的一些信息。 |
||||
|
||||
```javascript |
||||
import "mod"; //引入一个模块 |
||||
import v from "mod"; //把模块默认的导出值放入变量v |
||||
``` |
||||
|
||||
直接 import 一个模块,只是保证了这个模块代码被执行,引用它的模块是无法获得它的任何信息的,而带 from 的 import 意思是引入模块中的一部分信息,可以把它们变成本地的变量。 |
||||
|
||||
独立的 `export` 导入相当于是一个引用,导出的变量仍然指向同一个地址(无关引用类型和值类型)。 |
||||
|
||||
而 `export default` 导出的则是一个值,但是对于引用类型而言,也是会被修改的 |
||||
|
||||
## var 声明的预处理 |
||||
|
||||
>var 在预处理阶段,不关心赋值的部分,只管在当前作用域声明这个变量 |
||||
|
||||
```javascript |
||||
var a = 1; |
||||
function foo() { |
||||
console.log(a); // undefined |
||||
|
||||
// var 会穿透一切语言结构 |
||||
if(false) { |
||||
var a = 2; |
||||
} |
||||
} |
||||
|
||||
foo(); |
||||
``` |
||||
|
||||
## function 声明 |
||||
|
||||
>function 声明表现跟 var 相似,不同之处在于,function 声明不但在作用域中加入变量,还会给它赋值。 |
||||
|
||||
看一个与之不同的例子: |
||||
|
||||
```javascript |
||||
// foo 会被提升,但不会被赋值 |
||||
console.log(foo); // undefined |
||||
|
||||
if(true) { |
||||
function foo(){ } |
||||
} |
||||
``` |
||||
|
||||
## class 声明 |
||||
|
||||
class 声明也是存在预处理的,但是它的行为会更加的符合直觉,更倾向于抛出错误 |
||||
|
||||
## 指令序言机制 |
||||
|
||||
"use strict"是 JavaScript 标准中规定的唯一一种指令序言,但是设计指令序言的目的是,留给 JavaScript 的引擎和实现者一些统一的表达方式,在静态扫描时指定 JavaScript 代码的一些特性。 |
||||
|
||||
JavaScript 的指令序言是只有一个字符串直接量的表达式语句,它只能出现在脚本、模块和函数体的最前面。 |
@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
|
||||
- [ ] JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而 JavaScript 中是无法自定义类型的,为什么? |
||||
- [ ] 什么是装箱操作? |
@ -1,19 +0,0 @@
@@ -1,19 +0,0 @@
|
||||
- `12.toString()` 会报错,这是因为 JavaScript 允许10进制 Number 省略小数点前或者后,`12.toString()` 中的 `12.` 会被当作省略了小数点后的数字,如果想要这一句正常工作,需要在中间加个空格:`12 .toString()` 或是 `12..toString()` |
||||
- 模板支持添加处理函数的写法,这时模板的各段会被拆开,传递给函数当参数: |
||||
|
||||
```javascript |
||||
function f(){ |
||||
console.log(arguments); |
||||
} |
||||
|
||||
var a = "world" |
||||
f`Hello ${a}!`; // [["Hello", "!"], world] |
||||
``` |
||||
|
||||
- JavaScript switch 语句继承自 Java,Java 中的 switch 语句继承自 C 和 C++,原本 switch 语句是跳转的变形,所以我们如果要用它来实现分支,必须要加上 break。 |
||||
- 在 C 时代,switch 生成的汇编代码性能是略优于 if else 的,但是对 JavaScript 来说,则无本质区别。 |
||||
- 在一些通用的计算机语言设计理论中,能够出现在赋值表达式右边的叫做:右值表达式(RightHandSideExpression),而在 JavaScript 标准中,规定了在等号右边表达式叫做条件表达式(ConditionalExpression),不过,在 JavaScript 标准中,从未出现过右值表达式字样。 |
||||
- JavaScript 标准也规定了左值表达式同时都是条件表达式(也就是右值表达式) |
||||
- 仅在确认 == 发生在 Number 和 String 类型之间时使用 |
||||
- 实际上我们可以把 margin 理解为“一个元素规定了自身周围至少需要的空间” |
||||
- flex项中没有flex属性,justify-content才生效 |
@ -1,82 +0,0 @@
@@ -1,82 +0,0 @@
|
||||
tag : |
||||
>表示 Fiber 的类型,根据 ReactElement 组件的 `type` 生成 |
||||
|
||||
elementType : |
||||
>大部分时候和 type 是相同的 |
||||
>FunctionComponent 使用 React.memo 会有不同 |
||||
|
||||
type : |
||||
>FunctionComponent 而言是函数本身 |
||||
>ClassComponent 而言是 Class |
||||
>Host Component 而言是 DOM 节点的 Tag Name |
||||
|
||||
key : |
||||
>和 ReactElement 的 key 属性一致 |
||||
|
||||
stateNode : |
||||
>HostComponent 而言 是它的真实 DOM 节点 |
||||
>ClassComponent 而言 是 clsss 实例 |
||||
>RootFiber而言 是 FiberRootNode |
||||
|
||||
return : |
||||
>指向父节点 |
||||
|
||||
child : |
||||
>指向第一个子节点 |
||||
|
||||
sibling : |
||||
>指向下一个兄弟节点 |
||||
|
||||
index : |
||||
>代表在多个同级 Fiber 节点中,它们插入的位置索引 |
||||
>单节点默认为 0 |
||||
|
||||
ref : |
||||
>指向在 ReactElement 组件上设置的 ref |
||||
|
||||
pendingProps: |
||||
>组件的属性,也就是 ReactElement 传入的 props |
||||
>用于和后边的 memoizedProps 属性比较判断组件属性是否发生变化 |
||||
>在生成子 Fiber 节点之后被赋值到 memoizedProps |
||||
|
||||
memoizedProps: |
||||
>上一次组件生成的属性,用于和上边的 pendingProps 进行比较 |
||||
|
||||
alternate : |
||||
>指向在内存中的另外一条 Fiber 树 |
||||
|
||||
updateQueue : |
||||
>存储 update更新对象 的队列,每次发起更新,都需要在该队列上创建一个 update 对象 |
||||
|
||||
memoizedState: |
||||
>上一次生成子组件之后组件的状态 |
||||
|
||||
dependencies: |
||||
>该 Fiber 节点所依赖的 (contexts, events) |
||||
|
||||
mode : |
||||
>和 React 的运行模式有关 |
||||
|
||||
flags : |
||||
>用于标记组件的副作用,reconciler 会将所有存在 flag 标记的 Fiber 节点添加进 effectList 链表中,交给 commit 阶段 |
||||
|
||||
subtreeFlags : |
||||
>替代 16.x 版本中的 firstEffect、lastEffect,默认未开启 |
||||
|
||||
deletions : |
||||
>存储将要被删除的子组件,默认未开启 |
||||
|
||||
nextEffect : |
||||
> 指向下一个有副作用的 Fiber 节点 |
||||
|
||||
firstEffect : |
||||
>指向副作用链表的第一个 Fiber 节点 |
||||
|
||||
lastEffect : |
||||
>指向副作用链表的最后一个 Fiber 节点 |
||||
|
||||
lanes : |
||||
> Fiber 节点的优先级 |
||||
|
||||
childLanes : |
||||
>子 Fiber 节点的优先值 |
@ -1,14 +0,0 @@
@@ -1,14 +0,0 @@
|
||||
- createRoot 函数最终会返回一个 new ReactDOMRoot 的实例,而在 ReactDOMRoot 函数中会调用 createRootImpl |
||||
- ReactDOMRoot 主要任务是给实例上的 \_internalRoot 属性赋值 |
||||
- createRootImpl 函数接收三个参数,第一个是 React 开始渲染的根节点 DOM 元素,对应 createRoot 函数的第一个参数,而第二个是当前 React 的调度模式,第三个参数是 createRoot 第二个参数 options。 |
||||
- 调度模式 RootTag: |
||||
1. LegacyRoot:0 |
||||
2. BlockingRoot:1 |
||||
3. ConcurrentRoot:2 |
||||
- 由 createRootImpl 调用 createContainer,接受 DOM 元素 和 RootTag 参数,后两个和 SSR 相关,createContainer 主要任务是调用 createFiberRoot 函数,传递的参数和 createContainer 相同 |
||||
- 在 createFiberRoot 函数内部会 new FiberRootNode 创建 FiberRootNode 节点,紧接着会调用 createHostRootFiber 并传递 RootTag 创建第一个 Fiber 节点即 rootFiber,这个 rootFiber 就是 WorkInProgress 树的第一个节点 |
||||
- createFiber 函数接受三个参数,分别是 Fiber 类型、Fiber props、key 和 mode,其中 mode 参数似乎和 RootTag 有关,最终会 new FiberNode 并返回 |
||||
- [[Fiber 数据结构]]中有个 index 属性,这个属性记录的是当前节点在同级兄弟节点当中的位置索引,diff 时会和 key 一起做比较,也就是说,如果组件不给 key 属性,实际上 Fiber 节点也会自带一个 index 属性 |
||||
- createHostRootFiber 创建完 FiberRootNode 和 rootFiber 回到 createFiberRoot 调用栈,会将创建的 FiberRootNode 的 current 属性指向刚创建的 rootFiber,即 React 双缓存机制的开始,并将 rootFiber 的 stateNode 属性赋值为 FiberRootNode,同时调用 initializeUpdateQueue 初始化 rootFiber 的 updateQueue 属性,最后返回结束 createFiberRoot 调用栈返回 FiberRootNode 回到 createContainer 再回到 createRootImpl,接着执行 markContainerAsRoot 函数 |
||||
- markContainerAsRoot 函数的任务很简单,它会在根节点 DOM 元素上添加一个属性并赋值为 rootFiber |
||||
- createRootImpl 结束后会回到 createRoot 调用栈,并返回创建的 FiberRootNode,最终进入到 jsx 方法中去 |
@ -1,35 +0,0 @@
@@ -1,35 +0,0 @@
|
||||
- 每一个 Fiber 节点都会链接它的第一个子节点、下一个兄弟节点和父节点,并且在 Fiber 节点中还会保存组件的状态和需要更新的副作用 |
||||
- React 的双缓存:每一次 React 的更新都会创建一个 workInProgress Fiber 树,current Fiber 和 workInProgress Fiber 之间使用 alternate 属性链接,方便公用属性,当 workInProgress Fiber 完成渲染,FiberRootNode 的指针就会指向 workInProgress Fiber 树的根节点 RootFiber,这时 workInProgress Fiber 就变成了 current Fiber 树 |
||||
- React 会尽量的复用 Fiber,在创建 workInProgress Fiber 时,如果 current Fiber 中节点的 alternate 属性已经指向一个 Fiber 节点,那么新创建的 workInProgress Fiber 节点就会基于这个 alternate 指向的 Fiber 节点来创建,这种基于已有 Fiber 节点做对比生成新的 workInProgress Fiber 的过程就是 diff 算法,所以首屏渲染和更新渲染最大的区别就在于是否有 diff 算法 |
||||
- 对于首屏渲染来说,只有根节点具有 current Fiber 而子节点不存在 |
||||
- 如果 render 的递归阶段,如果节点只存在一个文本子节点,那么这个子节点将不会进入 beginWork 递阶段,而是由它父节点直接进入 completeWork 归阶段,这是 React 做的一个优化,而这一个逻辑在 updateHostComponent方法中的 isDirectTextChild 可以看到 |
||||
- updateHostComponent 方法中的 reconcileChildren 方法会为当前 Fiber 节点创建它的子 Fiber 节点,也就是 Fiber 中的 child 属性 |
||||
- reconcileChildren 方法接受 current 参数,通过判断这个参数是否为 null,分别执行 mountChildFibers 或 reconcileChildFibers 方法 |
||||
- mountChildFibers 和 reconcileChildFibers 都是由 ChildReconciler 方法创建的,只是传入的布尔值会不同,而这个参数表示是否追踪副作用,mountChildFibers 为 false,reconcileChildFibers则相反 |
||||
- 以 reconcileChildFibers 为例,会对 Children 的类型做判断,对判断结果分别做相应操作 |
||||
- completeWork 会对 beginWork 创建好的 Fiber 进行填充,根据 Fiber 类型的不同有不同的处理逻辑,其中有一步就是创建真实 DOM 元素并将之前创建好的 DOM 元素插入 |
||||
- finalizeInitialChildren 为创建的 DOM 元素,插入已有的 props,内部也根据 Fiber 节点的 tag 区分不同的处理逻辑,还有对 props 是否合法的校验,甚至根据 props 的属性也做了不同逻辑的处理,最终交由 setValueForProperty 处理 |
||||
- completeWork 中的 appendAllChildren 会将创建好的真实DOM元素插入之前创建的子DOM元素 |
||||
- 对于首屏渲染,只会有一个节点被打上 effectTag,就是根节点,只需要根节点被打上 effectTag 那么就能渲染剩下的全部内容 |
||||
- beginWork 在页面更新时,会根据一些条件判断 didReceiveUpdate 的 true 或者 false,这个变量代表了,在本次更新中这个 Fiber 节点是否有变化,这些条件分别是 |
||||
1. 是否有新旧 props |
||||
2. context 是否发生变化 |
||||
3. type 是否发生变化 |
||||
- 如果条件都为否,那么 didReceiveUpdate 变成 false 之外还会判断本次更新当前 Fiber 是否存在需要执行的任务 |
||||
- 如果也没有任务需要执行,和首屏渲染进入 update 的时候不同,最终会走到 bailoutOnAlreadyFinishedWork 函数中去,这个函数最终会执行 cloneChildFibers 方法,直接克隆一个子 Fiber 节点挂载到当前 Fiber 节点的 child 上 |
||||
- 对于 Function Component 会调用 renderWithHooks 方法,这个方法会执行 Function Component 自身,返回的值就是 React.createElement 返回的 JSX 对象,这里和 Host Component 不一样 |
||||
- reconcileChildren 根据 current Fiber 树、 WorkInProgress Fiber 树和 JSX 对象来生成子 Fiber 节点 |
||||
- WorkInProgress Fiber 节点不存在 alternate 有可能表示,在上一次更新中不存在这个 Fiber 节点 |
||||
- 对于一个 Host Components 来说,如果它有属性的增删改,那么它的Fiber 的 updateQueue 属性会赋值对应的一个数组,这个数组是由两轮属性的循环生成的,属性第 i 项为属性名,第 i + 1 项为属性值,在这个数组存在的情况下,也就是属性有改变的情况下会进入 markUpdate 函数的逻辑,这个函数会为当前的 Fiber 节点打上 Update 的标记(effectTag) |
||||
- 如果一个 Fiber 节点存在 effectTag,那么它会其他包含 effectTag 的 Fiber 节点链接形成一个链表,在 commit 阶段只需要遍历这个链表就能找出需要变动的 Fiber 节点 |
||||
- 在 commit 节点遍历 effectList 的操作叫做 mutation |
||||
- ClassComponent 的 getSnapshotBeforeUpdate 生命周期是在 before mutation 阶段被调用的 |
||||
- useEffect 的回调函数会在 before mutation 阶段会以普通优先级被调度,然后在 commit 阶段执行完毕之后再异步执行 |
||||
- commit 阶段开始于 commitRoot 函数,这个函数内部会执行 runWithPriority 函数,该函数接收两个参数,第一个是调度的优先级,第二个是调度的回调函数,在这个函数中触发的任务调度都会以第一个参数传递的优先级执行 |
||||
- 如果一个 FunctionComponent 内部有需要执行的 useEffect ,那么这个 FunctionComponent 的 Fiber 节点就会被打上 PassiveEffect 的 effectTag |
||||
- commitRootImpl 函数开头的fec do..while 循环是为了判断当前 Fiber 是否还有还未执行的 useEffect,如果有会再次执行 flushPassiveEft 函数 |
||||
- commitRootImpl 内会处理一些离散事件,然后会重置在 render 阶段使用的一些全局变量,然后会处理包含 effectList 链表:因为 effectList 只会处理根节点之下的子节点,所以这里要判断根节点是否存在 effectTag 然后将其挂载到 effectList 的末尾 |
||||
- commitBeforeMutationEffect、commitMutationEffects、commitLayoutEffects 三个函数分别代表了 mutation 的三个不同阶段 |
||||
- commitRootImpl 中有许多与 Interaction 相关的逻辑,这些逻辑和 React 的性能追踪有关,会在 React DevTool 中使用 |
||||
- 在 commit 阶段的结尾,也就是 commitRootImpl 函数的结尾,由于在 commit 阶段可能会产生新的更新,所以在这里会将整个应用的根节点重新调度一次 |
||||
- React 会将一些同步的更新放在一个叫 SyncCallbackQueue 的队列中,每次执行 flushSyncCallbackQueue 函数就会执行整个队列中的同步任务,比如 useLayoutEffect 中触发的 setState |
@ -1,59 +0,0 @@
@@ -1,59 +0,0 @@
|
||||
>React 从 16 开始,对底层架构做了一次重构,和 15 不同,渲染 vdom 的时候一改以往的递归执行,引入了一个新的概念:Fiber,虽然最后渲染到页面的时候仍然是递归,但是靠 Fiber 实现的递归是可中断的,根据优先级由浏览器优先执行任务,保证在大量视图需要更新的时候,浏览器仍然能保证快速的响应 |
||||
|
||||
在 React 中,视图的更新使用了双缓存的方式,也就是说在 React 运行时,同时有着两棵 Fiber 树,一颗是当前视图上的 Fiber 树,叫 current,另外一棵是存在内存当中的下一次视图更新时用的叫做 workInProgress,React 在构建时会创建整个 app 唯一的根 Fiber 节点,叫做 FiberRootNode,这个节点上有一个 current 指针,指向的是当前正在页面上显示的 Fiber 树也就是 current,当 workInProgress 递归生成完毕,指针会立即指向 workInProgress ,而旧的 current 则会在下一次渲染中变成 workInProgress ,就这样循环交替完成页面的递归渲染。 |
||||
|
||||
Fiber 递归开始首先会交由 createWorkInProgress 函数生成 WorkInProgress 的第一个 Fiber 节点,这个节点就是 FiberNode,所以我们也会从 createWorkInProgress 函数开始讲解 React 中 Fiber 递归的流程,**这个函数的主要工作就是根据传入的 Fiber 节点判断复用 Fiber 节点还是创建新的 Fiber 节点**,而这个判断的条件就是该 Fiber 节点的 alternate 属性是否存在。 |
||||
|
||||
节点的 alternate 属性不存在有两种可能: |
||||
1. 首屏刷新 |
||||
2. 当前 Fiber 节点属性新增的节点 |
||||
|
||||
如果判断 alternate 不存在,会进入到 createFiber 函数:即执行创建新 Fiber 节点的逻辑。 |
||||
|
||||
否则就会进行 Fiber 复用。 |
||||
|
||||
```javascript |
||||
function createWorkInProgress(current, pendingProps) { |
||||
if (workInProgress === null) { |
||||
// current 的 alternate 属性不存在会执行 createFiber 函数逻辑 |
||||
// 表示当前的 Fiber 节点不存在对应的 WorkInProgress Fiber |
||||
// 在首屏渲染和新增DOM节点的情况下,alternate 是会不存在的 |
||||
workInProgress = createFiber( |
||||
current.tag, |
||||
pendingProps, |
||||
current.key, |
||||
current.mode |
||||
); |
||||
// 赋值同名属性 |
||||
workInProgress.elementType = current.elementType; |
||||
workInProgress.type = current.type; |
||||
workInProgress.stateNode = current.stateNode; |
||||
|
||||
// 通过 alternate 属性互相链接 |
||||
workInProgress.alternate = current; |
||||
current.alternate = workInProgress; |
||||
} |
||||
|
||||
...... |
||||
} |
||||
``` |
||||
|
||||
这个函数具体的深入解析可以看 [[React 的深入探索 - createWorkInProgress]]。 |
||||
|
||||
createWorkInProgress 执行完毕之后,我们就有了一个 WorkInProgress Fiber 节点,接下来就会交给 beginWork 和 completeWork 开始正式的 Fiber 递归,在之后的 beginWork 流程中,进入 Bailout 逻辑之后也有可能会进入到 createWorkInProgress 函数逻辑中。 |
||||
|
||||
在 React 中 Fiber 的创建使用递归实现的**深度优先遍历**算法,即尽可能深的探索树的分支,探索完毕后再回溯,在这一过程中负责探索阶段的就是 beginWork 函数。 |
||||
|
||||
**beginWork 执行在递归节点的 Fiber 创建之前,主要是为传入的 Fiber 节点创建第一个子 Fiber 节点**,其内部可以看作一个大的 switch 语句,根据传入的 Fiber 节点的子元素类型不同执行不同的 Fiber 创建逻辑,不管父元素拥有多少个子元素,它最终都会创建并返回第一个子元素的 Fiber 节点,直到并不存在子元素为止,[[React 的深入探索 - beginWork]]。 |
||||
|
||||
如果探索到了可达的最后一个子元素,那么就会结束探索阶段,进入回溯阶段:执行completeWork 函数逻辑。 |
||||
|
||||
completeWork 负责深度优先遍历中的回溯阶段,**它执行在递归节点的 Fiber 创建之后,主要负责完善创建好的 Fiber 节点和插入其真实 DOM 树**,首屏渲染中首先进入该函数的可能是最小的不存在子元素的 Fiber 节点,也可能是只存在一个文本子元素的夫节点,因为 React 对这样的元素做了一些优化,它的子元素并不会进入 beginWork 函数逻辑,而是直接交由 beginWork 进行 Fiber 填充。 |
||||
|
||||
和 beginWork 中相同,completeWork 的主要逻辑也是一个巨大的 switch,根据 Fiber 节点的类型进入不同的处理逻辑 [[React 的深入探索 - completeWork]] |
||||
|
||||
如果最终回溯到了 Fiber 的起始节点,那么整个首屏渲染的 Fiber 递归渲染逻辑就完成了,最后会交由 commitRoot 函数进入 commit 阶段将其渲染到页面上:[[React 的流程解析 - commit阶段]] |
||||
|
||||
这里你可能会有一个疑惑,在 Fiber 递归阶段也就是 reconciler 阶段,会深度优先遍历找出所有的存在变化的 Fiber 节点,并将其打上对应的 effectTag,**那么进入 commit 阶段后,也需要再次进行一次深度优先遍历找出这些存在 effectTag 的 Fiber 节点吗?** |
||||
|
||||
其实是不需要的,因为这样的效率太低了,在深度优先遍历的回溯阶段也就是 completeWork 的执行逻辑中,会将所有存在 effectTag 的 Fiber 节点通过单项链表的形式连接起来,这样在 commit 阶段的时候只需要遍历这一条链表就能快速的找到发生了变化的 Fiber 节点:[[React 的深入探索 - effectList 链表]] |
@ -1,250 +0,0 @@
@@ -1,250 +0,0 @@
|
||||
>beginWork 执行在递归节点的 Fiber 创建之前,主要是为传入的 Fiber 节点根据类型创建第一个子 Fiber 节点 |
||||
|
||||
[代码位置](https://github.com/facebook/react/blob/bd4784c8f8c6b17cf45c712db8ed8ed19a622b26/packages/react-reconciler/src/ReactFiberBeginWork.old.js#L3818) |
||||
|
||||
```javascript |
||||
function beginWork(current, workInProgress, renderLanes) { |
||||
|
||||
// 前 beginWork 阶段 |
||||
if (current !== null) { |
||||
...... |
||||
} else { |
||||
...... |
||||
} |
||||
|
||||
// 正式 beginWork 阶段 |
||||
switch (workInProgress.tag) { |
||||
...... |
||||
} |
||||
} |
||||
``` |
||||
|
||||
beginWork 函数接受三个参数,分别是 current 节点,workInProgress 节点和 renderLanes 优先级,根据 beginWork 函数的结构,我们可以分成两个阶段,分别是:前 beginWork 阶段和正式 beginWork 阶段 |
||||
|
||||
## 前 beginWork 阶段 |
||||
|
||||
```javascript |
||||
if (current !== null) { |
||||
...... |
||||
} else { |
||||
didReceiveUpdate = false; |
||||
} |
||||
``` |
||||
|
||||
在进入正式 beginWork 阶段之前,会先对传入的 current 节点进行空值判断,根据 current 是否为空进入不同的处理逻辑。 |
||||
|
||||
**那么为什么需要判断 current 是否为空呢?答案是为了性能,前面说到 beginWork 函数的主要任务就是给当前传入的 Fiber 节点创建它的第一个子 Fiber 节点,要是在上次更新和本次更新中当前的 Fiber 节点并没有发生变化,那么还需要再次创建一个新的 Fiber 节点吗?肯定是不需要的,所以我们只需要将这个已经存在的并没有发生变化的 Fiber 节点拿过来复用就行了,而这段逻辑正是在前 beginWork 阶段中判断并执行的** |
||||
这一段代码的主要目的是为了赋值 didReceiveUpdate 变量,这个变量表示在本次更新中当前 current 节点是否存在变化 |
||||
|
||||
### current 不为空 |
||||
|
||||
在 current 不为空的逻辑中,会先取出先前存储在 Fiber 节点中的新旧 props,连同其他几个判断条件一起做 if 判断,判断条件如下: |
||||
1. 用三等判断 props 是否发生变化 |
||||
2. 检查 context 是否发生变化 |
||||
3. 检查新旧 Fiber type 是否发生变化 |
||||
|
||||
如果判断条件为 true 则会给 didReceiveUpdate 变量赋值为 true,进入正式 beginWork 阶段 |
||||
|
||||
```javascript |
||||
var oldProps = current.memoizedProps; |
||||
var newProps = workInProgress.pendingProps; |
||||
|
||||
if ( |
||||
// 对比新旧 props |
||||
oldProps !== newProps || |
||||
// 检查 context 是否发生变化 |
||||
hasContextChanged() || |
||||
// 判断 Fiber type 是否发生变化 |
||||
(workInProgress.type !== current.type ) |
||||
) { |
||||
didReceiveUpdate = true; |
||||
} else { |
||||
...... |
||||
} |
||||
``` |
||||
|
||||
如果判断条件为 false,会进入后续判断逻辑 |
||||
|
||||
此时会先调用 checkScheduledUpdateOrContext 函数检查 current 是否存在优先级相关的更新,关于 React 优先级相关我们先暂且按表不谈,进入 if 判断 |
||||
|
||||
- 不存在优先级相关的更新且 workInProgress 节点不存在 DidCapture flag |
||||
- True:跳过后续的正式 beginWork 阶段,进入 baliout 也就是组件复用逻辑 |
||||
- False:判断当前 current 节点是否存在 ForceUpdateForLegacySuspense flag |
||||
- True:didReceiveUpdate 赋值为 true |
||||
- False:didReceiveUpdate 赋值为 false |
||||
|
||||
```javascript |
||||
// props 和 context 都没有发生变化,检查优先级相关 |
||||
var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes); |
||||
|
||||
if ( |
||||
// 不存在优先级相关的更新c |
||||
!hasScheduledUpdateOrContext && |
||||
// workInProgress 节点上不存在 DidCapture flag |
||||
(workInProgress.flags & DidCapture) === NoFlags |
||||
) { |
||||
didReceiveUpdate = false; |
||||
// 这里会跳过正式 beginWork 阶段,进入 baliout 逻辑也就是组件复用 |
||||
return attemptEarlyBailoutIfNoScheduledUpdate( |
||||
current, |
||||
workInProgress, |
||||
renderLanes |
||||
); |
||||
} |
||||
|
||||
// current 节点不存在 ForceUpdateForLegacySuspense flag |
||||
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) { |
||||
didReceiveUpdate = true; |
||||
} else { |
||||
didReceiveUpdate = false; |
||||
} |
||||
``` |
||||
|
||||
### current 为空 |
||||
|
||||
首先 didReceiveUpdate 会被赋值为 false,紧接着会进入一个似乎关于 SSR 服务端渲染的判断逻辑,代码内容如下,这一段代码的行为还不清楚,暂时先跳过 |
||||
|
||||
```javascript |
||||
didReceiveUpdate = false; |
||||
// 检查 hydrate 状态 和 是否存在 Forked flag |
||||
if (getIsHydrating() && isForkedChild(workInProgress)) { |
||||
// 后续的逻辑似乎和 SSR 服务端渲染有关 |
||||
// 根据官方的注释,这边的代码似乎是为了给 React 的并发模式铺路 |
||||
var slotIndex = workInProgress.index; |
||||
var numberOfForks = getForksAtLevel(); |
||||
pushTreeId(workInProgress, numberOfForks, slotIndex); |
||||
} |
||||
``` |
||||
|
||||
至此整个前 beginWork 阶段就结束了,我们可以在这里做个小小的总结:**在前beginWork 阶段主要是判断当前组件是否发生变化需要更新, 是否可以复用,在满足复用条件情况下会跳过 正式beginWork 阶段进入 baliout 逻辑而不再创建新的 Fiber 节点,从中可以看到对于特殊组件如 Suspense 而言可能并不会进入 baliout 逻辑** |
||||
|
||||
在此之外我们也可以看到一些很有意思的点:**对于组件新旧 props 对比使用的是简单的三等判断** |
||||
|
||||
## 正式 beginWork 阶段 |
||||
|
||||
正式 beginWork 阶段开始,会将 workInProgress 的 lanes 清空,接着会进入一个 switch 逻辑,根据 tag 不同进入对应的 case 处理逻辑,代码如下: |
||||
|
||||
```javascript |
||||
workInProgress.lanes = NoLanes; |
||||
|
||||
switch (workInProgress.tag) { |
||||
case IndeterminateComponent: ... |
||||
case LazyComponent: ... |
||||
// Function Component 处理逻辑 |
||||
case FunctionComponent: ... |
||||
// Class Component 处理逻辑 |
||||
case ClassComponent: ... |
||||
case HostRoot: ... |
||||
case HostComponent: ... |
||||
case HostText: ... |
||||
// Suspense 处理逻辑 |
||||
case SuspenseComponent: ... |
||||
case HostPortal: ... |
||||
case ForwardRef: ... |
||||
case Fragment: ... |
||||
case Mode: ... |
||||
case Profiler: ... |
||||
case ContextProvider: ... |
||||
case ContextConsumer: ... |
||||
case MemoComponent: ... |
||||
case SimpleMemoComponent: ... |
||||
case IncompleteClassComponent: ... |
||||
case SuspenseListComponent: ... |
||||
case ScopeComponent: ... |
||||
case OffscreenComponent: ... |
||||
case LegacyHiddenComponent: ... |
||||
case CacheComponent: ... |
||||
} |
||||
``` |
||||
|
||||
switch 中的 case 逻辑太多了,全部都写出来会让笔记显得特别繁杂,这里只写出几个常用的处理逻辑如 HostComponent、FunctionComponent,其他处理逻辑后边有时间再另起一篇文章阐述 |
||||
|
||||
### updateHostRoot |
||||
|
||||
首先会执行 pushHostRootContext 函数,这个函数与 context 有关,暂且不谈 |
||||
|
||||
然后会接着执行 cloneUpdateQueue 方法 |
||||
|
||||
**cloneUpdateQueue** |
||||
|
||||
这个方法比较简单,会判断 current 和 workInProgress 中的 updateQueue 是否相同,如果相同会创建新的对象重复赋值以清除引用,这里是为了保证后续对 workInProgress 的操作不会影响到 current |
||||
|
||||
```javascript |
||||
function cloneUpdateQueue(current, workInProgress) { |
||||
var queue = workInProgress.updateQueue; |
||||
var currentQueue = current.updateQueue; |
||||
|
||||
// 用三等对比更新队列,如果为 true 表示 queue 还未清除引用 |
||||
if (queue === currentQueue) { |
||||
// 清除引用 |
||||
var clone = { |
||||
baseState: currentQueue.baseState, |
||||
firstBaseUpdate: currentQueue.firstBaseUpdate, |
||||
lastBaseUpdate: currentQueue.lastBaseUpdate, |
||||
shared: currentQueue.shared, |
||||
effects: currentQueue.effects |
||||
}; |
||||
// 赋值 |
||||
workInProgress.updateQueue = clone; |
||||
} |
||||
} |
||||
``` |
||||
|
||||
从 cloneUpdateQueue 函数出来回到 updateHostRoot 调用栈,会紧接着执行 processUpdateQueue 方法,这个方法和更新队列有关,暂时不展开讲 |
||||
|
||||
```javascript |
||||
function updateHostRoot(current, workInProgress, renderLanes) { |
||||
// context 相关 |
||||
pushHostRootContext(workInProgress); |
||||
// 取出 updateQueue |
||||
var updateQueue = workInProgress.updateQueue; |
||||
if (current === null || updateQueue === null) { |
||||
throw new Error("..."); |
||||
} |
||||
|
||||
// 取出组件新 props |
||||
var nextProps = workInProgress.pendingProps; |
||||
// 取出组件 state |
||||
var prevState = workInProgress.memoizedState; |
||||
// 从 state 中取出 element |
||||
var prevChildren = prevState.element; |
||||
cloneUpdateQueue(current, workInProgress); |
||||
processUpdateQueue(workInProgress, nextProps, null, renderLanes); |
||||
var nextState = workInProgress.memoizedState; |
||||
var root = workInProgress.stateNode; |
||||
|
||||
{ |
||||
var nextCache = nextState.cache; |
||||
pushRootCachePool(root); |
||||
pushCacheProvider(workInProgress, nextCache); |
||||
|
||||
if (nextCache !== prevState.cache) { |
||||
propagateContextChange( |
||||
workInProgress, |
||||
CacheContext, |
||||
renderLanes |
||||
); |
||||
} |
||||
} |
||||
|
||||
...... |
||||
} |
||||
``` |
||||
|
||||
再往后是一些针对服务端渲染的一些处理逻辑,服务端渲染也不是这次讨论的目的,也先跳过 |
||||
最终调用 reconcileChildren 为 FIber 创建一个子 Fiber 节点并返回 |
||||
|
||||
至此一个节点的 beginWork 流程就走完了,下一次会根据是否存在子 Fiber 节点判断是执行当前 WorkInProgress Fiber 节点的 completeWork ,还是继续对子节点执行 beginWork |
||||
|
||||
|
||||
### beginWork |
||||
|
||||
第一次触发更新,进入这个函数的逻辑前边和首屏渲染是都是一样的,但是在判断组件是否发生修改的时候逻辑和首屏渲染并不同,在首屏渲染,beginWork 会走到 switch 逻辑,并根据 Fiber tag 不同分别执行不同的 mount l逻辑,在这里会经过一些判断最终进入 attemptEarlyBailoutIfNoScheduledUpdate 这个函数,这个函数内部又有着一个 switch 逻辑,也是根据 workInProgress tag 属性分别进入不同的 case,在这个例子中进入的是 HostRoot 对应的 case,首先会执行 pushHostRootContext 函数,Context 相关 ~~不谈~~,最后继续处理服务端渲染的逻辑最后返回交由 bailoutOnAlreadyFinishedWork 函数处理,bailoutOnAlreadyFinishedWork 会对 WorkInProgress 赋上 current 的 dependencies 属性,然后就是 bailoutOnAlreadyFinishedWork 最终的目的: 调用 cloneChildFibers |
||||
|
||||
### cloneChildFibers |
||||
|
||||
这个函数从名字上就能看出来,目的是为了 clone Fiber 的 child 属性,这个函数会调用 createWorkInProgress 函数,从上边可以知道,这个函数可以根据传入的 current 和 WorkInProgress 进行判断是否创建一个新的 Fiber 节点或者复用已有的 current Fiber 节点,并将其互相链接,最终将结果返回,cloneChildFiber 接收到返回的 Fiber 子节点,将其链接到当前 WorkInProgress Fiber 的 child 属性上,并把 Fiber 子节点的 return 赋值 WorkInProgress Fiber |
||||
|
||||
也就是说,如果在 beginWork 中当前,如果组件并没有发生变化且没有要执行的逻辑,那么他会直接 clone 已有的 Fiber 节点,而不会走到 reconcileChildren 函数创建新的 Fiber 节点 |
||||
|
||||
最后返回到 bailoutOnAlreadyFinishedWork 在由它返回到 beginWork,beginWork 流程执行结束 |
@ -1,84 +0,0 @@
@@ -1,84 +0,0 @@
|
||||
>completeWork 执行在递归节点的 Fiber 创建之后,主要是为创建好的 Fiber 节点插入内容和插入真实 DOM 树 |
||||
|
||||
函数开始,会执行 popTreeContext 并传入 workInProgress,这个函数应该也和 Context 相关,然后进入到 swtich 逻辑,根据 WorkInProgress Fiber 节点的 tag 属性进入不同的 case 逻辑,这里和 beginWork 基本上类似 |
||||
React 的遍历顺序是从父到子,最终再从子回到父,所以首屏渲染中首先进入 completeWork 的 WorkInProgress 不一定会是 FiberNode ,在这里是 HostComponent |
||||
进入到 HostComponent 的 case 之后,又执行了一遍 popTreeContext,官方的注释也写明似乎是有一些考虑在 |
||||
在 HostComponent 的 case 中,会判断 current 和 WorkInProgress.stateNode 是否为空,在首屏渲染的 completeWork 节点,这里都是空的,所以进入为空的逻辑:先检查当前 Fiber 节点是否存在 props 属性,最后调用 createInstance 创建一个 DOM 实例 |
||||
createInstance 会调用 createElement 方法创建一个 DOM 实例,并调用 precacheFiberNode 和 updateFiberProps 方法,这两个方法都是在插入属性,一个在当前 WorkInProgress Fiber 节点上插入 DOM,一个是在 DOM 上插入 props,最后将处理好的 DOM 返回 |
||||
|
||||
### appendAllChildren |
||||
函数首先会判断 WorkInProgress 的 child 属性是否为空,如果不为空就进入 while 循环,因为这里是首屏渲染的第一次 completeWork 所以进入到一定是最小的子组件,不会存在 child 属性,所以会跳过循环,也就会跳过插入逻辑,回到 completeWork 的调用栈中 |
||||
|
||||
然后回到 completeWork 的调用栈,它会将处理完毕的 DOM 示例插入到 WorkInProgress 的 stateNode 属性中,然后会调用 inalizeInitialChildren ,这个函数会为创建的 DOM 元素,插入已有的 props,内部也根据 Fiber 节点的 tag 区分不同的处理逻辑,还有对 props 是否合法的校验,甚至根据 props 的属性也做了不同逻辑的处理,最后会返回一个 Boolean 用于判断是否进入 markUpdate 逻辑,这个函数会把 Fiber 打上 Update 标记 |
||||
最后再执行 bubbleProperties 进行一些处理,~~不谈~~ 整个 completeWork 的流程就走完了 |
||||
|
||||
|
||||
### completeWork |
||||
|
||||
和首屏渲染不同,这次 current 和 WorkInProgress.stateNode 不为空,所以会进入和首屏渲染时不一样的逻辑:调用 updateHostComponent 函数 |
||||
|
||||
### updateHostComponent |
||||
|
||||
函数内部会对当前 WorkInProgress Fiber 节点的新旧 props 进行对比,如果完全相同会直接返回,这里的完全相同实际上是指引用地址也相同,所以在本次就算新旧 props 相同也并会被 return,然后取出 Fiber 中的 stateNode 传递给 prepareUpdate 函数,然后被调用 diffProperties 函数,这里还有一个针对 props 的 children 属性是否为字符串或者数字的判断,暂时还不清楚具体目的是什么 |
||||
|
||||
### diffProperties |
||||
|
||||
函数开始会先执行对 props 属性的校验方法:`validatePropertiesInDevelopment` 方法,然后根据 Fiber tag 进入不同的 case,这里只有针对三种 tag有特殊的处理,分别是:input、select 和 textarea,本次进入 completeWork 的 Fiber 节点 tag 为 img,所以不会进入 特殊处理的 case,最终进入到 default 逻辑,然后被直接 break,去往 assertValidProps 函数 |
||||
这里还有一个针对 Fiber 旧 props 属性 onClick 不是 function 且 新 props 属性 onClick 是 function 的一个特殊处理:就是直接给 DOM 的 onclick 属性赋值为一个空函数 ,从官方留下的注释中大概可以知道,这似乎是为了修复 Safari 浏览器的问题的特殊处理 |
||||
|
||||
### assertValidProps |
||||
|
||||
函数一开始会检查 props 是否存在,不存在直接 return |
||||
然后就是一大串的 if 用于对 props 是否合法的兜底操作,比如说 dangerouslySetInnerHTML 属性和 children 属性智能存在一个之类的,如果找到不合法的操作会直接通过 throw Error 被抛出 |
||||
|
||||
之后就进入 diffProperties 最主要的任务:对新旧 props 进行对比然后生成 updateQueue |
||||
这里有两个 for in 循环:循环旧 `props` 属性和循环新 `props` 属性 |
||||
|
||||
**警告:以下解析受限于我的表达能力,可能会有读起来一头雾水的情况,建议和源码一起看** |
||||
|
||||
循环旧 props 属性: |
||||
- 以下条件跳出当前循环: |
||||
1. 新 props 自身不存在相同属性 |
||||
2. 属性非旧 props 自身属性 |
||||
3. 该属性在旧 props 的值为 null |
||||
- 当找到 style 属性,会遍历 style 属性,初始化 `styleUpdates` 对象为空,并在上边新增当前 style 属性的 key 并赋值空字符串 |
||||
- 当找到一些特殊属性如 `dangerouslySetInnerHTML` 等,会初始化 `updatePayload` 为空数组 |
||||
- 如果没有以上属性,即没有 style 和 特殊属性,会进入最后的 else 逻辑:给 `updatePayload` 数组 `push` 进属性名和 null,进入下一轮循环 |
||||
|
||||
循环新 props 属性: |
||||
- 以下条件跳出当前循环: |
||||
1. 新 props 自身不存在当前属性 |
||||
2. 新 props 属性值和旧 props 属性值相同(引用相同) |
||||
3. 新旧 props 属性值都为 null |
||||
- 当找到 style 属性: |
||||
- 如果新 props 属性值存在且值合法(就是转换成 boolean 不为 false),会调用 Object.freeze 函数冻结当前属性值 |
||||
- 如果旧 props 也存在 style 属性且不等于 null: |
||||
- 开始遍历旧 props style |
||||
- 如果出现旧 `props style` 有 style 样式,但是新 `props style` 没有的也就是新增样式的情况,那么会初始化 `styleUpdates` 对象为空(如果已经初始化过就会跳过),并在上边新增当前 style 属性的 key 并赋值空字符串 |
||||
- 开始遍历新 props style |
||||
- 如果找到新 `props style` 属性存在,但是与旧 `props style` 属性不同,也就是样式发生修改的情况,那么会初始化 `styleUpdates` 对象为空(如果已经初始化过就会跳过),并在上边对当前 style 属性的 key 做新增或是修改,值为新 `props style` 属性 |
||||
- 如果旧 `props` 不存在 `style 属性` 或者等于 `null`: |
||||
- 如果 `styleUpdates` 不为 `null`,且是数组时长度不等于 0,给 `updatePayload` `push` 进 `style` 和 `styleUpdates` |
||||
- 否则给 `styleUpdates` 赋值 `props style` 属性 |
||||
- 当找到特殊属性 `dangerouslySetInnerHTML` : |
||||
- 会与旧 props 的 `dangerouslySetInnerHTML` 对比,发生变化则 push 进 updatePayload 数组 |
||||
- 当找到 children 属性,会将其转换为字符串和 属性名一起 `push` 进 `updatePayload` |
||||
- 当找到除 `dangerouslySetInnerHTML` 之外的特殊属性,会对其进行专属的逻辑 |
||||
- 最后没有找到以上属性,会进入最后的 else 逻辑:给 `updatePayload` 数组 `push` 进 props 属性名和 对应的属性值,进入下一轮循环 |
||||
|
||||
当所有的循环结束会判断 styleUpdates 是否合法(不为 null,且数组长度不为0),合法则进入将调用 validateShorthandPropertyCollisionInDev 函数,传递 styleUpdates 和 新 props style 属性,最后将 `style 字符串` 和 `styleUpdates` 推入 `updatePayload` |
||||
返回 `updatePayload` |
||||
|
||||
回到 `updateHostComponent` 函数,将 `prepareUpdate` 也就是 `diffProperties` 返回的 `updatePayload` 赋值给 `workInProgress` 的 `updateQueue` 属性,如果它不为空则执行 `markUpdate` 函数 |
||||
|
||||
### markUpdate |
||||
为传入的 workInProgress 打上 Update 标记 |
||||
|
||||
返回 null,回到 `completeWork` 的调用栈,如果 current ref 和 workInProgress ref 不同,调用 markRef 函数 |
||||
|
||||
### markRef |
||||
为传入的 workInProgress 打上 Ref 和 RefStatic 标记 |
||||
|
||||
最后调用 bubbleProperties 函数,这又是一个比较长的函数,~~暂且不谈~~ |
||||
|
||||
completeWork 逻辑结束 |
@ -1,77 +0,0 @@
@@ -1,77 +0,0 @@
|
||||
>这个函数的任务是创建 WorkInProgress 树的 Fiber 节点,根据传入参数的判断是复用已有的 Fiber 节点或是创建新的 Fiber 节点 |
||||
|
||||
createWorkInProgress 在 Fiber 递归开始前和进入 Bailout 逻辑的时候都会被触发,React 为了提高性能会尽可能的复用 Fiber,如果当前的 current 节点已经存在一个链接的 WorkInProgress 节点,那么新创建的 WorkInProgress 就会基于这个已有的节点来创建。 |
||||
|
||||
逻辑开始会先判断传入的 Fiber 节点是否存在 alternate 属性。 |
||||
|
||||
```javascript |
||||
// 函数接受两个参数:current 节点和节点的 props 属性 |
||||
function createWorkInProgress(current, pendingProps) { |
||||
var workInProgress = current.alternate; |
||||
if (workInProgress === null) { |
||||
...... |
||||
} else { |
||||
...... |
||||
} |
||||
} |
||||
``` |
||||
|
||||
根据属性是否存在会进入不同的新 Fiber 创建逻辑: |
||||
|
||||
## 创建新 Fiber 节点 |
||||
|
||||
```javascript |
||||
workInProgress = createFiber( |
||||
current.tag, |
||||
pendingProps, |
||||
current.key, |
||||
current.mode |
||||
); |
||||
|
||||
// 赋值同名参数 |
||||
workInProgress.elementType = current.elementType; |
||||
workInProgress.type = current.type; |
||||
workInProgress.stateNode = current.stateNode; |
||||
|
||||
// 通过 alternate 属性互相链接 |
||||
workInProgress.alternate = current; |
||||
current.alternate = workInProgress; |
||||
``` |
||||
|
||||
可以看到 Fiber 的创建逻辑主要是调用了 createFiber 函数,而这个函数的逻辑也很简单: |
||||
|
||||
### createFiber |
||||
|
||||
```javascript |
||||
var createFiber = function (tag, pendingProps, key, mode) { |
||||
// 实例化一个初始的 Fiber 对象并返回 |
||||
return new FiberNode(tag, pendingProps, key, mode); |
||||
}; |
||||
``` |
||||
|
||||
主要任务就是实例化一个初始的 Fiber 对象并返回,然后在 createWorkInProgress 接下来的逻辑中会对一些存在 current 节点的属性进行复用 |
||||
|
||||
## 复用已有 Fiber 节点 |
||||
|
||||
|
||||
从以上的判断逻辑出来会进入一大串 Fiber 属性赋值的逻辑,代码太长就不贴了,其中有一个 switch 语句,是为 Fiber type 属性赋值对应的 Component Type: |
||||
|
||||
```javascript |
||||
switch (workInProgress.tag) { |
||||
case IndeterminateComponent: |
||||
case FunctionComponent: |
||||
case SimpleMemoComponent: |
||||
workInProgress.type = resolveFunctionForHotReloading(current.type); |
||||
break; |
||||
|
||||
case ClassComponent: |
||||
workInProgress.type = resolveClassForHotReloading(current.type); |
||||
break; |
||||
|
||||
case ForwardRef: |
||||
workInProgress.type = resolveForwardRefForHotReloading(current.type); |
||||
break; |
||||
} |
||||
``` |
||||
|
||||
最后返回创建好的 WorkInProgress Fiber 树,至此 createWorkInProgress 的逻辑就结束了。 |
@ -1,5 +0,0 @@
@@ -1,5 +0,0 @@
|
||||
在 React Fiber 的 completeWork 阶段,React 会将所有被标记上 effectTag 的 Fiber 节点通过一个单向链表给连接起来,这样在 commit 阶段的时候,只需要遍历这一条链表就能快速更新页面 |
||||
|
||||
如果一个 Fiber 节点在 completeWork 阶段抛出异常,那么它的父 Fiber 节点会被打上 Incomplete 标记,表示当前的父 Fiber 下的子 Fiber 树没有完成构建 |
||||
|
||||
这部分的代码相当的抽象,而且在React 后续的更新中,这一功能的实现逻辑被重构了,所以这一篇会尽可能的讲解被重写之前 React 生成 effectList 的逻辑,仅当作学习记录用,最新的处理逻辑以 React 最新的代码为准 |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
>在 React 开始阶段会创建整个 app 唯一的根 Fiber 节点:FiberRootNode |
@ -1,230 +0,0 @@
@@ -1,230 +0,0 @@
|
||||
React 的 commit 阶段从 commitRoot 这个函数开始,从 performSyncWorkOnRoot 函数中被调用,接收一个名为 root 的参数,这个 root 就是从 Fiber 递归中完成递归流程的 WorkInProgress Fiber 树 |
||||
|
||||
commitRoot 在 React 18 的代码和 React 17 有着较大的不同,但是最终的目的都是类似的:调用 commitRootImpl 函数,在 React 17 中,commitRootImpl 和一个优先级一起作为参数交由 runWithPriority 函数,而 React 18 中则是直接执行 commitRootImpl 函数 |
||||
|
||||
commitRoot 的代码很少,其中最主要的是执行 commitRootImpl 函数,也就是说 commit 阶段最核心的任务就发生在 commitRootImpl 中,在 React 18 中,commitRootImpl 函数发生了比较大的变化,以往 17 中,有三个主要的循环,这三个循环主要代表了 commit 的阶段的三个时刻:分别是 before、mutation 和 layout,也对应了三个函数 commitBeforeMutationEffect、commitMutationEffects 和commitLayoutEffects,18 中这三个函数依旧存在,但已经不是在循环被执行了,以下的文章内容会先从 React 17 开始,然后再探讨在 React 18 中发生的变化~~如果我不懒的话~~ |
||||
|
||||
commitRootImpl 内部有许多名字中带有 Interactive 的函数,这些函数逻辑和性能追踪有关,这篇文章里面会直接跳过 |
||||
|
||||
## React 17 |
||||
|
||||
进入 commitRootImpl 函数内部,首先会执行一个 do..while 循环 |
||||
|
||||
```javascript |
||||
do { |
||||
flushPassiveEffects(); |
||||
} while (rootWithPendingPassiveEffects !== null); |
||||
``` |
||||
|
||||
这个循环的跳出条件是 rootWithPendingPassiveEffects 等于 null,不然就执行 flushPassiveEffects 函数,那么 rootWithPendingPassiveEffects 是什么?为什么要执行 flushPassiveEffects 函数,这个 rootWithPendingPassiveEffects 就是带有 PassiveEffects 标记的链表,而 flushPassiveEffects 函数内部会遍历这个链表,然后执行其各自内部的 useEffect 中的回调函数,也就是说开头的循环是为了检查是否存在还未执行的 useEffect 回调函数,而这些回调函数有可能触发新的渲染,所以需要遍历直到没有任务 |
||||
|
||||
接下来会执行 flushRenderPhaseStrictModeWarningsInDEV 函数,这个函数从名字上看出之和开发环境下有关,负责 React 中的 StrictMode |
||||
|
||||
```javascript |
||||
flushRenderPhaseStrictModeWarningsInDEV(); |
||||
``` |
||||
|
||||
中间跳过一段逻辑无关和 performance 监控代码,进入对 Fiber Tree 和 effecrList 的一系列初始化 |
||||
|
||||
```javascript |
||||
...... |
||||
|
||||
root.finishedWork = null; |
||||
root.finishedLanes = NoLanes; |
||||
|
||||
root.callbackNode = null; |
||||
root.callbackPriority = NoLane; |
||||
|
||||
// 内部位运算 |
||||
var remainingLanes = mergeLanes( |
||||
finishedWork.lanes, |
||||
finishedWork.childLanes |
||||
); |
||||
|
||||
// 内部也是位运算 |
||||
markRootFinished(root, remainingLanes); |
||||
|
||||
if (rootsWithPendingDiscreteUpdates !== null) { |
||||
if ( |
||||
!hasDiscreteLanes(remainingLanes) && |
||||
rootsWithPendingDiscreteUpdates.has(root) |
||||
) { |
||||
rootsWithPendingDiscreteUpdates.delete(root); |
||||
} |
||||
} |
||||
|
||||
if (root === workInProgressRoot) { |
||||
workInProgressRoot = null; |
||||
workInProgress = null; |
||||
workInProgressRootRenderLanes = NoLanes; |
||||
} |
||||
|
||||
// 以下省略 |
||||
``` |
||||
|
||||
然后会开始处理 effectList,因为之前 completeWork 生成 effectList 的时候并没有处理 FiberNode ,所以这里需要判断 FiberNode 是否存在 effectTag,并将其加入到 effectList 的末尾 |
||||
|
||||
而这个 firstEffect,就会作为接下来三个阶段中被遍历的 effectList |
||||
|
||||
```javascript |
||||
var firstEffect; |
||||
if (finishedWork.effectTag > PerformedWork) { |
||||
if (finishedWork.lastEffect !== null) { |
||||
finishedWork.lastEffect.nextEffect = finishedWork; |
||||
firstEffect = finishedWork.firstEffect; |
||||
} else { |
||||
firstEffect = finishedWork; |
||||
} |
||||
} |
||||
else { |
||||
firstEffect = finishedWork.firstEffect; |
||||
} |
||||
``` |
||||
|
||||
最终走到 commitRootImpl 的第一个主要循环 |
||||
|
||||
```javascript |
||||
do { |
||||
{ |
||||
invokeGuardedCallback(null, commitBeforeMutationEffects, null); |
||||
if (hasCaughtError()) { |
||||
if (!(nextEffect !== null)) { |
||||
{ |
||||
throw Error( "Should be working on an effect." ); |
||||
} |
||||
} |
||||
var error = clearCaughtError(); |
||||
captureCommitPhaseError(nextEffect, error); |
||||
nextEffect = nextEffect.nextEffect; |
||||
} |
||||
} |
||||
} while (nextEffect !== null); |
||||
``` |
||||
|
||||
这个循环内部由 invokeGuardedCallback 执行 commitBeforeMutationEffects 函数,commitBeforeMutationEffects 就是开头说到的负责 before 阶段的函数,具体函数的深入可以看 [[React 的深入探索 - commitBeforeMutationEffects]] |
||||
|
||||
然后根据 hasCaughtError 函数的返回值,执行 captureCommitPhaseError 函数,这个函数和 React 的 Error Boundaries (错误边界)有关,这里不展开谈,后边的两个阶段的逻辑里边也有着类似的逻辑,从这里可以看出来,Error Boundaries 会捕获 commit 阶段的错误 |
||||
|
||||
循环的跳出条件是 nextEffect 等于 null,也就是这个循环会遍历 effectList,后边的两个主要循环的跳出条件也是相同的 |
||||
|
||||
跳出 before 阶段的循环之后进入第二个主要循环:mutation |
||||
|
||||
```javascript |
||||
do { |
||||
{ |
||||
invokeGuardedCallback( |
||||
null, |
||||
commitMutationEffects, |
||||
null, |
||||
root, |
||||
renderPriorityLevel |
||||
); |
||||
if (hasCaughtError()) { |
||||
if (!(nextEffect !== null)) { |
||||
{ |
||||
throw Error( "Should be working on an effect." ); |
||||
} |
||||
} |
||||
var _error = clearCaughtError(); |
||||
captureCommitPhaseError(nextEffect, _error); |
||||
nextEffect = nextEffect.nextEffect; |
||||
} |
||||
} |
||||
} while (nextEffect !== null); |
||||
``` |
||||
|
||||
和 before 的循环非常类似,commitMutationEffects 也是有 invokeGuardedCallback 调用,也有着相同 Error Boundaries 的逻辑 |
||||
|
||||
关于 commitMutationEffects: [[React 的深入探索 - commitMutationEffects]] |
||||
|
||||
最后一个阶段:layout 阶段 |
||||
|
||||
```javascript |
||||
do { |
||||
{ |
||||
invokeGuardedCallback( |
||||
null, |
||||
commitLayoutEffects, |
||||
null, |
||||
root, |
||||
lanes |
||||
); |
||||
if (hasCaughtError()) { |
||||
if (!(nextEffect !== null)) { |
||||
{ |
||||
throw Error( "Should be working on an effect." ); |
||||
} |
||||
} |
||||
var _error2 = clearCaughtError(); |
||||
captureCommitPhaseError(nextEffect, _error2); |
||||
nextEffect = nextEffect.nextEffect; |
||||
} |
||||
} |
||||
} while (nextEffect !== null); |
||||
``` |
||||
|
||||
关于 commitLayoutEffects: [[React 的深入探索 - commitLayoutEffects]] |
||||
|
||||
结束三个循环之后 commit 阶段并没有结束,还会进入接下来的逻辑 |
||||
|
||||
```javascript |
||||
var rootDidHavePassiveEffects = rootDoesHavePassiveEffects; |
||||
|
||||
if (rootDoesHavePassiveEffects) { |
||||
// 本次更新存在 useEffect |
||||
rootDoesHavePassiveEffects = false; |
||||
// 将 root 赋值给 rootWithPendingPassiveEffects,没错就是开头的循环 |
||||
rootWithPendingPassiveEffects = root; |
||||
pendingPassiveEffectsLanes = lanes; |
||||
pendingPassiveEffectsRenderPriority = renderPriorityLevel; |
||||
} else { |
||||
// 本次更新不存在 useEffect |
||||
nextEffect = firstEffect; |
||||
while (nextEffect !== null) { |
||||
var nextNextEffect = nextEffect.nextEffect; |
||||
// 循环设置为 null,目的是为了垃圾回收 |
||||
nextEffect.nextEffect = null; |
||||
if (nextEffect.effectTag & Deletion) { |
||||
detachFiberAfterEffects(nextEffect); |
||||
} |
||||
nextEffect = nextNextEffect; |
||||
} |
||||
} |
||||
``` |
||||
|
||||
从官方留下的注释中可以明白,这一段代码主要是是否进入了无限循环的更新当中 |
||||
|
||||
```javascript |
||||
if (remainingLanes === SyncLane) { |
||||
// Count the number of times the root synchronously re-renders without |
||||
// finishing. If there are too many, it indicates an infinite update loop. |
||||
// 翻译:计算根节点未完成同步重新呈现的次数。如果有太多,则表示无限更新循环。 |
||||
if (root === rootWithNestedUpdates) { |
||||
nestedUpdateCount++; |
||||
} else { |
||||
nestedUpdateCount = 0; |
||||
rootWithNestedUpdates = root; |
||||
} |
||||
} else { |
||||
nestedUpdateCount = 0; |
||||
} |
||||
``` |
||||
|
||||
这段代码是为了将当前的 root 重新调度一次,是因为在 commit 阶段有可能会产生新的更新 |
||||
|
||||
```javascript |
||||
ensureRootIsScheduled(root, now()); |
||||
``` |
||||
|
||||
React 内会存在一些同步的更新(useLayoutEffect 中的触发更新),React 会将此放在 flushSyncCallbackQueue 函数中在 commit 阶段同步的执行 |
||||
|
||||
```javascript |
||||
flushSyncCallbackQueue(); |
||||
``` |
||||
|
||||
至此, React 17 中 commit 阶段发生的事情就结束了 |
||||
|
||||
|
||||
|
||||
## React 18 |
||||
**TODO** |
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
before Mutation 阶段主要处理三件事情: |
||||
1. 处理用户离散事件 |
||||
2. 执行 commitBeforMutationEffectOnFiber 函数 |
||||
3. 判断 Fiber 是否存在 Passive 的标记,有的话就执行 flushPassiveEffects 回调函数 |
||||
|
||||
### commitBeforMutationEffectOnFiber(commitBeforeMutationLifeCycles) |
||||
内部会根据 Fiber 的 tag 进入不同的处理逻辑,以 ClassComponents 为例,如果 Fiber 节点上有 Snapshot 的标记,那么会通过 Fiber stateNode 属性取到 ClassComponent 实例执行 getSnapshotBeforeUpdate 这个生命周期函数 |
||||
|
||||
如果一个 Fiber 存在 Passive 标记,以 FunctonComponent 为例,那么它会将 flushPassiveEffects 作为回调函数传递给 scheduleCallback 函数以普通优先级进行调度,flushPassiveEffects 中就是 FunctonComponent 的 useEffect |
||||
|
||||
从这里可以看到,整个 commit 阶段是同步执行的,但是 useEffect 的回调函数会传递给 scheduleCallback 函数异步执行,所以 useEffect 回调是在 commit 阶段结束之后以异步优先级进行执行 |
@ -1,25 +0,0 @@
@@ -1,25 +0,0 @@
|
||||
commitMutationEffects 对应 commit 中的 layout 阶段,leyout 阶段会遍历执行 commitLayoutEffect 方法,之前提到的 React 双缓存机制中切换 current 指针的操作也存在于这一步中[[React 的流程解析 - Fiber 递归]] |
||||
这段代码会在 mutation 阶段之后,layout 阶段之前执行 |
||||
|
||||
### commitLayoutEffect |
||||
调用 commitLayoutEffectOnFiber |
||||
当 Fiber 存在 ref 标记,会执行 commitAttachRef 函数用于处理 ref 属性 |
||||
|
||||
### commitLayoutEffectOnFiber/commitLifeCycles |
||||
ClassComponents 组价的 componentDidMount 和 componentDidUpdate 生命周期函数在这个函数内会被执行 |
||||
|
||||
### commtHookEffectListMount |
||||
遍历 effectLayout 依次执行它们 useLayoutEffect 的回调函数,这些步骤都是同步执行的 |
||||
|
||||
### schedulePassiveEffects |
||||
内部调用 enqueuePendingPassiveHookEffectUnmount 函数 和 enqueuePendingPassiveHookEffectMount 函数 |
||||
|
||||
### enqueuePendingPassiveHookEffectUnmount |
||||
将 useEffect 的销毁函数和 Fiber 节点一起 push 进 pendingPassiveHookEffectsUnmount 队列 |
||||
|
||||
### enqueuePendingPassiveHookEffectMount |
||||
将 useEffect 的回调函数和 Fiber 节点一起 push 进 pendingPassiveHookEffectsMount 队列 |
||||
|
||||
### commitAttachRef |
||||
|
||||
### commitUpdateQueue |
@ -1,84 +0,0 @@
@@ -1,84 +0,0 @@
|
||||
commitMutationEffects 对应 commit 中的 mutation 阶段,这个方法内部是一个 while 循环,遍历 effectList 链表,遍历到的每一个 Fiber 节点首先会判断是否存在 ConentReset 标记,这个标记表示 Fiber 是否需要重置文本节点 |
||||
|
||||
### commitResetTextContent |
||||
|
||||
然后会判断是否存在 Ref 标记 |
||||
|
||||
### commitDetachRef |
||||
|
||||
然后进入 mutation 阶段最重要的逻辑:判断 Fiber 阶段是否存在以下 effectTag |
||||
1. Placement 插入DOM |
||||
2. Update 更新属性 |
||||
3. Deletion 删除 DOM 节点 |
||||
4. Hydrating SSR 相关 |
||||
|
||||
然后根据 effectTag 不同进入不同的处理逻辑 |
||||
|
||||
## Placement |
||||
|
||||
commitPlacement |
||||
如果当前环境不支持 mutation 会直接返回,ReactDOM 下是支持的 |
||||
首先会根据当前的 Fiber 节点,找到其最近的 Host 类型的父 Fiber 节点,Host 类型包括 HostComponent、HostRoot、HostPortal 和 FundamentalComponent,这几种类型有一个共同点:它们都有对应的 DOM 节点 |
||||
找到之后先进行各自的前置处理逻辑 |
||||
|
||||
### getHostParetFiber |
||||
|
||||
一直递归向上查找,直到找到 HostComponent 为止 |
||||
|
||||
如果父 Fiber 节点上存在 ConentReset 标记,就要先执行 resetTextConent 函数,然后会找到当前 Fiber 节点的 Host 类型的兄弟节点 |
||||
|
||||
### getHostSibling |
||||
该方法内部有着一个嵌套循环,因为兄弟 HostComponents 的查找可能是跨层级的 |
||||
|
||||
为什么要找到最近的兄弟节点 HostComponent? |
||||
- 因为 DOM 的插入有两种方法,第一种是 insertBefore 方法,第二种是 appendChild |
||||
- 使用 insertBefore 时需要找到兄弟节点 |
||||
- 使用 appendChild 时需要找到父节点 |
||||
|
||||
### insertInContainerBefor |
||||
|
||||
内部实际上还是使用了 insertBefore 方法 |
||||
|
||||
### appendChildToContainer |
||||
内部实际上还是使用了 appendChild 方法 |
||||
|
||||
## PlacementAndUpdate |
||||
|
||||
先调用 commitPlacement 方法,接着调用 commitWork |
||||
|
||||
### commitWork |
||||
|
||||
与 Function 有关的类型,会调用 commitHookEffectListUnmount |
||||
|
||||
### commitHookEffectListUnmount |
||||
|
||||
会调用 useLayoutEffect 的销毁函数,内部会遍历 EffectList,如果包含传入的 tag,当前是 HookLayout,也就是内部存在 useLayoutEffect 的函数组件,那么会执行它们 useLayoutEffect 的回调函数,也就是 useLayoutEffect 的 return |
||||
|
||||
在执行任意 useEffectLayout 的回调函数之前,会先执行所有 useEffectLayout 的销毁函数 |
||||
|
||||
HostComponent 组件,会调用 commitUpdate 方法 |
||||
|
||||
### commitUpdate |
||||
且接收的 updatePayload 参数就是当前 Fiber 组件的 updateQueue 属性 |
||||
内部最终会调用 updateProperties 函数来更新 DOM 的 props |
||||
|
||||
|
||||
## Deletion |
||||
|
||||
会执行 commitDeletion 函数 |
||||
|
||||
### commitDeletion |
||||
如果支持 mutation,那么会调用 unmountHostComponents |
||||
|
||||
### unmountHostComponents |
||||
|
||||
### commitNestedUnmount |
||||
递归删除 Fiber 子节点 |
||||
|
||||
### commitUnmount |
||||
|
||||
对于 FunctionComponent 类型的组件,需要执行 enqueuePendingPassiveHookEffectUnmount 函数,也就是注册需要被执行的 useEffect 回调函数 |
||||
|
||||
对于 ClassComponents 类型的组件,会执行它的 componentWillUnmount 生命周期函数 |
||||
|
||||
对于 HostComponents 类型的组件,会解绑它的 ref 属性 |
@ -1,22 +0,0 @@
@@ -1,22 +0,0 @@
|
||||
>commitRootImpl 是 React commit 阶段非常重要的一个函数,这个阶段完成了对 effectList 的遍历和页面渲染,这个函数处理可以分为三个阶段,分别是:before、mutation 和 layout |
||||
|
||||
进入 commitRootImpl 函数,首先会进入 do..while 循环内执行 flushPassiveEffects,直到 rootWithPendingPassiveEffects 不等于 null 才会跳出循环 |
||||
|
||||
### flushPassiveEffects |
||||
这个函数在 useEffect 和 useLayoutEffect 阶段会用到; TODO |
||||
|
||||
### flushRenderPhaseStrictModeWarningsInDEV |
||||
从名字可以看出是开发环境相关的函数,似乎是针对 Strict 模式;TODO |
||||
|
||||
### markCommitStarted |
||||
|
||||
### markCommitStopped |
||||
|
||||
重置 root 也就是全局唯一根节点 FiberRootNode 的 finishedWork 和 finishedLanes |
||||
重置 root 也就是全局唯一根节点 FiberRootNode 的 callbackNode 和 callbackPriority |
||||
|
||||
### mergeLanes |
||||
|
||||
### markRootFinished |
||||
|
||||
如果 FiberRootNode 和 workInProgressRoot 相等,重置 workInProgressRoot 、workInProgress 和 workInProgressRootRenderLanes,~~这里没有看懂~~ |
@ -1,15 +0,0 @@
@@ -1,15 +0,0 @@
|
||||
- [x] 什么是双缓存?React 是如何实现双缓存的 |
||||
React 运行时自始至终都存在着两颗 Fiber 树,一颗叫做 Current 另一棵叫 WorkInProgress |
||||
- [x] JSX 和 Fiber 的关系 |
||||
首屏渲染时 JSX 是创建 Fiber 节点的依据,更新渲染时,JSX 会和 current Fiber 树中的节点做对比生成 workInProgress Fiber |
||||
- [x] React Components 与 React Element 的关系 |
||||
Components 会作为 React.createElement 的第一个参数,也就是 type 参数 |
||||
- [x] 什么是深度优先遍历 |
||||
[[算法之美 - 深度优先遍历]] |
||||
- [ ] 什么是按位或? |
||||
- [ ] 为什么 React 要尽可能的复用 Fiber, 是因为创建新的 Fiber 非常消耗性能吗? |
||||
- [x] 是不是在 React 运行时中,自始至终都存在两个 Fiber Tree,只是他们的名字会来回交换,一会我是 current 一会他是 current ? |
||||
是的 |
||||
- [ ] reconcileChildren 的具体功能? |
||||
- [x] reconciler 阶段会深度优先遍历找出所有需要更新或者发生更改的 Fiber 节点,然后遍历出完整的Fiber,然后作为参数传递给 commitRoot 函数进入 commit 阶段,那么在 commit 阶段也要对 Fiber 树进行深度优先遍历吗? |
||||
[[React 的深入探索 - effectList 链表]] |
@ -1,23 +0,0 @@
@@ -1,23 +0,0 @@
|
||||
>相信就算过去很久很久,每次回首的时候2021都会是一个相当难忘的一年,这一年发生了很多的事情,每一篇都值得我单独写一篇长长的文章,但奈何词藻匮乏没法支撑起心里的波涛汹涌。 |
||||
|
||||
今年年初,刚刚踏出校园的我压力是很大很大的,有来自家里的也有来自自身的,但都指向一点:我需要找一份工作。其实找一份工作很简单,但是找一份心仪的满意的真的很难很难,自己非常清楚自己的能力界限在哪里,所拥有的能力还不足以支撑我找到一份专业对口的工作,拿着一份东拼西凑的简历,和一点点浅薄的前端知识,带着那些从视频中临时学到的所谓“前端必备基础”,就出来找工作了,顶着焦虑在 BOSS 直聘上不断地投递前端相关的工作,大多数都如同石沉大海一般没有一丝波澜,真的非常让人灰心丧气,但就和瞎猫碰上死耗子一样,这个世界上免不了有那么几只“瞎猫”—— 我得到了一个对口工作的面试。刚收到面试邀请的时候其实我是非常紧张的,想去又畏惧的矛盾心理一直来回反复横跳,最后迫于现实的压力和面试机会的宝贵,我还是选择了正面面对,第一份面试的机会好巧不巧在家附近,没有一点互联网产业痕迹的小镇上找到一家专业对口的公司真的是很让我惊讶的一件事。面试的那天其实我什么也没有准备,连临时抱佛脚去刷前端“面经”都没有,现在回过头来看,不知道是太过于自信还是已经是一副破罐子破摔的心态去面对。第一次坐在面试官面前,心情很忐忑,有些怯场,手心已经开始出汗了,大脑飞快地运转思考接下来可能会面对的问题,我想我那个时候的耳朵应该已经红透了吧,面试官问我的问题我依稀还记得一些,问了我一些 Vue 框架的使用和开发时候可能会遇到的情景题,其实考察的知识点也很简单,就是非常非常基础的JS和框架知识,问题并没有多少深度,但是很遗憾,缺乏实际开发经验的我没有一个能答得上来,后来面试官问我薪资的时候我甚至没有底气说出我期望的薪酬,悻悻而归。 |
||||
|
||||
这一次面试失利,无疑是对自信心的又一次打击,让本来就像被压着一座大山一样的我更加的喘不过气来。好在运气不错,没过多久我就又收到了一个面试邀请,这次的公司地址在离家比较远的地方,我需要坐快三个小时的车才能到那个公司,因为害怕面试的时候迟到,所有我提前了好几个小时,到达公司大门的时候甚至还没有到约定好的时间,这一次面试出乎意料的顺利,与其说是顺利不如说是在面试的时候几乎没有问我什么问题,问了我对 React 和 Socket.io 的了解程度,我说我不知道,这是实话,我那时对前端框架的了解程度仅限于知道一个叫 Vue 的框架和仅仅会使用而已,React 之流仅仅存在于“听到过”的层次,但是我的回答好像并没有对面试官的判断造成什么影响,他似乎并不关心我是否熟悉这些技术栈,转而开始和我讨论薪资,没错,我通过了面试,在我还在一脸懵逼的时候 |
||||
|
||||
“每周单休,试用期 3K,时长一个月,转正加 1K,半年后涨到 5K,而且涨薪时间可以酌情提前”,随着这一段话的话音落下,我的第一个与专业相关的 offer 就这样拿到了手,说完他还补充了两句:“我们这里是包住的,但是不包吃”,似乎是怕我有所顾虑,紧接着说:“但是饭堂的饭菜很便宜”,走出面试办公室,面试官带着我参观公司的产品,这是一家做无人零售的初创公司,面试官本人就是公司的技术主管,一手包办了公司里面的前端和后端技术,在那时的我眼里无疑就是大佬级别的存在。 |
||||
|
||||
回到家里,心里在犹豫和纠结,我清楚我现在的能力再拿到一个 offer 是非常困难的,现在的机会错过可能就要再找好久了,但是一方面又对这个 offer 的技术栈感到畏惧,毕竟是一个完全不熟悉的技术,万一我学不会怎么办,万一我干不了怎么办,就这样想去和不想去的想法在心里不断地交织,最后还是对新技术的渴望战胜了其他的担忧(这是真的,决定的一大原因就是因为对 React 和 Webscoket 的好奇),我最后接下了这个 offer。 |
||||
|
||||
刚入职的时候真的非常的痛苦,因为技术的贫瘠,看代码就像是在看天书一样,一个很小很小的功能我足足改了一天,接到的任务其实都是很简单的小任务,但是每一个都让我压力倍增,在那段时间里,甚至晚上做梦都会梦到在敲代码在解决任务,然后被惊醒吓出一身冷汗,我甚至好几次的怀疑自己是不是不适合程序员这条路,每天在边学边用的环境下工作,其实这还不是那个时候的最困难的事情,最困难的是无边无际的孤独,实际上我并不是第一次走出家门在外工作,但是全身都被孤独笼罩的时候很容易让人变得手足无措,那段时间我喜欢在晚上出去骑自行车,这是一个很不明智的选择,人在晚上会变得容易想很多事情,会让孤独感变得越来越深。 |
||||
|
||||
这样的日子持续了快一个多月,这要感谢我的宝宝,那个时候每天最期待的事情就是周末和她见面充电,安慰我陪我度过难关,慢慢的我对工作上的业务开始熟悉,写代码也脱离现学现卖的窘境开始能游刃有余起来,借助上班时间的空闲我得以有时间能更加深入的学习工作所需要的技术栈,对欠缺的基础进行查缺补漏,除此之外在下班时间,在晚上我也不再一个人骑自行车,而是和朋友一起,工作和健身两不误,每天都有着不错的运动量,日子一天比一天好了起来,但是我并没有就此停下脚步,因为早在我决定入职的那天起,我就想好了跳槽的时间,朝着更高的薪资上努力,是的,我并不满足于现在的条件,在我的展望中,有着更加宏大的目标,我必须朝着那个目标不断地前进,就这样我开始了“带薪学习”,完成工作任务的情况下完善自己的知识体系朝着更加深入的方向前进,我对 React 的理解越来越深,对它的哲学和思想愈发的认可,对 JS 的使用愈发的熟练,可能是领导看到了我对业务的熟悉,也可能是同事的接连离职,他在5月底的时候主动给我提前涨薪了,但是这并没能收买到我,因为在我的计划中,薪资要比这要高得多,我始终相信自己可以。 |
||||
|
||||
在6月份的时候,我切身的感受到了新冠带来的影响和心理上的恐惧,公司对面的医院出现了一个确诊病例,全市开始了核酸检查,第一次排了4个小时的队伍,第一次捅嗓子做核酸,但是就算如此我的每日自行车计划也没有停止,和朋友在骑行过程的谈资还多了一个关于疫情,只是每周最期待的周末去找女朋友的日程被迫终止了,学校封校,学生不允许外出,给本来就灰暗疫情生活再加了点难过。 |
||||
|
||||
疫情持续了快一个月,在7月中旬我向领导提了下个月的辞职,他似乎已经猜到了我的离职,没有做过多的挽留,就这样我的离职进入倒计时阶段,说来也很有意思,早在入职的时候就已经计划好了离职的时间,并且在工作的这几个月里边也无数次的出现想要离职的念头,但在终于要走的时候总会有些许不舍。日子过的很快,就这样来到8月份,到了离职的时间,花了点时间收拾完宿舍里的东西,走之前回头看了一眼住了半年多的宿舍,想到不出意外的话以后都不会再回来了,竟然还有点伤感,叫了辆顺风车把东西搬回家,打算给自己放一个短暂的小假。 |
||||
|
||||
休息最多是肉体上的,精神上并没有得到缓解,我需要找到下一份工作缓解这种心情上的焦虑,大概休息了两三天之后,没能压住心中的焦虑,我又开始找工作,都说面试是检验自己这段时间是否有进步的最好方法之一,面试中的我不再答非所问,而是非常流畅自然的回答出面试官提出的技术问题,甚至在询问自己的薪资要求的时候也非常有底气,同时我也感觉到在这一座并没多少互联网味道的城市里,我似乎快要触摸到了上限:面试官问的问题同质化严重,问的知识点大同小异,这实际上是很值得我思考的问题,最后,经过两个星期的不断面试,我如愿以偿的拿到了一个满意的 offer,说我很满意的原因除了薪资之外最重要的是它的待遇在那时的我看起来相当不错:是我心心念念的双休和不加班。 |
||||
|
||||
找到工作之后紧接着就需要找一个居住的地方,这是我第一次租房子自己一个人生活,脱离父母一个人在外,经过一番纠结最终在生活质量和经济质量中选择了妥协经济,就这样我的新生活开始了。 |
||||
|
||||
都说在一年的年度总结里需要展望一下未来,但是实际上这篇文章最后完工的时候已经到22年的7月份了,是的你没有听错,我在22年已经过了一半多的时候才写完了对21年的回顾,要问我对22年有什么展望,不如说是问我对于22下半年有什么计划,我希望我能更加的深入 JS 深入前端这个领域,多去了解自己感兴趣的事物,有空的话能在下半年的时候带着女朋友出去走走,去看看这个缤纷多彩的世界,同时也希望在写22年年度总结的时候不要再拖那么久:) |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
在其他同龄人的大学生活还未结束的时候,我却过早的离开了校园,离开校园的每一刻我都在怀念曾经在校园里的时光,只可惜时光 |
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
小学的时候我就喜欢看书,什么书都喜欢看,什么冒险小虎队马小跳男生日记女生日记笑猫日记我都不挑,周末的时候没有什么事情干的时候就会在家附近的书店坐上一天只因为那里可以免费看书还不赶人,最猛的时候一天我能看完两三本,经常看到姐姐来叫我回家吃饭,但是你要说我那时候的语文成绩好不好其实也说不上好,就好像看过的书没有一点体现在了试卷上。 |
||||
直到有一天好像是三年级的时候,老师布置了一个语文作业要我们回家要写一篇作文回来,题材是什么我现在已经记不清了,那时我对作文的影响可能还仅仅停留在多写成语的阶段,但是我挑了一个很有意思题材:我打算写一下家里的电脑,然后我就大笔一挥在纸上写下了作文的标题:初识电脑。 |
||||
内容其实没有什么特别的,就是回忆了在家里第一次买电脑的时候内心的波涛汹涌和第一印象,但是我写着写着就好像进入了一个境界,越写越停不下来,笔下的文字就像是水里的鱼一样自然而然地跃动到纸面上,丝毫没有之前写作文绞尽脑汁都想不出两三个句子窘迫,最后停笔收尾一气呵成,我就玩去了。 |
||||
作文交上去过了那么两三天,到了发回作文的时候,老师高兴的站在舞台上说:今天要表扬一位同学,他把作文写的非常的生动和通顺,下课的时候同学们可以去借他的作文学习学习,而这位同学就是我,瞬间我感觉春风满面,是我之前都没有得到过的荣誉,下课了班里的同学都来找我要作文“观摩”,听着同学们的称赞,我把作文纸留在桌子上,然后一个人走出教室,颇有一种深藏功与名的感觉 |
||||
|
||||
然后我就在所谓的“文学”路上一去不复返了 |
@ -1,8 +0,0 @@
@@ -1,8 +0,0 @@
|
||||
>这篇文章是对 [the-super-tiny-compiler](https://github.com/YongzeYao/the-super-tiny-compiler-CN) 的学习记录,旨在通过这篇文抛砖引玉,进入编译器世界的大门,能对编译器有个基本的认知 |
||||
|
||||
大部分编译器的工作可以被分解为三个主要阶段: |
||||
1. 解析(Parsing):将源代码转换为一个更抽象的形式 |
||||
2. 转化(Transformation):接受解析产生的抽象形式并且操纵这些抽象形式做任何编译器想让它们做的事 |
||||
3. 代码生成 (Code Generation):基于转换后的代码表现形式(code representation)生成目标代码 |
||||
|
||||
## 解析(Parsing) |
Before Width: | Height: | Size: 271 KiB |
Before Width: | Height: | Size: 193 KiB |
Before Width: | Height: | Size: 296 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 173 KiB |
Before Width: | Height: | Size: 191 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 39 KiB |
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
>JavaScript 的函数是一个老生常谈的问题,把这个关键词放在搜索引擎上能搜出大量相关的文章和解释,有无数的大佬前辈阐述的已经非常清楚,这篇文章实际上也只是 “走前辈路,述前人语” |
||||
|
||||
我喜欢箭头函数比普通函数更多,因为我认为和普通函数相比,箭头函数更加的“纯粹”,我们这里先简单的从作用域和 this 的角度来讨论这个话题。 |
||||
|
||||
## This |
||||
|
||||
对于 JavaScript 深入学习过的朋友应该都知道,**箭头函数并没有自身的 this 绑定,它会继承外层函数的 this 绑定**,而普通函数,this 的指向那就是千变万化,这也是很多 JS 初学者的噩梦,也很喜欢出现在面试官的嘴中, |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
>这篇文章是 [Executing shell commands from Node.js](https://2ality.com/2022/07/nodejs-child-process.html) 学习过程中的个人总结和笔记,大部分内容实际来自于原文章,如果你对 Nodejs 中的文件系统感兴趣,我更建议你直接去看原文章。 |
@ -1,340 +0,0 @@
@@ -1,340 +0,0 @@
|
||||
>这篇文章是 [Working with the file system on Node.js](https://2ality.com/2022/06/nodejs-file-system.html) 学习过程中的个人总结和笔记,大部分内容实际来自于原文章,如果你对 Nodejs 中的文件系统感兴趣,我更建议你直接去看原文章。 |
||||
|
||||
## Node.js 文件系统 APIs 具有多种不同的风格 |
||||
|
||||
- 同步风格的函数调用 `fs.readFileSync(path, options?): string | Buffer` |
||||
- 异步风格的函数调用 |
||||
- 异步回调 `fs.readFile(path, options?, callback): void` |
||||
- 异步 Promise `fsPromises.readFile(path, options?): Promise<string|Buffer>` |
||||
|
||||
它们在名称上很容易区分: |
||||
- 异步回调的函数名,如 `fs.readFile()` |
||||
- 异步 Promise 的函数名和异步回调的函数名相同,但是在不同的模块中,如 `fsPromises.readFile()` |
||||
- 对于同步的函数名,则是在函数结尾加上 Sync 字样,如 `fs.readFileSync` |
||||
|
||||
|
||||
## 访问文件的方式 |
||||
|
||||
1. 可以通过字符串的方式读写文件 |
||||
2. 可以打开读取流或写入流,并将文件分成更小的块,一次一个。流只允许顺序访问 |
||||
3. 可以使用文件描述符或文件句柄,通过一个类似流的 API 获得顺序或是随机访问 |
||||
- 文件描述符 (File descriptors) 是一个用于表示文件的整数,Nodejs 中有很多函数可以管理文件描述符 |
||||
4. 只有同步风格和异步回调风格的 API 使用文件描述符,对于异步 Promise 风格的 API,则有着更加抽象的类实现:文件句柄类 (class FileHandle),它基于文件描述符 |
||||
|
||||
|
||||
## 文件系统中的一些重要类 (Important classes) |
||||
|
||||
### URLs:字符串文件系统路径的替代方案 |
||||
|
||||
当 Nodejs 的一个文件系统 API 支持字符串形式的路径参数,那么它通常也会支持 URL 的实例 |
||||
|
||||
```javascript |
||||
fs.readFileSync('/tmp/text-file.txt') |
||||
fs.readFileSync(new URL('file:///tmp/text-file.txt')) |
||||
``` |
||||
|
||||
手动转换文件路径看起来很容易,但是实际上隐藏着许多坑,所以更加推荐以下函数对路径进行转换: |
||||
- [`url.pathToFileURL()`](https://nodejs.org/api/url.html#urlpathtofileurlpath) |
||||
- [`url.fileURLToPath()`](https://nodejs.org/api/url.html#urlfileurltopathurl) |
||||
|
||||
### [`Buffer`](https://nodejs.org/api/buffer.html):在 Nodejs 中表示固定长度的字节序列,它是 Uint8Array 的子类,更多的在二进制文件中使用 |
||||
|
||||
由于 Buffer 实际上是 Uint8Array 的子类,和 URL 与 String 的关系相同,当 Nodejs 接受一个 Buffer 的同时,它也接受 Uint8Array,考虑到 Uint8Array 具有跨平台(cross-platform) 的特性,很多时候可能会更加合适 |
||||
|
||||
Buffers 可以做一件 Uint8Array 不能做到的事:以各种编码方式对文本进行编码和解码,如果我们需要在 Uint8Array 中编码或解码 UTF-8,我们可以使用 `class TextEncoder` 或是 `class TextDecoder` |
||||
|
||||
|
||||
## 文件系统中的文件读和写 |
||||
|
||||
### 读取文件并将其拆分为行 |
||||
|
||||
>这里将会用两个读取文件并按行拆分的例子演示 fs.readFile 和 stream 读取的差异 |
||||
|
||||
**[`fs.readFileSync(filePath, options?)`](https://nodejs.org/api/fs.html#fsreadfilesyncpath-options)** |
||||
|
||||
```javascript |
||||
import * as fs from 'node:fs'; |
||||
fs.readFileSync('text-file.txt', {encoding: 'utf-8'}) |
||||
``` |
||||
|
||||
这种方法的优缺点: |
||||
- 优点:使用简单,在大多数情况下可以满足需求 |
||||
- 缺点:对大文件而言并不是一个好的选择,因为它需要完整的读取完数据 |
||||
|
||||
按行拆分 |
||||
|
||||
```javascript |
||||
// 拆分不包含换行符 |
||||
const RE_SPLIT_EOL = /\r?\n/; |
||||
function splitLines(str) { |
||||
return str.split(RE_SPLIT_EOL); |
||||
} |
||||
// ['there', 'are', 'multiple', 'lines'] |
||||
splitLines('there\r\nare\nmultiple\nlines'); |
||||
|
||||
// 拆分包含换行符 |
||||
const RE_SPLIT_AFTER_EOL = /(?<=\r?\n)/; |
||||
function splitLinesWithEols(str) { |
||||
return str.split(RE_SPLIT_AFTER_EOL); |
||||
} |
||||
// ['there\r\n', 'are\n', 'multiple\n', 'lines'] |
||||
splitLinesWithEols('there\r\nare\nmultiple\nlines') |
||||
``` |
||||
|
||||
**stream** |
||||
|
||||
原文章中使用的是跨平台的 `web stream`,且声明了两个 class (`ChunksToLinesStream`、`ChunksToLinesTransformer`),类的代码太长这里就不贴出来了,这两个类也是实现 `web stream` 逐行读取的关键。 |
||||
|
||||
```javascript |
||||
import * as fs from "node:fs"; |
||||
import { Readable } from "node:stream"; |
||||
|
||||
const nodeReadable = fs.createReadStream( |
||||
"text-file.txt", |
||||
{ encoding: 'utf-8' } |
||||
); |
||||
const webReadableStream = Readable.toWeb(nodeReadable); |
||||
// 逐行读取 |
||||
// const lineStream = webReadableStream.pipeThrough( |
||||
// new ChunksToLinesStream(); |
||||
// ) |
||||
for await (const line of lineStream) { |
||||
console.log(line); |
||||
} |
||||
``` |
||||
|
||||
`web stream` 是异步可迭代的,这也是为什么上述代码中使用 `for-await-of` |
||||
|
||||
这种方法的优缺点: |
||||
- 优点:对大文件友好,因为我们并不需要等到所有数据读取完 |
||||
- 缺点:使用复杂且都是异步操作 |
||||
|
||||
### 同步的将字符串写入到文件中 |
||||
|
||||
>和上面相同,这里也会使用两个例子演示 `fs.writeFile` 和 `stream` 的差异 |
||||
|
||||
**[`fs.writeFileSync(filePath, str, options?)`](https://nodejs.org/api/fs.html#fswritefilesyncfile-data-options)** |
||||
|
||||
使用这种办法写入时,如果文件已经存在于路径上,那么会被覆盖掉 |
||||
|
||||
```javascript |
||||
import * as fs from 'node:fs'; |
||||
|
||||
fs.writeFileSync( |
||||
'existing-file.txt', |
||||
'Appended line\n', |
||||
{ encoding: 'utf-8' } |
||||
); |
||||
``` |
||||
|
||||
如果并不希望已经存在的文件被覆盖掉,那么需要在 `fs.writeFileSync` 的 `options` 参数中添加一个属性: `flag: "a"`,这个属性表示我们需要追加而不是覆盖文件, |
||||
|
||||
ps: 在有些 fs 函数中,这个属性名叫做 `flag`,但是另外一些函数中会被叫做 `flags` |
||||
|
||||
**stream** |
||||
|
||||
```javascript |
||||
import * as fs from "node:fs"; |
||||
import { Writable } from "node:stream"; |
||||
|
||||
// 使用 fs.createWriteStream 创建一个 Nodejs srteam |
||||
const nodeWritable = fs.createWriteStream("text-file.txt", { |
||||
encoding: "utf-8", |
||||
}); |
||||
// 转换为 web stream |
||||
const webWritableStream = Writable.toWeb(nodeWritable); |
||||
const writer = webWritableStream.getWriter(); |
||||
|
||||
try { |
||||
await writer.write("First line\n"); |
||||
await writer.write("Second line\n"); |
||||
await writer.close(); |
||||
} finally { |
||||
writer.releaseLock(); |
||||
} |
||||
``` |
||||
|
||||
以上代码和 `fs.writeFileSync` 相同:如果路径上依旧存在文件,那么写入的内容会将原文件的内容覆盖掉,如果需要追加而不是覆盖,操作和 `fs.writeFile` 是相同的,在 `fs.createReadStream` 的 `options` 参数中添加一个属性 `flag: "a"` |
||||
|
||||
|
||||
## 遍历和创建目录 |
||||
|
||||
### 遍历目录 |
||||
|
||||
```typescript |
||||
import * as fs from "node:fs"; |
||||
import * as path from "node:path"; |
||||
|
||||
function* traverseDirectory(dirPath: string): any { |
||||
const dirEntries = fs.readdirSync( |
||||
dirPath, |
||||
{ withFileTypes: true } |
||||
); |
||||
dirEntries.sort((a, b) => a.name.localeCompare(b.name, "en")); |
||||
for (const dirEntry of dirEntries) { |
||||
const fileName = dirEntry.name; |
||||
const pathName = path.join(dirPath, fileName); |
||||
yield pathName; |
||||
|
||||
if (dirEntry.isDirectory()) { |
||||
yield* traverseDirectory(pathName); |
||||
} |
||||
} |
||||
} |
||||
|
||||
for (const filePath of traverseDirectory("../../../")) { |
||||
console.log(filePath); |
||||
} |
||||
``` |
||||
|
||||
在以上代码中我们用到了 `fs.readdirSync` 用来返回子目录路径,其中当 option 参数中的 `withFileTypes` 为 `true` 时,它会返回一个可迭代的 `fs.Dirent` 实例 (directory entries),如果 `withFileTypes` 为 false 或者不传,那么会以字符串类型返回字符串 |
||||
|
||||
### 创建目录 |
||||
|
||||
我们可以使用 [`fs.mkdirSync(thePath, options?)`](https://nodejs.org/api/fs.html#fsmkdirsyncpath-options) 来创建一个目录,其中 `options.recursive` 参数用于确定如何在 `thePath` 上创建目录: |
||||
- 如果 `recursive` 参数缺失或者为 false,那么 `fs.mkidrSync` 会返回 `undefined`,且在一些情况下会抛出异常: |
||||
- 目录已经存在于 `thePath` 上 |
||||
- `thePath` 的父目录不存在 |
||||
- 如果 `recursive` 为 true |
||||
- 即使目录已经存在于 `thePath` 上也不会抛出错误 |
||||
- 如果父目录不存在,那么会一起创建父目录,并返回第一个创建成功的目录 path |
||||
|
||||
### 确保父目录存在 |
||||
|
||||
如果我们希望按需设置嵌套文件结构 (nested file structure),我们就没法总是在创建新文件时确保其祖先目录存在,以下代码就是为了解决这个问题 |
||||
|
||||
```typescript |
||||
import * as fs from "node:fs"; |
||||
import * as path from "node:path"; |
||||
|
||||
const ensureParentDirectory = (filePath: string) => { |
||||
const parentDir = path.dirname(filePath); |
||||
if (!fs.existsSync(parentDir)) { |
||||
fs.mkdirSync(parentDir, { recursive: true }); |
||||
} |
||||
}; |
||||
``` |
||||
|
||||
### 创建临时目录 |
||||
|
||||
在有些场景,我们需要创建一个临时目录,那么这时我们可以使用 [`fs.mkdtempSync(pathPrefix, options?)`](https://nodejs.org/api/fs.html#fsmkdtempsyncprefix-options),它会在 `pathPrefix` 目录下创建一个随机6位字符串作为名称的目录,并返回路径 |
||||
|
||||
如果我们需要在系统的全局临时目录中创建临时目录,我们可以借助 `os.tmpdir()` |
||||
|
||||
```typescript |
||||
import * as os from "node:os"; |
||||
import * as fs from "node:fs"; |
||||
import * as path from "node:path"; |
||||
|
||||
const pathPrefix = path.resolve(os.tmpdir(), "my-app"); |
||||
const tmpPath = fs.mkdtempSync(pathPrefix); |
||||
``` |
||||
|
||||
需要注意的是,创建的临时目录并不会在 Nodejs 脚本结束后被删除,我们需要自己手动的去删除它,或是依赖系统自己的定期清除(不太可靠) |
||||
|
||||
|
||||
## 复制、重命名、移动文件或是目录 |
||||
|
||||
### 复制目录结构 |
||||
|
||||
[`fs.cpSync(srcPath, destPath, options?)`](https://nodejs.org/api/fs.html#fscpsyncsrc-dest-options) 可以帮助我们同步地将整个目录结构从src 复制到 dest,包括子目录和文件,而其中的 `options` 有如下一些有意思的参数: |
||||
- `recursive (default: false)`:如果该参数为 `true` 则递归复制目录(包括空目录),ps:在我的测试中,这个参数如果为 `false` 是一定会抛出异常的 |
||||
- `force (default: true)`:为 true 时则覆盖已经有文件 |
||||
|
||||
### 重命名或移动文件或文件夹 |
||||
|
||||
[`fs.renameSync(oldPath, newPath)`](https://nodejs.org/api/fs.html#fsrenamesyncoldpath-newpath) 它可以重命名或者移动文件或文件夹从 `oldPath` 到 `newPath`,对于目录而言,该函数仅能实现重命名,而对于文件则可以重命名或是移动 |
||||
|
||||
### 移除 (Remove) 文件或是目录 |
||||
|
||||
[`fs.rmSync(thePath, options?)`](https://nodejs.org/api/fs.html#fsrmsyncpath-options) 在 `thePath` 上删除一个文件或是目录,就像是 `rm, rm -rf` `options` 中一些有意思的参数: |
||||
- `recursive (default: false)`:只有该参数为 `true` 时,才会删除目录(包含空目录) |
||||
- `force (default: false)`:如果该参数为 `false`,`thePath` 上不存在文件或是目录时会抛出异常 |
||||
|
||||
[`fs.rmdirSync(thePath, options?)`](https://nodejs.org/api/fs.html#fsrmdirsyncpath-options) 则用来专门删除空的目录,如果目录并不是空的,则会抛出异常 |
||||
|
||||
在有些场景中,一些脚本执行之前需要清理它们的输出(output)目录的所有文件,这时可以用到一个简单的工具(utils)函数: |
||||
|
||||
```typescript |
||||
const clearDirectory = (dirPath: string) => { |
||||
for (const fileName of fs.readdirSync(dirPath)) { |
||||
const pathName = path.join(dirPath, fileName); |
||||
fs.rmSync(pathName, { recursive: true }); |
||||
} |
||||
}; |
||||
``` |
||||
|
||||
以上代码中我们用到了 `fs.readdirSync()` 函数,然后遍历其返回值并调用 `fs,rmSync()` 递归删除目录 |
||||
|
||||
|
||||
## 读取和更改文件系统条目 (entries) |
||||
|
||||
### 检查文件或者目录是否存在 |
||||
|
||||
[`fs.existsSync(thePath)`](https://nodejs.org/api/fs.html#fsexistssyncpath),早在之前的章节中我们就已经使用过这个 API 了,如果 `thePath` 文件或者目录已经存在则会返回 `true` |
||||
|
||||
### 检查文件详细信息,如创建时间 |
||||
|
||||
[`fs.statSync(thePath, options?)`](https://nodejs.org/api/fs.html#fsstatsyncpath-options) 会返回一个 `fs.stats` 的实例,`thePath` 上文件或者目录的详细信息,一些有意思的 `options`: |
||||
- `throwIfNoEntry (default: true)`:该参数用于控制 `thePath` 上不存在目录或文件的时候的行为 |
||||
- 如果参数为 `true`,则会抛出异常 |
||||
- 如果参数为 `false`,则会返回 `undefined` |
||||
- `bigint (default: false)`:如果该参数为 `true`,函数将用 bigints 表示数值,如时间戳 |
||||
|
||||
[`fs.Stats`](https://nodejs.org/api/fs.html#class-fsstats) 实例的一些属性: |
||||
- 系统条目(system entry) 属于哪种类型 |
||||
- `stats.isFile()` |
||||
- `stats.isDirectory()` |
||||
- `stats.isSymbolicLink()` |
||||
- `stats.size` 按字节为单位显示大小 |
||||
- 时间戳 (Timestamps) |
||||
- 总共有三种时间戳: |
||||
- `stats.atime` 最后存取时间 |
||||
- `stats.mtime` 最后修改时间 |
||||
- `stats.birthtime` 创建时间 |
||||
- 这三种时间戳都有三种不同的单位表示,以 atime 为例 |
||||
- `stats.atime` Date 的实例 |
||||
- `stats.atimeMS` 自 POSIX Epoch 起的毫秒数 |
||||
- `stats.atimeNs` 纳秒自 POSIX,需要 `bigint` 参数为 `true` |
||||
|
||||
### 修改文件属性:权限、所有者、组、时间戳 |
||||
|
||||
[`fs.chmodSync(path, mode)`](https://nodejs.org/api/fs.html#fschmodsyncpath-mode) 用于修改文件权限 |
||||
[`fs.chownSync(path, uid, gid)`](https://nodejs.org/api/fs.html#fschownsyncpath-uid-gid) 用于修改文件的所有者和组 |
||||
[`fs.utimesSync(path, atime, mtime)`](https://nodejs.org/api/fs.html#fsutimessyncpath-atime-mtime) 修改文件的时间戳 |
||||
|
||||
|
||||
## 使用链接 (links) |
||||
|
||||
Nodejs 文件系统(File System) 还提供了一些用于链接的 API |
||||
|
||||
### 硬链接 (hard links) API |
||||
|
||||
[`fs.linkSync(existingPath, newPath)`](https://nodejs.org/api/fs.html#fslinksyncexistingpath-newpath) 创建一个硬链接 |
||||
[`fs.unlinkSync(path)`](https://nodejs.org/api/fs.html#fsunlinksyncpath) 移除一个硬链接以及它指向的文件(如果这是指向那个文件的最后一个硬链接) |
||||
|
||||
### 符号链接(symbolic links) API |
||||
|
||||
[`fs.symlinkSync(target, path, type?)`](https://nodejs.org/api/fs.html#fssymlinksynctarget-path-type) 创建一个符号链接 |
||||
[`fs.readlinkSync(path, options?)`](https://nodejs.org/api/fs.html#fsreadlinksyncpath-options) 返回位于 `path` 的符号链接的目标 |
||||
|
||||
还有一些函数用于在不解除符号链接引用的情况下进行操作(它们的函数名实际上就是普通文件的操作 API 前用 “l” 开头) |
||||
|
||||
[`fs.lchmodSync(path, mode)`](https://nodejs.org/api/fs.html#fslchmodsyncpath-mode) 更改 `path` 上符号链接的权限 |
||||
[`fs.lchownSync(path, uid, gid)`](https://nodejs.org/api/fs.html#fslchownsyncpath-uid-gid) 更改 `path` 上符号链接的用户和组 |
||||
[`fs.lutimesSync(path, atime, mtime)`](https://nodejs.org/api/fs.html#fslutimessyncpath-atime-mtime) 更改 `path` 上符号链接的时间戳 |
||||
[`fs.lstatSync(path, options?)`](https://nodejs.org/api/fs.html#fslstatsyncpath-options) 返回 `path` 上符号链接的 stats |
||||
|
||||
还有一些有用的函数: |
||||
[`fs.realpathSync(path, options?)`](https://nodejs.org/api/fs.html#fsrealpathsyncpath-options) 通过解析(.)、(..) 和 符号链接计算并返回规范路径名 (canonical pathname) |
||||
|
||||
|
||||
## 关于 `existsSync` 的一些补充 |
||||
|
||||
>这些补充来自原文章的评论区,在作者的文章之外做了一些有意思的补充 |
||||
|
||||
`existsSync` 是一个比较特殊的 API,因为 `exists` 已经被废弃,但是 `existsSync` 却没有,并且在 `fsPromise` 中也没有相应的实现,有的只是 `fsPromises.access`:如果文件不可访问则抛出错误 |
||||
|
||||
因为 Nodejs 团队认为,在使用文件或目录之前检查是否存在是一个反模式 (anti-pattern),它会使用户暴露在 TOCTOU 的风险当中,相应的,Nodejs 团队推荐直接尝试访问、读/写文件,并在目录不存在时处理错误 |
||||
|
||||
详情请参阅 [`fs.access`](https://disq.us/url?url=https%3A%2F%2Fnodejs.org%2Fapi%2Ffs.html%23fspromisesaccesspath-mode%3APpN2_GmmtjnrYBAhPu0Xob7JfRw&cuid=611304 "https://nodejs.org/api/fs.html#fspromisesaccesspath-mode") 关于推荐和不推荐("recommended" "not recommended")的部分 |
@ -1,324 +0,0 @@
@@ -1,324 +0,0 @@
|
||||
>这篇文章是 [Working with file system paths on Node.js](https://2ality.com/2022/07/nodejs-path.html) 学习过程中的个人总结和笔记,大部分内容实际来自于原文章,如果你对 Nodejs 中的文件系统 (path) 感兴趣,我更建议你直接去看原文章。 |
||||
|
||||
>在下文中 path 会被翻译成 路径 |
||||
|
||||
## Nodejs 中路径相关(Path-related)的功能 |
||||
|
||||
- 大多数路径相关的功能都在模块 `node:path` 中 |
||||
- 全局变量 `process` 拥有一些方法可以改变当前工作目录(current working directory) |
||||
- `node:os` 模块拥有一些函数可以返回重要的目录路径 |
||||
|
||||
关于 `path` 的使用有三种方法,它们分辨是 |
||||
- 直接使用 `path` |
||||
- 使用平台特定版本(platform-specific versions)相关的 `path`: |
||||
- `path.posix` 对应 Unix 系系统,包括 MacOS |
||||
- `path.win32` 对应 Windows 系统 |
||||
|
||||
而直接使用 `path` 本身,它本身始终支持当前平台,内部似乎做了一些判断,可以从下面 Nodejs REPL 的例子中看出来: |
||||
|
||||
```javascript |
||||
path.parse === path.posix.parse // true |
||||
``` |
||||
|
||||
不同的 `path` 特定版本的处理逻辑也会有不同,如果没有使用正确的版本,可能会出现预期之外的结果 |
||||
|
||||
|
||||
## 基本路径概念及其API支持 |
||||
|
||||
### 当前工作目录 current working directory(CWD) |
||||
|
||||
- 如果我们使用一个包含相对路径的命令(command),该路径将针对 CWD 进行解析 |
||||
- 如果我们在省略了一个路径的期望路径(就是不传),那么这时候会使用 CWD |
||||
- 在 UNIX 和 Windows 中切换 CWD 的命令都是 `cd` |
||||
|
||||
`process` 是 Nodejs 提供的全局变量,它为我们提供了一些用于获取(getting)或设置(setting)CWD 的方法: |
||||
- [`process.cwd()`](https://nodejs.org/api/process.html#processcwd) 会返回当前 CWD |
||||
- [`process.chdir(dirPath)`](https://nodejs.org/api/process.html#processchdirdirectory) 改变当前 CWD 至 `dirPath` |
||||
- 在 `dirPath` 必须有一个路径 |
||||
- 改变并不会影响 shell,只影响当前运行的 Nodejs 进程 |
||||
|
||||
当路径不完整(isn’t fully qualified)的时候,Nodejs 会使用 CWD 来填补缺失的部分 |
||||
|
||||
### 在 Windows 中的当前工作目录(CWD) |
||||
|
||||
在 Windows 系统中,CWD 的工作会有些许不同: |
||||
- 每个驱动器都有一个 CWD |
||||
- 有一个当前驱动器(current drive)存在 |
||||
|
||||
我们可以使用 `path.chdir()` 同时修改两者: |
||||
|
||||
```javascript |
||||
process.chdir('C:\\Windows'); |
||||
process.chdir('Z:\\tmp'); |
||||
``` |
||||
|
||||
但是当我们重新访问一个驱动器的时候,Nodejs 会记住当前驱动器的前一个当前目录: |
||||
|
||||
```javascript |
||||
process.cwd() // 'Z:\\tmp' |
||||
|
||||
process.chdir('C:'); |
||||
process.cwd() // 'C:\\Windows' |
||||
``` |
||||
|
||||
### 完整路径和不完整路径以及路径解析 |
||||
|
||||
- 一个完整路径(fully qualified path)不依赖于任何信息,可以原样使用 |
||||
- 一个不完整的路径(partially qualified path)是缺少信息的,在使用之前我们需要将其解变成一个完整路径 |
||||
|
||||
**Unix** |
||||
|
||||
Unix 只知道两种路径: |
||||
|
||||
- 绝对路径(Absolute paths):是一个完整路径,总是以斜杠开头: |
||||
- `/home/user/video` |
||||
- 相对路径(Relative paths):是一个不完整的路径,会以文件名或是点(dot)开头: |
||||
- `dir` |
||||
- `../dir` |
||||
|
||||
使用 `path.resolve()` 在 Unix 中解析路径非常的简单,其返回会是一个绝对路径 |
||||
|
||||
**Windows** |
||||
|
||||
在 Windows 中有四种路径: |
||||
- 绝对路径和相对路径 |
||||
- 以上两种路径都可以带卷标和不带卷标 |
||||
|
||||
带卷标(drive letter)的绝对路径才是一个完整路径,而其他都是不完整路径,在 Windows 中使用 `path.resolve` 解析路径,规则会比较复杂,[详情](https://2ality.com/2022/07/nodejs-path.html#fully-and-partially-qualified-paths-on-windows)可以看原文章,这里就不贴出来了 |
||||
|
||||
|
||||
## 通过 `node:os` 模块获取重要目录的路径 |
||||
|
||||
模块 "node:os "为我们提供了两个重要目录的路径 |
||||
- [`os.homedir()`](https://nodejs.org/api/os.html#oshomedir) 返回当前用户的主目录路径 |
||||
- [`os.tmpdir()`](https://nodejs.org/api/os.html#ostmpdir) 返回当前操作系统的临时文件目录路径 |
||||
|
||||
|
||||
## 路径拼接(concatenating paths) |
||||
|
||||
有两个方法用于路径拼接: |
||||
- `path.resolve()` 总是返回一个完整路径 |
||||
- `path.join()` 则会保留相对路径 |
||||
|
||||
### `path.resolve()`:拼接并返回一个完整路径 |
||||
|
||||
可以把 `path.resolve` 的行为总结为以下的几点: |
||||
|
||||
- 从当前工作目录(CWD)开始拼接 |
||||
- 将 path[0] (参数0)与之前的结果进行比对 |
||||
- 将 path[1] (参数1)与之前的结果进行比对 |
||||
- 对所有剩余的路径参数做同样的处理 |
||||
- 返回完整的路径 |
||||
|
||||
在上边我们已经知晓了它的行为,下面这些结果也印证了上面的总结 |
||||
|
||||
- 当参数为空时,会返回当前的工作目录路径(CWD) |
||||
- `> path.resolve() // result: "/usr/local"` |
||||
- 当有一个或多个相对路径时,将会从当前工作路径(CWD)开始拼接 |
||||
- `> path.resolve('./bin', 'sub') // result: "/usr/local/bin/sub"` |
||||
- 当参数中有完整路径时,始终会覆盖前一个拼接结果 |
||||
- `> path.resolve('bin', '/home') // result: "/home"` |
||||
|
||||
### `path.join()`:拼接并保持相对路径 |
||||
|
||||
`path.join()` 和 `path.resolve()` 不同,它从 path[0] 开始拼接,保留了不完整的路径,如果 path[0] 是完整路径,那么返回的结果也是完整路径,如果是不完整路径,那么结果也是不完整路径 |
||||
|
||||
会有一些特别的结果——当第一个参数之后参数为绝对路径时,那么它会被解析成相对路径: |
||||
|
||||
```javascript |
||||
path.join('dir', '/tmp') // 'dir/tmp' |
||||
``` |
||||
|
||||
|
||||
## 确保路径是规范化(normalized)的,完整的,或相对的 |
||||
|
||||
### `path.normalize()`:用于将路径规范化 |
||||
|
||||
对于不同的系统环境,该函数的行为也有所不同: |
||||
|
||||
**在 Unix 系统中**: |
||||
- 移除单点(.)或双点(..)的路径段 |
||||
- 将多个路径分隔符变成一个路径分隔符 |
||||
|
||||
```javascript |
||||
path.normalize('/home/./john/lib/../photos///pet') |
||||
// '/home/john/photos/pet' |
||||
``` |
||||
|
||||
**在 Windows 系统中**: |
||||
- 移除单点(.)或双点(..)的路径段 |
||||
- 将斜杠转换为反斜杠 |
||||
- 多个反斜杠转换为一个反斜杠 |
||||
|
||||
```javascript |
||||
path.win32.normalize('C:\\Users/jane\\doc\\..\\proj\\\\src'), |
||||
// 'C:\\Users\\jane\\proj\\src' |
||||
``` |
||||
|
||||
**注意**:只有一个参数的 `path.join` 也会进行路径规范化转换,它的行为和 `path.normalize` 是相同的 |
||||
|
||||
### `path.resolve()` 单参数时的作用 |
||||
|
||||
在前面我们已经遇到过 `path.resolve()` 了,用于路径拼接,但是当它只有一个参数的时候它的作用会发生一些变化:它会既将路径规范化又确保返回完整路径 |
||||
|
||||
```javascript |
||||
path.resolve('/home/./john/lib/../photos///pet') |
||||
// '/home/john/photos/pet' |
||||
``` |
||||
|
||||
### `path.relative()` 创建相对路径 |
||||
|
||||
```javascript |
||||
path.relative(sourcePath: string, destinationPath: string): string |
||||
``` |
||||
|
||||
它会返回一个路径,让我们从 `sourcePath` 到 `destinationPath` |
||||
|
||||
```javascript |
||||
path.relative('/home/john/', '/home/john/proj/my-lib/README.md') |
||||
// 'proj/my-lib/README.md' |
||||
``` |
||||
|
||||
在 Windows 上,如果 sourcePath 和 destinationPath 在不同的驱动器上,我们会得到一个完整的路径,如下 |
||||
|
||||
```javascript |
||||
path.relative('Z:\\tmp\\', 'C:\\Users\\Jane\\') |
||||
// 'C:\\Users\\Jane' |
||||
``` |
||||
|
||||
这个函数对于相对路径也是适用的: |
||||
|
||||
```javascript |
||||
path.relative('proj/my-lib/', 'doc/zsh.txt') |
||||
// '../../doc/zsh.txt' |
||||
``` |
||||
|
||||
### `path.isAbsolute()` 判断是否是一个绝对路径 |
||||
|
||||
如果路径是一个绝对路径返回 `true`,反之则返回 `false` |
||||
|
||||
```javascript |
||||
path.isAbsolute('/home/john') // true |
||||
``` |
||||
|
||||
注意:在 Windows 系统中,绝对路径比不意味着就是一个完整路径,如下 |
||||
|
||||
```javascript |
||||
path.isAbsolute('C:\\Users\\jane') // true 只有这个是完整路径 |
||||
path.isAbsolute('\\Users\\jane') // true |
||||
``` |
||||
|
||||
|
||||
## 解析路径:提取路径中的各个部分(文件拓展名等) |
||||
|
||||
### `path.parse()` 创建一个含有路径部分的对象(object with path parts) |
||||
|
||||
提取路径的各个部分,并在一个具有以下属性的对象中返回: |
||||
- `base`: 路径的最后一段 |
||||
- `ext`: 路径中文件拓展名 |
||||
- `name`: `base` 中不包含拓展名的部分 |
||||
- `root`: 路径的开始 |
||||
- `dir`: `base` 的所在路径(不包含`base`) |
||||
|
||||
### `path.basename()` 提取路径中的 `base` 部分 |
||||
|
||||
```javascript |
||||
path.basename('/home/jane/file.txt') |
||||
// 'file.txt' |
||||
``` |
||||
|
||||
此外,它的第二个参数允许删除一些拓展名: |
||||
**注意:第二个参数是区分大小的** |
||||
|
||||
```javascript |
||||
path.basename('/home/jane/file.txt', 'xt') |
||||
// 'file.t' |
||||
``` |
||||
|
||||
### `path.dirname()` 提取路径中 `dir` 的部分 |
||||
|
||||
```javascript |
||||
path.dirname('C:\\Users\\john\\dir\\') |
||||
// 'C:\\Users\\john' |
||||
``` |
||||
|
||||
### `path.extname()` 提取路径中 `ext` 的部分 |
||||
|
||||
```javascript |
||||
path.extname('/home/jane/file.txt') |
||||
// '.txt' |
||||
``` |
||||
|
||||
|
||||
## `path.format()` 从路径对象中创建路径 |
||||
|
||||
和 `path.parse()` 的作用相反,`path.format()` 用于从路径对象中生成对应的路径并返回 |
||||
|
||||
例子:改变文件拓展名 |
||||
|
||||
```javascript |
||||
const changeFilenameExtension = (pathStr, newExtension) => { |
||||
if (!newExtension.startsWith(".")) { |
||||
throw new Error( |
||||
"Extension must start with a dot: " + |
||||
JSON.stringify(newExtension) |
||||
); |
||||
} |
||||
const parts = path.parse(pathStr); |
||||
return path.format({ |
||||
...parts, |
||||
// 防止 base 重写 name 和 ext |
||||
base: undefined, |
||||
ext: newExtension, |
||||
}); |
||||
}; |
||||
``` |
||||
|
||||
|
||||
## 在不同的平台使用相同的路径 |
||||
|
||||
有些时候,我们希望能在不同的平台使用相同的路径,这时候我们会面临两个问题: |
||||
1. 路径分隔符可能会不同 |
||||
2. 文件结构可能不同:主目录和临时目录路径不同 |
||||
|
||||
举一个例子,考虑一个 Nodejs 应用,它在一个有数据的目录上操作,假设它可以配置两种路径: |
||||
- 完整路径 |
||||
- 数据目录内的路径(相对的路径) |
||||
|
||||
由于开头所说的问题: |
||||
- 我们并不能使用复用相同的完整目录在不同的平台 |
||||
- 我们可以复用数据目录内的路径,这样的路径可以存储在配置文件或是代码的常量中,要做到这一点我们需要: |
||||
- 必须使用相对路径 |
||||
- 跨平台复用时确保路径分隔符的正确 |
||||
|
||||
### 与平台无关的相对路径(Relative platform-independent paths) |
||||
|
||||
与平台无关的相对路径可以存储在一个数组中,可以使用以下的方法转换为对应平台的路径 |
||||
|
||||
```javascript |
||||
const universalRelativePath = ['static', 'img', 'logo.jpg']; |
||||
const dataDirUnix = '/home/john/data-dir'; |
||||
path.posix.resolve(dataDirUnix, ...universalRelativePath), |
||||
// '/home/john/data-dir/static/img/logo.jpg' |
||||
``` |
||||
|
||||
以下代码用于将路径转换为数组 |
||||
|
||||
```javascript |
||||
const splitRelativePathIntoSegments = (relPath) => { |
||||
if (path.isAbsolute(relPath)) { |
||||
throw new Error("Path isn’t relative: " + relPath); |
||||
} |
||||
relPath = path.normalize(relPath); |
||||
const result = []; |
||||
while (true) { |
||||
const base = path.basename(relPath); |
||||
if (base.length === 0) break; |
||||
result.unshift(base); |
||||
const dir = path.dirname(relPath); |
||||
if (dir === ".") break; |
||||
relPath = dir; |
||||
} |
||||
return result; |
||||
}; |
||||
``` |
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
# useEffect 和 Debounce |
||||
最近在学习 React + TypeScript 仿 Jira 项目,不得不说这个课程是真的好,可能因为比较新的缘故,老师在一些库的选择上都会比较新,最最让我惊喜的是这门课程还用到了ReactRoute6,正好苦于网上没有清晰明了的教程,这不就来了。 |
||||
|
||||
在课程里面,有一个自定义 Hook 的小示例,就是用 React Hook 实现 Debounce 的功能,Debounce 的功能要求很简单,就是通过一些手段控制接口的请求次数,用定时器实现的Debounce 就是一个很典型的例子,课程里边的 Debounce Hook 也是使用定时器的方式实现,不过在编写这个自定义 Hook 的时候,用到了 React 自带的 useEffetc 这个 Hook,这不免让我有些好奇,这个Hook是怎么个原理,能够做些简单的逻辑就实现 Debounce 的呢? |
||||
首先先来捋清 useEffetc 这个 Hook 的参数和含义,useEffetc 第一个参数接收一个回调函数,第二个参数是一个数组,在数组中传入的值会作为函数的执行条件,什么意思呢,就是说第二个参数的值会在下一次 useEffect 执行的时候被比较,如果每个值都相同,那么 useEffect 就不执行,如果值发生了变化,useEffetc 就会被执行,这个数组还可以是一个空数组,表示 useEffect 只在函数组件初始化的时候执行。 |
||||
|
||||
接下来就是有意思的地方了,还记得前面说到的第一个参数回调函数吗?这个回调函数可以 return 一个函数,这个被 return 的函数,会在下一次 useEffect 被执行之前执行,用于清除上一个函数的副作用,至于什么是副作用,这个其实是函数式编程专有的名词,在这里不展开讲,以后写个新的文章讨论什么是函数式编程,到这里,就已经把 useEffect 的基本功能给说清楚了 ,这个时候可以回过头来看,useEffect 的执行时机是什么时候,默认情况下,useEffect 会在每轮的渲染结束之后执行,并且 React 为了保证严格的 render -> useEffect 顺序,所以在下一次 render 执行之前一定会执行一次 useEffect,那么我在 useEffect 的第一个参数回调函数中放一个定时器,并在 return 中返回一个清除定时器的函数,那么我不就可以在每一次 render 执行之后 useEffect 执行之前清除上一次在 useEffect 中存放的定时器了吗?这里为了保证每一次都会有 render 触发 useEffect,还需要用到 useState,这样不就是很典型的 Debounce 实现思路吗? |
||||
|
||||
具体代码如下图 |
||||
|
||||
![[useEffect 和 Debounce.png]] |
@ -1,104 +0,0 @@
@@ -1,104 +0,0 @@
|
||||
最近在写基于 React 和 Antd 的后台管理系统,用在我的博客管理上,在参考其他开源项目的时候有一个功能我很感兴趣,就是在管理后台中加入一个像浏览器的 Tabs 功能,让它能实现标签页的快速切换,虽然现在的管理系统的页面总数不多其实没有什么快速切换的需求 = = |
||||
|
||||
刚看到这个问题的时候,我的第一反应是有没有一个 API 可以记录并获取路由栈中的所有路由,再将其给渲染出来,就解决了标签页的显示问题,但是我找了一下似乎并没有这样的 API,所以这篇文章会用 Context + useEffect 监听路由的方式来实现标签页的显示。 |
||||
|
||||
## 大致的思路 |
||||
外层用 Context 包裹,在 Context 内部用 useEffect 监听路由是否发生变化,如果发生了变化就在 Tabs 列表中加入变化的路由属性,因为要实现显示Tab名称、Tab 跳转和保存路由的查询参数,所以记录的时候这些信息要一起保存起来。 |
||||
|
||||
其实这个功能也可以不用 Context 实现,在 Tabs 组件内部只使用 useEffect 也能实现相同的功能,但是我考虑到将来管理后台中或许会有别的页面也会需要用到类似的功能,就把它抽离出来方便复用。 |
||||
|
||||
## Tabs 添加路由 |
||||
有思路之后实现起来就很容易了,首先要写一个 Context ,这个很简单在这里直接跳过,然后实现一个路由的监听记录函数,这里用 useEffect 来做,具体的代码逻辑可以看函数的注释 |
||||
|
||||
```javascript |
||||
// react-router 的 useLocation Hook |
||||
const { pathname, search } = useLocation(); |
||||
// useEffect 监听路由是否发生变化 |
||||
useEffect(() => addTabItem(pathname, search), [pathname, search]); |
||||
|
||||
// 记录路由的 State,Tab列表 |
||||
const [tabList, setTabList] = useState( |
||||
[{ name: "Dashboard", path: "/", search: "" }] |
||||
); |
||||
|
||||
// 路由添加函数 |
||||
const addTabItem = (path, search) => { |
||||
// 判断路由是不是已经存在于 State 中,不存在就添加到 Tab 列表中 |
||||
if (tabList.findIndex((item) => item.path === path) === -1) { |
||||
// 这里主要是为了保存 Tab 对应的路由名称,flattenArray 是数组扁平化函数 |
||||
// 由于我的路由是用 useRoutes 实现的约定式路由,路由的名称也记录在里边 |
||||
// 所以这里需要将其扁平化和找到对应的路由名称 |
||||
// 文章后边会有这个扁平化函数的代码 |
||||
const flattenList = flattenArray(routerConfig); |
||||
const routerIndex = flattenList.findIndex((item) => { |
||||
item.path === path |
||||
}); |
||||
|
||||
// 最后将其一起保存在 Tab 列表中 |
||||
if (routerIndex !== -1) { |
||||
setTabList([ |
||||
...tabList, |
||||
{ path, name: flattenList[routerIndex].name, search }, |
||||
]); |
||||
} |
||||
} |
||||
|
||||
// 存在就切换路由,后面会写到 |
||||
switchTab(path, search); |
||||
}; |
||||
``` |
||||
|
||||
以上的逻辑就是最核心的部分,通过监听路由变化,我们得到了一个 Tabs 列表,后续只需要渲染出它那么 Tabs 显示的问题就解决了 |
||||
|
||||
## Tabs 切换路由和关闭路由 |
||||
解决了 Tab 添加的问题,接下来要解决的就是 Tab 的切换和关闭,总不能只能开不能关吧,切换和关闭的代码逻辑很简单,代码量也很少,没有什么难点 |
||||
|
||||
对于 Tab 的切换,我们需要一个新的状态来保存当前被选中的 Tab,这里叫作 activeTab |
||||
|
||||
```javascript |
||||
// 用于保存当前选中 Tabs 的状态 |
||||
const [activeTab, setActiveTab] = useState(""); |
||||
|
||||
const switchTab = (path, search = "") => { |
||||
// 切换选中的 Tabs |
||||
setActiveTab(path); |
||||
// 跳转路由 |
||||
navigate(path + search); |
||||
}; |
||||
``` |
||||
|
||||
而对于 Tab 的关闭,就没什么好说的了,非常简单的查找然后删除,这里有一个特殊的处理,就是判断关闭的 Tab 是不是当前选中的 Tab,对于关闭当前选中 Tab 的操作,这里会把当前选中的 Tab 向前移动,保证页面上不会出现关闭了 Tab 但是页面还停留在原地的问题 |
||||
|
||||
```javascript |
||||
const closeTab = (path) => { |
||||
const index = tabList.findIndex((item) => item.path === path); |
||||
if (index === -1 || path === "/") return; |
||||
|
||||
const newTabList = [...tabList]; |
||||
newTabList.splice(index, 1); |
||||
|
||||
// 判断是否关闭当前选中的 Tab |
||||
if (path === activeTab) switchTab(newTabList[index - 1].path); |
||||
|
||||
setTabList(newTabList); |
||||
}; |
||||
``` |
||||
|
||||
## 上边的数组扁平化函数 |
||||
|
||||
```javascript |
||||
export const flattenArray = (data, key = "children") => { |
||||
const flatRouter: any[] = []; |
||||
const flattenRecursion = (data: any) => { |
||||
data.forEach((item: any) => { |
||||
if (item[key]) flattenRecursion(item[key]); |
||||
flatRouter.push(item); |
||||
}); |
||||
}; |
||||
flattenRecursion(data); |
||||
return flatRouter; |
||||
}; |
||||
``` |
||||
|
||||
## End |
||||
至此,整个 Tabs 功能的基本逻辑就已经实现了,完整的代码可以看[这里](https://github.com/YuJian920/YuJian-Blog/blob/Remake/YuJianBlog-Admin/src/context/TabContext.tsx),其实完整的 Tabs 功能还没有结束,例如还有对 Tabs 长度的限制,避免打开的 Tab 太多导致长度过长,还有完备的 Tabs 应该有一个右键菜单,可以实现关闭其他、关闭所有之类更多的功能,这里就不展开谈了,如果有时间的话后边会再写一篇文章补全上述提到的这些功能 |
@ -1,13 +0,0 @@
@@ -1,13 +0,0 @@
|
||||
在看 Antd 的源码时看到下面这一段,是有关 bind 的使用方法,代码不多但是深入研究的话还是有不少知识盲区的 |
||||
|
||||
```typescript |
||||
const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/; |
||||
const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar); |
||||
isTwoCNChar("提交") |
||||
``` |
||||
|
||||
1. 为什么需要使用 `bind` 方法改变作用域? |
||||
`rxTwoCNChar.test` 这一行赋值相当于 `RegExp.prototype.test` 直接从 RegExp 原型中取 test 方法赋值 ,而不是通过 RegExp 实例 `rxTwoCNChar` 中调用函数,所以执行时的作用域并不会指向 `rxTwoCNChar`,需要使用 `bind` 函数生成并返回一个具有指定作用域的新函数。 |
||||
1. 不使用 bind 时 `rxTwoCNChar.test` 作用域指向谁? |
||||
1. 严格模式下 `this` 指向 `undefined` |
||||
2. 非严格模式下 `this` 指向 `global` |
@ -1,19 +0,0 @@
@@ -1,19 +0,0 @@
|
||||
>The React Framework for Production |
||||
|
||||
Next.js 是 React 框架的框架,属于 React 的上层建设,提供了类似于 Vue 全家桶的打包工具链,路由、状态、服务端渲染全都准备好了,最重要的是它有一个对于博客而言最最重要的功能:服务端渲染/静态增量更新 |
||||
|
||||
SSR 和 ISR 是吸引我用 Next.js 的最主要原因,这篇文章主要是记录 YuJianBlog 迁移至 Next.js 的一些障碍 |
||||
|
||||
## 工程化 |
||||
|
||||
Next.js 大而全,从 Vite 迁移到 Next 没有花多少的时间,安装 Next.js 移除 Vite 同时修改 `package.json` 中的 scripts 就已经完成了依赖方面的迁移,然后就可以和 React Router 说再见了。 |
||||
|
||||
对于客户端状态管理,博客秉承“小而美”的信念,是不使用客户端状态管理的,而服务端状态管理用的是 ReactQuery 的方案,而 Nextjs 是提供了一些用于请求数据的 API,迁移 Next.js 之后可以把它去掉了,对 package.json 做精简,总会有一种强迫症的舒服感觉 = = |
||||
|
||||
这里有一个比较大的改动,因为在原来的技术栈中,对于样式使用的是 less,并且样式命名用 BEM 做限制,不使用 css module,但是 Next.js 对于 less 需要做额外的配置,但是对于 scss 就很方便,所以我花了一点时间把 less 全部改成了 scss 并且使用 css module,都是体力活动 |
||||
|
||||
## 路由 |
||||
|
||||
前面说到 Next.js 是有自己的路由能力,所以我们这里需要对路由做一些调整,Next.js 的路由是基于文件系统的路由(file-system based),也就是说它会自动的根据 pages 文件夹下的目录生成路由,因为原项目中我的页面本来就放在 pages 下,所以等于基本没有做什么调整就完成这部分的内容 |
||||
|
||||
对于路由还有一个需要注意的地方,那就是动态路由,使用路由传参的场景是很常见的,那么 Next.js 要怎么实现动态路由呢?也不用做什么改动,只需要把动态路由的页面的文件名改成 [id].tsx ([id].jsx) 就行了,缺点就是丑 |
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
- [x] React 的 fiber 是什么,在 React v16 之前又是怎么做的? |
||||
- 在之前,React 使用递归的方式渲染DOM,这样在一些大型的Web应用会导致浏览器的阻塞,让一些优先级较高的任务没办法执行,在React16,React引入 Fiber 的数据结构,可以分片可以根据调度优先执行优先级较高的任务,重点在于 React 的性能优化 |
||||
- [x] useCallback 里边发生了什么 |
||||
- [[简单的 React 思考 - useCallback和useMemo]] |
||||
- [x] useMemo 里边发生了什么 |
||||
- [[简单的 React 思考 - useCallback和useMemo]] |
||||
- [x] \_\_proto\_\_ 和 prototype 是什么关系? |
||||
- 最简单的回答:prototype 是函数独有的属性,指向一个私有的空间/仓库,而 \_\_proto\_\_ 是浏览器厂商自己实现的 get/set 属性,指向对象中的 \[\[prototype\]\] 也就是继承的原型 |
||||
- 深入探索: [[深入 JavaScript 原型思考]] |
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
- [ ] 完整的个人事务管理系统,包括 TODO、提醒、记账、备忘录、活动清单,有想法集成定时脚本、运行指令、文件管理、docker 管理。 |
||||
- [ ] 简单的文件管理,实现服务器指定目录的文件查看,删除。 |
||||
- [ ] 集成多个音乐 API 的 AppleMusic 风格音乐播放器。 |
||||
- [ ] 全平台同步的单词生词本,基于 Spaced repetition 或者 莫宾浩斯遗忘曲线 复习。 |
||||
- [ ] 一个全自动的爬虫管理平台,支持爬虫脚本的启动和暂停、日志的查看和脚本定时执行。 |
||||
- [ ] 微信机器人兼顾视频网站下载、telegram、twitter 下载、监控服务器信息。 |
@ -1,29 +0,0 @@
@@ -1,29 +0,0 @@
|
||||
>原型 (prototype):为其他对象提供共享属性的对象。 |
||||
|
||||
JavaScript 是一门基于原型的语言,它靠着原型这一个特殊的概念实现了面对对象,在一众基于“类”的面对对象语言中脱颖而出,可以说原型就是 JavaScript 语言的基石。 |
||||
|
||||
那么原型是什么?开头那句话是 ECMAScript 5.1 中对原型做出的概述,但是深入到 JavaScript 的学习中会看到关于原型总会有不同的样子出现例如 `prototype`、`__proto__` ,它们分别代表着什么?在 JavaScript 中又有着怎样的作用? |
||||
|
||||
## `prototype`、`[[prototype]]` 和 `__proto__` 都是什么? |
||||
|
||||
`[[prototype]]` 是所有对象都具有的私有属性,这个属性的实现方式取决于各个平台,而对于浏览器环境而言,以 Chrome 为例,定义了 `__proto__` 访问器属性(getter、setter) 指向 `[[prototype]]`,也就是说在对象中,通过 `__proto__` 访问到的就是 `[[prototype]]` 私有属性 |
||||
|
||||
**注意: 这个属性由浏览器实现,且在 Web 标准中被删除,为保证 Web 浏览器的兼容性,建议使用 `Object.getPrototypeOf()` 替代** |
||||
|
||||
`prototype` 是函数独有的属性,无论什么时候只要创建了新的函数,就会根据特定的规则为该函数创建一个 `prototype` 属性,这个属性指向一个私有的对象,这个对象也有着一个 `constructor` 属性指回该函数,同时这个对象也具有 `__proto__` 属性指向 `Object.prototype` |
||||
|
||||
当一个对象通过 new 被实例化出来,那么这个对象的 `[[prototype]]` 属性也就是 `__proto__` 会被指向该函数的 `prototype` 属性,这是 JavaScript 实现面对对象的基石 |
||||
|
||||
>ECMAScript 5.1: |
||||
>当构造函数创建一个对象后,该对象隐式引用构造函数的 `prototype` 属性用以解决对象的属性引用。构造函数的 `prototype` 属性可以通过 `constructor.prototype` 表达式来访问,同时添加在该对象的原型里的属性会通过继承的方式与所有继承此原型的对象共享。或者,可以使用内置函数 `Object.create()` 明确指定原型来创建一个新对象 |
||||
|
||||
补充:并非所有对象都会具有 `[[prototype]]` 属性,你可以用 Object.create(null) 方法创建出一个没有原型的对象 |
||||
|
||||
![[图解 JavaScript 的原型关系.jpg]] |
||||
|
||||
![[深入 JavaScript 原型思考.png]] |
||||
参考文档: |
||||
- [\_\_proto\_\_ 和 prototype 到底有什么区别](https://juejin.cn/post/6844903869428793358) |
||||
- [所有javascript对象都有prototype还是仅仅函数对象有prototype?](https://segmentfault.com/q/1010000007980024) |
||||
- [ECMAScript5.1中文版](http://yanhaijing.com/es5/#book) |
||||
- [MDN Object.prototype.\_\_proto\_\_](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/proto) |
@ -1,62 +0,0 @@
@@ -1,62 +0,0 @@
|
||||
关于 React 中 Context 作为状态管理工具的思考具体可以这一篇 [[简单的 React 思考 - 状态管理工具]] |
||||
这里主要是对 Context API 的简单记录 |
||||
|
||||
|
||||
### React.createContext / Context.Provider |
||||
```javascript |
||||
const MyContext = React.createContext(defaultValue) |
||||
``` |
||||
|
||||
createContext 会创建一个 Context 对象,每个 Context 对象都会返回一个 Provieder 组件,它的子组件会订阅 Context 的变化 |
||||
|
||||
```javascript |
||||
<MyContext.Provider value={value} /> |
||||
``` |
||||
|
||||
订阅了 Context 的组件会在组件树中查找离自己最近的 Provider 中读取到 Context 值也就是 Provider 中的 value 属性,只有在找不到 Provider 时,createContext 中的 defaultValue 参数才会生效,但是将 underfined 传递给 Provider 的时候,defaultValue 并不会生效。 |
||||
多个 Provider 可以嵌套使用,里层的会覆盖外层的数据。 |
||||
当 Provider 中的 value 值发生变化时,它内部的所有消费组件也就是子组件都会重新渲染,这个用于判断值是否发生变化的方法和 Object.is 使用了同样的算法, 也就是说如果 value 是一个引用类型,可能会导致一些意外的问题。 |
||||
|
||||
|
||||
### Class.contextType / Context.Consumer |
||||
在 Hook 之前,在 React 组件中使用和消费 Context 的值可以使用 contextType 和 Consumer。 |
||||
|
||||
```javascript |
||||
class MyClass extends React.Component { |
||||
render() { |
||||
const value = this.context; |
||||
} |
||||
}; |
||||
|
||||
MyClass.contextType = MyContext; |
||||
``` |
||||
|
||||
contextType 属性可以让类组件使用 this.context 来获取 **最近**的 Context 上的值,并且可以在任何生命周期中访问到它 |
||||
|
||||
```javascript |
||||
<MyContext.Consumer> |
||||
{ value => /* 基于 context 值进行渲染*/ } |
||||
</MyContext.Consumer> |
||||
``` |
||||
|
||||
Consumer 是函数式组件订阅 Context 的方法。 |
||||
|
||||
|
||||
### Hook |
||||
之前说了,contextType 和 Consumer 都是在 Hook 出现之前订阅和消费 Context 的方法,那么在 Hook 出现之后,函数式组件自然也有了 Hook 的方法用于消费 Context |
||||
|
||||
```javascript |
||||
const value = useContext(MyContext); |
||||
``` |
||||
|
||||
useContext 接受一个 Context 对象,并返回该 Context 的当前值,和 contextType 中相同,Context 上的值由最近的 Provider 决定,当组件最上层的 Provider 更新时,该 Hook 会触发重新渲染,并使用最新传递的 value,且无视上层父组件使用了 React.memo,也就是说使用了 useContext 的组件总会在 context 值变化的时候重新渲染。 |
||||
如果重新渲染组件的开销较大,可以将组件放在 useMemo 中来优化 |
||||
|
||||
```javascript |
||||
return useMemo(() => { |
||||
const appContextValue = useContext(AppContext); |
||||
return <ExpensiveTree className={appContextValue.theme} />; |
||||
}, [theme]) |
||||
``` |
||||
|
||||
useContext Hook 相当于 class 组件中 Class.contextType / Context.Consumer,但是对于函数式组件,提供了更加优雅的 Context 使用方案,但是只是提供了 Context 的读取和订阅,你仍然需要在上层组件树中使用 Context.Provider |
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
首先先来看一段代码 |
||||
|
||||
![[简单的 React 思考 - Fiber 创建.png]] |
||||
|
||||
这是 React 中创建 memorizedState 链表的过程,注意其中 if 代码块中,赋值都使用到了两个等于号,因为这个函数的作用是在 Fiber 链表中创建一个新的 memorizedState 对象,那么这个函数的最终目的也是需要把创建好的 memorizedState 给返回出来,这里的两个连续等于号就相当于一直引用最新 Fiber 节点的 memorizedState 对象,最终给返回出来,看懂的时候感觉是真的牛逼 |
||||
|
||||
简单的源码解析: |
||||
1. 创建一个 hook 对象,这个创建的 hook 对象最终需要插入到 Fiber 节点中,用 next 连接起来形成一个链表,链接的下一个节点就是下一个被使用的 hook |
||||
2. 判断 workInProgressHook 是否为空,这个 workInProgressHook 永远指向最新的 Fiber 节点中的 memorizedState |
||||
- 如果为空表示当前是 Fiber 的第一个 hook,那么就会在当前 Fiber 中初始化 memorizedState 属性并传入创建的 hook 对象用于初始化,并返回最新的 Fiber memorizedState 对象的引用 |
||||
- 如果不为空,表示之前已经初始化过 hook,会在最新的 Fiber 中创建新的链接 next,并传入创建的 hook 对象用于初始化,返回最新的 Fiber memorizedState 对象的引用 |
@ -1,28 +0,0 @@
@@ -1,28 +0,0 @@
|
||||
useCallback 和 useMemo 都是 React 里边比较简单的 Hook,先看看官网对于这两个 Hook 的介绍。 |
||||
|
||||
```javascript |
||||
// useCallback |
||||
const memoizedCallback = useCallback( |
||||
() => { doSomething(a, b) }, |
||||
[a, b] |
||||
); |
||||
|
||||
// useMemo |
||||
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); |
||||
``` |
||||
|
||||
useCallback 会返回一个 memoized 函数,而 useMemo 会返回一个 memoized 值 |
||||
他们的第二个参数接受一个依赖项数组,回调函数只会在依赖项发生改变的时候才会更新,它的比较算法和 Object.is 的一致,即引用对比。 |
||||
他们都是作为性能优化的一个方法提供,他们的不同之处在于,useCallback 返回一个函数,而useMemo 会返回一个值,这个值由传入 useMemo 的回调函数在渲染期间执行。 |
||||
|
||||
上 React 源码,结合[[简单的 React 思考 - Fiber 创建]]一起看 |
||||
|
||||
useCallback |
||||
![[useCallback.png]] |
||||
|
||||
useCallback 在 mount 和 update 阶段都会对 deps 也就是依赖项做简单的 underfined 判断,在 update 的时候会用 areHookInputsEqual 对依赖项做对比,如果依赖项没有变就返回 memorizedState 中存储的回调函数 |
||||
|
||||
useMemo |
||||
![[useMemo.png]] |
||||
|
||||
和 useCallback 相同,useMemo 也会在 mount 和 update 的时候对 deps 依赖项参数做简单的underfined 判断,但是与 useCallback 在 mounte 阶段直接将 函数和 deps 一起存入 memoizedState 不同,useMemo 会执行传入的回调函数,并将函数的返回值和 deps 一起存入memoizedState,在 update 阶段的处理和 useCallback 几乎是一样的。 |
@ -1,44 +0,0 @@
@@ -1,44 +0,0 @@
|
||||
在一些需要连续更新状态的场景下,useReducer 会比 useState 更加的合适 |
||||
|
||||
```javascript |
||||
const [state, dispatch] = useReducer(reducer, initialArg, init); |
||||
``` |
||||
|
||||
React 官网的 useReducer |
||||
|
||||
```javascript |
||||
const initialState = {count: 0}; |
||||
|
||||
function reducer(state, action) { |
||||
switch (action.type) { |
||||
case 'increment': |
||||
return {count: state.count + 1}; |
||||
case 'decrement': |
||||
return {count: state.count - 1}; |
||||
default: |
||||
throw new Error(); |
||||
} |
||||
} |
||||
|
||||
function Counter() { |
||||
const [state, dispatch] = useReducer(reducer, initialState); |
||||
return ( |
||||
<> |
||||
Count: {state.count} |
||||
<button onClick={() => dispatch({type: 'decrement'})}>-</button> |
||||
<button onClick={() => dispatch({type: 'increment'})}>+</button> |
||||
</> |
||||
); |
||||
} |
||||
``` |
||||
|
||||
最简的 useReducer 实践 |
||||
|
||||
```javascript |
||||
const [state, dispatch] = useReducer((state, action) => ( |
||||
{ ...state, action }), |
||||
initState |
||||
) |
||||
``` |
||||
|
||||
在这个最简单的事件中,useReducer 就充当了 useState 的替代,dispatch 修改状态,返回最新的 state |
@ -1,12 +0,0 @@
@@ -1,12 +0,0 @@
|
||||
# 简单的 React 思考 - 状态管理工具 |
||||
|
||||
在组件间传值是一个老生常谈的问题,对于大型且复杂的项目来说,有许多优秀的状态管理库例如: Redux、Mobx之类的工具帮助统一的管理和分发状态,但是在小型的个人项目当中,引入复杂的状态管理不但不会提高项目的开发效率还可能会提高开发者的心智负担,如果只是需要简单的在兄弟组件层级中传递参数,React 也提供了 Context 这样的方案用于组件共享值,在这里是对常见的状态管理工具做一个简单的思考 |
||||
|
||||
## 1. Redux |
||||
Redux 是我在写 React 应用的时候状态管理的首选方案,只是因为我比较熟悉,Redux 可以应用但不限于在 React 框架甚至还可以运用在 Vue 和 VanillaJS 当中,Redux 的再封装库非常多,React-Redux、Redux-Toolkit、Dva等 |
||||
|
||||
## 2.Context |
||||
Context 是 React 官方提供的组件间传值共享值的解决方案,对于一些小心应用,Context 就已经足够满足状态传递的需求,但是这也会导致一些问题,比如说组件的复用性变差,React 官网中也有写到:如果你只是想避免使用状态提示的过程中出现的值层层传递的问题,可能控制反转的组件组合会相较于 Context 更加适合,什么是组件组合?就是说原先在组件间层层传递的参数,变为传递一个组件自身,这种对组件的控制反转减少了需要传递的 props 数量,关于 Context 的更深入的思考可以看这里 [[简单的 React 思考 - Context]] |
||||
|
||||
## 3. React Query / Swr |
||||
这两个工具实际上相较于前两个有较大的不同,Redux 和 Context 可以同时管理用户交互的中间状态,但是对于这两个工具而言,主要是负责服务端状态的管理,或许也可以将其称作为管理缓存。 |
@ -1,59 +0,0 @@
@@ -1,59 +0,0 @@
|
||||
![[纯JS实现下拉加载.jpg]] |
||||
|
||||
## 写在前面 |
||||
|
||||
>最近在深入学习Vue框架,想试着模仿[echo回声](https://www.app-echo.com/ "echo回声")的移动端网页写一个网页出来,参考了 uncleLian 大佬的[vue2-echo](https://github.com/uncleLian/vue2-echo "uncleLian/vue2-echo")的一些实现。在这之中遇到了一个问题:uncleLian大佬使用Mint UI组件库来实现页面的下拉加载更多功能,但是我的项目并没有使用Mint UI,所以自然这个功能我就没法直接使用,没有就没有吧,大不了自己动手写,于是就有了这么一篇文章。 |
||||
|
||||
## 实现思路 |
||||
|
||||
实现的思路很简单,就是判断 滚动高度 + 可视高度 是否大于文档高度 |
||||
|
||||
```javascript |
||||
const { pageYOffset, innerHeight } = window |
||||
pageYOffset + innerHeight >= document.documentElement.scrollHeight |
||||
``` |
||||
|
||||
如果判断结果为真,就向后端请求更多数据,再使用push方法添加新请求返回的数据 |
||||
|
||||
## 实现过程 |
||||
|
||||
实现每次下拉都返回不同的数据,就要对内容进行分页,根据页数返回不同的数据,但是这个项目并没有后端,后端返回的数据用的是 Mock.js 模拟,那么问题来了 Mock.mock 在拦截 GET 请求的时候是无法拦截到 url 后面带的参数的,这样一来就没法在 Mock.js 对传入的参数进行判断,这时候有两种解决思路,一种是数据分6页就写6个 Mock.mock 分别拦截不同的页数,使用这种方法的代码会很冗余,还有一种是 uncleLian 大佬的实现方法:使用 for 循环一次生成 6个 Mock.mock 拦截不同的页数 |
||||
|
||||
```javascript |
||||
for (let Page = 1; Page <= 6; Page++) { |
||||
let params = { |
||||
'data': [] |
||||
} |
||||
Mock.mock(`${baseURL}/list?Page=${Page}`, function () { |
||||
return params |
||||
}) |
||||
} |
||||
``` |
||||
|
||||
这样一来就实现了 Mock.js 对 GET 请求分页参数的模拟,光是这样还不行,需要对mock模拟的数据进行拆分,根据页数的不同返回的内容也不同,这里用到了array.prototype.slice() 方法对数据进行拆分 |
||||
|
||||
```javascript |
||||
for (let Page = 1; Page <= 6; Page++) { |
||||
let params = { |
||||
'data': [] |
||||
} |
||||
Mock.mock(`${baseURL}/list?Page=${Page}`, function () { |
||||
params.data = listJson.slice((Page - 1) * 6, Page * 6) |
||||
return params |
||||
}) |
||||
} |
||||
``` |
||||
|
||||
获取到了返回的分页数据,下一步就是要把数据给渲染出来,这里直接使用 push(params) 添加数据是不行的,因为params是一个数组,这里需要用到JS的展开运算 |
||||
|
||||
```javascript |
||||
this.MusicListJson.push(...params); |
||||
``` |
||||
|
||||
这样就完成了请求数据的添加操作 |
||||
|
||||
## 最后 |
||||
|
||||
解决了实际代码编写时可能遇到的一些难点,剩下的就是添加页数控制,事件触发之类的操作,这样无插件实现JS下拉加载的功能就完成了。 |
||||
|
||||
~~果然使用插件会更方便一些~~ |
@ -1,48 +0,0 @@
@@ -1,48 +0,0 @@
|
||||
![[Vue_on&emit.png]] |
||||
|
||||
## 写在前面 |
||||
|
||||
**阅读Vue的源码对深入学习Vue框架是非常有必要的,只知道它表面的用法,而不知道它内部的原理那就说不上是真正熟悉掌握了这一门框架,常常出了问题一头雾水** |
||||
|
||||
**Vue的\$emit、\$on和 Node.js 的 EventEmitter 的使用方法非常类似。** |
||||
|
||||
## 正式开始 |
||||
|
||||
### \$on绑定事件 |
||||
|
||||
> Vue中的\$on是一种将函数与事件绑定的方法。 |
||||
|
||||
通过Vue的源代码可以看到,\$on 调用时需要两个参数,第一个是字符串或数组类型作为事件的名称,第二个是触发事件执行的回调函数。\$on 第一步会对第一个参数做一个是否为数组的判断,这里可以看出 \$on 允许为多个事件绑定同一个回调函数。 |
||||
|
||||
如果参数为数组,那么它会使用for遍历这个数组,并进行递归。 |
||||
如果参数为字符串,那么它会在已有的事件(\_events)中寻找是否已经存在相同的事件,如果没有会创建并初始化为空数组同时将传递的第二个参数也就是事件被触发时需要执行的回调函数添加进去。 |
||||
|
||||
![[$on方法.png]] |
||||
|
||||
> 总结:$on方法允许为多个事件绑定同一个回调函数,还在里面使用了递归~~自己日自己~~ |
||||
|
||||
|
||||
|
||||
### $emit 触发事件 |
||||
|
||||
> Vue中的$emit是一种触发事件的方法。 |
||||
|
||||
它接受的第一个参数是一个字符串类型,这个字符串参数也就是需要触发的事件名称,第二个参数是触发事件时需要传递的数据。 |
||||
|
||||
第一步\$emit会将传递的事件名称转换成小写,并进行一系列的检查判断是否符合语法标准,然后它会在事件列表(\_events)中寻找事件对应的回调函数,也就是在\$on中传递并添加的回调函数,这里如果没有找到会直接返回,找到了会先进行一次判断,判断回调函数是否大于1,如果大于就把它转换成一个数组。 |
||||
|
||||
这时候第一个参数事件名已经不再需要了,\$emit会将其舍弃,并把剩下的数据转换成数组。传递给一个带有 try&catch 的 invokeWithErrorHandling() 方法,并在其中使用apply() 或 call() 来调用事件回调函数。 |
||||
|
||||
![[调用invokeWithErrorHandling方法.png]] |
||||
|
||||
**invokeWithErrorHandling()方法源代码** |
||||
|
||||
![[invokeWithErrorHandling方法.png]] |
||||
|
||||
>总结:用$emit方法触发事件允许一个事件有多个回调函数,它们会变成数组被遍历,同时也允许携带参数,它使用了apply()或call()更改作用域来保证参数有效 |
||||
|
||||
**( 注意:看源代码可以发现如果回调函数中有返回,invokeWithErrorHandling()方法是能拿到并会返回res,但是$emit中没有对这个返回的res进行赋值。)** |
||||
|
||||
## 最后 |
||||
|
||||
别看了,这里我实在不知道写什么好了 |