1
0
Fork 0
Browse Source

vault backup: 2023-02-15 10:08:54

Canvas
YuJian920 2 years ago
parent
commit
3d9f431a10
  1. 1
      .gitignore
  2. 2
      English Language/查不到的短语们.md
  3. 1
      FrontEnd 那些事/FrontEnd 那些事的碎片记录.md
  4. 10
      FrontEnd 那些事/深入探索 - 浏览器.md
  5. 4
      Linux 的那些事/Linux 日志位置.md
  6. 8
      README.md
  7. 1
      React 的那些事/React 开发碎片总结.md
  8. 6
      React 的那些事/React 性能优化碎片记录.md
  9. 5
      React 的那些事/奇怪的疑惑.md
  10. 2
      数据结构与算法之美/奇怪的疑惑.md
  11. 21
      数据结构与算法之美/数据结构 - 二叉树.md
  12. 6
      数据结构与算法之美/数据结构 - 堆.md
  13. 6
      数据结构与算法之美/数据结构 - 栈.md
  14. 91
      数据结构与算法之美/数据结构 - 链表.md
  15. 37
      数据结构与算法之美/数据结构 - 队列.md
  16. 6
      数据结构与算法之美/数据结构与算法 - 先导.md
  17. 7
      数据结构与算法之美/数据结构和算法的碎片记录.md
  18. 22
      数据结构与算法之美/时间和空间复杂度.md
  19. 25
      数据结构与算法之美/算法之美 - 二分.md
  20. 30
      数据结构与算法之美/算法之美 - 动态规划.md
  21. 14
      数据结构与算法之美/算法之美 - 排序.md
  22. 17
      数据结构与算法之美/算法之美 - 深度优先遍历.md
  23. 0
      数据结构与算法之美/算法之美 - 贪心.md
  24. 10
      数据结构与算法之美/算法之美 - 递归.md
  25. 32
      深入探索 JavaScript/JavaScript 对象.md
  26. 94
      深入探索 JavaScript/JavaScript 执行.md
  27. 88
      深入探索 JavaScript/JavaScript 类型.md
  28. 57
      深入探索 JavaScript/JavaScript 语法.md
  29. 2
      深入探索 JavaScript/奇怪的疑惑.md
  30. 19
      深入探索 JavaScript/碎片记录.md
  31. 82
      深入探索 React/Fiber 数据结构.md
  32. 14
      深入探索 React/React 源码的碎片记录 II.md
  33. 35
      深入探索 React/React 源码的碎片记录.md
  34. 59
      深入探索 React/React 的流程解析 - Fiber 递归/React 的流程解析 - Fiber 递归.md
  35. 250
      深入探索 React/React 的流程解析 - Fiber 递归/React 的深入探索 - beginWork.md
  36. 84
      深入探索 React/React 的流程解析 - Fiber 递归/React 的深入探索 - completeWork.md
  37. 77
      深入探索 React/React 的流程解析 - Fiber 递归/React 的深入探索 - createWorkInProgress.md
  38. 5
      深入探索 React/React 的流程解析 - Fiber 递归/React 的深入探索 - effectList 链表.md
  39. 1
      深入探索 React/React 的流程解析 - FiberRootNode/React 的流程解析 - FiberRootNode.md
  40. 230
      深入探索 React/React 的流程解析 - commit 阶段/React 的流程解析 - commit阶段.md
  41. 11
      深入探索 React/React 的流程解析 - commit 阶段/React 的深入探索 - commitBeforeMutationEffects.md
  42. 25
      深入探索 React/React 的流程解析 - commit 阶段/React 的深入探索 - commitLayoutEffects.md
  43. 84
      深入探索 React/React 的流程解析 - commit 阶段/React 的深入探索 - commitMutationEffects.md
  44. 22
      深入探索 React/React 的流程解析 - commit 阶段/React 的深入探索 - commitRootImpl.md
  45. 15
      深入探索 React/奇怪的疑惑.md
  46. 23
      纪世路/2021 年度总结 - 长风破浪会有时 直挂云帆济沧海.md
  47. 1
      纪世路/写在 2022 的迷惘与渴望中.md
  48. 6
      纪世路/零零碎碎.md
  49. 8
      编译世界/The-Super-Tiny-Compiler 学习笔记.md
  50. BIN
      随时随地/Images/useCallback.png
  51. BIN
      随时随地/Images/useEffect 和 Debounce.png
  52. BIN
      随时随地/Images/useMemo.png
  53. BIN
      随时随地/Images/welcome-to-vue.png
  54. BIN
      随时随地/Images/图解 JavaScript 的原型关系.jpg
  55. BIN
      随时随地/Images/深入 JavaScript 原型思考.png
  56. BIN
      随时随地/Images/简单的 React 思考 - Fiber 创建.png
  57. BIN
      随时随地/Images/纯JS实现下拉加载.jpg
  58. BIN
      随时随地/Images/通过源代码解析Vue 中的 $on、$emit 实现原理/$on方法.png
  59. BIN
      随时随地/Images/通过源代码解析Vue 中的 $on、$emit 实现原理/Vue.directive.png
  60. BIN
      随时随地/Images/通过源代码解析Vue 中的 $on、$emit 实现原理/Vue_on&emit.png
  61. BIN
      随时随地/Images/通过源代码解析Vue 中的 $on、$emit 实现原理/invokeWithErrorHandling方法.png
  62. BIN
      随时随地/Images/通过源代码解析Vue 中的 $on、$emit 实现原理/调用invokeWithErrorHandling方法.png
  63. BIN
      随时随地/Images/零基础教你用WordPress搭建个人网站/1.png
  64. BIN
      随时随地/Images/零基础教你用WordPress搭建个人网站/2.png
  65. BIN
      随时随地/Images/零基础教你用WordPress搭建个人网站/4.png
  66. BIN
      随时随地/Images/零基础教你用WordPress搭建个人网站/5.png
  67. BIN
      随时随地/Images/零基础教你用WordPress搭建个人网站/6.png
  68. BIN
      随时随地/Images/零基础教你用WordPress搭建个人网站/Tenxunyun.png
  69. BIN
      随时随地/Images/零基础教你用WordPress搭建个人网站/welcome-to-wp.png
  70. 7
      随时随地/JavaScript 函数的简单深入探索(一).md
  71. 1
      随时随地/Nodejs 如何执行 shell 命令学习笔记.md
  72. 340
      随时随地/Nodejs 文件系统(fs)学习笔记.md
  73. 324
      随时随地/Nodejs 文件系统(path)学习笔记.md
  74. 203
      随时随地/Yan Ruyu_Vue学习笔记.md
  75. 11
      随时随地/useEffect 和 Debounce.md
  76. 104
      随时随地/使用 Context + useEffect 实现管理后台中的 Tab 功能.md
  77. 13
      随时随地/关于 bind 的使用探索.md
  78. 19
      随时随地/博客 Next.js 迁移记录.md
  79. 9
      随时随地/奇怪的疑惑.md
  80. 6
      随时随地/异想天开.md
  81. 29
      随时随地/深入 JavaScript 原型思考.md
  82. 62
      随时随地/简单的 React 思考 - Context.md
  83. 11
      随时随地/简单的 React 思考 - Fiber 创建.md
  84. 28
      随时随地/简单的 React 思考 - useCallback和useMemo.md
  85. 44
      随时随地/简单的 React 思考 - useReducer.md
  86. 12
      随时随地/简单的 React 思考 - 状态管理工具.md
  87. 59
      随时随地/纯JS实现下拉加载.md
  88. 48
      随时随地/通过源代码解析 Vue 中的 $on、$emit 实现原理.md
  89. 173
      随时随地/零基础教你用WordPress搭建个人网站.md

1
.gitignore vendored

@ -1 +0,0 @@ @@ -1 +0,0 @@
.obsidian/workspace

2
English Language/查不到的短语们.md

@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
- it’s also important to realize 认识到这一点也很重要
- some other 其他一些

1
FrontEnd 那些事/FrontEnd 那些事的碎片记录.md

@ -1 +0,0 @@ @@ -1 +0,0 @@
- 正则表达式匹配所有中文:`^[\u4e00-\u9fa5]{2}$`

10
FrontEnd 那些事/深入探索 - 浏览器.md

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
## 工作流程
1. 浏览器首先使用 HTTP 协议或者 HTTPS 协议,向服务端请求页面;
2. 把请求回来的 HTML 代码经过解析,构建成 DOM 树;
3. 计算 DOM 树上的 CSS 属性;
4. 最后根据 CSS 属性对元素逐个进行渲染,得到内存中的位图;
5. 一个可选的步骤是对位图进行合成,这会极大地增加后续绘制的速度;
6. 合成之后,再绘制到界面上。
从 HTTP 请求回来,就产生了流式的数据,后续的 DOM 树构建、CSS 计算、渲染、合成、绘制,都是尽可能地流式处理前一步的产出:即不需要等到上一步骤完全结束,就开始处理上一步的输出,这样我们在浏览网页时,才会看到逐步出现的页面。

4
Linux 的那些事/Linux 日志位置.md

@ -1,4 +0,0 @@ @@ -1,4 +0,0 @@
/var/log/message 一般信息和系统信息
/var/log/secure 登陆信息
/var/log/maillog mail记录
/var/log/wtmp 登陆记录信息(last命令即读取此日志)

8
README.md

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
# 这是一篇使用 Obsidian 写的笔记
其实我没有自建个人知识库的打算,我只是想找一款趁手的界面美观的笔记软件,最好支持私有化部署,前前后后我总共试过不少笔记软件,最近一个最深得我心的是 Trilium,它支持私有化部署,还是用我吃饭的语言 JavaScript 写的,但是很遗憾,它的界面并不能让我满意,在经过一两个小时的新鲜体验之后就从我的服务器上消失了,我又开始了寻找一款优秀的还正好戳中我审美的笔记软件的路程,其实在这之前我一直都在使用 OneNote 写笔记,直到现在我也觉得没有什么问题,相当强大的云同步和全平台优势,至于为什么要寻找一个新的笔记软件,那可能就是刻在DNA里面的那一丝丝“折腾”的精神,现在决定选中 Obsidian + Git 的方式管理笔记,也正好最近在服务器上搭建了私有的 Gitea 服务,这不得用起来?
不得不说 Markdown 写笔记确实是舒服,学习成本低,排版相对容易,用 OneNote 的时候总会考虑对齐的问题,但是这不代表以后就会用 OneNote,在它的优势下依然会是我首选的笔记工具,只是在一些场景会更细化,两两结合
最后按照惯例
# Obsidian 牛逼!!

1
React 的那些事/React 开发碎片总结.md

@ -1 +0,0 @@ @@ -1 +0,0 @@
- useMemo 除了用于减少子组件的重复渲染,还可以用于避免重复计算

6
React 的那些事/React 性能优化碎片记录.md

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
- 父组件命中性能优化,子组件才有可能命中性能优化
- 没有传递 props 的时候,子组件接收到的 props 实际上是一个空对象
- React Context 内的数据发生变化时,只有使用了 useContext 的组件会发生 render
- 当父组件的状态发生变化 使用了 useContext 的组件会发生 render,即使它的 props 并没有发生变化
- 组件的状态尽可能的下放,粒度尽可能的精细,可以尽量减少组件的重复 render
- 有时候使用控制反转也就是直接使用 children props 传递子组件可以减少重复 render

5
React 的那些事/奇怪的疑惑.md

@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
- [ ] 为什么说父组件直接调用子组件的函数,会让执行过程处于生命周期以外
- [ ] React.memo 的作用和实际发生了什么?
- [ ] useMemo 的前提是当前组件必须是浅比较,为什么?
- [ ] 当使用了组件反转之后,children 传递的组件属于谁的子组件,React 源码中又是如何处理的
- [ ] React Context Get 和 Set 分离是否有用?

2
数据结构与算法之美/奇怪的疑惑.md

@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
- [ ] 什么是排序算法的稳定性
- [ ] 什么是原地排序算法

21
数据结构与算法之美/数据结构 - 二叉树.md

@ -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 树来组织数据

6
数据结构与算法之美/数据结构 - 堆.md

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
>1. 完全二叉树
>2. 最大堆:父节点 >= 子节点
>3. 最小堆:子节点 <= 父节点
>4. 逻辑结构是一颗二叉树,物理结构上是一个数组
## 堆栈模型

6
数据结构与算法之美/数据结构 - 栈.md

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
>栈是一种逻辑结构,从栈的操作特性来看,它是一种“操作受限”的线性表。
>后进者先出,先进者后出,这就是典型的“栈”结构。
当某个数据集合只涉及在一端插入和删除,并且满足后进先出、先进后出的特性,我们就应该首选“栈”这种数据结构。
由于栈只是一个逻辑结构,所以它既可以用数组实现,也可以用链表来实现,数组实现的栈,叫做**顺序栈**,而链表实现的栈,叫做**顺序栈**。

91
数据结构与算法之美/数据结构 - 链表.md

@ -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 缓存淘汰策略

37
数据结构与算法之美/数据结构 - 队列.md

@ -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;
}
}
```

6
数据结构与算法之美/数据结构与算法 - 先导.md

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
>数据结构就是数据的存储形式,算法就是对数据的一系列操作
一般而言,我们遇到一个算法问题,可以大致分为如下三个步骤
- 第一步是建立算法模型
- 第二步则是根据算法模型分析我们需要对数据进行哪些操作(增删改)
- 第三步,我们需要分析哪些操作最频繁,对算法的影响最大。最后,我们需要根据第三步的分析结果选择合适的数据结构

7
数据结构与算法之美/数据结构和算法的碎片记录.md

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
- 要有对时间复杂度的敏感性,如 length 不能遍历查找
- 数据结构的选择,要比算法优化更重要
- 凡有序,必二分
- 凡二分,时间复杂度必包含 O(logn)
- 优化嵌套循环,可以考虑双指针
- 二叉搜素树可以使用二分法快速查找
- 分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧

22
数据结构与算法之美/时间和空间复杂度.md

@ -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) 数据量 * 数据量的对数

25
数据结构与算法之美/算法之美 - 二分.md

@ -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;
};
```
## 两数之和

30
数据结构与算法之美/算法之美 - 动态规划.md

@ -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;
};
```

14
数据结构与算法之美/算法之美 - 排序.md

@ -1,14 +0,0 @@ @@ -1,14 +0,0 @@
对于排序算法执行效率的分析,我们一般会从这几个方面来衡量:
1. 最好情况、最坏情况、平均情况时间复杂度
2. 时间复杂度的系数、常数 、低阶
3. 比较次数和交换(或移动)次数
## 冒泡排序
## 插入排序
## 选择排序
## 归并排序 (Merge Sort)
## 快速排序 (Quick Sort)

17
数据结构与算法之美/算法之美 - 深度优先遍历.md

@ -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);
}
```

0
数据结构与算法之美/算法之美 - 贪心.md

10
数据结构与算法之美/算法之美 - 递归.md

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
只要同时满足以下三个条件,就可以用递归来解决:
1. 一个问题的解可以分解为几个子问题的解
2. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
3. 存在递归终止条件
写递归代码最关键的是写出递推公式,找到终止条件
编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤
递归代码虽然简洁高效,但是,递归代码也有很多弊端。比如,堆栈溢出、重复计算、函数调用耗时多、空间复杂度高等,所以,在编写递归代码的时候,一定要控制好这些副作用。

32
深入探索 JavaScript/JavaScript 对象.md

@ -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\]\] 的返回值是对象则返回该对象,否则返回第一步创建的对象
以上也是出现闭包的背景原因,因为如果构造函数返回一个对象,那么步骤一创建的新对象就会变成一个除了构造函数之外完全无法访问的对象。

94
深入探索 JavaScript/JavaScript 执行.md

@ -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

88
深入探索 JavaScript/JavaScript 类型.md

@ -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
```

57
深入探索 JavaScript/JavaScript 语法.md

@ -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 的指令序言是只有一个字符串直接量的表达式语句,它只能出现在脚本、模块和函数体的最前面。

2
深入探索 JavaScript/奇怪的疑惑.md

@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
- [ ] JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而 JavaScript 中是无法自定义类型的,为什么?
- [ ] 什么是装箱操作?

19
深入探索 JavaScript/碎片记录.md

@ -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才生效

82
深入探索 React/Fiber 数据结构.md

@ -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 节点的优先值

14
深入探索 React/React 源码的碎片记录 II.md

@ -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 方法中去

35
深入探索 React/React 源码的碎片记录.md

@ -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

59
深入探索 React/React 的流程解析 - Fiber 递归/React 的流程解析 - Fiber 递归.md

@ -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 链表]]

250
深入探索 React/React 的流程解析 - Fiber 递归/React 的深入探索 - beginWork.md

@ -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 流程执行结束

84
深入探索 React/React 的流程解析 - Fiber 递归/React 的深入探索 - completeWork.md

@ -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 逻辑结束

77
深入探索 React/React 的流程解析 - Fiber 递归/React 的深入探索 - createWorkInProgress.md

@ -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 的逻辑就结束了。

5
深入探索 React/React 的流程解析 - Fiber 递归/React 的深入探索 - effectList 链表.md

@ -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
深入探索 React/React 的流程解析 - FiberRootNode/React 的流程解析 - FiberRootNode.md

@ -1 +0,0 @@ @@ -1 +0,0 @@
>在 React 开始阶段会创建整个 app 唯一的根 Fiber 节点:FiberRootNode

230
深入探索 React/React 的流程解析 - commit 阶段/React 的流程解析 - commit阶段.md

@ -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**

11
深入探索 React/React 的流程解析 - commit 阶段/React 的深入探索 - commitBeforeMutationEffects.md

@ -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 阶段结束之后以异步优先级进行执行

25
深入探索 React/React 的流程解析 - commit 阶段/React 的深入探索 - commitLayoutEffects.md

@ -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

84
深入探索 React/React 的流程解析 - commit 阶段/React 的深入探索 - commitMutationEffects.md

@ -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 属性

22
深入探索 React/React 的流程解析 - commit 阶段/React 的深入探索 - commitRootImpl.md

@ -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,~~这里没有看懂~~

15
深入探索 React/奇怪的疑惑.md

@ -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 链表]]

23
纪世路/2021 年度总结 - 长风破浪会有时 直挂云帆济沧海.md

@ -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
纪世路/写在 2022 的迷惘与渴望中.md

@ -1 +0,0 @@ @@ -1 +0,0 @@
在其他同龄人的大学生活还未结束的时候,我却过早的离开了校园,离开校园的每一刻我都在怀念曾经在校园里的时光,只可惜时光

6
纪世路/零零碎碎.md

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
小学的时候我就喜欢看书,什么书都喜欢看,什么冒险小虎队马小跳男生日记女生日记笑猫日记我都不挑,周末的时候没有什么事情干的时候就会在家附近的书店坐上一天只因为那里可以免费看书还不赶人,最猛的时候一天我能看完两三本,经常看到姐姐来叫我回家吃饭,但是你要说我那时候的语文成绩好不好其实也说不上好,就好像看过的书没有一点体现在了试卷上。
直到有一天好像是三年级的时候,老师布置了一个语文作业要我们回家要写一篇作文回来,题材是什么我现在已经记不清了,那时我对作文的影响可能还仅仅停留在多写成语的阶段,但是我挑了一个很有意思题材:我打算写一下家里的电脑,然后我就大笔一挥在纸上写下了作文的标题:初识电脑。
内容其实没有什么特别的,就是回忆了在家里第一次买电脑的时候内心的波涛汹涌和第一印象,但是我写着写着就好像进入了一个境界,越写越停不下来,笔下的文字就像是水里的鱼一样自然而然地跃动到纸面上,丝毫没有之前写作文绞尽脑汁都想不出两三个句子窘迫,最后停笔收尾一气呵成,我就玩去了。
作文交上去过了那么两三天,到了发回作文的时候,老师高兴的站在舞台上说:今天要表扬一位同学,他把作文写的非常的生动和通顺,下课的时候同学们可以去借他的作文学习学习,而这位同学就是我,瞬间我感觉春风满面,是我之前都没有得到过的荣誉,下课了班里的同学都来找我要作文“观摩”,听着同学们的称赞,我把作文纸留在桌子上,然后一个人走出教室,颇有一种深藏功与名的感觉
然后我就在所谓的“文学”路上一去不复返了

8
编译世界/The-Super-Tiny-Compiler 学习笔记.md

@ -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)

BIN
随时随地/Images/useCallback.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

BIN
随时随地/Images/useEffect 和 Debounce.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

BIN
随时随地/Images/useMemo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

BIN
随时随地/Images/welcome-to-vue.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

BIN
随时随地/Images/图解 JavaScript 的原型关系.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

BIN
随时随地/Images/深入 JavaScript 原型思考.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

BIN
随时随地/Images/简单的 React 思考 - Fiber 创建.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

BIN
随时随地/Images/纯JS实现下拉加载.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
随时随地/Images/通过源代码解析Vue 中的 $on、$emit 实现原理/$on方法.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

BIN
随时随地/Images/通过源代码解析Vue 中的 $on、$emit 实现原理/Vue.directive.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

BIN
随时随地/Images/通过源代码解析Vue 中的 $on、$emit 实现原理/Vue_on&emit.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

BIN
随时随地/Images/通过源代码解析Vue 中的 $on、$emit 实现原理/invokeWithErrorHandling方法.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

BIN
随时随地/Images/通过源代码解析Vue 中的 $on、$emit 实现原理/调用invokeWithErrorHandling方法.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
随时随地/Images/零基础教你用WordPress搭建个人网站/1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
随时随地/Images/零基础教你用WordPress搭建个人网站/2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
随时随地/Images/零基础教你用WordPress搭建个人网站/4.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

BIN
随时随地/Images/零基础教你用WordPress搭建个人网站/5.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

BIN
随时随地/Images/零基础教你用WordPress搭建个人网站/6.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

BIN
随时随地/Images/零基础教你用WordPress搭建个人网站/Tenxunyun.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

BIN
随时随地/Images/零基础教你用WordPress搭建个人网站/welcome-to-wp.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

7
随时随地/JavaScript 函数的简单深入探索(一).md

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
>JavaScript 的函数是一个老生常谈的问题,把这个关键词放在搜索引擎上能搜出大量相关的文章和解释,有无数的大佬前辈阐述的已经非常清楚,这篇文章实际上也只是 “走前辈路,述前人语”
我喜欢箭头函数比普通函数更多,因为我认为和普通函数相比,箭头函数更加的“纯粹”,我们这里先简单的从作用域和 this 的角度来讨论这个话题。
## This
对于 JavaScript 深入学习过的朋友应该都知道,**箭头函数并没有自身的 this 绑定,它会继承外层函数的 this 绑定**,而普通函数,this 的指向那就是千变万化,这也是很多 JS 初学者的噩梦,也很喜欢出现在面试官的嘴中,

1
随时随地/Nodejs 如何执行 shell 命令学习笔记.md

@ -1 +0,0 @@ @@ -1 +0,0 @@
>这篇文章是 [Executing shell commands from Node.js](https://2ality.com/2022/07/nodejs-child-process.html) 学习过程中的个人总结和笔记,大部分内容实际来自于原文章,如果你对 Nodejs 中的文件系统感兴趣,我更建议你直接去看原文章。

340
随时随地/Nodejs 文件系统(fs)学习笔记.md

@ -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")的部分

324
随时随地/Nodejs 文件系统(path)学习笔记.md

@ -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;
};
```

203
随时随地/Yan Ruyu_Vue学习笔记.md

@ -1,203 +0,0 @@ @@ -1,203 +0,0 @@
![[welcome-to-vue.png]]
### Vue基本认知
**Vue是一个JavaScript框架,它的特点在于使用者不用再思考DOM的操作,一切都由Vue来承担,这就是Vue的MVVM模式,使用Vue框架你只需要操作数据而不是DOM。**
Vue的v-for: 是Vue中一个简单的for循环,目前学到的课程中它可以用来遍历数组。
Vue的v-on: 是Vue中用于绑定事件的指令,它的作用是绑定一个事件,它可以简写成@。
Vue的v-bind: 用于传递数据,它可以帮你在组件之中传递值。
Vue的v-model: 数据的双向绑定。
Vue的el: 是Vue实例的“管辖范围”,它用于确定Vue实例的生效区域。
Vue的data: 是Vue实例的数据区域,它一般用于放置Vue实例中的数据内容。
Vue的methods: 用于放置Vue实例的方法,任何Vue中的使用的函数方法都要在其中定义。
### Vue 的深入理解
#### Vue的组件间传值
需要用到`props`、`v-bind`、`$emit`等方法的配合。
##### Vue的生命周期函数
Vue在实例生成之前和生成之后总共有8个生命周期函数,他们分别是:
* `beforeCreate`:在实例生成之前
* `created`:实例生成之后
* `beforeMount`:实例渲染之前
* `mounted`:实例渲染之后
* `beforeUpdata`:实例数据修改之前
* `updatad`:实例数据修改之后
* `beforeDestroy`:实例销毁之前
* `destroyed`:实例销毁之后
#### Vue的模板语法
Vue中的模板语法有插值表达式、`v-text`、`v-html`等等。
任何v-开头的Vue指令,后面接的都是JS的表达式,也就是说可以在后面接上加减乘除等JS语句,插值表达式也是如此。
#### 计算属性,方法和侦听器
##### 计算属性 ```computed```:
直接通过属性名就能使用,因为原因和上面说的一样:
>任何v-开头的Vue指令,后面接的都是JS的表达式。也就是说可以在后面接上加减乘除等JS语句。
>
缓存功能在计算的成员值不变的情况下将不会进行第二次计算,节省资源。
##### 方法methods:
也可以直接通过属性名使用(注意方法独有的括号),但是它没有计算属性的缓存功能,在计算成员值不变的情况下每次都会重新计算一次,在网页元素比较多的情况下可能会有比较明显的资源浪费。
##### 监听器watch:
可以监听指定的方法,当监听的方法发生变化时会执行指定的操作,保留了计算属性的缓存功能。
当以上三种方法同是适用的候,优先推荐计算属性 `computed`,既简洁,也保留了性能。
#### 计算属性的getter和setter
计算属性你可以使用 `Get`方法,通过其他值算出一个新值,也可以使用 `Set`方法,通过设置一个值改变相关联的值,从而使用计算属性的特性引起第二次计算。
#### Vue中的样式绑定
可以使用 `isActivated`来控制样式名字的显示和隐藏,在 `data``methods` 中设置 `isActivated` 为true就会绑定上CSS的样式。也可以使用数组来控制样式名字,设置数组的第一个值为空,标签就没有绑定CSS样式,设置数组第一个值为CSS样式名字则会绑定,还可以使用`V-bind:style="CSS样式名字"`的方式来进行绑定,这样CSS样式名字就是一个Vue的 `data` 值,可以在Vue的`data` 中控制它的属性,以此类推,这种方法后面也能接一个数组,更多样化。
#### Vue的条件渲染
##### v-if、v-else-if、v-else:
`v-if`是一个Vue的指令,它的值决定了它所在的这个DIV标签是否渲染显示,每次的隐藏和显示都相当于删除和增加一个DOM元素,它还能搭配 `v-else``v-else-if`一起组合使用(警告,`v-if` 和 `v-else`、`v-else-if`必须紧靠在一起使用!)。
由于Vue的特性,Vue在渲染网页的时候会尽量复用页面上已有的DOM,这可能会产生一些问题,为了解决这些问题就有`key`值这个概念,`key`值是独一无二的,Vue并不会复用key值不一样的DOM元素。
##### v-show:
`v-show``v-if`类似,但是它每次的隐藏和显示并非是在页面上删除和增加一个新的DOM,而是通过隐藏DOM标签,在相同的环境下,`v-show`所需要的性能相对要低于前者。
#### Vue的列表循环
##### key、数组操作:
当使用`v-for`遍历来实现列表渲染编写`key`值时,考虑到性能和一些未知的问题,尽量不要使用数组的下标作为`key`值,而是使用数组中一些独一无二的值。
当你直接对Vue的数组操作其下标时,数据会发生变化,但是页面不会发生变化,你需要使用Vue提供的7个数组操作方法来对数组进行操作,它们分别是:
* `push`:向数组的末尾添加一个元素
* `pop`:删除数组的最后一个元素
* `shift`:删除数组的第一个元素
* `unshift`:向数组的开头添加一个或多个元素
* `splice` 向数组中删除或添加元素
* `sort` 对数组进行排序
* `reverse` 对数组进行取反
还有通过改变数组的引用来实现对数据的操作,即直接改变遍历的数组,这种方法也能在数据变化的同时页面同时变化
##### 遍历对象:
列表渲染不但可以使用数组,也可以使用一个对象,使用的方式和数组相同,但是使用对象进行列表渲染时,网页并不会渲染对象的键名,而是直接显示对象的值,取而代替的是一个叫`key`的变量,其中对象的`index`和数组相同。
在使用对象进行列表渲染时,你无法使用Vue提供的7个数组操作方法,你可以直接修改对象里的数据,数组变的同时页面也会跟着发生变化,但是和数字相同:直接往对象里添加数据时,页面是不会发生变化,与之类似,你可以直接改变对象的引用来实现数据的变化页面跟着变。
#### 使用组件的细节点
当你使用Vue的组件功能实现组件化时,你定义了一个组件,这个组件的名字可能与HTML自有的标准不符,使用这些组件的时候会产生一些问题,这时Vue提供了 `is="组件名"` 的语法来解决这种问题,这种方法的作用就是在遵守HTML标准的同时使用你自定义的组件。
在组件中定义`data` 时,`data` 必须是一个函数而不是一个对象。
##### ref:
当你在HTML标签中使用ref时,你获取到的是 `ref` 对应标签的DOM元素,而当你在组件中使用 `ref` 时,你获取到的是子组件的引用。
#### 父子组件传值
##### 隐形的规定——单向数据流:
父组件可以随意的向子组件传值,子组件也可以修改父组件传递过来的值,但是这样可能会导致一些问题,所以在子组件中使用父组件传递来的值时,需要使用子组件自己的 `data` 来复制一份来自父组件的值。
##### emit():
子组件向父组件传值需要用的 `$emit()` 这个方法,这个方法不但可以向外触发事件,还可以在触发事件的同时传递一个值(向外触发的事件名可能不允许大写)。
#### 组件参数校验与非props特性
##### 参数校验:
父组件可以任意向子组件传值,子组件对传递过来的值可以进行一个校验,也就是对传递的值有一定的约束,要使用这个特性,子组件里的props就不再是一个数组,而是一个对象,在对象中可以对传递过来的值进行一个约束,约束的条件还可以是一个数组,甚至是一个对象。
`props` 是一个对象时,你可以使用 `validator` 这一个函数还对传递值进行校验,还可以使用 `default` 来规定一个未传值时的默认值,required来规定父组件必须传值,当然还有 `type` 来约束传递的值类型。
##### 非props特性:
当父组件向子组件传值的时候,子组件需要声明 `props` 来获取来自父组件的值,这个时候父组件传递的值并不会显示在子组件DOM中,但是子组件如果并没有声明 `props` 来获取父组件的值,那么这个值会显示在子组件的DOM中,这就是 `非props` 特性。
##### 给组件绑定原生事件
当你直接在子组件的中使用父组件的函数,这时候绑定的其实是组件的自定义事件,需要用 `$emit` 来向外触发和监听,你需要在子组件中的模板中绑定这个事件,这时候绑定和触发的才会是父组件的函数,如果你确实需要直接用子组件绑定父组件的事件,你可以在子组件的自定义事件后面添加 `.native`,这个时候绑定的也是父组件的函数。
#### 非父子组件间的传值
##### bus:
在使用Vue进行组件化传值的时候,会遇到这样一个问题:非父子组件间传值。如果按照传统的方法将值一层一层传递上去,这样写出来的代码将会非常冗长,这是就有了 `bus`这个概念,在使用 `bus`前你需要事先给 `Vue.prototype.bus`赋值一个新的Vue实例: `Vue.prototype.bus = new Vue()`,这句话的意思在 `Vue.prototype`上挂载了一个名叫 `bus`的属性,它指向一个Vue的实例。每当你去创建一个Vue实例或者一个组件时,都会有bus这个属性。
有了 `bus`,非父子组件间传值就变的非常容易了,就像子组件向父组件传值那样,传值组件中用 `bus`向外触发一个事件:`this.bus.$emit("事件名","数据")`。然后在Vue的8个生命周期钩子之一:`mounted`(实例渲染之后)中写一个函数,这个函数 `bus`中的 `$on`函数监听 `bus`向外触发的事件名和数据,再传递给接收组件。这样就完成了非父子组件间传值的需要。(警告:在接受组件的 `mounted`函数中操作数据时,`this`的作用域可能会发生变化,需要用 `var this_ = this`来保存 `this`的数据。)
#### 在Vue中使用插槽
##### 插槽:
子组件内的某些元素需要由来自父组件或者其他组件的值来决定时,子组件自带的模板似乎就不是那么好用,这时Vue就提供了一个新的功能:插槽。插槽使用的方法很简单,只需要在子组件的模板中添加一行要插入的内容,之后在 `template`中相应的位置插入 `<slot></slot>`就可以了,在这个元素中还可以加入默认值以表示插槽未定义时的值。
##### 具名插槽v-slot:
假如你需要由来自两个不同的组件的值来显示在不同的插槽位置时,单单`<slot></slot>`是不够的,为了解决这个问题,Vue有一个新的指令 `v-slot`,它用于给插槽指定一个名字,之后在 `template`使用`name="插槽名"`就可以实现不同位置显示不同的插槽的需求。(注意:`v-slot`只能写在模板占位符即 `template`中)
##### 作用域插槽slot_sope:
组件化的使用过程中还有可能遇到一种情况:当子组件的 DOM结构应该由外部传递进来的时候,这个时候就要使用作用域插槽来解决问题,子组件插槽可以向父组件插槽传数据,子组件用 `v-bind`传,父组件用 `slot-scope="属性名"`接受数据。
#### 动态组件与v-once指令
##### 动态组件:
在之前的笔记中可以看到:在一个 DOM元素中使用 `is="组件名"`的方式可以在不影响语法要求的情况下将原来的DOM元素替换为定义的组件,这种特性与`<component></component>`结合就成了Vue的动态组件(也可以不是`<component></component>`),在 `data`中定义一个变量,变量的值是组件名,通过改变这个变量的名为其他组件名来实现动态组件。
##### v-once:
在上面的方法每当 `is="组件名"`的值变化,在渲染显示一个组件的同时,Vue会将其他的未被渲染的组件隐藏销毁,也就是说用上面的方法每次动态组件切换时Vue就会渲染组件的同时和销毁隐藏的组件,如果频繁切换组件这种方法无疑会影响性能,`v-once`指令会将组件在隐藏时放入内存中,重新调用的时候再从内存中拿出来,通过 `v-once`指令,可以有效提高一些静态内容的展示效率。
#### Vue中的CSS动画原理
为Vue元素添加动画时,需要用`<transition>`标签包住元素,这样才能为Vue元素添加动画效果,当你不为`<transition>`标签指定一个`name`时,动画样式的前缀默认为`v-`,还可以在`<transition>`使用`-class`为`enter`和`leave`动画指定一个CSS样式名字,这样Vue就会使用你指定的样式名字。(这个时候就不在需要`name`)
Vue动画从开始到结束会有3个过程即从开始到结束和中间,进入过程:Vue会为这三个不同的过程中添加不同的CSS样式:`enter`、`enter-active`、`enter-to`。
与之相似,退出过程也会有三个CSS样式:`leave`、`leave-active`、`leave-to`。
##### 为什么使用enter作为动画的入场开始:
使用`enter`作为入场开始是因为动画在默认情况下`opacity`为1,这时需要在第一帧是将其写为0,在之后的进行中中过程中`enter`会被移除,这时`opacity`为0。
##### 为什么使用leave-to作为动画的出场开始:
使用`leave-to`作为出场开始是因为动画第一帧`opacity`需要为1,从第二帧开始`leave`被移除,这时`opacity`的值还是1,需要将其改为0,动画才会被正常触发。
#### 在Vue中使用animate.css库
库的动画是CSS3标准的动画。由于Vue可以更改CSS样式的名字,所以我们可以使用在实现复杂的动画时可以使用使用一些CSS库如animate.css,用`-class`为动画指定名字为CSS库的样式名字,实现CSS库的绑定。
#### 在Vue中同时使用过渡和动画
##### 元素第一次显示的动画效果:
当需要元素第一次显示时也有动画效果,需要在`<transition>`标签中写入两个属性:`appear`、`appear-active-class`,第一个表示第一次显示元素时也有动画效果,第二个表示动画效果的CSS规则,它的值就是CSS动画规则名。
##### 同时使用CSS库和过渡动画:
要实现这种效果,需要为在`<transition>`标签中为元素绑定两个CSS规则,分别为CSS库动画和过渡动画——`enter`和`leave`
##### CSS库和过渡动画执行时间不一致:
在使用CSS库的时候,而它往往都是固定的,而自定义的过渡效果可能和它不一样,这是可以在`<transition>`标签中写一个新的属性type,它的值决定了最终动画时长以谁为准(`animation`  `transition`
>**来自官方的解释**:Vue 为了知道过渡的完成,必须设置相应的事件监听器。它可以是 `transitionend`  `animationend`,这取决于给元素应用的 CSS 规则。如果你使用其中任何一种,Vue 能自动识别类型并设置监听。
>但是,在一些场景中,你需要给同一个元素同时设置两种过渡动效,比如 `animation` 很快的被触发并完成了,而 `transition` 效果还没结束。在这种情况中,你就需要使用 `type attribute` 并设置 `animation`  `transition` 来明确声明你需要 Vue 监听的类型。
##### 显性的过渡持续时间:
在大多数情况下,Vue可以自动得出动画所需要的执行时长,当然也可以用`duration`属性定制执行时长(这个属性需要用v-bind绑定,并且以毫秒表示)。`duration`属性的值还可以是多个,用{}符号包裹,这个情况下可以分别为`enter`和`leave`动画定制执行时长。
#### Vue中的JS动画
在Vue动画的开始和结束,有6个JS钩子,它们分别是:
`before-enter`:入场动画之前
`enter`:入场动画执行中
`after-enter`:入场动画之后
`before-leave`:出场动画之前
`leave`:出场动画执行中
`after-leave`:出场动画之后
#### Vue中多个元素或组件的过渡
Vue在渲染网页的时候会尽量复用页面上已有的`DOM`,在使用Vue的动画效果时可能会产生一些问题,这时候也需要为元素指定`Key`值,保证动画的正常执行。
`<transition>`标签中有一个属性叫:`mode`,它有两个值:`in-out`和`out-in`,`mode`的值为`in-out`时,动画会先显示再隐藏,而`out-in`则与之相反,不使用`mode`属性时,动画就会同时执行。多个组件的过渡动画可以使用动态组件来实现。
#### Vue的列表过渡
与单个元素使用动画效果不同,列表渲染相当于渲染多个元素的,这是`<transition>`标签不支持的,这时候就需要使用`<transition-group>`标签,这个标签相当于为每一个渲染的元素添加上一个`<transition>`标签。
#### Vue中的动画封装
在很多时候一个相同的动画会用上很多次,尽管他们的元素各不相同,这样一来,每为一个元素设定这个动画都需要重复编写`<transition>`,这是很不方便的,为了解决这个问题,我们可以使用Vue组件来对重复使用的动画进行封装。除此之外还可以将CSS规则进行封装,用Vue动画的6个JS钩子将它改写为JS动画写在组件的`methods`中。
#### 拓展
或许我可以用Vue的模板功能来生成一个带有Vue实例接管的模板,例如:
`template: "<li id="app">Hello Wrold</li>"`
在Vue框架的使用中,你可以使用一个叫`<template>`的新标签,这个标签并不会真的渲染到页面之上,但是它可以结合Vue指令来实现一些意想不到的效果,一个既不会真的渲染多一个标签但又确实存在的效果。
还有一个特别的方法能改变列表渲染时的值:Vue.set方法

11
随时随地/useEffect 和 Debounce.md

@ -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]]

104
随时随地/使用 Context + useEffect 实现管理后台中的 Tab 功能.md

@ -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 应该有一个右键菜单,可以实现关闭其他、关闭所有之类更多的功能,这里就不展开谈了,如果有时间的话后边会再写一篇文章补全上述提到的这些功能

13
随时随地/关于 bind 的使用探索.md

@ -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`

19
随时随地/博客 Next.js 迁移记录.md

@ -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) 就行了,缺点就是丑

9
随时随地/奇怪的疑惑.md

@ -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 原型思考]]

6
随时随地/异想天开.md

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
- [ ] 完整的个人事务管理系统,包括 TODO、提醒、记账、备忘录、活动清单,有想法集成定时脚本、运行指令、文件管理、docker 管理。
- [ ] 简单的文件管理,实现服务器指定目录的文件查看,删除。
- [ ] 集成多个音乐 API 的 AppleMusic 风格音乐播放器。
- [ ] 全平台同步的单词生词本,基于 Spaced repetition 或者 莫宾浩斯遗忘曲线 复习。
- [ ] 一个全自动的爬虫管理平台,支持爬虫脚本的启动和暂停、日志的查看和脚本定时执行。
- [ ] 微信机器人兼顾视频网站下载、telegram、twitter 下载、监控服务器信息。

29
随时随地/深入 JavaScript 原型思考.md

@ -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)

62
随时随地/简单的 React 思考 - Context.md

@ -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

11
随时随地/简单的 React 思考 - Fiber 创建.md

@ -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 对象的引用

28
随时随地/简单的 React 思考 - useCallback和useMemo.md

@ -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 几乎是一样的。

44
随时随地/简单的 React 思考 - useReducer.md

@ -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

12
随时随地/简单的 React 思考 - 状态管理工具.md

@ -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 可以同时管理用户交互的中间状态,但是对于这两个工具而言,主要是负责服务端状态的管理,或许也可以将其称作为管理缓存。

59
随时随地/纯JS实现下拉加载.md

@ -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下拉加载的功能就完成了。
~~果然使用插件会更方便一些~~

48
随时随地/通过源代码解析 Vue 中的 $on、$emit 实现原理.md

@ -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进行赋值。)**
## 最后
别看了,这里我实在不知道写什么好了

173
随时随地/零基础教你用WordPress搭建个人网站.md

@ -1,173 +0,0 @@ @@ -1,173 +0,0 @@
![[welcome-to-wp.png]]
## 写在前面
>网上关于使用WordPress搭建个人网站的教程有很多,我的水平也没有那些真正的dalao高,写这一篇主要是为了帮还不会建站的朋友们入门门槛低一些,顺带复习一下语文,你可以问我问题,但没准你知道的比我还多
## 正文部分
### 第一步,注册一个域名
国内比较出名的有万网,腾讯云。国外比较出名的有GoDaddy,选好合适的然后付钱就可以了(  ̀ω•́ )✧
### 第二步,购买一台云服务器
**这一步要注意,国内主机用于网站搭建需要备案**
国内可以选择阿里云,腾讯云之类的比较出名的服务商
国外的涉及部分原因不予说明,请自行百度( ̄  ̄)/
腾讯云有学生优惠套餐,最低只需要10元一个月,但只有国内主机可选
![[Tenxunyun.png]]
阿里云学生认证之后也有学生优惠套餐,9.5一个月,年费还送.xin域名,只有国内主机可选
### 第三步,安装必要环境和WordPress
###### * 以下步骤全部都在CentOS下完成,并在CentOS6.8下测试通过
#### 搭建LAMP环境
>LAMP是Linux,Apache,MySQL,PHP的缩写,是WordPress博客系统的基础运行环境
###### 安装Apache
```shell
yum install httpd -y
```
![[1.png]]
###### 安装完后启动Apache服务
```shell
service httpd start
```
###### 将Apache设置为开机自启动
```shell
chkconfig httpd on
```
#### 安装MySQL
```shell
yum install mysql-server -y
```
![[2.png]]
###### 安装完成后启动MySQL服务
```shell
service mysqld restart
```
###### 将MySQL设置为开机自启动
```shell
chkconfig mysqld on
```
###### 设置 MySQL 账户 root 密码
```shell
/usr/bin/mysqladmin -u root password '输入密码'
```
###### 进入MySQL
```shell
mysql -uroot --password='你设置的密码'
```
###### 为 WordPress 创建一个名为WordPress的数据库
```shell
CREATE DATABASE wordpress;
```
###### 成功后退出
```shell
exit
```
#### 安装PHP和PHPMySQL(分两行输入)
#### *由于部分服务商的CentOS6可能自带php-mysql,所以可能会提示已经安装.不用理会就好了
```shell
yum install php -y
yum install php-mysql -y
```
###### 在/var/www/html目录下新建一个hellowrold.php文件来检查Apache是否支持php,输入
```php
<?php phpinfo(); ?>
```
###### 重启Apache服务
```shell
service httpd restart
```
###### 浏览器输入
http://外网IP/hellowrold.php
![[4.png]]
###### 应该就可以看到刚刚新建的PHP页面了
#### 安装WordPress博客系统
###### 下载最新版WordPress中文版
```shell
wget -c https://cn.wordpress.org/wordpress-4.9.1-zh_CN.tar.gz
```
###### 解压WordPress
```shell
tar -xzvf wordpress-4.9.1-zh_CN.tar.gz
```
###### 移动WordPress
```shell
mv wordpress /var/www/html/
```
*如果在后面的五分钟配置环节提示需要手动写入wp-config.php文件,则输入以下指令更改权限
```shell
chown -hR www-data /var/www/html/wordpress
```
如果还是不行,需要手动修改wp-config-sample.php
###### 接着浏览器输入
http://外网IP/wordpress
###### 应该就能看到WordPress五分钟配置页面了
![[5.png]]
### 第四步,绑定域名解析
域名解析这一步比较简单,基本上跟着步骤就可以啦
腾讯云域名解析示例
![[6.png]]
##### 主机名
www 是指解析之后域名为www.域名这样的形式,例如www.yujian.icu
@ 是指解析之后域名直接显示为 域名这样的形式,例如yujian.com
\*是将所有的次级域名均指向同一IP地址
##### 记录类型
A记录  用来指定域名的IPv4地址,如需要指向一个IP地址,则需要增加A记录
CNAME记录  如果将域名指向一个域名,实现与被指向域名相同的访问效果,则需要增加CNAME记录
##### 记录值
填你的服务器的IPv4地址
#### TTL
因为DNS是有缓存的,所以TTL就是表示DNS记录在DNS服务器上的缓存时间
一般保持默认就好啦
*尝试Ping你的域名,看看域名指向是否正确,DNS解析需要时间,添加完域名时间后需要等待一段时间
### 接下来就是花费时间把自己的网站建设的美观一点啦ヾ(。 ̄□ ̄)ツ
### 一些WordPress主题推荐
我用的MD主题MDx:[传送门](https://flyhigher.top/develop/788.html)
###### 最后,感谢Axton
### END
*以上内容大部分来自互联网ㄟ(▔ ,▔)ㄏ
*不来自的那就是我乱说的ㄟ(▔ ,▔)ㄏ
青梅枯萎,竹马老去,从此我爱的人都像你
晚安
Loading…
Cancel
Save