diff --git a/content/_index.md b/content/_index.md index 7b57c84..8a01b8d 100644 --- a/content/_index.md +++ b/content/_index.md @@ -1,6 +1,4 @@ --- --- - - -{{}} +本博客已迁移, 请移步 [twotwoba.site](https://www.twotwoba.site) diff --git a/content/posts/.DS_Store b/content/posts/.DS_Store index 5ce9758..4439178 100644 Binary files a/content/posts/.DS_Store and b/content/posts/.DS_Store differ diff --git a/content/posts/algorithm/data structure/LRU.md b/content/posts/algorithm/data structure/LRU.md deleted file mode 100644 index 63553e3..0000000 --- a/content/posts/algorithm/data structure/LRU.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -title: '缓存淘汰算法 -- LRU' -date: 2022-09-23T14:43:45+08:00 -lastmod: 2024-02-27 -series: [data structure] -categories: [algorithm] ---- - -缓存淘汰算法 - -## LRU - -LRU(Least recently used,最近最少使用)。 - -我个人感觉这个命名少了个动词,让人理解起来怪怪的,缓存淘汰算法嘛,**淘汰最近最少使用**。 - -它的核心时:如果数据最近被访问过,那么将来被访问的几率也更高。 - -### 简单实现 - -LRU 一般使用双向链表+哈希表实现,在 JavaScript 中我使用 **Map** 数据结构来实现缓存,因为 Map 可以保证加入缓存的**先后顺序**, - -不同的是,这里是把 Map cache 的尾当头,头当尾。 - -```js -class LRU { - constructor(size) { - this.cache = new Map() - this.size = size - } - // 新增时,先检测是否已经存在 - put(key, value) { - if (this.cache.has(key)) this.cache.delete(key) - this.cache.set(key, value) - // 检查是否超出容量 - if (this.cache.size > this.size) { - this.cache.delete(this.cache.keys().next().value) // 删除Map cache 的第一个数据 - } - } - // 访问时,附件重新进入缓存池的动作 - get(key) { - if (!this.cache.has(key)) return -1 - const temp = this.cache.get(key) - this.cache.delete(key) - this.cache.set(key, temp) - return temp - } -} -``` - -分析: - -1. cache 中的元素必须有时序, 便于后面删除需要淘汰的那个 -2. 在 cache 中快速找到某个 key,判断是否存在并且得到对应的 val O(1) -3. 访问到的 key 需要被提到前面, 也就是说得能实现快速插入和删除 O(1) - -### lc.146 LRU 缓存 - -```js -/** - * @param {number} capacity - */ -var LRUCache = function (capacity) { - this.cache = new Map() - this.capacity = capacity -} - -/** - * @param {number} key - * @return {number} - */ -LRUCache.prototype.get = function (key) { - if (!this.cache.has(key)) return -1 - const val = this.cache.get(key) - this.cache.delete(key) - this.cache.set(key, val) - return val -} - -/** - * @param {number} key - * @param {number} value - * @return {void} - */ -LRUCache.prototype.put = function (key, value) { - if (this.cache.has(key)) this.cache.delete(key) - this.cache.set(key, value) - if (this.cache.size > this.capacity) { - this.cache.delete(this.cache.keys().next().value) - } -} - -/** - * Your LRUCache object will be instantiated and called as such: - * var obj = new LRUCache(capacity) - * var param_1 = obj.get(key) - * obj.put(key,value) - */ -``` - ---- - -### 双向链表版本 - -```js -class ListNode { - constructor(key = 0, value = 0) { - this.key = key - this.value = value - this.prev = null - this.next = null - } -} - -/** - * @param {number} capacity - */ -var LRUCache = function (capacity) { - this.capacity = capacity - this.cache = new Map() - this.head = new ListNode() - this.tail = new ListNode() - this.head.next = this.tail - this.tail.prev = this.head -} - -/** - * @param {number} key - * @return {number} - */ -LRUCache.prototype.get = function (key) { - const node = this.cache.get(key) - - if (node) { - node.prev.next = node.next - node.next.prev = node.prev - - node.next = this.head.next - node.prev = this.head.next.prev - this.head.next.prev = node - this.head.next = node - } - - return node ? node.value : -1 -} - -/** - * @param {number} key - * @param {number} value - * @return {void} - */ -LRUCache.prototype.put = function (key, value) { - let node = null - if (this.cache.has(key)) { - node = this.cache.get(key) - node.value = value - node.prev.next = node.next - node.next.prev = node.prev - } else { - node = new ListNode(key, value) - } - - node.next = this.head.next - node.prev = this.head.next.prev - this.head.next.prev = node - this.head.next = node - - if (!this.cache.has(key) && this.cache.size === this.capacity) { - // remove - this.cache.delete(this.tail.prev.key) - this.tail.prev = this.tail.prev.prev - this.tail.prev.next = this.tail - } - - this.cache.set(key, node) -} - -/** - * Your LRUCache object will be instantiated and called as such: - * var obj = new LRUCache(capacity) - * var param_1 = obj.get(key) - * obj.put(key,value) - */ -``` - -小心指针的变换就好了。 - - - - diff --git "a/content/posts/algorithm/data structure/\344\272\214\345\217\211\346\240\221.md" "b/content/posts/algorithm/data structure/\344\272\214\345\217\211\346\240\221.md" deleted file mode 100644 index 067fb56..0000000 --- "a/content/posts/algorithm/data structure/\344\272\214\345\217\211\346\240\221.md" +++ /dev/null @@ -1,1535 +0,0 @@ ---- -title: '二叉树' -date: 2024-01-15T21:01:56+08:00 -lastmod: 2024-03-13 -tags: [] -series: [data structure] -categories: [algorithm] ---- - -## 二叉树 - -```java -class Node { - V value; - Node left; - Node right; -} -``` - -### 递归序 - -```java -/** - * 1 - * / \ - * 2 3 - * / \ / \ - * 4 5 6 7 - * - * 下方 go 函数就是对这棵二叉树的递归序遍历 - * 每个节点都会结果 3 次,分别在1,2,3位置;实际遍历顺序: - * - * 1(1),2(1),4(1),4(2),4(3), - * 2(2),5(1),5(2),5(3),2(3), - * 1(2),3(1),6(1),6(2),6(3), - * 3(2),7(1),7(2),7(3),3(3),1(3) - * - * 前序(根左右(1))结果:1,2,4,5,3,6,7 - * 前序(左根右(2))结果:4,2,5,1,6,3,7 - * 前序(左右根(3))结果:4,5,2,6,7,3,1 - */ -public void go(Node head) { - if(head == null) return; - // 1 - go(head.left); - // 2 - go(head.right); - // 3 -} -``` - -### 前/中/后序遍历 - -#### 递归 - -递归方法比较好理解,前中后序就是分别在上方 1,2,3 对应的位置访问(打印等操作)节点。 - -```java -public void traverse(Node head) { - if(head == null) return; - System.out.println(head.val); // 前序遍历 - traverse(head.left); - System.out.println(head.val); // 中序遍历 - traverse(head.right); - System.out.println(head.val); // 后序遍历 -} -``` - -```java -// 距离 lc.144 -class Solution { - public List preorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - if (root == null) return list; - traverse(root, list); - return list; - } - - public void traverse(TreeNode root, List list) { - if (root == null) return; - list.add(root.val); - traverse(root.left, list); - traverse(root.right, list); - } -} -``` - -#### 迭代 - -递归转成迭代,核心就是要自己模拟出栈。 - -##### 前 lc.144 - -```java -public List preorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - if (root == null) return list; - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.empty()) { - TreeNode top = (TreeNode) stack.pop(); - list.add(top.val); - if (top.right != null) stack.push(top.right); - if (top.left != null) stack.push(top.left); - } - return list; -} -``` - -> java 的 Stack 是 Vector 的一个子类 -> java 的 Queue 是由 LinkedList 类实现了 Queue 接口 - -##### 中 lc.94 - -```java -public List inorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - if (root == null) return list; - Stack stack = new Stack<>(); - while (!stack.empty() || root != null) { - while (root != null) { - stack.push(root); - root = root.left; - } - TreeNode top = stack.pop(); - list.add(top.val); - root = top.right; - } - return list; -} -``` - -##### 后 1c.145 - -- 一种方法是,把前序遍历的 stack 依次出栈时不打印,而是装到另一个栈中,最后对另一个栈依次出栈就是后序遍历的结果 -- 另一种方法稍微节约点内存,从上面的二叉树递归序我们知道每个节点有三次遍历到的情况(1 次进入,1 次从左节点返回,1 次从右节点返回), - 那么,在节点出栈的时候,先判定是否有右节点,如果有的话,就先别出栈了,把右节点入栈;问题是当从右节点再回到这个节点的时候, - 就需要一个额外变量来确定右侧的节点是否已经访问过了。 - -```java -public List postorderTraversal(TreeNode root) { - List list = new ArrayList<>(); - if (root == null) return list; - Stack stack = new Stack<>(); - TreeNode visited = null; - while (!stack.empty() || root != null) { - while (root != null) { - stack.push(root); - root = root.left; - } - TreeNode top = stack.pop(); - if (top.right == null || top.right == visited) { - list.add(top.val); - visited = top; - } else { - stack.push(top); - root = top.right; - } - } - return list; -} -``` - -```js -/** - * @param {TreeNode} root - * @return {number[]} - */ -var postorderTraversal = function (root) { - if (!root) return [] - const res = [] - const stack = [] - let visited = null - while (stack.length || root) { - while (root) { - stack.push(root) - root = root.left - } - const node = stack.pop() - if (node.right !== null && node.right !== visited) { - stack.push(node) - root = node.right - } else { - res.push(node.val) - visited = node - } - } - return res -} -``` - -### 层序遍历 lc.102 - -想要实现层序遍历的方法非常多,DFS 也可以,只不过一般不这么用,需要掌握的是 BFS。BFS 的应用非常广泛,其中包括寻找图中的最短路径、解决迷宫问题、树的层序遍历等等。 - -在遍历过程中,BFS 使用「队列」来存储已经访问的节点,以确保按照广度优先的顺序进行遍历。 - -```java -/** - * 如果只是对逐层从上到下从左到右打印出节点,是很容易的,一个queue就解决了。 - * 但是lc102是要返回形如 [[1],[2,3],...] 这样List>的数据结构,那么就需要两个队列了 - * 当然,(也有更省空间的方法,双指针记住每一层的结尾节点) - */ -public List> levelOrder(TreeNode root) { - List> list = new ArrayList<>(); - if (root == null) return list; - Queue queue = new LinkedList<>(); - queue.offer(root); - while (!queue.isEmpty()) { - List level = new ArrayList<>(); - int size = queue.size(); // 因为queue在变化,所以需要缓存一下size。当然也可以用两个队列,不断交换来实现,那我个人觉得这种方式更好一点。 - for (int i = 0; i < size; i++) { - TreeNode top = queue.poll(); - level.add(top.val); - if (top.left != null) queue.offer(top.left); - if (top.right != null) queue.offer(top.right); - } - list.add(level); - } - return list; -} -``` - -> 以上都是考验的基础硬编码能力,没什么难的,就是要多练。 - ---- - -## 特殊二叉树 - -### 二叉搜索树 - -左 < 根 < 右,整体上也是:左子树所有值 < 根 < 右字树所有值。 - -根据 BST 的特性,判定是否为 BST 的最简单的办法是,中序遍历后,看是否按照升序排序。 - -```java -/** - * 判定是否为二叉搜索树 lc.98 - */ -class Solution { - long preValue = Long.MIN_VALUE; - - public boolean isValidBST(TreeNode root) { - if (root == null) return true; - boolean isLeftValid = isValidBST(root.left); - if (!isLeftValid) return false; // 注意这里,拿到了左树信息后,就可以及时判断了,当然也可以放到后序的位置做~ - if (preValue == Long.MIN_VALUE) preValue = Long.MIN_VALUE; // // 力扣测试用例里超过了 int 的范围,懂得思想即可 - if (root.val <= preValue) return false; - preValue = root.val; - return isValidBST(root.right); // 不在中序立即对左树判断,放到最后也行,return isLeftValid && isValidBST(root.right); - } -} -``` - -这一题非常好,好在可以帮助我们更好的理解当递归中出现返回值的情况: - -- 递归中的 return,是结束当前的调用栈,他并不会阻塞后续的递归栈的执行 -- 每一次 return 的东西是用来看对后续的程序产生的影响,只需在对应的前中后序位置做好逻辑处理即可,具体问题,具体分析 - -### 完全二叉树 - -就是堆那样子的~挨个从上到下,从左到右排列在树中。毫无疑问,很容易联想到层序遍历,问题是怎么判断呢? - -1. 当遍历到一个节点没有左节点的时候,它也不应该有右节点 -2. 当遍历到一个节点没有右节点的时候,后面所有的节点,都不应该有子节点 - -```java -/** - * 判定是否为完全二叉树 lc.958 - */ -class Solution { - public boolean isCompleteTree(TreeNode root) { - List list = new ArrayList<>(); - Queue queue = new LinkedList<>(); - queue.offer(root); - boolean restShouldBeLeaf = false; - while (!queue.isEmpty()) { - TreeNode node = queue.poll(); - if (restShouldBeLeaf && (node.left != null || node.right != null)) { - return false; - } - if (node.left == null && node.right != null) { - return false; - } - if (node.left != null) { - queue.offer(node.left); - } - if (node.right != null) { - queue.offer(node.right); - } - if (node.right == null) { - restShouldBeLeaf = true; - } - } - return true; - } -} -``` - -### 满二叉树 - -满二叉树,特性:满了,所以深度为 `h`,则节点数为 `2^h - 1`。 - -根据特性去做,很简单,一次 dfs 就能用两个变量统计出深度和节点数。 - -```java -/** - * 判定是否为满二叉树 - */ -int maxDeep = 0; -int deep = 0; -int count = 0; - -public boolean isFullTree(TreeNode root) { - traverse(root); - return Math.pow(2, maxDeep) - 1 == count; -} - -public void traverse(TreeNode root) { - if (root == null) return; - count++; - deep++; - maxDeep = Math.max(deep, maxDeep); // 这一步在哪都行,只要在 deep++ 和 deep--之间就都是 ok 的 - traverse(root.left); - traverse(root.right); - deep--; -} -``` - -### 平衡二叉树 - -平衡二叉树的左右子树的高度之差不超过 1,**且**左右子树也都是平衡的。AVL 树和红黑树都是平衡二叉树,采取了不同的方法自动维持树的平衡。 - -```java -/** - * 判定是否为平衡二叉树 - * - * 定义一个返回体,是需要从左右子树获取的信息 - */ -class ReturnType { - int height; - boolean isBalance; - public ReturnType(int height, boolean isBalance) { - this.height = height; - this.isBalance = isBalance; - } -} - -public static ReturnType isBalanceTree(TreeNode root) { - if (root == null) { - return new ReturnType(0, true); - } - /** - * 第一步,甭管三七二十一,先把递归序写上来 - */ - ReturnType left = isBalanceTree(root.left); - ReturnType right = isBalanceTree(root.right); - /** - * 第二步,需要向左右子树拿信息了。 - */ - int height = Math.max(left.height, right.height); - boolean isBalance = left.isBalance && right.isBalance && Math.abs(left.height - right.height) <= 1; - return new ReturnType(height, isBalance); -} -``` - -定义好 ReturnType,整个递归的算法就很容易实现了。 - -## 二叉树套路技巧 - -其实经过一部分的训练,可以感受到后序遍历的“魔法了”,后序位置我们可以获取到左子树和右子树的信息,关键在于我们需要什么信息,具体问题,具体分析。由此,可以解决很多问题。 - -试着把上方没有用树形 DP 方式解决的方法改写成树形 DP。 - -```java -/** - * 二叉搜索树判定 - * - * 思考需要向左右子树获取什么信息:左、右子树是否是 bst,左子树最大值,右子树最小值 - * - * 好,这里出现了分歧,向左树要最大,向右树要最小,同一个递归中这咋处理? - * - * 答案:**合并处理!我全都要~** - */ -class ReturnType { - boolean isBst; - int max; - int min; - - public ReturnType(boolean isBst, int max, int min) { - this.isBst = isBst; - this.max = max; - this.min = min; - } -} -public boolean isValidBST(TreeNode root) { - return traverse(root).isBst; -} -public ReturnType traverse(TreeNode root) { - if (root == null) return null; // 有最大最小值的时候 遇到 null 还是返回 null 吧,若是用语言自带的最大最小值处理比较麻烦 - ReturnType l = traverse(root.left); - ReturnType r = traverse(root.right); - long min = root.val, max = root.val; - if (l != null) { - min = Math.min(min, l.min); - max = Math.max(max, l.max); - } - if (r != null) { - min = Math.min(min, r.min); - max = Math.max(max, r.max); - } - boolean isBst = true; - if (l != null && (!l.isBst || l.max >= root.val)) { - isBst = false; - } - if (r != null && (!r.isBst || r.min <= root.val)) { - isBst = false; - } - return new ReturnType(isBst, min, max); -} -``` - -上方的写法其实还可以更简化,这么写是为了更好理解递归里的最优子结构。 - -```java -/** - * 满二叉树判定 ReturnType(nodes, deep) - */ -public ReturnType traverse(TreeNode root) { - if (root == null) return new ReturnType(0, 0); - ReturnType l = traverse(root.left); - ReturnType r = traverse(root.right); - int nodes = l.nodes + r.nodes + 1; - int deep = Math.max(l.deep, r.deep) + 1; - return new ReturnType(nodes, deep); -} -public boolean isFullTree(TreeNode root) { - ReturnType res = traverse(root); - System.out.println(res); - return Math.pow(2, res.height) - 1 == res.nodes; -} -``` - -> 注意,还是那句话,具体问题具体分析,这种技巧,并不适合每种二叉树的问题,比如,一颗树,让你求整个树的中位数,树形 DP 的方式就做不到,其实就是没有最优子结构。 -> 无法进行分解子问题然后进行后序树形 dp 的问题,就只能进行遍历解决,这种一般运用「回溯」的算法思想。 - ---- - -## 练习 - -### lc.104 二叉树的最大深度 easy - -题简单,思想很重要。 - -方法一:深度优先遍历,回溯 - -```js -/** - * @param {TreeNode} root - * @return {number} - */ -var maxDepth = function (root) { - if (root == null) return 0 - let deep = 0 - let maxDeep = 0 - const traverse = root => { - if (root == null) return - deep++ - maxDeep = Math.max(maxDeep, deep) - traverse(root.left) - traverse(root.right) - deep-- - } - traverse(root) - return maxDeep -} -``` - -方法二:分解为子问题,树形 DP - -```js -var maxDepth = function (root) { - const traverse = root => { - if (root == null) return 0 - const left = traverse(root.left) - const right = traverse(root.right) - // 当前节点的最大深度为 左右较大的高度加上自身的 1 - return Math.max(left, right) + 1 - } - return traverse(root) -} -``` - -### lc.543 二叉树的直径 easy - -思考:dfs 遍历好像没啥好办法,但是如果分解为子问题,就很简单了,无非就是左边最长加上右边最长嘛~ - -```js -/** - * @param {TreeNode} root - * @return {number} - */ -var diameterOfBinaryTree = function (root) { - if (root == null) return 0 - let res = 0 - const traverse = root => { - if (root == null) return 0 - const l = traverse(root.left) - const r = traverse(root.right) - res = Math.max(l + r, res) // 就是左右子树最大深度之和,保证最大 - return Math.max(l, r) + 1 - } - traverse(root) - return res -} -``` - ---- - -### lc.226 翻转二叉树 easy - -```js -/** - * @param {TreeNode} root - * @return {TreeNode} - */ -var invertTree = function (root) { - if (root == null) return null - let p = root - const traverse = root => { - if (root == null) return null - const l = traverse(root.left) - const r = traverse(root.right) - root.right = l - root.left = r - return root - } - traverse(p) - return root -} -``` - -### lc.114 二叉树展开为链表 - -```js -/** - * @param {TreeNode} root - * @return {void} Do not return anything, modify root in-place instead. - */ -var flatten = function (root) { - if (root == null) return null - const traverse = root => { - if (root == null) return null - let l = traverse(root.left) - let r = traverse(root.right) - if (l) { - root.left = null - root.right = l - // 没啥难度就是注意拼接过去的时候可能是一个链表 - while (l.right) { - l = l.right - } - l.right = r - } - return root - } - traverse(root) -} -``` - -### lc.116 填充每个节点的下一个右侧节点指针 - -观察发现这道题,左右子树的操作都不一样,所以用分解问题的方式没什么思路。 - -那么遍历呢?那就比较简单了,就是把 root.left -> root.right, root.right -> 兄弟节点的 left,关键就在于这一步怎么做。 - -```js -/** - * @param {Node} root - * @return {Node} - */ -var connect = function (root) { - if (root == null) return root - const traverse = (left, right) => { - if (left == null || right == null) return - left.next = right - traverse(left.left, left.right) - traverse(right.left, right.right) - traverse(left.right, right.left) - } - traverse(root.left, root.right) - return root -} -``` - -官解中,是根据父节点的 next 指针去获取到父节点的兄弟节点。 - -### lcr.143 子结构判断 - -```js -/** - * @param {TreeNode} A - * @param {TreeNode} B - * @return {boolean} - */ -var isSubStructure = function (A, B) { - if (A == null || B == null) return false - return traverse(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B) -} - -function traverse(nodeA, nodeB) { - if (nodeB === null) return true - if (nodeA === null || nodeA.val !== nodeB.val) return false - const leftOk = traverse(nodeA.left, nodeB.left) - const rightOk = traverse(nodeA.right, nodeB.right) - return leftOk && rightOk -} -``` - -这道题还是挺有意义的,我一开始写 traverse 函数的时候就陷进去了,老想的先找到 `A === B` 的节点之后再开始一一比对,实际上可以通过 `isSubStructure(A.left, B)` 和 `isSubStructure(A.right, B)` 来巧妙地处理,只要有一个返回了 true,那么就是 ok 的。 - -[力扣大佬题解,写的不错](https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/solutions/791039/yi-pian-wen-zhang-dai-ni-chi-tou-dui-che-uhgs) - ---- - -构造类的问题,一般都是使用分解子问题的方式去解决,一个树 = 根+构造左子树+构造右子树。 - -### lc.654 最大二叉树 - -```js -/** - * Definition for a binary tree node. - * function TreeNode(val, left, right) { - * this.val = (val===undefined ? 0 : val) - * this.left = (left===undefined ? null : left) - * this.right = (right===undefined ? null : right) - * } - */ -/** - * @param {number[]} nums - * @return {TreeNode} - */ -var constructMaximumBinaryTree = function (nums) { - const build = (l, r) => { - if (l > r) return null - let maxIndex = l - for (let i = l + 1; i <= r; ++i) { - if (nums[i] > nums[maxIndex]) maxIndex = i - } - const node = new TreeNode(nums[maxIndex]) - node.left = build(l, maxIndex - 1) - node.right = build(maxIndex + 1, r) - return node - } - return build(0, nums.length - 1) -} -``` - -按照题目要求,很容易完成,但是此题的最优解是 「单调栈」。。。我的天哪,题目不是要递归地构建嘛,这谁想得到啊 😂 - -### lc.105 从前序与中序遍历序列构造二叉树 - -- 前序遍历,第一个节点是根节点 -- 中序遍历,根节点左侧为左树,右侧为右树 - -两者结合,中序从前序中确定根节点,前序根据中序根节点分割取到左侧有子树的 size。 - -```js -/** - * Definition for a binary tree node. - * function TreeNode(val, left, right) { - * this.val = (val===undefined ? 0 : val) - * this.left = (left===undefined ? null : left) - * this.right = (right===undefined ? null : right) - * } - */ -/** - * @param {number[]} preorder - * @param {number[]} inorder - * @return {TreeNode} - */ -var buildTree = function (preorder, inorder) { - // 缓存中序的索引 - const map = new Map() - for (let i = 0; i < inorder.length; ++i) { - map.set(inorder[i], i) - } - const build = (pl, pr, il, ir) => { - if (pl > pr) return null // 一定注意不要忘记递归结束条件。。。 - - const rootVal = preorder[pl] - const node = new TreeNode(rootVal) - - const inIndex = map.get(rootVal) - const leftSize = inIndex - il - node.left = build(pl + 1, pl + leftSize, il, inIndex - 1) - node.right = build(pl + leftSize + 1, pr, inIndex + 1, ir) - return node - } - return build(0, preorder.length - 1, 0, inorder.length - 1) -} -``` - -### lc.106 从中序与后序遍历序列构造二叉树 - -与 lc.105 逻辑一样 - -```js -/** - * @param {number[]} inorder - * @param {number[]} postorder - * @return {TreeNode} - */ -var buildTree = function (inorder, postorder) { - const map = new Map() - for (let i = 0; i < inorder.length; ++i) { - map.set(inorder[i], i) - } - const build = (pl, pr, il, ir) => { - if (pl > pr) return null - - const rootVal = postorder[pr] - const node = new TreeNode(rootVal) - - const inIndex = map.get(rootVal) - const leftSize = inIndex - il - node.left = build(pl, pl + leftSize - 1, il, inIndex - 1) - node.right = build(pl + leftSize, pr - 1, inIndex + 1, ir) - return node - } - - return build(0, postorder.length - 1, 0, inorder.length - 1) -} -``` - -### lc.889 根据前序和后序遍历构造二叉树 - -一个头是根,一个尾是根,无法通过根节点来区分左右子树了,但是仔细观察后,可以使用 pre 的左子树的第一个节点来区分。 - -```js -/** - * @param {number[]} preorder - * @param {number[]} postorder - * @return {TreeNode} - */ -var constructFromPrePost = function (preorder, postorder) { - const map = new Map() - for (let i = 0; i < postorder.length; ++i) { - map.set(postorder[i], i) - } - - const build = (pl, pr, tl, tr) => { - if (pl > pr) return null - if (pl === pr) return new TreeNode(preorder[pl]) // ! 这个关键点很容易漏掉, 只有一个节点的时候 - - const rootVal = preorder[pl] - const root = new TreeNode(rootVal) - - const leftRootVal = preorder[pl + 1] // 这里很可能会越界,所以上方需要单独判断 pl == pr 的情况 - const leftRootIndex = map.get(leftRootVal) - const leftSize = leftRootIndex - tl + 1 - - root.left = build(pl + 1, pl + leftSize, tl, leftRootIndex) - root.right = build(pl + leftSize + 1, pr, leftRootIndex + 1, tr - 1) - return root - } - return build(0, preorder.length - 1, 0, postorder.length - 1) -} -``` - -> 前序+后序还原的二叉树不唯一,比如一直左子树和一直右子树的前后序遍历结果是一样的。 - -### LCR.152 验证二叉搜索树的后序遍历序列 - -二叉搜索树:左 > 根 > 右,然而给出的是后序的遍历,最右边为 根,观察归纳:从左往右第一个大于根的为右子树,并且其后都应当大于根,由此找到突破口。 - -```js -/** - * @param {number[]} postorder - * @return {boolean} - */ -var verifyTreeOrder = function (postorder) { - if (postorder.length <= 1) return true - - const justify = (l, r) => { - if (l >= r) return true - - const root = postorder[r] - - let i = l - while (postorder[i] < root) i++ - let j = i - while (j < r) { - if (postorder[j++] < root) return false - } - - return justify(l, i - 1) && justify(i, r - 1) - } - - return justify(0, postorder.length - 1) -} -``` - -[力扣不错的解](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/solutions/383115/di-gui-he-zhan-liang-chong-fang-shi-jie-jue-zui-ha) - -### lc.297 二叉树序列化和反序列化 - -```js -/** - * Encodes a tree to a single string. - * @param {TreeNode} root - * @return {string} - */ -var serialize = function (root) { - const traverse = root => { - if (root == null) return '#_' - let str = root.val + '_' - str += traverse(root.left) - str += traverse(root.right) - return str - } - return traverse(root) -} - -/** - * Decodes your encoded data to tree. - * @param {string} data - * @return {TreeNode} - */ -var deserialize = function (data) { - const arr = data.split('_') - const generate = arr => { - const val = arr.shift() // 每次弹出,对剩下的递归建树 - if (val === '#') return null - const node = new TreeNode(val) - node.left = generate(arr) - node.right = generate(arr) - return node - } - return generate(arr) -} -/** - * Your functions will be called as such: - * deserialize(serialize(root)); - */ -``` - - - ---- - -### lc.652 寻找重复的子树 - -常规能想到的方法就是序列化,为了保证能区分结构,使用 `(,)` 来进行序列化。 - -```js -var findDuplicateSubtrees = function (root) { - const map = new Map() - const res = new Set() - const dfs = node => { - if (!node) { - return '' - } - let str = '' - str += node.val - str += '(' - str += dfs(node.left) - str += ')(' - str += dfs(node.right) - str += ')' - if (map.has(str)) { - res.add(map.get(str)) - } else { - map.set(str, node) - } - return str - } - dfs(root) - return [...res] -} -``` - -这道题有个技巧是使用 --- 三元组 (长见识了 😭) - -```js -var findDuplicateSubtrees = function (root) { - const map = new Map() - const res = new Set() - let idx = 0 // 关键点 - const dfs = node => { - if (!node) { - return 0 - } - const tri = [node.val, dfs(node.left), dfs(node.right)] // 三元数组 [根节点的值,左子树序号,右子树序号] - const hash = tri.toString() // 相同的字数 三元数组完全一样 - if (map.has(hash)) { - const pair = map.get(hash) - res.add(pair[0]) // - return pair[1] // - } else { - map.set(hash, [node, ++idx]) // - return idx - } - } - dfs(root) - return [...res] -} -// https://leetcode.cn/problems/find-duplicate-subtrees/solutions/1798953/xun-zhao-zhong-fu-de-zi-shu-by-leetcode-zoncw -``` - -### lc.236 最低公共祖先 - -常用的 git merge 用的就是这题原理。 - -```js -var lowestCommonAncestor = function (root, p, q) { - let ans = null - const dfs = node => { - if (node == null) { - return false - } - const leftRes = dfs(node.left) - const rightRes = dfs(node.right) - // 更容易理解的做法,向左右子树要信息,定义 dfs 返回是否含有 p 或 q - if ( - (leftRes && rightRes) || - ((node.val == p.val || node.val == q.val) && (leftRes || rightRes)) - ) { - ans = node - return - } - if (node.val == p.val || node.val == q.val || leftRes || rightRes) { - return true - } - return false - } - dfs(root) - return ans -} -``` - -```js -// 更加抽象的代码 -/** - * @param {TreeNode} root - * @param {TreeNode} p - * @param {TreeNode} q - * @return {TreeNode} - */ -var lowestCommonAncestor = function (root, p, q) { - const traverse = root => { - if (root === null) return null - if (root == p || root == q) return root - const left = traverse(root.left) - const right = traverse(root.right) - if (left && right) return root - return left ? left : right - } - return traverse(root) -} -``` - -### lc.235 二叉搜索树的最近公共祖先 - -BST 一般都要充分利用它的特性。 - -```js -/** - * @param {TreeNode} root - * @param {TreeNode} p - * @param {TreeNode} q - * @return {TreeNode} - */ -var lowestCommonAncestor = function (root, p, q) { - let res = root - while (true) { - if (p.val > res.val && q.val > res.val) { - res = res.right - } else if (p.val < res.val && q.val < res.val) { - res = res.left - } else { - break - } - } - return res -} -``` - -### lc.285 二叉树中序后继节点 - -这道题被力扣设为 vip 题目了,可以看 lcr053。 - -顾名思义,最简单的,根据题意中序遍历即可得到答案: - -```js -var inorderSuccessor = function (root, p) { - const stack = [] - let nextIsRes = false - while (stack.length || root) { - while (root) { - stack.push(root) - root = root.left - } - const node = stack.pop() - if (nextIsRes) return node - if (node == p) nextIsRes = true - if (node.right) root = node.right - } - return null -} -``` - -但是面试怎么可能这么简单呢,挑选候选人,当然需要更优解,因此需要探索到新的思路 - -1. 节点有右侧节点,那么根据中序规则,后继节点是 右侧节点的最左边的子节点 -2. 节点无右侧节点,那么根据中序规则,后续节点是 父节点中第一个作为左子节点的节点 - -另外,如果遇上了 BST,则往往有需要利用上 BST 的性质 - -```js -/** - * @param {TreeNode} root - * @param {TreeNode} p - * @return {TreeNode} - */ -var inorderSuccessor = function (root, p) { - if (p.right) { - let p1 = p.right - while (p1 && p1.left) { - p1 = p1.left - } - return p1 - } - let res = null - let p1 = root - while (p1) { - if (p1.val > p.val) { - res = p1 - p1 = p1.left - } else { - p1 = p1.right - } - } - return res -} -``` - -拓展姊妹题:假设每个节点有一个 parent 指针指向父节点,怎么找后继节点?原理基本一样,不做过多介绍。 - ---- - -接下来是二叉搜索树的相关题目 - -### lc.230 二叉搜索树中第 K 小的元素 - -```js -/** - * @param {TreeNode} root - * @param {number} k - * @return {number} - */ -var kthSmallest = function (root, k) { - let res - const dfs = root => { - if (root === null) return - dfs(root.left) - if (--k == 0) res = root.val - dfs(root.right) - } - dfs(root) - return res -} -``` - -这道题很简单的利用了 BST 的性质,但是每次查找 k 都是要从头找,频繁查找的效率比较低下。进阶的做法就是**记录下每个节点在当前树中的位置**,根据目标与节点的大小比较来决定向左树查还是向右树找。频繁查找的优化见 [「官解」](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/solutions/1050055/er-cha-sou-suo-shu-zhong-di-kxiao-de-yua-8o07) - -### lc.538 把二叉搜索树转换为累加树 - -观察发现累加的顺序和中序遍历正好相反,那就很简单啦~ - -```js -/** - * @param {TreeNode} root - * @return {TreeNode} - */ -var convertBST = function (root) { - let newRoot = root - const stack = [] - let newVal = 0 - while (stack.length || root) { - while (root) { - stack.push(root) - root = root.right - } - const node = stack.pop() - newVal += node.val - node.val = newVal - if (node.left) root = node.left - } - return newRoot -} -// 递归的中序反向就更简单啦,先 right,再 left 即可 -// 这题与 lc.1038 题目相同 -``` - -然而这道题的考点是 「Morris 遍历」,又又又涨新知识啦 😭**Morris 遍历的核心思想是利用树的大量空闲指针,实现空间开销的极限缩减** -上方的时间空间复杂度都是 O(n),用了莫里斯遍历后,空间复杂度可以降到 O(1) 水平。 - -> Morris 遍历是一种不使用递归或栈的树遍历算法。在这种遍历中,通过创建链接作为后继节点,使用这些链接打印节点。最后,将更改恢复以恢复原始树。 - -```js -// 先看一个正常的 Morris 中序遍历,说白了,之前是用栈来让我们从左回到根, -// Morris 只是利用了二叉树自身的指针,来达到从左回到根的操作。 -function morrisInorder(root) { - let curr = root - let res = [] - - while (curr !== null) { - // 没有左树,直接输出当前节点,并且走向右树 - // 当建立了 新的连接后,也可能是向后回退到根节点的操作 - if (curr.left === null) { - res.push(curr.val) - curr = curr.right - } else { - // 有左树就走到下一层左树,然后迭代找到左树的最右子树节点,这里判断 !== curr 是因为后面建立了连接 - let temp = curr.left - while (temp.right !== null && temp.right !== curr) { - temp = temp.right - } - - if (temp.right === null) { - /** 在这里输出则就是前序了 */ - temp.right = curr // 建立连接,比如题目中的 3 --> 4 - curr = curr.left - } else { - temp.right = null // 恢复原树,断开连接,比如从 0 回到到 1 时,0.right 指向 1 - res.push(curr.val) // 中序输出 - curr = curr.right // 退回父节点 - } - } - } - return res -} -/** 后序的 Morris 略微复杂一点点 */ -function morrisPostorder(root) { - let reverseOutput = [] - let curr = root - - while (curr !== null) { - if (curr.right === null) { - //判断右是否为空 - reverseOutput.push(curr.val) - curr = curr.left - } else { - let temp = curr.right - while (temp.left !== null && temp.left !== curr) { - // 寻找左尽头 - temp = temp.left - } - - if (temp.left === null) { - reverseOutput.push(curr.val) - temp.left = curr // 连接左尽头到当前节点 - curr = curr.right - } else { - temp.left = null - curr = curr.left - } - } - } - // Reverse the output array to get the correct postorder traversal - return reverseOutput.reverse() // 最终还得反转一下 -} -``` - -那么这道题,是中序的反向遍历,那就是 Morris 正常中序遍历的镜像: - -```js -/** - * @param {TreeNode} root - * @return {TreeNode} - */ -var convertBST = function (root) { - let curr = root - let sum = 0 - while (curr !== null) { - if (curr.right == null) { - sum += curr.val - curr.val = sum - curr = curr.left - } else { - let prev = curr.right - while (prev.left !== null && prev.left !== curr) { - prev = prev.left - } - - if (prev.left === null) { - prev.left = curr - curr = curr.right - } else { - prev.left = null - sum += curr.val - curr.val = sum - curr = curr.left - } - } - } - return root -} -``` - -### lc.98 验证二叉搜索树 - -在上方已经做过了,最简单的就是中序遍历的结果是否为升序。 - -```js -var isValidBST = function (root) { - let res = true - let pre = -Infinity - const traverse = root => { - if (!root) return - traverse(root.left) - if (root.val <= pre) { - res = false - return - } else { - pre = root.val - } - traverse(root.right) - } - traverse(root) - return res -} -``` - -### lc.700 二叉搜索树中的搜索 easy - -没啥难度,根据 BST 的性质进行左右半区搜索即可。 - -```js -/** - * Definition for a binary tree node. - * function TreeNode(val, left, right) { - * this.val = (val===undefined ? 0 : val) - * this.left = (left===undefined ? null : left) - * this.right = (right===undefined ? null : right) - * } - */ -/** - * @param {TreeNode} root - * @param {number} val - * @return {TreeNode} - */ -var searchBST = function (root, val) { - if (root === null) return null - while (root) { - if (root.val === val) return root - root = val > root.val ? root.right : root.left - } - return null -} -``` - -### lc.701 二叉搜索树中的插入操作 - -二叉树的「改」类似于构造,如过用递归遍历的函数需要返回 TreeNode 类型。 - -先来看下迭代的做法,比较简单,就是找到它合适的位置后,插入即可,由于 BST 的特性,插入的数字一定可以走到叶节点。 - -```js -/** - * @param {TreeNode} root - * @param {number} val - * @return {TreeNode} - */ -var insertIntoBST = function (root, val) { - if (root === null) return new TreeNode(val) - let p = root - while (p !== null) { - if (p.val < val) { - if (p.right === null) { - p.right = new TreeNode(val) - break - } - p = p.right - } else { - if (p.left === null) { - p.left = new TreeNode(val) - break - } - p = p.left - } - } - return root -} -``` - -```js -var insertIntoBST = function (root, val) { - // 递归做法 - if (root === null) return new TreeNode(val) - if (root.val < val) { - root.right = insertIntoBST(root.right, val) // 往右树里加 - } else { - root.left = insertIntoBST(root.left, val) // 往左树里加 - } - return root -} -``` - -### lc.450 删除二叉搜索树中的节点 - -插入是直接加在了叶节点,删除操作会对结构产生变化,需要进行不同情况的判断: - -- 删除节点没有左右子树,直接删除 -- 有一个子树,返回有的那个子树即可 -- 左右子树都有,这就比较麻烦了,它需要**把左子树的最大值或者右子树的最小值替换到被删的节点处** - -```js -/** - * 迭代法,比较容易出错,最好是同时画图,不然很容易漏掉一些指针操作 - * / -/** - * @param {TreeNode} root - * @param {number} key - * @return {TreeNode} - */ -var deleteNode = function (root, key) { - if (root === null) return null - let p = root, - parent = null - while (p && p.val !== key) { - parent = p - if (key < p.val) { - p = p.left - } else { - p = p.right - } - } - - if (p == null) return root // 没找到 key - if (p.left === null && p.right === null) { - p = null - } // key 在叶节点,直接删 - /** 如果没有 parent 指针,这后面逻辑走不下去 */ - else if (p.left === null) { - p = p.right - } else if (p.right === null) { - p = p.left - } else if (p.left && p.right) { - // 和堆排序的操作类似,核心:找到左子树最大值或者右子树最小值和 p 交换即可 - let r = p.right, - rp = p - // 找到右树最小节点 - while (r.left) { - rp = r - r = r.left - } - - /** - * 断掉 右树最小节点的父指针 - * - * 个人建议: 这块最好多画画图,脑补确实不易 [捂脸] - */ - // 压根没有左树节点 - if (rp.val === p.val) { - rp.right = r.right - } else { - // 有左子树节点,那么 r 的右侧节点应当在 rp 的左树 - rp.left = r.right - } - - // 这里好理解,把右树最小节点和删除节点交换 - r.right = p.right - r.left = p.left - p = r - } - - // 删除的是根节点 - if (parent === null) return p - // 把新的 P 节点重新连接 - if (parent.left && parent.left.val === key) { - parent.left = p - } else { - parent.right = p - } - return root -} -``` - -迭代法,通过指针操作,有很多需要注意的点,相比较之下,递归做起来就比较容易了。 - -```js -/** - * 再次提醒 --- 递归千万不要套进去,再就是定义好 出参/入参/结束条件, 其他都好说,都好说~ - * - * 定义: 返回删除树 root 中 key 后的根节点 root - */ -var deleteNode = function (root, key) { - if (root === null) return null - if (root.val === key) { - if (root.left === null) return root.right - if (root.right === null) return root.left - - // 否则和迭代一样,找左树最大值或右树最小值来替换删除的节点 - let r = root.right - while (r.left) { - r = r.left - } - // 断开右树最小值的连接 - root.right = deleteNode(root.right, r.val) - // 交换节点 - r.left = root.left - r.right = root.right - root = r - } else if (root.val < key) { - root.right = deleteNode(root.right, key) - } else if (root.val > key) { - root.left = deleteNode(root.left, key) - } - return root -} -``` - -### lc.95 不同的二叉搜索树 II - -这个题目一看就是穷举的问题,穷举~~~暴力递归 yyds!!!由于 BST 的性质,而不用去回溯。 - -另外,涉及到数组构造树,一般会使用「区间指针」去构造。 - -```js -/** - * @param {number} n - * @return {TreeNode[]} - */ -var generateTrees = function (n) { - // if(n == 1) return new TreeNode(n) - /** - * 定义递归: 输入:区间 [l..r],输出: TreeNode[] - * 结束条件: l > r - * - */ - const build = (l, r) => { - const res = [] - if (l > r) { - res.push(null) // 注意要加入空节点,易错 - return res - } - // 穷尽枚举 - for (let i = l; i <= r; ++i) { - const left = build(l, i - 1) - const right = build(i + 1, r) - for (const l of left) { - for (const r of right) { - const root = new TreeNode(i) - root.left = l - root.right = r - res.push(root) - } - } - } - - return res - } - - return build(1, n) -} -``` - -### lc.96 不同的二叉搜索树 - -做了 lc.95 那么这道题的思路还是挺容易的。这种穷尽枚举的题目,一般使用递归解决。因为是 BST,所以利用 bst 的性质即可,而不用去回溯。 - -```js -/** - * 这道题需要注意的是,需要加 「备忘录」,否则会超时~ - */ -var numTrees = function (n) { - const memo = Array.from(Array(n + 1), () => Array(n + 1).fill(0)) - /** - * 定义递归: 输入:区间 [l..r], 输出:不同的个数 number - * 结束条件:l > r, return 1 - */ - const build = (l, r) => { - let res = 0 - if (l > r) return 1 - if (memo[l][r]) return memo[l][r] - - for (let i = l; i <= r; i++) { - const left = build(l, i - 1) - const right = build(i + 1, r) - res += left * right - } - - memo[l][r] = res - return res - } - return build(1, n) -} -``` - -当然啦,这道题用动态规划去做,时间复杂度空间复杂度都会更低一点,详细见官解,另外官解给出了这道题的奥义 「卡塔兰数」,又又又长知识啦~~~~ - -```js -/** - * dp - */ -var numTrees = function (n) { - // 定义 dp[i] 表示 [1..i] 的二叉搜索树数量 - const dp = new Array(n + 1).fill(0) - dp[0] = 1 - dp[1] = 1 - - for (let i = 2; i <= n; ++i) { - for (let j = 1; j <= i; ++j) { - dp[i] += dp[j - 1] * dp[i - j] // 一个根节点,左子树的个数*右子树的个数就是当前根组合出的个数 - } - } - return dp[n] -} -/** - * 卡塔兰数 - */ -var numTrees = function (n) { - let C = 1 - for (let i = 0; i < n; ++i) { - C = (C * 2 * (2 * i + 1)) / (i + 2) - } - return C -} -``` - ---- - -总的来说,二叉树的问题,还是很有意思的,同时对指针迭代和递归思想的培养很有效,需要多思考,另外基础的操作需要多练习。 diff --git "a/content/posts/algorithm/data structure/\344\274\230\345\205\210\351\230\237\345\210\227.md" "b/content/posts/algorithm/data structure/\344\274\230\345\205\210\351\230\237\345\210\227.md" deleted file mode 100644 index 01067b5..0000000 --- "a/content/posts/algorithm/data structure/\344\274\230\345\205\210\351\230\237\345\210\227.md" +++ /dev/null @@ -1,144 +0,0 @@ ---- -title: '优先队列' -date: 2022-09-23T14:43:36+08:00 -lastmod: 2024-04-25 -series: [data structure] -categories: [algorithm] ---- - -JavaScript 中没有内置优先队列这个数据结构,需要自己来实现一下~👻 - -```javascript -class PriorityQueue { - constructor(data, cmp) { - this.data = data - this.cmp = cmp - for (let i = data.length >> 1; i >= 0; --i) { - this.down(i) - } - } - down(i) { - let left = 2 * i + 1 - while (left < this.data.length) { - let temp - if (left + 1) { - temp = this.cmp(this.data[left + 1], this.data[i]) ? left + 1 : i - } - temp = this.cmp(this.data[temp], this.data[left]) ? temp : left - if (temp === i) { - break - } - this.swap(this.data, temp, i) - i = temp - left = 2 * i + 1 - } - } - up(i) { - while (i >= 0) { - const parent = (i - 1) >> 1 - if (this.cmp(this.data[i], this.data[parent])) { - this.swap(this.data, parent, i) - i = parent - } else { - break - } - } - } - push(val) { - this.up(this.data.push(val) - 1) - } - poll() { - this.swap(this.data, 0, this.data.length - 1) - const top = this.data.pop() - this.down(0) - return top - } - - swap(data, i, j) { - const temp = data[i] - data[i] = data[j] - data[j] = temp - } -} -``` - - - -测试: - -```js -const pq = new PriorityQueue([4, 2, 3, 5, 6, 1, 7, 8, 9], (a, b) => a - b > 0) -console.log('📌📌📌 ~ pq', pq) -console.log(pq.poll()) -console.log(pq.poll()) -console.log(pq.poll()) -pq.push(10) -pq.push(20) -console.log(pq.poll()) -console.log(pq.poll()) -console.log(pq.poll()) -console.log(pq.poll()) -``` - ---- - -递归版本的 down,up,另外使用了堆顶守卫简化 - -- 精髓之一:**数组的第一个索引 0 空着不用** -- 精髓之二:插入或者删除元素的时候,需要元素自动排序 - -```js -class PriorityQueue { - constructor(data, cmp) { - // 使用堆顶守卫,更方便上浮时父节点的获取 p = i >> 1, 子节点本身就比较好获取倒是无所谓 - this.data = [null, ...data] - this.cmp = cmp - for (let i = this.data.length >> 1; i > 0; --i) this.down(i) // 对除最后一层的子节点进行堆化初始化 - } - get size() { - return this.data.length - 1 - } - swap(i, j) { - ;[this.data[i], this.data[j]] = [this.data[j], this.data[i]] - } - // 递归上浮和下沉 - down(i) { - if (i === this.size) return - const j = i - const l = i << 1 - const r = l + 1 - if (l <= this.size && this.cmp(this.data[i], this.data[l])) i = l - if (l <= this.size && this.cmp(this.data[i], this.data[r])) i = r - if (i !== j) { - this.swap(i, j) - this.down(i) - } - } - up(i) { - if (i === 1) return - const p = i >> 1 - if (this.cmp(this.data[p], this.data[i])) { - this.swap(p, i) - this.up(p) - } - } - push(val) { - this.up(this.data.push(val) - 1) // 加入队列后进行上浮处理 - } - poll() { - this.swap(1, this.size) // 先交换首尾,方便后面出队 - const top = this.data.pop() - this.down(1) - return top - } -} -``` - -场景: - -- lc.23 合并 K 个有序链表 -- 堆排序也有其中的思想 diff --git "a/content/posts/algorithm/data structure/\345\211\215\347\274\200\346\240\221.md" "b/content/posts/algorithm/data structure/\345\211\215\347\274\200\346\240\221.md" deleted file mode 100644 index 6ed3a08..0000000 --- "a/content/posts/algorithm/data structure/\345\211\215\347\274\200\346\240\221.md" +++ /dev/null @@ -1,359 +0,0 @@ ---- -title: '前缀树(字典树)' -date: 2024-02-13T17:54:23+08:00 -lastmod: 2024-02-28 -series: [data structure] -categories: [algorithm] ---- - -## 概念及实现 - -前缀树,也叫字典树,就是一种数据结构,比如有一组字符串 `['abc', 'ab', 'bc', 'bck']`,那么它的前缀树是这样的: - - - -核心:**字符在树的树枝上,节点上保存着信息** (当然不是这么死,个人习惯,程序怎么实现都是 ok 的),含义如下: - -- p:通过树枝字符的字符串数量. -- 可以查询前缀数量 -- e:以树枝字符结尾的字符串数量. -- 可以查询字符串 - -对应的数据结构如下: - -```js -class TrieNode { - constructor(pass = 0, end = 0) { - this.pass = pass // 通过下接树枝字符的字符串数量 - this.end = end // 以上接树枝字符结尾的字符串数量 - this.next = {} // {char: TrieNode} 的 map 集, 字符有限,有些教程也用数组实现;next 的 key 就可以抽象为树枝 - } -} -class Trie { - constructor() { - this.root = new TrieNode() - } - insert(str) { - let p = this.root - for (const c of str) { - if (!p.next[c]) { - p.next[c] = new TrieNode() - } - p = p.next[c] - p.pass++ - } - p.end++ - } - // 查询字符串。根据实际问题,看是返回 Boolean 还是 end - search(str) { - let p = this.root - for (const c of str) { - if (!p.next[c]) return 0 - // if (!p.next[c].pass) return 0 // 根据实际情况看是否需要做什么额外操作 - p = p.next[c] - } - return p.end - } - // 有几个以 str 为前缀的字符串。根据实际问题,看是返回 Boolean 还是 pass - startWidth(str) { - let p = this.root - for (const c of prefix) { - if (!p.next[c]) return 0 - // if (!p.next[c].pass) return 0 // 根据实际情况看是否需要做什么额外操作 - p = p.next[c] - } - return p.pass - } - delete(str) { - if (this.search(str) !== 0) { - let p = this.root - p.pass-- - for (const c of str) { - p.next[c].pass-- - if (p.next[c].pass == 0) { - p.next[c] = null // 当某个节点的 pass 为 0 的时候,说明后面都没得了,可以直接把后续置 null 了 - return - } - p = p.next[c] - } - p.end-- - } - } -} -``` - -### lc.208 实现前缀树 - -```js -/** - * 自定义前缀树节点 - */ - -class TrieNode { - constructor() { - this.pass = 0 - this.end = 0 - this.next = {} - } -} -var Trie = function () { - this.root = new TrieNode() -} - -/** - * @param {string} word - * @return {void} - */ -Trie.prototype.insert = function (word) { - let p = this.root - for (const c of word) { - if (!p.next[c]) { - p.next[c] = new TrieNode() - } - p = p.next[c] - p.pass++ - } - p.end++ -} - -/** - * @param {string} word - * @return {boolean} - */ -Trie.prototype.search = function (word) { - let p = this.root - for (const c of word) { - if (!p.next[c]) return false - p = p.next[c] - } - return p.end > 0 -} - -/** - * @param {string} prefix - * @return {boolean} - */ -Trie.prototype.startsWith = function (prefix) { - let p = this.root - for (const c of prefix) { - if (!p.next[c]) return false - p = p.next[c] - } - return true -} - -/** - * Your Trie object will be instantiated and called as such: - * var obj = new Trie() - * obj.insert(word) - * var param_2 = obj.search(word) - * var param_3 = obj.startsWith(prefix) - */ -``` - -### lc.211 添加与搜索单词 - 数据结构设计 - -```js -class TrieNode { - constructor(pass = 0, end = 0) { - this.pass = pass - this.end = end - this.next = {} - } -} -var WordDictionary = function () { - this.root = new TrieNode() -} - -/** - * @param {string} word - * @return {void} - */ -WordDictionary.prototype.addWord = function (word) { - let p = this.root - for (const c of word) { - if (!p.next[c]) { - p.next[c] = new TrieNode() - } - p = p.next[c] - p.pass++ - } - p.end++ -} - -/** - * @param {string} word - * @return {boolean} - */ - // 注意第二个参数 是后来自己写的时候添加的,因为要寻找 . 之后的 -WordDictionary.prototype.search = function (word, newRoot) { - let p = this.root - if (newRoot) p = newRoot - for (let i = 0; i < word.length; ++i) { - const c = word[i] - if (c === '.') { - // 关键在怎么处理这里,因为 . 匹配任意字符 - // 最直观的做法就是把 . 替换成可能得字符,然后挨个尝试 - if (i === word.length) return true - const keys = Object.keys(p.next) - // 一开始这么写的,缺少了 start,每次都从头开始搜索,这就不对了,那就把 p 带上 - // return keys.some(d => this.search(d + word.slice(i + 1))) - return keys.some(d => this.search(d + word.slice(i + 1), p)) - } else { - if (!p.next[c]) return false - p = p.next[c] - } - } - return p.end > 0 -} - -/** - * Your WordDictionary object will be instantiated and called as such: - * var obj = new WordDictionary() - * obj.addWord(word) - * var param_2 = obj.search(word) - */ -/** 这题我写的和官解略有不同,但总体思路都是深度优先遍历 dfs */ -``` - -### lc.648 单词替换 - -```js -/** - * @param {string[]} dictionary - * @param {string} sentence - * @return {string} - */ -var replaceWords = function (dictionary, sentence) { - let trie = new Trie() - dictionary.forEach(s => trie.insert(s)) - - return sentence - .split(' ') - .map(s => trie.search(s)) - .join(' ') -} -// 读这道题意,很容易想得到 前缀树 -class TrieNode { - constructor(pass = 0, end = 0) { - this.pass = pass - this.end = end - this.next = {} - } -} -class Trie { - constructor() { - this.root = new TrieNode() - } - insert(str) { - let p = this.root - for (const c of str) { - if (!p.next[c]) { - p.next[c] = new TrieNode() - } - p = p.next[c] - p.pass++ - } - p.end++ - } - search(str) { - let p = this.root - let i = 0 - for (const c of str) { - if (!p.next[c]) { - return p.end > 0 ? str.slice(0, i) : str - } - p = p.next[c] - i++ - /** - * 一开始这两个边界条件我给漏了。。。 - * 一个是 p 走到头了, 一个是 i 走到头了~ - */ - if (p.end > 0) return str.slice(0, i) - if (i === str.length && p.pass > 0) return str - } - } -} -``` - -### lc.677 键值映射 - -```js -class TrieNode { - constructor(pass = 0, end = 0) { - this.pass = pass - this.end = end - this.val = 0 - this.next = {} - } -} -var MapSum = function () { - this.root = new TrieNode() -} - -/** - * @param {string} key - * @param {number} val - * @return {void} - */ -MapSum.prototype.insert = function (key, val) { - let p = this.root - for (const c of key) { - if (!p.next[c]) { - p.next[c] = new TrieNode() - } - p = p.next[c] - p.pass++ - } - p.end++ - p.val = val -} - -/** - * @param {string} prefix - * @return {number} - */ -MapSum.prototype.sum = function (prefix) { - let p = this.root - for (const c of prefix) { - if (!p.next[c]) return 0 - p = p.next[c] - } - // 递归查找之后所有的 val 并累加即可 - let sum = 0 - /** 第一次做,漏掉了恰好相等的条件 */ - if (p && p.end > 0) sum += p.val - const getVal = p => { - if (!p) return - const allKeys = Object.keys(p.next) - if (!allKeys.length) return - allKeys.forEach(c => { - let newP = p // 第一次做,这里也忘记处理了。。。 - newP = newP.next[c] - if (newP && newP.end > 0) sum += newP.val - getVal(newP) - }) - } - getVal(p) - return sum -} - -/** - * Your MapSum object will be instantiated and called as such: - * var obj = new MapSum() - * obj.insert(key,val) - * var param_2 = obj.sum(prefix) - */ -``` - -这道题我纯用前缀树实现了,递归的过程有点费时间,优化时间复杂度,自然上 hashMap,官解里有实现,自行理解。 - ---- - -### 场景 - -前缀树的作用: - -1. 查询字符串 -2. 查询以某个字符串为前缀的字符串有多少个 -3. 自动补完 - -上面两个作用,第一个 hashMap 也能做到,但是其他点,则是前缀树发挥其本领的绝对领地了。 diff --git "a/content/posts/algorithm/data structure/\345\215\225\350\260\203\346\240\210.md" "b/content/posts/algorithm/data structure/\345\215\225\350\260\203\346\240\210.md" deleted file mode 100644 index d667008..0000000 --- "a/content/posts/algorithm/data structure/\345\215\225\350\260\203\346\240\210.md" +++ /dev/null @@ -1,306 +0,0 @@ ---- -title: '单调栈' -date: 2023-01-31T09:14:58+08:00 -lastmod: 2024-02-26 -series: [data structure] -categories: [algorithm] ---- - -## 概念及实现 - -栈,同端进同端出,具有先进后出的特性,当栈内所有元素具有单调性(递增/递减)就是一个**单调栈**了。 - -自行干预单调栈的实现:当一个元素入栈时破坏了单调性,那么就 pop 栈顶(可能需要迭代),直到能使得新加入的元素保持栈的单调性。 - -```js -for (const item of arr) { - while(stack.length && item (>= | <=) stack[stack.length - 1]) { - stack.pop() - } - stack.push(item) -} -``` - -> 栈存储的信息,可以是索引、元素,根据实际情况进行处理 -> 灵活控制遍历顺序来简化算法 - -## 场景 - -单调栈的应用场景比较单一,只处理一类典型的问题:比如 「下一个更/最...」 之类的问题,另一类是接雨水,柱状图中的最大矩形这种变形问题。 - -## 练一练 - -### lc.402 移掉 K 位数字 - -分析:为了让数字最小,从左往右遍历,左侧为高位,所以高位越小越好,那么从左往右遍历的过程中,当索引位置的元素 index > index + 1,时,把 index 位置的数字删掉即可。 - -```js -/** - * @param {string} num - * @param {number} k - * @return {string} - */ -var removeKdigits = function (num, k) { - const nums = num.split('') - const stack = [] - for (const el of num) { - while (stack.length && k && el < stack[stack.length - 1]) { - stack.pop() - k-- - } - stack.push(el) - } - - /** k 还有富余继续pop */ - while (k > 0) { - stack.pop() - k-- - } - - while (stack[0] === '0') stack.shift() - return stack.join('') || '0' -} -``` - -### lc.496 下一个更大元素 I easy - -```js -/** - * @param {number[]} nums1 - * @param {number[]} nums2 - * @return {number[]} - */ -var nextGreaterElement = function (nums1, nums2) { - // 思路:对 nums2 构建单调栈,同时用哈希表存储信息,最后遍历 nums1 并从哈希表中取出数据即可 - const stack = [] - const map = {} - for (const el of nums2) { - while (stack.length && el > stack[stack.length - 1]) { - const last = stack.pop() // 拍扁过程中,el 是 所所有被拍扁元素的下一个更大元素 - map[last] = el - } - stack.push(el) - } - let res = [] - for (const el of nums1) { - res.push(map[el] || -1) - } - return res -} -``` - -### lc.503 下一个更大元素 II - -处理循环数组有个常规的技巧是将循环数组拉直 --- 即复制该序列的前 n−1 个元素拼接在原序列的后面,访问拼接位置元素的索引为 `index % arr.length` - -> 本题值的注意的是:如过数组中有重复的元素,那么就不能用 map 存储 「元素」 作为 key 了,索引是单调的,因为可以在单调栈中存储索引。 - -```js -// [ 0,1,2,3,4,0,1,2,3 ] 5 % 5 == 0, 6 % 5 ==1 -/** - * @param {number[]} nums - * @return {number[]} - */ -var nextGreaterElements = function (nums) { - const res = Array(nums.length).fill(-1) - const stack = [] - for (let i = 0; i < nums.length * 2 - 1; i++) { - while (stack.length && nums[i % nums.length] > nums[stack[stack.length - 1]]) { - const index = stack.pop() - res[index] = nums[i % nums.length] - } - stack.push(i % nums.length) - } - return res -} -``` - -### lc.739 每日温度 - -看见下一个更高温度,直接单调栈解决,根据题意,要求的是几天后,那么根据数组索引去解决即可。 - -```js -/** - * @param {number[]} temperatures - * @return {number[]} - */ -var dailyTemperatures = function (temperatures) { - const res = Array(temperatures.length).fill(0) - const stack = [] - for (let i = 0; i < temperatures.length; ++i) { - while (stack.length && temperatures[i] > temperatures[stack[stack.length - 1]]) { - const lastIndex = stack.pop() - res[lastIndex] = i - lastIndex - } - stack.push(i) - } - return res -} -``` - -### lc.901 股票价格跨度 - -```js -var StockSpanner = function () { - this.arr = [] - this.monoStack = [] -} - -/** - * @param {number} price - * @return {number} - */ -StockSpanner.prototype.next = function (price) { - this.arr.push(price) - while (this.monoStack.length && price > this.arr[this.monoStack[this.monoStack.length - 1]]) { - this.monoStack.pop() - } - // -1 表示-1 天,用来计算间距 - let lastIndex = this.monoStack.length ? this.monoStack[this.monoStack.length - 1] : -1 - const interval = this.arr.length - lastIndex - 1 // (lastIndex...this.arr.length) - this.monoStack.push(this.arr.length - 1) - return interval -} - -/** - * Your StockSpanner object will be instantiated and called as such: - * var obj = new StockSpanner() - * var param_1 = obj.next(price) - */ -``` - ---- - -下方两道 hard 题,加深对单调栈的理解。 - -### lc.84 柱状图中的最大矩形 hard - -首先暴力解,枚举每个柱子的高度,对每个柱子找到其左边和右边第一个比它矮的柱子,那么这个柱子的最大面积就是 w \* h 了 - -暴力解的时间复杂度可以用单调栈来降低。暴力寻找左右第一个矮的柱子,时间复杂度是 `O(n^2)`,用上单调栈后,时间复杂度可以降到 `O(n)`。 - -```js -/** - * @param {number[]} heights - * @return {number} - */ -var largestRectangleArea = function (heights) { - const left = [] - const right = [] - - const monoStack = [] - for (let i = 0; i < heights.length; ++i) { - // 找到索引 i 左边第一个比 i 的高要小的 - while (monoStack.length && heights[i] <= heights[monoStack[monoStack.length - 1]]) { - monoStack.pop() - } - left[i] = monoStack.length > 0 ? monoStack[monoStack.length - 1] : -1 - monoStack.push(i) - } - - monoStack.length = 0 - for (let i = heights.length - 1; i >= 0; --i) { - while (monoStack.length && heights[i] <= heights[monoStack[monoStack.length - 1]]) { - monoStack.pop() - } - right[i] = monoStack.length ? monoStack[monoStack.length - 1] : heights.length - monoStack.push(i) - } - let max = 0 - for (let i = 0; i < heights.length; ++i) { - max = Math.max(max, (right[i] - left[i] - 1) * heights[i]) - } - return max -} -``` - -> 这题有个小技巧是:因为是根据索引求宽度,那么首尾的时候就有可能有越界行为,上面使用了 -1 和 数组的长度作为 0 前面和最后一个索引后面的索引,这么做是比较麻烦的,那么就可以使用老熟人**哨兵守卫**了,先把 heights 变成 `[0, ...heights, 0]`,后面就不用担心首尾索引了。 - -在上方的解题中,每次迭代 pop 完之后找到最左、右第一个小于 i 的边界,这就把 pop 出的这个信息给浪费了。而当有了守卫的时候,这道题的常数项优化就更简单了([官解的常数项优化](https://leetcode.cn/problems/largest-rectangle-in-histogram/solutions/266844/zhu-zhuang-tu-zhong-zui-da-de-ju-xing-by-leetcode-/))。 - -因为有了最左侧的 0,会保证左侧边界不越界;因为有了右侧的 0 会保证每个柱子都会遍历到。当每个柱子被 pop 的时候,它的左边界就是它在栈中的的上一个,它的右边界就是新进入的,在这个时候去计算并更新最大值即可。 - -```js -/** - * @param {number[]} heights - * @return {number} - */ -var largestRectangleArea = function (heights) { - let max = 0 - const monoStack = [] - heights = [0, ...heights, 0] - for (let i = 0; i < heights.length; ++i) { - while (monoStack.length && heights[i] < heights[monoStack[monoStack.length - 1]]) { - const h = heights[monoStack.pop()] - const left = monoStack[monoStack.length - 1] - max = Math.max(max, (i - left - 1) * h) - } - monoStack.push(i) - } - return max -} -``` - -### lc.42 接雨水 hard - -这道题是经典中的经典了,解法很多:双指针/动态规划/单调栈,此处使用单调栈的方式来解题。 - -```js -/** - * @param {number[]} height - * @return {number} - */ -var trap = function (height) { - let area = 0 - const monoStack = [] - for (let i = 0; i < height.length; ++i) { - // 找到下一个更大的元素,就能形成 凹 槽 - while (monoStack.length && height[i] > height[monoStack[monoStack.length - 1]]) { - const lowH = height[monoStack.pop()] // 中间的凹槽的高度 - // 注意这里,对边界做判断 - if (monoStack.length) { - const highH = Math.min(height[monoStack[monoStack.length - 1]], height[i]) - area += (highH - lowH) * (i - monoStack[monoStack.length - 1] - 1) - } - } - monoStack.push(i) - } - return area -} -``` - -补充一下这道题的最优解:双指针法,能做到空间复杂度 O(1) - -```js -// 相比单调栈横向计算面积, 双指针是纵向计算面积的,主要根据两边高度的较小个 -var trap = function (height) { - // 每个坐标点能装下的水是 左右最高柱子较小的那一个 减去自身的高度 - const n = height.length - let l = 0, - r = n - 1 - let res = 0 - let l_max = 0, - r_max = 0 - while (l < r) { - l_max = Math.max(l_max, height[l]) - r_max = Math.max(r_max, height[r]) - if (l_max < r_max) { - res += l_max - height[l] - l++ - } else { - res += r_max - height[r] - r-- - } - } - return res -} -``` - ---- - -总结:对于应用类型的问题,要学会转化问题 - -- 接雨水需要找到「盛水的凹点」,被 pop 出来的就是 凹槽 -- 最大矩形需要找到「左右的低点」,被 pop 出来的就是 峰顶 - -此类根据凹凸边界去求解的问题,应当联想到单调栈~ diff --git "a/content/posts/algorithm/data structure/\345\215\225\350\260\203\351\230\237\345\210\227.md" "b/content/posts/algorithm/data structure/\345\215\225\350\260\203\351\230\237\345\210\227.md" deleted file mode 100644 index 380fc5c..0000000 --- "a/content/posts/algorithm/data structure/\345\215\225\350\260\203\351\230\237\345\210\227.md" +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: '单调队列' -date: 2023-01-31T09:15:05+08:00 -lastmod: 2024-02-27 -series: [data structure] -categories: [algorithm] ---- - -### 概念 - -栈有单调栈,队列自然也有单调队列,性质也是一样的,保持队列内的元素有序,单调递增或递减。 - -其实动态求极值首先可以联想到的应该是 「优先队列」,但是,优先队列无法满足**「先进先出」**的时间顺序,所以单调队列应运而生。 - -```js -// 关键点, 保持单调性,其拍平效果与单调栈一致 -while (q.length && num (<= | >=) q[q.length - 1]) { - q.pop() -} -q.push(num) -``` - -## 场景 - -给你一个数组 window,已知其最值为 A,如果给 window 中添加一个数 B,那么比较一下 A 和 B 就可以立即算出新的最值;但如果要从 window 数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是 A,就需要遍历 window 中的所有元素重新寻找新的最值。 - -## 练一练 - -### lc239. 滑动窗口最大值 hard - -动态计算极值,直接命中单调队列的使用条件。 - -```js -/** - * @param {number[]} nums - * @param {number} k - * @return {number[]} - */ -var maxSlidingWindow = function (nums, k) { - let res = [] - const monoQueue = [] - for (let i = 0; i < nums.length; ++i) { - while (monoQueue.length && nums[i] >= nums[monoQueue[monoQueue.length - 1]]) { - monoQueue.pop() - } - /** 一个重点是存储索引,便于判断窗口的大小 */ - monoQueue.push(i) - if (i - monoQueue[0] + 1 > k) monoQueue.shift() - - // r - l + 1 == k 所以 l = r - k + 1 保证有意义 - if (i - k + 1 >= 0) res.push(nums[monoQueue[0]]) - } - return res -} -``` - -### lc.862 和至少为 K 的最短子数组 hard - -看题目就知道,离不开前缀和。想到单调队列,是有难度的,起码我一开始想不到 😭 - - - -```js -var shortestSubarray = function (nums, k) { - const n = nums.length - const preSumArr = new Array(n + 1).fill(0) - for (let i = 0; i < n; i++) { - preSumArr[i + 1] = preSumArr[i] + nums[i] - } - let res = n + 1 - const queue = [] - for (let i = 0; i <= n; i++) { - const curSum = preSumArr[i] - while (queue.length != 0 && curSum - preSumArr[queue[0]] >= k) { - res = Math.min(res, i - queue.shift()) - } - while (queue.length != 0 && preSumArr[queue[queue.length - 1]] >= curSum) { - queue.pop() - } - queue.push(i) - } - return res < n + 1 ? res : -1 -} -``` - -### lc.918 环形子数组的最大和(单调队列) - -这道题在前缀和的时候遇到过,再来复习一次吧 😁 - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var maxSubarraySumCircular = function (nums) { - // 这道题是比较综合的一道题,用到了前缀和,循环数组技巧,滑动窗口和单调队列技巧 - let max = -Infinity - const monoQueue = [[0, nums[0]]] // 单调队列存储 【index,preSum】的数据结构 - const n = nums.length - let preSum = nums[0] - for (let i = 1; i < 2 * n; ++i) { - preSum += nums[i % n] - max = Math.max(max, preSum - monoQueue[0][1]) // 子数组和越大,减去的就应该越小 - while (monoQueue.length && preSum <= monoQueue[monoQueue.length - 1][1]) { - monoQueue.pop() - } - monoQueue.push([i, preSum]) - // 根据索引控制 窗口大小 - while (monoQueue.length && i - monoQueue[0][0] + 1 > n) { - monoQueue.shift() - } - } - return max -} -``` - - diff --git "a/content/posts/algorithm/data structure/\345\233\276.md" "b/content/posts/algorithm/data structure/\345\233\276.md" deleted file mode 100644 index 28a08a9..0000000 --- "a/content/posts/algorithm/data structure/\345\233\276.md" +++ /dev/null @@ -1,587 +0,0 @@ ---- -title: '图' -date: 2023-02-19T16:03:41+08:00 -lastmod: 2024-04-03 -series: [data structure] -categories: [algorithm] ---- - -## 图论基础 - - - -### 邻接表&邻接矩阵 - - - -上面这幅有向图,分别用邻接表和邻接矩阵实现如下: - -```ts -// 邻接表,当然也可以用 hashmap 来实现 -const graph: Array = [[1, 3, 4], [2, 3, 4], [3], [4], []] -// 邻接矩阵,当然元素不仅仅只能为 Boolean 值 -const graph: Array = [ - [false, true, false, true, true], - [false, false, true, true, true], - [false, false, false, true, false], - [false, false, false, false, true], - [false, false, false, false, false] -] -``` - -- 邻接表:占用空间少;判断两个节点是否相邻,需要遍历所有相邻的节点 -- 邻接矩阵:存在很多空洞,占用空间大;判断两个节点是否相邻简单,获取 `matrix[i][j]` 的值即可 - -### 图的遍历 - -图的遍历,需要注意的是图可能有环: - -1. 必须要有 `visited` 变量来防止走入死循环 -2. 遍历过程中可以使用 `onPath` 变量判断当时的路径是否成环(类比贪吃蛇蛇身) - -```js -/** visited 类似贪吃蛇走过的所有路径;onPath 类似设蛇身 */ -// 记录所有遍历过的节点 -const visited = [] -// 记录从起点到当前节点的路径 -const onPath = [] - -/* 图遍历框架 DFS */ -function traverse(graph, s) { - if (visited[s]) return - // 经过节点 s,标记为已遍历 - visited[s] = true - // 做选择:标记节点 s 在路径上 - onPath[s] = true - for (const neighbor of graph.neighbors(s)) { - traverse(graph, neighbor) - } - // 撤销选择:节点 s 离开路径 - onPath[s] = false -} -``` - -> 在暴力递归-回溯时学过: -> -> - 回溯做选择和撤销选择是在 for 循环内,对应选择、撤销选择的对象是「树枝」 -> - DFS 做选择和撤销选择是在 for 循环外,对应选择、撤销选择的对象是「节点」 -> -> 抽象出「树枝,节点」是为了更加形象的理解,其实放在 for 循环外就是为了不要漏掉 「初始节点」。具体的请参看 [暴力递归-DFS&回溯](../暴力递归-dfs回溯/) 这篇文章。 - -#### lc.797 所有可能的路径 - -```js -var allPathsSourceTarget = function (graph) { - // 有向无环图, graph 的 index 自身即为 node; graph[index] 为邻居 - let res = [] - const onPath = [] - const dfs = node => { - onPath.push(node) - if (node === graph.length - 1) { - res.push([...onPath]) - /** 因为无环,所以不用 return;如果 return 同时需要维护 onPath */ - // onPath.pop() - // return - } - for (let i = 0; i < graph[node].length; ++i) { - dfs(graph[node][i]) - } - onPath.pop() - } - dfs(0) - return res -} -``` - - - - - ---- - -## 经典问题 - -### 有向图环检测&拓扑排序 - -对于有「依赖关系」的问题,一般可以抽象为一副有向图,检测是否有循环依赖即可。 - -#### lc.207 课程表 - -用邻接表的形式来抽象本题的有向图。 - -- dfs 检测环,借助图遍历中的 onPath 数组,判断蛇身是否相撞即可 - -```js -/** - * @param {number} numCourses - * @param {number[][]} prerequisites - * @return {boolean} - */ -var canFinish = function (numCourses, prerequisites) { - // 先构图 - const graph = Array.from(Array(numCourses), () => []) - for (let i = 0; i < prerequisites.length; ++i) { - const [to, from] = prerequisites[i] - graph[from].push(to) // from -> to - } - // 再判断是否有环 - const onPath = [] // 记录遍历过程 - const visited = [] // 防止进入死循环 - let hasCycle = false - const dfs = node => { - // 蛇身成环 - if (onPath.indexOf(node) > -1) { - hasCycle = true - return - } - if (visited.indexOf(node) > -1 || hasCycle) return - onPath.push(node) - visited.push(node) - for (const neighbor of graph[node]) { - dfs(neighbor) - } - onPath.pop() - } - - for (let i = 0; i < numCourses; ++i) { - dfs(i) - } - return !hasCycle -} -``` - ---- - -- bfs 检测环,需要借助入度数组,当某个节点入度为 0 的时候代表它成为了头了,加入队列 - -```js -/** - * @param {number} numCourses - * @param {number[][]} prerequisites - * @return {boolean} - */ -var canFinish = function (numCourses, prerequisites) { - // 先构图 + 构建入度数组 - const graph = Array.from(Array(numCourses), () => []) - const indegree = Array(numCourses).fill(0) - for (let i = 0; i < prerequisites.length; ++i) { - const [to, from] = prerequisites[i] - graph[from].push(to) // from -> to - indegree[to]++ - } - // 寻找入度为 0 的节点加入队列,然后开始 bfs 遍历 - const queue = [] - for (let i = 0; i < numCourses; ++i) { - indegree[i] === 0 && queue.push(i) - } - let visitedCount = 0 - while (queue.length) { - // 这道题不用向四周分散,而是去操作入度数组 - visitedCount++ - const node = queue.shift() - const indegreeOfNode = graph[node] - for (let i = 0; i < indegreeOfNode.length; ++i) { - indegree[indegreeOfNode[i]]-- - if (indegree[indegreeOfNode[i]] === 0) { - queue.push(indegreeOfNode[i]) - } - } - } - - // 如果所有节点都被遍历过,说明不成环 - // 1. 如果就是一个完整环,那么没有入度为 0 的节点不会进入遍历 - // 2. 如果是图中有一部分为环,那么环起点的入度始终不会为 0 - return visitedCount === numCourses -``` - -#### lc.210 课程表 II - -此题相比上一题,无非就是**需要记录下来完整的依赖图**,那么就得了解下 「拓扑排序了」:[拓扑排序-维基百科](https://zh.wikipedia.org/wiki/%E6%8B%93%E6%92%B2%E6%8E%92%E5%BA%8F) - -> 直观点讲,就是把一幅有向图拉平后,每条边的指向相同。 - -- dfs 拓扑: 在上一题 dfs 检测环的后续位置收集节点为一个集合,对这个集合逆序即为拓扑排序的一个结果;或者在构图时,`graph[to].push(from)`,最后就不用逆序了 - - - -```js -/** - * @param {number} numCourses - * @param {number[][]} prerequisites - * @return {number[]} - */ -var findOrder = function (numCourses, prerequisites) { - const graph = Array.from(Array(numCourses), () => []) - for (let i = 0; i < prerequisites.length; ++i) { - const [to, from] = prerequisites[i] - graph[to].push(from) // 注意这里! - } - let res = [] - let hasCycle = false - const visited = [] - const onPath = [] - const dfs = node => { - if (onPath.indexOf(node) > -1) { - hasCycle = true - return - } - if (visited.indexOf(node) > -1 || hasCycle) return - onPath.push(node) - visited.push(node) - for (const neighbor of graph[node]) { - dfs(neighbor) - } - res.push(node) - onPath.pop() - } - for (let i = 0; i < numCourses; ++i) { - dfs(i) - } - if (hasCycle) return [] - return res -} -``` - ---- - -- bfs 拓扑:在上一题 bfs 检测环的基础上,很容易理解出队的顺序就是拓扑排序的结果。 - -```js -var findOrder = function (numCourses, prerequisites) { - // 先构图 + 构建入度数组 - const graph = Array.from(Array(numCourses), () => []) - const indegree = Array(numCourses).fill(0) - for (let i = 0; i < prerequisites.length; ++i) { - const [to, from] = prerequisites[i] - graph[from].push(to) // from -> to - indegree[to]++ - } - // 寻找入度为 0 的节点加入队列,然后开始 bfs 遍历 - const queue = [] - for (let i = 0; i < numCourses; ++i) { - indegree[i] === 0 && queue.push(i) - } - let visitedCount = 0 - const res = [] - while (queue.length) { - // 这道题不用向四周分散,而是去操作入度数组 - visitedCount++ - const node = queue.shift() - res.push(node) - const indegreeOfNode = graph[node] - for (let i = 0; i < indegreeOfNode.length; ++i) { - indegree[indegreeOfNode[i]]-- - if (indegree[indegreeOfNode[i]] === 0) { - queue.push(indegreeOfNode[i]) - } - } - } - if (visitedCount !== numCourses) return [] - return res -} -``` - -从检测环的角度来说:「拓扑排序」可以判断-有向图-是否有环,「并查集」可以判断-无向图-是否有环 - -### 并查集 - -> 参考了此篇文章 https://oi-wiki.org/ds/dsu/ - -并查集通常用来解决「连通性」问题 --- 主要功能大白话就是: - -1. Union - 将两个元素合并到一个集合中 -2. Find - 判断两个元素是否在同一个集合里 - -如何让两个元素连通呢?`father[a] = b; father[b] = c`,这样就好了,通过 a 可以找到 b,通过 b 可以找到 c。 - -#### 基础 - -并查集的实现为一个森林(多个树),每个树表示一个集合,树的每个节点表示一个元素。 - -```js -/** disjoint sets union */ -class Dsu { - // 初始化:每个元素位于一个「单独的集合」,--根节点为自身-- - constructor(size) { - this.father = Array.from(Array(size), (_, index) => index) - } - // 把集合 u, v 合并到一个集合,合并的是根~ - join(u, v) { - u = find(u) // 寻根 - v = find(v) // 寻根 - if (u === v) return - this.father[v] = u // 请注意!:这里连通的是入参 u,v 的根 - } - // 向上寻根 - find(u) { - if (u === this.father[u]) return u // 自身 - // return find(this.father[u]) - /** - * 路径压缩: 如果每次都如上行那样递归寻找上级,比较浪费时间 - * 路径压缩就是把节点直接接到根节点上,而这一步操作可以巧妙的在 find 过程中完成,如下: - */ - return (this.father[u] = find(this.father[u])) - } -} -``` - -- 启发式合并: - - 上面的合并比较随意,合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵。将较小集合合并到较大集合有助于平衡树的高度,从而提高查询效率。 - - ```js - // constructor - this.size = Array(size).fill(1) - - union(u, y) { - u = this.find(u); - v = this.find(v); - if (u === v) return - if (this.size[u] < this.size[v]) { - [u, v] = [v, u]; - } - this.father[v] = u; - this.size[u] += this.size[v]; - } - ``` - -#### 练习 - -##### lc.684 冗余连接 - -检测无向图的环~ 思想也很简单,利用并查集的 find,当新加入的边如果找到了同一个根,则说明新加入的边使得原来的树形成了环;否则就 union 新加入的边即可。 - -```js -/** - * @param {number[][]} edges - * @return {number[]} - */ -var findRedundantConnection = function (edges) { - const len = edges.length - const DSU = Array.from(Array(len + 1), (_, index) => index) - for (let i = 0; i < len; ++i) { - const [u, v] = edges[i] - const u1 = find(DSU, u) - const v1 = find(DSU, v) - if (u1 === v1) return edges[i] - DSU[v1] = u1 // union 简化到这里 - } - return [0] -} -function find(p, u) { - if (u === p[u]) return u - return (p[u] = find(p, p[u])) -} -``` - -##### lc.130 被围绕的区域 - -```js -/** - * @param {character[][]} board - * @return {void} Do not return anything, modify board in-place instead. - */ -var solve = function (board) { - if (board.length === 0) return - // 这道题一看就是岛屿类的问题,自然可以用 flood fill 算法去搞定,但本次重点是并查集~ - const rowLen = board.length - const colLen = board[0].length - - const DSU = Array.from(Array(rowLen * colLen + 1), (_, index) => index) // 多一个最后的节点用于给 dummyNode - const dummyNode = rowLen * colLen // 如果不用 dummyNode 就略微复杂了 - - // 遍历四条件边,把四条边上的 O 与 dummyNode 连通 - for (let i = 0; i < rowLen; ++i) { - if (board[i][0] === 'O') union(DSU, i * colLen, dummyNode) - if (board[i][colLen - 1] === 'O') union(DSU, i * colLen + colLen - 1, dummyNode) - } - for (let j = 0; j < colLen; ++j) { - if (board[0][j] === 'O') union(DSU, j, dummyNode) - if (board[rowLen - 1][j] === 'O') union(DSU, (rowLen - 1) * colLen + j, dummyNode) - } - - // 遍历整个内部节点,把所有的 O 连通起来(连完后,如果和边缘连接的就会在一个集合,否则在另一个集合) - // 利用方向数组遍历上下左右 - const dirs = [ - [-1, 0], - [1, 0], - [0, -1], - [0, 1] - ] - for (let i = 1; i < rowLen - 1; ++i) { - for (let j = 1; j < colLen - 1; ++j) { - if (board[i][j] === 'O') { - for (const [x, y] of dirs) { - const nx = i + x - const ny = j + y - if (board[nx][ny] === 'O') { - union(DSU, i * colLen + j, nx * colLen + ny) - } - } - } - } - } - // 遍历整个 board, 把中间包围的且没有和 dummyNode 连通的 O 变为 X - for (let i = 1; i < rowLen - 1; ++i) { - for (let j = 1; j < colLen - 1; ++j) { - if (board[i][j] === 'O' && find(DSU, i * colLen + j) !== find(DSU, dummyNode)) { - board[i][j] = 'X' - } - } - } -} -function find(p, u) { - if (u === p[u]) return u - return (p[u] = find(p, p[u])) -} -function union(p, u, v) { - u = find(p, u) - v = find(p, v) - if (u === v) return - p[v] = u -} -``` - -- 简单说一下,用 flood fill 算法怎么做,也很简单,就是先 dfs 四条边,先把与边相连的 O 都标记上,最后对整个 board 遍历,把 O 转为 x,把标记的转回为 O 即可。 lc.200 & lc.1905 与本题类似,之前用的 dfs flood fill 算法去做的,有时间可以用 DSU 也做一做。 - - - -##### lc.990 等式方程的可满足性 - -知道用并查集的前提下,这还不是小 case~ 又是一个无向图检测环的问题罢了。 - -```js -/** - * @param {string[]} equations - * @return {boolean} - */ -var equationsPossible = function (equations) { - // 很明显的环检测问题 - const DSU = Array.from(Array(26), (_, index) => index) - for (let i = 0; i < equations.length; ++i) { - const edge = equations[i] - const isConnected = edge[1] === '=' - if (isConnected) { - const u = edge.charCodeAt(0) - 'a'.charCodeAt() - const v = edge.charCodeAt(3) - 'a'.charCodeAt() - union(DSU, u, v) - } - } - for (let i = 0; i < equations.length; ++i) { - const edge = equations[i] - const isNotConnected = edge[1] === '!' - if (isNotConnected) { - const u = edge.charCodeAt(0) - 'a'.charCodeAt() - const v = edge.charCodeAt(3) - 'a'.charCodeAt() - if (find(DSU, u) === find(DSU, v)) return false - } - } - return true -} -function find(p, u) { - if (u === p[u]) return u - return (p[u] = find(p, p[u])) -} -function union(p, u, v) { - u = find(p, u) - v = find(p, v) - if (u === v) return - p[v] = u -} -``` - - - ---- - - - - diff --git "a/content/posts/algorithm/data structure/\351\223\276\350\241\250.md" "b/content/posts/algorithm/data structure/\351\223\276\350\241\250.md" deleted file mode 100644 index d34831f..0000000 --- "a/content/posts/algorithm/data structure/\351\223\276\350\241\250.md" +++ /dev/null @@ -1,894 +0,0 @@ ---- -title: '链表' -date: 2024-01-09T20:10:19+08:00 -lastmod: 2024-03-04 -series: [data structure] -categories: [algorithm] ---- - -## 链表 - -```java -class Node { - V value; - Node next; -} -class Node { - V value; - Node next; - Node last; -} -``` - -> 对于链表算法,在面试中,一定尽量用到空间复杂度最小的方法(不然凭啥用咱是吧 🐶)。 - -### 链表「换头」情况 - -操作链表出现「换头」的情况,函数的递归调用形式应该是 `head = func(head.next)`,所以函数在设计的时候就应该有一个 `Node` 类型的返回值,比如反转链表。 - -### 哨兵守卫 - -「哨兵守卫」是链表中的常用技巧。通过在链表头部或尾部添加守卫节点,可以简化对边界情况的处理。 - -### 链表中常用的技巧-快慢指针 - -#### 找到链表的中点、中点前一个、中点后一个 - -这个是硬编码能力,需要大量练习打好基本功。 - -```java -/** - * 奇数的中点; 偶数的中点靠前一位 - */ -Node s = head; -Node f = head; -while (f.next != null && f.next.next != null) { - s = s.next; - f = f.next.next; -} - -/** - * 奇数的中点; 偶数的中点靠后一位 lc.876 easy - */ -while (f != null && f.next != null) { - s = s.next; - f = f.next.next; -} -``` - -如果要进一步继续偏移,修改 f 或 s 的初始节点即可。 - -注意:因为 f 一次走两步,所以: - -- 想要获取中点往前的节点,修改 f 初始节点时 `f=head.next.next`,两个 next 才会让结果往前偏移一步 -- 想要获取中点往后的节点,修改 s 的初始节点 `s=head.next`,一个 next 就可以让结果往后偏移一步 - -### 练习 - -#### lc.2 两数相加 - -```js -/** - * Definition for singly-linked list. - * function ListNode(val, next) { - * this.val = (val===undefined ? 0 : val) - * this.next = (next===undefined ? null : next) - * } - */ -/** - * @param {ListNode} l1 - * @param {ListNode} l2 - * @return {ListNode} - */ -var addTwoNumbers = function (l1, l2) { - let dummy = new ListNode(-1) - let p = dummy - - let p1 = l1, - p2 = l2 - let carry = 0 - while (p1 || p2) { - let a = p1 ? p1.val : 0 - let b = p2 ? p2.val : 0 - let sum = a + b + carry - carry = (sum / 10) | 0 - p.next = new ListNode(sum % 10) - p = p.next - p1 = p1 ? p1.next : null - p2 = p2 ? p2.next : null - } - if (carry) { - p.next = new ListNode(carry) - } - return dummy.next -} -``` - -#### lc.19 删除链表倒数第 N 个节点 - -```js -/** - * @param {ListNode} head - * @param {number} n - * @return {ListNode} - */ -var removeNthFromEnd = function (head, n) { - let p = head - while (n--) { - p = p.next - } - let dummy = new ListNode(-1, head) - let p2 = dummy - while (p) { - p = p.next - p2 = p2.next - } - p2.next = p2.next.next - return dummy.next -} -``` - -lc.2 和 lc.19 是 dummy 守卫节点的最佳实践了,一个是新增,一个是删除。 - - - ---- - -#### lc.21 合并两个有序链表 easy - -```js -/** - * Definition for singly-linked list. - * function ListNode(val, next) { - * this.val = (val===undefined ? 0 : val) - * this.next = (next===undefined ? null : next) - * } - */ -/** - * @param {ListNode} list1 - * @param {ListNode} list2 - * @return {ListNode} - */ -var mergeTwoLists = function (list1, list2) { - let dummy = new ListNode(-1) - let p = dummy - let p1 = list1, - p2 = list2 - while (p1 && p2) { - if (p1.val < p2.val) { - p.next = p1 - p1 = p1.next - } else { - p.next = p2 - p2 = p2.next - } - p = p.next - } - if (p1) p.next = p1 - if (p2) p.next = p2 - return dummy.next -} -``` - -#### lc.23 合并 K 个有序链表 hard - -这道题比较朴素的做法是:已知两个有序链表的合并,那么遍历所有链表逐条合并即可。 - -性能强一点的做法是利用优先队列。JS 中没有,得先手动实现;Java 中有 PQ,得设置比较函数。 - -```js -/** - * Definition for singly-linked list. - * function ListNode(val, next) { - * this.val = (val===undefined ? 0 : val) - * this.next = (next===undefined ? null : next) - * } - */ -/** - * @param {ListNode[]} lists - * @return {ListNode} - */ -var mergeKLists = function (lists) { - let dummy = new ListNode(-1) - let p = dummy - lists = lists.filter(item => item !== null) - const pq = new PQ(lists, (a, b) => a > b) - while (!pq.isEmpty()) { - const top = pq.pop() - p.next = top - p = p.next - - if (top && top.next) { - pq.push(top.next) - } - } - return dummy.next -} - -/** 手动实现优先队列 */ -class PQ { - constructor(data, comparator) { - this.data = data - this.comparator = comparator - for (let i = data.length >> 1; i >= 0; --i) this.down(i) - } - up(i) { - while (i > 0 && this.comparator(this.data[(i - 1) >> 1].val, this.data[i].val)) { - this.swap((i - 1) >> 1, i) - i = (i - 1) >> 1 - } - } - down(i) { - let left = 2 * i + 1 - while (left < this.data.length) { - let min = left - if (left + 1 < this.data.length) { - min = this.comparator(this.data[left + 1].val, this.data[left].val) - ? left - : left + 1 - } - min = this.comparator(this.data[i].val, this.data[min].val) ? i : min - if (min === i) break - this.swap(min, i) - i = min - left = 2 * i + 1 - } - } - push(val) { - this.up(this.data.push(val) - 1) - } - pop() { - this.swap(0, this.data.length - 1) - const top = this.data.pop() - this.down(0) - return top - } - swap(i, j) { - const temp = this.data[i] - this.data[i] = this.data[j] - this.data[j] = temp - } - isEmpty() { - return this.data.length === 0 - } -} -``` - -另一种递归解:归并 - -```js -var mergeKLists = function (lists) { - if (lists.length === 0) return null - if (lists.length === 1) return lists[0] - const mid = lists.length >> 1 - const left = mergeKLists(lists.slice(0, mid)) - const right = mergeKLists(lists.slice(mid)) - return mergeTwoList(left, right) -} -function mergeTwoList(a, b) { - if (!a) return b - if (!b) return a - if (a.val < b.val) { - a.next = mergeTwoList(a.next, b) - return a - } else { - b.next = mergeTwoList(a, b.next) - return b - } -} -``` - -#### lc.24 两两交换链表中的节点 - -不错的一道题 - -```js -var swapPairs = function (head) { - if (head === null || head.next === null) return head - const newHead = head.next // cache - head.next = swapPairs(head.next.next) - newHead.next = head // 归 - 后序位置 - return newHead -} - -/** 迭代解法 */ -var swapPairs = function (head) { - const dummy = new ListNode(-1) - dummy.next = head - let p = dummy - while (p.next !== null && p.next.next !== null) { - const a = p.next // cache - const b = p.next.next // cache - - p.next = b - a.next = b.next - b.next = a - p = a - } - return dummy.next -} -``` - -可以看见,无论是递归还是迭代,因为要变动后方的节点,所以一般都可以先做一层缓存。 - -#### lc.83 删除链表的重复元素 easy - -```js -/** - * @param {ListNode} head - * @return {ListNode} - */ -var deleteDuplicates = function (head) { - let p = head - while (p && p.next) { - if (p.val === p.next.val) { - p.next = p.next.next - } else { - p = p.next - } - } - return head -} -``` - -#### 单链表分区(左<,中=,右>) - -在三路快排中,有类似的操作。所以如果借助数组,那么就是荷兰国旗问题了。 - -但是这样做时间复杂度就高了,对于链表,能用指针操作的,就不借助额外空间。要想空间复杂度为 O(1),那么就得借助 6 个变量 head >tail。 - -##### lc.86 分隔链表 - -这道题是上方分区的简化版本,只用把小于 X 的节点放到大于等于 X 的节点之前即可。 - -```js -/** - * @param {ListNode} head - * @param {number} x - * @return {ListNode} - */ -var partition = function (head, x) { - if (head === null || head.next === null) return head - let small = new ListNode(-1) - let large = new ListNode(-1) - let p = head, - p1 = small, - p2 = large - while (p !== null) { - const val = p.val - if (val < x) { - p1.next = p - p1 = p1.next - } else { - p2.next = p - p2 = p2.next - } - - p = p.next - } - p2.next = null // 注意需要给大的收尾~ - p1.next = large.next - return small.next -} -``` - -#### lc.138 复制含有随机指针的链表 - -```java -/** - * Java版本:这道题要是空间复杂度不需要 O(1),那么用哈希表就挺好做的 - * - * O(1) 的空间复杂度,就需要一定的技巧了,就是 拼接+拆分。 - */ - // 哈希表 -class Solution { - public Node copyRandomList(Node head) { - Map map = new HashMap<>(); - Node p = head; - while (p != null) { - map.put(p, new Node(p.val)); - p = p.next; - } - p = head; - while(p != null) { - map.get(p).next = map.get(p.next); - map.get(p).random = map.get(p.random); - p = p.next; - } - return map.get(head); - } -} -/** 空间复杂度 O(1) */ -public Node copyRandomList(Node head) { - // 恰到好处的拼接,复制节点直接拼到原节点后,这样会发现: - // ** 新节点的random指向的就是原节点random指向节点的下一个节点 ** - // 1.复制节点 - Node p = head; - while (p != null) { - Node newNode = new Node(p.val); - newNode.next = p.next; - p.next = newNode; - p = newNode.next; - } - // 2.设置 random - p = head; - while (p != null) { - if(p.random != null) { - p.next.random = p.random.next; - } - p = p.next.next; - } - // 3. 分离链表 - Node dummy = new Node(-1); - p = head; - Node curr = dummy; - while (p != null) { - curr.next = p.next; - curr = curr.next; - p.next = curr.next; - p = p.next; - } - return dummy.next; -} -``` - -```js -/** - * // Definition for a Node. - * function Node(val, next, random) { - * this.val = val; - * this.next = next; - * this.random = random; - * }; - */ - -/** - * @param {Node} head - * @return {Node} - */ -var copyRandomList = function (head) { - /** 尽可能降低空间复杂度,使用拼接技巧 */ - let p = head - while (p !== null) { - const cpNode = new Node(p.val) - cpNode.next = p.next - p.next = cpNode - p = cpNode.next - } - p = head - while (p !== null) { - if (p.random) { - p.next.random = p.random.next - } - p = p.next.next - } - const dummy = new Node(-1) - let curr = dummy - p = head - while (p !== null) { - curr.next = p.next - curr = curr.next - p.next = curr.next - p = p.next - } - return dummy.next -} -``` - -#### 单链表环相关问题 - -快慢指针判断是否有环的原理是:如果有环,则必定两个指针会相遇;否则,快指针将走到空。 - -##### lc.141 判断链表是否有环 easy - -```js -/** - * @param {ListNode} head - * @return {boolean} - */ -var hasCycle = function (head) { - if (head == null) return false - let f = head.next, - s = head - while (true) { - if (f == null || f.next == null) return false - s = s.next - f = f.next.next - if (s === f) return true - } - // 下面那种写法也行 - // if (head == null) return false - // let s = head, - // f = head.next // !!! 注意这里是先走了一步的 - // while (s !== f) { - // if (f == null || f.next == null) return false - // s = s.next - // f = f.next.next - // } - // return true -} -``` - -##### lc.142 返回环的起点 - -此题应用的是著名的 [Floyd 判圈算法(维基百科)](https://zh.wikipedia.org/wiki/Floyd%E5%88%A4%E5%9C%88%E7%AE%97%E6%B3%95)。应用快慢指针,第一次相遇后快指针回到头部和慢指针一起走,再次相遇就是环的起点。 - -假设 head 到环起点的距离为 a,环起点到快慢指针相遇的距离为 b,环长度为 c ,则: - -- 慢指针走了 a + b, -- 快指针走了 a + b + n\*c, n 为圈数 - -第一次相遇时,慢指针走 k,快指针走 2k,快比慢多走了 k 步,所以 n \* c = k,因此 k 是环的整数倍。所以回到起点的指针要再走 k - b 步才到起点,而从第一次相遇点走到环起点的距离也恰好也为 k - b。 - -```js -/** - * @param {ListNode} head - * @return {ListNode} - */ -var detectCycle = function (head) { - if (head === null || head.next === null) return null - let s = head, - f = head - while (true) { - if (f == null || f.next == null) return null - s = s.next - f = f.next.next - if (s === f) break - } - - f = head - while (s !== f) { - f = f.next - s = s.next - } - return s -} -``` - -#### 单链表相交问题 - -> 处理链表相交一类的问题,核心在于 **「抹除长度差异」**,然后齐头并进,有相等节点则相交。 - -##### lc.160 相交链表 easy - -```js -/** - * @param {ListNode} headA - * @param {ListNode} headB - * @return {ListNode} - */ -var getIntersectionNode = function (headA, headB) { - if (headA == null || headB == null) return null - - let n = 0 - let p1 = headA, - p2 = headB // 假定 p1 为长,p2 为 短 - while (p1 !== null) { - n++ - p1 = p1.next - } - while (p2 !== null) { - n-- - p2 = p2.next - } - - if (n > 0) { - p1 = headA - p2 = headB - } else { - p1 = headB - p2 = headA - } - - n = Math.abs(n) - while (n--) { - p1 = p1.next - } - - while (p1 !== p2) { - p1 = p1.next - p2 = p2.next - } - - return p1 -} -``` - -```java -public class Solution { - public ListNode getIntersectionNode(ListNode headA, ListNode headB) { - if (headA == null || headB == null) return null; - ListNode l = headA, s = headB; - int n = 0; - while (l != null) { - n++; - l = l.next; - } - while (s != null) { - n--; - s = s.next; - } - if (l != s) return null; - l = n > 0 ? headA : headB; - s = n > 0 ? headB : headA; - n = Math.abs(n); - while (n != 0) { - l = l.next; - n--; - } - while (l != s) { - l = l.next; - s = s.next; - } - return l; - } -} -``` - -有个取巧的小技巧是,当两个指针第一次走到末尾后,分别跳到对方链表上去,这样*等价于*抹除了长度差异,如有相交,则会走到相同节点上。具体见官解。 - -##### 进阶:有环单链表相交 - -```java -/** - * 有环单链表相交要分清楚情况: - * 1. 不相交 - * 2. 环的起始节点一样,则把这个起始节点看成两个无环链表的终点,利用上题的解法求出相交节点即可 - * 3. 环的起始节点不一样,则这两个节点都是相交节点,返回任意一个即可 - * - * 1 和 3 情况的区分是,一个环的节点继续走,走回自己之前能遇到另一个环的节点就是情况3,否则就是情况1 - */ -public ListNode bothLoop(ListNode head1, ListNode head2, ListNode loop1, ListNode loop2) { - /** - * 第 2 种情况,两个有环链表共用环起点, - * 那么此时可以把环的起点看成是head1和head2到的无环链表的终点, - * 也就转化成了寻找两个无环单链表相交点的问题了。 - */ - if (loop1 == loop2) { - ListNode p1 = head1, p2 = head2; - int n = 0; - while (p1 != loop1) { - n++; - p1 = p1.next; - } - while (p2 != loop2) { - n--; - p2 = p2.next; - } - p1 = n > 0 ? head1 : head2; - p2 = n > 0 ? head2 : head1; - n = Math.abs(n); - while (n > 0) { - p1 = p1.next; - n--; - } - while (p1 != p2) { - p1 = p1.next; - p2 = p2.next; - } - return p1; - } else { - /** - * 第 1 种情况不相交和第 3 种情况相交在环上不同的节点 - * 区分方式是让节点从 loop1 开始继续绕环走,能遇到 loop2 则说明相交了,否则为不相交 - */ - ListNode p = loop1.next; // 先前进一步,否则while进不去喽~ - while (p != loop1) { - if (p == loop2) { - return loop2; // loop1 loop2都是相交点,随意返回一个 - } - p = p.next; - } - return null; - } -} -``` - -#### 反转链表问题 - -##### lc.206 反转链表 hot easy - -最基本的考验对双指针和递归的理解。不错的一道经典题。 - -```java -// 迭代法,那就是双指针了,先存储后继节点,剩下的都好办 -public ListNode reverseList(ListNode head) { - ListNode p = head, pre = null; - while(p != null) { - ListNode next = p.next; // 储存后继节点 - p.next = pre; - pre = p; - p = next; - } - return pre; -} -// 递归法 -public ListNode reverseList(ListNode head) { - if (head == null || head.next == null) return head; - ListNode newHead = reverseList(head.next); // newHead 是最后一个节点 - // 后序遍历,假设递归到最后了,此时的 head 是最后一个节点的前一个节点 - head.next.next = head; // 把下一个节点的next指向自身 - head.next = null; // 把自身的next指向null - return newHead; // 返回尾部节点的引用指针即可 -} -``` - -```js -/** - * @param {ListNode} head - * @return {ListNode} - */ -var reverseList = function (head) { - if (!head || !head.next) return head - const newHead = reverseList(head.next) - head.next.next = head - head.next = null - return newHead -} - -var reverseList = function (head) { - let p = head, - pre = null - while (p) { - const next = p.next - p.next = pre - pre = p - p = next - } - return pre -} -``` - -##### lc.92 反转链表 II - -如果不要求只一次遍历,利用 lc.206 就可以完成了。 - -只能遍历一次的话,需要技巧,就是每遍历到反转区间内的一个元素,就把这个元素放到反转区间的头部。 - -```js -/** - * Definition for singly-linked list. - * function ListNode(val, next) { - * this.val = (val===undefined ? 0 : val) - * this.next = (next===undefined ? null : next) - * } - */ -/** - * @param {ListNode} head - * @param {number} left - * @param {number} right - * @return {ListNode} - */ -var reverseBetween = function (head, left, right) { - const dummy = new ListNode(-1) // left 可能为头,守卫简化操作 - dummy.next = head - - let p = dummy - for (let i = 0; i < left - 1; ++i) { - p = p.next - } - // 此时,p 指向区间的前一个节点 - - const len = right - left + 1 - let curr = p.next - let step = len - 1 - while (step--) { - const next = curr.next - curr.next = next.next - next.next = p.next - p.next = next - } - - return dummy.next -} -``` - -##### lc.25 K 个一组反转链表 hard - -```js -/** - * Definition for singly-linked list. - * function ListNode(val, next) { - * this.val = (val===undefined ? 0 : val) - * this.next = (next===undefined ? null : next) - * } - */ -/** - * @param {ListNode} head - * @param {number} k - * @return {ListNode} - */ -var reverseKGroup = function (head, k) { - if (head === null) return head - let a = (b = head) - for (let i = 0; i < k; ++i) { - if (b == null) return head // 不足 k - b = b.next - } - - const newHead = reverse(a, b) - a.next = reverseKGroup(b, k) // [a, b) 反转后 a 的 next 为剩下的链表反转 - return newHead -} - -/** - * 反转 [head, tail) 区间的链表,其实普通的反转链表反转的就是 [head, null) 区间罢了 - */ -function reverse(head, tail) { - let prev = null, - curr = head - while (curr !== tail) { - const next = curr.next - curr.next = prev - prev = curr - curr = next - } - return prev -} -``` - -#### lc.234 回文链表 easy - -```js -/** - * Definition for singly-linked list. - * function ListNode(val, next) { - * this.val = (val===undefined ? 0 : val) - * this.next = (next===undefined ? null : next) - * } - */ -/** - * @param {ListNode} head - * @return {boolean} - */ -var isPalindrome = function (head) { - if (head === null) return false - let s = head, - f = head - while (f !== null && f.next !== null) { - s = s.next - f = f.next.next - } - - // 对后半部分, s 进行反转 - let prev = null - while (s !== null) { - const next = s.next - s.next = prev - prev = s - s = next - } - - // 对 prev 和 head 进行比较 - while (prev) { - if (prev.val !== head.val) return false - prev = prev.next - head = head.next - } - - return true -} -``` - ---- - - diff --git "a/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\21302\345\256\214\345\205\250\350\203\214\345\214\205.md" "b/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\21302\345\256\214\345\205\250\350\203\214\345\214\205.md" deleted file mode 100644 index f2aaba2..0000000 --- "a/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\21302\345\256\214\345\205\250\350\203\214\345\214\205.md" +++ /dev/null @@ -1,214 +0,0 @@ ---- -title: '动态规划之完全背包' -date: 2022-10-12T04:31:28+08:00 -draft: true ---- - -首先,继续看一下 01 背包中引用的图吧。 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210161922324.png) - -与 01 背包的区别就是 物品是无限的。在做选择时,同样也是只有两种,选择和不选择。 - -公式: - -```js -// 求组合 -for (int i = 0; i < coins.size(); i++) { // 遍历物品 - for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量 - dp[j] += dp[j - coins[i]]; - } -} - -// 求排列 -for (int j = 0; j <= amount; j++) { // 遍历背包容量 - for (int i = 0; i < coins.size(); i++) { // 遍历物品 - if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]]; - } -} -``` - -为什么要固定这种顺序,很简单,因为先遍历背包容量的话,会有产生重复的问题: - -```js -如果交换两层循环顺序的话会先遍历金额j后再遍历每个coin: -f[i][j] = f[i][j-c[0]] + f[i][j-c[1]] +...+ f[i][j-c[i]] ① -①中,每一项表示在f[i][j-c[k]]基础上加上c[k]达成金额j的组合数,也即由前i个coin组成金额j且至少存在一个coin[k]的组合数。 -一共i项,每一项组合情况相互可能会有重叠的情况。 - -例如有硬币{1,2}组成5,f[1][5] = f[1][3] + f[1][4]. - f[1][3]中有{1,1,1},{1,2} + {2}. - f[1][4]中有{1,1,1,1}{1,1,2}{2,2} + {1}。 -实际上,其中{1,1,1,2}与{1,1,2,1}、{1,2,2}与{2,2,1}是同一种组合情况,重复进行了计数。 - - -正确情况应该为: -f[i][j] =f[i-1][j] + f[i][j-c[i]]; -f[i-1][j] = f[i-2][j] + f[i-1][j-c[i-1]] -... -f[1][j] = f[0][j] + f[1][j-c[1]] -f[0][j] = f[0][j-c[0]] -上式两边相加得 -f[i][j] = f[0][j-c[0]] + f[1][j-c[1]] +...+ f[i][j-c[i]] ② -②中,每一项表示在f[k][j-c[k]]基础上加上c[k]达成金额j的组合数,也即由前k个coin组成金额j且至少存在一个coin[k]的组合数。不重不漏地将f[i][j]分成i组。 - -例如有硬币{1,2}组成5,f[1][5] = f[0][4] + f[1][3]. - f[0][4]中有{1,1,1,1} + {1}. - f[1][3]中有{1,1,1}{1,2} + {2}。 -所有组合情况加起来不重不漏。 -``` - ---- - -### [518. 零钱兑换 II](https://leetcode.cn/problems/coin-change-ii/) - -```js -/** - * @param {number} amount - * @param {number[]} coins - * @return {number} - */ -var change = function (amount, coins) { - // 定义dp[i][j] 表示前i个([0..i-1])个数字凑成总金额为j的组合个数。 - // base case dp[0][..] = 0 dp[..][0] = 1 - const m = coins.length - const dp = Array.from(new Array(m + 1), () => new Array(amount + 1).fill(0)) - for (let i = 0; i <= m; ++i) dp[i][0] = 1 - for (let i = 1; i <= m; ++i) { - for (let j = 1; j <= amount; ++j) { - if (j - coins[i - 1] >= 0) { - dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]] - } else { - dp[i][j] = dp[i - 1][j] - } - } - } - return dp[m][amount] -} -``` - -> 注意:求组合数,不能交换内外 for 循环的遍历顺序,否则会有重叠情况,导致了重复计数。 - -完全背包的一维: - -```js -// 这道题是求组合 -var change = function (amount, coins) { - const m = coins.length - const dp = new Array(amount + 1).fill(0) - dp[0] = 1 - for (let i = 0; i < m; ++i) { - for (let j = coins[i]; j <= amount; ++j) { - dp[j] += dp[j - coins[i]] - } - } - return dp[amount] -} -``` - -### [377.组合总和 IV](https://leetcode.cn/problems/combination-sum-iv/) - -题目实际上求的是排列!一定要先遍历背包!有争议的题 - -```js -/** - * @param {number[]} nums - * @param {number} target - * @return {number} - */ -// 完全背包求排列, 先遍历背包 -var combinationSum4 = function (nums, target) { - const dp = Array(target + 1).fill(0) - dp[0] = 1 - for (let i = 1; i <= target; ++i) { - for (const num of nums) { - num <= i && (dp[i] += dp[i - num]) - } - // for(let j = 0; j < nums.length; ++j) { - // if(i - nums[j] >= 0) { - // dp[i] = dp[i] + dp[i - nums[j]] - // } - // } - } - return dp[target] -} -``` - -这道题用二维 dp 其实是挺难理解的,见力扣上的[讨论-这道题根本不是背包问题](https://leetcode.cn/problems/combination-sum-iv/solutions/842528/zhe-dao-ti-gen-ben-bu-shi-bei-bao-wen-ti-eynx/?page=2)以及给出的解法。 - -### [322.零钱兑换](https://leetcode.cn/problems/coin-change/) - -可以无限使用凑数这不就是完全背包问题吗?动态规划走起: - -```js -/** - * @param {number[]} coins - * @param {number} amount - * @return {number} - */ -var coinChange = function (coins, amount) { - // 定义dp[i] 表示 整数 i 需要的最少硬币个数 - const dp = Array(amount + 1).fill(amount + 1) // 肯定不会超过amount+1 - // base case - dp[0] = 0 - for (let i = 0; i <= amount; ++i) { - // 遍历状态 - for (const coin of coins) { - // 遍历选择 - if (i < coin) continue - dp[i] = Math.min(dp[i], dp[i - coin] + 1) - } - } - return dp[amount] === amount + 1 ? -1 : dp[amount] -} -``` - -### [279.完全平方数](https://leetcode.cn/problems/perfect-squares/) - -动态规划 定义 dp[i] 表示数字 i 的完全平方数最少。 - -```js -/** - * @param {number} n - * @return {number} - */ -var numSquares = function (n) { - const dp = Array(n + 1).fill(0) // 根据定义,要求dp[n]所以空间得是n+1 - for (let i = 0; i <= n; ++i) { - dp[i] = i // 最差每次+1 - for (let j = 1; j * j <= i; ++j) { - dp[i] = Math.min(dp[i], dp[i - j * j] + 1) - } - } - return dp[n] -} -``` - -### [139.单词拆分](https://leetcode.cn/problems/word-break/) - -没错,这也可以看成是完全背包~ - -```js -// 完全背包 s为背包, 但是物品wordDict中的每个单词的使用有讲究,看下方重点 -var wordBreak = function (s, wordDict) { - // 定义dp[i] 表示s[0..i-1](前i个)可以用字典中的词组成 - const n = s.length - const dp = new Array(n + 1).fill(false) - // base case - dp[0] = true - for (let i = 0; i <= n; i++) { - for (let j = 0; j < i; j++) { - // ! 重点 j表示分割 状态转移方程看s[0..j-1]和s[j..i-1] 是否都合法 - if (dp[j] && wordDict.includes(s.substring(j, i))) { - dp[i] = true - break - } - } - } - return dp[n] -} -``` - -## 参考 - -- [代码随想录](https://programmercarl.com/0518.%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2II.html#%E6%80%9D%E8%B7%AF) diff --git "a/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\344\271\260\345\215\226\350\202\241\347\245\250.md" "b/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\344\271\260\345\215\226\350\202\241\347\245\250.md" deleted file mode 100644 index ab8be96..0000000 --- "a/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\344\271\260\345\215\226\350\202\241\347\245\250.md" +++ /dev/null @@ -1,203 +0,0 @@ ---- -title: '动态规划之买卖股票' -date: 2022-10-12T09:31:28+08:00 -draft: true ---- - -### [121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) - -这道题最直观的方法是,贪心算法 --- 此题中,只需要左边最小,右边最大,那么就是最大利润啦 🕶。 - -```js -/** - * @param {number[]} prices - * @return {number} - */ -var maxProfit = function(prices) { - // 贪心算法 - let min = Infinity - let res = 0 - for(let i = 0; i < prices.length; ++i) { - min = Math.min(min, prices[i]) - res = Math.max(res, prices[i] - min) - } - return res -}; -``` - -不过我们的重点是动态规划,还是得好好分析一下: - -- 状态 天数/持有状态(未持有/持有),所以需要一个二维 dp 数组,算出每个第 i 天持有或不持有状态下的最大收益 -- 选择 买入/卖出/不操作 - -```js -/** - * @param {number[]} prices - * @return {number} - */ -var maxProfit = function(prices) { - // 定义dp[i][0] 表示第i天未持有股票的最大现金; dp[i][0] 表示第i天持有股票的最大现金 - const m = prices.length - const dp = new Array(m).fill([0, 0]) - dp[0] = [0, -prices[0]] // [未持有, 持有] - for(let i = 1; i < m; ++i) { - dp[i] = [ - // 手里未持有 前一天未持有(不操作)/卖了 - Math.max(dp[i-1][0], dp[i-1][1] + prices[i]), - // 手里持有 前一天持有(不操作)/买了 - Math.max(dp[i-1][1], -prices[i]) - ] - } - return dp[m-1][0] -}; -``` - -其实 `[未持有, 持有]` 这就是最简单的 「状态机」 了,和之前 `打家劫舍III` 的类似。 - -### [122.买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) - -与上一题不同的地方只有一个,就是可以连续交易多次。状态与选择都不变,影响的只是买入的时候的状态变化。 - -```js -/** - * @param {number[]} prices - * @return {number} - */ -var maxProfit = function(prices) { - const m = prices.length - const dp = new Array(m).fill([0,0]) - dp[0] = [0, -prices[0]] - for(let i = 1; i < m; ++i) { - dp[i] = [ - Math.max(dp[i-1][0], dp[i-1][1] + prices[i]), - Math.max(dp[i-1][1], dp[i-1][0] - prices[i]) // 注意这里,只交易一次,一定是-prices[i] 交易多次就要受到上一次未持有买入的情况了 - ] - } - return dp[m-1][0] -}; -``` - -### [123. 买卖股票的最佳时机 III](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/) - -121 只能交易一次,122 可以交易 n 次,这次,至多交易两次,也就是可能交易[0..2]次~ 搞人嘛这不是(╥╯^╰╥) - -选择从 「买入/卖出」 拓展为 「第一次买入/第一次卖出/第二次买入/第二次卖出」,直接影响到了持有状态,好,那么就来把状态机发扬光大吧! - -```js -/** - * @param {number[]} prices - * @return {number} - */ -var maxProfit = function(prices) { - const m = prices.length - const dp = new Array(m).fill([0,0,0,0]) // [1无,1有,2无,2有] - dp[0] = [ 0, -prices[0], 0, -prices[0]] // 关于第二次可以理解为第一次直接买完后卖掉了 - for(let i = 1; i < m; ++i) { - dp[i] = [ - Math.max(dp[i-1][0], dp[i-1][1] + prices[i]), // 卖 - Math.max(dp[i-1][1], -prices[i]), // 买 - Math.max(dp[i-1][2], dp[i-1][3] + prices[i]), // 卖 - Math.max(dp[i-1][3], dp[i-1][0] - prices[i]), // 买 - ] - } - return dp[m-1][2] -}; -``` - -其实我是故意把 dp 的定义 "卖" 放到 "买" 前面,目的是不要记公式,而是清楚推理逻辑,实际 先买后卖 的定义更加方便理解与好看 😁。 - -另外,这道题把 i 这层维度干掉,就是空间压缩版本了。 - -### [188. 买卖股票的最佳时机 IV](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/) - -相比 123 题,这道题因为是 k 次交易,并不能精准确定每天的持有状态。 - -相对于 121,122 题等于是多了一个 「交易次数」 的状态,那么需要对每天的状态进行拓展。 - -```js -/** - * @param {number} k - * @param {number[]} prices - * @return {number} - */ -var maxProfit = function(k, prices) { - const m = prices.length - const dp = Array.from(new Array(m), () => new Array(k + 1).fill([0, 0])) - - for(let j = 0; j <= k; ++j) { - dp[0][j] = [0, -prices[0]] - } - for(let i = 1; i < m; ++i) { - dp[i][0] = [0, Math.max(dp[i-1][0][1], dp[i-1][0][0] - prices[i])] - } - - for(let i = 1; i < m; ++i) { - for(let j = 1; j <= k; ++j) { - dp[i][j] = [ - Math.max(dp[i-1][j][0], dp[i-1][j-1][1] + prices[i]), - Math.max(dp[i-1][j][1], dp[i-1][j][0] - prices[i]) - ] - } - } - return dp[m-1][k][0] -}; -``` - -注意两点吧:1. 第二个维度交易次数,初始化 dp 时,它具体是[0..k],所以容量应该是 `k + 1`;2. base case 初始化顺序不能颠倒,先初始化 i 为 0,在初始化 k 为 0 的情况。 - -不难发现,123 题就是这道题 k = 2 的情况~ - -### [309.最佳买卖股票时机含冷冻期](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/) - -相比 121 题,多了个冷冻期的限制,转换过来就是每天的状况又多了一种 -- 冷冻期即卖出后第二天不能操作。 -所以当天的状态就是 「持有/未持有(非冷冻期)/未持有(冷冻期)」,其实就是把状态机拓展了一位。 - -```js -/** - * @param {number[]} prices - * @return {number} - */ -var maxProfit = function(prices) { - const m = prices.length - // 持有/未持有(冷冻期)/未持有(非冷冻期) - const dp = new Array(m).fill([0,0,0]) - // base case - dp[0] = [-prices[0], 0, 0] - for(let i = 1; i < m; ++i) { - dp[i] = [ - Math.max(dp[i-1][0], dp[i-1][2] - prices[i]), // 之前已经持有或者从非冷冻期卖出 - dp[i-1][0] + prices[i], // 一定是今天卖掉了 - Math.max(dp[i-1][1], dp[i-1][2]) // 可能是从冷冻期或者非冷冻期过来的 - ] - } - return Math.max(dp[m-1][1], dp[m-1][2]) -}; -``` - -### [714.买卖股票的最佳时机含手续费](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) - -```js -/** - * @param {number[]} prices - * @param {number} fee - * @return {number} - */ -var maxProfit = function(prices, fee) { - const m = prices.length - const dp = new Array(m).fill([0, 0]) - dp[0] = [0, -prices[0]] - for(let i = 1; i < m; ++i) { - dp[i] = [ - Math.max(dp[i-1][0], dp[i-1][1] + prices[i] - fee), - Math.max(dp[i-1][1], dp[i-1][0] - prices[i] ) - ] - } - return dp[m-1][0] -}; -``` - -和 122 题没什么两样,多了个费用而已~ - -### 总结 - -股票问题,可以让我们对 「状态」 和 「选择」 的理解更加深刻,同时对 「状态机」 的运用也更加熟练,应当理解思想并熟练运用。 diff --git "a/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\211\223\345\256\266\345\212\253\350\210\215.md" "b/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\211\223\345\256\266\345\212\253\350\210\215.md" deleted file mode 100644 index 7b45520..0000000 --- "a/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\211\223\345\256\266\345\212\253\350\210\215.md" +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: '动态规划之打家劫舍' -date: 2022-10-12T09:31:28+08:00 -draft: true ---- - -### [198.打家劫舍](https://leetcode.cn/problems/house-robber/description/) - -这道题比较简单,属于比较常规的动态规划,转移方程也比较好写。题目说:“如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。” - -- 状态:持续影响偷 💰 结果的因素,是 --> 房屋索引区间 [0...i] 的变化。状态只有一个,所以一维 dp 数组即可 -- 选择:到这个房屋偷/还是不偷 - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var rob = function(nums) { - // 定义 dp[i] 表示 房屋[0..i] 能偷到的最大金额 - const m = nums.length - const dp = new Array(m).fill(0) - dp[0] = nums[0] - dp[1] = Math.max(nums[0], nums[1]) - // dp[i] = max(dp[i-1](不偷这家), dp[i-2] + nums[i](偷这家)) - for(let i = 2; i < m; ++i) { - dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]) - } - return dp[m - 1] -}; -``` - -### [213.打家劫舍 II](https://leetcode.cn/problems/house-robber-ii/) - -与上题类似,唯一的不同是,房屋首尾相连了。 - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var rob = function(nums) { - // 首尾相连,意味着只能选一个, 那么就变成了区间[0..m-2] 和[1...m-1]之间取最大值 - const m = nums.length - if(m === 1) return nums[0] - const dp = new Array(m).fill(0) - dp[0] = nums[0] - dp[1] = Math.max(nums[0], nums[1]) - // [2..m-1) - for(let i = 2; i < m - 1; ++i) { - dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]) - } - const max1 = dp[m-2] - - dp[1] = nums[1] - dp[2] = Math.max(nums[1], nums[2]) - // [3..m) - for(let i = 3; i < m; ++i) { - dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]) - } - const max2 = dp[m-1] - return Math.max(max1, max2) -}; -``` - -### [337.打家劫舍 III](https://leetcode.cn/problems/house-robber-iii/) - -前面打劫了一条街道和一圈房屋,再来打劫一下二叉树吧 🌲 - -其实无非就是从遍历数组变成了遍历树而已。 -而动规中对于子树的遍历,因为需要推导,后序遍历可以有返回值,拿到子树的推导结果,所以一般的都是后续遍历。 - -```js -/** - * @param {TreeNode} root - * @return {number} - */ -var rob = function(root) { - const traverse = (node) => { - if (!node) return [0, 0]; - const left = traverse(node.left); - const right = traverse(node.right); - // 不偷当前节点,左右子节点都可以偷或不偷,取最大值 - const DoNot = Math.max(left[0], left[1]) + Math.max(right[0], right[1]); - // 偷当前节点,左右子节点只能不偷 - const Do = node.val + left[0] + right[0]; - // [不偷,偷] - return [DoNot, Do]; - } - return Math.max(...traverse(root)) -}; -``` diff --git "a/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\347\212\266\346\200\201\345\216\213\347\274\251.md" "b/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\347\212\266\346\200\201\345\216\213\347\274\251.md" deleted file mode 100644 index 93e41bb..0000000 --- "a/content/posts/algorithm/trick/dp/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\347\212\266\346\200\201\345\216\213\347\274\251.md" +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: '动态规划之状态压缩' -date: 2022-10-12T01:26:36+08:00 -draft: true ---- - -## 状态压缩 - -状态压缩是对空间复杂度的一种优化手段,推荐看看 labuladong 大佬的文章。(因为他的网站经常跟新文章地址,就不贴链接了)。 - -**对于状态压缩要有一副二维 table 的脑图,以及对遍历方向掌握到位。** - -```js -for (int i = n - 2; i >= 0; i--) { - for (int j = i + 1; j < n; j++) { - // 状态转移方程 - if (s[i] == s[j]) - dp[i][j] = dp[i + 1][j - 1] + 2; - else - dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); - } -} -``` - -对于上方递推公式,`dp[i][j]` 只依赖于 `dp[i+1][j-1]`,`dp[i+1][j]`,`dp[i][j-1]`,见下图。 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210180953079.png) - -记住:**空间压缩的核心思路就是,将二维数组「投影」到一维数组**: - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210181835977.png) - -> 在代码中呢,就是无脑去掉 dp[i][j] 的 [i] 这个维度,去掉维度后,可能会产生覆盖问题。 -> 然后思考每个一维 dp 的含义。 -> 有可能需要调整遍历顺序。 -> base case 也要看着修改,同样也是投影 - -继续上面的例子: - -i 从下往上遍历, j 从左往右遍历,即 i 是控制遍历层数的,i+1 就是上一层。 - -- dp[j]: 在被赋值之前代表上一轮遍历的值,即 dp[i+1][j] -- dp[j-1]: 因为 j 之前的已经被覆盖过了,所以代表 dp[i][j-1] -- 现在就差一个 dp[i+1][j-1] 了,因为一维,它被 dp[i][j-1] 无情地覆盖掉了,可是按照原来的二维 dp 必须得知道这个值才行,咋办呢?其实可以很自然的想到,用个变量缓存住不就好了?Yes,you got it! - -```js -for (int i = n - 2; i >= 0; i--) { - // 每层循环都定义一个变量协助储存一维数组要求 j 时 j-1 位置被覆盖前的值 - let pre = 0; - for (let j = i + 1; j < n; j++) { - let temp = dp[j]; // dp[j] 此时取到的是上一轮遍历的值 即 dp[i+1][j] - if (s[i] == s[j]) - // dp[i][j] = dp[i+1][j-1] + 2; - dp[j] = pre + 2; - else - // dp[i][j] = max(dp[i+1][j], dp[i][j-1]); - dp[j] = max(dp[j], dp[j - 1]); - // 下轮遍历 j + 1, pre 就是 dp[i+1][j-1] 了 - pre = temp; - } -} -``` - -可能有点抽象,再举个例子: -比如 j 索引从 1 变为 2 时,一维数组中初始化 base case 已经占满了 1, -当遍历索引 1 时, 先把索引 1 的值用 temp 存住, 然后索引 1 会被新的值 new 去覆盖掉, -遍历到索引 2 时, 需要根据 dp[j-1] 也就是刚刚被覆盖掉后新值 new (原二维 dp[i][j-1]) 和索引 1 之前的值 temp (原二维 dp[i+1][j-1]) 以及 索引 2 被赋值之前的值 dp[j] (原二维 dp[i+1][j])来推导出。如下图 😂(画的有点简约了哈) - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210182331990.png) - -更简单的,如果 dp[i][j] 只与 dp[i-1][...] 相关,投影到一维上,那更可以压缩状态啦,而且还没有覆盖的问题。 -两个注意点: - -1. 只是滚动数组需要注意下遍历方向,有时候存在维度的遍历顺序会相反,比如 dp[j] 依赖于 dp[j-1]时,如果正序遍历,dp[j-1]会被新的值给覆盖,这就不对了,所以倒着来就好了。 -2. 当去掉一个维度时,如果存在 if 判断,有可能会消除这个判断,加到 for 循环判断上去。 - -比如 1049 题的状态压缩: - -```js -/** - * @param {number[]} stones - * @return {number} - */ -var lastStoneWeightII = function(stones) { - const sum = stones.reduce((t,v) => t + v, 0) - const target = sum >> 1 - const m = stones.length - // 状态压缩, 去掉第一个维度 - const dp = new Array(target + 1).fill(0) - for(let i = 1; i <= m; ++i) { - // 去掉了一个维度, 遍历顺序改变,防止需要用到的推导数据被新数据覆盖, - // 少了一个if 加到 for 循环上了 - for(let j = target; j >= stones[i - 1]; --j) { - dp[j] = Math.max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]) - } - } - return sum - 2 * dp[target] -}; -``` - -以上是状态压缩的思路,还是需要多多实践。如有不对,请指教。 diff --git "a/content/posts/algorithm/trick/\344\272\214\345\210\206\346\220\234\347\264\242.md" "b/content/posts/algorithm/trick/\344\272\214\345\210\206\346\220\234\347\264\242.md" deleted file mode 100644 index 4c88046..0000000 --- "a/content/posts/algorithm/trick/\344\272\214\345\210\206\346\220\234\347\264\242.md" +++ /dev/null @@ -1,703 +0,0 @@ ---- -title: '二分搜索' -date: 2023-01-03T11:24:54+08:00 -lastmod: 2024-02-25 -tags: [] -series: [trick] -categories: [algorithm] ---- - -## 二分法适用条件 - -一开始,我简单地认为是数据需要具有**单调性**,才能应用二分;后来刷了一部分题之后,才晓得,`应用二分的本质是数据具有**二段性**`,即:一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。 - -## 重点理解!!! - -### 循环不变式 - -- `while low <= high` - - 1. 终止条件,一定是 `low + 1 == high`,也即 low > right - 2. 意味着,整个区间 `[low..high]` 里的每个元素都被遍历过了 - -- `while low < high` - - 1. 终止条件,一定是 `low == high` - 2. 意味着,整个区间 `[low..high]` 里当 low == high 时的元素可能会没有被遍历到,需要打个补丁 - 3. 补充:如 lc.153,采用逼近策略寻找答案时,两指针最后指向相同位置,如过这个位置的元素不用拿出来做什么操作,只是找到它就行,那么就用 < 也是合适的。 - -### 可行解区间 - -对于二分搜索过程中的每一次循环,它的可行解区间都应当一致,结合对于循环不变式的理解: - -- `[low...high]`,左右都闭区间,一般根据 mid 就能判断下一个搜索区间是 `low = mid + 1` 还是 `high = mid - 1` -- `[low...high)`,左闭右开区间,维持可行解区间,下一个搜索区间左边是 `low = mid + 1`,右边是 `high = mid` - -因为数组的特性,常用的就是这两种区间。当然也有左右都开的区间 `(low...high)`,对应的循环不变式为 `while low + 1 < high`,不过比较少见。 - -> 另外请务必理解可行解区间到底是个啥!不是说定义了指针为 `low = 0, high = len - 1`,就代表着可行解区间为 `[low...high]`,而是需要看实际题意。比如,你能确定 low = 0 指针和 high = len - 1 指针的解一定不在我需要的结果之中,那么对应的可行解区间就是 `(low...high)`,相应的就可以使用 `while low + 1 < high` 的循环不变式 - -> 对于寻找左右边界的问题,也是根据可行解区间,去决定 low 或 high 的每一轮 update。搜索左侧边界:mid == x 时 r = mid; 搜索右侧边界: mid == x 时,l = mid + 1,需要注意,搜索右边界结束时 l = mid + 1,所以搜索数据的真实位置是 l - 1 。 - -## 练习 - -### lc.33 搜索旋转排序数组 - -```js -/** - * @param {number[]} nums - * @param {number} target - * @return {number} - */ -var search = function (nums, target) { - let l = 0, - r = nums.length - 1 - while (l <= r) { - const mid = l + ((r - l) >> 1) - if (nums[mid] === target) { - return mid - } else if (nums[mid] < nums[l]) { - // 右半边有序的情况 - if (nums[mid] < target && target <= nums[r]) { - l = mid + 1 - } else { - r = mid - 1 - } - } else { - // 左半边有序的情况 target 在有序区间内 - if (nums[l] <= target && target < nums[mid]) { - r = mid - 1 - } else { - l = mid + 1 - } - } - } - return -1 -} - -/** - * 来看一下,如果用开闭右开区间的方式,怎么改写代码 - */ -var search = function (nums, target) { - let l = 0, - r = nums.length - while (l < r) { - const mid = l + ((r - l) >> 1) - if (nums[mid] === target) { - return mid - } else if (nums[mid] > nums[l]) { - // 左半边有序的情况 target 在有序区间内 - if (nums[l] <= target && target < nums[mid]) { - r = mid - } else { - l = mid + 1 - } - } else { - // 右半边有序的情况 - /** - * 这里需要格外注意!!!,因为右边界是开区间,所以比较的是 nums[r - 1] - */ - if (nums[mid] < target && target <= nums[r - 1]) { - l = mid + 1 - } else { - r = mid - } - } - } - return -1 -} -``` - -### lc.34 在排序数组中查找元素的第一个和最后一个位置 - -比上一题还简单~ - -```js -/** - * @param {number[]} nums - * @param {number} target - * @return {number[]} - */ -var searchRange = function (nums, target) { - // 就是寻找左右边界 - const res = [] - let l = 0, - r = nums.length - while (l < r) { - const mid = l + ((r - l) >> 1) - if (nums[mid] < target) { - l = mid + 1 - } else { - r = mid - } - } - if (nums[l] !== target) return [-1, -1] // 注意点,记得判断是否存在target - res[0] = l - l = 0 - r = nums.length - while (l < r) { - const mid = l + ((r - l) >> 1) - if (nums[mid] > target) { - r = mid - } else { - l = mid + 1 - } - } - res[1] = r - 1 // 因为我使用的是左闭右开区间,最终取右边界 - 1 即可 - return res -} -/** - * 对于求边界的二分,如果用左右都闭的形式,需要在 while 后判断一下 l、r 是否在区间内 - * 因为 l <= r 的结束条件是 l + 1 = r,有可能产生越界情况, - * 这道题恰好是题目要求了,所以做了个判断 - */ -``` - -> 在判断 nums[mid] 和 target 的大小来进行决策的时候,我的习惯是看 target 在 mid 的左边还是右边,这样就更直观一些。比如: nums[mid] > target,直观理解应该是 mid 在 target 的右边,这时候可能一下子有点懵,是去收缩左边还是收缩右边(可能我比较菜)😂 如果转换为 target 在 mid 的左边,脑补一下就能想得到要去左边寻找,所以要收缩右边界。 - -### lc.35 搜索插入位置 easy - -```js -/** - * @param {number[]} nums - * @param {number} target - * @return {number} - */ -var searchInsert = function (nums, target) { - let l = 0, - r = nums.length - while (l < r) { - const mid = l + ((r - l) >> 1) - if (nums[mid] === target) { - return mid - } else if (nums[mid] > target) { - r = mid - } else { - l = mid + 1 - } - } - return l -} -``` - -比较普通的一题,我看了官解和评论后,有人问找到 target 后为什么不直接返回(官解是没有返回的),仔细看了下题目,因为题目中规定了 nums 中不会有重复的元素,所以,按道理说是可以直接返回的,官解是考虑到了有重复元素的情况,变成了寻找左边界去了。 - ---- - - - -### lc.74 搜索二维矩阵 - -```js -/** - * @param {number[][]} matrix - * @param {number} target - * @return {boolean} - */ -var searchMatrix = function (matrix, target) { - let l = 0, - r = matrix.length - while (l < r) { - const mid = l + ((r - l) >> 1) - if (matrix[mid][0] === target) { - return true - } else if (matrix[mid][0] > target) { - r = mid - } else { - l = mid + 1 - } - } - // 因为寻找的是 第一个 >= target 的,即寻找第一列的右边界 - if (r - 1 < 0) return false - - const row = matrix[l - 1] - let i = 0, - j = row.length - while (i < j) { - const mid = i + ((j - i) >> 1) - if (row[mid] === target) { - return true - } else if (row[mid] > target) { - j = mid - } else { - i = mid + 1 - } - } - return false -} -``` - -### lc.81 搜索旋转排序数组 II - -对比 lc.33 题只是多了重复的元素,问题是,如过旋转点恰好是重复的元素,就会使得数据丧失 「二段性」: - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202402232329422.png) - -官解的做法是恢复二段性即可:`if(nums[l] == nums[mid] && nums[mid] == nums[r]) {l++, r--}` - -偷懒点就只收缩一边也行的,左边或右边都行,比如我选择了当 `nums[l] === nums[mid]` 时,收缩左边界 - -```js -/** - * @param {number[]} nums - * @param {number} target - * @return {boolean} - */ -var search = function (nums, target) { - let l = 0, - r = nums.length - while (l < r) { - const mid = l + ((r - l) >> 1) - if (nums[mid] === target) { - return true - } else if (nums[mid] > nums[l]) { - if (nums[l] <= target && target < nums[mid]) { - r = mid - } else { - l = mid + 1 - } - } else if (nums[mid] < nums[l]) { - if (nums[mid] < target && target <= nums[r - 1]) { - l = mid + 1 - } else { - r = mid - } - } else { - // nums[mid] === nums[l] 情况 收缩左边界目的是恢复 二段性 - l++ - } - } - return false -} - -/** 再来看看收缩右边界的情况 */ -var search = function (nums, target) { - let l = 0, - r = nums.length - while (l < r) { - const mid = l + ((r - l) >> 1) - if (nums[mid] === target) { - return true - } else if (nums[mid] < nums[r - 1]) { - if (nums[mid] < target && target <= nums[r - 1]) { - l = mid + 1 - } else { - r = mid - } - } else if (nums[mid] > nums[r - 1]) { - if (nums[l] <= target && target < nums[mid]) { - r = mid - } else { - l = mid + 1 - } - } else if (nums[mid] === nums[r - 1]) { - r-- - } - } - return false -} - -/** 两边同时收缩的看官解吧~ */ -``` - -> 收缩左边界,就让 mid 和左侧比;收缩右边界,就让 mid 和右侧的比 - -### lc.153 寻找旋转排序数组中的最小值 - -说实话,这道题一开始困扰了我很久 😭,因为它有点与众不同~ - -当时我是这么分析的(以下的 mid 代表 nums[mid], l 代表 nums[l]): - -1. 如果,数组 n 次旋转后,单调有序,最小值就是最左边的 -2. 如果,数组 n 次旋转后,无序,则最小值就在二分后无序的那半边了 - - 2.1 mid > l,右半边无序,选择右半边(坑) - - 2.2 mid < l,左半边无序,选择左半边 - -死活有那个几个用例过不了~ 看了题解,是跟右侧比较的 😭 why???跟左侧比有啥不一样嘛?后来仔细想了下,问题出在了 2.1 mid > l 右边无序,第一条就是最好的反例,此时最小值在最左边得选择左半边了,因此,**mid 与 l 比较是满足不了二段性**的,而与 r 比较就不一样了: - -1. mid > r,则右侧无序,选择右侧,忽略左边 -2. mid < r,则若左侧无序,选择左侧,忽略右边;若左侧有序,r 持续收缩逼近,也能得到结果,所以也可以选择左侧 - - 也可以这么想:mid < r,右侧有序,则结果一定在 `[l..mid]` 中 - -因此,**mid 与 r 比较能满足二段性**。(PS:mid 想要与 l 比较也是可以的,二分前先排除掉整个数据单调不就好了,此处就不拓展了) - -再思考一下,如果求最大值呢?😁,那就应该是不断收缩 l 去逼近,就适合用 mid 和 l 做比较了。 - ---- - -接下来还有第二个坑 😭 - -最初初始化右侧边界用的 `r === nums.length`,我寻思就跟之前一样,用左闭右开区间得了,不料却有测试用例没有过去 --- `[4,5,1,2,3]`,这是为啥呢,带进去一看,原来是 mid 恰好为最小值时,r 更新为 mid,但是遍历却并没有停止,l 仍然是小于 r 的,然后就会走到错误的答案去了。 - -可是为什么之前这样写就没啥问题呢?我又思考了下,奥,之前都是给一个目标 target,会有判断 `nums[mid] === target` 的情况,命中直接 return;而这里是无目标的,只能让双指针不断逼近从而得到最后的结果。根据前面的分析 r 的 update 策略为 `r = mid`,麻烦就在这里了,再来看一下它的两层含义: - -1. r == mid 的第一层含义,左侧无序,舍去右侧,但此时的 mid 有可能为可行解,所以 r 不能等于 mid -1 -2. r == mid 的第二层含义,左侧有序,不断逼近,此时 r == mid, **每次逼近的值都有可能为可行解**,这就与 `[l..r)` 的可行解区间不符了 - -因此可行解区间设为 `[l..r]` 是符合要求的,所以初始化为 `r = nums.length - 1`。(我隐约觉得这背后一定是有某种数学逻辑,求大佬赐教) - -```js -/** 错误解法 */ -var findMin = function (nums) { - let l = 0, - r = nums.length // bug - while (l < r) { - const mid = l + ((r - l) >> 1) - if (nums[mid] > nums[r - 1]) { - l = mid + 1 - } else { - r = mid - } - } - return nums[r] -} -/** - * [2,1] 举例 - * 1. l == 0, r == 2, mid = 1, nums[1] > nums[1] : false --> r = 1 - * 2. l == 0, r == 1,mid = 0, nums[0] > nums[0] : false --> r = 0 - * 结束循环 l == r == 0 - * - * 会发现,一旦数据巧合了后,就一直进入 nums[mid] 和 nums[mid] 自身比较的情况了 - */ - -/** 修改后 */ -var findMin = function (nums) { - let l = 0, - r = nums.length - 1 - // 由此可见,用 < 还是 <= 完全取决于题意,它只是用来控制遍历的结束时机, - // 在这里,当 l == r 时,就是解,所以 < 即可,(因为不需要对最后一个元素做什么操作) - while (l < r) { - const mid = l + ((r - l) >> 1) - // 右侧无序 - if (nums[mid] > nums[r]) { - l = mid + 1 - } else { - // 否则右侧有序 结果就肯定在 [l..mid] 中,所以 r = mid - r = mid - } - } - return nums[r] -} -/** - * [2,1] 举例 - * 1. l == 0, r == 1, mid = 0, nums[0] > nums[1] : true --> l = mid + 1 === 1 - * 结束循环 l == r == 1 - */ -``` - -那通过这道题能积累的经验是: - -1. 务必要分析好题目的二段性,正确的去选择左右分区,同时注意可行解区间在每一轮中是否保持一致 -2. 当无目标,根据双指针去探测极值的时候,使用 `[l..r]` 的闭区间较为稳妥 -3. 再回过头来结合 l.81 题,旋转数组寻找极小值,就是选择无序区间,然后再无序区间内寻找到有序区间,在有序区间内(单调增),那肯定是从右往左收缩,所以和右侧比没毛病;假如是搜索极大值,那么就是和左侧比了。 - -### lc.154 寻找旋转排序数组中的最小值 III hard - -相比 lc.153 就是多了重复元素,纸老虎罢了。 - -```js -var findMin = function (nums) { - let l = 0, - r = nums.length - 1 - while (l < r) { - const mid = l + ((r - l) >> 1) - if (nums[mid] > nums[r]) { - l = mid + 1 - } else if (nums[mid] < nums[r]) { - r = mid - } else { - // nums[mid] === nums[r] - r-- - } - } - return nums[r] -} -``` - -注意:在 lc.153 题里也有 nums[mid] === nums[r] 的判断,只是因为 153 题保证了数据不是重复的,从而直接把 r = mid 即可,当遇到有重复数据的时候,就不能这么做了,即再参考 lc.81 题,重复数据恰好在旋转点的时候,会**丧失二段性**,解法也是类似的。 - -### lc.162 寻找峰值 - -解法和 lc.153 简直如出一辙,只是从比较 mid 和 r 变为比较 mid 和 mid+1。 - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var findPeakElement = function (nums) { - let l = 0, - r = nums.length - 1 - while (l < r) { - const mid = l + ((r - l) >> 1) - /** - * mid > mid + 1,所以 mid 可能为极大值 r = mid - * 否则 mid <= mid + 1, mid < mid + 1 时, l = mid + 1, mid = mid + 1 时, l 也可以等于 mid + 1 - */ - if (nums[mid] > nums[mid + 1]) { - r = mid - } else { - l = mid + 1 - } - } - return l -} - -var findPeakElement = function (nums) { - let l = 0, - r = nums.length - 1 - while (l < r) { - const mid = l + ((r - l) >> 1) - /** - * mid < mid + 1 时, l = mid + 1 - * 否则 mid >= mid + 1 时,mid > mid + 1, r = mid; mid = mid + 1, r 也可以等于 mid - */ - if (nums[mid] < nums[mid + 1]) { - l = mid + 1 - } else { - r = mid - } - } - return l -} -``` - -与旋转数组不一样,这道题从左往右,从右往左都可以得到极大值,所以 mid 和左右比都 ok。 - - - -### lc.240 搜索二维矩阵 II - -与 lc.74 不同的是,上一行的尾不再大于下一行的首了。最直观的做法就是对每一行做二分。 - -```js -/** - * @param {number[][]} matrix - * @param {number} target - * @return {boolean} - */ -var searchMatrix = function (matrix, target) { - for (let i = 0; i < matrix.length; i++) { - const row = matrix[i] - let l = 0, - r = row.length - while (l < r) { - const mid = l + ((r - l) >> 1) - if (row[mid] === target) { - return true - } else if (row[mid] < target) { - l = mid + 1 - } else { - r = mid - } - } - } - return false -} -``` - -这样做的时间复杂度是 O(mlogn),管解给了更优的方案 [Z 字形查找](https://leetcode.cn/problems/search-a-2d-matrix-ii/solutions/1062538/sou-suo-er-wei-ju-zhen-ii-by-leetcode-so-9hcx/),看了一下就是从右上角进行搜索,根据条件更新坐标 ++y 或 --x。 - -### lc.410 分割数组的最大值 hard - -这道题的常规做法是动态规划,能用到二分我是属实没有想到。。。 - -> 这道题确实有难度,直接看官解吧,用的是 [二分+贪心](https://leetcode.cn/problems/split-array-largest-sum/solutions/345417/fen-ge-shu-zu-de-zui-da-zhi-by-leetcode-solution/) - -### lc.658 找到 k 个最接近的元素 - -```js -var findClosestElements = function (arr, k, x) { - // 先找到 x 的位置 i,再从 i 往左右两边拓展 [p..q] 直到 q - p + 1 === k - let l = 0, - r = arr.length - while (l < r) { - const mid = l + ((r - l) >> 1) - if (arr[mid] < x) { - l = mid + 1 - } else { - r = mid - } - } - /** - * 关键点,此时 l == r,且 [r..] 都 **大于等于** x; [..r-1] 都小于 x - * - * 如过没有 l = r - 1 这一步,那么 [...l] 也都是小于等于 x 的,就丧失了二段性, - * 当 leftAbs === rightAbs 时,就分不清到底是该 l-- 还是 r++ - * - * 有了 l == r - 1,则就能保证 [...l] 一定是小于 x 的,也就有了 (l..r] 的可行解区间, - * 当 leftAbs === rightAbs 时,应该 l-- - */ - l = r - 1 - while (r - l <= k) { - const leftAbs = x - arr[l] - const rightAbs = arr[r] - x - /** 同时需要考虑x不在数组索引内的情况 */ - if (l < 0) { - r++ - } else if (r > arr.length - 1) { - l-- - } else if (leftAbs <= rightAbs) { - l-- - } else { - r++ - } - } - return arr.slice(l + 1, r) -} -``` - -再一次加深可行解区间的理解 🐶 - -### lc.793 阶乘函数后 K 个零 hard - -> 数学题,[不看答案是真不会啊~](https://leetcode.cn/problems/preimage-size-of-factorial-zeroes-function/solutions/1776603/jie-cheng-han-shu-hou-k-ge-ling-by-leetc-n6vj/) - -### lc.852 山脉数组的峰顶索引 - -```js -/** - * @param {number[]} arr - * @return {number} - */ -var peakIndexInMountainArray = function (arr) { - let l = 0, - r = arr.length - 1 - while (l < r) { - const mid = l + ((r - l) >> 1) - if (arr[mid] < arr[mid + 1]) { - l = mid + 1 - } else { - r = mid - } - } - return l -} -``` - -就问你这和 lc.162 有啥区别吗。。。 - -### lc.875 爱吃香蕉的珂珂 - -```js -/** - * @param {number[]} piles - * @param {number} h - * @return {number} - */ -var minEatingSpeed = function (piles, h) { - // 寻找 k,根据题意 k 的最小值为 1, 最大值为 piles 里的最大值 - let max = 0 - for (const num of piles) { - max = num > max ? num : max - } - let l = 1, - r = max - while (l < r) { - const mid = l + ((r - l) >> 1) - if (getHourWhenSpeedIsMid(piles, mid) > h) { - l = mid + 1 - } else { - r = mid // 又去收缩右边界了~ - } - } - return l -} -function getHourWhenSpeedIsMid(piles, speed) { - let hours = 0 - for (const num of piles) { - hours += Math.ceil(num / speed) - } - return hours -} -``` - -### lc.1011 在 D 天内送达包裹的能力 - -与 lc.875 几乎一模一样 - -```js -var shipWithinDays = function (weights, days) { - // 根据题意,最低运载能力的最小值为 weights 的最大值,最大运载能力为 weights 的和 - let l = Math.max(...weights), - r = weights.reduce((a, b) => a + b) - while (l < r) { - const mid = l + ((r - l) >> 1) - if (getDays(weights, mid) > days) { - l = mid + 1 - } else { - r = mid - } - } - return l -} -function getDays(weights, mid) { - let days = 1 - let count = 0 - for (const weight of weights) { - count += weight - if (count > mid) { - days++ - count = weight - } - } - return days -} -``` - -### lc.1201 丑数 III - -这题也是没点数学知识是真的不会啊 😭 本题答案是我拷贝的,算是开了眼界了 - -1. [1..i] 中能被数字 a 整除的数字个数为 i / a -2. 容斥原理:[1..i]中能被 a 或 b 或 c 整除的数的个数 = i/a−i/b−i/c−i/ab−i/bc−i/ac+i/abc。其中 i/ab 代表能被 a 和 b 整除的数,其他同理。 - -```js -/** - * @param {number} n - * @param {number} a - * @param {number} b - * @param {number} c - * @return {number} - */ -var nthUglyNumber = function (n, a, b, c) { - const ab = lcm(a, b) - const ac = lcm(a, c) - const bc = lcm(b, c) - const abc = lcm(ab, c) - let left = Math.min(a, b, c) - let right = n * left - while (left < right) { - const mid = Math.floor((left + right) / 2) - const count = - low(mid, a) + - low(mid, b) + - low(mid, c) - - low(mid, ab) - - low(mid, ac) - - low(mid, bc) + - low(mid, abc) - if (count >= n) { - right = mid - } else { - left = mid + 1 - } - } - return right -} -function low(mid, val) { - return (mid / val) | 0 -} -function lcm(a, b) { - //最小公倍数 - return (a * b) / gcd(a, b) -} -function gcd(a, b) { - //最大公约数 - return b === 0 ? a : gcd(b, a % b) -} -``` - ---- - -总结:一旦对循环不变量和可行解区间有了深刻的理解,二分法本身是没有什么难点的,上方的 hard 题,难在了二分与其他逻辑的揉和,比如贪心和数学,所以对于二分本身的东西要贼贼贼熟练的掌握,当成工具一样,在遇到快速查询且能用二分的情况下,能信手拈来,peace! diff --git "a/content/posts/algorithm/trick/\345\212\250\346\200\201\350\247\204\345\210\222.md" "b/content/posts/algorithm/trick/\345\212\250\346\200\201\350\247\204\345\210\222.md" deleted file mode 100644 index d2f204f..0000000 --- "a/content/posts/algorithm/trick/\345\212\250\346\200\201\350\247\204\345\210\222.md" +++ /dev/null @@ -1,1116 +0,0 @@ ---- -title: '动态规划' -date: 2022-10-12T00:31:28+08:00 -lastmod: 2024-04-24 -series: [trick] -categories: [algorithm] ---- - -动态规划是一种通过把**原问题分解为相对简单的子问题**的方式求解复杂问题的方法,它**不是一种具体的算法,而是一种解决特定问题的方法**。 - -## 三要素 - -能用动态规划解决的问题,需要满足三个条件: - -- 最优子结构 - - 简单说就是:一个问题的最优解包含子问题的最优解。 - - 注意:最优子结构不是动态规划方法独有的,也可能适用贪心。 - -- 重叠子问题 - - 子问题之间会有重叠,参考斐波那契数列,求 f(5) 依赖于 f(4)fn(3), f(4) 依赖于 f(3)f(2)。如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。 - -- 无后效性 - - 已经求解的子问题,不会再受到后续决策的影响。 - -```js -// 每个节点只能向左下或者向右下走一步 - 7 - 3 8 - 8 1 0 - 2 7 4 4 -4 5 2 6 5 -``` - -从顶到底,最大和路径 `7 -> 3 -> 8 -> 7 -> 5`。 - -依次来看: - -- 每一层都会有最优解,且就是那一层所有子问题的最优解 --- 满足最优子结构 -- 每一层的最优解不会因为下一层的选择而发生改变 --- 满足无后效性; -- 这道题倒是没有类似斐波那契类似的交叉的子问题,没有重叠子问题 - - (注意不是依赖于上一层的最优解,那就变成贪心了。 比如 第二层最优为 7-8,但是第三层是 7-3-8。) - -## 基本思路 - -> 对于一个能用动态规划解决的问题,一般采用如下思路解决: - -> 1. 将原问题划分为若干阶段,**每个阶段对应若干个子问题,提取这些子问题的结果即「状态」**; -> 2. **寻找每一个状态的可能「决策」**,或者说是各状态间的相互转移方式(用数学的语言描述就是「状态转移方程」)。 -> 3. **按顺序求解每一个阶段的问题**。 -> -> 如果用图论的思想理解,我们建立一个 有向无环图,每个状态对应图上一个节点,**决策对应节点间的连边**。这样问题就转变为了一个在 DAG 上寻找最长(短)路的问题。 - - - ---- - -## 练一练 - -### 序列 dp - -#### lc.1143 最长公共子序列 - -思考: - -原问题是从 a、b 两个字符串寻找最长公共子序列,那么子问题就是 a、b 字符串的子串之间的最长公共子序列,比如 a[0] 和 b[0],往前推的子问题可能是 a[0-1] 和 b[0]、a[0] 和 b[0-1]、a[0-1]和 b[0-1]等等,每一个子问题的结果都是一个「状态」 - -稍微有点经验后,一般应对两两比较类的动态规划,可以使用双指针进行子问题推进,也就是使用二维 dp 数组去解决。 - -精练一下: - -- 「状态」:`定义 dp[i][j] 表示**A 的前 i 个元素,B 的前 j 个元素**的最长公共子序列的长度`,这样每一个 `dp[i][j]` 都表示了一个状态。 - > 注意:通常将`dp[i]`定义为表示`前i个`元素的状态,更容易处理边界情况,例如 `dp[0]`通常被定义为初始状态,表示空集或空字符串对应的状态。 -- 「决策」:每一个 dp[i][j]都有 3 个决策: - 1. a[i] == b[j],dp[i][j] = dp[i-1][j-1] + 1 - 2. a[i] != b[j],dp[i][j] = Math.max(dp[i-1][j] + dp[i][j-1]) ,两个决策中选择较大的那个 - > 注意:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 - - -```js -/** - * @param {string} text1 - * @param {string} text2 - * @return {number} - */ -var longestCommonSubsequence = function (text1, text2) { - const m = text1.length - const n = text2.length - const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0)) - // base case 分别 a 串为 0 或 b 串为 0 的情况,在初始化时已经处理。一般也可以分别 for 循环搞定 - for (let i = 1; i <= m; ++i) { - for (let j = 1; j <= n; ++j) { - if (text1[i - 1] === text2[j - 1]) { - dp[i][j] = dp[i - 1][j - 1] + 1 - } else { - dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) - } - } - } - return dp[m][n] -} -``` - -lc.1035 不相交的线,与本题基本一模一样。 - - - - - -#### lc.300 最长递增子序列 - -做过上题,再做这题,很容易写出如下代码: - -```js -var lengthOfLIS = function (nums) { - /** 定义 dp[i] 表示前 i 个元素的最长递增子序列的长度*/ - const dp = Array.from(Array(nums.length + 1), () => 0) - // dp[0] = 0 - dp[1] = 1 - for (let i = 2; i <= nums.length; ++i) { - if (nums[i - 1] > nums[i - 2]) { - dp[i] = dp[i - 1] + 1 - } else { - dp[i] = dp[i - 1] - } - } - return dp[nums.length] -} -``` - -很遗憾的是,这么做是错的,问题在 `nums[i - 1] > nums[i - 2]` 只是和前一个元素比较,而 nums[0..i-1] 之间最长的递增子序列不一定是 nums[0..i-1],有可能是 nums[0..i-2]等等,所以如此定义 dp 数组的含义,**状态转移方程是有问题的**,应当与之前的每一个元素进行比较,然后更新最大的。 - -那么,请看另一种定义:**dp[i] 表示以 nums[i] 结尾的最长递增子序列,且 nums[i-1] 必须被选上** - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var lengthOfLIS = function (nums) { - /** - * 定义: dp[i] 表示**以 nums[i] 结尾**的最长递增子序列的长度 - * 注意: nums[i] 必须选中 - */ - const n = nums.length - const dp = Array(n).fill(1) // 长度至少为 1,即其自身 - for (let i = 1; i < n; ++i) { - // 应当与之前的每一个元素进行比较 - for (let j = 0; j < i; ++j) { - if (nums[i] > nums[j]) { - // nums[i] 必选 - dp[i] = Math.max(dp[i], dp[j] + 1) - } - } - } - return Math.max(...dp) -} -``` - -这道题进阶,将时间复杂度降到 O(nlogn),这基本上就是在暗示使用二分法了,还是有点挑战的。 - -```js -// TODO 二分+贪心 -``` - -lc.354 俄罗斯套娃信封问题就是基于此题,只不过需要稍微转变一下思路:**要保证对于每一种 w 值,我们最多只能选择 1 个信封**,所以对 w 升序排序,当 w 相等时,对 h 降序排序。且这道题只能使用二分+贪心的方式求最长递增子序列,否则会超时。 - - - -#### lc.674 最长连续递增子序列 - -重点在于连续递增~,此时,就不用和 i 之前的每个元素比了,只用和前一个比,但是定义依然是 dp[i] 表示以 nums[i] 为选中元素的最长递增子序列的长度 😂 - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var findLengthOfLCIS = function (nums) { - const n = nums.length - /** 定义 dp[i] 表示以 nums[i] 结尾的最长连续递增子序列的长度 */ - const dp = Array(n).fill(1) - for (let i = 1; i < n; ++i) { - if (nums[i] > nums[i - 1]) { - dp[i] = dp[i - 1] + 1 - } - } - return Math.max(...dp) -} -``` - -#### lc.392 判断子序列 easy - -判断字符串 a 是否是 b 的子序列。这很明显用双指针就能搞得定。 - -```js -var isSubsequence = function (s, t) { - let i = 0, - j = 0 - while (i < s.length && j < t.length) { - if (s[i] === t[j]) { - i++ - } - j++ - } - return i === s.length -} -``` - -此处为了使用动态规划而动态规划一下[旺柴],a 是 b 的子序列,则 a 和 b 的最长公共子序列就是 a 的长度~ - -```js -/** - * @param {string} s - * @param {string} t - * @return {boolean} - */ -var isSubsequence = function (s, t) { - const m = s.length - const n = t.length - const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0)) - for (let i = 1; i <= m; ++i) { - for (let j = 1; j <= n; ++j) { - if (s[i - 1] === t[j - 1]) { - dp[i][j] = dp[i - 1][j - 1] + 1 - } else { - dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) - } - } - } - return s.length === dp[m][n] -} -``` - -#### lc.115 不同的子序列 hard - -字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。 - -需要动一下脑筋,举例思考,abb 和 ab,定义 dp[i][j] 表示 a 的前 i 个字符的子序列包含 b 的前 j 个字符的个数 - -- 当 a[i] == b[i] 时 - - 若使用 a[i],则 dp[i][j] = dp[i-1][j-1] - - 若不使用 a[i],则 dp[i][j] = dp[i-1][j] -- 当 a[i] != b[i] 时,dp[i][j] = dp[i-1][j] - -```js -/** - * @param {string} s - * @param {string} t - * @return {number} - */ -var numDistinct = function (s, t) { - const m = s.length - const n = t.length - const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0)) - // 这道题需要格外注意 base case - for (let i = 0; i <= m; ++i) { - dp[i][0] = 1 // 当 t 为 0 的时候,总能包含一次~ - } - for (let i = 1; i <= m; ++i) { - for (let j = 1; j <= n; ++j) { - if (s[i - 1] == t[j - 1]) { - dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] // 相同就用+不用 - } else { - dp[i][j] = dp[i - 1][j] // 不同就不用 - } - } - } - return dp[m][n] -} -``` - -#### lc.53 最大子数组和 - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var maxSubArray = function (nums) { - const n = nums.length - // 定义 dp[i] 表示以 nums[i] 结尾的的最大连续子数组的和 - const dp = Array(n) - dp[0] = nums[0] - for (let i = 1; i < n; ++i) { - // 决策是 +nums[i] 与 nums[i] 自身比较,如果还没 nums[i] 大,那么就 nums[i] 自成一派即可 - dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]) - } - return Math.max(...dp) -} -``` - -#### lc.718 最长重复子数组 - -```js -/** - * @param {number[]} nums1 - * @param {number[]} nums2 - * @return {number} - */ -var findLength = function (nums1, nums2) { - // 定义 dp[i][j] 表示nums1前 i 和 nums2 前 j 个最长的子数组的长度 - // 且第 i 个和第 j 个必须选上 - let res = 0 - const m = nums1.length - const n = nums2.length - const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0)) // 覆盖了dp[0][0] dp[i][0] dp[0][j] 都为 0 - for (let i = 1; i <= m; ++i) { - for (let j = 1; j <= n; ++j) { - // i-1,j-1 位置必须选上,否则就为 0,初始化时已经覆盖 - if (nums1[i - 1] === nums2[j - 1]) { - dp[i][j] = dp[i - 1][j - 1] + 1 - res = Math.max(res, dp[i][j]) - } - } - } - return res -} -``` - -#### lc.72 编辑距离 hard - -这道题以前是 hard~ 🐶 简单分析一下 word1 -> word2,可以增删改,求最少操作数。(就问你烦不烦吧 🌧) - -仔细分析下来,你会发现,此题和 lc.1143 有异曲同工之处,差别不是很大。 - -两个字符串,少不了两个指针,定义 dp[i][j] 表示 a 前 i 个转为 b 前 j 个字符串使用的最少操作数。 - -至于决策,相比 lc.1143 多了一个罢了,每个状态都有 4 种决策: - -1. a[i] === b[j],dp[i][j] = dp[i-1][j-1],不需要操作 -2. a[i] !== b[j],dp[i][j] = Math.min(a 插,a 删,a 改) + 1 - > 再次强调:这里的 a[i]、b[j] 的 i,j 只是指针游走,表示第 i、j 个,(实际位置应当是 a[i-1]、b[j-1]),方便状态推导,与 dp 的前 i,j 个相呼应。 - -问题来了,当 a[i] != b[j] 时,a 插,a 删,a 改,怎么用 dp 表示呢?说实话需要点思考。 - - - -- a 插,`dp[i][j-1] + 1`,在 a[i] 后插入一个和 b[j] 相同的字符,a[i+1]把 b[j]抵消了,此时就需要看 a[i] 转为 b[j-1]的最小步骤了,此时要让 a == b 则就需要看 a[1:i] 转换为 b[1:j-1]的最小步骤了,即 dp[i][j-1] - -- a 删,`dp[i-1][j] + 1`,删除 a[i],即看 a[1:i-1] 转换为 b[1:j]的最小步骤。 -- a 改,`dp[i-1][j-1] + 1`,换一个字符只需要看 a[1:i-1] 转换为 b[1:j-1] - -```js -/** - * @param {string} word1 - * @param {string} word2 - * @return {number} - */ -var minDistance = function (word1, word2) { - const m = word1.length - const n = word2.length - // 定义 dp[i][j] 表示 A 的前 i 个转为 B 的前 j 个元素所用的最小步骤 - const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0)) - // base case - for (let i = 0; i <= m; ++i) { - dp[i][0] = i - } - for (let j = 0; j <= n; ++j) { - dp[0][j] = j - } - for (let i = 1; i <= m; ++i) { - for (let j = 1; j <= n; ++j) { - if (word1[i - 1] == word2[j - 1]) { - dp[i][j] = dp[i - 1][j - 1] - } else { - dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1 - } - } - } - return dp[m][n] -} -``` - -#### lc.583 两个字符串的删除操作 - -搞懂 lc.72,那么这道题就是小 case 的啦~ - -```js -/** - * @param {string} word1 - * @param {string} word2 - * @return {number} - */ -var minDistance = function (word1, word2) { - // 定义 dp[i][j] 表示 word1 前 i 个 -> word2 前 j 个 的最小步数 - const m = word1.length - const n = word2.length - const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0)) - for (let i = 1; i <= m; ++i) { - dp[i][0] = i - } - for (let j = 1; j <= n; ++j) { - dp[0][j] = j - } - - for (let i = 1; i <= m; ++i) { - for (let j = 1; j <= n; ++j) { - if (word1[i - 1] === word2[j - 1]) { - dp[i][j] = dp[i - 1][j - 1] - } else { - dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1 // 做删 word1 或者删 word2 的选择 - } - } - } - return dp[m][n] -} -``` - - - -#### 经验 - -> 做到这里,可以发现,目前对于序列/字符串 dp 的定义一般有两种: -> -> 1. dp[i] 表示`前 i 个`... -> 2. dp[i] 表示`以 el[i] 结尾`... - -最后,再来两道略微不一样的题。 - -#### lc.516 最长回文子序列 - -定义 dp[i][j] 表示 s[i:j] 之间的最大回文串的长度(闭区间)。一个常识:怎么判断区间 s[i:j] 是否为一个回文串,从两头往中间,看是否相等。 - -所以: - -- a[i] == a[j] 时,dp[i][j] = dp[i+1][j-1] + 2 -- a[i] != a[j] 时,dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) - -> 注意:当出现 i 依赖于 i + 1 的情况,就需要分析一下遍历的方式。首先根据定义,i <= j,base case 就是对角线;根据状态转移方程每个节点都是从左边和下边推导上来的,如下图 1;由此可以脑补出二位矩阵图,如下图 2。 -> ->
-> 遇到上图 2 的情况,可以斜着遍历(虚线),当然也可以从下往上&从左往右这样子遍历。 - -```js -/** - * @param {string} s - * @return {number} - */ -var longestPalindromeSubseq = function (s) { - const len = s.length - const dp = Array.from(Array(len), () => Array(len).fill(0)) - // base case 自身一定为回文串 - for (let i = 0; i < len; ++i) { - dp[i][i] = 1 - } - // 根据状态转移公式,确定遍历方法,可以倒着遍历(简单就不演示了), - // 也可以斜着遍历,此处采用斜着遍历 - // 从左往右数第2根线开始遍历,因为第一根线已经都确定了 - for (let l = 2; l <= len; ++l) { - for (let i = 0; i <= len - l; ++i) { - // 关键获取 j 坐标,右上半区的 i,j 坐标与 l 的关系: - // l 就是闭区间 [i,j] 的长度,因此可以得出 j = l + i - 1 - const j = l + i - 1 - if (s[i] === s[j]) { - dp[i][j] = dp[i + 1][j - 1] + 2 // 相同的情况下, 收缩 长度+2 - } else { - dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]) // 比较选择[i+1..j] 和[i..j-1]中较大的那个 - } - } - } - return dp[0][len - 1] -} -``` - -#### lc.1312 让字符串成为回文串的最少插入次数 hard - -一个重要的思路:让字符串成为回文串的最少插入次数,**等价于原字符长度减去最长回文子串的长度**。如果想不到这一点,那确实挺难的。 - -```js -/** - * @param {string} s - * @return {number} - */ -var minInsertions = function (s) { - const n = s.length - const dp = Array.from(new Array(n), _ => new Array(n).fill(0)) - // base case - for (let i = 0; i < dp.length; ++i) { - dp[i][i] = 1 - } - for (let l = 2; l <= n; ++l) { - for (let i = 0; i <= n - l; ++i) { - let j = l + i - 1 - if (s[i] === s[j]) { - dp[i][j] = dp[i + 1][j - 1] + 2 - } else { - dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]) - } - } - } - return n - dp[0][n - 1] -} -``` - -#### 小结 - -总的来说,序列 dp 类的问题,不是很难,只要多思考,状态转移方程还是挺容易找得到的,奥利给 💪🏻。 - ---- - -### 背包 dp - -背包类问题,可以学习前人的总结---[背包九讲-崔添翼](https://github.com/tianyicui/pack),十分详尽,初级阶段,先掌握好基础的「01 背包」「完全背包」和「多重背包」问题吧 💪🏻。 -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202404192204453.png) - -#### 01 背包 - -01 背包是背包问题的基础中的基础,完全背包和多重背包都可以转换为 01 背包问题。 - -- 问题:**N 个物品, V 容量的背包。放入第 i 个物品至背包,占用 Wi 的容量,获得 Ci 的价值**。 -- 特点:**每个物品都只有一件,选择放或者不放**。 -- 状态定义及转移:**`dp[i][v]` 表示前 i 个物品放入 v 容量背包能获得的最大价值**。 - - 不放,`dp[i][v] = dp[i-1][v]`,容量不变,价值与放入前一个一致 - - 放入,`dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v])`,放入则需要与不放入进行比较,因为能放入的容量为 v-wi,需要找到能放入前的最大价值再加上 ci 即放入的最大价值。 - -##### 空间复杂度优化 - -⭐️:**空间压缩的核心思路就是,将二维数组「投影」到一维数组** - -单就 01 背包的二维状态转移方程来看 `dp[i][v] = Math.max(dp[i-1][v - wi] + ci, dp[i-1][v])`,i 只与 i-1 有关,那么就可以这么干:`dp[v] = Math.max(dp[v], dp[v-wi] + ci)`。需要理解的是: - -在 dp[v] 还未被赋值时: - -1. 右侧的 `dp[v]` 是外层 for 循环上一次迭代出的结果。在这里对应着 dp[i-1][v]。 -2. 右侧的 `dp[v-wi] ` 如果内层 for 循环是顺序遍历,则是表示内层 for 循环的结果 dp[i][v-wi],这显然是与二维方程所违背的,因此,内层遍历背包的时候需要倒序才能拿到上一轮外循环的结果,否贼就会被覆盖。举例:假设 dp[10] 依赖于 dp[6],如果是顺序,则 dp[6] 会先于 dp[10]更新,导致 dp[10] 无法得到上一轮迭代的 dp[6]。 - ---- - -进一步了解空间压缩,上面做过的最长回文子序列的状态转移方程为 `dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) || dp[i+1][j-1] + 2`,dp[i][j] 依赖于三个状态,那么投影干掉 i 维度后: - -- a[i] == a[j]: dp[j] = dp[j-1] + 2 -- a[i] != a[j]: dp[j] = Math.max(dp[j], dp[j-1]) - -此时,右侧的 `dp[j]` 表示的是上一轮外层 for 循环的结果,即 `dp[i+1][j]`,由于是顺序遍历,`dp[j-1]` 表示的是内层 for 循环的结果,即 `dp[i][j-1]`,那么就剩下 `dp[i + 1][j - 1]` 还没有结果,那是因为二维变一维的时候,它的值被 `dp[i][j-1]` 覆盖掉啦(想象从上往下投影),解决办法也很简单,类似普通的交换方法中的技巧,先暂存起来不就好了 ☀️! - -```js -/** - * !!!PS:斜着遍历不便于理解空间压缩,使用倒着遍历比较好理解~~~ - */ -var longestPalindromeSubseq = function (s) { - var n = s.length - // base case:一维 dp 数组全部初始化为 1 - var dp = new Array(n).fill(1) - // 从倒着遍历二维 i,来处理较为轻松 - for (var i = n - 2; i >= 0; i--) { - var pre = 0 - for (var j = i + 1; j < n; j++) { - var temp = dp[j] - // 状态转移方程 - if (s.charAt(i) == s.charAt(j)) { - dp[j] = pre + 2 - } else { - dp[j] = Math.max(dp[j], dp[j - 1]) - } - pre = temp - } - } - return dp[n - 1] -} -``` - - - -##### lc.416 分割等和子集 - -```js -/** - * @param {number[]} nums - * @return {boolean} - */ -var canPartition = function (nums) { - const sum = nums.reduce((t, v) => t + v) - if (sum % 2 !== 0) return false - const target = sum / 2 // 背包 - const n = nums.length - // 定义前 i 个元素可以装满容量为 j 的背包 - const dp = Array.from(Array(n + 1), () => Array(target + 1).fill(false)) - for (let i = 0; i <= n; ++i) dp[i][0] = true - for (let i = 1; i <= n; ++i) { - for (let j = 1; j <= target; ++j) { - if (nums[i - 1] > j) { - dp[i][j] = dp[i - 1][j] - } else { - dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]] - } - } - } - return dp[n][target] -} -``` - -此题的巧妙之处在于没有真的去累加选择元素的值,而是直接根据 Boolean 状态的传递来判断的。 - - - -##### lc.474 一和零 - -普通 01 背包只有一个容量限制,这题有 2 个,分别是 0 和 1 的数量,因此可以预想到时间复杂度要高一些,但总体思想还是一致的。 - -```js -/** - * @param {string[]} strs - * @param {number} m - * @param {number} n - * @return {number} - */ -var findMaxForm = function (strs, m, n) { - // 定义 dp[i][j][k] 表示 前 i 个元素,含 j 个 0 和 k 个 1 的最大子集数 - const len = strs.length - const dp = Array.from(Array(len + 1), () => - Array(m + 1) - .fill(0) - .map(_ => Array(n + 1).fill(0)) - ) - for (let i = 1; i <= len; ++i) { - const el = strs[i - 1] - const oneCount = el.split('').filter(_ => _ == '1').length - const zeroCount = el.length - oneCount - for (let j = 0; j <= m; j++) { - for (let k = 0; k <= n; ++k) { - if (j < zeroCount || k < oneCount) { - dp[i][j][k] = dp[i - 1][j][k] - } else { - dp[i][j][k] = Math.max( - dp[i - 1][j][k], - dp[i - 1][j - zeroCount][k - oneCount] + 1 - ) - } - } - } - } - return dp[len][m][n] -} -``` - -##### lc.494 目标和 - -这道题比较绝~,第一反应应该是回溯。 - -```js -var findTargetSumWays = function (nums, target) { - let res = 0 - const backtrack = (index, sum) => { - if (index === nums.length) { - if (sum === target) { - res++ - } - } else { - backtrack(index + 1, sum + nums[index]) - backtrack(index + 1, sum - nums[index]) - } - } - backtrack(0, 0) - return res -} -``` - - - -再来看一下动态规划,讲道理,第一时间我是真的想不到。。。需要先做一个数学推导: - -集合总体可以分为 A、B 两个子集,那么就有: - -- `A - B = target` -- `A + B = sum` - -因此 `2A = sum + target,A = (sum + target) >> 1`;如此一来就巧妙地转变为了 lc.416 分割等和子集的问题了~~~ - -定义 dp[i][j] 表示前 i 个元素构造运算结果为 j 的不同表达式数目 - -```js -var findTargetSumWays = function (nums, target) { - const n = nums.length - const sum = nums.reduce((t, v) => t + v) - if (Math.abs(target) > sum || (sum + target) % 2 !== 0) return 0 // 注意需要对 target 去绝对值 - const goal = (sum + target) / 2 - // 定义 dp[i][j] 表示前 i 个元素构造运算结果为 j 的不同表达式数目 - const dp = Array.from(Array(n + 1), () => Array(goal + 1).fill(0)) - // 特殊 base case - dp[0][0] = 1 // 不考虑任何数,凑出计算结果为 0 的方案数为 1 种。 - for (let i = 1; i <= n; ++i) { - for (let j = 0; j <= goal; ++j) { - if (nums[i - 1] > j) { - dp[i][j] = dp[i - 1][j] - } else { - dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]] // 能选择选 - } - } - } - return dp[n][goal] -} -``` - -##### 1049. 最后一块石头的重量 II - -该题可以抽象为将 n 块石头分为两堆,而后求两堆石头重量总和的最小差值。 -因此便可看为 01 背包问题,背包最大容量为这 n 块石头总重量的一半 -那么理想情况下,如果可以刚好装满背包,两堆石头重量总和的最小差值便可为零。 - -```js -/** - * @param {number[]} stones - * @return {number} - */ -var lastStoneWeightII = function (stones) { - const n = stones.length - const sum = stones.reduce((t, v) => t + v) - const target = sum >> 1 - // 定义 dp[i][j] 表示前 i 个元素,容量为j的背包所能存放的实际最大值 - const dp = Array.from(Array(n + 1), () => Array(target + 1).fill(0)) - for (let i = 1; i <= n; ++i) { - for (let j = 0; j <= target; ++j) { - if (stones[i - 1] > j) { - dp[i][j] = dp[i - 1][j] - } else { - dp[i][j] = Math.max(dp[i - 1][j - stones[i - 1]] + stones[i - 1], dp[i - 1][j]) // 放或不放最大值 - } - } - } - return sum - dp[n][target] * 2 -} -``` - -可以看到有些问题是 01 背包的变种问题,掌握住核心思想,拓展思维(其实得多练,不然 👻 能想得到啊 😭) - -#### 完全背包 - -- 问题:**N 种物品, V 容量的背包。放入第 i 种物品至背包,占用 Wi 的容量,获得 Ci 的价值**。 -- 特点:**每种物品有无数件,选择不再是选货不选,而且选 0、1、2...直到 V/Wi 次**。 -- 状态定义及转移:**`dp[i][v]` 表示前 i 种物品恰放入一个容量为 v 的背包的最大价值**,类似 01 背包可以给出基础方程:`dp[i][v] = Math.max(dp[i-1][v], dp[i-1]v-k*wi] + k*ci ),0 <= kwi <= v`。然而,这样求解每个状态的时间复杂度就不是常数级了,来到了`O(v/wi)`,总的时间复杂度是 `O(NVΣV/Ci)`,是比较高的,所以一般都需要优化。 - - -##### 空间复杂度优化 - -**⭐️ 巧的是:完全背包的 O(NV) 的状态转移方程,与 01 背包的一维状态转移方程几乎一样,唯一的不同是内层不需要倒着遍历了~~~** 01 背包倒着遍历,是为了保证拿到上一轮的 dp[v-wi],完全背包则无需这个保证。 - -> 不过我个人并不推荐上来直接就写一维方程,会比较难以理解,写熟练之后可以这么干 - -##### lc.139 单词拆分 - -字典中的单词可以重复使用,每个单词也只有选择用和不用,完全背包问题,分析出这个,后面就简单了: - -```js -/** - * @param {string} s - * @param {string[]} wordDict - * @return {boolean} - */ -var wordBreak = function (s, wordDict) { - // 完全背包排列问题,定义 dp[j] 表示s前 i 个字符,可以由单词组成 - const n = s.length - const dp = Array(n + 1).fill(false) - dp[0] = true - for (let i = 0; i <= n; i++) { - for (let j = 0; j < i; j++) { - // ! 重点 j表示分割 状态转移方程看s[0..j-1]和s[j..i-1] 是否都合法 - if (dp[j] && wordDict.includes(s.substring(j, i))) { - dp[i] = true - break - } - } - } - return dp[n] -} -``` - -##### lc.279 完全平方数 - -动态规划 定义 dp[i] 表示数字 i 的完全平方数最少。 - -```js -/** - * @param {number} n - * @return {number} - */ -var numSquares = function (n) { - const dp = Array(n + 1).fill(0) - for (let i = 0; i <= n; ++i) { - dp[i] = i // 最差每次+1 - for (let j = 1; j * j <= i; ++j) { - dp[i] = Math.min(dp[i], dp[i - j * j] + 1) - } - } - return dp[n] -} -``` - -##### lc.322 零钱兑换 - -求组合成 target 使用物品最少数 - -```js -/** - * @param {number[]} coins - * @param {number} amount - * @return {number} - */ -var coinChange = function (coins, amount) { - // 经典完全背包题目,定义 dp[i] 表示不同面额组合成 i 的最少硬币个数 - const dp = Array(amount + 1).fill(amount + 1) // 肯定不会超过 amount + 1 - dp[0] = 0 - for (let i = 0; i <= amount; ++i) { - for (const coin of coins) { - if (coin > i) { - continue - } else { - dp[i] = Math.min(dp[i], dp[i - coin] + 1) - } - } - } - return dp[amount] === amount + 1 ? -1 : dp[amount] -} -``` - - - -##### lc.518 零钱兑换 II - -相比上一题,本题求的是填满背包物品组合的问题的,一定是先遍历物品再遍历背包。 - -```js -/** - * @param {number} amount - * @param {number[]} coins - * @return {number} - */ -var change = function (amount, coins) { - const n = coins.length - const dp = Array(amount + 1).fill(0) - dp[0] = 1 - for (let i = 0; i < n; ++i) { - for (let j = coins[i]; j <= amount; ++j) { - dp[j] = dp[j] + dp[j - coins[i]] - } - } - return dp[amount] -} -``` - -##### lc.377 组合总和 Ⅳ - -不要被题目名欺骗了,看题目,是一道赤裸裸的排列问题。。。 - -```js -/** - * @param {number[]} nums - * @param {number} target - * @return {number} - */ -var combinationSum4 = function (nums, target) { - const dp = Array(target + 1).fill(0) - dp[0] = 1 - for (let j = 0; j <= target; ++j) { - for (let i = 0; i < nums.length; ++i) { - if (nums[i] <= j) { - dp[j] = dp[j] + dp[j - nums[i]] - } - } - } - return dp[target] -} -``` - -#### 多重背包 - -多重背包和完全背包相比,就是限定每种物品的数量为 Mi。 - -TODO - - - -> 如开头所讲,动态规划之时一种解决方法,更是一种思路,题型和题目杂而多,本文只是对动态规划有一个初步的概念,想要熟练掌握,还是得多刷题多思考啊,peace! - -## 推荐阅读 - -- [Pascal's Triangle](https://www.mathsisfun.com/pascals-triangle.html) diff --git "a/content/posts/algorithm/trick/\345\216\211\345\256\263\347\232\204KMP.md" "b/content/posts/algorithm/trick/\345\216\211\345\256\263\347\232\204KMP.md" deleted file mode 100644 index 648e36a..0000000 --- "a/content/posts/algorithm/trick/\345\216\211\345\256\263\347\232\204KMP.md" +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: '厉害的KMP' -date: 2022-10-28T15:33:46+08:00 -tags: [] -series: [trick] -categories: [algorithm] -draft: true ---- - -## 前言 - -很早之前刚刚开始刷题的时候,对 `lc28` 不屑一顾,这不就是个 `indexOf` 吗 🐻?看了答案后我懵逼了,因为我不会,甚至看不懂 😂。 - -so,那就搞懂它。 - -## KMP 目的 - -字符串 txt,模式串 pat。 - -KMP 算法的核心思想是:利用模式串中的重复子串来进行匹配。在匹配过程中,如果遇到重复子串,就跳过它,只匹配后面的部分。这样可以避免在匹配过程中多次重复处理同一个子串,从而提高匹配效率。 - -现象就是:在 txt 中寻找 pat,永不回退在 txt 上移动的指针 i,不走回头路(不会重复扫描 txt),而是借是前缀函数 `π(i)` (网上很多也称为 `next 数组`)中储存的信息把 pat 移到正确的位置继续匹配,时间复杂度只需 `O(N)`,用空间换时间。 - -## 两种实现 - -- 一种是 AC 自动机的思路来求解 `π函数`,这部分实现后续了解 -- 另一种就是利用前缀函数`π(i)`的性质来实现的,本文介绍这种 - -## 基本概念 - -[关于真前缀,真后缀的基本定义见这里](https://oi-wiki.org/string/kmp/#%E5%89%8D%E7%BC%80%E5%87%BD%E6%95%B0) - -- `前缀函数`:**对于长度为 `n` 的字符串 `s`,其前缀函数 `π(i) (0≤i **重点:就是求前缀函数 π(i)** -> 按照定义去求前缀函数时间复杂度将达到 `O(n^3)`,所以需要利用到性质。 -> -> 1. π(0) = 0 -> 2. π(i) 最大为 π(i-1) + 1,即每次最大只能 +1 - -解释:假设已知 `π(0...i-1)`,求 `π(i)`,定义指针 `j = π(i-1)`,`j` 指针含义———— **在 j 对应的字符前已经有 j 个元素 与 i 对应的字符的前 j 个元素相等** - -- 如果 `s[i] === s[j]` ,那么 `π(i) = j + 1`; -- 如果 `s[i] !== s[j]`,那么挪动指针 `j` 到下一个最长的可能匹配的位置,即 j 指针的前一位 π 函数的位置 `j = π(j - 1)`(重难点!!!) - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202304141407906.svg) - -### 试一试:[28. 找出字符串中第一个匹配项的下标](https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/) - -```js -var strStr = function (haystack, needle) { - const m = haystack.length - const n = needle.length - const pi = new Array(n).fill(0) - // 对pat求前缀函数 - for (let j = 0, i = 1; i < n; ++i) { - // π(0) == 0; 求 π(1...n-1) - // 不等, 找已匹配的字符中最长的前后缀长度,即 j 指针的前一位的长度,这部分带点动态规划的思想 - while (j > 0 && needle[j] !== needle[i]) j = pi[j - 1] - if (needle[j] === needle[i]) j++ // 相等, j 指针和 i 指针同时往后移动一位 - pi[i] = j // 更新 pi[i] 的最长相等 「真前/后缀」长度 - } - console.log(pi) - // i txt指针, j pat指针 - for (let i = 0, j = 0; i < m; ++i) { - while (j > 0 && haystack[i] !== needle[j]) j = pi[j - 1] - if (haystack[i] === needle[j]) j++ - if (j === n) { - return i - n + 1 // 遍历完了patten, 起始索引位置为i - n + 1 - } - } - return -1 -} -``` - -假想把 pat 和 txt 通过特殊符号#连接起来。然后求前缀函数,当前缀值等于 pat 的长度时,就是匹配上了。闭区间 [x, i],长度为 n, 求索引 x,不用我多说了吧?直接返回索引 x 为 `i - n + 1` - -## 参考 - -- [前缀函数与 KMP 算法](https://oi-wiki.org/string/kmp/) -- [KMP 算法 next 数组推导详解](https://blog.csdn.net/weixin_50168558/article/details/121318627) -- [最浅显易懂的 KMP 算法讲解(视频)](https://www.bilibili.com/video/BV1AY4y157yL/?share_source=weixin_web&vd_source=55aa8441b3f5438f746a87f0ac946d08&wxfid=o7omF0bo1aj3AH8fOHTxGWdFxrdM&share_times=1) diff --git "a/content/posts/algorithm/trick/\345\217\214\346\214\207\351\222\210.md" "b/content/posts/algorithm/trick/\345\217\214\346\214\207\351\222\210.md" deleted file mode 100644 index 43d2306..0000000 --- "a/content/posts/algorithm/trick/\345\217\214\346\214\207\351\222\210.md" +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: '数组-双指针应用' -date: 2024-02-16 -tags: [] -series: [trick] -categories: [algorithm] ---- - -## 数组双指针 - -双指针,一般两种,**快慢指针**和**左右指针**,根据不同场景使用不同方法。 - -以下题基本都是简单题,知道使用双指针就没啥难度了~ - -### lc.167 两数之和 II - 有序数组 - -```js -/** - * @param {number[]} numbers - * @param {number} target - * @return {number[]} - */ -var twoSum = function (numbers, target) { - let left = 0, - right = numbers.length - 1 - while (left < right) { - if (numbers[left] + numbers[right] < target) { - left++ - } else if (numbers[left] + numbers[right] > target) { - right-- - } else if (numbers[left] + numbers[right] === target) { - return [left + 1, right + 1] - } - } -} -``` - -### lc.26 删除有序数组中的重复项 - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var removeDuplicates = function (nums) { - let left = 0, - right = 1 - while (right < nums.length) { - if (nums[left] === nums[right]) { - right++ - } else { - nums[++left] = nums[right++] - } - } - return left + 1 -} -``` - -### lc.27 移除元素 - -```js -/** - * @param {number[]} nums - * @param {number} val - * @return {number} - */ -var removeElement = function (nums, val) { - let left = 0, - right = nums.length - 1 - while (left <= right) { - if (nums[left] === val) { - nums[left] = nums[right] - right-- - } else { - left++ - } - } - return left -} -``` - -### lc.283 移动零 - -```js -/** - * @param {number[]} nums - * @return {void} Do not return anything, modify nums in-place instead. - */ -var moveZeroes = function (nums) { - let left = 0, - right = 0 - while (right < nums.length) { - if (nums[right] !== 0) { - ;[nums[left++], nums[right++]] = [nums[right], nums[left]] - } else { - right++ - } - } -} -``` - -### lc.344 反转字符串 - -```js -/** - * @param {character[]} s - * @return {void} Do not return anything, modify s in-place instead. - */ -var reverseString = function (s) { - let left = 0, - right = s.length - 1 - while (left <= right) { - ;[s[left++], s[right--]] = [s[right], s[left]] - } -} -``` - -### lc.6 最长回文子串 - -回文子串的自身上,基本都是用双指针,比如: - -- 判断是否是回文子串,两边往中间走 -- 寻找回文子串,中间往两边走,只不过需要注意,这里的**中间**,要看字符是奇数还是偶数,所以一般两种情况都要考虑 - -```js -/** - * @param {string} s - * @return {string} - */ -var longestPalindrome = function (s) { - const getPalindrome = (s, l, r) => { - while (l >= 0 && r < s.length && s[l] == s[r]) { - l-- - r++ - } - return s.substring(l + 1, r) - } - let res = '' - for (let i = 0; i < s.length; ++i) { - const s1 = getPalindrome(s, i, i) - const s2 = getPalindrome(s, i, i + 1) - res = res.length > s1.length ? res : s1 - res = res.length > s2.length ? res : s2 - } - return res -} -``` - -这道题也可以用动态规划的方式来做,但是那样空间复杂度将为 O(n^2) diff --git "a/content/posts/algorithm/trick/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" "b/content/posts/algorithm/trick/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" deleted file mode 100644 index 43eb688..0000000 --- "a/content/posts/algorithm/trick/\345\277\205\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" +++ /dev/null @@ -1,703 +0,0 @@ ---- -title: '必须掌握的排序方法&递归时间复杂度' -date: 2024-01-04T15:37:23+08:00 -tags: [] -series: [trick] -categories: [algorithm] ---- - -## 冒泡,选择,插入 - -其中冒泡和选择一定 `O(n^2)`,插入最坏 `O(N^2)`,最好 `O(N)` 取决于数据结构。 - -```java -// 测试数组 -int[] arr = new int[]{2, 1, 5, 9, 5, 4, 3, 6, 8, 9, 6, 7, 3, 4, 2, 7, 1, 8}; - -/** - * 交换数组的两个值,此种异或交换值方法的前提是 i != j,否则会变为 0 - */ -public void swap(int[] arr, int i, int j) { - arr[i] = arr[i] ^ arr[j]; - arr[j] = arr[i] ^ arr[j]; - arr[i] = arr[i] ^ arr[j]; -} -// 保险还是用这个吧~ -public void swap(int[] arr, int i, int j) { - int temp = arr[i]; - arr[i] = arr[j]; - arr[j] = temp; -} -``` - -### 冒泡 - -从左往右,两两比较,**冒**出极值 - -```java -public void bubbleSort(int[] arr) { - for (int i = 0; i < arr.length - 1; i++) { - boolean sorted = true; // 用于优化 提前终止 - for (int j = 0; j < arr.length - 1 - i; j++) { - if (arr[j] > arr[j + 1]) { - swap(arr, j, j + 1); - sorted = false; - } - } - if (sorted) break; - } -} -``` - -```js -function bubble(arr) { - for (let i = 0; i < arr.length - 1; i++) { - let sorted = true - for (let j = 0; j < arr.length - 1 - i; j++) { - if (arr[j] > arr[j + 1]) { - swap(arr, j, j + 1) - sorted = false - } - } - if (sorted) break - } -} -``` - -### 选择 - -**选择**每个数作为极值(最大/最小),然后去和其之后的数比较,更新极值的指针,最后交换 - -```java -public void selectSort(int[] arr) { - for (int i = 0; i < arr.length - 1; i++) { - int minIndex = i; - for (int j = i + 1; j < arr.length; j++) { - if (arr[j] < arr[minIndex]) { - minIndex = j; - } - } - if (minIndex != i) { - swap(arr, minIndex, i); - } - } -} -``` - -```js -function select(arr) { - for (let i = 0; i < arr.length; ++i) { - let minIndex = i - for (let j = i + 1; j < arr.length; ++j) { - if (arr[minIndex] > arr[j]) { - minIndex = j - } - } - if (minIndex !== i) swap(arr, minIndex, i) - } -} -``` - -### 插入 - -构建有序数列,从后往前找准位置插入 - -```java -public static void insertSort(int[] arr) { - for (int i = 1; i < arr.length; i++) { - for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; --j) { - swap(arr, j, j + 1); - } - } -} -``` - -```js -function insert(arr) { - for (let i = 1; i < arr.length; ++i) { - for (let j = i - 1; j >= 0 && arr[j] > arr[j + 1]; --j) { - swap(arr, j, j + 1) - } - } -} -``` - -## 递归时间复杂度估算 - -1. 数学归纳法 -2. 主定理 master 公式:`T(N) = a * T(N/b) + O(N^c)`,a ≥ 1,b > 1;是一种用于求解分治算法时间复杂度的方法 - -- T(N): 母问题体量 -- a,表示子问题被调了多少次 -- b,**等量子问题**的体量 -- O(N^c):表示其他部分的时间复杂度 - -- 当 log_b(a) > c,则 O(n^log_b(a)) -- 当 log_b(a) = c,则 O(n^c\*logn) -- 当 log_b(a) < c,则 O(n^c) - -主定理给出了这类递归式时间复杂度的上界。但请注意,主定理并不适用于所有类型的递归式,有时可能需要配合其他方法。 - -3. 对于树,一般用递归树法:将递归过程表示为一棵树,每个节点表示一个子问题的解,通过分析树的层数和每层的节点数来确定时间复杂度。 - -## 归并排序 - -分治思想,就是把数看成二叉树,然后从底往上合并(发挥想象力~)。 - -既然是从底往上合并,想来应该是后序遍历的递归了。 - -```java -/** - * 归并排序 - * - * @param arr - * @param left - * @param right - */ -public void mergeSort(int[] arr, int left, int right) { - if (left == right) return; // 结束条件 - int mid = left + (right - left >> 1); - mergeSort(arr, left, mid); - mergeSort(arr, mid + 1, right); - merge(arr, left, mid, right); -} - -public void merge(int[] arr, int left, int mid, int right) { - int[] help = new int[right - left + 1]; - - int p1 = left; - int p2 = mid + 1; - int i = 0; - while (p1 <= mid && p2 <= right) { - help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; - } - while (p1 <= mid) { - help[i++] = arr[p1++]; - } - while (p2 <= right) { - help[i++] = arr[p2++]; - } - - for (int j = 0; j < help.length; j++) { - arr[left + j] = help[j]; - } -} -``` - -```js -function mergeSort(arr, l, r) { - if (l == r) return - const mid = l + ((r - l) >> 1) - mergeSort(arr, l, mid) // 细节 分区的时候 [l..mid] [mid+1..r],如过 [l..mid-1] [mid, r] 则可能死循环 012, 1,,2 - mergeSort(arr, mid + 1, r) - merge(arr, l, mid, r) -} - -function merge(arr, l, mid, r) { - const help = [] // size 为 r-l+1 - let i = l - let j = mid + 1 - - let p = 0 - while (i <= mid && j <= r) { - help[p++] = arr[i] < arr[j] ? arr[i++] : arr[j++] - } - while (i <= mid) { - help[p++] = arr[i++] - } - while (j <= r) { - help[p++] = arr[j++] - } - for (let k = 0; k < help.length; k++) { - arr[l + k] = help[k] - } -} -``` - -来算下一下时间复杂度吧:根据 master 公式:`T(N) = a * T(N/b) + O(N^c)`,看代码中 mergeSort 内有两个地方调用自己,所以 a == 2,N 被等分了,所以 b == 2,其他地方 merge 函数的时间复杂度是 O(N),所以 c == 1,那么我们看:log(b,a): `log 以 b 为底 a 的对数`,log(2,2) == 1 == c,所以时间复杂度是:O(N^c \* logN) 即 O(NlogN)。 - ---- - -归并排序的思想过程可以帮助解决很多问题,比如小和问题和逆序对问题。 - -### 小和问题 - -一个数组中,每一个数左边比当前数小的数加起来的和就是这个数组的小和。比如:[1,3,4,2,5],小和为:0 + 1 + 4 + 1 + 10 == 16。请你写一个算法求小和。 - -> 这道题需要转变一下思想:也可以看成每个数右侧有几个比它大:1 \* 4 + 3 \* 2 + 4 \* 1 + 2 \* 1 = 16。那么就可以考虑归并排序啦。 - -```java -/** - * 归并排序 - * - * @param arr - * @param left - * @param right - */ -public int mergeSort(int[] arr, int left, int right) { - if (left == right) return 0; // 结束条件 - int mid = left + (right - left >> 1); - return mergeSort(arr, left, mid) - + mergeSort(arr, mid + 1, right) - + merge(arr, left, mid, right); -} - -public int merge(int[] arr, int left, int mid, int right) { - int[] help = new int[right - left + 1]; - - int p1 = left; - int p2 = mid + 1; - int i = 0; - int res = 0; // 小和计算 - while (p1 <= mid && p2 <= right) { - res += arr[p1] < arr[p2] ? arr[p1] * (right - p2 + 1) : 0; // 关键点 - help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; - } - while (p1 <= mid) { - help[i++] = arr[p1++]; - } - while (p2 <= right) { - help[i++] = arr[p2++]; - } - - for (int j = 0; j < help.length; j++) { - arr[left + j] = help[j]; - } - - return res; -} -``` - -### 逆序对 - -一个数组中,左边的数如果比右边的数大,则两个数构成一个逆序对,请打印所有逆序对。这个跟小和问题异曲同工,就不多介绍了。 - -## 快速排序 - -### 荷兰国旗问题 - -荷兰国旗三色,其实就是一个简单的分区问题,一组数,把小于 target 的放一组,等于 target 的放中间,大于 target 的放右边。 - -要求:额外空间复杂度 O(1),时间复杂度 O(N)。 - -```java -/** - * 荷兰国旗问题,可以想象游标卡尺的两端往中间挤; - * - * @param arr - * @param target - */ -public static void partition(int[] arr, int target) { - int l = 0; - int r = arr.length - 1; - - int i = 0; - while (i <= r) { - if (arr[i] < target) { - swap(arr, i, l); - i++; - l++; - } else if (arr[i] == target) { - i++; - } else { - swap(arr, i, r); - r--; - } - } -} -``` - -### 快速排序实现 - -荷兰国旗的问题是快速排序的一环。 - -随机选择一个数作为 pivot,把 < pivot 的放左边, = pivot 的放中间,> pivot 的放右边。这也就是三路快排的基础。 - -```js -function quickSort(arr, l, r) { - if (l >= r) return - const [lb, rb] = partition(arr, l, r) - quickSort(arr, l, lb - 1) - quickSort(arr, rb + 1, r) -} - -function partition(arr, l, r) { - const pivotIndex = Math.floor(Math.random() * (r - l + 1)) + l - const pivot = arr[pivotIndex] - // swap(arr, pivotIndex, l) // 这一步是为了增加程序的鲁棒性,有没有结果都一样 - - let left = l - let right = r - let i = l - while (i <= right) { - if (arr[i] === pivot) { - i++ - } else if (arr[i] < pivot) { - swap(arr, i++, left++) - } else { - swap(arr, i, right--) - } - } - return [left, right] -} - -quickSort(arr, 0, arr.length - 1) -``` - -```java -/** - * 快速排序 - * - * @param arr - * @param l - * @param r - */ -public void quickSort(int[] arr, int l, int r) { - if (l < r) { - // 随机选择一个数,把它作为基准,同时把它和最右边的数交换,然后进行分区 - int pivot = l + (int) (Math.random() * (r - l + 1)); - swap(arr, pivot, r); - - // 返回 荷兰国旗的 <区右边界下一个 和 >区左边界上一个 即等于区域的[左右边界] - // 意味着:等于区域已经排好序了,对剩下的左右区域再分别排序即可 - int[] p = partition(arr, l, r); - quickSort(arr, l, p[0] - 1); - quickSort(arr, p[1] + 1, r); - } -} - -/** - * 快速排序分区 partition 函数 - * - * @param arr - * @param l - * @param r - * @return 等于pivot的左右边界,闭区间 - */ -private int[] partition(int[] arr, int l, int r) { - int left = l - 1; // < 区右边界 - int right = r; // > 区左边界,因为 r 位置为基准值 - - // l 表示当前数位置 - // 这里 小于 right 的原因是因为此时的 arr[r] 为基准值,不需要参与分区过程中 - // 在分区完毕后,放到正确的位置即可 - while (l < right) { - if (arr[l] < arr[r]) { - swap(arr, ++left, l++); - } else if (arr[l] == arr[r]) { - l++; - } else { - swap(arr, l, --right); - } - } - - swap(arr, l, r); // 最终 l == right,基准值归位 - return new int[]{left + 1, right}; // 因为最后r回归到等于它的位置 -} -``` - -> 快速排序变体算法 --- `快速选择` 算法 ==> 快速排序 + 二分的思想,见 lc.215 - -## 堆排序 - -堆就是一组数字从左往右逐个填满二叉树,这就是`满二叉树`,也就是堆了。特殊的堆是大/小根堆,也叫优先队列。比如 React 的底层就用了小根堆,java 中的 PriorityQueue 默认也是小根堆,可以传入比较器来控制。 - -### 节点关系 - -- 左子节点:2\*i + 1 -- 右子节点:2\*i + 2 -- 父节点:(i - 1)/2,注意:java 中可以这么做因为 java 中 -1 / 2 == 0;js 中就可以使用绝对值和位运算来简化操作。 - -### 上浮和下沉 - -上浮就是当数字一个一个进入堆中,从最后往上走,构建出大/小根堆。 - -```java -/** - * 上浮操作:一组数,逐个插入堆中,形成大/小根堆,时间复杂度O(logn) - */ -public void shiftUp(int[] arr, int index) { - while (arr[index] > arr[(index - 1) / 2]) { // 子大于父 就交换 (大根堆) - swap(arr, index, (index - 1) / 2); - index = (index - 1) / 2; - } -} - -/** - * 建堆,注意时间复杂度是 O(nlogn) - */ -int[] data = new int[]{3, 1, 2, 3, 8, 6, 6, 4, 9, 3, 7}; -for (int i = 0; i < data.length; i++) { - shiftUp(data, i); -} -``` - -下沉一般是在堆顶出堆后(与最后一个交换), - -```java -/** - * @param arr - * @param index - * @param heapSize,传size是为了方便当出堆的时候,重新建堆 - */ -public void shiftDown(int[] arr, int index, int heapSize) { - int left = 2 * index + 1; - while (left < heapSize) { // 证明存在子节点 - int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left; // 左右孩子中较大的那个 - largest = arr[largest] > arr[index] ? largest : index; // 较大的子 与 父 比更大的那个 - if (largest == index) { - break; - } - swap(arr, largest, index); - index = largest; - left = 2 * index + 1; - } -} -``` - -注意:一个数一个数加入堆中上浮的方式建堆的时间复杂度是 O(nlogn),一般我们也可以直接对 n/2 的数据进行下沉操作来进行优化,整体时间复杂度是 `O(n)`。 - -```java -int[] data = new int[]{3, 1, 2, 3, 8, 6, 6, 4, 9, 3, 7}; -for (int i = data.length / 2; i >= 0; --i) { - shiftDown(data, i, data.length); -} -``` - -时间复杂度证明: - -- T(n) = n/2 \* 1 + n/4 \* 2 + n/8 \* 3 + ... -- 2T(n) = n/2 \* 2 + n/2 \* 2 + n/4 \* 3 + ... - -错位相减: `T(n) = n + n/2 + n/4 + n/8 + ...`, 等比数列求和公式:sn = an。所以时间复杂度是 O(n)。 - -### 堆排序实现 - -有了上浮和下沉的 api,堆排序就是堆顶不断出堆的过程。(堆顶与最后一个交换,同时减少 heap 的 size,从头部 shiftDown 重新建堆) - -```java -public void heapSort(int[] arr) { - // 初始建堆 - for (int i = arr.length / 2; i >= 0; --i) { - shiftDown(arr, i, arr.length); - } - // 进行排序 - int heapSize = arr.length; - for (int i = arr.length - 1; i >= 0; --i) { - swap(arr, 0, i); - heapSize--; - shiftDown(arr, 0, heapSize); // 不断与最后一个数字切断连接,对0位置重新建堆; - } -} -``` - -```js -function shiftDown(arr, index, heapSize) { - let left = 2 * index + 1 - while (left < heapSize) { - // 注意边界条件 left + 1 < heapSize,因此,三元不等式不能写成 arr[left] < arr[left+1] ? left : left + 1 - let minIndex = left + 1 < heapSize && arr[left + 1] < arr[left] ? left + 1 : left - minIndex = arr[index] < arr[minIndex] ? index : minIndex - if (arr[index] <= arr[minIndex]) { - break - } - swap(arr, index, minIndex) - index = minIndex - left = 2 * index + 1 - } -} - -function heapSort(arr) { - for (let i = arr.length >> 1; i >= 0; --i) { - shiftDown(arr, i, arr.length) - } - let n = arr.length - for (let i = n - 1; i >= 0; --i) { - swap(arr, 0, i) - n-- - shiftDown(arr, 0, n) - } -} -``` - -> 几乎有序的数组排序,建议使用堆排序 - -## 希尔排序 - -插入排序的升级版,也叫缩小增量排序,用 gap 分组,没每个组内进行插入排序,当 gap 为 1 时,就排好序了,相比插入排序多了设定 gap 这一层最外部 for 循环 - -```js -function shellSort(nums) { - for (let gap = arr.length >> 1; gap > 0; gap >>= 1) { - // 多了设定gap增量这一层 - for (let i = gap; i < arr.lenght; i++) { - let curr = i - let temp = nums[i] - while (curr - gap >= 0 && nums[curr - gap] > temp) { - nums[curr] = nums[curr - gap] - curr -= gap - } - nums[curr] = temp - } - } -} -``` - -## 稳定性 - -稳定性就是相同的数字在排序后仍然保持着相对的位置。这种特性还是比较重要的,比如对一个年级的学生,先按照成绩排序,再按照班级排序。 - -冒泡选择和插入只有选择排序是不稳定的,因为在它的过程中,会把 min 或 max 与后面的交换,同理快速排序也不是稳定的,堆排序更不用提了~ - -- 快速排序: 时间 O(nlogn), 空间 O(logn),相对而言速度最快 -- 归并排序: 时间 O(nlogn),空间 O(n),具有稳定性 -- 堆排序的:时间 O(nlogn),空间 O(1),空间最少 - -## 非比较排序 - -非比较排序往往都是牺牲空间换取时间,所以通常是需要数据结构满足一定的条件下才会去使用的。 - -### 计数排序 - -计数排序一般适合于样本空间不大的正整数排序,比如人的年龄,一定大于 0 小于 200。(当然对于负数咱可以通过 + 一个数让所有的数都变成正数后再排序, 排序完成后再减去) - -核心:`将数据作为另一个数组的键存储到另一个数组中`,所以一般来说只针对正整数,当然咱可以通过 + 一个数让所有的数都变成正数后再排序, 排序完成后再减去。 - -```java -/** - * 计数排序 - * - * @param arr - * @param k - */ -public static void countSort(int[] arr, int k) { - int[] count = new int[k + 1]; - for (int i = 0; i < arr.length; i++) { - count[arr[i]] += 1; - } - int[] bucket = new int[arr.length]; - // 构建计数尺子的前缀和,统计频率,基数排序也会用到这种。 - // 现在: count[i] 的含义为 arr 中 <= i 的数字有多少个 - for (int i = 1; i < count.length; i++) { - count[i] += count[i - 1]; - } - // 从右往左遍历原数组,根据count统计的频率进行排序 - // 从右往左是为了保证稳定性 - for (int i = arr.length - 1; i >= 0; --i) { - bucket[count[arr[i]] - 1] = arr[i]; - count[arr[i]]--; - } - - for (int i = 0; i < bucket.length; i++) { - arr[i] = bucket[i]; - } -} -``` - -时间复杂度 `O(n + k)`: n 个数, k 为范围。 - -### 基数排序 - -基数排序比计数排序的使用范围更加广一点,因为是根据每个**进位**来产生桶,最多也就 0-9 十个桶。 - -相对于计数排序,根据进位,多次入桶。 - -```java -public static void main(String[] args) { - // 这个数据的范围就是 0 ~ 13 - int[] data = new int[]{4, 5, 1, 8, 13, 0, 9, 200}; - int maxBit = maxBit(data); - radixSort(data, 0, data.length - 1, maxBit); - System.out.println(Arrays.toString(data)); -} -/** - * 基数排序 - */ -public static void radixSort(int[] arr, int l, int r, int maxBit) { - final int radix = 10; // 基数 0 ~ 9 一共10位 - int i = 0, j = 0; - int[] bucket = new int[r - l + 1]; // 桶的大小和原数组一样大小 - for (int d = 0; d < maxBit; d++) { // 对每个进行进行单独遍历入桶、出桶 - int[] count = new int[radix]; - // 进行基数统计 - for (i = l; i <= r; i++) { - j = getDigit(arr[i], d); - count[j]++; - } - // 求前缀和 - for (i = 1; i < count.length; i++) { - count[i] += count[i - 1]; - } - // 从右向左遍历原数组,根据前缀和找到它对应的位置,入桶 - for (i = r; i >= l; --i) { - j = getDigit(arr[i], d); - bucket[count[j] - 1] = arr[i]; - count[j]--; - } - // 出桶还原到原数组 - for (i = l, j = 0; i <= r; i++, j++) { - arr[i] = bucket[j]; - } - } -} - -/** - * 找到最大数有多少位 - */ -public static int maxBit(int[] arr) { - int max = Integer.MIN_VALUE; - for (int digit : arr) { - max = Math.max(max, digit); - } - int bit = 0; - while (max != 0) { - bit++; - max = max / 10; - } - return bit; -} - -/** - * 取出数x进位d上的数字 - * - * @param x - * @param d - * @return - */ -public static int getDigit(int x, int d) { - return (x / (int) Math.pow(10, d)) % 10; -} -``` - -### 桶排序 - -前提:假设输入数据服从均匀分布。 - -它利用函数的映射关系,将待排序元素分到有限的桶里,然后桶内元素再进行排序(可能是别的排序算法),最后将各个桶内元素输出得到一个有序数列 - -时间复杂度 `O(n)` - -```js -function bucketSort(nums) { - // 先确定桶的数量,要找出最大最小值,再根据 scope 求出桶数 - const scope = 3 // 每个桶的存储的范围 - const min = Math.min(...nums) - const max = Math.max(...nums) - const count = Math.floor((max - min) / scope) + 1 - const bucket = Array.from(new Array(count), _ => []) - - // 遍历数据,看应该放入哪个桶中 - for (const value of nums) { - const index = ((value - min) / scope) | 0 - bucket[index].push(value) - } - - const res = [] - // 对每个桶排序 然后放入结果集 - for (const item of bucket) { - insert(item) // 插入排序 - res.push(...item) - } - return res -} -``` diff --git "a/content/posts/algorithm/trick/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" "b/content/posts/algorithm/trick/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" deleted file mode 100644 index 707fae8..0000000 --- "a/content/posts/algorithm/trick/\346\225\260\347\273\204-\345\211\215\347\274\200\345\222\214\346\225\260\347\273\204.md" +++ /dev/null @@ -1,465 +0,0 @@ ---- -title: '数组-前缀和数组' -date: 2023-02-19T15:57:07+08:00 -lastmod: 2024-02-20 -tags: [Array] -series: [trick] -categories: [algorithm] ---- - -### 概念 - -应用场景:在原始数组不会被修改的情况下,**快速、频繁查询某个区间的累加和**。通常涉及到连续子数组和相关问题时,就可以考虑使用前缀和技巧了。 - -核心思路:开辟新数组 `preSum[i]` 来存储原数组 `nums[0..i-1] `的累加和,`preSum[0] = 0`。这样,当求原数组区间和就比较容易了,区间 `[i:j]` 的和等于 `preSum[j+1] - preSum[i]` 的结果值。 - -```js -const preSum = [0] // 一般可使用虚拟 0 节点,来避免边界条件 - -for (let i = 0; i < arr.length; ++i) { - preSum[i + 1] = preSum[i] + nums[i] // 构建前缀和数组 -} - -// 查询 sum([i:j]) -preSum[j + 1] - preSum[i] -``` - -### 构造:[lc.303 区域和检索-数组不可变](https://leetcode.cn/problems/range-sum-query-immutable/) - -```js -/** - * @param {number[]} nums - */ -var NumArray = function (nums) { - this.preSum = [0] // preSum 首位为0 便于计算 - for (let i = 1; i <= nums.length; ++i) { - this.preSum[i] = this.preSum[i - 1] + nums[i - 1] - } -} - -/** - * @param {number} left - * @param {number} right - * @return {number} - */ -NumArray.prototype.sumRange = function (left, right) { - return this.preSum[right + 1] - this.preSum[left] -} - -/** - * Your NumArray object will be instantiated and called as such: - * var obj = new NumArray(nums) - * var param_1 = obj.sumRange(left,right) - */ -``` - -### 构造:[lc.304 二维区域和检索-矩阵不可变](https://leetcode.cn/problems/range-sum-query-2d-immutable/) - -```js -/** - * @param {number[][]} matrix - */ -var NumMatrix = function (matrix) { - const row = matrix.length - const col = matrix[0].length - this.sums = Array.from(Array(row + 1), () => Array(col + 1).fill(0)) - for (let i = 0; i < row; ++i) { - for (let j = 0; j < col; ++j) { - this.sums[i + 1][j + 1] = - this.sums[i + 1][j] + this.sums[i][j + 1] - this.sums[i][j] + matrix[i][j] - } - } - console.log(this.sums) -} - -/** - * @param {number} row1 - * @param {number} col1 - * @param {number} row2 - * @param {number} col2 - * @return {number} - */ -NumMatrix.prototype.sumRegion = function (row1, col1, row2, col2) { - return ( - this.sums[row2 + 1][col2 + 1] - - this.sums[row1][col2 + 1] - - this.sums[row2 + 1][col1] + - this.sums[row1][col1] - ) -} - -/** - * Your NumMatrix object will be instantiated and called as such: - * var obj = new NumMatrix(matrix) - * var param_1 = obj.sumRegion(row1,col1,row2,col2) - */ -``` - -### lc.523 连续的子数组和 - -这道题有点意思的,首先要知道一个数学知识:**同余定理:a, b 模 k 后的余数相同,则 a,b 对 k 同余**,本题需要利用同余的整除性性质: - -** `a % k == b % k`,则 `(a-b) % k === 0`。即同余数之差能被 k 整除 ** - -根据题意,需要获取到的信息是:**(preSum[j] - preSum[i]) % k === 0**,如过存在则返回 true,那么就可以转化为:**寻找是否有两个前缀和能 % k 后,余数相同的问题了~**,那就很自然想到用哈希表来存储 `{余数:索引}` 了,不然这题还真挺难想的~ 再一次 respect 数学! - -```js -var checkSubarraySum = function (nums, k) { - if (nums.length <= 1) return false - const map = { 0: -1 } - let preSum = 0 - for (let i = 0; i < nums.length; ++i) { - preSum += nums[i] - let remainder = preSum % k - if (map[remainder] >= -1) { - // 左开右闭区 - if (i - map[remainder] >= 2) { - return true - } - } else { - map[remainder] = i - } - } - return false -} -``` - -**But!请注意,有坑**,上面的算法是没有问题的,然而数据量一大,且每个数都很大,则有可能有数字溢出的风险,力扣上有个测试用例就是这样的超出范围了~~~,所以这里需要用**余数去累加!** - -```js -/** - * @param {number[]} nums - * @param {number} k - * @return {boolean} - */ -var checkSubarraySum = function (nums, k) { - if (nums.length <= 1) return false - // 注意这里:初始 key 为 0,value 为 -1,是为了计算第一个可以整除 k 的子数组长度 - const map = { 0: -1 } - let remainder = 0 - for (let i = 0; i < nums.length; ++i) { - remainder = (remainder + nums[i]) % k - if (map[remainder] >= -1) { - if (i - map[remainder] >= 2) { - // 左开右闭区间 - return true - } - } else { - map[remainder] = i - } - } - return false -} -``` - -### lc.525 连续数组 - -绝,可以把 0 看成 -1,转为求前缀和为 0 的情况。实现上用一个 counter 变量即可。 - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var findMaxLength = function (nums) { - if (nums.length <= 1) return 0 - let max = 0 - const map = { 0: -1 } - let counter = 0 - for (let i = 0; i < nums.length; ++i) { - nums[i] === 1 ? ++counter : --counter - // 哈希表中就有记录,表明此刻 区间前缀和之差 中 0 和 1 的数量相等 - // 举个例子 [1,1,1,0(-1)] 对应的前缀和为 1,2,3,2, 那么 (1, 3] 区间 1 个 0 和 1 个 1,长度为 3-1=2 - if (map[counter] >= -1) { - max = Math.max(max, i - map[counter]) - } else { - map[counter] = i - } - } - return max -} -``` - -上面两题,有一丢丢类似,比如一种感觉,当通过哈希表来存储 `{need: 索引}` 时,都需要设定 map 初始为 `{0: -1}`,可以理解为**空的前缀的元素和为 0,空的前缀的结束下标为 −1**。再一个前缀和之差区间为左开右闭区间 `(i, j]`,所以长度为 j - i。 - -### [lc.528 按权重随机选择](https://leetcode.cn/problems/random-pick-with-weight/) - -第一次碰见道题大概率是没读懂它到底是个啥意思的 😂,看了题解后,豁然开朗(怎么每次都是这种感觉,f\*\*k!) - -首先就是前缀和 `[0...arr.length-1]` 是总的前缀和,把这个总前缀和看成是一把尺子,那么每个数字的权重可以看成是在这把尺子上占用的长度。由此,可以得到一个重要的特点:**每个长度区间的右边界是当前数字 i 的前缀和,左边界是上一个区间的前缀和右边界 + 1** - -例如 w=[3,1,2,4]时,权重之和 total=10,那么我们按照 [1,3],[4,4],[5,6],[7,10]对 [1,10] 进行划分,使得它们的长度恰好依次为 3,1,2,4。 - -```js -/** - * @param {number[]} w - */ -var Solution = function (w) { - this.preSum = [0] - for (let i = 0; i < w.length; ++i) { - this.preSum[i + 1] = this.preSum[i] + w[i] - } -} - -/** - * @return {number} - */ -Solution.prototype.pickIndex = function () { - // 随机数 randomX 应该落在 pre[i] >= randomX >= pre[i] - w[i] + 1 - const randomX = (Math.random() * this.preSum[this.preSum.length - 1] + 1) | 0 - // 又因为 pre[i] 是单调递增的,那么 pre[i] >= randomX 转化为了一个二分搜索左边界的问题了 - const binarySearchlow = x => { - let low = 1, - high = this.preSum.length - while (low < high) { - const mid = low + ((high - low) >> 1) - if (this.preSum[mid] < x) { - // target > mid 接着去搜索右边,low 进化 - low = mid + 1 - } else if (this.preSum[mid] > x) { - // target < mid 接着去搜索左边,high 进化 - high = mid - } else if (this.preSum[mid] === x) { - // target === mid, 因为是搜索左边界,所以排除 mid, high = mid (牢记可行解区间为 [low...high)) - high = mid - } - } - return low - } - - return binarySearchlow(randomX) - 1 // 我们定义的前缀和的索引比原数组索引大 1,所以要减去 1,按照官解定义的前缀和就不需要了 -} -``` - -### [lc.560 和为 k 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/) - -```js -/** - * @param {number[]} nums - * @param {number} k - * @return {number} - */ -var subarraySum = function (nums, k) { - const preSum = [0] - for (let i = 0; i < nums.length; ++i) { - preSum[i + 1] = preSum[i] + nums[i] - } - let count = 0 - // 遍历出所有区间 - for (let i = 0; i < nums.length; ++i) { - for (let j = i; j < nums.length; ++j) { - preSum[j + 1] - preSum[i] === k && ++count - } - } - return count -} -``` - -这样做能得到正确结果,但是并不能 AC,时间复杂度 O(n^2),不知道谁搞了个恶心的测试用例。。。会超时~ - -看了下题解,可以使用哈希表进行优化 - -```js -var subarraySum = function (nums, k) { - const map = { 0: 1 } - let preSum = 0 - let res = 0 - for (const num of nums) { - preSum += num - if (map[preSum - k]) { - res += map[preSum - k] - } - - if (map[preSum]) { - map[preSum]++ - } else { - map[preSum] = 1 - } - } - - return res -} -``` - -### lc.724 寻找数组的中心下标 easy - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var pivotIndex = function (nums) { - const preSum = [0] - for (let i = 0; i < nums.length; ++i) { - preSum[i + 1] = preSum[i] + nums[i] - } - console.log(preSum) - for (let i = 1; i < preSum.length; ++i) { - // 根据题意很容易写出来 - if (preSum[i - 1] === preSum[preSum.length - 1] - preSum[i]) { - return i - 1 // 如果使用了 0 虚拟节点,那么前缀和的索引 == 原数组的的索引 + 1 的 - } - } - return -1 -} -``` - -### lc.918 环形数组的最大和 NICE! - -这道题很有意思,有多重方法可以解答。 - -#### 1. 动态规划,是 lc.53 的进阶版本 - -#### 2. 滑动窗口+单调队列+前缀和 - -将数组延长一倍,可以看成是两个数组拼接起来,问题转化为:在 2n 的数组上,寻找最大子数组和,且数组的长度不超过 n(用滑动窗口控制) - -```js -var maxSubarraySumCircular = function (nums) { - const n = nums.length - const queue = [] - let preSum = nums[0], - res = nums[0] - queue.push([0, preSum]) // 单调队列保存 [index, preSum] - for (let i = 1; i < 2 * n; i++) { - // 根据索引控制 窗口大小 - while (queue.length !== 0 && i - queue[0][0] > n) { - queue.shift() - } - preSum += nums[i % n] - res = Math.max(res, preSum - queue[0][1]) // 求当前窗口内的 最大子数组和, 那么单调队列顶部应该是越小越好 - // 所以当新的前缀和小于等于单调队列里的前缀和时,直接“压扁” -- 即单调队列尾部 pop,并 push 新的 preSum - while (queue.length !== 0 && queue[queue.length - 1][1] >= preSum) { - queue.pop() - } - queue.push([i, preSum]) - } - return res -} -``` - -#### 3. 最优解 空间复杂度 O(1) - -解题思路:分两种情况,一种为没有跨越边界的情况,一种为跨越边界的情况 - -- 没有跨越边界的情况直接求子数组的最大和即可; -- 跨越边界的情况可以对数组求和再减去无环的子数组的最小和,即可得到跨越边界情况下的子数组最大和; - - 求以上两种情况的大值即为结果,另外需要考虑全部为负数的情况 - -```js -/** - * @param {number[]} nums - * @return {number} - */ -var maxSubarraySumCircular = function (nums) { - // 1. 没有跨边界,直接求 子数组的最大和 - // 2. 跨了边界,等价与求 最大(前缀和 - 子数组的最小和) - // 两种情况取最大那个即可 - let preSum = nums[0] - let preMax = nums[0] - let preMin = nums[0] - let resMax = nums[0] - let resMin = nums[0] - - for (let i = 1; i < nums.length; ++i) { - preSum += nums[i] - preMax = Math.max(preMax + nums[i], nums[i]) - resMax = Math.max(resMax, preMax) - preMin = Math.min(preMin + nums[i], nums[i]) - resMin = Math.min(resMin, preMin) - } - // 最大都小于 0 了,意味着数组中所有元素都小于 0 - if (resMax < 0) return resMax // 考虑全部为负数的情况 - return Math.max(resMax, preSum - resMin) -} -``` - -### lc.974 和可被 k 整除的子数组 - -这题与题目 「lc.560 和为 K 的子数组」非常相似,同时与 lc.523 相呼应,如果不知道同余定理,则比较棘手。 - -```js -/** - * @param {number[]} nums - * @param {number} k - * @return {number} - */ -var subarraysDivByK = function (nums, k) { - let res = 0 - let remainder = 0 - const map = { 0: 1 } // 存储 { %k : count } 这里求数量,则初始化为 1,之前有题是求距离,初始化为了 -1 - for (let i = 0; i < nums.length; ++i) { - // 当有负数时,js 语言的取模和数学上的取模是不一样的,所以为了修正这种逻辑,先 +个 k 再去模即可 - remainder = (((remainder + nums[i]) % k) + k) % k - // if(remainder < 0) remainder += k // 评论里看到也可以这样修正 - - if (map[remainder]) { - res += map[remainder] - map[remainder]++ - } else { - map[remainder] = 1 - } - } - return res -} -``` - -`remainder = (((remainder + nums[i]) % k) + k) % k`,这里我的理解是,因为使用了数学上的同余定理性质,所以程序的取余远算应当与之保持一致,比如 js 中 `-5 % 3 == -2`,数学中 `-5 % 3 == 1`。 - ---- - -### 前缀积与后缀积:[lc.238 除以自身以外的数组的乘积](https://leetcode.cn/problems/product-of-array-except-self/) - -数组 nums,求除了 nums[i] 之外所有数字的乘积 === `preMulti * postMulti` - -```js -/** - * @param {number[]} nums - * @return {number[]} - */ -var productExceptSelf = function (nums) { - const n = nums.length - const front = new Array(n) - const end = new Array(n) - front[0] = 1 - end[n - 1] = 1 - for (let i = 1; i < n; i++) { - front[i] = front[i - 1] * nums[i - 1] - } - for (let i = n - 2; i >= 0; i--) { - end[i] = end[i + 1] * nums[i + 1] - } - const res = [] - for (let i = 0; i < n; i++) { - res[i] = front[i] * end[i] - } - return res -} -``` - -优化 - -```js -var productExceptSelf = function (nums) { - // 优化 动态构造前缀和后缀 一次遍历, 让返回数组自身来承载 - const res = new Array(nums.length).fill(1) - // 求出左侧所有乘积 - for (let i = 1; i < nums.length; i++) { - res[i] = nums[i - 1] * res[i - 1] - } - // 右侧的乘积需要动态的求出, 倒叙遍历 - let r = 1 - for (let i = nums.length - 1; i >= 0; --i) { - res[i] = res[i] * r - r *= nums[i] - } - return res -} -``` - -> lc.327 lc.862 (也有用到滑动窗口) 两道 hard 题,后续有时间再看看 😁 - -### 同余定理 - -> [维基百科 - 同余](https://zh.wikipedia.org/wiki/%E5%90%8C%E9%A4%98) diff --git "a/content/posts/algorithm/trick/\346\225\260\347\273\204-\345\267\256\345\210\206\346\225\260\347\273\204.md" "b/content/posts/algorithm/trick/\346\225\260\347\273\204-\345\267\256\345\210\206\346\225\260\347\273\204.md" deleted file mode 100644 index eda6c5a..0000000 --- "a/content/posts/algorithm/trick/\346\225\260\347\273\204-\345\267\256\345\210\206\346\225\260\347\273\204.md" +++ /dev/null @@ -1,127 +0,0 @@ ---- -title: '数组-差(cha)分数组' -date: 2023-02-19T16:02:32+08:00 -lastmod: 2024-02-20 -tags: [Array] -series: [trick] -categories: [algorithm] ---- - -> 前缀和数组 是应对数组区间被频繁访问求和查询 -> 差分数组 是为了应对区间内元素的频繁加减修改 - -## 概念 - -```js -const diff = [nums[0]] -for (let i = 1; i < arr.length; ++i) { - diff[i] = nums[i] - nums[i - 1] // 构建差分数组 -} -``` - -对区间 `[i:j]` 进行加减 val 操作只需要对差分数组 **`diff[i] += val`, `diff[j+1] -= val`** 进行更新,然后依据更新后的差分数组还原出最终数组即可: - -```js -nums[0] = diff[0] -for (let i = 1; i < diff[i]; ++i) { - nums[i] = diff[i] + nums[i - 1] -} -``` - -原理也很简单: - -- `diff[i] += val`,等于对 `[i...]` 之后的所有元素都加了 val -- `diff[j+1] -=val`,等于对 `[j+1...]` 之后的所有元素都减了 val - -这样就使用了常数级的时间对区间 `[i:j]` 内的元素进行了修改,最后一次性还原即可。 - -### lc.370 区间加法 vip - -假设你有一个长度为  n  的数组,初始情况下所有的数字均为  0,你将会被给出  k​​​​​​​ 个更新的操作。 - -其中,每个操作会被表示为一个三元组:[startIndex, endIndex, inc],你需要将子数组  A[startIndex ... endIndex](包括 startIndex 和 endIndex)增加  inc。 - -请你返回  k  次操作后的数组。 - -示例: - -输入: length = 5, updates = [[1,3,2],[2,4,3],[0,2,-2]] -输出: [-2,0,3,5,3] - -```js -/** - * @param {number} length - * @param {number[][]} updates - * @return {number[]} - */ -var getModifiedArray = function (length, updates) { - if (updates.length < 0) return [] - // 构建 - const diff = new Array(length).fill(0) - for (let i = 0; i < updates.length; ++i) { - const step = updates[i] - const [start, end, num] = step - diff[start] += num - end + 1 < length && (diff[end + 1] -= num) - } - // 还原 - const res = [] - res[0] = diff[0] - for (let i = 1; i < length; ++i) { - res[i] = res[i - 1] + diff[i] - } - return res -} -``` - -### lc.1094 拼车 - -```js -/** - * @param {number[][]} trips - * @param {number} capacity - * @return {boolean} - */ -var carPooling = function (trips, capacity) { - // 1. 因为初始都为 0 所以差分数组也都为 0 - // 2. 初始化差分数组的容量时,根据题意来即可,不用遍历 - const diff = Array(1001).fill(0) - for (const [people, from, to] of trips) { - diff[from] += people - diff[to] -= people // 根据题意,乘客在车上的区间是 [form..to - 1],即需要变动的区间 - } - - if (diff[0] > capacity) return false - let arr = [diff[0]] - for (let i = 1; i < diff.length; ++i) { - arr[i] = arr[i - 1] + diff[i] - if (arr[i] > capacity) return false - } - return true -} -``` - -### lc.1109 航班预定统计 - -```js -/** - * @param {number[][]} bookings - * @param {number} n - * @return {number[]} - */ -var corpFlightBookings = function (bookings, n) { - // 1. 初始化预定记录都为 0,所以差分数组也都为 0 - // 2. 根据题意,需要变动的区间为 [first...last] - const diff = Array(n + 1).fill(0) - for (const [from, to, seat] of bookings) { - diff[from] += seat - // if (to + 1 < diff.length) // 题目保证了不会越界,因此也可以不写 - diff[to + 1] -= seat // to + 1 的时候才下去 - } - const ans = [diff[0]] - for (let i = 1; i < diff.length; ++i) { - ans[i] = ans[i - 1] + diff[i] - } - return ans.slice(1) // 题目是 [1:n] -} -``` diff --git "a/content/posts/algorithm/trick/\346\232\264\345\212\233\351\200\222\345\275\222-BFS.md" "b/content/posts/algorithm/trick/\346\232\264\345\212\233\351\200\222\345\275\222-BFS.md" deleted file mode 100644 index f85a0d2..0000000 --- "a/content/posts/algorithm/trick/\346\232\264\345\212\233\351\200\222\345\275\222-BFS.md" +++ /dev/null @@ -1,226 +0,0 @@ ---- -title: '暴力递归-BFS' -date: 2022-10-09T20:49:13+08:00 -lastmod: 2024-03-27 -series: [trick] -categories: [algorithm] ---- - -## 概念 - -在学习二叉树的时候,层序遍历就是用 BFS 实现的。 - -从二叉树拓展到多叉树到图,从一个点开始,向四周开始扩散。一般来说,写 BFS 算法都是用「队列」这种数据结构,每次将一个节点周围的所有节点加入队列。 - -BFS 一大常见用途,就是找 start 到 target 的最近距离,要能把实际问题往这方面转。 - -```js -// 二叉树中 start 就是 root -function bfs(start, target) { - const queue = [start] - const visited = new Set() - visited.add(start) - - let step = 0 - while (queue.length > 0) { - const size = queue.length - /** 将当前队列中的所有节点向四周扩散 */ - for (let i = 0; i < size; ++i) { - const el = queue.shift() - if (el === target) return step // '需要的信息' - const adjs = el.adj() // 泛指获取到 el 的所有相邻元素 - for (let j = 0; j < adjs.length; ++j) { - if (!visited.has(adjs[j])) { - queue.push(adjs[j]) - visited.push(adjs[j]) - } - } - } - step++ - } -} -``` - -## 练习 - -### lc.111 二叉树的最小深度 easy - -```js -/** - * 忍不住上来先来了个回溯 dfs,但不是今天的主角哈😂 ps: 此处用了回溯的思想,也可以用转为子问题的思想 - * @param {TreeNode} root - * @return {number} - */ -var minDepth = function (root) { - if (root === null) return 0 - let min = Infinity - const dfs = (root, deep) => { - if (root == null) return - if (root.left == null && root.right == null) { - min = Math.min(min, deep) - return - } - deep++ - dfs(root.left, deep) - deep-- - deep++ - dfs(root.right, deep) - deep-- - } - dfs(root, 1) - return min -} -``` - -```js -/** BFS 版本 */ -/** - * @param {TreeNode} root - * @return {number} - */ -var minDepth = function (root) { - // 这里求的就是 root 到 最近叶子结点(target)的距离,明确了这个,剩下的交给手吧~ - if (root === null) return 0 - const queue = [root] - let deep = 0 - while (queue.length) { - const size = queue.length - deep++ - for (let i = 0; i < size; ++i) { - const el = queue.shift() - if (el.left === null && el.right === null) return deep - if (el.left) queue.push(el.left) - if (el.right) queue.push(el.right) - } - } - return deep -} -``` - -不得不说,easy 面前还是有点底气的,😄。 - -### lc.752 打开转盘锁 - -这道题是中等题,but,真的有点难度,需要好好分析。 - -首先毫无疑问,是一个穷举题目,其次,题目一个重点是:**每次旋转都只能旋转一个拨轮的一位数字**,这样我们就能抽象出每个节点的相邻节点了, '0000', '1000','9000','0100','0900'......如是题目就转变成了从 '0000' 到 target 的最短路径问题了。 - -```js -/** - * @param {string[]} deadends - * @param {string} target - * @return {number} - */ -var openLock = function (deadends, target) { - const queue = ['0000'] - let step = 0 - const visited = new Set() - visited.add('0000') - while (queue.length) { - const size = queue.length - for (let i = 0; i < size; ++i) { - const el = queue.shift() - if (deadends.includes(el)) continue - if (el === target) return step - // 把相邻元素加入 queue - for (let j = 0; j < 4; j++) { - const up = plusOne(el, j) - const down = minusOne(el, j) - !visited.has(up) && queue.push(up), visited.add(up) - !visited.has(down) && queue.push(down), visited.add(down) - } - } - step++ - } - return -1 -} -// 首先定义 每个转盘 +1,-1 操作 -function plusOne(str, i) { - const arr = str.split('') - if (arr[i] == '9') { - arr[i] = '0' - } else { - arr[i]++ - } - return arr.join('') -} -function minusOne(str, i) { - const arr = str.split('') - if (arr[i] == '0') { - arr[i] = '9' - } else { - arr[i]-- - } - return arr.join('') -} -``` - -> 提一下 「双向 BFS 优化」:传统的 BFS 框架就是从起点开始向四周扩散,遇到终点时停止;而双向 BFS 则是从起点和终点同时开始扩散,当两边有交集的时候停止。[labuladong](https://labuladong.online/algo/essential-technique/bfs-framework-2/#%E5%9B%9B%E3%80%81%E5%8F%8C%E5%90%91-bfs-%E4%BC%98%E5%8C%96) - -### lc.773 滑动谜题 hard - -又是一个小时候玩过的经典小游戏。初学者是真的想不到怎么做,知道用什么方法的也在把实际问题转为 BFS 问题上犯了难。 - -来一点点分析,1. 每次都是空位置 0 做选择,移动相邻的上下左右的元素到 0 位置,2. target 就是 [[1,2,3],[4,5]],这可咋整呢?借鉴上一题的思路,如果转为字符串,那不就好做多了?!难就难在如何把二维数组压缩到一维字符串,同时记录下每个数字的邻居索引呢。 - -一个技巧是:**对于一个 `m x n` 的二维数组,如果二维数组中的某个元素 e 在一维数组中的索引为 `i`,那么 e 的左右相邻元素在一维数组中的索引就是 `i - 1` 和 `i + 1`,而 e 的上下相邻元素在一维数组中的索引就是 `i - n` 和 `i + n`,其中 `n` 为二维数组的列数。** - -当然了,本题 2\*3 可以直接写出来 😁。 - -```js -/** - * @param {number[][]} board - * @return {number} - */ -const neighbors = [ - [1, 3], - [0, 2, 4], - [1, 5], - [0, 4], - [1, 3, 5], - [2, 4] -] -// 有点邻接表的意思奥~ -var slidingPuzzle = function (board) { - let str = '' - for (let i = 0; i < board.length; ++i) { - for (let j = 0; j < board[0].length; ++j) { - str += board[i][j] - } - } - const target = '123450' - const queue = [str] - const visited = [str] // 用 set 也行~ - let step = 0 - while (queue.length) { - const size = queue.length - for (let i = 0; i < size; ++i) { - const el = queue.shift() - if (el === target) return step - - /** 找到 0 的索引 和它周围元素交换 */ - const idx = el.indexOf('0') - for (const neighborIdx of neighbors[idx]) { - // 交换得转为数组 - const newEl = swap(el, idx, neighborIdx) - if (visited.indexOf(newEl) === -1) { - // 不走回头路 - queue.push(newEl) - visited.push(newEl) - } - } - } - step++ - } - return -1 -} -function swap(str, i, j) { - const chars = str.split('') - const temp = chars[i] - chars[i] = chars[j] - chars[j] = temp - return chars.join('') -} -``` - - diff --git "a/content/posts/algorithm/trick/\346\232\264\345\212\233\351\200\222\345\275\222-DFS&\345\233\236\346\272\257.md" "b/content/posts/algorithm/trick/\346\232\264\345\212\233\351\200\222\345\275\222-DFS&\345\233\236\346\272\257.md" deleted file mode 100644 index 602c76a..0000000 --- "a/content/posts/algorithm/trick/\346\232\264\345\212\233\351\200\222\345\275\222-DFS&\345\233\236\346\272\257.md" +++ /dev/null @@ -1,960 +0,0 @@ ---- -title: '暴力递归-dfs&回溯' -date: 2022-10-09T20:49:13+08:00 -lastmod: 2024-03-27 -series: [trick] -categories: [algorithm] ---- - -## 概念 - -刚开始学习算法的时候,看了某大佬讲解回溯算法和 dfs 的区别: - -```js -// DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面 -var dfs = function (root) { - if (root == null) return - // 做选择 - console.log('我已经进入节点 ' + root + ' 啦') - for (var i in root.children) { - dfs(root.children[i]) - } - // 撤销选择 - console.log('我将要离开节点 ' + root + ' 啦') -} - -// 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面 -var backtrack = function (root) { - if (root == null) return - for (var i in root.children) { - // 做选择 - console.log('我站在节点 ' + root + ' 到节点 ' + root.children[i] + ' 的树枝上') - backtrack(root.children[i]) - // 撤销选择 - console.log('我将要离开节点 ' + root.children[i] + ' 到节点 ' + root + ' 的树枝上') - } -} -``` - - - ---- - -后面在 B 站看了左神的算法课,他说了这么一句话:国外根本就不存在什么回溯的概念,就是暴力递归。 - -纸上得来终觉浅,绝知此事要躬行! - -```js -/** 就用最简单的二叉树来看看,到底,在外面和在里面做选择与撤销选择有什么区别 */ -class Node { - constructor(val) { - this.left = null - this.right = null - this.val = val - } -} -/* 构建出简单的一棵树,构建过程简单,就不赘述了 - 1 - / \ - 2 3 - / \ / \ - 4 5 6 7 -*/ -function dfs(root) { - if (root === null) return - console.log(`--->> 入 ${root.val} ---`) - for (const branch of [root.left, root.right]) { - dfs(branch) - } - console.log(`<<--- 出 ${root.val} ---`) -} -dfs(root) - -console.log('🔥🔥🔥 --------------------------- 🔥🔥🔥') - -function backtrack(root) { - if (root === null || root.left === null || root.right === null) return - for (const branch of [root.left, root.right]) { - console.log(`--->> ${root.val} - ${branch.val} 的树枝上; branch.val: ${branch.val}`) - backtrack(branch) - console.log(`<<--- ${root.val} - ${branch.val} 的树枝上; branch.val: ${branch.val}`) - } -} -backtrack(root) - -// --->> 入 1 --- -// --->> 入 2 --- -// --->> 入 4 --- -// <<--- 出 4 --- -// --->> 入 5 --- -// <<--- 出 5 --- -// <<--- 出 2 --- -// --->> 入 3 --- -// --->> 入 6 --- -// <<--- 出 6 --- -// --->> 入 7 --- -// <<--- 出 7 --- -// <<--- 出 3 --- -// <<--- 出 1 --- -// -// 🔥🔥🔥 --------------------------- 🔥🔥🔥 -// -// --->> 1 - 2 的树枝上; branch.val: 2 -// --->> 2 - 4 的树枝上; branch.val: 4 -// <<--- 2 - 4 的树枝上; branch.val: 4 -// --->> 2 - 5 的树枝上; branch.val: 5 -// <<--- 2 - 5 的树枝上; branch.val: 5 -// <<--- 1 - 2 的树枝上; branch.val: 2 -// --->> 1 - 3 的树枝上; branch.val: 3 -// --->> 3 - 6 的树枝上; branch.val: 6 -// <<--- 3 - 6 的树枝上; branch.val: 6 -// --->> 3 - 7 的树枝上; branch.val: 7 -// <<--- 3 - 7 的树枝上; branch.val: 7 -// <<--- 1 - 3 的树枝上; branch.val: 3 -``` - -观察不难发现,`dfs 的 root.val` 打印和 `backtrack 的 branch.val` 打印只是相差了第一个节点的值,这是因为回溯内做选择是直接从 「邻居」开始! - -因此,**“for 外选择是节点,for 内选择是树枝”** --- 是前人总结出来的经验,从而抽象出来的概念。学习一定要知其然且知其所以然,不能迷失在各种概念里! - -**总结:** - -- 回溯也是 dfs,只不过是特殊的策略罢了,比如「排列组合」类问题,往往都是往一个「空盒子」里装选择的节点,当空盒子满足 target 时,就是一个可行解。因为是从一个「空盒子」开始,所以也就是在各个树枝上做选择放入盒子里,因此适合在 for 内做选择。 - -- 传统的 dfs,就是递归穷尽到叶节点,比如求两个节点之间的距离,自然是节点与节点之间的关系,所以适合在 for 外做选择。 - - - ---- - -### 回溯练习-排列组合 - - - -#### lc.39 组合总和 - -```js -/** - * @param {number[]} candidates - * @param {number} target - * @return {number[][]} - */ -var combinationSum = function (candidates, target) { - /** - * 无重复元素,所以不用剪枝 - * 可以被重复选取,那么就是类似这么个树 - * 1 2 3 - * 1 2 3 1 2 3 1 2 3 - * ..... ..... .... - */ - const res = [] - const track = [] - let sum = 0 - /** - * 仍然不要忘记定义递归 - * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res - * 结束条件,这道题比较明显 sum === target - */ - const backtrack = level => { - if (sum === target) { - res.push([...track]) // 注意拷贝一下 - return - } - if (sum > target) return // 结束条件不要忘了~ - // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 - // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 - for (let i = level; i < candidates.length; ++i) { - track.push(candidates[i]) - sum += candidates[i] - backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 - track.pop() - sum -= candidates[i] - } - } - backtrack(0) - return res -} -``` - -此题完全弄懂之后,排列组合就都是纸老虎了。 - -#### lc.40 组合总和 II - - - -```js -/** - * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 - * @param {number[]} candidates - * @param {number} target - * @return {number[][]} - */ -var combinationSum2 = function (candidates, target) { - const res = [] - const track = [] - let sum = 0 - candidates.sort((a, b) => a - b) - const backtrack = level => { - if (sum === target) { - res.push([...track]) - return - } - if (sum > target) return - for (let i = level; i < candidates.length; ++i) { - // 因为元素有重复的,所以需要先进行 「排序」,然后进行剪枝 - if (i > level && candidates[i] === candidates[i - 1]) continue - track.push(candidates[i]) - sum += candidates[i] - backtrack(i + 1) // 不能复用元素,下一层 level 不能包扩 i 自身 - sum -= candidates[i] - track.pop() - } - } - backtrack(0) - return res -} -``` - -#### lc.216 组合总和 III - -```js -/** - * @param {number} k - * @param {number} n - * @return {number[][]} - */ -var combinationSum3 = function (k, n) { - const res = [] - const track = [] - let sum = 0 - const backtrack = level => { - if (sum === n && track.length === k) { - res.push([...track]) - return - } - if (sum > n) return - for (let i = level; i < 10; ++i) { - if (track.length > k) continue - track.push(i) - sum += i - backtrack(i + 1) - track.pop() - sum -= i - } - } - backtrack(1) - return res -} -``` - -#### lc.77 组合 - - - -```js -/** - * @param {number} n - * @param {number} k - * @return {number[][]} - */ -var combine = function (n, k) { - const res = [] - const track = [] - const backtrack = level => { - if (track.length === k) { - res.push([...track]) - return - } - for (let i = level; i <= n; ++i) { - track.push(i) - backtrack(i + 1) - track.pop() - } - } - backtrack(1) - return res -} -``` - -索然无味的一题~ - ---- - -#### lc.78 子集 - - - -```js -/** - * @param {number[]} nums - * @return {number[][]} - */ -var subsets = function (nums) { - // root - // 1 2 3 - // 2 3 3 - // 3 - const res = [[]] - const track = [] - const backtrack = level => { - // res.push([...track]) 在这里加入 是另一种无需 res 提前 [[]] - // 此处是在「节点」操作区 - if (track.length === nums.length) return - for (let i = level; i < nums.length; ++i) { - track.push(nums[i]) - // 在这里加入 需要 res 需要提前加一个空 [] - // 此处是 「树枝」操作区 - res.push([...track]) - backtrack(i + 1) - track.pop() - } - } - backtrack(0) - return res -} -``` - -#### lc.90 子集 II - - - -```js -/** - * @param {number[]} nums - * @return {number[][]} - */ -var subsetsWithDup = function (nums) { - const res = [] - const track = [] - nums.sort((a, b) => a - b) - const backtrack = level => { - res.push([...track]) - if (level === nums.length) return - for (let i = level; i < nums.length; ++i) { - if (i > level && nums[i] === nums[i - 1]) continue - track.push(nums[i]) - backtrack(i + 1) - track.pop() - } - } - backtrack(0) - return res -} -``` - ---- - -#### lc.46 全排列 - - - -排列和组合最大的区别是: - -- 组合无序,[1,2]和 [2,1] 是同一个组合,所以需要 level 来控制 -- 排序有序,所以每次都是从 level-0 开始,但是不能重复使用元素,就需要一个 「used」(可以为一个简单的 boolean[]数组,也可以为一个栈) 来进行剪枝操作 - -```js -/** - * @param {number[]} nums - * @return {number[][]} - */ -var permute = function (nums) { - const res = [] - const track = [] - const used = [] - const backtrack = () => { - if (track.length === nums.length) { - res.push([...track]) - return - } - for (let i = 0; i < nums.length; ++i) { - if (used[i]) continue // 剪枝 - track.push(nums[i]) - used[i] = true - backtrack() - track.pop() - used[i] = false - } - } - backtrack() - return res -} -``` - -#### lc.47 全排列 II - - - -```js -/** - * @param {number[]} nums - * @return {number[][]} - */ -var permuteUnique = function (nums) { - const res = [] - const track = [] - const used = [] - nums.sort((a, b) => a - b) - const backtrack = () => { - if (track.length === nums.length) { - res.push([...track]) - return - } - for (let i = 0; i < nums.length; ++i) { - if (used[i]) continue // 去除自身剪枝 - if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) continue // 关键 - track.push(nums[i]) - used[i] = true - backtrack() - track.pop() - used[i] = false - } - } - backtrack() - return res -} -``` - -上方 `!used[i-1]` 是为了去除重复的排列,当同一层前后两个元素相同时,如果前一个元素没有使用,那么就 continue,这样做的结果就是会让 `[2,2',2'']` 这样的数组保持固定的顺序,即 2 一定在 2' 前,2' 一定在 2'' 前。如果改为 `used[i-1]` 也能得到去重的效果,就是固定顺序为 2'' -> 2' -> 2,但是剪枝的效率会大大折扣,可以参考 labuladong 大佬的示意图理解。 - -
- - -
- -### 经典题 - -#### lc.51 N 皇后 - -```js -/** - * @param {number} n - * @return {string[][]} - */ -var solveNQueens = function (n) { - // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 - // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 - const res = [] - const board = Array.from(Array(n), () => Array(n).fill('.')) - const backtrack = level => { - if (level === n) { - res.push([...board.map(item => item.join(''))]) - return - } - for (let i = 0; i < n; ++i) { - //剪枝操作 - if (!isValid(board, level, i)) continue - board[level][i] = 'Q' - backtrack(level + 1) - board[level][i] = '.' - } - } - backtrack(0) - return res -} -/** - * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 - */ -function isValid(board, row, col) { - for (let i = 0; i < row; ++i) { - if (board[i][col] === 'Q') return false - } - for (let i = row - 1, j = col - 1; i >= 0; --i, --j) { - if (board[i][j] === 'Q') return false - } - for (let i = row - 1, j = col + 1; i >= 0; --i, ++j) { - if (board[i][j] === 'Q') return false - } - return true -} -``` - - - -#### lc.698 划分为 k 个相等的子集 - -给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 - -典中典,也是深入理解回溯的绝佳好题,必会必懂。 - -```js -// lc.416 分割两个等和子集,可以用动态规划去做,本文是 k 个子集,得上 dfs -// lc.78 是求出所有子集,这道题是固定了子集的数量,去分配 -/** - * @param {number[]} nums - * @param {number} k - * @return {boolean} - */ -var canPartitionKSubsets = function (nums, k) { - if (k > nums.length) return false - const sum = nums.reduce((a, b) => a + b) - if (sum % k !== 0) return false - - const bucketTarget = sum / k - const used = [] - // 定义递归:输入 k 号桶, 每个数字不能重复使用,所以从 level 层开始选择 - // 输出:k 号桶,是否应该把 nums[level] 加入进来 - const backtrack = (k, level, bucketSum) => { - if (k === 0) return true // 所有桶装满了 - // 一个桶装满了,继续下一个桶 - if (bucketSum === bucketTarget) { - return backtrack(k - 1, 0, 0) // 从 0 层 0 sum 重新开始累加和 - } - for (let i = level; i < nums.length; ++i) { - if (used[i]) continue // 剪枝,被用过啦 - if (nums[i] + bucketSum > bucketTarget) continue // 这个数装不得,装了就超载~ - used[i] = true - bucketSum += nums[i] - if (backtrack(k, level + 1, bucketSum)) return true // 递归下一个数字是否加入桶 - used[i] = false - bucketSum -= nums[i] - } - return false - } - return backtrack(k, 0, 0) -} -/** - * 上方代码,逻辑上是没有问题的,但是效率低下,跑力扣的测试用例会超时 - * - * 优化自然是可以想到 memo 缓存,再就是我没想到的 used 改为位运算[捂脸]。。。 - */ -var canPartitionKSubsets = function (nums, k) { - if (k > nums.length) return false - const sum = nums.reduce((a, b) => a + b) - if (sum % k !== 0) return false - - const bucketTarget = sum / k - let used = 0 // 使用位图技巧 - const backtrack = (k, level, bucketSum, memo) => { - if (k === 0) return true - if (bucketSum === bucketTarget) { - const nextBucket = backtrack(k - 1, 0, 0, memo) - memo.set(used, nextBucket) - return nextBucket - } - if (memo.has(used)) { - // 避免冗余计算 - return memo.get(used) - } - for (let i = level; i < nums.length; ++i) { - // 判断第 i 位是否是 1 - if (((used >> i) & 1) === 1) { - continue // nums[i] 已经被装入别的桶中 - } - if (nums[i] + bucketSum > bucketTarget) continue - used |= 1 << i // 将第 i 位置为 1 - bucketSum += nums[i] - if (backtrack(k, level + 1, bucketSum, memo)) return true - used ^= 1 << i // 使用异或运算将第 i 位恢复 0 - bucketSum -= nums[i] - } - return false - } - return backtrack(k, 0, 0, new Map()) -} -``` - -这道题,我是觉得挺有难度的。。。位运算俺着实没想到啊 😭 - - - -> 推荐阅读:[「labuladong 球盒模型」](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack-2/) - -#### lc.22 括号生成 - -```js -/** - * @param {number} n - * @return {string[]} - */ -var generateParenthesis = function (n) { - // 当出现右括号数量 > 左括号数量时,无效 - const res = [] - const track = [] - const backtrack = (l, r) => { - if (l > r) return - if (l < 0 || r < 0) return - if (l === 0 && r === 0) { - res.push([...track].join('')) - return - } - /** 之前可选择的是很多个,这里就两个直接摊开写方便 */ - track.push('(') - backtrack(l - 1, r) - track.pop() - track.push(')') - backtrack(l, r - 1) - track.pop() - } - backtrack(n, n) - return res -} -``` - -#### lc.37 解数独 hard - -建议先做岛屿类问题,再做此题。 - -```js -/** - * 根据题意,一开始很容易写出这样子的代码,但是此刻 dfs 是什么意思呢?应该是一个探测的过程 - * 也就是说 -- 此处有回溯!与岛屿类问题 flood fill 算法不同的是:flood fill 它直接就 flush 掉了找到的陆地 - * 而此处的 dfs 是在不断探测的,是要走回头路的,所以这两个 for 循环是在 dfs 之内的,即: - * dfs() {for(for(选择 dfs 撤销选择))} - */ -var solveSudoku = function (board) { - for (let i = 0; i < m; ++i) { - for (let j = 0; j < n; ++j) { - if (board[i][j] === '.') { - for (let k = 1; k <= 9; ++k) { - dfs(grid, i, j, k.toString()) - } - } - } - } -} -/** 开奖 */ -/** - * @param {character[][]} board - * @return {void} Do not return anything, modify board in-place instead. - */ -var solveSudoku = function (board) { - dfs(board) -} -function dfs(grid) { - for (let i = 0; i < 9; ++i) { - for (let j = 0; j < 9; ++j) { - if (grid[i][j] === '.') { - for (let k = 1; k <= 9; ++k) { - // 重点是剪枝 - if (!isValid(grid, i, j, k.toString())) continue - grid[i][j] = k.toString() - if (dfs(grid)) return true // 找到一种可行解 直接结束 - grid[i][j] = '.' - } - return false // 9 个数字都不行 - } - } - } - return true // 遍历完没有返回 false,说明找到了一组合适棋盘位置了 -} -function isValid(grid, row, col, tryVal) { - for (let i = 0; i < 9; ++i) { - if (grid[row][i] === tryVal) return false - if (grid[i][col] === tryVal) return false - } - - const startRow = Math.floor(row / 3) * 3 - const startCol = Math.floor(col / 3) * 3 - for (let i = startRow; i < startRow + 3; ++i) { - for (let j = startCol; j < startCol + 3; ++j) { - if (grid[i][j] === tryVal) return false - } - } - - return true -} -``` - -#### lc.200 岛屿的数量 - -首先考验的是 dfs 遍历二维数组的能力。 - -```js -/** - * 在二维矩阵中的 dfs --- for{for{dfs}} - */ -function dfs(matrix, i, j, visited) { - if (i < 0 || j < 0 || i >= m || j >= n) return - /** 进节点 */ - if (visited[i][j]) return - visited[i][j] = true - /** 上下左右遍历 */ - dfs(matrix, i - 1, j) - dfs(matrix, i + 1, j) - dfs(matrix, i, j - 1) - dfs(matrix, i, j + 1) - /** 出节点 */ -} - -/** 此外有方向数组的技巧 */ -const dirs = [ - [-1, 0], - [1, 0], - [0, -1], - [0, 1] -] -// 把上方的四个 dfs 配合 dirs 改为 for 循环即可 -for (var d of dirs) { - const next_i = i + d[0] - const next_j = j + d[1] - dfs(matrix, next_i, next_j, visited) -} -``` - -```js -/** - * @param {character[][]} grid - * @return {number} - */ -var numIslands = function (grid) { - let res = 0 - const m = grid.length - const n = grid[0].length - const visited = Array.from(Array(m), () => Array(n).fill(false)) - for (let i = 0; i < m; ++i) { - for (let j = 0; j < n; ++j) { - // 从某个陆地节点开始 detect - if (grid[i][j] === '1') { - res++ - dfs(grid, i, j, visited) - } - } - } - return res -} -function dfs(grid, i, j, visited) { - const m = grid.length - const n = grid[0].length - if (i < 0 || j < 0 || i >= m || j >= n) return - if (grid[i][j] === '0') return - grid[i][j] = '0' // 淹没土地 - dfs(grid, i - 1, j, visited) - dfs(grid, i + 1, j, visited) - dfs(grid, i, j - 1, visited) - dfs(grid, i, j + 1, visited) -} -``` - -> 这种“淹掉岛屿”的 dfs 算法有自己的名字 --- [「经典的 Flood fill 算法」](https://zh.wikipedia.org/wiki/Flood_fill),这样可以不用维护 visited 数组。如果题目要求不能修改原数组,那么还是用 visited 去做,就此题而言具体就是多两步操作,一个是在 res++ 前判断是否 visit 过,另一个就是在每次 dfs 前判断是否 visit 过。 - -_另外此题还可以使用 BFS 和 并查集 解决_ - - - -#### lc.695 岛屿的最大面积 - -```js -/** - * @param {number[][]} grid - * @return {number} - */ -var maxAreaOfIsland = function (grid) { - let maxArea = 0 - const m = grid.length - const n = grid[0].length - for (let i = 0; i < m; ++i) { - for (let j = 0; j < n; ++j) { - if (grid[i][j] === 1) { - maxArea = Math.max(maxArea, dfs(grid, i, j)) - } - } - } - return maxArea -} -// 继续 flood fill -function dfs(grid, i, j) { - const m = grid.length - const n = grid[0].length - if (i < 0 || j < 0 || i >= m || j >= n) return 0 - if (grid[i][j] === 0) return 0 - grid[i][j] = 0 - return dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1) + 1 -} -``` - - - -#### lc.1020 飞地的数量 - -```js -/** - * @param {number[][]} grid - * @return {number} - */ -var numEnclaves = function (grid) { - let res = 0 - const m = grid.length - const n = grid[0].length - // 先把四个边界的淹没掉 - for (let i = 0; i < m; ++i) { - dfs(grid, i, 0) - dfs(grid, i, n - 1) - } - for (let i = 0; i < n; ++i) { - dfs(grid, 0, i) - dfs(grid, m - 1, i) - } - for (let i = 0; i < m; ++i) { - for (let j = 0; j < n; ++j) { - if (grid[i][j] === 1) { - res++ - } - } - } - return res -} -function dfs(grid, i, j) { - const m = grid.length - const n = grid[0].length - if (i < 0 || j < 0 || i >= m || j >= n) return - if (grid[i][j] === 0) return - grid[i][j] = 0 - dfs(grid, i - 1, j) - dfs(grid, i + 1, j) - dfs(grid, i, j - 1) - dfs(grid, i, j + 1) -} -``` - -#### lc.1254 统计封闭岛屿的数目 - -```js -/** - * @param {number[][]} grid - * @return {number} - */ -var closedIsland = function (grid) { - let res = 0 - const m = grid.length - const n = grid[0].length - for (let i = 0; i < m; ++i) { - dfs(grid, i, 0) - dfs(grid, i, n - 1) - } - for (let j = 0; j < n; ++j) { - dfs(grid, 0, j) - dfs(grid, m - 1, j) - } - for (let i = 0; i < m; ++i) { - for (let j = 0; j < n; ++j) { - if (grid[i][j] === 0) { - res++ - dfs(grid, i, j) - } - } - } - return res -} -function dfs(grid, i, j) { - const m = grid.length - const n = grid[0].length - if (i < 0 || j < 0 || i >= m || j >= n) return - if (grid[i][j] === 1) return - grid[i][j] = 1 - dfs(grid, i - 1, j) - dfs(grid, i + 1, j) - dfs(grid, i, j - 1) - dfs(grid, i, j + 1) -} -``` - -#### lc.1905 统计子岛屿 - -这道题需要稍微思考一下,同样大小的 grid,统计 grid2 中的子岛屿,直接遍历是不太好操作的,得先排除掉非子岛屿,然后再统计。什么是非子岛屿,那就是在 grid 中是陆地,但是在 grid1 中是海洋,直接淹掉,这样最后再遍历的时候剩下的就都是子岛屿啦。 - -```js -/** - * @param {number[][]} grid1 - * @param {number[][]} grid2 - * @return {number} - */ -var countSubIslands = function (grid1, grid2) { - let res = 0 - const m = grid2.length - const n = grid2[0].length - for (let i = 0; i < m; ++i) { - for (let j = 0; j < n; ++j) { - if (grid2[i][j] === 1 && grid2[i][j] !== grid1[i][j]) { - dfs(grid2, i, j) - } - } - } - for (let i = 0; i < m; ++i) { - for (let j = 0; j < n; ++j) { - if (grid2[i][j] === 1) { - res++ - dfs(grid2, i, j) - } - } - } - return res -} -function dfs(grid, i, j) { - const m = grid.length - const n = grid[0].length - if (i < 0 || j < 0 || i >= m || j >= n) return - if (grid[i][j] === 0) return - grid[i][j] = 0 - dfs(grid, i - 1, j) - dfs(grid, i + 1, j) - dfs(grid, i, j - 1) - dfs(grid, i, j + 1) -} -``` - -这道题自然也是可以使用 「并查集」来解决,后序并查集章节会详细使用。 - ---- - -> 请注意,本文所有的回溯函数,都使用了闭包的特性,如果不使用闭包,把需要的参数变为回溯函数的入参即可。 - -我亦无他,唯手熟尔!peace~ diff --git "a/content/posts/algorithm/trick/\346\273\221\345\212\250\347\252\227\345\217\243.md" "b/content/posts/algorithm/trick/\346\273\221\345\212\250\347\252\227\345\217\243.md" deleted file mode 100644 index cad2f8c..0000000 --- "a/content/posts/algorithm/trick/\346\273\221\345\212\250\347\252\227\345\217\243.md" +++ /dev/null @@ -1,453 +0,0 @@ ---- -title: '滑动窗口' -date: 2024-02-21 -lastmod: -tags: [] -series: [trick] -categories: [algorithm] ---- - -## 核心 - -滑动窗口的核心就是维持一个 `[i..j)` 的区间窗口,在数据上游走,来获取到需要的信息,在数组,字符串等中的表现,往往如下: - -```js -function slideWindow() { - // 前后快慢双指针 - let left = 0 - let right = 0 - /** 具体的条件逻辑根据实际问题实际处理,多做练习 */ - while(slide condition) { - window.push(s[left]) // s 为总数据(字符串、数组) - right++ - while(shrink condition) { - window.shift(s[left]) - left++ - } - } -} -``` - -> 滑动窗口的算法时间复杂度为 `O(n)`,适用于处理大型数据集 - -## 练一练 - -### lc.3 无重复字符的最长子串 - -```js -/** - * @param {string} s - * @return {number} - */ -var lengthOfLongestSubstring = function (s) { - let max = 0 - let l = 0, - r = 0 - let window = {} - while (r < s.length) { - const c = s[r] - window[c] ? ++window[c] : (window[c] = 1) - r++ - while (window[c] > 1) { - const d = s[l] - window[d]-- - l++ - } - const len = r - l - max = Math.max(max, len) - } - return max -} -``` - -### lc.76 最小覆盖子串 - -```js -/** - * @param {string} s - * @param {string} t - * @return {string} - */ -var minWindow = function (s, t) { - if (s.length < t.length) return '' - let minCoverStr = '' - - let need = {} - for (const c of t) { - need[c] ? ++need[c] : (need[c] = 1) - } - const ValidCount = Object.keys(need).length - - let l = 0, - r = 0 - let window = {} - let validCount = 0 - while (r < s.length) { - const c = s[r] - window[c] ? ++window[c] : (window[c] = 1) - if (window[c] == need[c]) validCount++ - r++ - - while (validCount === ValidCount) { - const d = s[l] - if (window[d] === need[d]) { - validCount-- - // 分析出,此时字符串区间 应当为[left, right),因为 此时 right 已经++,left 还未++ - const str = s.slice(l, r) - if (!minCoverStr) minCoverStr = str // 这一步很容易忘记。。。 - minCoverStr = str.length < minCoverStr.length ? str : minCoverStr - } - window[d]-- - l++ - } - } - - return minCoverStr -} -``` - -### lc.438 找到字符串中所有字母异位词 - -```js -/** - * @param {string} s - * @param {string} p - * @return {number[]} - */ -var findAnagrams = function (s, p) { - if (s.length < p.length) return [] - let res = [] - const need = {} - for (const c of p) { - need[c] ? need[c]++ : (need[c] = 1) - } - const ValidCount = Object.keys(need).length - - let l = 0, - r = 0 - const window = {} - let count = 0 - while (r < s.length) { - const c = s[r] - window[c] ? window[c]++ : (window[c] = 1) - if (window[c] === need[c]) count++ - r++ - // 收缩条件容易犯错的地方,不能 AC的时候可以考虑一下是不是收缩条件有问题 - while (r - l >= p.length) { - if (count === ValidCount) res.push(l) - const d = s[l] - if (window[d] === need[d]) { - count-- - } - window[d]-- - l++ - } - - /** - 奇葩的我写第二遍的时候也是这么写的。。。。 - 因为比如 s=abbc,p=abc,这种情况,在统计 count 的时候就会有漏洞 - while(count === ValidCount) { - const d = s[l] - if(window[d] === need[d]) { - res.push(l) - count-- - } - window[d]-- - l++ - } - */ - } - - return res -} -``` - -### lc.567 字符串的排列 - -```js -/** - * @param {string} s1 - * @param {string} s2 - * @return {boolean} - */ -var checkInclusion = function (s1, s2) { - if (s1.length > s2.length) return false - const need = {} - for (const c of s1) { - need[c] ? need[c]++ : (need[c] = 1) - } - const ValidCount = Object.keys(need).length - const size = s1.length - - let l = 0, - r = 0 - let window = {} - let count = 0 - while (r < s2.length) { - const c = s2[r] - window[c] ? window[c]++ : (window[c] = 1) - if (window[c] === need[c]) count++ - r++ - - while (r - l == size) { - if (count === ValidCount) return true - const d = s2[l] - if (window[d] === need[d]) count-- - window[d]-- - l++ - } - } - - return false -} -``` - -### lc.209 长度最小的子数组 - -```js -/** - * @param {number} target - * @param {number[]} nums - * @return {number} - */ -var minSubArrayLen = function (target, nums) { - // 求数组区间和 自然想到前缀和数组的啦~ - const preSum = [0] - for (let i = 0; i < nums.length; ++i) { - preSum[i + 1] = preSum[i] + nums[i] - } - - let l = 0, - r = 0 - let min = Infinity - while (r < nums.length) { - r++ - while (preSum[r] - preSum[l] >= target) { - min = Math.min(min, r - l) - l++ - } - } - - return min === Infinity ? 0 : min -} - -/** - * 一般这种都可以空间优化一下 - */ -var minSubArrayLen = function (target, nums) { - let res = Infinity - let l = 0, - r = 0 - let sum = 0 - while (r < nums.length) { - sum += nums[r] - r++ - while (sum >= target) { - res = Math.min(r - l, res) - sum -= nums[l] - l++ - } - } - return res === Infinity ? 0 : res -} -``` - -### lc.219 存在重复元素 II easy (形式) - -这道题是 easy 题,用哈希表做会非常容易,但是面试和做链表题一样,最好能做出空间复杂度更小的解法。 - -```js -/** - * @param {number[]} nums - * @param {number} k - * @return {boolean} - */ -var containsNearbyDuplicate = function (nums, k) { - const set = new Set() - for (let i = 0; i < nums.length; ++i) { - if (set.has(nums[i])) return true - set.add(nums[i]) - if (set.size > k) set.delete(nums[i - k]) - } - return false -} -``` - -这道题的窗口和上方其他题的窗口形式,略有不同,首先没有使用双指针,其次使用了 set 集合而不是 map,这样就可以**通过固定 set 的大小来作为窗口**,就很巧妙。 - -> 在前缀和 lc.918 中,通过控制单调队列的大小来控制窗口,与本题类似,此种形式,也应当熟练运用 - - - -### lc.395 至少有 K 个重复字符的最长子串 - -这道题还是比较特殊的,因为窗口也与常规的不一样,需要换个思路。 - -枚举最长子串中的字符种类数目,它最小为 1,最大为 26,所以把字符种类数作为窗口,同时统计每个字符在窗口内出现的次数,这样: - -- 当字符种类数 total 固定时,一旦 total > t,就去移动左指针,同时更新 window 内的字符出现次数统计 -- 判断字符出现次数是否小于 k,可以通过 less 变量来控制 - -当限定字符种类数目为 t 时,满足题意的最长子串,就一定出自某个 s[l..r]。因此,在滑动窗口的维护过程中,就可以直接得到最长子串的大小。 - -```js -var longestSubstring = function (s, k) { - let res = 0 - // 限定字符种类数,创造窗口,注意是 [1..26] - for (let t = 1; t <= 26; t++) { - let l = 0, - r = 0 - const window = {} // 维护窗口内每个字符出现的次数 - let total = 0 // 字符种类数 - let less = 0 // 当前出现次数小于 k 的字符的数量 (避免遍历整个 window) - while (r < s.length) { - const c = s[r] - window[c] ? window[c]++ : (window[c] = 1) - if (window[c] === 1) { - total++ - less++ - } - if (window[c] === k) { - less-- - } - r++ - - while (total > t) { - const d = s[l] - if (window[d] === k) { - less++ - } - if (window[d] === 1) { - total-- - less-- - } - window[d]-- - l++ - } - // 当没有存在小于 k 的字符时,此时满足条件 - if (less == 0) { - res = Math.max(res, r - l) - } - } - } - return res -} -/** 此题还有分治解法,见官解 */ -``` - -### lc.424 替换后的最长重复字符 - -这道题算是滑动窗口的进阶了,收缩时,left 只向右移动一步(即与 right 一起向右平移),原因是更小的窗口没有考虑的必要了。 - -```js -/** - * @param {string} s - * @param {number} k - * @return {number} - */ -var characterReplacement = function (s, k) { - let l = 0, - r = 0 - let window = {} - let maxN = 0 // 记录窗口内最大的相同字符出现次数 - while (r < s.length) { - const c = s[r] - window[c] ? window[c]++ : (window[c] = 1) - maxN = Math.max(maxN, window[c]) - r++ - - // 窗口大到 k 不够换下除了最大出现次数字符外的其他所有字符时,收缩左指针 - if (r - l - maxN > k) { - // 好像换成 while 也没问题,但是小于 r - l 的长度没有必要考虑 - window[s[l]]-- - l++ - } - } - - return r - l -} -``` - -### lc.713 乘积小于 K 的子数组 - -这道题是变长窗口 - -```js -/** - * @param {number[]} nums - * @param {number} k - * @return {number} - */ -var numSubarrayProductLessThanK = function (nums, k) { - // 读完题目,感觉要用到前缀积 + 滑动窗口 - let res = 0 - let multi = 1, - l = 0 - - for (let r = 0; r < nums.length; ++r) { - multi *= nums[r] - while (multi >= k && l <= r) { - multi /= nums[l] - l++ - } - // 每次右指针位移到一个新位置,应该加上 x 种数组组合: - // nums[right] - // nums[right-1], nums[right] - // nums[right-2], nums[right-1], nums[right] - // nums[left], ......, nums[right-2], nums[right-1], nums[right] - // 共有 right - left + 1 种 - res += r - l + 1 - } - return res -} -/** 本题 与 lc.209 相似,把求和或求积变成窗口,寻找与索引的关系 */ -``` - -> 这个[「题解」](https://leetcode.cn/problems/subarray-product-less-than-k/solutions/1373555/hua-dong-chuang-kou-by-fenjue-xvg5) 不错 - ---- - -## 总结 - -滑动窗口算法适用于解决以下类型的问题: - -- 查找最大子数组和 -- 查找具有 K 个不同字符的最长(短)子串 -- 查找具有特定条件的最长(短)子串或子数组 -- 查找连续 1 的最大序列长度(可以在允许将最多 K 个 0 替换为 1 的情况下) -- 查找具有特定和的子数组 -- 查找具有不同元素的子数组 -- 查找具有特定条件的最大或最小子数组 -- ... - -滑动窗口算法通常用于解决需要在数组或字符串上维护一个固定大小的窗口,并在窗口内执行特定操作或计算的问题。这种算法技术可以有效降低时间复杂度,通常为 `O(n)`,适用于处理大型数据集。 - -> 对于窗口大小的区间可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。如果你设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。 - -> 滑动窗口可以分为固定窗口大小,非固定窗口大小。**存储窗口数据的数据类型可以为 hashMap,hashSet 或者简单的数字(比如前缀和前缀积)** diff --git "a/content/posts/algorithm/z-others/JS\344\270\255\347\232\204\346\225\260\347\273\204\344\270\216\351\223\276\350\241\250.md" "b/content/posts/algorithm/z-others/JS\344\270\255\347\232\204\346\225\260\347\273\204\344\270\216\351\223\276\350\241\250.md" deleted file mode 100644 index e77b9cf..0000000 --- "a/content/posts/algorithm/z-others/JS\344\270\255\347\232\204\346\225\260\347\273\204\344\270\216\351\223\276\350\241\250.md" +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: 'JS中的数组与链表' -date: 2022-09-20T17:02:29+08:00 -draft: true ---- - -> 忘记在那本书看到的一句话说: -> JavaScript 中的数组只不过是名字叫数组而已,与数据结构中的数组几乎没有关系 - -JSArray 继承自 JSObject,是特殊的对象,内部也是用 key-value 的形式存储数据,所以可以存放不同类型的值,这在 Java 类语言的数组是做不到的。 - -> 专业术语要注意准确,数组根据下标索引查找元素的时间复杂度为 O(1) ,而不是数组查找的时间复杂度,即使是用二分查找也得 O(logn)。 - -## 快慢数组 - -JavaScript 中的数组分为快慢数组。 - -- 快数组:存储结构是`FixedArray`,`length <= elements.length()`,push 和 pop 会动态扩容缩容 -- 慢数组:存储结构是`HashTable`,数组下标作为 key - -快数组,使用的是一段连续的内存,可以直接用索引定位,新创建的数组默认就是快数组,new Array(x),当数组长度大于 x 继续 push 会动态扩容。 - -慢数组,不需要连续内存空间,维护一个哈希表,与快数组相比性能较差。 - -### 快慢转换 - -快变慢: - -1. 当前加入的索引减去当前容量(capacity)大于等于 1024。(index - capacity >= 1024),意味着空洞大于等于 1024。 -2. 3*扩容后容量*2 <= 新容量 - > 其实目的就是节省内存空间 - -慢变快: - -1. 当慢数组 节省空间<= 50% 的时候就会变成快数组。 - -### 为什么下标从 0 开始 - -1. 历史原因: C -> Java -> JavaScript -2. 数组寻址时减少 CPU 指令运算 -3. 物理内存的地址是从 0 开始的 - -```js -// 寻址公式 -arr[i] = base_address + i * type_size -// 其中base_address为数组arr首地址,arr0就是 **偏移量** 为0的数组,即数组arr首地址; -// i为偏移量,type_size为数组类型字节数,比如int为32位,即4个字节 - -arr[i] = base_address + (i -1)* type_size -// 多了一次减1的运算 -``` - -## 一点经验 - -### 二维数组初始化 - -```js -const dp = Array.from(Array(m), () => Array(n).fill(0)) -``` - -### 索引区间长度 - -- `[i:j]`,闭区间索引之间的长度是`j - i + 1` -- `(i..j)`,开区间索引之间的长度是`j - i - 1` -- `[i..j)`,表示从 i 到 j 需要多少步,`j - i` - -### 随机数小技巧 - -- 区间 `[i..j)` 随机数:`i + Math.random() * (j - i) | 0` -- 区间 `[i:j]` 随机数:`i + Math.random() * (j - i + 1) | 0` - -> [JS 生成限定范围内随机整数](https://www.cnblogs.com/f6056/p/13362504.html) - -## 链表 - -JS 中没有链表的数据结构,一般也就是根据对象,去模拟,next 作为指针。 - -一般对链表的操作经常需要用额外一个指针去穿针引线 -当需要操作头结点的时候,经常会使用哨兵守卫的技巧(单调栈中经常使用),在链表中也被称为‘虚拟头结点 dummy’。 diff --git "a/content/posts/algorithm/z-others/JS\347\261\273\345\270\270\350\247\201\344\270\200\350\210\254\347\256\227\346\263\225\351\242\230.md" "b/content/posts/algorithm/z-others/JS\347\261\273\345\270\270\350\247\201\344\270\200\350\210\254\347\256\227\346\263\225\351\242\230.md" deleted file mode 100644 index 22dbcb3..0000000 --- "a/content/posts/algorithm/z-others/JS\347\261\273\345\270\270\350\247\201\344\270\200\350\210\254\347\256\227\346\263\225\351\242\230.md" +++ /dev/null @@ -1,270 +0,0 @@ ---- -title: 'JS类常见一般算法题' -date: 2022-09-23T14:32:40+08:00 -draft: true ---- - -记录一下非力扣,但是是 JS 常见的一般类算法题,比较简单。 - -### tree/扁平化互换 - -题目: - -```js -// 扁平数组 -const arr = [ - { id: 1, name: '1', pid: 0 }, - { id: 2, name: '2', pid: 1 }, - { id: 3, name: '3', pid: 1 }, - { id: 4, name: '4', pid: 3 }, - { id: 5, name: '5', pid: 3 } -] -// tree -const tree = [ - { - id: 1, - name: '1', - pid: 0, - children: [ - { - id: 2, - name: '2', - pid: 1, - children: [] - }, - { - id: 3, - name: '3', - pid: 1, - children: [ - { - id: 4, - name: '4', - pid: 3, - children: [] - } - ] - } - ] - } -] -``` - ---- - -规律不要太明显,最容易想到的就是递归喽~ - -```js -// tree扁平化 就是个树的遍历而已 -function treeToArr(tree) { - const res = [] - const getChildren = tree => { - for (const node of tree) { - const { id, name, pid } = node - res.push({ id, name, pid }) - if (node.children) getChildren(node.children) - } - } - getChildren(tree) - return res -} - -const transToArr = arr => { - const res = [] - const getChildren = arr => { - arr.forEach(item => { - const obj = { - id: item.id, - pid: item.pid, - name: item.name - } - res.push(obj) - if (item.children.length) getChildren(item.children) - }) - } - getChildren(arr) - return res -} -``` - ---- - -扁平化转树,往往有些人写不出来,是因为对于递归不够熟悉。如果能够联想到使用 pid 去寻找子集,那么我觉得还是比较容易的吧。 - -```js -// 扁平化转tree -function arrToTree(arr) { - const res = [] - // 递归: 根据pid寻找子节点塞入child - const getChildren = (pid, child) => { - for (const item of arr) { - if (item.pid === pid) { - const newItem = { ...item, children: [] } - getChildren(newItem.id, newItem.children) - child.push(newItem) - } - } - } - getChildren(0, res) - return res -} - -/** - * 2024.02.15 重新写了一版,应该更简单 - */ -function arrToTree(arr, pid) { - if (!arr.length) return - const rootItems = arr.filter(item => item.pid === pid) - for (const el of rootItems) { - el.children = arr.filter(item => item.pid === el.id) - arrToTree(arr, el.id) - } - return rootItems -} -``` - - - -上方是写出来了,但是呢,这个复杂度有点高,怎么优化呢?往往需要借助数据结构 Map: - -```js -function arrToTree(arr) { - const res = [] - const map = new Map() // 便于查找 - - for (const item of arr) { - map.set(item.id, { ...item, children: [] }) - } - - for (const item of arr) { - const newItem = map.get(item.id) - if (item.pid === 0) { - res.push(newItem) - } else { - if (map.has(item.pid)) { - map.get(item.pid).children.push(newItem) - } - } - } - return res -} -``` - ---- - -### 斐波那契数列 - -核心就是滚动数组的思想。 - -```js -function fib(n) { - if (n <= 1) return n - let p = 0, - q = 0, - r = 1 - for (let i = 2; i <= n; ++i) { - p = q - q = r - r = p + q - } - return r -} -``` - ---- - -### 大数相加 - -JS 的数值是有范围的,超过范围后就会损失精度,大数相加往往是通过字符串来实现的。 - -在相加的过程中考虑进位即可。 - -```js -function bigSum(a, b) { - // 先补齐长度 - const maxLength = Math.max(a.length, b.length) - a = a.padStart(maxLength, 0) - b = b.padStart(maxLength, 0) - - // 再从末尾开始相加 - let c = 0 // 进位 - let sum = '' - for (let i = maxLength - 1; i >= 0; --i) { - let t = parseInt(a[i]) + parseInt(b[i]) + c - c = Math.floor(t / 10) - sum = (t % 10) + sum - } - if (c == 1) sum = c + sum // 注意不要遗漏最后的进位 - return sum -} - -console.log(bigSum('9007199254740991', '1234567899999999999')) -``` - -### 给数字每三位加逗号 - -可以通过一个 counter 变量计数,也可以每次从末尾截取 3 个。 - -```js -const num = 20230102 // expect: 20,230,102 - -function toThousand(num) { - let counter = 0 - let temp = num.toString() - let res = '' - for (let i = temp.length - 1; i >= 0; --i) { - counter++ - res = temp[i] + res - if (counter % 3 === 0 && i !== 0) { - res = ',' + res - } - } - return res -} - -function toThousand2(num) { - let temp = num.toString() - let res = '' - while (temp.length) { - res = ',' + temp.slice(-3) + res - temp = temp.slice(0, -3) - } - return res.slice(1) -} -``` - -### flatten 多种实现 - -```js -// 1. api -arr.flat(Infinity) -// 2. 递归 -const arr = [1, 2, 3, [4, [5, 6], [7, [8, [9]]]], 10] -const flatten = arr => { - const res = [] - // 定义递归遍历 入参为数组 过程中加入 res - const traverse = arr => { - for (let i = 0; i < arr.length; ++i) { - if (Array.isArray(arr[i])) { - traverse(arr[i]) - } else { - res.push(arr[i]) - } - } - } - traverse(arr) - return res -} -// 3. reduce -const flatten = arr => - arr.reduce((t, v) => (Array.isArray(v) ? t.push(...flatten(v)) : t.push(v), t), []) -``` diff --git "a/content/posts/algorithm/z-others/\344\275\215\350\277\220\347\256\227.md" "b/content/posts/algorithm/z-others/\344\275\215\350\277\220\347\256\227.md" deleted file mode 100644 index 301adfc..0000000 --- "a/content/posts/algorithm/z-others/\344\275\215\350\277\220\347\256\227.md" +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: '位运算' -date: 2022-09-24T23:05:33+08:00 -tags: [JavaScript] ---- - -位运算,对二进制进行操作,有时候更加简练,高效,所以是很有必要学习和了解的。 - -> 基础的怎么变化的就不赘述了,工科生学 C 语言应该都学过吧,记住负数是以补码形式存在即可,补码 = 反码 + 1。 - -## 常用操作 - -- `&` (有 0 为 0): 判断奇偶性: `4 & 1 === 0 ? 偶数 : 奇数` -- `|` (有 1 为 1): 向 0 取整: `4.9 | 0 === 4`; `-4.9 | 0 === -4` -- `~` (按位取反): 加 1 取反: `!~-1 === true`,只有-1 加 1 取反后为真值; - `~~x`也可以取整或把 Boolean 转为 0/1 -- `^` : 自己异或自己为 0: `A ^ B ^ A = B` 异或可以很方便的找出这个单个的数字(也叫无进位相加) -- `x >> n` :`x / 2^n`: 取中并取整 `x >> 1` -- `x << n` :`x * 2^n` -- `x >>> n`: 无符号右移,有个骚操作是在 splice 时,`x >>> 0`获取要删除的索引可以用这个来避免对 -1 的判断,因为 `-1 >>> 0` 符号位的 1 会让右移 0 位后变成了一个超大的数字 42 亿多... - -### 技巧 - -1. `n & (n-1)`:消除二进制中 n 的最后一位 1 - - [191. 位 1 的个数](https://leetcode.cn/problems/number-of-1-bits/) - - ```js - /** - * @param {number} n - a positive integer - * @return {number} - */ - var hammingWeight = function (n) { - let res = 0 - while (n) { - n &= n - 1 - res++ - } - return res - } - ``` - - [231. 2 的幂](https://leetcode.cn/problems/power-of-two/) - - ```js - /** - * @param {number} n - * @return {boolean} - */ - var isPowerOfTwo = function (n) { - if (n <= 0) return false - // 2的n次方 它的二进制数一定只有一个1 去掉最后一个1应该为0 - return (n & (n - 1)) === 0 - } - ``` - -2. `n & (~n + 1)`,提取出最右边的 1,这个式子即:n 和它的补码与。等价于 `n & -n` - -3. `A ^ A === 0`:找出未成对的那个数字 - - [268.丢失的数字](https://leetcode.cn/problems/missing-number/submissions/) - - ```js - /** - * @param {number[]} nums - * @return {number} - */ - var missingNumber = function (nums) { - const n = nums.length - let res = 0 - for (let i = 0; i < n; ++i) { - res ^= nums[i] ^ i - } - res ^= n - return res - } - ``` - -## 补充:异或的运用 - -### 一组数,只有一个数出现了一次,其他都是偶数次,找出这个数 - -```java -int[] arr = new int[]{2, 1, 5, 9, 5, 4, 3, 6, 8, 12, 9, 6, 7, 3, 4, 2, 7, 1, 8}; - -/** - * 找到唯一数字 - * - * @param arr - * @return - */ -public static int findOdd(int[] arr) { - int res = 0; - for (int num : arr) { - res ^= num; - } - return res; -} -``` - -### 进阶:一组数,只有 2 个数出现了一次,其他都是偶数次,找出这 2 个数 - -lc.260 - -```java -/** - * 根据上一题可以很容易得到 a^b 的值,问题在于怎么找到这两个数字 - * 1. 异或特性:相同为 0,不同为 1。所以可以找到 a^b 上的一位 1 -- rightOne 来区分 a 和 b - * 2. rightOne 不断与每个数 &,结果不是 0 就是 rightOne 自身,也就可以把原来的数分为两组,相同的数一定进入一组 - * 3. 用另一个变量去与一组数再异或,就能剥离出一个不同的数,另一个数也就很容易得到了。 - * - * @param arr - * @return - */ -public static int[] findTwoOdd(int[] arr) { - int var = 0; // 找到两个单独的数; a^b 的值 - for (int num : arr) { - var ^= num; - } - - // 找到最右边的 1 a&(~a + 1) 或者 a & -a - int rightOne = var & -var - - int a = 0; - for (int num : arr) { - // num 与 rightOne 的结果只有两种 0 或者 rightOne 自身,根据这个区分开两组数 - if ((rightOne & num) == 0) { - a ^= num; - } - } - - int b = a ^ var; - - return new int[]{a, b}; -} -``` - -- 取最右边为 1 的数:`n & -n` 或者 `n & (~n + 1)` -- 消灭最右边的 1:`n & (n - 1)`,可以用来计算整数的二进制 1 的个数 - -## 其他位运算的技巧(待学习) - -- [Bit Twiddling Hacks](http://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel) diff --git "a/content/posts/algorithm/z-others/\345\233\236\346\226\207\347\233\270\345\205\263.md" "b/content/posts/algorithm/z-others/\345\233\236\346\226\207\347\233\270\345\205\263.md" deleted file mode 100644 index 1cd8a8f..0000000 --- "a/content/posts/algorithm/z-others/\345\233\236\346\226\207\347\233\270\345\205\263.md" +++ /dev/null @@ -1,130 +0,0 @@ ---- -title: '回文相关' -date: 2022-12-19T17:22:17+08:00 -draft: true ---- - -- 判断一个回文串从两边往中间来判断 -- 寻找回文串从中间往两边找,中心拓展法 - -### [647. 回文子串](https://leetcode.cn/problems/palindromic-substrings/description/) - -统计回文子串的数据,这道题其实常规思路是中心拓展法,往动态规划上不太好想,因为此题需要定义的是一个 Boolean 类型的二维 dp。 - -- 状态:区间[i:j] -- 选择:由内而外 'ababa',指针 i 和 j 相等时,得看 i+1 和 j-1 - -```js -/** - * @param {string} s - * @return {number} - */ -var countSubstrings = function(s) { - const m = s.length - // 定义dp[i][j] 表示 s[i:j] 是否是回文子串 - const dp = Array.from(new Array(m), () => new Array(m).fill(false)) - // 确定转移方程 只有当s[i] == s[j] 的时候, dp[i][j] 才取决于 dp[i+1][j-1] - // 根据[i,j] [i+1,j-1]的相对位置确定遍历顺序, i 应当倒着遍历 - let res = 0 - for(let i = m - 1; i >= 0; --i) { - for(let j = i; j < m; ++j) { - if(s[i] === s[j] && ( j-i+1 <= 2 || dp[i+1][j-1] == true) ) { - dp[i][j] = true - res++ - } - } - } - return res -}; -``` - -这里还有个小注意点是 `j-i+1 <= 2`,它的意思是区间 `[i:j]` 的长度小于等于 2,也就是 'a' || 'aa',那么对应的 dp[i][j] 都应该为 true;另外,它虽然是 或 的关系,但是得在 dp[i+1][j-1] 之前,否则会引起边界报错。 - -另外再使用一下常规的中心拓展法来解决一下这道题: - -```js -/** - * @param {string} s - * @return {number} - */ -var countSubstrings = function(s) { - let res = 0 - const m = s.length - for(let i = 0; i < m; ++i) { - // 分别以i为中心和以i,i+1为中心 - res += getNum(i, i, m, s) - res += getNum(i, i + 1, m, s) - } - return res -}; - -function getNum(l, r, m, str) { - let res = 0 - while(l >= 0 && r < m && str[l] === str[r]) { - l-- - r++ - res++ - } - return res -} -``` - -### [5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/description/) - -这道题也是可以用动态规划和中心拓展法去解决,就不多说了,开干。 - -```js -/** - * @param {string} s - * @return {string} - */ -var longestPalindrome = function(s) { - let res = '' - for(let i = 0; i < s.length; ++i) { - // 可能用一个字符串为中心或者两个字符串为中心 - const s1 = getPalindrome(s, i, i) - const s2 = getPalindrome(s, i, i + 1) - const len = s1.length > s2.length ? s1 : s2 - res = len.length > res.length ? len : res - } - return res -}; - -function getPalindrome(s, l, r) { - while(l >= 0 && r < s.length && s[l] === s[r]) { - l-- - r++ - } - return s.substring(l + 1, r) -} -``` - -### [516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/solutions/) - -因为不是连着的,所以中心拓展法没办法施展,只能动态规划了。 - -```js -/** - * @param {string} s - * @return {number} - */ -var longestPalindromeSubseq = function(s) { - const m = s.length - // 定义dp[i][j] 表示 s[i:j] 中最长回文子序列的长度 - // 转移方程 s[i] == s[j] dp[i+1][j-1] + 2 否则 dp[i+1][j] 和 dp[i][j-1] 取较长的那个 - const dp = Array.from(new Array(m), () => new Array(m).fill(0)) - // base case - for(let i = 0; i < m; ++i) dp[i][i] = 1 // 单个字符自身就是回文串 - // 依赖于i+1,j-1 所以 i 需要倒着遍历 - for(let i = m - 1; i >= 0; --i) { - for(let j = i + 1; j < m; ++j) { - if(s[i] == s[j]) { - dp[i][j] = dp[i+1][j-1] + 2 - }else { - dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) - } - } - } - return dp[0][m-1] -}; -``` diff --git "a/content/posts/algorithm/z-others/\345\274\202\344\275\215\350\257\215.md" "b/content/posts/algorithm/z-others/\345\274\202\344\275\215\350\257\215.md" deleted file mode 100644 index 337ec1d..0000000 --- "a/content/posts/algorithm/z-others/\345\274\202\344\275\215\350\257\215.md" +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: '异位词' -date: 2022-10-11T21:42:41+08:00 -draft: true ---- - -异位词一般主要是考验 HashMap 数据结构的使用和计数的技巧。 - -### [49.字母异位词分组](https://leetcode.cn/problems/group-anagrams/?favorite=2cktkvj) - -这道题可以对每个字符串排序得出结果,但是时间复杂度较高。 - -使用计数的技巧会更加简单,因为异位词之间每个字母出现的频率一定是一样的,使用这个频率去做 Map 的键,就可以很容易找到它的异位词了。 - -```js -/** - * @param {string[]} strs - * @return {string[][]} - */ -var groupAnagrams = function (strs) { - const map = {} - for (const s of strs) { - const count = new Array(26).fill(0) - for (const c of s) { - count[c.charCodeAt() - 'a'.charCodeAt()]++ // 制作键 - } - map[count] ? map[count].push(s) : (map[count] = [s]) - } - return Object.values(map) -} -``` - -是不是有点计数排序那个味儿了?👻 - -### [242.有效的字母异位词](https://leetcode.cn/problems/valid-anagram/) - -```js -/** - * @param {string} s - * @param {string} t - * @return {boolean} - */ -var isAnagram = function (s, t) { - const count1 = new Array(26).fill(0) - for (const c of s) { - count1[c.charCodeAt() - 'a'.charCodeAt()]++ - } - const count2 = new Array(26).fill(0) - for (const c of t) { - count2[c.charCodeAt() - 'a'.charCodeAt()]++ - } - return count1.toString() === count2.toString() -} -``` diff --git "a/content/posts/algorithm/z-others/\346\225\260\347\273\204\351\201\215\345\216\206\346\214\207\345\215\227.md" "b/content/posts/algorithm/z-others/\346\225\260\347\273\204\351\201\215\345\216\206\346\214\207\345\215\227.md" deleted file mode 100644 index 8f52944..0000000 --- "a/content/posts/algorithm/z-others/\346\225\260\347\273\204\351\201\215\345\216\206\346\214\207\345\215\227.md" +++ /dev/null @@ -1,162 +0,0 @@ ---- -title: '数组遍历指南' -date: 2023-03-28T14:20:14+08:00 -tags: [JavaScript] -draft: true ---- - -本文由浅入深总结数组的各种花式遍历技巧,欢迎品尝 👻 - ---- - -## 一维数组遍历 - -```TS -const Arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9] -for (let i = 0; i < Arr.length; ++i) console.log(Arr[i]) -for (let i = Arr.length - 1; i >= 0; --i) console.log(Arr[i]) -``` - -很简单对吧?但是如果让你正着、倒着遍历区间 `[3, 6]`,你能不假思索的就知道怎么写吗? -写惯了对整个数组遍历的我反正是最初没有反应过来...但其实也很简单: - -```TS -for (let i = 3; i < 7; ++i) console.log(Arr[i]) // 即 i <= 6 -for (let i = 6; i >= 3; --i) console.log(Arr[i]) -``` - -重点就是 `循环不变式` 值的确定,对于区间 `[l, r]`:。 - -- 正序,`i < (r + 1)` 即 `i <= r` -- 倒序,`i >= l` - -那么如果某个闭区间变成开区间呢?稍加变化即可,不再赘述 - -为什么说这么简单的遍历也需要拿出来讲,主要是根据我个人的经验,做算法题对于区间的遍历场景太频繁了,如果每次在这种地方还要绕一下而不是条件反射地直接写出来,挺浪费资源的(时间,脑细胞),在后面二维数组的遍历中,理解区间的遍历就不会犯迷糊,在动态规划的遍历,降维遍历转换就能够如鱼得水了。 - -## 二维遍历数组 - -一维数组没什么花样,主要就是理解 `区间`,理解 `循环不变式`,二维就相对而言多一些了。 - -先准备好一个 `5\*5`,填充 `1~25` 的数组: - -```TS -/** - * [ - * [ 1, 2, 3, 4, 5 ], - * [ 6, 7, 8, 9, 10 ], - * [ 11, 12, 13, 14, 15 ], - * [ 16, 17, 18, 19, 20 ], - * [ 21, 22, 23, 24, 25 ] - * ] - */ -const Arr: number[][] = Array.from(Array(5), (_, i) => Array(5).fill(0)).map( - (row, rowIndex) => row.map((_, colIndex) => rowIndex * 5 + colIndex + 1) -) - -const m = Arr.length -``` - -普通的遍历无论正序/倒序,应该谁都会,就不赘述。 - -### 螺旋遍历 - -螺旋遍历,和 `BFS` 有一点点像,需要 `while` 和 `for` 配合,同时需要四个边界来进行收缩。 - -```TS -let count = 1 -let top = 0, - left = 0, - bottom = m - 1, - right = n - 1 -while (count <= 25) { - // 遍历顶部的行 区间 [left, right] - for (let i = left; i <= right; ++i) { - console.log(Arr[top][i]) - count++ - } - top++ - // 遍历右边的列 [top, bottom] - for (let i = top; i <= bottom; ++i) { - console.log(Arr[i][right]) - count++ - } - right-- - // 倒序遍历底部的行 [left, right] - for (let i = right; i >= left; --i) { - console.log(Arr[bottom][i]) - count++ - } - bottom-- - // 倒序遍历左侧的列 [top, bottom] - for (let i = bottom; i >= top; --i) { - console.log(Arr[i][left]) - count++ - } - left++ -} -``` - -上方是正序螺旋遍历,逆序就不用多说了吧?知道遍历遍历区间,一切都很 easy~ - -顺带提一嘴 `while` 的循环不变式: `count <= 25`,因为 `count` 初始化为 1,遍历总区间为 `[1, 25]` 所以用 `<=`,如果 `count` 初始化为 0, 那么就 `< 25` 了。 - -### 斜向遍历 - -- 主对角线:左上角到右下角,坐标 i === j -- 副对角线:右上角到左下角,坐标 i + j === len - 1 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202303291708525.png) 如图,按照这样的顺序怎么遍历呢? - -```TS -// 数组大小为 5 * 5, 斜线对应索引为 [1, 4],所以外层for遍历斜线 -for (let l = 1; l < m; ++l) { - // 接着分别确定 i 和 j坐标,这个需要找规律(数学归纳法嘛😂): - // 1. i 每次是减少 1 的, 区间为 [0,m-1-l] - // 2. j 不难发现线 l 与坐标 i,j 的关系: j = l + i - for (let i = 0; i < m - l; ++i) { // l yueda i de youbianjie yue xiao - const j = l + i - console.log(Arr[i][j]) - } -} -// 若想倒着遍历只需要对for循环的条件稍微变化一下即可,不再赘述 -``` - -再来看看下半部分: - -```TS -for(let l = 1; l <= m - 1; ++l) { - for(let j = 0; j <= m - 1 - l; ++j) { // 仅仅是 i 和 j 调换一下位置即可 - const i = l + j - console.log(Arr[i][j]) - } -} -``` - -那么副对角线呢? - -```TS -// 依然是 [1, m-1] 条线段 -for(let l = 0; l <= m - 1; ++l) { - for(let i = 0; i <= l; ++i) { // 不同的是这里 - const j = l - i // 和这里 - console.log(Arr[i][j]) - } -} -``` - -其实总结起来很简单: - -- 主对角线 `i - j` 是固定值 -- 副对角线 `i + j` 是固定值 - -而固定值就是线段 `l`,找出三者的关系就很容易了。 - -## 补充一点经验 - -- [485.最大连续 1 的个数](https://leetcode.cn/problems/max-consecutive-ones/),简单题,就是 max 更新过程最后的一次是有可能遍历到结束,从而触发不到更新,最后再与状态累加器进行一次比较即可 -- [495.提莫攻击](https://leetcode.cn/problems/teemo-attacking/),对区间理解清楚,就不会迷糊,区间截取,添加增量即可 -- [414.第三大的数](https://leetcode.cn/problems/third-maximum-number/),一般第 k 大的数可以用优先队列来搞定,这里只有 3,那么直接利用三个状态值的方式来解决即可 -- [516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/),推导出状态转移方程后,根据依赖关系确定遍历方向/方式,可以使用斜向遍历 - -> 读完本文,相信你对数组的各种遍历能更加游刃有余,加油,打工人! diff --git "a/content/posts/daily/Mac\344\270\212\347\232\204\351\253\230\346\225\210\350\275\257\344\273\266\344\270\216\351\205\215\347\275\256.md" "b/content/posts/daily/Mac\344\270\212\347\232\204\351\253\230\346\225\210\350\275\257\344\273\266\344\270\216\351\205\215\347\275\256.md" deleted file mode 100644 index 23bbfad..0000000 --- "a/content/posts/daily/Mac\344\270\212\347\232\204\351\253\230\346\225\210\350\275\257\344\273\266\344\270\216\351\205\215\347\275\256.md" +++ /dev/null @@ -1,225 +0,0 @@ ---- -title: 'Mac上的高效软件与配置' -date: 2022-09-13T11:24:00+08:00 -tags: [] -categories: [tool] -weight: 1 ---- - -**工欲善其事,必先利其器 🥷** - -_文章取自本人日常使用习惯,不一定适合每个人,如您有更好的提效工具或技巧,欢迎留言 👏🏻_ - -## 软件推荐 - -### Homebrew - -[官网](https://brew.sh/) - -懂得都懂,mac 的包管理器,可以直接去官网按照提示安装即可。 -安装完成后记得替换一下镜像源,推荐腾讯[镜像源](https://mirrors.cloud.tencent.com/)。 - -```sh -# 替换brew.git -cd "$(brew --repo)" -git remote set-url origin https://mirrors.cloud.tencent.com/homebrew/brew.git - -# 替换homebrew-core.git -cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core" -git remote set-url origin https://mirrors.cloud.tencent.com/homebrew/homebrew-core.git -``` - -
-如果没有 🪜,可以使用国内大神的脚本傻瓜式安装: - -```sh -# 按照提示操作下去即可 -/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" -``` - -
- -### oh-my-zsh - -直接点击[官网](https://ohmyz.sh/)安装即可。 - -`~/.zshrc` 配置文件的部分配置: - -```sh -# zsh theme;default robbyrussell,prefer miloshadzic -ZSH_THEME="miloshadzic" -# plugins -plugins=( - # 默认的,配置了很多别名 ~/.oh-my-zsh/plugins/git/git.plugin.zsh - git - # 语法高亮 - # https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/INSTALL.md#oh-my-zsh - zsh-syntax-highlighting - # 输入命令的时候给出提示 - # https://github.com/zsh-users/zsh-autosuggestions/blob/master/INSTALL.md#oh-my-zsh - zsh-autosuggestions -) - -# 让terminal标题干净 -DISABLE_AUTO_TITLE="true" -``` - -
-当VsCode终端出现git乱码问题,添加以下代码进 `~/.zshrc`: - -```sh -# solve git messy code in vscode terminal -export LC_ALL=en_US.UTF-8 -export LANG=en_US.UTF-8 -export LESSHARESET=utf-8 -``` - -
- -### alfred(废弃) - -
-选择使用 raycast 平替 alfred - -~~懂得都懂,这个是 mac 上的效率神器了,剪贴板、搜索引擎、自动化工作流等等就不多说了,网上教程很多。~~ - -~~分享一下平时使用的脚本吧:~~ - -~~- [VsCode 快速打开项目](https://github.com/alexchantastic/alfred-open-with-vscode-workflow),别再用手拖了,直接`code 文件夹名` 不香嘛 🍚~~ -~~- [CodeVar](https://github.com/xudaolong/CodeVar),作为程序员起名字是个头疼事,交给它 👈🏻~~ -~~- [markdown table](https://github.com/crispgm/alfred-markdown-table),用 vscode 写 markdown 我想只有 table 最让人厌烦了吧哈哈~~ -~~- [alfred-github-repos](https://github.com/edgarjs/alfred-github-repos),github 快捷搜索~~ -~~- [alfred-emoji](https://github.com/jsumners/alfred-emoji) emoji 表情~~ - -
- -### Raycast - -[官网](https://www.raycast.com/) - -对比 **alfred**, 我感觉 **Raycast** 更加现代化,同时也更加符合我的需求,插件也都比较新,集成了 **chatgpt**。so,我毫不犹豫的投入了它的怀抱。 - -它的插件生态较好,使用起来也相当简单,安装完成后,可以去设置中设置 **别名** 或者 **快捷键**。 - -插件推荐(直接 store 里搜即可): - -- Visual Studio Code Recent Projects,vscode 快读打开项目 -- Easy Dictionary,翻译单词 -- emoji -- IP-Geolocation 查询 IP -- Github - -### Karabiner Elements - -[下载地址](https://karabiner-elements.pqrs.org/) - -用这个软件我是为了使用 F19 键,来丰富我的快捷键操作~💘 - -点击 [Change right_command to F19](https://ke-complex-modifications.pqrs.org/?q=F19)。进入页面后,直接 `import`,然后到 `Karabiner Elements` 的 `complex modifications` 内添加规则即可。 - -不得不说体验真的完美啊~~~ 🥳 - -### 其他软件 - -- `clashX`,🪜 工具,[github 地址](https://github.com/yichengchen/clashX),选择它是因为好用,而且支持了 apple chip - [clasX 科学上网教程](https://merlinblog.xyz/wiki/ClashX.html),很简单,但是需要提前购买 🪜 哦。 -- `iShot Pro`,截图、贴图软件,功能较全,目前为止很好用,AppStore 下载 -- `keka`,目前用过的 mac 上最好用的解压缩软件,[下载地址](https://www.keka.io/en/),AppStore 也有,不过是收费的,有条件建议支持一下 -- `IINA`,干净好用的播放器,[下载地址](https://iina.io/) -- `Downie 4`,下载视频神器,[下载地址](https://software.charliemonroe.net/downie/),这个我支持了正版~ -- `PicGo`,图床工具。[github 地址](https://github.com/Molunerfinn/PicGo) -- `Dash`,汇集了计算机的各种文档,配合 Alfred 查起来特别方便,[下载地址](https://kapeli.com/dash),这个我也支持了正版~ -- `AppCleaner`,干净卸载软件,这个更较小,支持 M1(推荐),[下载地址](https://freemacsoft.net/appcleaner/)。(更新:用了 raycast 后,此软件好像有点多余了哈哈) - -欢迎路过的兄弟留言补充 👏🏻👏🏻👏🏻 - -### 字体 - -强迫症,个人目前最喜欢的字体是 `inconsolata`,可以保证两个英文和一个汉字对齐。 -点击[inconsolata](https://fonts.google.com/specimen/Inconsolata)进去下载安装即可。 - -
-另外,连体字可以选择 Fira Code -如果使用下方命令安装不上,建议去 [github 地址](https://github.com/tonsky/FiraCode) 下载下来后手动安装。 - -```sh -brew tap homebrew/cask-fonts -brew install --cask font-fira-code -``` - -
- -## vim 配置 - -对于习惯了 mac 快捷键 `ctrl + f/b/a/e/n/p` 的我来说,vim 在插入模式下,鼠标光标的控制太难用了,好在可以修改配置解决: - -1. 先创建配置文件 - -```sh -# 如果没有,先创建 .vimrc -touch ~/.vimrc -``` - -2. 写入配置(更多配置请自查) - -```sh -syntax on "语法高亮" -set number "显示行号" -set cursorline "高亮光标所在行" -set autoindent "回车缩进跟随上一行" -set showmatch "高亮显示匹配的括号([{和}])" - -"配置插入模式快捷键" -inoremap -inoremap -inoremap -inoremap -inoremap -inoremap -inoremap -inoremap -``` - -## 前端开发环境配置 - -### fnm - -之前有用过一段时间 `nvm`,咋说呢,慢。。。后来发现了 `fnm` 这个好东西,Rust 打造,相信前端一听到这个大名就一个反应,快! - -[fnm github](https://github.com/Schniz/fnm) - -```sh -brew install fnm -# 根据官网提示,把下方代码贴进对应shell配置文件 .zshrc -eval "$(fnm env --use-on-cd)" - -# 安装不同版本node -fnm install version -# 设置默认node -fnm default version -# 临时使用node -fnm use version - -# 查看本地已安装 node -fnm ls -# 查看远程可安装版本 -fnm ls-remote -``` - -### nrm - -```sh -npm i nrm -g -# nrm 常用命令 -nrm ls -nrm use -nrm add [name] [url] # 添加新的镜像源(比如公司的私有源) -nrm del [name] -``` - -### Vscode monokai pro 主题 license - -```txt -id@chinapyg.com -d055c-36b72-151ce-350f4-a8f69 -``` diff --git "a/content/posts/daily/Vite\350\207\252\347\224\250\346\211\213\345\206\214.md" "b/content/posts/daily/Vite\350\207\252\347\224\250\346\211\213\345\206\214.md" deleted file mode 100644 index 3c93aa7..0000000 --- "a/content/posts/daily/Vite\350\207\252\347\224\250\346\211\213\345\206\214.md" +++ /dev/null @@ -1,174 +0,0 @@ ---- -title: 'Vite自用手册' -date: 2024-05-08 -lastmod: -series: [] -categories: [tool] -weight: ---- - -**本文基于 Vite 5.2.11,node18/20+,按照[官方文档](https://vitejs.dev/)做一个个人的小总结,涵盖主要理念、注意点和主要功能配置。** - ---- - -## 开始 - -### 开发 & 生产 - -- Vite 开发基于 ES module & esbuild - - esm 依赖模块地图,静态分析,便于动态加载,能让 vite 能做到最小范围的 HMR 的基础 - - esbuild 进行**依赖预构建**提升性能 -- Vite 打包基于 Rollup,不使用 esbuild 是因为与插件 API 不兼容,后续有 rust 版本的 Rollup -- Rolldown - -> Vite 以 原生 ESM 方式提供源码,只需要在浏览器请求源码时进行转换并按需提供源码 - -> 为什么生产环境仍然需要打包? -> -> 1. 如果不打包,浏览器端处理 ESM 嵌套导入会导致额外的网络往返,浪费性能 -> 2. 生产打包可以进行 tree shaking,懒加载和代码分割(可以获得更好的缓存) - - - - -#### ESM 必知必会 - -ESM 自动采用严格模式 - -- 浏览器端创建 ESM:``,会延迟执行脚本,通过 CORS 请求外部 JS -- Node 环境创建 ESM:文件名后缀 `.mjs` 或者 package.json 添加 `"type":"module"` - -在 ESM 中可以动态导入模块,由此也产生了 top-level await 提案: 将整个文件模块视为一个巨大的 async 函数,打破了 await 必须跟随在 async 内的定律:`const moduleA = await import('path/module.mjs')` - - - -> ESM 中的 `import.meta` 是特殊的对象,它的原型对象是 null,原生包含了: -> -> - `url`,类似于 node 环境中的 `__filename`,不同的是 `import.meta.url` 可以分别应用在浏览器端和 node 端,浏览器端返回带协议域名的完整的访问路径, node 端返回 `file://absolutePath`;而 `__filename` 没有 `file://` 前缀。一般可以配合 `new URL()` 使用,如 `new URL('./worker.js', import.meta.url)`,但 SSR 中无法使用 -> - `resolve(file)`,用于解析 file 路径,第二个参数还在实验性阶段,可以看 [node 官方文档](https://nodejs.cn/api-v14/esm.html#importmetaresolvespecifier-parent) - - - -### 核心:依赖 & 源码 - -Vite 把模块区分为 **依赖** 和 **源码** 两类。 - -- 依赖:开发时基本不会变动的 JS。对此,Vite 使用 esbuild 进行预构建。 -- 源码:会进行编辑的文件,包括很多类型,如 JSX、css、Vue 等,但并不会加载所有源码,只是加载用到的部分,如根据路由进行拆分了的代码模块。 - -> Vite 利用 HTTP 头来加速整个页面的重新加载: -> -> - **依赖**模块的请求则会通过 Cache-Control: max-age=31536000,immutable 进行**强缓存**,因此一旦被缓存它们将不需要再次请求。 -> - **源码**模块的请求会根据 304 Not Modified 进行**协商缓存** - -### 依赖预构建 - -> 依赖预构建仅适用于开发模式;在生产构建中,将使用 @rollup/plugin-commonjs。 - -[详细见 vite 文档](https://cn.vitejs.dev/guide/dep-pre-bundling.html),需要了解的是: - -1. `import { debounce } from 'lodash'` 原生 ESM 中这样引入依赖包是会报错的,其他的打包器是利用了 npm 的依赖查找算法。因此,Vite 使用 `esbuild` 进行了依赖预构建,主要**把一些 CJS/UMD 规范的依赖转为 ESM 模块**,同时**重写依赖路径为绝对路径**如 `/node_modules/.vite/deps/my-dep.js?v=f3sf2ebd` -2. 在预构建过程中,把包预构建成单个模块,就可以减少 http 请求,提升性能,比如 `lodash-es` 这个库 - -- 系统缓存 - Vite 将预构建的依赖项缓存到 `node_modules/.vite` 中,当 `NODE_ENV`,`vite.config.js`,`xxx-lock.json` 或`补丁文件夹的修改时间`的某一项发生变动,触发重新预构建。 -- 浏览器缓存 - 依赖强缓存,源码协商缓存 - - 一个小点:vite 的 `index.html` 在项目最外层而不是在 public 文件夹内,`index.html` 所在位置被 Vite 设定为 root 根目录。`vite build` 构建生产版本时 。默认情况下,它使用 `/index.html` 作为其构建入口点。 - -### HMR - -[Vite 提供了原生 ESM HMR api](https://cn.vitejs.dev/guide/api-hmr.html),对于前端同学来说,vite 对常用的 react 和 vue 都做了集成。对应的插件是: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react) -- [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue) - -### TS 支持 - -Vite **仅支持 ts 转译**(依赖 esbuild),并不会进行类型检查。[tsconfig 注意配置](https://cn.vitejs.dev/guide/features#typescript-compiler-options) - -TS 默认也不会识别引入的静态资源类型,在 Vite 中需要特殊处理(二选一): - -- src 下添加 `d.ts` 文件 -- 或者在 tsconfig.json 中添加 `"types": ["vite/client"]` - -```ts -// 1. d.ts -/// - -// 2. tsconfig -{ - "compilerOptions": { - "types": ["vite/client"] - } -} -``` - -然后就可以拿到导入资源的类型以及`import.meta.ev`、`import.meta.hot`两个重要的类型。 - - - -### 构建优化 - -[Vite 默认自动应用一部分构建优化](https://cn.vitejs.dev/guide/features#build-optimizations) - -- css 代码分割,`build.cssCodeSplit` 默认为 true,将会对异步的 chunk 的 css 文件提取,在文件加载时通过 link 引入;设为 false ,所有的 css 将生成一个文件。 -- 预加载指令生成 -- 异步 chunk 加载优化,就是预构建处理原生 ESM 嵌套导入消耗过多网络性能的问题 - -> Vite 有很多精简优化,比如使用了 `@vitejs/plugin-react`,应该避免配置 Babel 选项,这样就可以构建期间只使用 esbuild。[使用更少或更原生化的工具链](https://cn.vitejs.dev/guide/performance.html#use-lesser-or-native-tooling) - -### CLI - -[直接看文档](https://cn.vitejs.dev/guide/cli.html) - -- vite -- vite build -- vite preview --port 8888 # 这个挺好,可以预览打包后的产物 - -### 插件 - -[直接看文档](https://cn.vitejs.dev/guide/api-plugin.html) - -Vite 插件基于 Rollup 的插件系统,[Vite 的社区插件列表](https://github.com/vitejs/awesome-vite#plugins)。与大多数打包工具的插件一样,在 `devDependencies` 安装,在配置文件中引入执行。值的注意的是:有可能存在为了与 Rollup 插件兼容的情况,需要强制设定插件的执行顺序:[看这里](https://cn.vitejs.dev/guide/using-plugins.html#enforcing-plugin-ordering) - -插件配置中可以利用 `apply: 'serve' | 'build'` 来判断插件是在开发阶段还是生产阶段使用,不设置则默认都会使用到。 - -```js {open=true, lineNos=false, wrap=false, header=true, title="配置插件"} -export default defineConfig({ - plugins: [ - { - ...pluginXxx(), - apply: 'build' // 只在生产模式使用 - } - ] -}) -``` - -### 静态资源 - -1. [publicDir,编译不用 hash 处理(保持原文件名),不会被源码引用的资源,应当放在该文件夹下,默认为 public](https://cn.vitejs.dev/config/shared-options.html#publicdir),注意:public 中的资源不应该被 JavaScript 文件引用,其次引用路径应该为根路径,如 `public/a.text` 应该被引用为 `/a.txt`。与之对应的 `build.assetsDir` 是配置生产静态资源的存放路径,默认为 assets -2. [base,这个配置类似于 webpack 的 publicPath,指定静态资源访问路径的,比如生产环境的访问路径一般都不会直接把包放在服务器根目录下,此时就需要单独配置了](https://cn.vitejs.dev/config/shared-options.html#base) -3. [assetsinclude,可以配置不识别类型的拓展](https://cn.vitejs.dev/config/shared-options.html#assetsinclude) -4. [build.assetsInlineLimit,可以配置类似 webpack 中 url-loader 的效果,把资源内联为 base64 编码,默认为 4096,4KB](https://cn.vitejs.dev/config/build-options.html#build-assetsinlinelimit) - -### 基本配置 - -[Vite 的所有基础配置、环境配置等见官网,都挺简单的,好上手](https://cn.vitejs.dev/config) - -### 生产构建 - -`build.rollupOptions` 自定义生产构建过程,详细参考 [Rollup 官网配置](https://rollupjs.org/configuration-options/) - -`build.rollupOptions.output.manualChunks` 自定义分块策略,在 vite 中必须使用函数形式,具体参考 [Rollup 配置](https://cn.rollupjs.org/configuration-options/#output-manualchunks),返回的字符串将作为 `output.chunkFileNames` 的 `[name]` 值 - ---- - -## 一个最简单的 vite react ts 配置 - -实际上官网有给出 vite react-ts 的模板。那我这里搞了一个自己常用的技术栈模板,方便使用。 -TODO - - diff --git "a/content/posts/daily/bugs/\346\234\211\350\266\243\347\232\204bug.md" "b/content/posts/daily/bugs/\346\234\211\350\266\243\347\232\204bug.md" deleted file mode 100644 index a10d765..0000000 --- "a/content/posts/daily/bugs/\346\234\211\350\266\243\347\232\204bug.md" +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: '记项目重构中的bug' -date: 2023-10-10T23:40:56+08:00 -lastmod: -tags: [] -series: [] -categories: ['bug🐞'] ---- - -## 背景 -s -我司某个项目一开始是基于 vue3 进行开发的,使用的是开源的 antd UI 组件库,结果开发完了,领导说界面要与公司主产品的 UI 契合,也就是说要改用公司的组件库,然而公司的组件库是基于 react 的,没有办法只能对整个项目进行重构。 - -项目选型:`React@18.2.0 + react-router@6.11.0 + rematch@2.2.0 + typescript@5.0.4 tailwindss@3.3.3`,基于 `webpack5` 搭建了一套脚手架。 - -随后,我就开始了风风火火的重构之路 🔥 - -## 问题 - -1. 重构比我预计的要顺利,这一套技术栈的开发体检直接拉满,比较坑的是公司的组件库中 `TreeSelect` 组件有严重的 bug:从表象看,就是下拉时面板点击无反应,大致定位了下,是下拉框的 `pointer-events: none` 这么个 css 属性的加载时机刚好搞反了你敢信?一度怀疑是不是我哪里用错了。。。或者是我电脑环境(node,os 等)的问题,那么就控制变量进行对比,在同事的电脑上也复现出了这个 bug。 - - 于是我用 cra 脚手架和公司组件库做了个最小 demo project 给到公司组件库的负责人,然而一个月过去了他也没能解决。 - -2. 另外一个就是我今天遇到的一个比较奇葩又有趣的问题:关于 `[...new Set(arr)]`。第一反应,就是一个对数组去重的简单操作罢了。然而,从老项目中拷贝到新项目中使用了`[...new Set(arr)]` 的公共方法却失效了,使得 echarts 图表没有按照预期渲染出来。稍微调试了一下,发现居然是 `[...new Set()]` 的锅--于是我在项目中做了一个实验,打印`[...new Set([1,2,1])]`的输出结果:`[Set(2)]`,WHAT?! 按照我的预期,难道不应该是 `[1,2]` 吗?百思不得其解,我又尝试了另一种写法:`Array.from(new Set([1,2,1]))`,这种就能得到正确的结果,why? - -## 解决 - -1. 针对 TreeSelect 组件,我有注意到公司组件库兼容antd@4.x,于是我对 atnd4 的 TreeSelect 组件进行了单独封装在项目中使用,后续有时间再去详细看看公司组件库对这个组件是怎么写的吧~ -2. `[...new Set(arr)]`,编译的问题,如上,这种写法其实可以直接使用 `Array.from(new Set())` 来代替,但是我这个犟种怎么允许拦路虎呢?发挥我面向 google 编程的能力 😂 由于本人曾对 `babel` 有过深入的学习,猜测大概率应该是编译过程出了问题,或者说使用了高版本的 es 语法?于是我就谷歌了 `[...new Set()]` `babel` 相关的关键词,还真让我找到了一篇文章:[Babel6: loose mode](https://2ality.com/2015/12/babel6-loose-mode.html),里面有这么一段描述:![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202310111331756.png) 简单讲就是:`loose mode打开时的缺点就是 -- 在转译es6语法的时候有较低的风险会产生问题`。好家伙,我兴奋的去查看我的 babelrc 文件,果然 `loose: true`...改为 `false` 后,`[...new Set()]` 的表现就回归正常了。 - ---- - -最后,对 loose mode 这个配置详细地了解了下。PS:这个 API 官网的描述真的绝了~ diff --git a/content/posts/daily/english/en.md b/content/posts/daily/english/en.md deleted file mode 100644 index 09a324e..0000000 --- a/content/posts/daily/english/en.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: '我的英语学习之路' -date: 2023-01-01T10:30:18+08:00 -lastmod: 2023-10-10 -weight: 1 -draft: true ---- - -> Learning English can make me happy~ -- 2023/01/01 - ---- - -## 学习纲领 - -根据以往本人自学编程语言的经验,我深知不能盲目地学习,否则就是英语字典的第一个单词:abandon ~,so,我就先去 YouTube 上找了一些关于学习英语的建议,下面是我的总结。 - - - -### 关于英语思维 - -{{}} - -### 以听为主记单词 - -{{}} - -- 其实那些极个别的词根词缀我认为还是可以记一记的~ -- 另外,个人觉得语境真的也很重要 - -#### 推荐 - -- YouTube 地址:[EnglishClass101](https://www.youtube.com/@EnglishClass101/playlists) -- 手机 app :「墨墨背单词」,按照人的记忆曲线背单词,同时提供发音和例句~ 正在坚持打卡中 👊 - -### 美式发音:无脑信 Lisa 老师 - -#### 推荐 - -- YouTube 地址:[Accurate English](https://www.youtube.com/@AccurateEnglish) - ---- - -### 语法 - -语法这个东西,还是需要系统的学习下的~ 语感好的就忽略吧 😂 - -#### 推荐 - -- [EngVid](https://www.youtube.com/@EngVid/playlists),这里有初中高级语法完成视频教程,也有雅思托福听说写~。 - -#### 时态小总结 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202308251435558.png) - -其中有以下几个注意点: - -1. 动词形式:`past & present` 只需要修改 verb 的形态就好了,`feture` 需要在 verb 前加些料:`be going to`、`will`、`might`,就这么简单与 `aspect` 无关,**`but!`**,有时候 `present simple` 或 `present continuous` 也可以表示将来的意思,需要意会哦~ 比如: `what are you doing this weekend / class starts at 10:00` -2. 分清楚各个 `aspect` 的 `verb forms` 及意义 - - - `simple`,描述事实或习惯(重复的 actions/states) `past & verb & will+verb` - - `continuous`,描述某个时刻正在做(动作未完成或临时的) `was/were & am/is/are & will be + verb-ing` - - `perfect`,潜在意义里连接了两个时间点 `had & have(has) & will have + past`,我的感觉上是到终止日期完成(不一定是个具体的日期) - - - `By yesterday, I had remembered 54 words` 从过去的一个点到另一个过去的点,译:已经.... - - `I have remember 60 words now` 从过去一个点到现在,译:已经... - - `I will have remember 100 words by Sunday` 从现在到将来的一个点,译:将会/会... - - - `perfect continus`,在 perfect 的基础上强调动作未完成或临时的`~ `had & have(has) & will have + been + verb-ing`,有时候可以被 perfect 取代~ - -{{< admonition note >}} -以上是我自己学习后的感悟就记录了下来,其实看文字没啥卵用,重点是一定要理解了 -{{}} - -#### would have; could have; should have(注意发音~) - -这三个是比较常用的 `情态动词 + have + 过去分词` 组合形式,需要理解含义,多说。 - -- `would have`,等价于中文的 `要是...,就/就会...` 的 **`就/就会`** - - `If I had studied harder, I would have gone to a better university` -- `could have`,等价于中文的 `就可以.../本可以.../有可能.../(本)应该.../本来...` - - `If I had more time, I could have finished it/ He could have won it if he hadn't...` - - `They could have mixed up the date/ I could have lost my keys` _这里表猜测_ - - `You could have told me sooner` _这里表批评_ - - `We were really late, and we could have missed our plane, but luckily we just made it` _表示幸运地避免了坏情况_ -- `should have`,等价于中文的 `(本)应该...`, 带有点批评的意思,有时候也表示期望的事情没有发生 - - `You should have call me like you promised` 表示批评 - - `They should been here by now` 表示期待的事情未发生 - ---- - -## 其他推荐 - -- YouTuBe 地址:[LearnEnglishWithTVSeries](https://www.youtube.com/@LearnEnglishWithTVSeries/playlists),看美剧学英语 -- 手机 app:「**voscreen**」,跟随电影配音练听力口语,很方便。PS: 需要有美区 appleID - -## 英语杂志推荐 - -- https://www.economist.com/ -- https://www.vanityfair.com/ -- https://www.rd.com/ -- https://www.newyorker.com/ -- https://www.nationalgeographic.com/ -- https://www.lonelyplanet.com/ - -## 最最最重要的 - -> 坚持坚持还是坚持!不积跬步无以至千里,不积小流无以成江海~ 加油 💪🏻 diff --git "a/content/posts/daily/git\350\207\252\347\224\250\346\211\213\345\206\214.md" "b/content/posts/daily/git\350\207\252\347\224\250\346\211\213\345\206\214.md" deleted file mode 100644 index 136c31b..0000000 --- "a/content/posts/daily/git\350\207\252\347\224\250\346\211\213\345\206\214.md" +++ /dev/null @@ -1,191 +0,0 @@ ---- -title: 'Git 自用手册' -date: 2022-09-19 -lastmod: 2024-05-07 -series: [] -categories: [tool] -weight: ---- - -**本文基于 git version 2.32.0** - ---- - -当然,许多人选择使用 SourceTree 这样的图形界面来管理版本,但我作为一个习惯使用命令行和喜欢简约风格的人,更喜欢在终端中输入命令来进行 Git 相关操作。在这篇文章中,我将分享我这几年来常用的命令和经验。(一些基础的知识就不在本文中赘述了,可以自行网上搜索资料。) - -## 基础配置 - -### 配置别名 - -别名可以极大简化命令号的操作复杂度~~~,是我换电脑或者重做系统后的必做的待办项之一。 - -```sh {open=true, lineNos=false, wrap=false, header=true, title="常用别名"} -# 请直接复制进 terminal 执行一下即可。提高幸福度的别名使用⭐️标记。 -# -------------------------------------------------------------------------------------- -# 常规 -git config --global alias.g git -git config --global alias.c 'config' # g c user.name eric -git config --global alias.cg 'config --global' # g cg user.email eric@gmail.com - -# 查看配置 -g cg alias.cl 'config --list' -g cg alias.cgl 'config --global --list' -g cg alias.cll 'config --local --list' # 查看当前仓库下的 git 配置 - -# 对于大型仓库只 clone 对应分支, g cloneb [bracnchName | tagName] [url] ⭐️ -g cg alias.cloneb 'clone --single-branch --branch' - -g cg alias.st status -g cg alias.ad 'add -A' -g cg alias.cm 'commit -m' -g cg alias.ps push -g cg alias.pso 'push origin' -g cg alias.pl pull -g cg alias.plo 'pull origin' - -g cg alias.cam 'commit --amend -m' # 修改最后一次 commit(⭐️会变更commitId) -g cg alias.can 'commit --amend --no-edit' # 追加修改,不加新 commit(g can ⭐️ 经常使用了属于是) - -# -------------------------------------------------------------------------------------- -# 分支相关 (对于很多新手都不清楚的是:branchName 也只是一个指针!!!s) -g cg alias.br branch -g cg alias.rename 'branch --move' # g rename oldname newname -g cg alias.ck checkout # 带着 HEAD 到处跑~(⭐️ g ck - 快速返回上一个分支,同理 g merge -) -g cg alias.cb 'checkout -b' -g cg alias.db 'branch -d' # 删除分支 -g cg alias.fdb 'branch -D' # 强制删除 -g cg alias.drb 'push origin --delete' # 删除远程 g drb brname ⭐️; 也可以推送一个空本地分支: g pso :brname - -# -------------------------------------------------------------------------------------- -# tag 相关 -# 打 tag: g tag [tagName] -# 推 tag: tag: g pso [tagName] -g cg alias.psot 'push origin --tags' # 推多个 tag -g cg alias.dt 'tag -d' # 删除 tag -g cg alias.drt 'push origin --delete' # 删除远程 tag 也可以推送空tag g pso :refs/tags/[version] - -# -------------------------------------------------------------------------------------- -# 进阶操作 -# 常用 ⭐️,开发到一半要去改 bug 🙅🏻‍♀️ -g cg alias.sta stash -g cg alias.stap 'stash pop' - -g cg alias.rv 'revert' # 反向操作,产生新的 commit -# 下面的 reset 是移动分支指针,并移出之后的 commit,同时还带有一点副作用 -g cg alias.rh 'reset --hard' # 副作用:会重置暂存区和工作区 -g cg alias.rs 'reset --soft' # 副作用:不会重置暂存区和工作区 -# 常用场景: g rs HEAD^ ,想要修改创建了 commit 还未提交的内容 -# g reset --mixed commitId # 副作用:重置暂存区,工作区不变,是 reset 的默认方式 -g cg alias.cp cherry-pick # g cp [commit/brname] 如果是 brname 则是把该分支最新commit合并(再次验证 brname 也就是一个指针~) -# cp 区间 g cp commitA..commitB 把区间 (A, B] 的 commit 都合进来,A 早于 B 的 -# cp 区间 g cp commitA^..commitB 把区间 [A, B] 的 commit 都合进来 -# git reflog 时光机神器~~~~~~~~~~~~~~能查看HEAD指针行走的所有轨迹,包括因为reset而被移出的commit -# 有了 reflog:cherry-pick 轻松找回被删除的 commit,reset 后悔了也可以轻松地回到未来 - -# 变基开启交互模式 g ri cmid -g cg alias.ri 'rebase -i' - -# -------------------------------------------------------------------------------------- -# log 美化 -g cg alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit" -# 根据 commit 内容查找 commit -g cg alias.find "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --grep" -# 根据 commit 用户查找 commit -g cg alias.findby "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --author" - -# -------------------------------------------------------------------------------------- -# 不常用命令(2.23 新增) -# git restore / restore --staged [filename] -# get switch / switch -c [brname] -``` - -> 注意:reflog 查看的是本地历史,在别的电脑上是看不见的,同理如果电脑坏了,那么再也回不到未来了...另外,git reflog 不会永远保持,Git 会定期清理那些 “用不到的” 对象,不要指望几个月前的提交还一直在那里。 - -### 配置用户信息 - -命令简化完后,需要配置下个人信息,我个人习惯是公司的项目都单独配置,全局给自己用。 - -```sh {open=true, lineNos=false, wrap=false, header=true, title=""} -# 全局配置 -g cg user.name 'yourname' -g cg user.email 'yourmail@xx.com' - -# 独立配置 -g c user.name 'yourname' -g c user.email 'yourmail@xx.com' -``` - -### HEAD 知多少 - -说一下个人的认知: - -- git 整个 commit 就是一个多叉树 -- 每一个 branch 就是一条新的分支,它的 branchName 也是一个指针,指向的这条分支上最新的 commit -- 每一个 tag 也都可以看成是对应 commit 的别名 - -而 HEAD 是特殊的指针,指向的是当前所在 commit。平时 checkout 操作的就是 HEAD,而 reset 一般操作的是 branchName(HEAD 被迫跟着一起回退) - -常用的 HEAD 简写 `HEAD^n` 与 `HEAD~n`: - -- HEAD^^^ 等价于 HEAD~3 表示父父父提交 -- HEAD^3 表示的是父提交的第三个提交,即合并进来的其他提交 - ---- - - - -## 提交规范 - -通过 `husky` + `lint-staged` 配合来进行约束,详细配置根据项目来设定。 - -## 常见问题 - -### git log 中文字符乱码 - -当 vscode terminal 内使用 `g lg` 出现中文字符乱码问题, 可以去这么配置 - -```yml {c=false} -# ~/.gitconfig -export LC_ALL=zh_CN.UTF-8 -export LANG=zh_CN.UTF-8 -export LESSHARESET=utf-8 -``` - -### 处理拒绝合并不相关历史 - -`fatal: refusing to merge unrelated histories` - -```sh {lineNos=false} -g plo develop --allow-unrelated-histories -``` - -## Git error on git pull (unable to update local ref) - -```sh -g remote prune origin -``` - -## 参考 - -- [Pro Git 2nd Edition](https://git-scm.com) -- [“纸上谈兵”之 Git 原理](https://mp.weixin.qq.com/s/FSBEM2GqhpVJ6yw9FkxnGA) -- [Git Tools - Submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) -- [Git submodule 子模块的管理和使用](https://www.jianshu.com/p/9000cd49822c) diff --git a/content/posts/daily/new_Life.md b/content/posts/daily/new_Life.md deleted file mode 100644 index c4f32ac..0000000 --- a/content/posts/daily/new_Life.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: 'Change in 2024' -date: 2024-01-03T14:33:13+08:00 -lastmod: -tags: [] -series: [] -categories: [] ---- - -## Hello 2024 - -> Well, I have to admit that it is too inefficient to take notes while studying! - -so, I give up! - ---- - -## Plan - -But I won't stop, I will learn English and Spring Framework quickly this year, and use them in daily life. - -Of course, I will do some algorithm if I have time 😏. - -good luck to me,peace! diff --git a/content/posts/eight-legged essay/base-css/.DS_Store b/content/posts/eight-legged essay/base-css/.DS_Store deleted file mode 100644 index 30f83b3..0000000 Binary files a/content/posts/eight-legged essay/base-css/.DS_Store and /dev/null differ diff --git a/content/posts/eight-legged essay/base-css/BFC.md b/content/posts/eight-legged essay/base-css/BFC.md deleted file mode 100644 index 0e56592..0000000 --- a/content/posts/eight-legged essay/base-css/BFC.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: '块级格式化上下文(BFC)' -date: 2022-09-27T17:11:00+08:00 -tags: [CSS] ---- - -块级格式化上下文,英文全称(Block Formatting Context)。 - -其实我个人的理解呢,就是独立的容器,是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及与其他元素的关系和相互作用。 - -### 创建 BFC - -常见的: - -- 根 html -- position: absolute/fixed -- display: flex/inline-flex/inline-block/grid/inline-grid -- float 不为 none -- overflow 不为 visible 和 clip - -> 最新见 [MDN](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context) - -### BFC 特性 - -- 同一个 BFC 内的元素 `上下 margin` 会发生重叠 -- BFC 元素会计算子浮动元素的高度 -- BFC 元素不会被兄弟浮动元素覆盖 - -对于特性 1,可以把 margin 重叠的元素分别放入不同的 BFC 下即可 -利用特性 2,可以清除浮动 -利用特性 3,可以做自适应两栏布局 diff --git a/content/posts/eight-legged essay/base-css/SC.md b/content/posts/eight-legged essay/base-css/SC.md deleted file mode 100644 index 92fe627..0000000 --- a/content/posts/eight-legged essay/base-css/SC.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: '层叠上下文(SC)' -date: 2022-09-27T17:11:07+08:00 -tags: [CSS] ---- - -### 层叠顺序 - -DOM 发生重叠在垂直方向上的显示顺序: - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/20221006131841.png) - -从最低到最高实际上是 `装饰->布局->内容` 的变化,比如内联才比浮动的高。 - -> 注意,`background/border` 是必须层叠上下文的,普通元素的会高于负 `z-index` 的。 -> `z-index:0` 与 `z-index:auto` 的层叠等级一样,但是层叠上下文有根本性的差异。 - -### 层叠上下文 - -层叠上下文,英文全称(stacking context)。相比普通的元素,级别更高,离人更近。 - -#### 特性 - -- 层叠上下文的层叠水平要比普通元素高 -- 层叠上下文可以阻断元素的混合模式([isolation: isolate](https://www.zhangxinxu.com/wordpress/2016/01/understand-css3-isolation-isolate/)) -- 层叠上下文可以嵌套,内部层叠上下文及其所有子元素均受制于外部的层叠上下文 -- 每个层叠上下文和兄弟元素独立,也就是当进行层叠变化或渲染的时候,只需要考虑后代元素 -- 每个层叠上下文是自成体系的,当元素发生层叠的时候,整个元素被认为是在父层叠上下文的层叠顺序中 - -### 创建 - -- 根 html -- `position` 为 `relative/absolute/fixed` 且具有 `z-index` 为数值的元素 - 目前 chrome 单独 `position: fixed` 也会变成层叠上下文 -- 父 `display: flex/inline-flex`,具有 `z-index` 为数值的子元素(注意是子元素) -- `opacity` 不为 1 -- `transform` 不为 none -- `filter` 不为 none -- 其他 css3 属性,见下方张鑫旭大佬的博客。 - -### 层叠等级 - -同一个层叠上下文中,元素在垂直方向的显示顺序。 - -所有元素都有层叠等级,普通元素的层叠等级由所在的层叠上下文决定,否则无意义。 - -### 规则 - -- 谁大谁上:在同一个层叠上下文领域,层叠等级值大的那一个覆盖小的那一个。 -- 后来居上:当元素的层叠等级一致、层叠顺序相同的时候,在 DOM 流中处于后面的元素会覆盖前面的元素。 - -### 一道经典题 - -改动下方代码,让方框内显示红色。 - -```html - - - - - - - Document - - - -
-
-
-
我是文案
-
-
- - -``` - -其实就是把父元素变成层叠上下文即可。 -parent 为普通元素时,普通 block > 层叠上下文的 background,显示为黄色。 -parent 为层叠上下文的时候(比如 z-index: 0),后来居上,子元素的背景就会覆盖父元素的背景了。 -子元素 z-index 为-1 才能让文案显示出来,因为文案是 inline - -### 参考 - -- [深入理解 CSS 中的层叠上下文和层叠顺序](https://www.zhangxinxu.com/wordpress/2016/01/understand-css-stacking-context-order-z-index/?shrink=1) diff --git a/content/posts/eight-legged essay/base-css/flex.md b/content/posts/eight-legged essay/base-css/flex.md deleted file mode 100644 index 93e39fa..0000000 --- a/content/posts/eight-legged essay/base-css/flex.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: 'flex' -date: 2022-09-27T17:10:53+08:00 -tags: [CSS] ---- - -flex 是 css 界的宠儿,基础的就不记录了,直接参考[Flex 布局教程:语法篇](https://www.ruanyifeng.com/blog/2015/07/flex-grammar.html) - -主要记录下需要注意的地方。 - -### 上下文影响 - -1. flex 会让元素变成 BFC -2. 会让具有 z-index 且值不为 auto 的子元素成为 SC - -### align-items 和 align-content - -- align-items 针对 单行,作用于行内所有盒子的对齐行为 -- align-content 针对 多行,对于 `flex-wrap: no-wrap` 无效,作用于行的对齐行为 - -### flex 的计算 - -flex 能完美自适应大小,是因为有一套计算逻辑,学习一下。 - -```css -/* grow shrink basis */ -/* 1 1 0 */ -flex: 1; -/* 1 1 auto */ -flex: auto; -/* 0 0 auto */ -flex: none; -``` - -> 涉及到计算的比较重要的属性是 `flex-basis`,表示分配空间之前,占据主轴的空间大小,`auto` 表示为元素本身的大小。 -> `flex-basis` 为百分比时,会忽略自身的 width 来计算空间。 - -下面说下大致的计算过程: - -1. 得到剩余空间 = 总空间 - 确定的空间 -2. 得到需要分配的数量 = `flex-grow` 为 1 的盒子数量 -3. 得到单位空间大小 = 剩余空间 / 分配数量 -4. 得到最终长度 = 单位空间 \* 分配数量 + 原来的长度 - -```html -
-
-
-
-
- -``` - -过程如下: - -```m -rest = 600 - 100 - 200 - 0 = 300 -count = 1 -unit = rest / count -total = unit * 1 + 0 -``` - -所以 `flex: 1` 就是占据剩下的所有空间,但是有时候会被内部空间撑开,此时需要加上 `overflow: hidden` 来解决。 - -如果是下面这样的呢? - -```css -.item-1 { - flex: 2 1 10%; -} -.item-2 { - flex: 1 1 10%; -} -.item-3 { - width: 100px; - flex: 1 1 200px; -} -``` - -过程如下: - -```m -rest = 600 - 600 * 0.1 - 600 * 0.1 - 200 = 280 -count = 2 + 1 + 1 = 4 -unit = rest / count = 70 -item1: 600 _ 0.1 + 2 _ 70 = 200 -item2: 600 _ 0.1 + 1 _ 70 = 130 -item3: 200 + 70 270 -``` - -关于缩小的,可以参考 - [这篇文章](https://www.cnblogs.com/liyan-web/p/11217330.html) - -### flex: 1 滚动条不生效 - -当发生 flex 嵌套,多个 flex: 1 时,会产生 flex1 内的元素滚动失效,往往需要加上 `height: 0` 来解决。 - -```html - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -``` - -### justify-content: center 最后一行的对齐问题 - -参考张鑫旭大佬的文章 -- [让 CSS flex 布局最后一行列表左对齐的 N 种方法](https://www.zhangxinxu.com/wordpress/2019/08/css-flex-last-align/) diff --git "a/content/posts/eight-legged essay/base-css/\347\273\217\345\205\270\344\270\211\346\240\217\345\270\203\345\261\200.md" "b/content/posts/eight-legged essay/base-css/\347\273\217\345\205\270\344\270\211\346\240\217\345\270\203\345\261\200.md" deleted file mode 100644 index 1092b98..0000000 --- "a/content/posts/eight-legged essay/base-css/\347\273\217\345\205\270\344\270\211\346\240\217\345\270\203\345\261\200.md" +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: '经典三栏布局' -date: 2022-09-27T17:21:29+08:00 -tags: [CSS] ---- - -学习 BFC 时,知道利用 BFC 特性,可以制作两栏自适应布局,这里复习一下经典的三栏布局吧。 - -### 圣杯布局 - -重点就是利用 container 的 padding 来给杯壁空间。 - -关键代码如下: - -```html - - -
-
-
-
-
-``` - -1. container 提供 padding 占位 -2. float: left 脱离文档流 -3. margin-left 偏移进 container 内,relative left 偏移进 padding 占位 - -### 双飞翼布局 - -重点就是利用 container 内部 m 的 margin 来给翅膀空间。 - -```html - - -
-
-
-
-
-``` - -1. container 内部 m 使用 margin 来占位 -2. float: left 脱离文档流 -3. 左右通过 margin-left 来偏移到正确位置 - -相比下来,双飞翼不需要定位来做偏移,css 更加便捷一些。 - -### flex 布局 - -这个常用,就短说吧,中间 `flex: 1` 即可。 diff --git "a/content/posts/eight-legged essay/base-html/JS\345\205\263\344\272\216DOM\351\203\250\345\210\206.md" "b/content/posts/eight-legged essay/base-html/JS\345\205\263\344\272\216DOM\351\203\250\345\210\206.md" deleted file mode 100644 index 68c7f0f..0000000 --- "a/content/posts/eight-legged essay/base-html/JS\345\205\263\344\272\216DOM\351\203\250\345\210\206.md" +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: 'JS关于DOM部分(元素)' -date: 2022-09-20T17:06:54+08:00 -tags: [HTML, DOM] ---- - -## DOM 的大小 - -### box-sizing - -`content-box` | `border-box` 控制 css 中 width 设置的是 content 还是 content + padding(根据属性名很容易区分)。 - -> 文字会溢出到`padding-bottom` - -### offsetTop/offsetLeft offsetParent - -offsetParent: 获取最近的祖先: - -- position 为 absolute,relative 或 fixed -- 或 body -- 或 table, th, td - -offsetTop/offsetLeft: 元素带 border 相对于最近的祖先偏移距离 - -### offsetWidth/offsetHeight - -包含 boder 的完整大小 - -### clientTop/clientLeft - -content 相对于 border 外侧的宽度 - -> 当系统为阿拉伯语滚动条在左侧时,client 就变成了 border-left + 滚动条的宽度 - -### clientWidth/clientHeight - -content + padding 的宽度,不包括滚动条 - -### scrollTop/scrollLeft - -元素超出 contentHeight/conentWidth 的部分 - -### scrollWidth/scrollHeight - -元素实际的宽高 - -## 滚动 - -```js -scrollBy(x, y) // 相对自身偏移 -scrollTo(pageX, pageY) // 滚动到绝对坐标 -scrollToView() // 滚到视野里 -``` - -## 常用的操作 - -- 判断是否触底(无限加载之类):`offsetHeight + scrollTop >= scollHeight` -- 判断是否进入可视区域(懒加载图片之类):`元素的offsetTop - wrap的scrollTop < 窗口clientHeight` - -## 坐标 - -- clientX/clientY 相对于窗口 -- pageX/pageY 相对于文档 - -一个 API -`dom.getBoundingClientRect()` -返回: x,y,top,left,right,bottom,width,height - -> 注意,x,y 与 left,top 并不是多余重复的元素,而是在制定了起点时,x,y 会改变,它是矩形原点相对于窗口的 X/Y 坐标。 - -## 获取 dom 样式的方式 - -- dom.style,获取行内样式,并且可以修改 -- dom.currentStyle,只能获取样式 -- windwo.getComputedStyle(dom),只能获取样式(IE 兼容较差) diff --git "a/content/posts/eight-legged essay/base-html/JS\345\205\263\344\272\216DOM\351\203\250\345\210\2062.md" "b/content/posts/eight-legged essay/base-html/JS\345\205\263\344\272\216DOM\351\203\250\345\210\2062.md" deleted file mode 100644 index d1651be..0000000 --- "a/content/posts/eight-legged essay/base-html/JS\345\205\263\344\272\216DOM\351\203\250\345\210\2062.md" +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: 'JS关于DOM部分(事件)' -date: 2022-09-20T21:51:51+08:00 -tags: [HTML, DOM] ---- - -## 浏览器事件 - -当事件发生时,浏览器会创建一个 event 对象,将详细信息放入其中,并将其作为参数传递给处理程序。 -每个处理程序都可以访问 event 对象的属性: - -- event.target —— 引发事件的层级最深的元素。 -- event.currentTarget —— 处理事件的当前元素(具有处理程序的元素) -- event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。 - -### DOM0 和 DOM2 事件模型 - -有三种事件绑定方式: - -- 行内绑定,利用 html 属性,`onclick="..."` -- 动态绑定,利用 dom 属性,`dom.onclick = function` -- 方法, `addEventListener`,`dom.addEventListener(event, handler[, option]`,一定记得清除。 - -> 其中前三种属于 DOM0 级,第三种属于 DOM2 级。option 若为 Boolean 值,true 表示捕获阶段触发,false 为冒泡阶段触发。 - -区别: -DOM0 只绑定一个执行程序,如果设置多个会被覆盖。 -DOM2 可以绑定多个,不会覆盖,依次执行。 - -DOM2 有三个阶段: - -- 捕获阶段 -- 处于目标阶段 -- 冒泡阶段 - -> 对于同一个元素不区分冒泡还是捕获,按照绑定顺序执行 -> 阻止事件冒泡,`e.stopPropgation()`; 阻止默认行为,`e.preventDefault()` - -如果一个元素在一个事件上有多个处理程序,即使其中一个停止冒泡,其他处理程序仍会执行。 -换句话说,event.stopPropagation() 停止向上移动,但是当前元素上的其他处理程序都会继续运行。 -有一个 event.stopImmediatePropagation() 方法,可以用于停止冒泡,并阻止当前元素上的处理程序运行。使用该方法之后,其他处理程序就不会被执行。 - -DOM3 在 DOM2 的基础上添加了更多事件类型。 - ---- - -### 事件委托 (react 旧版本中的事件处理方式) - -利用冒泡机制。 - -1. 一个好用的 api:`const ancestor = dom.closest(selector)` 返回最近的与 selector 匹配的祖先 -2. 如果`event.target`在 ancestor 中不存在,就不会触发委托在祖先元素上的事件。 - -实例: - -1. react 旧版本事件机制 -2. markup 标记 - -```HTML - - - -``` - -- 优点 - - 简化初始化并节省内存:无需添加许多处理程序。 - - 更少的代码:添加或移除元素时,无需添加/移除处理程序。 -- 缺点 - - 首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用 event.stopPropagation()。 - - 其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。 - -## 自定义事件 - -```js -const event = new Event(type[, options]); -const customEvent = new CustomEvent(type[, options]); - -options: { - bubbles: false, // 能否冒泡 - cancelable: false // 是否阻止默认行为 -} - -// CustomEvent 中多了个 detail 的选项 -``` - -> 自定义事件的监听一定要写在在分发之前 - -> 有一种方法可以区分“真实”用户事件和通过脚本生成的事件。 -> 对于来自真实用户操作的事件,event.isTrusted 属性为 true,对于脚本生成的事件,event.isTrusted 属性为 false。 - -## 参考 - -- [事件委托](https://zh.javascript.info/event-delegation) -- [事件模型](https://javascript.ruanyifeng.com/dom/event.html#toc16) diff --git "a/content/posts/eight-legged essay/base-html/script\346\240\207\347\255\276.md" "b/content/posts/eight-legged essay/base-html/script\346\240\207\347\255\276.md" deleted file mode 100644 index f94bf82..0000000 --- "a/content/posts/eight-legged essay/base-html/script\346\240\207\347\255\276.md" +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: 'Script标签' -date: 2022-09-20T17:07:56+08:00 -tags: [HTML] ---- - -[async vs defer attributes](https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html) - -## async 和 defer - -一般情况下,脚本` - -
Second div
- - -``` - -2. 返回值不同 - -- getElementsBy\* 返回的是 HTMLCollection -- querySelectorAll 返回值是 NodeList - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/20220918155922.png) - -> HTMLCollection 比 NodeList 多了个 `namedItem(name)` 方法,根据 name 属性获取 dom -> NodeList 相比 HTMLCollection 多了更多的信息,比如注释,文本等 - -## navigator | location | history - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/20220918151505.png) - -### navigator - -提供了有关浏览器和操作系统的背景信息,api 有很多,记两个常用的 -`navigator.userAgent` —— 关于当前浏览器 -`navigator.platform` —— 关于平台(有助于区分 Windows/Linux/Mac 等) - -### location - -location 顾名思义,主要是对地址栏 URL 的操作。 - -具体如下: `location.xxx` - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/20220918164450.png) - -两个主要点: - -1. origin -- 只能获取,不能设置,其他都可 -2. protocol -- 不要漏了最后的冒号`:` -3. 还有一种带 password 的,很少用就不记录了 - -### history - -history 顾名思义,主要是对浏览器的浏览历史进行操作。 - -### vue-router/react-router 原理 - -都有两种模式 hash 模式和 history 模式,分别基于 location.hash 和 history 的 api: `pushState`,`replaceState` - -- hash 模式 - - 1. 改变 hash 值 - 2. 监听 hashchange 事件即可实现页面跳转 - - ```js - window.addEventListener('hashchange', () => { - const hash = window.location.hash.slice(1) - // 根据hash值渲染不同的dom - }) - ``` - - > 不会向服务器发起请求,只会修改浏览器访问历史记录 - -- history 模式 - - 1. 改变 url (通过 `pushState()` 和 `replaceState()`) - - ```js - // 第一个参数:状态对象,在监听变化的事件中能够获取到 - // 第二个参数:标题 - // 第三个参数:跳转地址url - history.pushState({}, '', '/a') - ``` - - 2. 监听 `popstate` 事件 - - ```js - window.addEventListener('popstate', () => { - const path = window.location.pathname - // 根据path不同可渲染不同的dom - }) - ``` - - > pushState 和 replaceState 也只是改变历史记录,不会向服务器发起请求 - > 但是如果直接访问非 index.html 所在位置的 url 则服务器会报 404 因为我们是单页应用,根本就没有子路由的路径 - - 解决方案很多,常用的解决方案的话就是后端配置 nginx - - ```sh - location / { - root html; - index index.html index.htm; - #新添加内容 - #尝试读取$uri(当前请求的路径),如果读取不到读取$uri/这个文件夹下的首页 - #如果都获取不到返回根目录中的 index.html - try_files $uri $uri/ /index.html; - } - ``` - - > 增加一个前端需要了解的 nginx 知识,跨域配置:[Nginx 跨域配置](https://www.cnblogs.com/itzgr/p/13343387.html) - -## slice | substr | substring - -这个是常用的三个字符串的截取方法,经常搞混,记录一下。 - -1. 都有两个参数,只不过不太一样的是 `substr` 截取的是长度,其他是索引 - - - `slice(start,end)`[^1] - - `substr(start,len)` - - `substring(start,end)` - > 注意索引都是左闭右开的:`[start, end)` - -2. 对于负值的处理不同 - - slice 把所有的负值加上长度转为正常的索引,且只能从前往后截取 - (`start > end`则返回空串) - - substring 负值全部转为 `0`,可以做到从后往前截取 - (`substring(5, -3)` <==> `substring(0, 5)`) - - substr 第一个参数为负与 slice 处理方式相同,第二个参数为负与 substring 处理方式相同 - -[^1]: 字符串中有一些和数组共用的方法,类似的还有 indexOf,includes,concat 等 - -## undefined | null - -null 和 undefined 都是 JavaScript 的基本数据类型之一,初学者有时候会分不清。 - -主要有以下的不同点: - -- null 是 JavaScript 的保留关键字,undefined 只是 JavaScript 的全局属性,所以 undefined 可以用作变量名,然后被重新赋值,like this:`var undefined = '变身'` -- null 表示空,undefined 表示已声明但未赋值 -- null 是原型链的终点 -- Number(null) => 0;Number(undefined) => NaN - -对于上方 undefined 只是一个属性,可以被重新赋值,所以经常可以在很多源码中看见 `void 0` 被用来获取 undefined。 - -> 关于 void 运算符,就是执行后面的表达式,并且最后始终返回纯正的 undefined - -常用的用法还有: - -- 更优雅的立即调用表达式(IIFE) - `void function(){...}()` - -- 箭头函数确保返回 undefined。(防止本来没有返回值的函数返回了数据影响原有逻辑) - `button.onclick = () => void doSomething()` - -## 参考 - -- [现代 JavaScript 教程](https://zh.javascript.info/document) diff --git "a/content/posts/eight-legged essay/base-js/\345\270\270\347\224\250\345\206\205\347\275\256\345\216\237\347\220\206.md" "b/content/posts/eight-legged essay/base-js/\345\270\270\347\224\250\345\206\205\347\275\256\345\216\237\347\220\206.md" deleted file mode 100644 index 27c2218..0000000 --- "a/content/posts/eight-legged essay/base-js/\345\270\270\347\224\250\345\206\205\347\275\256\345\216\237\347\220\206.md" +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: '常用内置 API 的实现' -date: 2022-09-25T19:25:24+08:00 -tags: [JavaScript] ---- - -### new - -```js -function _new(ctor, ...args) { - const obj = Object.create(ctor.prototype) - const ret = ctor.call(obj, ...args) - const isFunc = typeof ret === 'function' - const isObj = typeof ret === 'object' && ret !== null - return isFunc || isObj ? ret : obj -} -``` - -### instanceof - -```js -/** - * ins - instance - * ctor - constructor - */ -function _instanceof(ins, ctor) { - let __proto__ = ins.__proto__ - let prototype = ctor.prototype - while (true) { - if (__proto__ === null) return false - if (__proto__ === prototype) return true - __proto__ = __proto__.__proto__ - } -} -``` - -> 建议判断类型使用: -> `Object.prototype.toString.call(source).replace(/\[object\s(.*)\]/, '$1')` - -### call/apply - -加载到函数原型上,注意:不能使用箭头函数哦,否则 this 指向会指向全局对象去了~ - -```js -Function.prototype._call = function (ctx = window, ...args) { - ctx.fn = this - const res = ctx.fn(...args) - delete ctx.fn - return res -} -Function.prototype._apply = function (ctx, args) { - ctx.fn = this - const res = args ? ctx.fn(...args) : ctx.fn() - delete ctx.fn - return res -} -``` - -### bind - -bind 与 call/apply 不同的是返回的是函数,而不是改变上下文后直接立即就执行了。 - -另外需要考虑返回的函数如果能做构造函数的情况。 - -```js -Funtion.prototype._bind = function (ctx = window, ...args) { - const fn = this - const resFn = function () { - // 这里的this指向调用该函数的对象,如果被new则指向new生成的新对象 - const _ctx = this instanceof resFn ? this : ctx - return fn.apply(_ctx, args.concat(...arguments)) - } - // 作为构造函数要继承 this,采用寄生组合式继承 - function F() {} - F.prototype = this.prototype - resFn.prototype = new F() - resFn.prototype.constructor = resFn - - return resFn -} -``` - -上方原型式继承也可以这么写: - -```js -let p = Object.create(this.prototype) -p.constructor = resFn -resFn.prototype = p -``` - -### 深拷贝 - -- 需要注意 函数 正则 日期 ES 新对象等,需要用他们的构造器创建新的对象 -- 需要注意循环引用的问题 - -```js -/** - * 借助 WeakMap 解决循环引用问题 - */ -function deepClone(target, wm = new WeakMap()) { - if (target === null || typeof target !== 'object') return target - - const constructor = target.constructor - // 处理特殊类型 - if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) return new constructor(target) - - if (wm.has(target)) return wm.get(target) - wm.set(target, true) - - const commonTarget = Array.isArray(target) ? [] : {} - for (let prop in target) { - if (target.hasOwnProperty(prop)) { - t[prop] = deepClone(target[prop], wm) - } - } - return commonTarget -} -``` - -### Promise.all & Promise.race - -```js -const promiseAll = promises => { - let res = [] - let count = 0 - return new Promise(resolve => { - promises.forEach((p, i) => { - p().then(r => { - res[i] = r - count++ - if (count === promises.length) resolve(res) - }) - }) - }) -} -promiseAll(reqs).then(res => console.log('🔥 ---', res)) - -/* ---------- race ---------- */ -function promiseRace(promises) { - return new Promise((resolve, reject) => { - for (const p of promises) { - Promise.resolve(p).then( - r => resolve(r), - e => reject(e) - ) - } - }) -} -``` diff --git "a/content/posts/eight-legged essay/base-js/\346\267\261\345\205\245npm_script.md" "b/content/posts/eight-legged essay/base-js/\346\267\261\345\205\245npm_script.md" deleted file mode 100644 index 333b7bb..0000000 --- "a/content/posts/eight-legged essay/base-js/\346\267\261\345\205\245npm_script.md" +++ /dev/null @@ -1,335 +0,0 @@ ---- -title: '深入npm script' -date: 2022-12-02T20:40:32+08:00 -tags: [script] -series: [npm] ---- - -**npm script 是一个前端人必须得掌握的技能之一。本文基于 npm v7 版本** - ---- - -下文是我认为前端人至少需要掌握的知识点。 - -> [npm - package.json](https://docs.npmjs.com/cli/v7/configuring-npm/package-json) - -## npm init - -创建项目的第一步,一般都是使用 `npm init` 来初始化或修改一个 `package.json` 文件,后续的工程都将基于 `package.json` 这个文件来完成。 - -```sh -# -y 可以跳过询问直接生成 pkg 文件(description默认会使用README.md或README文件的第一行) -npm init [-y | --scope=] # 作用域包是需要付费的 -# 初始化预使用 npm 包 -npm init [initializer] -``` - -`initializer` 会被解析成 `create-` 的 npm 包,并通过 `npmx exec` 安装(临时)并执行安装包的二进制执行文件。 - -`initializer` 匹配规则:`[<@scope/>]`,比如: - -- npm init react-app demo ---> npm exec create-react-app demo -- npm init @usr/foo ---> npm exec @usr/create-foo - -## npm exec 与 npx - -这两个命令都是从 npm 包(本地安装或远程获取)中运行任意命令。 - -```sh -# pkg 为 npm 包名,可以带上版本号 -# [args...] 这个在文档中被称为 "位置参数"...奶奶的看了我好久才理解 -# --package=xxx 等价于 -p xxx, 可以多次指定包 -# --call=xxx 等价于 -c xxx, 指定自定义指令 -npm exec -- [args...] -npm exec --package= -- [args...] -npm exec -c ' [args...]' -npm exec --package=foo -c ' [args...]' - -# 旧版 npx -npx -- [args...] -npx --package= -- [args...] -npx -c ' [args...]' -npx --package=foo -c ' [args...]' -``` - -拓展: - -1. `-p` 可以指定多个需要安装的包,如果本地没有指定的包会去远程下载并临时安装。 -2. `-c` 自定义指令运行的是已经安装过的包,也就是说要么已经本地安装过 shell 中可以直接执行,要么`-p`指定包。另外,可以带入 npm 的环境变量 - - ```sh - # 查询npm环境变量 - npm run env | grep npm_ - # 把某个环境变量带入shell命令 - npm exec -c 'echo "$npm_package_name"' - ``` - -辨析: - -- npx: - ```sh - # 这里是把 foo 当指令, 后面的全部是参数 - npx foo bar --package=@npm/foo # ==> foo bar --package=@npm/foo - ``` -- npm exec: - - ```sh - # 这里会优先去解析 -p 指定的包 - npm exec foo bar --package=@npm/foo # ==> foo bar - # 想要让 exec 与 npx 实现一样的效果使用 -- 符号, 抑制 npm 对 -p 的解析 - npm exec -- foo bar --package=@npm/foo # ==> foo bar --package=@npm/foo - ``` - - ps 一句:官网(英文真的很重要)和一些中文文档读的是真 tm 累~或许这就是菜狗吧... - -## npm run - -> npm 环境变量中有一个是:`npm_command=run-script`,它的别名就是 `run` - -`npm run [key]`,实际上调用的是 `npm run-script [key]`,根据 `key` 从 `package.json` 中 `scripts` 对象找到对应的要交给 shell 程序执行的命令。(mac 默认是 bash,个人设为 zsh) - -`test`、`start`、`restart`、`stop`这四个是内置可以直接执行的命令。 - -再次遇见 `--`,作用一样也是抑制 npm 对形如 `--flag="options"` 的解析,最终把 `--flag="options"` 整体传给命令脚本。eg: - -```sh -npm run test -- --grep="pattern" -#> npm_test@1.0.0 test -#> echo "Error: no test specified" && exit 1 "--grep=pattern" -``` - -正如 shell 脚本执行需要指定 shell 程序一样,`run-script` 从 `package.json`的 script 对象中解析出的 shell 命令在执行之前会有一步 “装箱” 的操作:**把 `node_modules/.bin` 加在环境变量 `$PATH` 的中**,这意味着,我们就不需要每次都输入可执行文件的完整路径了。 - -`node_modules/.bin` 目录下存着所有安装包的脚本文件,文件开头都有 `#!/usr/bin/env node`,这个东西叫 [Shebang]()。 - -```sh -# "scripts": { -# "eslint": "eslint ./src/**/*.js" -# } -npm run eslint # ==>node ./node_modules/.bin/eslint *.js -``` - -`node_modules/.bin` 中的文件,实际上是在 `npm i` 安装时根据安装库的源代码中`package.json` 的 `bin` 指向的路径创建软链。 - -```JSON -"node_modules/eslint": { - "version": "8.30.0", - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } -} -``` - -如果 `node_modules/.bin` 中没有对应的执行脚本,那么会去全局目录下查找,如果还没有再从环境变量 `$PATH` 下查找是否有同名的可执行程序,否则就报错啦。 - -PS: `npm run` 后不带参数直接执行,可以查看 `package.json` 中所有可执行的命令,就没必要再去点开文件看了。 - -## npm script 传参 - -直接举个常用的打印日志例子: - -```sh -# 日志输出 精简日志和较全日志 -npm run [key] -s # 全称是 --loglevel silent 也可简写为 --silent -npm run [key] -d # 全称是 --loglevel verbose 也可简写为 --verbose -``` - -两个常用的内置变量 `npm_config_xxx` 和 `npm_package_xxx`,eg: - -```JSON -"view": "echo $npm_config_host && echo $npm_package_name" -``` - -当执行命令 `npm run view --host=123`,就会输出 123 和 package.json 的 name 属性值。 - -如果上方 [key] 指向的是另一个 `npm run` 命令,想传参给真正指向的命令该怎么做呢?又得依靠 `--`的能力了。下面两条命令对比,就是可以把 `--fix` 传递到 `eslint ./src/**/*.js` 之后。 - -```diff -"eslint": "eslint ./src/**/*.js", -- "eslint:fix": "npm run eslint --fix", -+ "eslint:fix": "npm run eslint -- --fix" -``` - -在脚本文件中,也可以获取命令的传参: - -```JSON -"go": "node test.js --key=val --host=123" -``` - -```js -// test.js -const args = process.argv -console.log('📌📌📌 ~ args', args) - -const env = process.env.NODE_ENV -console.log('📌📌📌 ~ env', env) -``` - -此外,`process.env` 可以获取到本机的环境变量配置,常用的如: - -- NODE_ENV -- npm_lifecycle_event,正在运行的脚本名称 -- npm_package\_[xxx] -- npm_config\_[xxx] -- ...等等 - -其中 `process.env.NODE_ENV` 也可以通过命令来设置,在 \*NIX 系统下可以这么使用: - -```sh -"go": "export NODE_ENV=123 && node test.js --key=val --host=123" -``` - -为了抹除平台的差异,常常使用的是 `cross-env` 这个库。 - -## npm script 钩子 - -npm 提供 `pre`和`post`两种钩子机制,分别在对应的脚本前后执行。 - -## npm script 命令自动补全 - -官网提供了集成方法: - -```sh -npm completion >> ~/.zshrc # 本地 shell 设置的是哪个就是哪个 -``` - -把 `npm completion` 的输出注入 `.zshrc` 之后就可以通过 tab 来自动补全命令了。 - -## npm 配置 - -```sh -npm config set -npm config get -npm config delete - -# 查看配置 -npm config list -# 查看全局安装包和全局软链 -npm ls -g -``` - -## node_modules 的扁平结构 - -npm 3 之前: - -```txt -+-------------------------------------------+ -| app/ | -+----------+------------------------+-------+ - | | - | | -+----------v------+ +---------v-------+ -| | | | -| webpack@1.15.0 | | nconf@0.8.5 | -| | | | -+--------+--------+ +--------+--------+ - | | - +-----v-----+ +-----v-----+ - |async@1.5.2| |async@1.5.2| - +-----------+ +-----------+ -``` - -npm 3 之后: - -```txt - +-------------------------------------------+ - | app/ | - +-+---------------------------------------+-+ - | | - | | -+----------v------+ +-------------+ +---------v-------+ -| | | | | | -| webpack@1.15.0 | | async@1.5.2 | | nconf@0.8.5 | -| | | | | | -+-----------------+ +-------------+ +-----------------+ -``` - -优势很明显,相同的包不会再被重复安装,同时也防止树过深,导致触发 windows 文件系统中的文件路径长度限制错误。 - -能这么做的原因:得益于 node 的模块加载机制,[node 之 require 加载顺序及规则](https://www.jianshu.com/p/7cf8fdd3d2bf)。 - -## npm link - -当我们开发一个 npm 模块或者调试某个开源库时,`npm link` 就发挥本事了,主要分为两步: - -1. 作为包的目标文件下执行 `npm link`。它会在创建一个全局软链 `{prefix}/lib/node_modules/` 指向该命令执行时所处的文件夹。 - 这里的 `prefix` 可以通过 `npm prefix -g` 来查看 -2. `npm link ` 然后把刚刚创建的全局链接目标链接到项目的 `node_modules` 文件夹中。 - 注意 这里的 是 `package.json` 的 `name` 属性而不是文件夹名 - -举个例子吧,对 `react v17.0.2` 源码打包,然后在自己项目中链接打包的代码进行调试: - -```sh -# 安装完依赖后对核心打包 -yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE - -# 分别进入react react-dom scheduler 创建软链 -cd ./build/react -npm link -cd ./build/react-dom -npm link -cd ./build/scheduler -npm link -``` - -对三个包 link 后,本地全局就会多了这三个包,如图: - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202301062356149.png) - -然后在项目中使用这三个包: - -```sh -npm link raect react-dom scheduler # 此优先级是高于本地安装的依赖的 -``` - -> 解除包是注意:如果是开发 cli 这样的全局包时,需要使用 `npm unlink -g` 才能生效. - -## npm 发布 - -首先得有 npm 账号,直接去官网注册就好,其次有一个可以发布的包,然后: - -```sh -# ------ terminal ------ -# 1. 登录 npm 账号 -npm adduser 或者 npm login -# npm whoami 可以查看登录的账号 -# 2. 发布 -npm publish -# 3. 带有 @scope 的发布需要跟上如下参数 -npm publish --access=public -# 4. 更新版本 直接手动指定版本,也可以 npm version [major | minor | patch],自动升对应版本 -npm version [semver] -``` - -> [sermver 版本规范](https://semver.org/lang/zh-CN/) - -## 脚本执行顺序符号 - -这里与 shell 的符号是一样的: - -- &:如果命令后加上了 &,表示命令在后台执行,往往可用于并行执行 - - 想要看执行过程可以最后添加 `wait` 命令,等待所有子进程结束,不然类似 watch 监听的命令在后台执行的时候没法 `ctrl + c` 退出了 -- &&: 前一条命令执行成功后才执行后面的命令 -- |:前一条命令的输出作为后一条命令的输入 -- ||:前一条命令执行失败后才执行后面的命令 -- ; 多个命令按照顺序执行,但不管前面的命令是否执行成功 d - -> npm-run-all 这个库也实现了以上的执行逻辑,不过我是不建议使用,写命令就老老实实写不好嘛,越写越熟练哈哈~ - -## package.json 查漏补缺 - -- devDependencies,首先无论 dependencies 还是 devDependencies,npm i 都会被安装上的,区别是被安装包的 devDependencies 不会被安装,也就是说一个包作为第三方包被安装时,devDependencies 里的依赖不会被安装,目的就是为了减少一些不必要的依赖。`npm install --production`或`NODE_ENV` 被设置为 `production` 即生产环境,也不会下载 `devDependencies` 的依赖 -- peerDependencies,就是对等依赖,在 monorepo 和 npm 包中很常见。 - 提示宿主环境去安装满足 peerDependencies 所指定依赖的包,然后在 import 或者 require 所依赖的包的时候,永远都是引用宿主环境统一安装的 npm 包,最终解决插件与所依赖包不一致的问题。 - -## 参考 - -- [npm 官方文档](https://docs.npmjs.com/) -- [Node.js process 模块解读](https://juejin.cn/post/6844903614784225287) -- [node_modules 扁平结构](https://juejin.cn/post/6844903582337237006#heading-8) -- [模块加载官网伪代码](https://nodejs.org/dist/latest-v12.x/docs/api/modules.html#modules_all_together) -- [npm 发包流程](https://segmentfault.com/a/1190000023075167) -- [探讨 npm 依赖管理之 peerDependencies](https://segmentfault.com/a/1190000022435060) diff --git "a/content/posts/eight-legged essay/base-js/\346\267\261\345\205\245package.json.md" "b/content/posts/eight-legged essay/base-js/\346\267\261\345\205\245package.json.md" deleted file mode 100644 index 02521c1..0000000 --- "a/content/posts/eight-legged essay/base-js/\346\267\261\345\205\245package.json.md" +++ /dev/null @@ -1,161 +0,0 @@ ---- -title: '深入package.json' -date: 2023-03-24T14:16:10+08:00 -tags: [package.json] -# series: [npm] ---- - -## introduction - -之前一深入学习了 npm script,`package.json`的内容一笔带过了,想了想还是总结一下 pkg 内常见的字段。 - -首先,起码我个人刚入行时是对 `package.json` 这个东西不在意的,它什么档次还需要我研究?不就是记录了装了啥嘛,老夫 `npm run dev/build` 一把梭,上来就是淦!但是当俺想要自己开发一个包并且发布时 -- oh my god,my head wengwengweng~ - -所以先有一个基本的认识,发布的包发布的是什么? - -- `package.json` 文件 -- 其他文件,协议,文档,构建物等 - -其中构建物就是我们打包完的东西,当引用发布的包时,引用的是什么?怎么做到的?请看下文。 - - - -## 一个 npm 包,你引用的撒? - -脱口而出: package.json 中的入口配置 `main` 字段指向的文件!小 case 的啦。 - -没错,但还不够。 - -### main & browser & module & exports - -- `main`:这个是众所周知的,就是指定主入口文件,默认为 `index.js` -- `browser`:这个顾名思义,作用就是指定浏览器环境下加载的文件,比如:`"axios": "node_modules/axios/dist/axios.min.js"` -- `module`:该字段也是指定主文件入口,只不过是指定 ES6 模块环境下的入口文件路径,优先级高于 `main` -- `exports`:这个字段作用是指定导出内容,可以为多个,遵循 node 模块解析算法。 - - ```JSON - { - "name": "example", - "version": "1.0.0", - "main": "./lib/example.js", - "exports": { - ".": "./lib/example.js", // main显示 这里必须再指定一次 - "./module1": "./lib/module1.js", - "./module2": "./lib/module2.js", - } - } - ``` - - 举例:`import { module1 } from 'example/module1'` - - 注意点: - - 1. 如果没有显示指定 `main`,`main` 默认为 `./index.js`; 如果指定了 `main`,则必须在 `exports` 中再显示的指定 - 2. 如果 `key` 为 `import` 和 `require`,则是针对不同的模块系统导出。 - -现在,我们知道了引入一个 npm 包的多种方式,接下来继续了解下其他字段吧。 - ---- - -## other props - -### scripts - -指定脚本,详细的在 `npm scripts` 中已经讲过,`npm run ` 实际上调用的是 `run-script` 命令,`run` 是它的别名。 - -原理就是执行命令时会把 `node_modules/.bin` 加到环境变量 `PATH` 中,以便在执行命令时能够正确找到本地安装的可执行文件 - -### bin - -上面说到了命令文件都在 `.bin` 目录下,而 `pacakge.json` 中的 `bin` 字段是指定可执行文件的路径。 - -```JSON -{ - "name": "my-project", - "version": "1.0.0", - "bin": { - "my-cli": "./bin/my-cli.js", - } -} -``` - -全局安装后就可以通过 `my-cli` 命令来执行脚本。 - -### type - -`type: "module"||"commonjs"` - -指定该 npm 包内文件都遵循 type 的模块化规范。 - -### typs 和 typings - -`types` 属性指定了该包所提供的 TypeScript 类型的入口文件(.d.ts 文件)。 - -`typings` 属性是 `types` 属性的旧版别名,如果需要向后兼容,都写上即可。 - -### files - -包发布时,需要将哪些文件上传。 - -```JSON -{ - "name": "my-package", - "version": "1.0.0", - "main": "dist/main.js", - "files": [ - "dist/", - "README.md" - ] -} -``` - -注意:`dist/` 写法是上传 `dist` 文件夹下的所有文件,如果整个 dist 需要加入发包,改为 `dist` 即可。 - -### peerDependencies - -解决 npm 包被下载多次,以及统一包版本的问题。 - -```JS -{ - // ... - "peerDependencies": { - "PackageOther": "1.0.0" - } -} -``` - -上方配置:如果某个 `package` 把我列为依赖的话,那么那个 `package` 也必需应该有对 PackageOther 的依赖。 - -### workspaces - -`monorepo` 绕不开 `workspaces`. - -```JSON -{ - "name": "my-project", - "workspaces": [ - "packages/a" - ] -} -``` - -作用:当 `npm install` 的时候,就会去检查 `workspaces` 中的配置,然后创建软链到顶层 `node_modules`中。 - -### repository - -描述包源代码的位置,指定包代码的存储库类型(如 git,svn 等)和其位置(URL)。 - -`repository` 字段的格式通常为一个对象,其中包含了 `type` 和 `url` 字段。`type` 指定代码仓库的类型,通常为 `git`。`url` 指定代码仓库的 `Web` 地址。 - -```JSON -"repository": { - "type": "git", - "url": "https://github.com/example/my-project.git" -} -``` - -如果一个项目没有在 `package.json` 文件中指定 `repository` 字段,则无法通过 `npm` 安装该项目。 - -## Reference - -[npm - package.json](https://docs.npmjs.com/cli/v7/configuring-npm/package-json) diff --git "a/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-This.md" "b/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-This.md" deleted file mode 100644 index d6af2bb..0000000 --- "a/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-This.md" +++ /dev/null @@ -1,179 +0,0 @@ ---- -title: '老生常谈 -- This' -date: 2022-09-17T01:25:06+08:00 -tags: [JavaScript] ---- - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/this.png) - -回想起刚刚进入这行的时候,被 this 没少折腾,回忆记录一下吧~ - -## 默认绑定 - -非严格模式下,this 指向 window/global,严格模式下 this 指向 undefined - -```js -function demo() { - console.log(this) // window -} -``` - -```js -'use strict' -function demo() { - console.log(this) // undefined -} -``` - -## 显式绑定 - -其实我更愿意称其为`强制绑定`哈哈哈。三个流氓 `apply`、`call`、`bind` 强行霸占 `this` 小美女! -这三个函数都会劫持 this,绑定到指定的对象上。 -不同的地方是:`call 和 apply 会立即执行,而 bind 不会立即执行,返回 this 修改指向后的函数`。 -如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则,也就是全局对象。 - -## 隐式绑定 - -普通函数在的执行时,创建它的执行上下文,此时才能决定 this 的指向,谁调用 this 指向这个对象。 - -```js -var name = 'yokiizx'; -var demo = { - name: '94yk', - learn () { - console.log(this.name + ' is learning JS'); - } -}; -demo.learn(); // 上下文为demo -// 94yk is learning JS - -/* ---------- 对象方法被传递到了某处,这里为learn ---------- */ -var learn = demo.learn; -learn(); // 上下文为window -// yokiizx is learning JS (注意是浏览器环境下) -``` - -有些人喜欢把上方第二种情况称为 `this 丢失`,其实在我看来,`无非就是上下文的不同而已`。 - -> 注意:变量是使用`var`关键字进行声明的而非`let/const`; -> 若是 `name` 用 `let/const` 声明,则最后在浏览器中的输出是'undefined is learning JS'; -> 原因是:只有 var 声明的变量才会被加到它所在的上下文的变量对象上,详细见前文--闭包 - -## 箭头函数 - -首先明确一点,`箭头函数自身没有 this!没有 this!没有 this!` - -平时所见到的箭头函数中的 this,其实指向的就是是它定义时所处的上下文,这是与普通函数的区别 - -```js -var name = 'yokiizx'; -var demo = { - name: '94yk', - learn: () => { - console.log(this.name + ' is learning JS'); // this 指向全局上下文 window - } -}; -demo.learn() // yokiizx is learning JS -``` - -> 箭头函数还有没有原型链,不能 new 实例化,没有 arguments 等特点。 - -小结: - -```js -var name = 'outer_name' -var obj = { - name: 'inner_name', - log1: function () { - console.log(this.name) // 普通函数自带上下文,this 指向调用时的上下文 (谁调用指向谁) - }, - log2: () => { - console.log(this.name) // window环境: this 指向 window || node环境: this 指向空对象 {} - }, - log3: function () { - return function () { - console.log(this.name) // this 指向调用时的上下文 (谁调用指向谁) - } - }, - log4: function () { - return () => { - console.log(this.name) // this 指向声明所处位置外部第一个上下文,若是函数上下文,指向调用函数的对象,若是全局上下文,就指向window - } - } -} - -/******* 注意 *******/ -var log_4 = obj.log4() // 让内部箭头函数确定了 this 指向的是obj -var demo = { - name: 'demo' -} -log_4.call(demo) // inner_name -``` - -> 注意上方,箭头函数的另一个特点,一旦确定了它的 this 指向,即使是强制绑定也不能改变它的 this 指向。 - -## new 绑定 - -this 指向新创建的实例对象 - -```js -function _new(fn, ...args) { - // 创建新对象,修改原型链 obj.__proto__ 指向 fn.prototype - const obj = Object.create(fn.prototype) - // 修改this指向 - const res = fn.call(obj, ...args) - // 看返回的结果是不是对象 - return res instanceof Object ? res : obj; -} -``` - -## Class - -```js -class Button { - constructor(value) { - this.value = value; - } - // 类方法,不可枚举 - click() { - alert(this.value); - } -} - -let button = new Button("hello"); -setTimeout(button.click, 1000); // undefined,因为变更了上下文,this指向到了全局上下文 -``` - -没错,类中也会产生 `this丢失` 的问题,解决方法: - -1. 将方法绑定到 `constructor` 中 - ```TS - class Button { - constructor(value) { - this.value = value; - this.click = this.click.bind(this); - } - click() { - console.log(this.value); - } - } - ``` -2. 把方法赋值给类字段(推荐,更优雅),因为类字段不是加在 `类.prototype`,而是在每个独立对象中。 - ```js - class Button { - constructor(value) { - this.value = value; - } - click = () => { - console.log(this.value) - } - } - ``` - -用过 React 类组件的兄弟应该对上方两种处理方式很熟悉吧~ - -当然了,对于上方代码,也可以仅修改 setTimeout 即可 `setTimeout(() => { /** button.click() */})` - -## 总结 - -其实 this 存在的原因就是为了方便程序员的书写,相比 window.xxx/someObj.xxx,this.xxx 显然来的更加简单便捷,可以看做它只不过是你调用的方法所在上下文的一个代理而已。简单一句话:`谁调用的这个方法,this 就指向谁`,当然了,想要清楚来龙去脉,还是要对上下文的理解够深入。 diff --git "a/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-\345\216\237\345\236\213\351\223\276\345\222\214\347\273\247\346\211\277.md" "b/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-\345\216\237\345\236\213\351\223\276\345\222\214\347\273\247\346\211\277.md" deleted file mode 100644 index 3f200e4..0000000 --- "a/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-\345\216\237\345\236\213\351\223\276\345\222\214\347\273\247\346\211\277.md" +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: '老生常谈 -- 原型链和继承' -date: 2022-09-17T01:20:45+08:00 -tags: [JavaScript] ---- - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/prototype.png) - -## 正如上图 - -理解了上面的图,就理解了原型链。(如果你看不见图,用个梯子试试~) - -一个函数创建的时候就会同时创建一个`prototype`属性和`constructor`属性。 -其中`prototype`指向原型对象,实际上指向隐藏特性`[[prototype]]`;`constructor`指回构造函数。 - -如果把实例的原型看作是另一个类型的实例,那么原型对象的内部就会有一个指针指向另一个类型,同理下去,就会在实例和原型之间构造了一条原型链,直到 null 为止。 - -## 七大继承方式 - -### 原型链继承 - -这种继承是直接把子类的原型对象修改为父类的实例。 - -```js -function Super(father) { - this.father = father -} -Super.prototype.getSuper = function() { - return this.father -} -function Sub(son) { - this.son = son -} -Sub.prototype = new Super() -// 解释: -// 1. Sub.prototype.__proto__ = Super.prototype ==> 其实就是修改了子类的原型对象指针,让原型链搜索的时候去Super的原型上去搜索. -// 2. Sub.proto.constructor = Super.prototype.constructor -// ...把其他变量/方法加到修改后的Sub.prototype上 -``` - -直接原型链继承一般不会单独使用,有一些问题需要面对: - -1. 如果父类中有引用类型数据,当子类实例对其进行修改的时候,会影响到所有子类的实例 -2. 子类实例化的时候无法给父类构造器传参 - -> 一个注意点,如果用字面量直接修改 Sub.prototype,会导致之前的继承失效。 - -### 盗用构造函数继承 - -在子类构造函数中调用父类构造函数,解决了原型链继承的问题 - -```js -function Super(name) { - this.name = name -} -// 可以给父类传参了,且不会访问到原型链上的属性 -function Sub(age, name) { - Super.call(this, name) - this.age = age -} -``` - -盗用构造函数继承一般也不会单独使用,也有一些问题需要面对: - -1. 访问不了父类的原型,因此方法必须在父类构造器中定义,无法实现方法的复用。 - -### 组合继承 - -巧妙的将原型链继承和盗用构造函数继承结合到一起: - -1. 用盗用构造函数继承属性 -2. 用原型链继承方法 - -```js -function Sub(name) { - Super.call(this, name) - this.age = age -} -Sub.prototype = new Super() -``` - -问题是:调用了两次父类构造函数,导致相同的属性在实例上和原型上同时出现。 - -### 原型式继承 - -借用临时构造函数来继承,这样就无自定义一个新的子类类型。 -适用于在一个对象基础之上进行创建一个新对象。 - -```js -function Object(obj) { - function F() {} // 创建临时构造函数 - F.prototype = obj - return new F() -} -// 与 Obje.create(obj) 的效果相同 -``` - -注意:obj 中属性包含的引用值,也会在所有实例间共享。 - -### 寄生式继承 - -主要是利用工厂模式的思想和寄生构造函数。 - -```js -function Obj(obj) { - let newObj = Object.create(obj) - newObj.func = function () { - console.log('demo') - } - return newObj -} -``` - -工厂函数用原型式继承创建出新对象,然后在工厂内对新对象进行增强,最后返回这个新对象。 -所以该继承也是适用于只关注对象,而不关注子类类型和构造函数的场景下。 - -同时寄生继承的缺点:给对象添加函数,导致函数难以复用。(这点与盗用构造函数继承一样) - -### 寄生式组合继承 - -```js -function Demo(Super, Sub) { - let super = Object.create(Super.prototype) // 创建新对象 - super.constructor = Sub // 增强对象(修正constructor) - Sub.prototype = super // 修改子类的原型 -} -/* ---------- 更详细一点 ---------- */ -function Demo(Super, Sub) { - function F() {} // dummyFunciton 守卫函数,获取父类原型 - F.prototype = Super.prototype - Sub.prototype = new F() // 子类继承父类 - Sub.prototype.constructor = Sub // 修正构造函数 -} -``` - -这下看的够仔细了吧,其实寄生组合继承和寄生式都是是基于原型式继承。 -寄生组合继承的优势就是中间借助一个临时的构造器,让原型链连接上了,这样避免了组合继承时调用两次父类构造器,让实例和原型链上有相同属性的问题。 - -### class 继承 - -使用 extends 关键字 - -```js -class A { - -} -class B extends A { - constructor() { - super() - } -} - -// 构造函数的原型链指向父函数的原型对象 -B.prototype.__proto__ === A.prototype; -// 构造函数继承自父函数 -B.__proto__ === A; -``` - -> 注意:子类中如果没有调用`super(...args)`,是不允许使用`this`的。 - -## ES5 和 ES6 继承的区别 - -- es5 中,先创建了子类的实例,然后把从父类继承的方法添加到实例 this 上 -- es6 中,子类自身是没有 this 的,通过调用`super()`获取到父类的 this,然后对 this 进行增强 - -> class 也可以继承普通函数 - -## 参考 - -- JavaScript 高级程序设计(第四版) diff --git "a/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-\351\227\255\345\214\205.md" "b/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-\351\227\255\345\214\205.md" deleted file mode 100644 index 61157c0..0000000 --- "a/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-\351\227\255\345\214\205.md" +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: '老生常谈 -- 闭包' -date: 2022-09-17T00:45:17+08:00 -tags: [JavaScript] -description: '清楚了作用域和变量对象,才能真正理解什么是闭包' ---- - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/closure.png) - -## 开门见山 - -闭包是由函数和作用域共同产生的一种词法绑定现象,看上去就是**内部函数能访问到外部函数内的变量,即使外部函数已经执行完毕**。 - -## 深度探索 - -### 执行上下文&变量对象(VO) - -先说一下上下文,主要分为`全局上下文`和`函数上下文`,每一个上下文都有一个关联的`变量对象`,这个上下文中定义的变量和函数都保存在这个`VO`上。 - -> 变量对象(variable object) 无法通过代码访问,但后台处理数据会用到,**代码执行期间始终存在** - -- 全局上下文: 是最外层的上下文,就是 `window/global`(根据宿主环境),_销毁时机_:应用程序退出前,eg:关闭程序 -- 函数上下文: 是函数在调用时函数的上下文被推入到`上下文调用栈`中,_销毁时机_:函数执行完毕,被弹出上下文栈,把控制权还给之前的上下文。 - -### 作用域链&活动对象(AO) - -上下文中的代码执行的时候会创建`作用域链`,上面连着一个个上下文的变量对象,如果上下文是函数,则把函数的活动对象(AO)用作变量对象。 -正在执行的上下文的变量对象始终位于作用域链的最顶端,作用域链的下一个变量对象来自包含函数的上下文,依此类推直到全局上下文的变量对象。 - -> 活动对象(ativation object) 由 arguments 和形参来初始化,**只在函数执行期间存在** - -### 注意 - -除了全局上下文和函数上下文,还有一个不常用的 eval()上下文。 -`try/catch` 的 `catch(e)` 和 `with(ctx)` 会临时在作用域链的前端添加一个上下文 - ---- - -有了上面的铺垫,对于闭包就比较好理解了。let‘s go! - -## 定义在全局的函数 - -全局上下中的函数,在定义的时候就会创建的作用域链,把全局上下文的变量对象 VO 保存到函数的`[[scope]]`中。当函数执行的时候,创建函数的执行上下文,然后*复制*函数的`[[scope]]`来创建作用域链,接着创建并且把活动对象 AO 推入作用域链的顶端。 - -> 这是一个比较普通的情况,当全局上下文中的函数执行完毕,活动对象会被销毁,内存中就只剩下全局变量对象了。 - -### 闭包的不同 - -闭包不一样在哪里呢?其实就是它会把包含函数的变量对象保存到自己的作用域中。这样即使是外部包含函数执行完毕,包含函数的执行上下文和作用域链被销毁,但是包含函数的变量对象仍然保留在内存中,直到引用它的函数被销毁后才会销毁。 - -### demo - -```js -function demo(val1, val2) { - return function () { - var val3 = 14 - return val1 + val2 - } -} -var a = 1000 -var b = 300 -var sum = demo(a, b) -var res = sum() // 1314 -``` - -在上面的例子中: -全局上下文的变量对象上有变量 a,b,sum,res 和函数 demo; -demo 函数定义的时候已经拷贝到了全局变量对象到它的作用域链上,执行的时候会创建活动对象(arguments,val1,val2)并把它推导作用域链的顶端; -而内部的匿名函数把包含函数的变量对象再放入自己的作用域链上,当执行的时候也会创建活动对象并把它推到作用域链的顶端。 - -> tips: 顶级声明中,`var` 声明的变量、函数是在全局上下文的`VO`中的,`let、const`并不是,虽然作用域链的解析效果一样。 - -## 闭包的问题 - -内存泄露 - -## 闭包的应用 - -访问包含函数的 this, arguments。 -回调函数,柯里化,模拟私有成员变量,防抖节流等等。 - -### 更新 - -[一文颠覆大众对闭包的认知](https://mp.weixin.qq.com/s/oZk4zarKr_0ypDnhEq7NeA) - -## 参考 - -- JavaScript 高级程序设计(第四版) -- [JavaScript 闭包的底层运行机制](http://blog.leapoahead.com/2015/09/15/js-closure/) -- [深入理解 JavaScript 的执行上下文](https://qianduandaren.com/js-execution-context/) diff --git "a/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-\351\232\220\345\274\217\350\275\254\346\215\242.md" "b/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-\351\232\220\345\274\217\350\275\254\346\215\242.md" deleted file mode 100644 index 5805141..0000000 --- "a/content/posts/eight-legged essay/base-js/\350\200\201\347\224\237\345\270\270\350\260\210-\351\232\220\345\274\217\350\275\254\346\215\242.md" +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: '老生常谈 -- 隐式转换' -date: 2022-09-17T11:16:04+08:00 -tags: [JavaScript] ---- - -背景: js 中使用 ==、+ 会进行隐式转换 -重点: `Symbol.toPrimitive(input,preferedType?)`,针对的是对象转为基本类型,preferedType 默认为 number,也可以是字符串。 - -### 装箱拆箱 - -- 装箱 : `1 .toString()`(注意前面的有个空格哦) - 实际上是个装箱过程 `Number(1).toString()` --> '1'; -- 拆箱 : `toPrimitive(input,preferedType?)` - -### toPrimitive 规则: - -1. 原始值直接返回 -2. 否则,调用 `input.valueOf()`,如果结果是原始值,返回结果 -3. 否则,调用 `input.toString()`,如果是原始值,返回结果 -4. 否则,抛出错误 - -> 如果 preferedType 为 string,那么 2,3 执行顺序对调 - -### 一般符号的规则: - -1. 如果`{}`既可以被认为是代码块,又可以被认为是对象字面量,那么 js 会把他当做代码块来看待 -2. '+' 符定义: 如果其中一个是字符串,另一个也会被转换为字符串(preferedType 为 string),否则两个运算数都被转换为数字(preferedType 为 number) -3. 关系运算符`>、<、==`数据转为 number 后再比较(preferedType 为 number),注意,如果两边都是字符串调用的是 `number.charCodeAt()`这个方法来转换 - -### 经典面试题 - -1. - -```js -[] + [] // -> '' + '' -> '' -[] + {} // -> '' + "[object Object]" -{} + [] // -> 代码块 + '' -> 0 -![] == [] // -> false == '' -> 0 == 0 -> true -``` - -> 空数组/空对象 调用 valueOf() 返回的是自身 -> 空数组/空对象 调用 toString() 返回的分别是 '' / "[object Object]" - -2. - -```js -let a = { - i: 1, - valueOf() { - return a.i++ - } -} -if (a == 1 && a == 2 && a == 3) { - console.log('make it come true') -} -``` diff --git "a/content/posts/eight-legged essay/base-js/\350\247\202\345\257\237\350\200\205\345\222\214\345\217\221\345\270\203\350\256\242\351\230\205.md" "b/content/posts/eight-legged essay/base-js/\350\247\202\345\257\237\350\200\205\345\222\214\345\217\221\345\270\203\350\256\242\351\230\205.md" deleted file mode 100644 index c988d73..0000000 --- "a/content/posts/eight-legged essay/base-js/\350\247\202\345\257\237\350\200\205\345\222\214\345\217\221\345\270\203\350\256\242\351\230\205.md" +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: '观察者和发布订阅' -date: 2022-09-29T17:19:13+08:00 -tags: [JavaScript, design mode] ---- - -在前端领域,这两个设计模式,有着很多的应用,必须熟练掌握。 - -### 观察者模式 - -在 `promsie A+` 实现时,我们初始化了两个数组 -- `onFulfilledCb` 和 `onRejectedCb`,它们用来收集依赖,当 promise 注册了多个 then 且因为 promise 是可能进行时,就把 `onFulfilled` 函数加入 `onFulfilledCb` 中,当得到 `resolve` 的通知后,再去通知执行。 - -这个过程其实就是一个简单的观察者模式。这里被观察者是: promise,观察者是: 两个队列里的回调函数,promise 的 resolve 通知回调执行。 两者的关系是通过 **被观察者** 建立起来的,被观察者有**添加**,**删除**和**通知**观察者的功能。 - -```js -// 简单实现一下 -class Subject { - constructor() { - this.subs = [] - } - addObserver(observer) { - this.subs.push(observer) - } - removeObserver(observer) { - const rmIndex = this.subs.findIndex((ob) => ob.id === observer.id ) >>> 0 - this.subs.splice(rmIndex,1) - } - notify(message) { - this.subs.forEach(ob => ob.notified(message)) - } -} - -class Observer { - constructor(id, name, subject) { - this.id = id - this.name = name - // 除了让被观察者主动添加进, 观察者创建时也可以直接添加到观察者列表 - if(subject) subject.addObserver(this) - } - notified(msg) { - console.log('copy that: ', msg) - } -} - -// test -const subject = new Subject() - -const ob1 = new Observer(001, '001') -const ob2 = new Observer(007, '007', subject) -subject.addObserver(ob1) - -subject.notify('world') -// 007 copy that: world -// 001 copy that: world -``` - -### 发布-订阅模式 - -用过 vue 的朋友应该都知道在 vue 中有一种跨组件通信的方式叫 `EventBus`,这就是一个典型的 `发布-订阅模式`。 - -发布订阅模式将发布者与订阅者通过消息中心/事件池来联系起来。 - -```js -class EventCenter { - constructor() { - this.eventMap = {} - } - on(eventType, callback) { - if (typeof callback !== 'function') { - throw new Error('请设置正确的函数作为回调') - } - if (!this.eventMap[eventType]) { - this.eventMap[eventType] = [] - } - this.eventMap[eventType].push(callback) - } - emit(eventType, data) { - if (!this.eventMap[eventType]) return - this.eventMap[eventType].forEach(callback => callback(data)) - } - off(eventType, callback) { - if (!this.eventMap[eventType]) return - if (!callback.name) return - const cbIndex = this.eventMap[eventType].findIndex(cb => cb.name === callback.name) >>> 0 - this.eventMap[eventType].splice(cbIndex, 1) - } -} - -// test -const EC = new EventCenter() -const log = data => console.log(`demo `, data) - -EC.on('demo', log) -EC.emit('demo', 'hello world') - -EC.off('demo', log) -EC.emit('demo', 'hello world') -``` - -> 可以看到,订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。 - -### 总结 - -- 在观察者模式中,观察者是知道发布者的,发布者一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者互相不知道对方的存在。它们通过调度中心进行通信。 -- 在发布订阅模式中,组件是松耦合的,正好和观察者模式相反。 -- 观察者模式大多数时候是同步的,比如当事件触发,发布者就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。 diff --git "a/content/posts/eight-legged essay/base-js/\350\256\276\350\256\241\346\250\241\345\274\217--\345\210\233\345\273\272\345\236\213.md" "b/content/posts/eight-legged essay/base-js/\350\256\276\350\256\241\346\250\241\345\274\217--\345\210\233\345\273\272\345\236\213.md" deleted file mode 100644 index 35ac0db..0000000 --- "a/content/posts/eight-legged essay/base-js/\350\256\276\350\256\241\346\250\241\345\274\217--\345\210\233\345\273\272\345\236\213.md" +++ /dev/null @@ -1,233 +0,0 @@ ---- -title: '重学设计模式 -- 创建型' -date: 2023-03-07T19:33:04+08:00 -tags: [design patterns] ---- - -设计模式其实并不是多么高大尚的东西,都是码农届前辈积累下来的一些编程经验,在工作中可以说随处可见,作为前端开发者,学习这些"套路"很有必要,跟着 [desing-patterns](https://refactoringguru.cn/design-patterns) 再来回顾一下吧。 - -## 创建型模式 - -首先先了解下常用的工厂模式,又分为 「工厂方法模式」 和 「抽象工厂模式」。 - -### 工厂方法模式 - -一句话:父类工厂提供创建对象的方法,由子类去决定实例化什么样的对象。 - -- 意义:可以在子类中重写工厂方法, 从而改变其创建「产品」的类型 -- 注意:仅当「产品」具有共同的基类或者接口时, 子类才能返回不同类型的「产品」 - -> 工厂方法返回的对象被称作 “产品” - -想象一下 Audi 的工厂,一开始只生产 A3,每台车出厂前都要做一次最高时速测试。随着发展,后面又要生产 A4,A5...省略不了的步骤是 -- 出厂前最高时速测试,很明显这是可以与 A3 共用测试车间,但同时又可以用不同的测试方案,这就可以利用起 「工厂方法」 模式了。 - -```TS -/* ---------- 生产类 -- 提供 「抽象工厂方法」 ---------- */ -abstract class Produce { - /** - * 抽象工厂方法 - */ - abstract produceAudi(): Car; - /** - * 重点是: 1. 一些业务逻辑是依赖于工厂方法返回的产品 - * 2. 这些产品都有相同的业务逻辑 - */ - fastSpeed(): void { - const car = this.produceAudi(); - car.run(); - } -} -/** - * 实现具体工厂方法 - */ -class ProduceA3 extends Produce { - produceAudi() { - return new A3(); - } -} -class ProduceA4 extends Produce { - produceAudi() { - return new A4(); - } -} - -/* ---------- 产品类 -- 具体对象的创建和业务逻辑的重写 ---------- */ -type Model = 'A3' | 'A4' | 'A5'; -type Engine = 'EA888' | 'EA777' | 'EA666'; - -interface Car { - model: Model; - engine: Engine; - run: () => void; -} - -class A3 implements Car { - model: Model = 'A3'; - engine: Engine = 'EA666'; - run(): void { - console.log( - `${this.model} runs with ${this.engine}, testing maximum speed..., wow is 200km/h` - ); - } -} -class A4 implements Car { - model: Model = 'A4'; - engine: Engine = 'EA888'; - run(): void { - console.log( - `${this.model} runs with ${this.engine}, testing maximum speed..., wow is 280km/h` - ); - } -} - -const A3ins = new ProduceA3(); // A3 runs with EA666, testing maximum speed..., wow is 200km/h -A3ins.fastSpeed(); -const A4ins = new ProduceA4(); // A4 runs with EA888, testing maximum speed..., wow is 280km/h -A4ins.fastSpeed(); -``` - -不用太在意具体的方法实现,抓住重点思想: - -1. when use:**当相似业务逻辑依赖于不同类型的产品时,就可以使用「工厂方法模式」了** -2. how use:父类提供创建对象的工厂方法,子类可以重写覆盖 - -### 抽象工厂模式 - -核心:创建一系列相关的对象,而无需指定其具体类。 - -举个例子,一辆 Audi A3 由很底盘,方向盘,发动机,轮子等等一系列零件组成,需要在流水线上一步步安装,很好 A4,A5 也需要同样的流程,那么此时就可以使用抽象工厂了。 - -```TS -/** - * 抽象工厂接口 返回不同 「抽象产品」 的方法 - */ -interface AudiFactory { - createEngine(): Engine; - createWheel(): Wheel; - // ... -} - -class A3Factory implements AudiFactory { - createEngine() { - return new EA666(); - } - createWheel(): Wheel { - return new Michelin(); - } -} - -/** - * 『抽象零件』 - */ -interface Engine { - useEnginType(): string; -} -interface Wheel { - useWheelType(): string; -} - - -class EA666 implements Engine { - useEnginType(): string { - return 'EA666'; - } -} -class Michelin implements Wheel { - useWheelType(): string { - return 'michelin'; - } -} -``` - -上方代码,当拓展生产 A4,A5 的时候就很容易了,不用对抽象工厂进行修改,直接拓展个新类即可,同时原来的生产线材料有些也可以共用,比如轮子。 - -掌握思想:主要针对的是 「系列」 相关的对象,或者经常换代升级的。 - -### 生成器模式 - -核心:分步骤创建复杂对象。 - -```TS -interface Builder { - producePartA(): void; - producePartB(): void; - producePartC(): void; - // ... more and more -} - -class ConcreteBuilder implements Builder { - private product: Product; - constructor() { - this.product = new Product(); - } - producePartA(): void { - this.product.parts.push('partA'); - } - producePartB(): void { - this.product.parts.push('partB'); - } - producePartC(): void { - this.product.parts.push('partC'); - } - getProduct(): Product { - return this.product; - } -} - -class Product { - parts: string[] = []; - listParts() { - console.log(`Product parts: ${this.parts.join(', ')}\n`); - } -} - -const builder = new ConcreteBuilder(); -builder.producePartA(); -builder.producePartC(); -builder.getProduct().listParts(); // Product parts: partA, partC -``` - -可以看出,生成器模式就是一堆零件摆在面前需要什么用什么,可以能帮助我们处理构造函数入参过多带来的混乱问题。 - -### 原型模式 - -JavaScript 天然具有原型链。TypeScript 的 Cloneable (克隆) 接口就是立即可用的原型模式。 - -```TS -class Prototype { - public clone(): this { - const clone = Object.create(this); - clone.xx = xx - /** - * ...一系列对clone对象的增强, 最后把 clone 返回出去 - */ - return clone - } -} -``` - -### 单例模式 - -核心:保证一个类只有一个实例。类似于 windows 的回收站,只能创建打开一个窗口。 - -这个前端应该很熟悉了,比如 dialog 弹窗同理。 - -```TS -class Singleton { - private static instance: Singleton; - public static getInstance(): Singleton { - if (!Singleton.instance) { - Singleton.instance = new Singleton(); - } - return Singleton.instance; - } - // ...其他业务逻辑 -} - -// test -const a = Singleton.getInstance() -const b = Singleton.getInstance() -console.log(a === b ? 1 : 2); // 1 -``` - -逻辑也比较简单,私有静态实例属性 `instance` 用来存储实例,`getInstance` 返回唯一的实例。 diff --git "a/content/posts/eight-legged essay/base-js/\350\256\276\350\256\241\346\250\241\345\274\217--\347\273\223\346\236\204\345\236\213.md" "b/content/posts/eight-legged essay/base-js/\350\256\276\350\256\241\346\250\241\345\274\217--\347\273\223\346\236\204\345\236\213.md" deleted file mode 100644 index ec64387..0000000 --- "a/content/posts/eight-legged essay/base-js/\350\256\276\350\256\241\346\250\241\345\274\217--\347\273\223\346\236\204\345\236\213.md" +++ /dev/null @@ -1,426 +0,0 @@ ---- -title: '重学设计模式 -- 结构型' -date: 2023-03-13T18:14:53+08:00 -tags: [design patterns] ---- - -设计模式其实并不是多么高大尚的东西,都是码农届前辈积累下来的一些编程经验,在工作中可以说随处可见,作为前端开发者,学习这些"套路"很有必要,跟着 [desing-patterns](https://refactoringguru.cn/design-patterns) 再来回顾一下吧。 - -## 结构型 - -### 适配器模式 - -顾名思义,最简单的例子就是苹果笔记本 `type-c` 接口转 `HDMI`,这就是一种适配器模式,让不兼容的对象都够相互合作,这里的对象就是 `mac` 笔记本和外接显示器。 - -往往我们会对接口使用这种模式。 - -```TS -class Old { - public request(): string { - return 'old: the default behavior'; - } -} - -class New { - public request(): string { - return '.eetpadA eht fo roivaheb laicepS :wen'; - } -} - -class Adapter extends Old { - private adaptee: New; - constructor(adaptee: New) { - super(); - this.adaptee = adaptee; - } - request(): string { - const result = this.adaptee.request().split('').reverse().join(''); - return `Adapter: (TRANSLATED) ${result}`; - } -} - -// test -const new1 = new New(); -const adaptee = new Adapter(new1); -console.log(adaptee.request()); // Adapter: (TRANSLATED) new: Special behavior of the Adaptee. -``` - -其实也很简单,适配器需要继承旧接口,同时把新接口的实例作为入参传递给适配器,在适配器内对接口内的方法使用新接口的东西进行重写,完事。 - -### 桥接模式 - -核心:可将「业务逻辑」或一个「大类」**拆分为不同的层次结构**, 从而能独立地进行开发。 - -层次结构中的第一层 (通常称为抽象部分) 将包含对第二层 (实现部分) 对象的引用。 抽象部分将能将一些 (有时是绝大部分) 对自己的调用委派给实现部分的对象。 所有的实现部分都有一个通用接口, 因此它们能在抽象部分内部相互替换。 - -注意:这里的抽象与实现与编程语言的概念不是一回事。抽象指的是用户界面,实现指的是底层操作代码。 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202303141146609.png) -假设我们正在设计一个图形化编辑器,其中包括不同种类的形状,例如圆形、矩形、三角形等。同时,每个形状可以具有不同的颜色,例如红色、绿色、蓝色等。 - -```TS -/** - * 颜色接口 - */ -interface Color { - fill(): void; -} - -/** - * 红色实现类 - */ -class Red implements Color { - fill(): void { - console.log('红色'); - } -} - -/** - * 绿色实现类 - */ -class Green implements Color { - fill(): void { - console.log('绿色'); - } -} - -/** - * 形状抽象类 - */ -abstract class Shape { - protected color: Color; - - constructor(color: Color) { - this.color = color; - } - - abstract draw(): void; -} - -/** - * 圆形实现类 - */ -class Circle extends Shape { - constructor(color: Color) { - super(color); - } - - draw(): void { - console.log('画一个圆形,填充颜色为:'); - this.color.fill(); - } -} - -/** - * 矩形实现类 - */ -class Rectangle extends Shape { - constructor(color: Color) { - super(color); - } - - draw(): void { - console.log('画一个矩形,填充颜色为:'); - this.color.fill(); - } -} - -/** - * 运行示例代码 - */ -const red = new Red(); -const green = new Green(); - -const circle = new Circle(red); -circle.draw(); - -const rectangle = new Rectangle(green); -rectangle.draw(); -``` - -> 桥接模式在处理跨平台应用、 支持多种类型的数据库服务器或与多个特定种类 (例如云平台和社交网络等) 的 API 供应商协作时会特别有用。 - -### 组合模式 - -核心:允许将对象组合成树形结构来表示“部分-整体”的层次结构,使得客户端可以统一对待单个对象和组合对象。 -在 TypeScript 中实现组合模式需要以下几个关键元素: - -1. 抽象构件(Component):定义组合中对象的通用行为和属性。可以是一个抽象类或者接口。 -2. 叶子构件(Leaf):表示组合中的叶子节点对象,它没有子节点。 -3. 容器构件(Composite):表示组合中的容器节点对象,它有子节点。容器构件可以包含叶子节点和其他容器节点。 - -使用组合模式,可以通过递归的方式遍历整个树形结构,从而对整个树形结构进行统一的操作。 - -```TS -abstract class Component { - protected parent: Component | null = null; - - public setParent(parent: Component | null) { - this.parent = parent; - } - - public getParent(): Component | null { - return this.parent; - } - - public addChild(child: Component): void {} - - public removeChild(child: Component): void {} - - public isComposite(): boolean { - return false; - } - - public abstract operation(): string; -} - -class Leaf extends Component { - public operation(): string { - return 'Leaf'; - } -} - -class Composite extends Component { - protected children: Component[] = []; - - public addChild(child: Component): void { - this.children.push(child); - child.setParent(this); - } - - public removeChild(child: Component): void { - const index = this.children.indexOf(child); - this.children.splice(index, 1); - child.setParent(null); - } - - public isComposite(): boolean { - return true; - } - - public operation(): string { - const results: string[] = []; - for (const child of this.children) { - results.push(child.operation()); - } - return `Branch(${results.join('+')})`; - } -} - -// test -const simple = new Leaf(); -const tree = new Composite(); -const branch1 = new Composite(); -branch1.addChild(new Leaf()); -branch1.addChild(new Leaf()); -const branch2 = new Composite(); -branch2.addChild(new Leaf()); -tree.addChild(branch1); -tree.addChild(branch2); -console.log(tree.operation()) // Branch(Branch(Leaf+Leaf)+Branch(Leaf)) -``` - -### 装饰模式 - -核心:允许在不改变一个对象的基础结构和功能的情况下,动态地为该对象添加一些额外的行为。 - -
-TS 开启装饰器需要配置一下 `tsconfig.json`: - -```JSON -{ - "compilerOptions": { - "target": "ES6", - "experimentalDecorators": true - } -} -``` - -
- -```TS -function logger(constructor: Function) { - console.log(`${constructor?.name} has been invoked.`); -} - -function enumerable(value: boolean) { - return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { - descriptor.enumerable = value; - }; -} - -function readonly(target: any, propertyKey: string) { - Object.defineProperty(target, propertyKey, { - writable: false - }); -} - -@logger -class Point { - constructor(private x: number, private y: number) {} - - @readonly - @enumerable(false) - getDistanceFromOrigin() { - return Math.sqrt(this.x * this.x + this.y * this.y); - } -} - -const point = new Point(3, 4); -console.log(point.getDistanceFromOrigin()); // 输出 5 - -point.x = 5; // Cannot assign to 'x' because it is a read-only property. -``` - -### 外观模式 - -这种模式其实核心就是封装~~~ - -```TS -// 子系统 A -class SystemA { - public operationA(): string { - return "System A is doing operation A."; - } -} - -// 子系统 B -class SystemB { - public operationB(): string { - return "System B is doing operation B."; - } -} - -// 子系统 C -class SystemC { - public operationC(): string { - return "System C is doing operation C."; - } -} - -// 外观类 -class Facade { - private systemA: SystemA; - private systemB: SystemB; - private systemC: SystemC; - - constructor() { - this.systemA = new SystemA(); - this.systemB = new SystemB(); - this.systemC = new SystemC(); - } - - // 统一的接口方法,通过调用多个子系统的方法来完成任务 - public executeAllOperations(): string { - let result = ""; - - result += this.systemA.operationA() + "\n"; - result += this.systemB.operationB() + "\n"; - result += this.systemC.operationC() + "\n"; - - return result; - } -} - -// 客户端代码 -function clientCode() { - const facade = new Facade(); - console.log(facade.executeAllOperations()); -} - -clientCode(); -``` - -很简单,就是封装 -- 通过提供一个统一的接口,封装了整个子系统的复杂性,使客户端代码更加简洁和易于维护。 - -### 享元模式 - -享元模式在消耗少量内存的情况下支持大量对象,它只有一个目的: 将内存消耗最小化。 - -
-对于前端应用较少(点击查看详细内容) - -在享元模式中,创建一个共享对象工厂,它负责创建和管理共享对象。该工厂接收来自客户端的请求,并确定是否具有相应的共享对象。如果共享对象不存在,它将创建一个新的并将其添加到共享池中。否则,它将返回池中已有的对象。 - -通过将相同数据封装在享元池中,我们可以减少内存使用并提高程序性能。但是,享元模式需要额外的开销来维护享元池,因此在决定是否使用它时需要权衡其优缺点。 - -```TS -class Flyweight { - private readonly sharedState: string; - - constructor(sharedState: string) { - this.sharedState = sharedState; - } - - public operation(uniqueState: string): void { - console.log(`Flyweight says: shared state (${this.sharedState}) and unique state (${uniqueState})`); - } -} - -class FlyweightFactory { - private flyweights: {[key: string]: Flyweight} = {}; - - constructor(sharedStates: string[]) { - sharedStates.forEach((state) => { - this.flyweights[state] = new Flyweight(state); - }); - } - - public getFlyweight(sharedState: string): Flyweight { - if (!this.flyweights[sharedState]) { - this.flyweights[sharedState] = new Flyweight(sharedState); - } - return this.flyweights[sharedState]; - } -} - -const flyweightFactory = new FlyweightFactory(['A', 'B', 'C']); - -const flyweight1 = flyweightFactory.getFlyweight('A'); -flyweight1.operation('X'); - -const flyweight2 = flyweightFactory.getFlyweight('B'); -flyweight2.operation('Y'); - -const flyweight3 = flyweightFactory.getFlyweight('A'); -flyweight3.operation('Z'); -``` - -在这个示例中,Flyweight 类表示享元对象,它包含了内部状态 sharedState,该状态初始化时被设置为不变的值。FlyweightFactory 类是享元工厂,它负责创建和管理所有的享元对象。 - -我们通过调用 flyweightFactory.getFlyweight() 方法来获取 Flyweight 对象,如果池中不存在相应的对象,则创建一个新对象。当我们使用 Flyweight 对象时,我们将外部状态 uniqueState 传递给它,这些状态可以随时更改。注意到我们获取到的相同内部状态的 Flyweight 对象是同一个对象,因此我们可以重复使用它而不会产生额外的内存负担。 - -
- -### 代理模式 - -这个对于前端来说再熟悉不过了吧。比如为什么 Vue 中 可以通过 this.xxx 拿到实例的数据,实际上一开始是加载到 `vm._data` 上的,通过内部实现的 proxy 方法结合 `Object.defineProperty` - -```TS -interface ISubject { - request(): void; -} -class RealSubject implements ISubject { - public request(): void { - console.log("Real Subject Request"); - } -} - -class ProxySubject implements ISubject { - private subject: RealSubject; - constructor() { - this.subject = new RealSubject(); - } - - public request(): void { - console.log("Proxy Request"); - this.subject.request(); - } -} - -// test -const subject: ProxySubject = new ProxySubject(); -subject.request(); -``` - -有利于解耦,但是会增大开销,有可能降低性能。 diff --git "a/content/posts/eight-legged essay/base-js/\350\256\276\350\256\241\346\250\241\345\274\217--\350\241\214\344\270\272\345\236\213.md" "b/content/posts/eight-legged essay/base-js/\350\256\276\350\256\241\346\250\241\345\274\217--\350\241\214\344\270\272\345\236\213.md" deleted file mode 100644 index ca35107..0000000 --- "a/content/posts/eight-legged essay/base-js/\350\256\276\350\256\241\346\250\241\345\274\217--\350\241\214\344\270\272\345\236\213.md" +++ /dev/null @@ -1,493 +0,0 @@ ---- -title: '重学设计模式 -- 行为型' -date: 2023-03-15T11:41:52+08:00 -tags: [design patterns] ---- - -设计模式其实并不是多么高大尚的东西,都是码农届前辈积累下来的一些编程经验,在工作中可以说随处可见,作为前端开发者,学习这些"套路"很有必要,跟着 [desing-patterns](https://refactoringguru.cn/design-patterns) 再来回顾一下吧。 - -## 行为型 - -### 职责链 - -核心:使得多个对象都有机会处理请求,从而避免了请求发送者和接收者之间的耦合关系,将这些对象形成一条链,并沿着这条链传递请求,直到有一个对象能够处理该请求为止。 - -```TS -abstract class Handler { - nextHandler: Handler; - - setNextHandler(handler: Handler): Handler { - this.nextHandler = handler; - return handler; - } - - handleRequest(request: string): string { - if (this.nextHandler) { - return this.nextHandler.handleRequest(request); - } - return null; - } -} - -class ConcreteHandler1 extends Handler { - handleRequest(request: string): string { - if (request === 'type1') { - return 'Type 1 handled'; - } - return super.handleRequest(request); - } -} - -class ConcreteHandler2 extends Handler { - handleRequest(request: string): string { - if (request === 'type2') { - return 'Type 2 handled'; - } - return super.handleRequest(request); - } -} - -class ConcreteHandler3 extends Handler { - handleRequest(request: string): string { - if (request === 'type3') { - return 'Type 3 handled'; - } - return super.handleRequest(request); - } -} - -// usage -const handler1 = new ConcreteHandler1(); -const handler2 = new ConcreteHandler2(); -const handler3 = new ConcreteHandler3(); - -handler1.setNextHandler(handler2).setNextHandler(handler3); - -console.log(handler1.handleRequest('type2')); // "Type 2 handled" -console.log(handler1.handleRequest('type4')); // "null" - -``` - -该示例中,定义了一个抽象类 Handler,表示职责链中的处理者。每个具体的处理者类(ConcreteHandler1、ConcreteHandler2、ConcreteHandler3)都继承自 Handler,并实现了 handleRequest 方法。在处理请求时,每个处理者都会判断能否处理该请求,如果不能处理则将请求转发给下一个处理者。最后,通过设置每个处理者的下一个处理者,形成了一个职责链。 - -通过该示例可以看到,职责链模式可以方便地扩展处理者,从而增强系统的灵活性。同时,职责链模式也有可能导致请求被多个处理者处理,从而可能降低系统性能。因此,在使用该模式时需要根据具体情况进行权衡。 - -### 命令 - -核心:将请求封装为一个对象,从而使得可以将请求操作和请求对象解耦,并且可以很容易地添加新的请求操作。 - -可以使用命令模式来消除操作的调用者与接收者之间的耦合关系,适用于需要将请求排队、记录请求指令历史或者撤销请求的场景。 - -```TS -interface Command { - execute(): void; -} - -class TurnOnCommand implements Command { - private receiver: Receiver; - constructor(receiver: Receiver) { - this.receiver = receiver; - } - - execute(): void { - console.log("执行开灯操作"); - this.receiver.turnOn(); - } -} - -class TurnOffCommand implements Command { - private receiver: Receiver; - constructor(receiver: Receiver) { - this.receiver = receiver; - } - - execute(): void { - console.log("执行关灯操作"); - this.receiver.turnOff(); - } -} - -/** - * 请求对象 - */ -class Receiver { - turnOn() { - console.log("开灯"); - } - - turnOff() { - console.log("关灯"); - } -} - -class Invoker { - private commands: Command[] = []; - - addCommand(command: Command): void { - this.commands.push(command); - } - - executeCommands(): void { - this.commands.forEach(command => command.execute()); - } -} - -// usage -const invoker = new Invoker(); -const receiver = new Receiver(); - -const turnOnCommand = new TurnOnCommand(receiver); -const turnOffCommand = new TurnOffCommand(receiver); - -invoker.addCommand(turnOnCommand); -invoker.addCommand(turnOffCommand); -invoker.executeCommands(); - -``` - -该示例中,定义了 Command 接口,其包含一个方法 execute()。实现了两个具体命令类 TurnOnCommand 和 TurnOffCommand,它们均实现了 Command 接口,并封装了对应的请求操作。定义了 Receiver 类,它代表了请求的接收者,包含了对应的请求操作具体实现。最后定义了 Invoker 类,它包含了命令队列的集合,并包含了执行命令的方法。 - -在使用命令模式时,可以解除命令的调用者和接收者之间的关系,通过命令对象进行传递和操作。通过 Invoker 类来管理命令集合和执行请求,从而提高了代码的灵活性和复用性。 - -### 中介者 - -核心:旨在降低对象之间的耦合度,通过通过一个中介者对象进行协调通信。 - -`MessageChannel` 就是一个典型的中介者模式。 - -```TS -var channel = new MessageChannel(); -var port1 = channel.port1; -var port2 = channel.port2; -port1.onmessage = function(event) { - console.log("port1收到来自port2的数据:" + event.data); -} -port2.onmessage = function(event) { - console.log("port2收到来自port1的数据:" + event.data); -} - -port1.postMessage("发送给port2"); -port2.postMessage("发送给port1"); -``` - -原理类似: - -```TS -interface Mediator { - send(message: string, colleague: Colleague): void; -} - -abstract class Colleague { - private mediator: Mediator; - - constructor(mediator: Mediator) { - this.mediator = mediator; - } - - public getMediator(): Mediator { - return this.mediator; - } - - public send(message: string): void { - this.mediator.send(message, this); - } - - public abstract receive(message: string): void; -} - -class ConcreteColleague1 extends Colleague { - constructor(mediator: Mediator) { - super(mediator); - } - - public receive(message: string): void { - console.log(`[ConcreteColleague1] received message: ${message}`); - } -} - -class ConcreteColleague2 extends Colleague { - constructor(mediator: Mediator) { - super(mediator); - } - - public receive(message: string): void { - console.log(`[ConcreteColleague2] received message: ${message}`); - } -} - -class ConcreteMediator implements Mediator { - private colleague1: ConcreteColleague1; - private colleague2: ConcreteColleague2; - - public setColleague1(colleague: ConcreteColleague1) { - this.colleague1 = colleague; - } - - public setColleague2(colleague: ConcreteColleague2) { - this.colleague2 = colleague; - } - - public send(message: string, colleague: Colleague): void { - if (colleague === this.colleague1) { - this.colleague2.receive(message); - } else { - this.colleague1.receive(message); - } - } -} - -// usage -const mediator: ConcreteMediator = new ConcreteMediator(); -const colleague1: ConcreteColleague1 = new ConcreteColleague1(mediator); -const colleague2: ConcreteColleague2 = new ConcreteColleague2(mediator); - -mediator.setColleague1(colleague1); -mediator.setColleague2(colleague2); - -colleague1.send("Hello from ConcreteColleague1"); // ConcreteColleague2 receives message: Hello from ConcreteColleague1 -colleague2.send("Hello from ConcreteColleague2"); // ConcreteColleague1 receives message: Hello from ConcreteColleague2 -``` - -### 观察者 - -还用多说嘛?Vue 的响应式原理就是观察者模式。 - -一个对象(称为“ Subject” 或“ Observable”)维护一组订阅者(称为“ Observer” 或“ Subscriber”),并在发生某些重要事件时自动通知它们。 - -```TS -/** - * 被观察对象 - */ -interface Subject { - subs: Observer[]; // 订阅者集合 - attach: (observer: Observer) => void; - detach: (observer: Observer) => void; -} - -class Sub implements Subject { - subs: Observer[]; - constructor() { - this.subs = []; - } - attach(observer: Observer) { - this.subs.push(observer); - } - detach(observer: Observer) { - const rmIndex = this.subs.findIndex((ob) => ob.name === observer.name) >>> 0; - this.subs.splice(rmIndex, 1); - } - notify(message: string) { - this.subs.forEach((ob) => ob.update(message)); - } -} - -/** - * 观察者 - */ -interface Observer { - name: string; - update: (data: any) => void; -} - -class Ob implements Observer { - name: string; - constructor(name: string) { - this.name = name; - } - update(received) { - console.log(`${this.name} got ${received}`); - } -} - -const subject = new Sub(); -const ob1 = new Ob('hello'); -const ob2 = new Ob('world'); - -subject.attach(ob1); -subject.attach(ob2); - -subject.notify('ABC'); - -subject.detach(ob2); -subject.notify('123'); - -// hello got ABC -// world got ABC -// hello got 123 -``` - -### 状态 - -核心:允许对象在内部状态改变时改变它的行为。这种模式可以看作是根据状态改变而改变实例化对象的行为。 - -举个例子比如电灯,一般也就开关两个状态,开关按一下开,再按一下关。但是现在高级点的还有弱光、强光等状态,以前一个 if-else 就能搞定,如果在原来的状态中继续补充扩大 if-else 会使得代码变得难以维护。 - -思考:按一下微光,按两下强光,按三下暖光,按四下关闭:这是一种状态的改变,且状态之间是相关联的:微->强->暖->闭。 - -```TS -interface Light { - currentState: LightState; - offLight: LightState; - weakLight: LightState; - strongLight: LightState; - setState(lightState: LightState): void; -} - -interface LightState { - light: Light; - setLightState?(): void; -} -class ConcreteLightState implements LightState { - light: Light; - constructor(light: Light) { - this.light = light; - } -} -class OffLight extends ConcreteLightState { - setLightState(): void { - console.log('weak'); - this.light.setState(this.light.weakLight); - } -} -class WeakLight extends ConcreteLightState { - setLightState(): void { - console.log('strong'); - this.light.setState(this.light.strongLight); - } -} -class StrongLight extends ConcreteLightState { - setLightState(): void { - console.log('off'); - this.light.setState(this.light.offLight); - } -} - -class ConcreteLight implements Light { - offLight: LightState; - weakLight: LightState; - strongLight: LightState; - currentState: LightState; - constructor() { - this.offLight = new OffLight(this); - this.weakLight = new WeakLight(this); - this.strongLight = new StrongLight(this); - this.currentState = this.offLight; - } - setState(lightState: LightState) { - this.currentState = lightState; - } -} - -const light = new ConcreteLight(); -light.currentState.setLightState(); // weak -light.currentState.setLightState(); // strong -light.currentState.setLightState(); // off -light.currentState.setLightState(); // weak -``` - -> https://juejin.cn/post/6844903862013280269#heading-6 - -### 策略 - -核心:允许在运行时选择算法的行为。 - -```TS -// 黄/绿/红 之间没有联系 -interface Signal { - yellow: () => void; - green: () => void; - red: () => void; -} - -function yellow() { - console.log('yellow'); -} -function green() { - console.log('green'); -} -function red() { - console.log('red'); -} - -const ActionWithSignal: Signal = { - yellow, - green, - red -}; - -ActionWithSignal.yellow(); // green -ActionWithSignal.green(); // yellow -ActionWithSignal.red(); // red -``` - -> 策略模式和状态模式都是能让 `if-else` 变得优雅方式,但是两者的内核完全不一样,状态模式是内部状态有联系,一个状态会对另一个状态产生影响,而策略模式则是平行的逻辑。 - -### 访问者 - -熟悉 babel 的应该都懂,babel 的插件机制中就利用了访问者模式。 - -核心:在不修改对象结构的情况下向其中添加新的操作。 - -该模式中有两类元素,一类是访问者,一类是被访问的元素。访问者可以访问被访问的元素,并对其执行某些操作,而被访问的元素并不需要知道是哪个具体的访问者来访问自己,也不需要知道访问者将对自己进行哪些操作。 - -```TS -interface Visitor { - visitElementA(element: ElementA): void; - visitElementB(element: ElementB): void; -} - -class ConcreteVisitor implements Visitor { - visitElementA(element: ElementA): void { - console.log(element.operationA()); - } - - visitElementB(element: ElementB): void { - console.log(element.operationB()); - } -} - -interface VisitedElement { - accept(visitor: Visitor): void; -} - -class ElementA implements VisitedElement { - public operationA(): string { - return 'ElementA operation'; - } - - public accept(visitor: Visitor): void { - visitor.visitElementA(this); - } -} - -class ElementB implements VisitedElement { - public operationB(): string { - return 'ElementB operation'; - } - - public accept(visitor: Visitor): void { - visitor.visitElementB(this); - } -} - -const elements: VisitedElement[] = [new ElementA(), new ElementB()]; -const visitor: Visitor = new ConcreteVisitor(); - -elements.forEach((element: VisitedElement) => { - element.accept(visitor); -}); -// ElementA operation -// ElementB operation -``` - -看上去也比较简单, - -- 访问者 visitor:内部有方法以 `被访问者` 为入参,之后可以对被访问者进行操作; -- 被访问者 element:设定一个 `接受访问者实例` 的接口,这个接口负责把 `被访问者自身` 传递给访问者 - -think a little bit,再 Vue 的响应式原理中 wather 和 Dep 之间关联的操作,好像都是被访问者,互相把自身传递给对方。 - ---- - -> 这篇文章内容主要来自 chatgpt~,重点是要掌握住编程思想。 -> 行为模式还有迭代器、模板方法、备忘录模式,此处省略。 diff --git "a/content/posts/eight-legged essay/base-js/\351\207\215\345\255\246TypeScript.md" "b/content/posts/eight-legged essay/base-js/\351\207\215\345\255\246TypeScript.md" deleted file mode 100644 index 90fb0e3..0000000 --- "a/content/posts/eight-legged essay/base-js/\351\207\215\345\255\246TypeScript.md" +++ /dev/null @@ -1,665 +0,0 @@ ---- -title: 'TypeScript自用手册' -date: 2023-03-07T11:39:43+08:00 -tags: [TypeScript] ---- - -编程语言,用进废退 🤦🏻‍♀️ 好久没用感觉都忘了,还是整理一下常用的东西吧,太基础的就看文档好了,记录一下必要的以及平时踩的坑。 - -> 基于 TypeScript v4.9.5 - ---- - -## 基础 - -```sh -# ts-node是为了直接运行 ts 文件,避免先tsc再node脚本 -npm i typescript ts-node -g -``` - -- [Type Challenge](https://github.com/type-challenges/type-challenges/blob/main/README.md),TS 类型中的 leetcode👻 - -
-点击查看详细内容 - -### 两个基础配置 - -- noImplicitAny,开启后,类型被推断为 any 将会报错 -- strictNullChecks,开启后,null 和 undefined 只能被赋值给对应的自身类型了 - -### 联合类型 - -注意点是:在调用联合类型的方法前,除非这个方法在联合类型的所有类型上都有,否则必须明确指定是哪个类型,才能调用。 - -```TS -function test(type: string | string[]) { - type.toString(); // no problem, both have methods toString() - type.toLowerCase(); // error 👇🏻 - // it should be: typeof type === 'string' && type.toLowerCase() - // 在 TS 中,这叫做 收紧 Narrow,typeof/instanceof/in/boolean/equal/assert -} -``` - -对于下方类似对象中某个值使用联合类型且「作为函数入参」的处理: - -```TS -// bad -interface Shape { - kind: "circle" | "square"; // 即使确定了是circle, 但是使用radius还得做一次非空断言 - radius?: number; - sideLength?: number; -} -function getArea(shape: Shape) { - if (shape.kind === 'circle') { - return Math.PI * shape.radius! ** 2; // 需要非空断言 (在变量后添加!,用以排除null和undefined) - } -} - -// good -interface Circle { - kind: "circle"; - radius: number; -} -interface Square { - kind: "square"; - sideLength: number; -} -type Shape = Circle | Square; // 这样确定为某一个类型后,使用对应的属性不再用作非空断言了 - -function getArea(shape: Shape) { - if (shape.kind === 'circle') { - return Math.PI * shape.radius ** 2; - } -} -``` - -### type 和 interface - -- type - - 其他类型的别名,可以是任何类型。 - - 不能重复声明 - - 对象类型通过 `&` 实现拓展 -- interface - - 只能表示对象。 - - 重复声明会合并 - - 对象类型通过 `extends` 实现拓展 - -### 类型断言 - -有 `as` 和 `var` 两种方式。在 `tsx`中只能使用`as`的方式。遇到复杂类型断言,可以先断言为`any`或`unknown`: - -```TS -const demo = (variable as any) as T -``` - -### 对象中的字面量类型 - -```TS -// 此处的method会被推断为 string -const req = { url: "https://example.com", method: "GET"} -// 想要让 「整个」 对象的所有字符串都变成字面量类型,可以使用 as const -const req = { url: "https://example.com", method: "GET"} as const -// 如果只想让对象中的某个属性变为字面量类型,单独使用断言即可 -const req = { url: "https://example.com", method: "GET" as "GET"} -``` - -`as const` 类似的断言也出现在 rest arguments - -```TS -// Inferred as 2-length tuple -const args = [8, 5] as const; -const angle = Math.atan2(...args); -``` - ---- - -### 函数类型 - -- 函数表达式 - ```TS - type Fn = (p: string) => void - ``` -- 调用签名 - ```TS - type DescribableFunction = { - description: string; - (someArg: number): boolean; - }; - interface CallOrConstruct { - new (s: string): Date; // 构造函数类型 - (n?: number): number; - } - ``` - -### 函数的泛型 - -当函数的『输入、输出有关联』或者『输入的参数之间』有关联,那么就可以考虑到使用泛型了。 - -```TS -// 这样函数调用后的返回数据的类型会被 「自动推断」 出来 -function firstEl(arr: T[]): T | undefined { - return arr[0]; -} - -// 「泛型约束」 也使用 extends 关键字, 下方T必须具有一个length属性 -function first(p: T[]): T | undefined { - return p[0]; -} - -// 有时候泛型不确定可能有不同值,那么在--调用的时候--需要 「手动指明」 -function combine(arr1: Type[], arr2: Type[]): Type[] { - return arr1.concat(arr2); -} -const demo = combine([1,2,3], ['hello']) -``` - -### 函数约束 - -两个函数类型之间约束涉及到两个概念: - -- 协变:子类型赋值给父类型 -- 逆变:父类型赋值给子类型。 - -```TS -interface Animal { - name: string; -} -interface Dog extends Animal { - bark: 'wang'; -} - -type a = (value: Animal) => Dog -type b = (value: Dog) => Animal - -type c = a extends b ? true : false // true - -type e = (value: Dog) => Dog; -type f = (value: Animal) => Animal; - -type g = e extends f ? true : false; // false -``` - -结论:入参逆变,返回值协变。 - -逆变的一大特性是在逆变位置时的推断类型为交叉类型。 - -```TS -type Demo = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never; - -type SSS = Demo<{a: (x: string) => void, b: (x:number) => void}> // string & number ==> never -``` - -这一特性在把对象联合类型转变为对象交叉类型还是挺好用的。 - -```TS -type Value = { a: string } | { b: number } -// extends 分发联合类型 -type ToUnionFunction = T extends unknown ? (x: T) => void : never; -type UnionToIntersection = ToUnionFunction extends (x: infer R) => unknown - ? R - : never -type Res = UnionToIntersection // type Res= { a: string } & { b: number }; -``` - -### 函数重载 - -```TS -function fn(x: boolean): void; -function fn(x: string): void; -// Note, implementation signature needs to cover all overloads -function fn(x: boolean | string) { - console.log(x) -} -``` - -### unknown | void | never - -- unknown,相比 any 更加安全,比如: - ```TS - function demo(a: unknown) { - a.b() // ts 会提示 'a' is of type 'unknown' - } - ``` -- void, 不是表示不返回值,而是会忽略返回值 - - ```TS - type voidFunc = () => void; - const fn: voidFunc = () => true; // is ok - const a = fn() // a is void type - - // but! 如果直接字面量function声明的话 机会报错 - function f2(): void { - // @ts-expect-error - return true; // 没有上方注释就会报错了 - } - ``` - -- never,表示不存在的类型 - ---- - -### 索引签名 - -预先不清楚具体属性名,但是知道数据结构就可以使用这个了。 - -能做索引签名的有这几种: - -- string -- number -- symbol -- 模板字符串 -- 以上四种的组合的联合类型 - -需要注意的是,如果对象中同时存在两个索引,那么其他索引的返回类型必须是 string 索引的子集: - -```TS -interface Animal { - name: string; -} -interface Dog extends Animal { - age: string; -} -// Animal 和 Dog 交换一下才对 -interface Demo { - [x: number]: Animal; - [x: string]: Dog; -} -``` - -### 对象的泛型 - -更优雅的处理对象类型,这可以帮助我们避免写函数重载。 - -```TS -interface Demo { - type: T; -} -const a: Demo = { - type: 'hello' -} -``` - -`type` 别名表示范围比 `interface` 接口更大,所以对于泛型的使用范围更广: - -```TS -type OrNull = Type | null; - -type OneOrMany = Type | Type[]; - -type OneOrManyOrNull = OrNull>; // 等价于 OneOrMany | null - -type OneOrManyOrNullStrings = OneOrManyOrNull; // 等价于 string | string[] | null -``` - ---- - -### class 相关 - -`strictPropertyInitialization` 控制 class 中的属性必须初始化。 - -- public,默认值 -- protected,父类自身和子类可以访问,实例不行 -- private,只有父类自身访问,实例不行 - -抽象类,(abstract) 不能被实例化,可以被继承,抽象属性和方法在子类中必须被实现。 - -[classes 基础](https://www.typescriptlang.org/docs/handbook/2/classes.html) - -
-一个高阶知识:1. 父类构造器总是会使用它自己字段的值,而不是被重写的那一个,2. 但是它会使用被重写的方法。这是类字段和类方法一大区别,另一大区别就是this的指向问题了,类字段赋值的方法可以保证this不丢失。 - -```js -class Animal { - showName = () => { - console.log('animal'); - }; - - constructor() { - this.showName(); - } -} - -class Rabbit extends Animal { - showName = () => { - console.log('rabbit'); - }; -} - -new Animal(); // animal -new Rabbit(); // animal -/* ---------- 因为上方都是类字段,父构造器只使用自己的字段值而不是重写的---------- */ -class Animal { - showName() { - console.log('animal'); - } - constructor() { - this.showName(); - } -} - -class Rabbit extends Animal { - showName() { - console.log('rabbit'); - } -} - -new Animal(); // animal -new Rabbit(); // rabbit -``` - -另一个知识点就是为什么子类构造器中想要使用 this 必须先调用 `super()`? - -在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 [[ConstructorKind]]:"derived"。这是一个特殊的内部标签。该标签会影响它的 new 行为: - -- 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this。 -- 但是当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作 - -因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建。并且我们会收到一个报错。 - -
- -### enum 枚举类型 - -枚举比较特别,它不是一个 type-level 的 JS 拓展。 - -```TS -enum Direction { - Up = 'up', - Down = 'down', - Left = 'left', - Right = 'right' -} -/* ---------- 编译后 ---------- */ -var Direction; -(function (Direction) { - Direction["Up"] = "up"; - Direction["Down"] = "down"; - Direction["Left"] = "left"; - Direction["Right"] = "right"; -})(Direction || (Direction = {})); -``` - -可以看见,枚举类型编译后实际上是创建了一个完完整整的对象的。 - -如果枚举类型未被初始化,默认从 0 开始。值为数字的枚举会被编译成如下模式: - -```TS -enum Type { - key -} -Type[Type["key"] = 0] = "key"; -// 这样 Type[0] == key || Type[key] == 0 - -// 如果想要只能通过 key 访问,可以设置为常量枚举: -const enum Type { - key -} - -const A: Type = Type.key // A === 0 -``` - -
- -## 类型操控 - -### keyof - -获取其他类型的 「键」 收集起来 组合成联合类型。 - -```TS -type Point = { x: number; 1: number }; -type P = keyof Point; - -const d: P = 'x' -const d: P = 1 - -// 对于索引签名 keyof 获取到其类型,特殊的:索引签名为字符串时,keyof拿到的类型是 string|number -type Mapish = { [k: string]: boolean }; -type M = keyof Mapish; - -const a: M = '1234'; -const b: M = 1234; -``` - -### typeof - -对应基本类型,typeof 和 js 没什么区别。主要应用在引用类型。 - -```TS -type fn = () => boolean; -type x = ReturnType; // x: boolean - -/* ---------- 如果想直接对一个函数进行返回类型的获取就得用到typeof了 ---------- */ -const demo = () => true; -type y = ReturnType; // y: boolean -``` - -### 索引访问类型 - -```TS -type Person = { age: number; name: string; alive: boolean }; -type Age = Person["age"]; -// 注意: 不能通过设置变量 x 为 'age',然后通过 Person[x] 来获取 -// 但是,可以通过设置type x = ‘age',再通过 Person[x] 来获取 -``` - -特殊的,针对数组,可以通过 `number` 和 `typeof` 来获取到数组每个元素类型组成的联合类型: - -```TS -const Arr = [ - 'hello', - 18, - { - man: true - } -]; - -type demo = typeof Arr[number]; - -// type demo = string | number | { -// name: boolean; -// } -``` - -### 映射类型 - -如果当两种类型 key 一样,只是改变了其对应的类型,那么就可以考虑基于索引类型的类型转换了。 - -```TS -type FeatureFlags = { - darkMode: () => void; - newUserProfile: () => void; -}; -type OptionsFlags = { - [Property in keyof Type]: boolean; -}; - -type FeatureOptions = OptionsFlags; - -// type FeatureOptions = { -// darkMode: boolean; -// newUserProfile: boolean; -// } - -// 可以通过 -readonly -? 来去除原来的描述符 -``` - -TS4.1 版本之后,可以通过 `as` 关键字来重命名获取到的 key。 - -```TS -type MappedTypeWithNewProperties = { - [Properties in keyof Type as NewKeyType]: Type[Properties] -} - -// NewKeyType 往往是使用 模板字符串 -type Getters = { - [Property in keyof Type as `get${Capitalize}`]: () => Type[Property] -}; - -interface Person { - name: string; - age: number; - location: string; -} - -type LazyPerson = Getters; - -// type LazyPerson = { -// getName: () => string; -// getAge: () => number; -// getLocation: () => string; -// } -``` - -## 有用内置类型操作 - -参见 [Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html) - -记几个常用的吧: - -- Awaited -- Partial -- Required -- Readonly -- Record - 类型约束时,`object` 不能接收原始类型,而 `{}`和 `Object` 都可以,这是它们的区别。 - - 而 `object` 一般会用 `Record` 代替,约束索引类型更加语义化 - - ```TS - // keyof any === string | number | symbol - type Record = { - [P in K]: T; - }; - // 定义对象类型很方便 - type keys = 'A' | 'B' | 'C' - const result: Record = { - A: 1, - B: 2, - C: 3 - } - ``` - -- Pick - ```TS - type Pick = { - [P in K]: T[P]; - }; - ``` -- Omit - ```TS - // omit 忽略 - type Omit = Pick> - ``` -- Exclude - ```TS - // 注意:这个是针对联合类型的,当 T 为联合类型时,会触发自动分发 - // 1 | 2 extends 3 === 1 extends 3 | 2 extends 3 - type Exclude = T extends U ? never : T; - ``` -- Extract - ```TS - // 提取 - type Extract = T extends U ? T : never; - ``` -- NonNullable -- ReturnType - - ```TS - type ReturnType = T extends (...args: any[]) => infer P ? P : any; - // 注意这里使用了一个 infer 关键字 - // 如果 T 能赋值给 (arg: infer P) => any,则结果是 (arg: infer P) => any 类型中的参数 P,否则返回为 T - ``` - -### infer 关键字 - -个人理解,可以把 `infer` 理解为一个占位符,往往用在泛型约束中,只有确定了泛型的类型后,才能把这个 `infer` 的占位类型也确定。 - -- [理解 TypeScript 中的 infer 关键字](https://juejin.cn/post/6844904170353328135) - -### declare - -- [Typescript 书写声明文件](https://juejin.cn/post/6844904034621456398) - -## tsconfig.json - -```JSON -{ - "compilerOptions": { - - /* 基本选项 */ - "target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT' - "module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015' - "lib": [], // 指定要包含在编译中的库文件 - "allowJs": true, // 允许编译 javascript 文件 - "checkJs": true, // 报告 javascript 文件中的错误 - "jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react' - "declaration": true, // 生成相应的 '.d.ts' 文件 - "sourceMap": true, // 生成相应的 '.map' 文件 - "outFile": "./", // 将输出文件合并为一个文件 - "outDir": "./", // 指定输出目录 - "rootDir": "./", // 用来控制输出目录结构 --outDir. - "removeComments": true, // 删除编译后的所有的注释 - "noEmit": true, // 不生成输出文件 - "importHelpers": true, // 从 tslib 导入辅助工具函数 - "isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似). - - /* 严格的类型检查选项 */ - "strict": true, // 启用所有严格类型检查选项 - "noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错 - "strictNullChecks": true, // 启用严格的 null 检查 - "noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误 - "alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict' - - /* 额外的检查 */ - "noUnusedLocals": true, // 有未使用的变量时,抛出错误 - "noUnusedParameters": true, // 有未使用的参数时,抛出错误 - "noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误 - "noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿) - - /* 模块解析选项 */ - "moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6) - "baseUrl": "./", // 用于解析非相对模块名称的基目录 - "paths": {}, // 模块名到基于 baseUrl 的路径映射的列表 - "rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容 - "typeRoots": [], // 包含类型声明的文件列表 - "types": [], // 需要包含的类型声明文件名列表 - "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。 - - /* Source Map Options */ - "sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置 - "mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置 - "inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件 - "inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性 - - /* 其他选项 */ - "experimentalDecorators": true, // 启用装饰器 - "emitDecoratorMetadata": true // 为装饰器提供元数据的支持 - } -} -``` - -## 常见报错(持续更新) - -- `Cannot redeclare block-scoped variable 'xxx'` || `Duplicate function implementation` 这种错误除了在自身文件中有重复声明,也有可能是因为在上下文中被声明了,比如你 tsc 一个 ts 文件后,ts 文件内的代码就会飘出此类报错~ - -## 参考 - -- [TypeScript 官网](https://www.typescriptlang.org/) -- [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/) - -其他: - -- [React + TypeScript 实践](https://mp.weixin.qq.com/s/Uw5FzVCopxi4uDM1VmjukA) -- [TypeScript 类型中的逆变协变](https://mp.weixin.qq.com/s/KuR-_CCYE2qkg2AV8RWcAw) -- [接近天花板的 TS 类型体操,看懂你就能玩转 TS 了](https://mp.weixin.qq.com/s/CweuipYoHwOL2tpQpKlYLg) -- [细数这些年被困扰过的 TS 问题](https://mp.weixin.qq.com/s/Bo3Z8vzFkCvfDJDoKzxr8w) - -周边: - -- ts-node - node 端直接运行 ts 文件 -- typedoc - ts 项目自动生成 API 文档 -- DefinitelyTyped - @types 仓库 -- type-coverage - 静态类型覆盖率检测 -- ts-loader、rollup-plugin-typescript2 - rollup、webpack 插件 -- typeorm - 一个 ts 支持度非常高的、易用的数据库 orm 库 diff --git "a/content/posts/eight-legged essay/base-js/\351\230\262\346\212\226\350\212\202\346\265\201.md" "b/content/posts/eight-legged essay/base-js/\351\230\262\346\212\226\350\212\202\346\265\201.md" deleted file mode 100644 index f09dfc1..0000000 --- "a/content/posts/eight-legged essay/base-js/\351\230\262\346\212\226\350\212\202\346\265\201.md" +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: '防抖节流' -date: 2022-09-25T19:26:40+08:00 -tags: [JavaScript] ---- - -诸如 scroll 之类的高频事件,或者恶意疯狂点击,往往需要防抖节流来帮我们做一些优化。 - -## 防抖 - -如果事件再设定事件内再次触发,就取消之前的一次事件。 - -```js -function debounce(fn, wait) { - let timeout = null - return function () { - if (timeout) clearTimeout(timeout) - const args = [...arguments] - timeout = setTimeout(() => { - fn.apply(this, args) - }, wait) - } -} -``` - -是否立即先执行一次版: - -```js -/** - * @description: 防抖函数 - * @param {Function}: fn - 需要添加防抖的函数 - * @param {Number}: wait - 延迟执行时间 - * @param {Boolean}: immediate - true,立即执行,wait内不能再次执行;false,wait后执行 - * @return {Function}: function - 返回防抖后的函数 - * @author: yk - */ -function debounce(fn, time, immediate) { - let timeout - return (...args) => { - if (timeout) clearTimeout(timeout) - if (immediate) { - if (!timeout) fn.apply(this, args) - timeout = setTimeout(() => { - timeout = null - }, time) - } else { - timeout = setTimeout(() => { - fn.apply(this, args) - }, time) - } - } -} -``` - -## 节流 - -事件高频触发,让事件根据设定的时间,按照一定的频率触发。 - -```js -// timeout 版本 -function throttle(fn, time) { - let timeout - return (...args) => { - if (timeout) return - /** 若是想先执行把 fn.apply 提到这里即可 */ - timeout = setTimeout(() => { - fn.apply(this, args) - timeout = null - clearTimeout(timeout) - }, time) - } -} -``` - -```js -// 时间戳版本 -function throttle(fn, time) { - /** 若想先执行,把 prev 设为 0 即可 */ - let prev = new Date() - return (...args) => { - if (new Date() - prev > time) { - fn.apply(this, args) - prev = new Date() - } - } -} -``` diff --git "a/content/posts/eight-legged essay/http/TCP-IP\347\275\221\347\273\234\346\250\241\345\236\213.md" "b/content/posts/eight-legged essay/http/TCP-IP\347\275\221\347\273\234\346\250\241\345\236\213.md" deleted file mode 100644 index d0ca671..0000000 --- "a/content/posts/eight-legged essay/http/TCP-IP\347\275\221\347\273\234\346\250\241\345\236\213.md" +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: 'TCP/IP网络模型' -date: 2022-09-22T17:22:23+08:00 -tags: [http] -draft: true ---- - -TCP/IP 模型是互联网的基础,它是一系列网络协议的总称。这些协议可以划分为四层,如下: - -- 应用层,针对特定应用的协议,电子邮件,文件传输等,通俗点说就是让用户程序能访问网络,并获得各种网络服务(http、FTP、DNS...) -- 传输层,根本任务是提供端到端的可靠通信(TCP、UDP) -- 网络层,地址管理与路由选择(IP...) -- 链路层,互连设备之间传送和识别数据帧。 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210202335427.png) - ---- - -作为前端,在应用层主要关注 http 协议,在 [http](https://yokiizx.site/posts/http/http%E5%89%8D%E4%B8%96%E4%BB%8A%E7%94%9F/) 中已经有了详细介绍。 - -在传输层,核心的就是 TCP 了,三次握手四次挥手那更是老生常谈的问题了。简单记录下常见面试题,详细的见文末参考文章,或者自行 google~ - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210210008339.png) - -1. 为啥握手是三次 - 双端确认各自及对方的接收、发送能力正常。 - - 第一次,服务端确认了客户端的发送能力和服务端的接收能力正常 - - 第二次,客户端确认了服务端的接收、发送能力,客户端的接收、发送能力正常 - - 第三次,服务端确认了服务端的发送能力和客户端的接收能力正常 - 如果没有第三次,一旦第二次出了问题,客户端会发起第二个请求,而丢失的报文段只是某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,这第二个迟来的连接会被客户端忽略,服务端一直等待客户端发送数据,浪费资源。 -2. 三次握手中可以携带数据吗 - 前两次不行,第三次可以。前两次还没建立连接,而且第一次如果能携带数据会被利用,在 SYN 携带大量数据来攻击服务器。 -3. SYN 攻击是什么 - 服务器端的资源分配是在二次握手时分配的。就是利用大量不存在的 IP 来向服务器发送连接请求,服务端回复后等待第三次连接,因为客户端 IP 根本不存在,所以会成为半连接,服务端会不断重发直至超时,大量的半连接导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210210009002.png) - -1. 为啥挥手是四次 - 因为服务端接收到 FIN 的时候,很可能并不能立刻关闭连接,所以先只回复了应答 ACK 报文,告诉服务端我知道了,等服务端报文发送完毕后,再发送 FIN 报文同步关闭。 -2. 四次会后释放连接,等待 2MSL 的意义是什么 MSL(Maximum Segment Lifetime) - 1. 说白了就是让客户端最后的一个确认报文 ACK 能够到达服务器。 - 因为网络是不稳定的,有可能丢失,等待 2MSL 就是保证服务端没有收到 ACK 后重新发起一次关闭请求,客户端也刷新等待时间从头计时;如果不等待直接关闭,一旦丢失,服务端将不能正确关闭。 - 2. 防止“已失效的连接请求报文段”出现在本连接中 - 客户端在发送完最后一个 ACK 报文段后,再经过 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。 - ---- - -计算机基础 - -- 位(bit):二进制单位,一个 0 或一个 1 -- 字节(Byte):8 bit = 1 Byte,最小存储单位。 这就是为什么百兆宽带下行速度往往只有 15 MB/s 左右 - -## 参考 - -- [面试官,不要再问我三次握手和四次挥手](https://mp.weixin.qq.com/s/WI9045Sd7gRsE-WZ5x8tcA) diff --git "a/content/posts/eight-legged essay/http/XSS\345\222\214CSRF.md" "b/content/posts/eight-legged essay/http/XSS\345\222\214CSRF.md" deleted file mode 100644 index 42336d0..0000000 --- "a/content/posts/eight-legged essay/http/XSS\345\222\214CSRF.md" +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: 'XSS和CSRF' -date: 2022-10-06T23:29:05+08:00 -tags: [http, browser] -draft: true ---- - -XSS 和 CSRF 问题是很基础、传统的安全问题,本文不做安全拓展,仅学习这两个知识点,对安全感兴趣的大佬,各种“帽子”书看起来 😄。 - -专业的话术请见参考,我个人喜欢用土话,起码我自己能理解的话来学习这两个知识点。 - -## XSS - Cross Site Script - -顾名思义,跨站脚本攻击。 -说白了 --- 就是想方设法把恶意脚本搞到你的网页上,脚本执行后获取敏感信息,如 cookie,session id 等。 - -### XSS 分类 - -- 存储型:攻击者通过发帖,评论等方式把恶意代码提交到数据库中,用户打开网站后恶意代码从数据库中取出并拼接到 HTML 中返回,浏览器接收后执行执行。 -- 反射型:攻击者构造包含恶意代码的 url,用户打开后(发起请求或跳转页面),把恶意代码从 url 中取出并拼接到 HTML 中返回,浏览器接收后执行。 - ```js - // 比如: - http://xxx/search?keyword="> - http://xxx/?redirect=%20javascript:alert('XSS') - ``` -- DOM 型:攻击者构造包含恶意代码的 url,用户打开后,浏览器直接解析恶意代码,发起攻击。比如直接使用 `document.write()`、`.innerHTML` 这种不安全的 api。 - -前两个属于服务端的安全漏洞,往往也是服务端渲染,DOM 型属于纯前端的安全漏洞,即取出和执行恶意代码都是由浏览器端完成。 - -### XSS 预防 - -对症下药: - -- 存储型和反射型的 XSS - 1. 如果不需要 SEO 的网站,完全可以做前后端分离 - 2. 如果需要 SEO,那么还是服务端渲染,就需要对 HTML 进行转义 -- DOM 型 XSS - 1. 在使用 `.innerHTML`、`.outerHTML`、`document.write()` 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 `.textContent`、`.setAttribute() `等 - 2. vue/react 不 v-html/dangerouslySetInnerHTML,使用替代方案 - -其他预防措施: - -- HTTP 头:`Set-Cookie: HttpOnly`,禁止脚本读取敏感 cookie -- 验证码,防止脚本冒充用户提交危险操作 -- HTTP 头/meta: `Content-Security-Policy: [policy]`,具体 policy 见 [MDN CSP](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Security-Policy#%E8%A7%84%E8%8C%83) - - ```http - Content-Security-Policy: connect-src http://yokiizx.site/; - script-src http://yokiizx.site/ - ``` - -## CSRF - Cross Site Request Forgery - -顾名思义,跨站请求伪造(学好英语的重要性 👻)。 -说白了 --- 就是在**登录后**,被诱导去第三方网站,在第三方网站中向被攻击网站发起跨站请求。这个攻击请求会携带被攻击网站的 cookie,绕过用户验证,冒充用户进行一系列危险操作。 - -常见于各种垃圾邮件中 --- [真实事件](https://www.davidairey.com/google-gmail-security-hijack/) - -### CSRF 分类 - -- GET 请求 - ```html - - - ``` -- POST 请求 - - ```html -
- - - -
- - ``` - -- 诱导操作类,这类往往需要用户操作后才会触发,比如点击个链接或者 - - ```html - 重磅消息!! - ``` - -- 脚本类 - ```html - - - - -    - - - - - ``` - -### CSRF 预防 - -CSRF 通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对 CSRF 的防护能力来提升安全性。 - -- CSRF(通常)发生在第三方域名 -- CSRF 攻击者不能获取到 Cookie 等信息,**只是使用** - -对症下药: - -- 阻止不明外域的访问 - - 同源检测 - 1. `origin` 和 `referer` 这两个请求头一般会自动带上,且不能由前端自定义内容,服务端可以解析这两个 header 中的域名,来确定请求的来源域。 - 2. `origin` 不会包含在 IE 11 的 CORS 请求 和 302 重定向请求中 - 3. `referer` 见 [MDN Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) - ```html - - - ``` - - SameSite Cookie - ```http - Set-Cookie: CookieName=CookieValue; SameSite=Strict|Lax|None; - ``` -- 提交时要求附加本域才能获取的信息 - - CSRF Token,由于 cookie 能被冒用,使用 token 来进行身份认证 - - 双重 Cookie 验证 - -## 参考 - -- [如何防止 XSS 攻击?](https://tech.meituan.com/2018/09/27/fe-security.html) -- [如何防止 CSRF 攻击?](https://tech.meituan.com/2018/10/11/fe-security-csrf.html) -- [Cookie 的 SameSite 属性](https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html) diff --git "a/content/posts/eight-legged essay/http/http\345\211\215\344\270\226\344\273\212\347\224\237.md" "b/content/posts/eight-legged essay/http/http\345\211\215\344\270\226\344\273\212\347\224\237.md" deleted file mode 100644 index 0300032..0000000 --- "a/content/posts/eight-legged essay/http/http\345\211\215\344\270\226\344\273\212\347\224\237.md" +++ /dev/null @@ -1,137 +0,0 @@ ---- -title: 'HTTP的前世今生' -date: 2022-09-21T23:22:07+08:00 -tags: [http] -draft: true ---- - -Hypertext transfer protocol,超文本传输协议。 - -- 1991 http 0.9 -- 1996 http 1.0 -- 1997 http/1.1 -- 2015 http/2 - - 2010 Google SPDY,2.0 的基础 -- 2018 http 3.0 - -### http 0.9/1.0 - -http 0.9 只支持 GET 请求,1.0 在其基础之上增加了: - -1. 版本号,位于 request GET 一行后 -2. 状态码 -3. header 请求头 -4. Content-Type,支持不同类型文件传输 -5. content-encoding,支持不同编码格式文件传输 - -http 1.0 有巨大性能问题,每个请求都要创建 tcp 连接,而且是串行的。 - -### http/1.1 - -http/1.1 主要解决了 1.0 的性能问题,同时增加了一些新东西: - -1. 长连接机制。`connection: keep-alive; keep-alive: timeout=5, max=1000`,可以复用 TCP,避免了每次新建 tcp 连接的开销 -2. Pipeline 机制。不必再等到上一个请求的响应返回再发起下一个请求,主流浏览器默认关闭,因为会有队头阻塞的问题。因为 http 是无状态协议,发起是可以不同等待,但是返回必须按照请求顺序返回,一旦一个阻塞,则全部阻塞了。 - - 只有幂等的方法才能使用 pipeline,例如 GET 和 HEAD 请求 - - 新建立的连接也不能使用 pipeline,因为不知道服务器是否支持 http1.1 -3. chunked 编码传输,服务器端需要在 header 中添加“Transfer-Encoding: chunked”头域来替代传统的“Content-Length”。因为随着发展,动态资源引入,传输之前服务器也不知道资源的大小,服务器可以一边动态产生资源,一边传递给用户,这种机制称为“分块传输编码”。没有说明 `Content-Length` 这样,客户端就不能断连接,直到收到服务端的 EOF 标识。 -4. 引入了 options, put, delete, tarce 和 connect 方法。其中 options 主要应用于 CORS。 -5. 增加了缓存机制,cache-control, etag, if-none-match -6. 增加了 host 头,指定服务器的域名。这样在同一台服务器上就可以搭建不同的网站了 - -http/1.1 的性能问题: - -- 无状态协议,要求 response 按照请求顺序串行返回,会产生*队头阻塞*的问题 -- 文本传输,借助耗 CPU 的 zip 压缩的方式减少网络带宽,但是耗了前端和后端的 CPU - -### http/2 - -http/2 是二进制协议。 -基本概念: - -- 流(stream):一个 request 和对应的 response 组成一个流 -- 帧(frame):每个流由多个帧组成。帧根据承载内容不同,分为控制帧和数据帧。控制帧根据作用不同,有:同步帧、设置帧、ping 帧、header 帧等 - 每个帧都是二进制数据,有利于数据压缩 - -特性: - -- 利用流和帧,http/2 解决了应用层的队头阻塞的问题。 - 每个流都有 id,每个帧也被打上了流的 id,将多个流的帧"混在一起",发送到服务端,服务端根据 id 还原出流,最后再以同样的方式返回响应。 -- 多路复用,一个 tcp 连接可以进行任意数量的 http 请求,解决了一个域名下的请求数量限制。 - 问题:TCP 层的队头阻塞问题并没有解决。一但 tcp 发生了丢包,那么这个 tcp 连接中的所有请求都将被阻塞,表现反而不如 http1.1。 -- [HPACK 头部压缩算法](https://zhuanlan.zhihu.com/p/51241802) - HPACK 中会维护一张静态列表和一张动态列表,在静态列表中会预置一些常用的 header(详见 RFC),当要发送的请求符合静态列表中的内容时,会使用其对应的 index 进行替换,这样就可以大大压缩头部的 size 了 -- 更完善的服务端推送。 - `Server Push` 通过在 header 中添加 `X-Associated-Content` 头域(X-开头的头域都属于非标准头域,为自定义头域)来告知客户端会有新的内容推送过来。一般当用户第一次打开网站首页的时候,server 端会将很多资源主动推送过来。 - 实际上“Server Push”机制只是省去了浏览器发送请求的过程。只有当服务端认为某些资源存在一定的关联性,即用户申请了资源 A,势必会继续申请资源 B、资源 C、资源 D...的时候,服务端才会主动推送这些资源,以此来达到节省浏览器发送 request 请求的过程。 - -### http/3 - -为了解决 http/2 中多路复用 TCP 层的队头阻塞问题,google 专家们又创建了一个 QUIC (Quick UDP Internet Connections)协议,http 3.0 破天荒的**把 http 底层从 TCP 换成了 UDP 协议。** - -> 在一条链接上可以有多个流,流与流之间是互不影响的,当一个流出现丢包影响范围非常小,从而解决队头阻塞问题 - -建立一个 HTTPS 的连接,先是 TCP 的三次握手,然后是 TLS 的三次握手,要整出六次网络交互,一个链接才建好,虽说 HTTP/1.1 和 HTTP/2 的连接复用解决这个问题,但是基于 UDP 后,UDP 也得要实现这个事。于是 QUIC 直接把 TCP 的和 TLS 的合并成了三次握手(对此,在 HTTP/2 的时候,是否默认开启 TLS 业内是有争议的,反对派说,TLS 在一些情况下是不需要的,比如企业内网的时候,而支持派则说,TLS 的那些开销,什么也不算了)。 -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210251606478.png) -QUIC 是一个在 UDP 之上的伪 TCP +TLS +HTTP/2 的多路复用的协议。 - -目前看下来,HTTP/3 目前看上去没有太多的协议业务逻辑上的东西,更多是 HTTP/2 + QUIC 协议。但,HTTP/3 因为动到了底层协议,所以,在普及方面上可能会比 HTTP/2 要慢的多的多。 - ---- - -### https - -https 依赖于 SSL/TLS,端口号是 443,最大的特点就是安全了,相比 http 也更利于 SEO。 - -安全的 HTTP 最重要的三个需求: - -1. 数据加密。传输内容进行混淆 -2. 身份验证。通信双方验证对方的身份真实性 -3. 数据完整性保护。检测传输的内容是否被篡改或伪造 - -来看看 https 是怎么做的: - -数据加密 - -数据加密主要有两种: - -- 共享秘钥加密,加密和解密都是用相同密钥,所以也被称为"对称加密",常见的有:「AES」「DES」「动态口令」等。 - 问题:如果接收方不知道密钥是什么,发送方就要通过互联网发送密钥给接收方,此时密钥可能会被第三者监听,就会产生泄漏密钥的问题,不安全 -- 公开秘钥加密,加密和解密使用不同的密钥,所以也被称为“非对称加密”,公钥是密钥对外公开的部分,私钥则是非公开的部分,由用户自行保管,安全性高。一般由接收方产生密钥对,常见的「RSA」。 - 问题:加密方式复杂,处理速度慢,如果我们的通信都是用公开密钥的方式加密,那么通信效率会很低。 - -Https 结合两种加密方式的特长,采用公开秘钥加密进行共享密钥的交换,在之后的通信使用共享密钥加密。 - -身份验证 - -- 数字证书 - -数字证书是一种权威性的电子文档,它提供了一种在 Internet 上验证身份的方式。 其作用类似于司机的驾驶执照或日常生活中的身份证。 它是由一个权威机构——CA 证书授权(Certificate Authority)中心发行的,人们可以在互联网交往中用它来识别对方的身份。 - -原理就是公钥加密制。 - -数据完整性 - -- 数字签名 - -发送者使用私钥对数据摘要进行签名,并附带和数据一起发送。 - -接受者使用发送者的公钥对签名进行解密,得到「摘要 I」,证明发送者身份; -再对内容使用相容的散列函数计算「摘要 II」,两个摘要相同就说明数据没有被篡改,是完整的 - -> 数据量大的时候计算数字签名将会比较耗时,所以一般做法是先将原数据进行 Hash 运算,得到的 Hash 值就叫做「摘要」。(128 位的散列值) - -原理是私钥签名,公钥验签 - -https 握手过程 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210272237638.jpeg) - -## 参考 - -- [HTTP 演进之路](https://www.zhihu.com/column/c_1050708448047706112) -- [HTTP 的前世今生](https://coolshell.cn/articles/19840.html) -- [http2.0 协议](https://juejin.cn/post/6844903984524705800) -- [HTTP3.0 和 QUIC 协议那些事](https://blog.csdn.net/wolfGuiDao/article/details/108729560) -- [一文彻底搞懂加密、数字签名和数字证书!](https://segmentfault.com/a/1190000024523772) -- [HTTPS - 揭秘 TLS 1.2 协议完整握手过程](https://www.51cto.com/article/698090.html) diff --git "a/content/posts/eight-legged essay/http/\345\270\270\350\247\201\347\212\266\346\200\201\347\240\201.md" "b/content/posts/eight-legged essay/http/\345\270\270\350\247\201\347\212\266\346\200\201\347\240\201.md" deleted file mode 100644 index b41f16e..0000000 --- "a/content/posts/eight-legged essay/http/\345\270\270\350\247\201\347\212\266\346\200\201\347\240\201.md" +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: '常见状态码' -date: 2022-09-22T17:19:40+08:00 -tags: [http] -draft: true ---- - -## 概览 - -HTTP 状态代码分为以下五组: - -- `1xx` 信息响应。 收到并理解的请求。 请求处理将继续。 -- `2xx` 成功。 已成功接收、理解和接受该操作。 -- `3xx` 重定向。 客户端必须采取进一步操作才能完成请求。 -- `4xx` 客户端错误。 可能是客户端导致的错误。 请求包含错误的语法或无法实现。 -- `5xx` 服务器错误。 服务器遇到错误,无法满足请求。 - -## 常见的 - -### 1xx - -- 100 - 目前为止一切正常,客户端应该继续请求,如果已完成请忽略 - -### 2xx - -- 200 - 请求成功。响应取决于请求,GET 响应在消息正文中,PUT/POST 在消息体中 -- 201 - 请求成功,并且创建了一个新的资源 -- 202 - 请求已收到,但未处理 -- 204 - 请求成功,但未返回任何内容 - 使用惯例是: - 在 PUT 请求中进行资源更新,但是不需要改变当前展示给用户的页面,那么返回 204 No Content。 - 如果创建了资源,则返回 201 Created 。 - 如果应将页面更改为新更新的页面,则应改用 200 。 -- 206 - 对资源的某一部分请求。请求报文中包含 Range 字段;响应报文中包含由 Content-Range 指定范围的实体内容 - -### 3xx - -- 301 - 永久重定向。 -- 302 - 临时重定向。 - 都表示重定向,浏览器根据响应的 Location 首部指定的新 URL 进行跳转。不同的是: - - 301: 旧的资源已经永久的移除了,搜索引擎会把旧网址改为新网址,再次访问不会再去原服务器发起请求; - - 302: 旧的资源还在,只是临时到新网址,搜索引擎会保存旧的网址,再次访问还会去原服务器发起访问。 -- 304 - 重定向至缓存。资源没被修改,可以直接用缓存。 - -### 4xx - -- 400 - 客户端错误。往往都是请求参数写错了可能 -- 401 - 客户端没有身份认证,缺失或错误的认证。 -- 403 - 客户端请求没有权限。 - - 与 401 不同的是,服务端知道客户端的身份,而 401 不知道 -- 404 - 请求资源不存在。 -- 405 - 请求方法错误,可能使用错了请求方法。 -- 415 - 服务器不支持请求数据的媒体格式,因此服务器拒绝请求。问题可能在 Content-Type 或 Content-Encoding 首部中指定的格式。 - -### 5xx - -- 500 - 服务端错误。 -- 501 - 服务器不支持请求方法。只有 GET 和 HEAD 是要求服务器支持的 -- 502 - 作为网关或代理的服务器,从上游服务器接收到的响应无效或错误 -- 504 - 作为网关或代理的服务器无法及时获得响应 -- 503 - 服务器没有准备好处理请求 -- 505 - 服务器不支持请求 HTTP 版本 - -## 参考 - -- [MDN http 状态码](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status) -- [URI 和 URL 的区别](https://www.cnblogs.com/blknemo/p/13198506.html) -- [301,302 区别](https://blog.csdn.net/u010142437/article/details/79541205) diff --git "a/content/posts/eight-legged essay/http/\346\265\217\350\247\210\345\231\250\347\274\223\345\255\230.md" "b/content/posts/eight-legged essay/http/\346\265\217\350\247\210\345\231\250\347\274\223\345\255\230.md" deleted file mode 100644 index c04bb2e..0000000 --- "a/content/posts/eight-legged essay/http/\346\265\217\350\247\210\345\231\250\347\274\223\345\255\230.md" +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: '浏览器和http缓存' -date: 2022-10-06T23:19:05+08:00 -tags: [http, browser] -draft: true ---- - -缓存不论是在前端还是后端,都是一个重要的性能优化手段。 -Web 缓存种类: 数据库缓存,CDN 缓存,代理服务器缓存,浏览器缓存。 - -浏览器缓存的作用:每次发起 http 请求时,会先去检查浏览器缓存中是否有该请求的结果及缓存标识,如果可以使用就直接用了,就不用再去向服务器发起请求了,这样就能节约带宽,缓解服务器压力,提升前端性能;如果浏览器缓存没有或已过期再去向服务器请求,然后根据响应的缓存标识看看是否需要缓存。 - -## 缓存过程 - -### 强缓存 - -缓存生效直接用浏览器缓存,不向服务器发起请求,返回状态码 200。 - -关键响应头: - -- `cache-control`: max-age=2592000 - 是 `http1.1` 字段,优先级比 expires 高。主要有以下几个值: - - public: 资源客户端和服务器都可以缓存 - - privite: 只有客户端可以缓存 - - no-cache: 客户端缓存资源,但是是否缓存需要经过协商缓存来验证 - - no-store: 不用缓存 - - max-age=: 缓存保质期 -- `expires`: Wed, 21 Oct 2015 07:28:00 GMT - 是 `http1.0` 字段,指定过期日期。缺点是判断是否过期是用本地时间来判断的,本地时间是可以自己修改的。 - -浏览器遇见以上两个响应头时,就会把资源缓存到 `memory cache` 或 `disk cache` 中。 -存存储图像和网页等资源主要缓存在 `disk cache`,操作系统缓存文件等资源大部分都会缓存在 `memory cache` 中。具体操作浏览器自动分配,看谁的资源利用率不高就分给谁。 - -### 协商缓存 - -协商缓存就是强缓存失效后,浏览器携带`缓存标识`向服务器发送请求,由`服务器根据缓存标识来决定`是否使用缓存的过程。缓存生效,返回 304。 - -关键响应头: - -- `last-modified`: Wed, 19 Oct 2022 09:54:33 GMT - 资源文件在服务器最后被修改的时间。 -- `etag`: "634fc959-13ad03" - 当前资源文件的一个唯一标识(由服务器生成) - -关键请求头: - -- `if-modified-since`: Wed, 19 Oct 2022 09:54:33 GMT - 携带上次请求返回的 `Last-Modified` 值,服务器与请求资源的最后被修改时间作对比,如果大于 `if-modified-since`,则返回新资源,状态码为 200,否则用缓存,状态码为 304。 -- `if-none-match`: W/"634fc959-13ad03" - 服务器资源的 etag 与 if-none-match 做对比,相同则返回 304,继续使用缓存。 - -> etag/if-none-match 优先级高于 last-modified/if-modified-since;同时存在则先验证 etag/if-none-match,一致的情况下,才会继续比对 last-modified。 -> 为什么需要 etag:1. 资源发生了周期性的修改,但最终内容未变,那么仍然是期望命中缓存的。2.last-modified 是秒级的,1 秒内的变化不会识别,此时希望是重新请求的。 - -## 缓存位置 - -缓存位置一般分为四类(按查找优先级): `Service Worker --> Memory Cache --> Disk Cache --> Push Cache`。 - -### Service Worker - -是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker 的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。 - -### Memory Cache - -内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。 - -### Disk Cache - -存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。 - -### Push Cache - -Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在 Chrome 浏览器中只有 5 分钟左右,同时它也并非严格执行 HTTP 头中的缓存指令。 - -## 刷新 - -- F5 跳过强缓存,会检查协商缓存 -- Command/Ctrl + F5,跳过强缓存和协商缓存 - ---- - -## cookie,localStorage,sessionStorage - -| | cookie | localStorage | sessionStorage | -| -------- | -------------------------------------------------------------------------------- | ------------------------ | -------------------------------------------------- | -| 大小 | 4k | 5M | 5M | -| 通信 | 每次请求携带在 http 请求头中 | X | X | -| 作用域 | 与所有同源窗口均共享 | 与所有同源窗口均共享 | 不在不同的浏览器窗口共享 | -| 生命周期 | 一般由服务器生成,可以自定义失效时间,如果由浏览器生成,则默认为浏览器关闭时失效 | 只要不删除,就会一直存在 | 仅在当前会话下,当你关闭窗口或关闭浏览器时都会失效 | - -重点说一下 cookie 的 secure 和 httpOnly 属性。 - -- httpOnly:顾名思义,就是不能通过 js 来获取 cookie,预防 XSS 攻击 -- secure:http 和 https 都可以携带 cookie,加上此属性后,就只能在 https 中传输 cookie,更安全 - -## 参考 - -- [前端浏览器缓存知识梳理](https://juejin.cn/post/6947936223126093861) -- [彻底理解浏览器的缓存机制](https://juejin.cn/post/6844903593275817998) diff --git "a/content/posts/eight-legged essay/http/\350\267\250\345\237\237.md" "b/content/posts/eight-legged essay/http/\350\267\250\345\237\237.md" deleted file mode 100644 index 8cf6347..0000000 --- "a/content/posts/eight-legged essay/http/\350\267\250\345\237\237.md" +++ /dev/null @@ -1,120 +0,0 @@ ---- -title: 'CORS及其他跨域方案' -date: 2022-09-22T19:13:03+08:00 -tags: [http] -draft: true ---- - -## 前言 - -同源策略(same origin policy)是由网景公司提出的最基础的安全策略。同源,就是*协议,域名和端口*。其中任意一个不同,就会产生跨域问题。 - -## CORS - -`CORS` 是 W3C 标准,的全名是 `Cross-Origin Resource Sharing`,即跨域资源共享机制,满足可以跨域请求资源,是平时最常用的解决跨域问题的方案。 - -浏览器会自动向 HTTP header 添加一个额外的请求头字段:Origin。Origin 标记了请求的站点来源: - -```http -GET https://api.target.com/demo HTTP/1.1 -Origin: https://www.source.com // <- 浏览器自己加的 -``` - -为了使浏览器允许访问跨域资源,服务器返回的 response 还需要加一些响应头字段,这些字段将显式表明此服务器是否允许这个跨域请求。 - -- Access-Control-Allow-Origin 必须的,指定可访问的源 (带 cookie 的请求不支持\*号) -- Access-Control-Allow-Methods -- Access-Control-Allow-Headers -- Access-Control-Allow-Credentials 带上 cookie,ajax/axios 也要配置 `withCredential` -- Access-Control-Expose-Headers 暴露更多可获取的响应头 -- Access-Control-Max-Age 指定预检请求有效期 - -### 处理机制 - -CORS 把请求分为了两种,简单请求和非简单请求,其中非简单请求需要先发起一次 OPTIONS 预检请求。 - -简单请求: - -1. GET/HEAD/POST 三者之一 -2. 请求头不超出以下几个字段 - - Accept - - Accept-Language - - Last-Event-ID - - Content-Language - - Content-Type:application/x-www-form-urlencoded、multipart/form-data、text/plain 三者之一 - -发起简单请求时,自动带上 origin,服务器返回数据,并返回检查结果,配置 CORS 响应头,浏览器对响应头进行检查,包含则放行,否则拦截。 - -非简单请求: - -先发起一次 options 预检请求,过程与简单请求一致,响应头没有通过检查就不会发起真正的请求。 - -只对非简单请求做预检是因为,此类请求往往会对服务器产生副作用。 - -### 为什么请求要带上 origin - -主要是因为 CORS 给开发带来了便利,同时也带来了安全隐患——CSRF 攻击。 -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210280002617.png) -如果严格按照同源政策,第 2 步的跨域请求不能进行的,也就不会造成危害。所以 CORS 策略的心智模型是:所有跨域请求都是不安全的,浏览器要带上来源给服务器检验。 - -### html 标签的 cors 配置 - -script 标签 有一个 crossorigin 属性,就是用来进行 CORS 的配置的。 -CORS 有三种值: - -- anonymous -- use-credentials,需要设置请求凭证 cookie。 -- "",就是在标签中直接使用 crossorigin,与 anonymous 效果一样 - -html 标签中 有些是自带跨域的,img,link,script 等,但是当添加了 crossorigin 后,浏览器就不能使用自带的跨域去请求资源了,而是使用 CORS,那就需要服务器设置跨域了。 - -PS:script 加载的外部脚本内发生错误时,浏览器上 onerror 事件是捕获不到具体错误信息的 ,只会捕获到 Script error 错误,添加上 crossorigin 就可以看见了,不过前提是目标源要支持 CORS 哦。 - -## jsonp - -利用 script 自带跨域的功能,动态创建 script 标签,指定 src 为目标跨域 url,在 url 中指定回调函数,当请求返回时,就会执行这个回调,从而实现跨域请求,所以这个方法只支持 GET 请求。 - -## 配置 nginx - -配置 `nginx` 代理跨域,也是常用的方法,实质和 CORS 跨域原理一样,通过配置文件设置请求响应头 `Access-Control-Allow-Origin` 等字段。。 - -1. 解决字体跨域 - 浏览器跨域访问 js、css、img 等常规静态资源被同源策略许可,但 iconfont 字体文件(eot|otf|ttf|woff|svg)例外,此时可在 nginx 的静态资源服务器中加入以下配置。 - -```nginx -location / { - add_header Access-Control-Allow-Origin *; -} -``` - -2. 反向代理 - -反向代理,代理服务器。 - -```nginx -# proxy服务器 -server { - listen 80; - server_name www.target.com; - - location / { - proxy_pass http://www.turntoweb.com:8080/; # 反向代理 目标服务器 - proxy_cookie_domain www.turntoweb.com www.target.com; # 修改cookie里域名 - index index.html index.htm; - - # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用 - #当前端只跨域不带cookie时,可为* - add_header Access-Control-Allow-Origin http://www.target.com; - add_header Access-Control-Allow-Credentials true; - } -} -``` - -以上是常用的跨域方法,还有其他的方案见文末的参考。 - -## 参考 - -- [跨域资源共享 CORS 详解](https://www.ruanyifeng.com/blog/2016/04/cors.html) -- [http 演进之路四](https://zhuanlan.zhihu.com/p/50979016) -- [CORS 为什么能保障安全?为什么只对复杂请求做预检?](https://mp.weixin.qq.com/s/W38vyzlqRtUysjguHeqiNQ) -- [9 种常见的前端跨域解决方案](https://juejin.cn/post/6844903882083024910) diff --git "a/content/posts/eight-legged essay/http/\350\276\223\345\205\245url\345\210\260\346\270\262\346\237\223.md" "b/content/posts/eight-legged essay/http/\350\276\223\345\205\245url\345\210\260\346\270\262\346\237\223.md" deleted file mode 100644 index ae18fb9..0000000 --- "a/content/posts/eight-legged essay/http/\350\276\223\345\205\245url\345\210\260\346\270\262\346\237\223.md" +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: '从输入url到页面加载完成' -date: 2022-09-22T15:33:19+08:00 -tags: [http, browser] -draft: true ---- - -## 经典面试 - -这是一道非常经典的面试题,也是每一个前端都必须掌握的基础知识。对每一个阶段了如指掌会帮助我们进行更好的性能优化。 - -下面是一个常见的图:` - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210191532653.png) - -## 加餐--现代浏览器架构 - -## 参考 - -- [Chrome 浏览器架构](https://xie.infoq.cn/article/5d36d123bfd1c56688e125ad3) -- [Inside look at modern web browser](https://developer.chrome.com/blog/inside-browser-part1/) -- [从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系](http://www.dailichun.com/2018/03/12/whenyouenteraurl.html) \ No newline at end of file diff --git a/content/posts/react/React-hooks.md b/content/posts/react/React-hooks.md deleted file mode 100644 index 1a35b55..0000000 --- a/content/posts/react/React-hooks.md +++ /dev/null @@ -1,618 +0,0 @@ ---- -title: 'React -- Hooks' -date: 2022-11-27T11:28:43 -tags: [React] -draft: true ---- - -> React 需要为共享状态逻辑提供更好的原生途径。**Hook 使你在无需修改组件结构的情况下复用状态逻辑** --- React 官网 - -> 你可以从 React 的 LOGO 中看到这些围绕着核心的电子飞行轨道,Hooks 可能一直就在其中。 --- Dan - -## hook 工作原理 - -从 `useState` 入手: - -```JSX -const [demo, setDemo] = useState({name: 'yokiizx', age: 18}) - -
- {demo.name} -
-``` - -组件更新 `useState` 的工作主要分为两种: - -- mount:调用 `ReactDOM.render`/`ReactDOM.createRoot().render()` 引起的更新,更新内容为 初始化值 -- update:setXxx 引起的更新,更新内容为 setXxx 创建的 Update。 - -### Update - -重点之一是 Update,这个在之前学习过:HostRoot 和 ClassComponent 共用一套 Update 数据结构,回顾一下: - -```js -export type Update = {| - eventTime: number, // 任务时间,通过performance.now()获取的毫秒数 - lane: Lane, // 优先级相关字段 - - tag: 0 | 1 | 2 | 3, // 四种更新类型 UpdateState | ReplaceState | ForceUpdate | CaptureUpdate - payload: any, // 更新挂载的数据:ClassComponent挂载的this.setState 的第一个参数;HostRoot挂载的ReactDOM.render的第一个参数 - callback: (() => mixed) | null, - - next: Update | null, // 与其他Update连接形成链表(比如连续setState了几下) -|}; - -type SharedQueue = {| - pending: Update | null, // Update 组成的环状链表 -|}; -// Update 最终被加入 UpdateQueue 对象中,而 fiber 保存的就是这个 updateQueue -export type UpdateQueue = {| - // 更新前状态,初始化为fiber.memoizedState - baseState: State, - firstBaseUpdate: Update | null, - lastBaseUpdate: Update | null, - // enqueueUpdate时把Update组装成单向环状链表 - shared: SharedQueue, - // 数组。保存update.callback !== null 的 Update, 在commit layout阶段执行 - effects: Array> | null, -|}; -``` - -而 Hook 使用的是另一种 Update 数据结构: - -```js -// ReactFiberHooks.old.js -type Update = {| - lane: Lane, - action: A, // 更新函数,如 () => setDemo({age: 35}) - eagerReducer: ((S, A) => S) | null, - eagerState: S | null, - next: Update, - priority?: ReactPriorityLevel, -|}; - -type UpdateQueue = {| - pending: Update | null, // 同样的,也是单向环状链表,dispatchAction 时把Update接入 - dispatch: (A => mixed) | null, // 保存 dispatchAction.bind() 的值 - lastRenderedReducer: ((S, A) => S) | null, // 上一次render时使用的reducer - lastRenderedState: S | null, // 上一次render时的state -|}; -``` - -### UpdateQueue 存储位置 - -更新产生的 Update 保存在 UpdateQueue 中,classComponent 的实例可以存储数据,它的 UpdateQueue 放在对应的 fiber.updateQueue;而 Hook 的 UpdateQueue 放在数据结构如下的 Hook 的 queue 中: - -```ts -export type Hook = {| - memoizedState: any, // 存储 hook 对应的数据 - baseState: any, - baseQueue: Update | null, - queue: UpdateQueue | null, - next: Hook | null, // 连接 hook 组成单向链表 -|}; - -/** - * 不同 hook,Hook.memoizedState 保存的是不同类型数据 -* memoizedState - * const [state, setState] = useStaet(state) 存的是 state 的值 - * const [state, dispatch] = useReducer(recuder, {}) 存的是 state 的值 - * useRef(1) 存的是 {current: 1} - * useMemo(callback, [depA]) 存的是 [callback(), depA] - * useCallback(callback, [depA]) 存的是 [callback, depA] - * - * useMemo 和 useCallback 的区别显而易见: - * 前者存的是 callback 执行后的结果;后者存的是 callback 函数本身 - * - * useContext 没有 memoizedState - * / -``` - -而 Hook 又存在 fiber.memoizedState: - -```ts -const fiber = { - // 注意 Fiber 保存的是该FunctionComponent对应的 Hooks 链表 - memoizedState: null, - // 指向 App 函数 - stateNode: App - // ... others -} -``` - -注意区分关系: - -- Fiber 对应的是组件 -- Hook 对应的是 hook 函数 -- Update 是更新过程创建出的对象 - - ClassComponent 把 UpdateQueue 挂载到 Fiber.updateQueue; - - FunctionComponent 把 UpdateQueue 挂在到 Hook.updateQueue;而 Hook 挂载在 Fiber.memoizedState。 - -### 进入 render 阶段 - -`dispatchAction` 最终会去调用 `scheduleUpdateOnFiber` 进入调度更新阶段。在 `beginWork` 时,对 `FunctionComponent` 调用 `updateFunctionComponent` 函数,最终调用 `renderWithHooks` 这个方法。 - -```js -export function renderWithHooks( - current: Fiber | null, - workInProgress: Fiber, - Component: (p: Props, arg: SecondArg) => any, - props: Props, - secondArg: SecondArg, - nextRenderLanes: Lanes, -): any { - // ... 省略其他代码 - // 设置全局变量 ReactCurrentDispatcher - ReactCurrentDispatcher.current = - current === null || current.memoizedState === null - ? HooksDispatcherOnMount - : HooksDispatcherOnUpdate; - // ... -} -``` - -### dispatcher - -从上方可知:**Hook 在 mount 和 update 阶段调用的完全不同的函数**: - -```TS -const HooksDispatcherOnMount: Dispatcher = { - useCallback: mountCallback, - useContext: readContext, - useEffect: mountEffect, - useImperativeHandle: mountImperativeHandle, - useLayoutEffect: mountLayoutEffect, - useMemo: mountMemo, - useReducer: mountReducer, - useRef: mountRef, - useState: mountState, - // ... -}; - -const HooksDispatcherOnUpdate: Dispatcher = { - useCallback: updateCallback, - useContext: readContext, - useEffect: updateEffect, - useImperativeHandle: updateImperativeHandle, - useLayoutEffect: updateLayoutEffect, - useMemo: updateMemo, - useReducer: updateReducer, - useRef: updateRef, - useState: updateState, - // ... -}; -``` - -可以看见,在 `FunctionComponent` 渲染之前,根据 mount/update 先指定了 `dispatcher` 给 `ReactCurrentDispatcher.current`,等到渲染的时候,就从当前的 dispatcher 中寻找需要的 hook。 - -> dispatcher 不仅仅只有上方两种,比如 useEffect 就会把 `ReactCurrentDispatcher.current` 指向 `ContextOnlyDispatcher`,而 `ContextOnlyDispatcher` 的 hook 都会调用 `throwInvalidHookError` 抛错。这就是为什么不能在 `useEffect` 内调用其他 hook 的原因。 - -## 各个 Hook - -### useState 和 useReducer - -如上,这两个 hook 调用的时候也分 mount 和 update 阶段。 - -1. mount - -```TS -function mountReducer( - reducer: (S, A) => S, - initialArg: I, - init?: I => S, -): [S, Dispatch] { - // 1. 创建并返回当前 hook - const hook = mountWorkInProgressHook(); - // 2. 初始化 state - let initialState; - if (init !== undefined) { - initialState = init(initialArg); - } else { - initialState = ((initialArg: any): S); - } - hook.memoizedState = hook.baseState = initialState; - // 3. 创建 queue - const queue = (hook.queue = { - pending: null, - dispatch: null, - lastRenderedReducer: reducer, - lastRenderedState: (initialState: any), - }); - // 4. 创建 dispatch ==> dispatchAction.bind() 的结果 - const dispatch: Dispatch = (queue.dispatch = (dispatchAction.bind( - null, - currentlyRenderingFiber, - queue, - ): any)); - return [hook.memoizedState, dispatch]; -} -function mountState( - initialState: (() => S) | S, -): [S, Dispatch>] { - // 1. 创建当前 hook - const hook = mountWorkInProgressHook(); - // 2. 初始化 state - if (typeof initialState === 'function') { - // $FlowFixMe: Flow doesn't like mixed types - initialState = initialState(); - } - hook.memoizedState = hook.baseState = initialState; - // 3. 创建 queue - const queue = (hook.queue = { - pending: null, - dispatch: null, - lastRenderedReducer: basicStateReducer, - lastRenderedState: (initialState: any), - }); - // 4. 创建 dispatcher - const dispatch: Dispatch> = (queue.dispatch = (dispatchAction.bind( - null, - currentlyRenderingFiber, - queue, - ): any)); - return [hook.memoizedState, dispatch]; -} -``` - -可见,`useState` 和 `useReducer` 基本是相同,只是两者 `queue` 的 `lastRenderedReducer` 不同。 - -- useReducer 使用的是传入的 reducer -- useState 使用的是 `basicStateReducer` - ```TS - function basicStateReducer(state: S, action: BasicStateAction): S { - // $FlowFixMe: Flow doesn't like mixed types - return typeof action === 'function' ? action(state) : action; - } - ``` - -2. update - -```TS -function updateReducer( - reducer: (S, A) => S, - initialArg: I, - init?: I => S, -): [S, Dispatch] { - // 获取当前hook - const hook = updateWorkInProgressHook(); // 该方法防止循环更新 - const queue = hook.queue; - - queue.lastRenderedReducer = reducer; - - // ...同update与updateQueue类似的更新逻辑 - - const dispatch: Dispatch = (queue.dispatch: any); - return [hook.memoizedState, dispatch]; -} -function updateState( - initialState: (() => S) | S, -): [S, Dispatch>] { - return updateReducer(basicStateReducer, (initialState: any)); -} -``` - -一句话:找到对应 hook,根据 update 计算该 hook 新的 state 并返回。 - -注意:初始化阶段也是有可能调用 `setXxx` 引起更新的,那么就会引起新的更新,从而产生无限循环更新,为了防止这种情况,React 使用 `didScheduleRenderPhaseUpdate` 判断是否是 `render阶段` 触发的更新。 - -调用时,回去触发 `dispatchAction`: - -```TS -function dispatchAction(fiber, queue, action) { - - // ...创建update - var update = { - eventTime: eventTime, - lane: lane, - suspenseConfig: suspenseConfig, - action: action, - eagerReducer: null, - eagerState: null, - next: null - }; - - // ...将update加入queue.pending - - var alternate = fiber.alternate; - - // currentlyRenderingFiber 即 workInProgress fiber - if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) { - // render阶段触发的更新 - didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; - } else { - if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) { - // ...fiber的updateQueue为空,优化路径 - } - - scheduleUpdateOnFiber(fiber, lane, eventTime); - } -} -``` - -一句话:创建 Update,并把它加入 queue.pending 中,然后开启调度更新。 - -### useEffect - -先看一下 Effect 这个数据结构: - -```TS -export type Effect = {| - tag: HookFlags, - create: () => (() => void) | void, // 返回的函数就是清理函数 - destroy: (() => void) | void, - deps: Array | null, - next: Effect, -|}; -``` - -当初一开始学 React,有人说可以类比 class 组件的生命周期,现在看来,这么理解实际上是不好的,不能帮我们认清真正的 React hook 逻辑。 - -回顾之前的源码学习,`useEffect` 是在 `commit` 阶段执行相关逻辑的: - -- `beforeMutation` 阶段内 `调度useEffect`;遍历并执行上一轮 render 的清理函数 - - 执行 `flushPassiveEffects`,该方法设置优先级,并执行 `flushPassiveEffectsImpl` - - `flushPassiveEffectsImpl` 方法内从全局变量 `rootWithPendingPassiveEffects` 获取 `effectList` -- `mutation` 阶段内 `调度useEffect的清理函数` -- `layout` 阶段之后 执行之前的调度后的回调函数和清理函数 - -对比 `useLayoutEffect`: - -- `mutation` 阶段的 update 操作内执行上一轮更新的清理函数 -- `layout` 后执行回调函数 - -**useEffect 清理函数和回调函数的执行:** -`useEffect|useLayoutEffect` 的执行需要保证所有组件 `useEffect` 的*清理函数必须都执行完后*才能执行任意一个组件的 `useEffect` 的回调函数 --- 否则其他组件可能会产生影响,比如多个组件间可能共用同一个 `ref`。如果不按照 `全销毁再--->全执行` 的顺序,假如某个清理函数内修改了 `ref.current`,会影响到其它组件中 `useEffect` 回调函数中相同 ref 的 current 属性。 - -- 清理函数的执行 - -```js -// pendingPassiveHookEffectsUnmount 中保存了所有需要执行销毁的 useEffect -const unmountEffects = pendingPassiveHookEffectsUnmount; - pendingPassiveHookEffectsUnmount = []; - for (let i = 0; i < unmountEffects.length; i += 2) { - const effect = ((unmountEffects[i]: any): HookEffect); // 偶数存 effect - const fiber = ((unmountEffects[i + 1]: any): Fiber); // 奇数存 fiber - const destroy = effect.destroy; - effect.destroy = undefined; - - if (typeof destroy === 'function') { - try { - startPassiveEffectTimer(); - //销毁上一轮 render 的 effect - destroy(); - } finally { - recordPassiveEffectDuration(fiber); - } - } - } -``` - -这里 `pendingPassiveHookEffectsUnmount` 是在 commit layout 阶段通过 `commitLayoutEffectOnFiber` 即 `commitLifeCycles` 中的`schedulePassiveEffects` 方法向其内 push 数据: - -```TS -function schedulePassiveEffects(finishedWork: Fiber) { - const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); - const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - let effect = firstEffect; - do { - const {next, tag} = effect; - if ( - (tag & HookPassive) !== NoHookEffect && - (tag & HookHasEffect) !== NoHookEffect - ) { - // 向`pendingPassiveHookEffectsUnmount`数组内`push`要销毁的effect - enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); - // 向`pendingPassiveHookEffectsMount`数组内`push`要执行回调的effect - enqueuePendingPassiveHookEffectMount(finishedWork, effect); - } - effect = next; - } while (effect !== firstEffect); - } -} -``` - -- 回调函数的执行 - -```js -const mountEffects = pendingPassiveHookEffectsMount; -pendingPassiveHookEffectsMount = []; -for (let i = 0; i < mountEffects.length; i += 2) { - const effect = ((mountEffects[i]: any): HookEffect); - const fiber = ((mountEffects[i + 1]: any): Fiber); - try { - const create = effect.create; - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - effect.destroy = create(); // 执行回调函数并 创建 新的清理函数 - } finally { - recordPassiveEffectDuration(fiber); - } - } else { - effect.destroy = create(); - } - } catch (error) { - captureCommitPhaseError(fiber, error); - } -} -``` - -### useRef - -`ref` 是 `reference`(引用) 的缩写,在 Vue 和 React 中都有它的一席之地,最初的习惯是用它来保存 DOM,进而进行一些 DOM 操作。实际上,任何需要被引用的数据都可以保存到 `ref` 中。 - -`useRef(state)` 对应 hook 的 `memoizedState` 保存的就是 `{current: state}`。 - -```js -function mountRef(initialValue: T): {|current: T|} { - // 获取当前useRef hook - const hook = mountWorkInProgressHook(); - // 创建ref - const ref = {current: initialValue}; - hook.memoizedState = ref; - return ref; -} - -function updateRef(initialValue: T): {|current: T|} { - // 获取当前useRef hook - const hook = updateWorkInProgressHook(); - // 返回保存的数据 - return hook.memoizedState; -} -``` - -可见,useRef 就是返回一个形如 `{current: state}` 这么个对象。 - -**Ref 的工作流程** - -- render 阶段给 fiber 添加 Ref flags - -```js -// beginWork -function markRef(current: Fiber | null, workInProgress: Fiber) { - const ref = workInProgress.ref; - if ( - (current === null && ref !== null) || - (current !== null && current.ref !== ref) - ) { - // Schedule a Ref effect - workInProgress.flags |= Ref; - } -} -// completeWork -function markRef(workInProgress: Fiber) { - workInProgress.flags |= Ref; -} -``` - -- commit 阶段对具有 Ref flags 的 fiber 执行对应的操作 - -```js -// commit mutation 阶段对于ref属性改变的情况会先移除之前的 ref -function commitDetachRef(current: Fiber) { - const currentRef = current.ref; - if (currentRef !== null) { - if (typeof currentRef === 'function') { - // function类型ref,调用他,传参为null - currentRef(null); - } else { - // 对象类型ref,current赋值为null - currentRef.current = null; - } - } -} -// commit layout阶段 会进行 ref 赋值 -function commitAttachRef(finishedWork: Fiber) { - const ref = finishedWork.ref; - if (ref !== null) { - // 获取ref属性对应的Component实例 - const instance = finishedWork.stateNode; - let instanceToUse; - switch (finishedWork.tag) { - case HostComponent: - instanceToUse = getPublicInstance(instance); - break; - default: - instanceToUse = instance; - } - - // 赋值ref - if (typeof ref === 'function') { - ref(instanceToUse); - } else { - ref.current = instanceToUse; - } - } -} -``` - -### useMemo 和 useCallback - -这两 hook,最大的区别从 hook.memoizedState 存储值就能看出区别: - -- useMemo:将回调函数的结果作为 value 保存 -- useCallback:将回调函数作为 value 保存 - -```TS -// mount -function mountMemo( - nextCreate: () => T, - deps: Array | void | null, -): T { - // 创建并返回当前hook - const hook = mountWorkInProgressHook(); - const nextDeps = deps === undefined ? null : deps; - // 计算value - const nextValue = nextCreate(); - // 将value与deps保存在hook.memoizedState - hook.memoizedState = [nextValue, nextDeps]; - return nextValue; -} -function mountCallback(callback: T, deps: Array | void | null): T { - // 创建并返回当前hook - const hook = mountWorkInProgressHook(); - const nextDeps = deps === undefined ? null : deps; - // 将value与deps保存在hook.memoizedState - hook.memoizedState = [callback, nextDeps]; - return callback; -} -// update -function updateMemo( - nextCreate: () => T, - deps: Array | void | null, -): T { - // 返回当前hook - const hook = updateWorkInProgressHook(); - const nextDeps = deps === undefined ? null : deps; - const prevState = hook.memoizedState; - - if (prevState !== null) { - if (nextDeps !== null) { - const prevDeps: Array | null = prevState[1]; - // 判断update前后value是否变化 - if (areHookInputsEqual(nextDeps, prevDeps)) { - // 未变化 - return prevState[0]; - } - } - } - // 变化,重新计算value - const nextValue = nextCreate(); - hook.memoizedState = [nextValue, nextDeps]; - return nextValue; -} -function updateCallback(callback: T, deps: Array | void | null): T { - // 返回当前hook - const hook = updateWorkInProgressHook(); - const nextDeps = deps === undefined ? null : deps; - const prevState = hook.memoizedState; - - if (prevState !== null) { - if (nextDeps !== null) { - const prevDeps: Array | null = prevState[1]; - // 判断update前后value是否变化 - if (areHookInputsEqual(nextDeps, prevDeps)) { - // 未变化 - return prevState[0]; - } - } - } - - // 变化,将新的callback作为value - hook.memoizedState = [callback, nextDeps]; - return callback; -} -``` - -## 参考 - -- [React 官网](https://zh-hans.reactjs.org/docs/hooks-intro.html) -- [React 博客](https://zh-hans.reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing) -- [Dan blog - A Complete Guide to useEffect](https://overreacted.io/a-complete-guide-to-useeffect/) diff --git a/content/posts/react/React-setState.md b/content/posts/react/React-setState.md deleted file mode 100644 index 23e6da2..0000000 --- a/content/posts/react/React-setState.md +++ /dev/null @@ -1,229 +0,0 @@ ---- -title: 'React基础原理 - setState' -date: 2022-11-23T09:30:25+08:00 -tags: [React] -draft: true ---- - -触发状态更新,前面说了五种: - -- ReactDOM.render —— HostRoot -- this.setState —— ClassComponent -- this.forceUpdate —— ClassComponent -- useState —— FunctionComponent -- useReducer —— FunctionComponent - -今天重点说一下 `setState`: - -setState 最终调用的是 `this.updater.enqueueSetState(this, partialState, callback, 'setState')`。 - -首先要明确一点,针对 React 中的 state,不像 vue 那样,是没有做任何数据绑定的,当 state 发生变化时,是发生了一次重新渲染,**每一次渲染都能拿到独立的 state 状态,这个状态值是函数中的一个常量**。 - -```js -const classComponentUpdater = { - isMounted, - enqueueSetState(inst, payload, callback) { - // 通过组件实例获取对应 fiber - const fiber = getInstance(inst); - const eventTime = requestEventTime(); - - // 获取优先级 - const lane = requestUpdateLane(fiber); - - // 创建 update 对象 把 payload 和 callback 添加到 update 对象上 - const update = createUpdate(eventTime, lane); - update.payload = payload; - if (callback !== undefined && callback !== null) { - update.callback = callback; - } - - // 将 update 插入 updateQueue - enqueueUpdate(fiber, update); - - // ******* 调度 update ******* - scheduleUpdateOnFiber(fiber, lane, eventTime); - - if (enableSchedulingProfiler) { - markStateUpdateScheduled(fiber, lane); - } - }, - enqueueReplaceState(inst, payload, callback) { - // ... - }, - // this.forceUpdate 使用这个方法,与enqueueSetState唯二的不同就是 - // 1. update.tag = ForceUpdate 2. 没有 payload - // 所以,当某次更新含有tag为ForceUpdate的Update,那么当前ClassComponent不会受其他性能优化手段 - //(shouldComponentUpdate|PureComponent)影响,一定会更新。 - enqueueForceUpdate(inst, callback) { - // ... - }, -}; -``` - -## 关于 setState 的同步异步问题(v18 分割线) - -首先明确一点:无论哪个版本,`setState` 这个函数自身始终是同步的。 -关于 `setState` 同步异步问题一般指的是由其引发的更新渲染是同步还是异步的,而今时今日(v18 已经发布),对于这个问题要分为 v18 之前 和 v18 之后去看待了。 - -### v18 之前 - -**结论:** - -- `setTimeout`、`setInterval`、`DOM2级事件回调`、`Promise.then()的回调` 中,`setState` 触发的状态更新是同步的 -- 其它直接处在 React 生命周期和 React 合成事件内的 `setState` 状态更新都是异步的 - -```js -// 老版的 批量更新 -export function batchedUpdates(fn: A => R, a: A): R { - const prevExecutionContext = executionContext; - // 当脱离了 React 的环境,就获取不到 BatchedContext 上下文 - executionContext |= BatchedContext; - try { - return fn(a); - } finally { - executionContext = prevExecutionContext; - // 所以下面就会同步执行 - if (executionContext === NoContext) { - // Flush the immediate callbacks that were scheduled during this batch - resetRenderTimer(); - flushSyncCallbackQueue(); - } - } -} -``` - -### v18 之后 - -**结论:** - -- 默认所有的 `setState` 会自动进行批处理,表现都是异步渲染的。 - -```js -// 新版批量更新 依赖于 lane 模型:ensureRootIsScheduled 方法内 -// Check if there's an existing task. We may be able to reuse it. -if (existingCallbackNode !== null) { - const existingCallbackPriority = root.callbackPriority; - if (existingCallbackPriority === newCallbackPriority) { // 相同级别的 setState 后续都给return掉了 - // The priority hasn't changed. We can reuse the existing task. Exit. - return; - } - // The priority changed. Cancel the existing callback. We'll schedule a new - // one below. - cancelCallback(existingCallbackNode); -} -``` - -### 各种现象的原因 - -- `setState` 传对象/函数差异的原因: - 传对象,在同一周期内会对多个 setState 进行批处理,效果类似于 Object.assign;而传入一个函数由于 setState 的调用是分批的,所以可以链式地进行更新,确保它们是一个建立在另一个之上的。 -- 产生异步渲染现象的原因: - 如上一条,是由于`setState` 的批处理,这么做是为了避免中间状态引发不必要的渲染,等所有 `setState` 完成后再渲染,从而提升性能。 -- v18 表现和以前不同的原因: - 使用了 `ReactDOM.createRoot` 创建应用后, 所有的更新都会自动进行批处理;`ReactDOM.render` 则保持着和以前一样的行为。 - -如果想要同步的更新行为,使用 `react-dom 的 flushSync`,把 `setState` 用 `flushSync` 的回调包裹一下。 - -```js -// 注意写法,在同一个 flushSync内是无效的 -import { flushSync } from 'react-dom'; - -flushSync(() => { - this.setState({count: 1}) -}) -console.log(this.state.count) // 1 -flushSync(() => { - this.setState({count: 2}) -}) -console.log(this.state.count) // 2 -``` - -### 从源码看现象 - -从之前的文章中应该很清楚,状态更新都会走入 `scheduleUpdateOnFiber` 这个方法,此处我们关注这里: - -```js -// scheduleUpdateOnFiber 部分代码: -// export const NoContext = 0b0000000; 是一个二进制常量 -// let executionContext = NoContext; 也是 executionContext 的初始值 -if (executionContext === NoContext) { - // 把回调执行完,然后清空队列,见下方 - flushSyncCallbackQueue(); -} - -// flushSyncCallbackQueue 的核心: -runWithPriority(ImmediatePriority, () => { - for (; i < queue.length; i++) { - let callback = queue[i]; - do { - callback = callback(isSync); - } while (callback !== null); - } -}); -syncQueue = null; - -// syncQueue 是什么? 存的是调用 scheduleSyncCallback(callback) 里的 callback -// 在源码中就是 ensureRootIsScheduled 内 performSyncWorkOnRoot方法,这就和之前串起来了 -newCallbackNode = scheduleSyncCallback( - performSyncWorkOnRoot.bind(null, root), -); -``` - -当 `executionContext` 值为 `NoContext` 时,表示非批量,会同步执行同步更新策略,否则为异步更新。 - ---- - -`setState` 的合并策略见 `getStateFromUpdate` 这个方法: - -```js -function getStateFromUpdate( workInProgress: Fiber, - queue: UpdateQueue, - update: Update, - prevState: State, - nextProps: any, - instance: any) { - // 根据 update 的tag 分别处理 - switch(update.tag) { - //... - case UpdateState: { - const payload = update.payload; - let partialState; - if (typeof payload === 'function') { - // Updater function - partialState = payload.call(instance, prevState, nextProps); - } else { - // Partial state object - partialState = payload; - } - // Null and undefined are treated as no-ops. - if (partialState === null || partialState === undefined) { - return prevState; - } - // Merge the partial state and the previous state. - return Object.assign({}, prevState, partialState); - } - //... - } -} -``` - -### hook 中的 "setState" - -它的更新流程最终也是要走入 `scheduleUpdateOnFiber`,所以与 `this.setState` 的区别不大。但是,hook 的 "setState" 不会做自动的合并。 - -```js -this.state = { - name: 'yokiizx', - age: 18 -} -this.setState({age: 35}) // state = {name: 'yokiizx', age: 35} - -const [demo, setDemo] = useState({name: 'yokiizx', age: 18}) -setDemo({age: 35}) // state = {age: 35} -``` - -## 参考 - -- [官网 setState](https://zh-hans.reactjs.org/docs/react-component.html#setstate) -- [官网 setState 什么时候是异步的](https://zh-hans.reactjs.org/docs/faq-state.html#when-is-setstate-asynchronous) -- [Automatic batching for fewer renders in React 18 ](https://github.com/reactwg/react-18/discussions/21) diff --git "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\206.md" "b/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\206.md" deleted file mode 100644 index b28f3d8..0000000 --- "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\206.md" +++ /dev/null @@ -1,431 +0,0 @@ ---- -title: 'React基础原理1 - JSX' -date: 2022-11-07T16:29:27+08:00 -tags: [React] -draft: true ---- - -PS:阅读源码前,推荐一个 VsCode 插件,`bookmarks`,比较方便,谁用谁知道。 - -### React 大事件 - -- React 16 引入 fiber 架构 - - 引入 Fiber - - 可以中途中断更新,Reconciler 和 Renderer 的交替工作在 v16 之前是递归不可中断的 - - 生命周期变化 -- React 16.8 引入 hooks -- React 17 - - 全新的 JSX 转换(源代码中无需引入 React 了) - - 事件委托的变更(之前是委托在 document 上,现在是渲染树的根容器) - - 事件系统相关更改(onFocus 和 onBlur 事件已在底层切换为原生的 focusin 和 focusout 事件) -- React 18 - - 不再兼容 ie11 - - 并发特性 - - ReactDome.render(, root) 改为使用 ReactDom.createRoot(root).render() - - setState 自动批处理 - -> 详细的见[React 官方博客](https://zh-hans.reactjs.org/blog/all.html) - -### JSX 怎么生成 Element - -点击查看[Facebook 关于 JSX 的介绍](https://facebook.github.io/jsx/#sec-intro)。 - -```JSX -// React 17 以前 -const demo = ( -
- hello - world -
-) - -// @babel/plugin-transform-react-jsx 会把上方jsx会被转化为: -const demo = React.createElement( - 'div', - null, - React.createElement( - 'span', - {className: '1'}, - 'hello' - ), - React.createElement( - 'span', - {className: '2'}, - 'world' - ) -) -``` - -朴实无华的 API `createElement`,如名字一样,就是用来创建 element 的。 `React.createElement(type, config, children)`,这也是为什么 React17 之前每个文件需要手动引入 React,React17 之后则不需要了。 - -```js -export function createElement(type, config, children) { - let propName; - - // Reserved names are extracted - const props = {}; - - let key = null; - let ref = null; - let self = null; - let source = null; - - if (config != null) { - if (hasValidRef(config)) { - ref = config.ref; - } - if (hasValidKey(config)) { - key = '' + config.key; - } - - self = config.__self === undefined ? null : config.__self; - source = config.__source === undefined ? null : config.__source; - - // Remaining properties are added to a new props object - for (propName in config) { - if ( - hasOwnProperty.call(config, propName) && - !RESERVED_PROPS.hasOwnProperty(propName) - ) { - props[propName] = config[propName]; - } - } - } - - // Children can be more than one argument, and those are transferred onto - // the newly allocated(被分配) props object. - const childrenLength = arguments.length - 2; - if (childrenLength === 1) { - props.children = children; - } else if (childrenLength > 1) { - const childArray = Array(childrenLength); - for (let i = 0; i < childrenLength; i++) { - childArray[i] = arguments[i + 2]; - } - props.children = childArray; - } - - // Resolve default props - if (type && type.defaultProps) { - const defaultProps = type.defaultProps; - for (propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; - } - } - } - - return ReactElement( - type, - key, - ref, - self, - source, - ReactCurrentOwner.current, // null || fiber - props, - ); -} -``` - -上面的代码比较简单,认认真真看,还好很好理解的,其实就是对 JSX 进行了解析,将参数整合后传给 `ReactElement` 使用,简单总结一下: - -- 把 `config` 中除了 `key`,`ref`,`__self`,`__source` 的值赋值给了 `props`,(`__self`,`__source`是开发环境使用的) -- `children` 也被加入到 `props`,然后这个 `props` 被传递给 `ReactElement` 使用 - -再来看看 `ReactElement`: - -```js -const ReactElement = function(type, key, ref, self, source, owner, props) { - const element = { - // This tag allows us to uniquely identify this as a React Element - $$typeof: REACT_ELEMENT_TYPE, // react元素标识,源码中使用了一个十六进制表示,如果原生支持Symbol,会使用Symbol来标识 - - // Built-in properties that belong on the element - type: type, - key: key, - ref: ref, - props: props, - - // Record the component responsible for creating this element. - _owner: owner, - }; - - return element; -}; - -// 辨识一个 React 元素 -export function isValidElement(object) { - return ( - typeof object === 'object' && - object !== null && - object.$$typeof === REACT_ELEMENT_TYPE - ); -} -``` - -也很简单,就是返回了一个对象。 - ---- - -注意: React17 后添加了全新的 JSX 转换,不过 React.createElement 仍然保留,所以了解一下也是挺好的。 - -全新的 JSX 转换方法使得无需每个组件都引入 React 就能解析 JSX,但是要使用 React 导出的其他方法或 hook 还是需要引入的。 - -- [介绍全新的 JSX 转换](https://zh-hans.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) -- [@babel/plugin-transform-react-jsx](https://www.babeljs.cn/docs/babel-plugin-transform-react-jsx#react-automatic-runtime) - -```js -function App() { - return

Hello World

; -} -// ↓ ↓ ↓ ↓ ↓ -// 由编译器引入(禁止自己引入!) 见上方的babel插件 -// 这些入口只会被 Babel 和 TypeScript 等编译器使用 -import {jsx as _jsx} from 'react/jsx-runtime'; - -function App() { - return _jsx('h1', { children: 'Hello world' }); -} -``` - -看看源码: - -```js -// 1.react/jsx/ReactJSXElement ReactElement与上方的基本一样 -// 2.替代 React.createElement()的jsx() -export function jsx(type, config, maybeKey) { - let propName; - - // Reserved names are extracted - const props = {}; - - let key = null; - let ref = null; - - // Currently, key can be spread in as a prop. This causes a potential - // issue if key is also explicitly declared (ie.
- // or
). We want to deprecate key spread, - // but as an intermediary step, we will use jsxDEV for everything except - //
, because we aren't currently able to tell if - // key is explicitly declared to be undefined or not. - if (maybeKey !== undefined) { - key = '' + maybeKey; - } - if (hasValidKey(config)) { - key = '' + config.key; - } - if (hasValidRef(config)) { - ref = config.ref; - } - - // 提取除key,ref的其他props和默认属性defaultProps到 props 中 - for (propName in config) { - if ( - hasOwnProperty.call(config, propName) && - !RESERVED_PROPS.hasOwnProperty(propName) - ) { - props[propName] = config[propName]; - } - } - if (type && type.defaultProps) { - const defaultProps = type.defaultProps; - for (propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; - } - } - } - - return ReactElement( - type, - key, - ref, - undefined, - undefined, - ReactCurrentOwner.current, - props, - ); -} -``` - -最大的差别就是 children 变成了 maybeKey,官网上说这个主要就是为了做一些:[性能优化和简化](https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md#motivation)。 - -### ReactDom.render(el, container, cb) - -这个 api 在 React18 之前使用,也是有必要学习一下的。 - -```js -// render 就是调用了 legacyRenderSubtreeIntoContainer 个方法 -export function render( - element: React$Element, - container: Container, - callback: ?Function, -): React$Component | PublicInstance | null { - // ... - return legacyRenderSubtreeIntoContainer( - null, - element, - container, - false, - callback, - ); -} - -/** - * @desc 渲染子树到容器中 - * @param parentComponent - 父组件,ReactDom.render的时候传 null - * @param children - 待渲染 dom (经过解析后的 ReactElement) - * @param container - 容器 dom - * @param forceHydrate - true-服务端渲染; false-客户端渲染 - * @param callback - 渲染完成后回调函数 - */ -function legacyRenderSubtreeIntoContainer( - parentComponent: ?React$Component, - children: ReactNodeList, - container: Container, - forceHydrate: boolean, - callback: ?Function, -): React$Component | PublicInstance | null { - - // 在 legacyCreateRootFromDOMContainer 后续调用了几个api返回了 FiberRoot - // 这个 FiberRoot 被挂载在 container._reactRootContainer - const maybeRoot = container._reactRootContainer; - - let root: FiberRoot; - if (!maybeRoot) { - // Initial mount - root = legacyCreateRootFromDOMContainer( // 创建 FiberRoot - container, - children, - parentComponent, - callback, - forceHydrate, - ); - } else { - root = maybeRoot; - if (typeof callback === 'function') { - const originalCallback = callback; - callback = function() { - const instance = getPublicRootInstance(root); - originalCallback.call(instance); - }; - } - // Update - updateContainer(children, root, parentComponent, callback); - } - return getPublicRootInstance(root); -} -``` - -大致流程:`ReactDom.render` 返回 `legacyRenderSubtreeIntoContainer` 函数执行的结果。这个函数会先调用 `legacyCreateRootFromDOMContainer` 来创建 `FiberRoot`,在初始化过程中,调用 `createContainer` 方法,在更新过程中调用 `updateContainer`,最后返回 `getPublicRootInstance(root)` 的结果。 - -`createContainer`,`updateContainer`,`getPublicRootInstance` 这三个方法都引自 `ReactFiberReconciler.old.js`。从源码中能看见最终调用的是 `createFiberRoot` 这个方法,接着调用 `new FiberRootNode()` 最终创建了这个 FiberRoot。 - -> react-reconciler 有 xx.old.js 和 xx.new.js 之分,两个都在维护,由 ReactFeatureFlags 中的 enableNewReconciler 变量来控制使用哪种(默认为 false 使用从 old 中导出的)。主要目的是为了向前兼容、并且不影响之前和之后代码的稳定性。 - -#### ReactDom.createRoot() - -React18 引入的方法,取代了 `ReactDom.render`,在 `packages/react-dom/src/client/ReactDOMRoot.js` 文件中。 - ---- - -### React.Element 和 React.Component 的关系 - -1. ClassComponent 和 pureComponent,都导出自 ReactBaseClasses.js - -```js -function Component(props, context, updater) { - this.props = props; - this.context = context; - // If a component has string refs, we will assign a different object later. - this.refs = emptyObject; - // We initialize the default updater but the real one gets injected by the renderer. - this.updater = updater || ReactNoopUpdateQueue; -} -Component.prototype.isReactComponent = {}; - -/* ---------- pureComponent ---------- */ -// 这里就是用来做寄生组合基础的 -function ComponentDummy() {} -ComponentDummy.prototype = Component.prototype; -/** - * Convenience component with default shallow equality check for sCU. - */ -function PureComponent(props, context, updater) { - this.props = props; - this.context = context; - // If a component has string refs, we will assign a different object later. - this.refs = emptyObject; - this.updater = updater || ReactNoopUpdateQueue; -} - -const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy()); -pureComponentPrototype.constructor = PureComponent; -// Avoid an extra prototype jump for these methods. -assign(pureComponentPrototype, Component.prototype); -pureComponentPrototype.isPureReactComponent = true; -``` - -> 注意,无法通过引用类型来判断一个组件是 class 组件还是 function 组件,因为他两都是 Funciton,所以源码中才添加了这个 `Component.prototype.isReactComponent = {};` 这个。 - -### ReactElement 如何转为真实 DOM - -上面知道了 react 是如何把我们写的 jsx 变成 ReactElement,那么究竟是怎么变成真实 DOM 的呢?这是一个比较精密的过程。 - -JSX 转为的 ReactElement 只是一个简单的数据结构,携带着 key,ref 和其他的 dom 上的 attr,v17 以前还携带者 children。但是 ReactElement 始终不包含以下信息: - -- 组件在更新中的 `优先级` -- 组件的 `state` -- 组件被打上的用于更新的标记 - -这些内容都包含在 Fiber 节点中。 - -所以: - -- 在组件 mount 时,Reconciler 根据 JSX 描述的组件内容生成组件对应的 Fiber 节点。 -- 在组件 update 时,Reconciler 将 JSX 与 Fiber 节点保存的数据对比,生成组件对应的 Fiber 节点,并根据对比结果为 Fiber 节点打上标记。然后再进入 render 阶段。 - -### 三大件 - -从上方清楚了 JSX 转为 ReactElement 的过程,剩下的就交给三大件吧: - -- Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入 Reconciler - - 是独立于 React 单独的包,react16 后加入 - - 功能类似于 `requestIdleCallback` 这个 api,但是兼容性更好,并且触发频率稳定 - - 除了在空闲时触发回调的功能外,Scheduler 还提供了多种调度优先级供任务设置 -- Reconciler(协调器)—— 负责找出变化的组件 - - React 15, 协调器是递归处理处理虚拟 DOM,16 后可以中断/恢复了,看代码: - ```js - function workLoopConcurrent() { - // Perform work until Scheduler asks us to yield - while (workInProgress !== null && !shouldYield()) { - performUnitOfWork(workInProgress); - } - } - ``` - - React 16 解决中断更新时 DOM 渲染不完全的方法是,Reconciler 与 Renderer 不再是交替工作。当 Scheduler 将任务交给 Reconciler 后,Reconciler 会为变化的虚拟 DOM 打上代表增/删/更新的标记。 - ```js - // ReactFiberFlags.js 中 - export const Placement = /* */ 0b0000000000010; - export const Update = /* */ 0b0000000000100; - export const PlacementAndUpdate = /* */ 0b0000000000110; - export const Deletion = /* */ 0b0000000001000; - ``` - 整个 Scheduler 与 Reconciler 的工作都在内存中进行。只有当所有组件都完成 Reconciler 的工作,才会统一交给 Renderer。 -- Renderer(渲染器)—— 负责将变化的组件渲染到页面上 - - 根据 Reconciler 打的标记对 DOM 进行操作 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202211111612280.png) -其中红框中的步骤随时可能由于以下原因被中断: - -- 有其他更高优任务需要先更新 -- 当前帧没有剩余时间 - -由于红框中的工作都在内存中进行,不会更新页面上的 DOM,所以即使反复中断,用户也不会看见更新不完全的 DOM。 - -## 参考 - -- [十五分钟读懂 React 17](https://juejin.cn/post/6894204813970997256) -- [React18 新特性解读 & 完整版升级指南](https://juejin.cn/post/7094037148088664078) -- [深入理解 JSX](https://react.iamkasong.com/preparation/jsx.html) -- [卡颂大佬的 React 技术揭秘](https://react.iamkasong.com/) diff --git "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2062.md" "b/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2062.md" deleted file mode 100644 index 7ec24d8..0000000 --- "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2062.md" +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: 'React基础原理2 - Fiber' -date: 2022-11-11T16:16:15+08:00 -tags: [React] -draft: true ---- - -## Fiber - -三大件之一的 Reconciler 在 React16 之后是需要依靠 Fiber 这个数据结构去实现的。 - -首先 Fiber 自身就是一种数据结构,每个 ReactElement 都对应着一个 Fiber 节点;让 Reconciler 支持任务的不同`优先级`,可中断与恢复,并且恢复后可以复用之前的 `中间状态`。 -相比 ReactElement,Fiber 存储了对应的组件类型和 DOM 节点等信息,以及本次更新中该组件`要改变的状态`和`要更新的动作`。 - -那么来看看 Fiber 的真实面容: - -```js -function FiberNode( - tag: WorkTag, - pendingProps: mixed, - key: null | string, - mode: TypeOfMode, -) { - /* 作为静态数据结构的属性 */ - this.tag = tag; // 标记 Fiber 类型 FunctionComponent/ClassComponent/HostRoot...共25种(17.0.2) - this.key = key; // ReactElement 里的 key - this.elementType = null; // ReactElement.type,也就是我们调用 createElement 的第一个参数 - this.type = null; // 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName - this.stateNode = null; // 跟当前Fiber相关的本地状态(比如浏览器环境就是DOM节点) - - /* Fiber 树 */ - this.return = null; // 指向它在 Fiber 树中的 parent,用来在处理完这个节点之后向上返回 - this.child = null; // 指向自己的第一个子节点,单链表结构 - this.sibling = null; // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点 - this.index = 0; // 在多节点 diff 时会用到, 记录fiber在同级中的索引位置 - - this.ref = null; // ref 属性 - - /* 保存本次更新造成的状态改变相关信息 */ - this.pendingProps = pendingProps; // 新的变动带来的新的props - this.memoizedProps = null; // 上一次渲染时的 props - this.updateQueue = null; // 该Fiber对应的组件产生的Update会存放在这个队列里面; classComponent的UpdateQueue - this.memoizedState = null; // 上一次渲染时的 state; Hook组成的单链表,存这该hook下的UpdateQueue - this.dependencies = null; - - this.mode = mode; - - /* 保存本次更新会造成的DOM操作 */ - this.flags = NoFlags; - this.nextEffect = null; - - this.firstEffect = null; - this.lastEffect = null; - - /* 调度优先级 */ - this.lanes = NoLanes; // 2020.05 expirationTime的优先级模型被lanes取代 - this.childLanes = NoLanes; - - this.alternate = null; // 对应关系: currentFiber <==> workInProgressFiber 在渲染完成之后他们会交换位置 -} -``` - -## Fiber 架构原理 - -React 中最多存在两条 Fiber 树: - -- currentFiber 树,当前在屏幕上内容对应的 Fiber 树 -- workInProgressFiber 树,正在内存中构建的 Fiber 树。 - -```js -// 两棵树通过 alternate 连接 -currentFiber.alternate === workInProgressFiber; -workInProgressFiber.alternate === currentFiber; -``` - -这两棵树都有各自的 rootFiber,React 应用根节点 FiberRoot 通过 curret 指针在这两个树的 rootFiber 上切换。 -其实就是 ”我长大后,就成了你“,workInProgressFiber 树的最终归属是交给 renderer 拿去渲染后呈现给用户,这个时候就 ”成长“ 为了 currentFiber 树了,而原先的 currentFiber 退居幕后变成了 workInPorgressFiber,等待着下一次”成长“。 - -## mount/update 流程 - -```JSX -function App() { - const [num,setNum] = useState(0) - - return ( -

setNum(num + 1)}>{num}

- ) -} -``` - -在 ReactDome.render 的时候最终会: - -1. 调用 `createFiberRoot`,`new FiberRootNode` 去创建一个根节点,这个根节点暂且称为 `FiberRoot` -2. `createFiberRoot` 内部创建了 `FiberRoot` 后,调用 `createHostRootFiber`,创建 `RootFiber`,并加入初始化更新队列去 - - ```js - export function createFiberRoot( - containerInfo: any, - tag: RootTag, - ... - ): FiberRoot { - const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); - if (enableSuspenseCallback) { - root.hydrationCallbacks = hydrationCallbacks; - } - - // 创建未初始化的 rootFiber 并且将 fiberRoot 和 rootFiber 联系起来 - const uninitializedFiber = createHostRootFiber(tag); - root.current = uninitializedFiber; // 修改 FiberRoot 指向 RootFiber - uninitializedFiber.stateNode = root; // RootFiber 的 stateNode 指向 FiberRoot, stateNode is any. - - // 把未初始化的RootFiber加入初始化更新队列 - initializeUpdateQueue(uninitializedFiber); - return root; - } - - // ReactUpdateQueue.old.js - export function initializeUpdateQueue(fiber: Fiber): void { - const queue: UpdateQueue = { - baseState: fiber.memoizedState, - firstBaseUpdate: null, - lastBaseUpdate: null, - shared: { - pending: null, - }, - effects: null, - }; - fiber.updateQueue = queue; - } - ``` - -> `fiberRootNode` `是整个应用的根节点,rootFiber` 是 所在组件树的根节点。 - -### mount - -从源码中可以看到,在首屏渲染时,创建了 fiberRootrootFiber,此时的页面中还没有挂载任何 DOM,所以 rootFiber 还没有任何子 Fiber 节点。 - -进入 render 阶段时,根据组件返回的 JSX 在内存中依次创建 Fiber 节点并连接在一起构建 Fiber 树,这就是 workInProgressFiber 树。**每次构建 workInProgressFiber 树时,都会尝试复用 currentFiber 树已有 Fiber 节点的数据,决定是否复用的过程就是 Diff 算法**。 - -进入 commit 阶段,把 workInProgressFiber 树渲染到页面上,fiberRoot 的 current 指向 workInProgressFiber 树,workInProgressFiber 树变成了 currentFiber 树。 -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202211130020382.png) - -### update - -更新时,与 mount 阶段几乎一样,只不过此时的 currentFiber 的 rootFiber 已经有了子节点了,workInProgressFiber 会尽量复用 currentFiber 节点的数据。 -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202211130020090.png) - -## 参考 - -- [React 源码](https://github.com/facebook/react) -- [卡颂大佬的 React 技术揭秘](https://react.iamkasong.com/) diff --git "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2063.md" "b/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2063.md" deleted file mode 100644 index b2828ff..0000000 --- "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2063.md" +++ /dev/null @@ -1,324 +0,0 @@ ---- -title: 'React基础原理3 - Update' -date: 2022-11-13T10:09:22+08:00 -tags: [React] -draft: true ---- - -React 工作的整个流程: - -```txt -触发状态更新 ---> ReactDOM.render()进入updateContainer后/this.setState()调用`this.updater.enqueueSetState` 创建 Update 对象 ---> 从触发更新的fiber回溯到fiberRoot,触发调度 ---> render ---> commit -``` - -在 JSX 拥有了 ReactElement,ReactElement 进化为 Fiber 后(render 阶段),就要被渲染进入视野了(commit 阶段)。render 阶段是协调器 Reconciler 发挥作用,commit 阶段是渲染器 Renderer 发挥作用。 - -但是在 render 阶段之前,我们需要一个阶段,去创建 update 对象。 - -## updateContainer - -在 `render --> legacyRenderSubtreeIntoContainer` 方法中,创建完 fiberRoot 后,就会调用 `updateContainer` 方法,创建 Update 对象,并把 update 加入更新队列,最后调度更新。 - -```js -export function updateContainer( - element: ReactNodeList, - container: OpaqueRoot, - parentComponent: ?React$Component, - callback: ?Function -): Lane { - // 以下代码与创建 update 逻辑无关 - // const current = container.current - // const eventTime = requestEventTime() - // const lane = requestUpdateLane(current) - // if (enableSchedulingProfiler) { - // markRenderScheduled(lane) - // } - // const context = getContextForSubtree(parentComponent) - // if (container.context === null) { - // container.context = context - // } else { - // container.pendingContext = context - // } - - - const update = createUpdate(eventTime, lane) // 创建 update - update.payload = { element } // update.payload为需要挂载在根节点的组件 - - callback = callback === undefined ? null : callback // callback为ReactDOM.render的第三个参数 —— 回调函数 - if (callback !== null) { - update.callback = callback - } - - enqueueUpdate(current, update) // 将生成的update加入updateQueue - scheduleUpdateOnFiber(current, lane, eventTime) // 调度更新 - - return lane -} -``` - -接下来重点看看这个 update 对象吧。 - -## update 对象 - -首先在 React 中,有如下方法可以触发状态更新(排除 SSR 相关): - -- ReactDOM.render —— HostRoot -- this.setState —— ClassComponent -- this.forceUpdate —— ClassComponent -- useState —— FunctionComponent -- useReducer —— FunctionComponent - -以上方法的使用场景不同,但是殊途同归,最终都计入同一套更新机制 -- **每次状态更新都会创建一个保存*更新状态*相关内容的对象 -> Update 对象**,在 render 阶段的 beginWork 中会根据 Update 计算新的 state。 - -上方一共有三种组件,HostRoot 和 ClassComponent 共用一套 Update 数据结构,FunctionComponent 使用另一种 Update 数据结构。 - -```js -export function createUpdate(eventTime: number, lane: Lane): Update<*> { - const update: Update<*> = { - eventTime, // 任务时间,通过performance.now()获取的毫秒数 - lane, // 优先级相关字段 - - tag: UpdateState, // 更新的类型 创建时设置为UpdateState UpdateState | ReplaceState | ForceUpdate | CaptureUpdate ==> 0|1|2|3 - payload: null, // 更新挂载的数据,不同类型组件挂载的数据不同。对于ClassComponent,payload为this.setState的第一个传参。对于HostRoot,payload为ReactDOM.render的第一个传参。 - callback: null, // 更新的回调函数,commit阶段layout子阶段的回调函数,比如setState的第二个参数 - - next: null, // 与其他Update连接形成链表 - }; - return update; -} -``` - -> 注意 Update 的 next 属性是连接这个节点的其他 Update 形成链表,最终保存在对应 Fiber 的 updateQueue 属性上(Fiber 见前一篇文章)。为什么一个节点会有多个 Update 对象呢?很简单,比如多次 setState。 - -## UpdateQueue 对象 - -再来看看 UpdateQueue 对象: - -```js -// 在创建 fiberRoot 和 mount 阶段都会调用该方法 -export function initializeUpdateQueue(fiber: Fiber): void { - const queue: UpdateQueue = { - // 该 Fiber 节点更新前的 state,Update基于该state计算更新后的state - baseState: fiber.memoizedState, - // 该 Fiber 节点更新前已保存的 Update,以链表形式存在,链表头为firstBaseUpdate,链表尾为lastBaseUpdate - // 更新前就有Update 是因为上轮render阶段 低优先级的 Update 计算 state 时被忽略 - firstBaseUpdate: null, - lastBaseUpdate: null, - - // 触发更新时,产生的Update会保存在shared.pending中形成单向环状链表。 - // 当由Update计算state时这个环会被剪开并连接在lastBaseUpdate后面。 - shared: { - pending: null, - }, - - // 数组。保存update.callback !== null 的 Update - effects: null, - }; - fiber.updateQueue = queue; -} -``` - -对于 `shared.pending` 关注一下 enqueueUpdate: - -```js -export function enqueueUpdate(fiber: Fiber, update: Update) { - const updateQueue = fiber.updateQueue - if (updateQueue === null) { - // Only occurs if the fiber has been unmounted. - return - } - - const sharedQueue: SharedQueue = (updateQueue: any).shared - const pending = sharedQueue.pending - - /* 下面这段是初始时创建了单向环状链表,后续每次都将pending指向最新的 update */ - /* shared.pending -> updateN ->... -> update2 -> update1 -> updateN */ - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update - } else { - update.next = pending.next - pending.next = update // 此时的pending是上一轮的update - } - - sharedQueue.pending = update // 始终指向最新的 update -} -``` - -在进入 render 阶段后,`shared.pending` 会被剪开 接在 `lastBaseUpdate` 之后形成 `baseUpdate` 这个单链表。接下来遍历这个单链表,`fiber.updateQueue.baseState` 为初始 state,依次与遍历到的每个 `Update` 计算并产生新的 `state`。这些步骤发生在 `processUpdateQueue` 里: - -```js -export function processUpdateQueue( - workInProgress: Fiber, - props: any, - instance: any, - renderLanes: Lanes -): void { - // This is always non-null on a ClassComponent or HostRoot - const queue: UpdateQueue = (workInProgress.updateQueue: any) - - hasForceUpdate = false - - let firstBaseUpdate = queue.firstBaseUpdate - let lastBaseUpdate = queue.lastBaseUpdate - - // Check if there are pending updates. If so, transfer them to the base queue. - let pendingQueue = queue.shared.pending - if (pendingQueue !== null) { - queue.shared.pending = null - - // The pending queue is circular. Disconnect the pointer between first - // and last so that it's non-circular. - const lastPendingUpdate = pendingQueue - const firstPendingUpdate = lastPendingUpdate.next - lastPendingUpdate.next = null - // Append pending updates to base queue - if (lastBaseUpdate === null) { - firstBaseUpdate = firstPendingUpdate - } else { - lastBaseUpdate.next = firstPendingUpdate - } - lastBaseUpdate = lastPendingUpdate - - // If there's a current queue, and it's different from the base queue, then - // we need to transfer the updates to that queue, too. Because the base - // queue is a singly-linked list with no cycles, we can append to both - // lists and take advantage of structural sharing. - // 同时把 剪开后的 pendingQueue 也加在了 currentQueue.lastBaseUpdate 上 - const current = workInProgress.alternate - if (current !== null) { - // This is always non-null on a ClassComponent or HostRoot - const currentQueue: UpdateQueue = (current.updateQueue: any) - const currentLastBaseUpdate = currentQueue.lastBaseUpdate - if (currentLastBaseUpdate !== lastBaseUpdate) { - if (currentLastBaseUpdate === null) { - currentQueue.firstBaseUpdate = firstPendingUpdate - } else { - currentLastBaseUpdate.next = firstPendingUpdate - } - currentQueue.lastBaseUpdate = lastPendingUpdate - } - } - } - - // These values may change as we process the queue. - if (firstBaseUpdate !== null) { - // Iterate through the list of updates to compute the result. - let newState = queue.baseState - let newLanes = NoLanes - - let newBaseState = null - let newFirstBaseUpdate = null - let newLastBaseUpdate = null - - let update = firstBaseUpdate - do { - const updateLane = update.lane - const updateEventTime = update.eventTime - if (!isSubsetOfLanes(renderLanes, updateLane)) { - // Priority is insufficient. Skip this update. If this is the first - // skipped update, the previous update/state is the new base - // update/state. - const clone: Update = { - eventTime: updateEventTime, - lane: updateLane, - - tag: update.tag, - payload: update.payload, - callback: update.callback, - - next: null - } - if (newLastBaseUpdate === null) { - newFirstBaseUpdate = newLastBaseUpdate = clone - newBaseState = newState - } else { - newLastBaseUpdate = newLastBaseUpdate.next = clone - } - // Update the remaining priority in the queue. - newLanes = mergeLanes(newLanes, updateLane) - } else { - // This update does have sufficient priority. - - if (newLastBaseUpdate !== null) { - const clone: Update = { - eventTime: updateEventTime, - // This update is going to be committed so we never want uncommit - // it. Using NoLane works because 0 is a subset of all bitmasks, so - // this will never be skipped by the check above. - lane: NoLane, - - tag: update.tag, - payload: update.payload, - callback: update.callback, - - next: null - } - newLastBaseUpdate = newLastBaseUpdate.next = clone - } - - // Process this update. - newState = getStateFromUpdate(workInProgress, queue, update, newState, props, instance) - const callback = update.callback - if (callback !== null) { - workInProgress.flags |= Callback - const effects = queue.effects - if (effects === null) { - queue.effects = [update] - } else { - effects.push(update) - } - } - } - update = update.next - if (update === null) { - pendingQueue = queue.shared.pending - if (pendingQueue === null) { - break - } else { - // An update was scheduled from inside a reducer. Add the new - // pending updates to the end of the list and keep processing. - const lastPendingUpdate = pendingQueue - // Intentionally unsound. Pending updates form a circular list, but we - // unravel them when transferring them to the base queue. - const firstPendingUpdate = ((lastPendingUpdate.next: any): Update) - lastPendingUpdate.next = null - update = firstPendingUpdate - queue.lastBaseUpdate = lastPendingUpdate - queue.shared.pending = null - } - } - } while (true) - - if (newLastBaseUpdate === null) { - newBaseState = newState - } - - queue.baseState = ((newBaseState: any): State) - queue.firstBaseUpdate = newFirstBaseUpdate - queue.lastBaseUpdate = newLastBaseUpdate - - // Set the remaining expiration time to be whatever is remaining in the queue. - // This should be fine because the only two other things that contribute to - // expiration time are props and context. We're already in the middle of the - // begin phase by the time we start processing the queue, so we've already - // dealt with the props. Context in components that specify - // shouldComponentUpdate is tricky; but we'll have to account for - // that regardless. - markSkippedUpdateLanes(newLanes) - workInProgress.lanes = newLanes - workInProgress.memoizedState = newState - } -} -``` - -state 的变化在 render 阶段产生与上次更新不同的 JSX 对象,通过 Diff 算法产生 flags(16 叫 effectTag),在 commit 阶段渲染在页面上。 - -注意: 剪开后的 pendingQueue 同时也加在了 currentQueue.lastBaseUpdate 上,这是为了,当低优先级的任务被打断后重新开始时,能够基于 current fiber 的 updateQueue 克隆出 workInProgress fiber 的 updateQueue。保证了所有 Update 不会丢失。 - -当某个 Update 由于优先级低而被跳过时,保存在 baseUpdate 中的不仅是该 Update,还包括链表中该 Update 之后的所有 Update,从而保障状态依赖的连续性。 - -## 参考 - -- [React 源码](https://github.com/facebook/react) -- [卡颂大佬的 React 技术揭秘](https://react.iamkasong.com/) diff --git "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2064.md" "b/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2064.md" deleted file mode 100644 index 0b8eb04..0000000 --- "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2064.md" +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: 'React基础原理4 - 获取fiberRoot&调度更新' -date: 2022-11-14T16:33:10+08:00 -tags: [React] -draft: true ---- - -上文的 5 种触发更新使得被触发更新的 Fiber 对象上已经记录下了所有需要变化的 Update,那么接下来就是要调用 `markUpdateLaneFromFiberToRoot` 这个方法。 - -### markUpdateLaneFromFiberToRoot (获取到 fiberRoot) - -```js -function markUpdateLaneFromFiberToRoot(sourceFiber: Fiber, lane: Lane): FiberRoot | null { - // Update the source fiber's lanes - sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane) - let alternate = sourceFiber.alternate - if (alternate !== null) { - alternate.lanes = mergeLanes(alternate.lanes, lane) - } - - // Walk the parent path to the root and update the child expiration time. - let node = sourceFiber - let parent = sourceFiber.return - while (parent !== null) { - parent.childLanes = mergeLanes(parent.childLanes, lane) - alternate = parent.alternate - if (alternate !== null) { - alternate.childLanes = mergeLanes(alternate.childLanes, lane) - } - node = parent - parent = parent.return - } - if (node.tag === HostRoot) { - const root: FiberRoot = node.stateNode - return root - } else { - return null - } -} -``` - -可以看见这个方法,主要干了两件事: - -1. 对 lane 进行了操作 -2. 从触发更新的 Fiber 遍历到 FiberRoot,并返回 FiberRoot - -接下来做的就是去触发调度更新了。 - -### ensureRootIsScheduled (调度更新) - -该方法的源码的注释需要认真理解,非常之重要。 - -```js -// Use this function to schedule a task for a root. There's only one task per -// root; if a task was already scheduled, we'll check to make sure the priority -// of the existing task is the same as the priority of the next level that the -// root has work on. This function is called on every update, and right before -// exiting a task. -function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { - const existingCallbackNode = root.callbackNode; - - // Check if any lanes are being starved by other work. If so, mark them as - // expired so we know to work on those next. - markStarvedLanesAsExpired(root, currentTime); // 把不断被打断任务的小可怜置为过期,让下回赶紧执行掉 - - // Determine the next lanes to work on, and their priority. - const nextLanes = getNextLanes( - root, - root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, - ); - // This returns the priority level computed during the `getNextLanes` call. - const newCallbackPriority = returnNextLanesPriority(); - - if (nextLanes === NoLanes) { - // Special case: There's nothing to work on. - if (existingCallbackNode !== null) { - cancelCallback(existingCallbackNode); - root.callbackNode = null; - root.callbackPriority = NoLanePriority; - } - return; - } - - // Check if there's an existing task. We may be able to reuse it. - if (existingCallbackNode !== null) { - const existingCallbackPriority = root.callbackPriority; - if (existingCallbackPriority === newCallbackPriority) { - // The priority hasn't changed. We can reuse the existing task. Exit. - return; - } - // The priority changed. Cancel the existing callback. We'll schedule a new - // one below. - cancelCallback(existingCallbackNode); - } - - // Schedule a new callback. - let newCallbackNode; - if (newCallbackPriority === SyncLanePriority) { - // Special case: Sync React callbacks are scheduled on a special - // internal queue - newCallbackNode = scheduleSyncCallback( - performSyncWorkOnRoot.bind(null, root), // 同步工作 - ); - } else if (newCallbackPriority === SyncBatchedLanePriority) { - newCallbackNode = scheduleCallback( - ImmediateSchedulerPriority, // 优先级 - performSyncWorkOnRoot.bind(null, root), // 同步工作 - ); - } else { - const schedulerPriorityLevel = lanePriorityToSchedulerPriority( - newCallbackPriority, - ); - newCallbackNode = scheduleCallback( - schedulerPriorityLevel, // 优先级 - performConcurrentWorkOnRoot.bind(null, root), // 并发 - ); - } - - root.callbackPriority = newCallbackPriority; - root.callbackNode = newCallbackNode; -} -``` - -关注一下 `scheduleCallback`方法,它有两个参数,第一个是优先级,第二个是回调,有两种类型的回调函数即:`performSyncWorkOnRoot`和`performConcurrentWorkOnRoot`,这两个方法就是 render 阶段的入口函数,下文将学习 render 阶段。 diff --git "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2065.md" "b/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2065.md" deleted file mode 100644 index 955987a..0000000 --- "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2065.md" +++ /dev/null @@ -1,432 +0,0 @@ ---- -title: 'React基础原理5 - render 阶段' -date: 2022-11-14T17:06:21+08:00 -tags: [React] -draft: true ---- - -从创建 FiberRoot,到创建 Update,再遍历到 FiberRoot,然后发起一个 task,进入到了 `performSyncWorkOnRoot`或`performConcurrentWorkOnRoot` 方法中,这便是 render 阶段的开端。 - -这两个方法又分别调用了 `renderRootSync` 和 `renderRootConcurrent`(这两个方法返回 `exitStatus` 供后续使用),其内部又分别调用了 `workLoopSync` 和 `workLoopConcurrent`: - -```js -function workLoopSync() { - // Already timed out, so perform work without checking if we need to yield. - while (workInProgress !== null) { - performUnitOfWork(workInProgress); - } -} - -function workLoopConcurrent() { - // Perform work until Scheduler asks us to yield - while (workInProgress !== null && !shouldYield()) { - performUnitOfWork(workInProgress); - } -} -``` - -可以看出这两唯一的区别就是多了一个 `shouldYield` 的判断,如果当前浏览器帧没有剩余时间,`shouldYield` 会中止循环,直到浏览器有空闲时间后再继续遍历。 - -源码中追踪到最后是 `throw new Error('This module must be shimmed by a specific build.')`,就是这个模块必须由特定的构建进行微调,下面是这个方法的模拟实现: - -```js -export function shouldYieldToHost(): boolean { - if ( - (expectedNumberOfYields !== -1 && - yieldedValues !== null && - yieldedValues.length >= expectedNumberOfYields) || - (shouldYieldForPaint && needsPaint) - ) { - // We yielded at least as many values as expected. Stop flushing. - didStop = true; - return true; - } - return false; -} -``` - -### performUnitOfWork - -`performUnitOfWork(unitOfWork: Fiber)`,入参即是 workInProgress Fiber,这是一个全局变量。 - -```js -function performUnitOfWork(unitOfWork: Fiber): void { - // The current, flushed, state of this fiber is the alternate. Ideally - // nothing should rely on this, but relying on it here means that we don't - // need an additional field on the work in progress. - const current = unitOfWork.alternate - - let next - if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { - startProfilerTimer(unitOfWork) - next = beginWork(current, unitOfWork, subtreeRenderLanes) - stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true) - } else { - next = beginWork(current, unitOfWork, subtreeRenderLanes) - } - - unitOfWork.memoizedProps = unitOfWork.pendingProps - - if (next === null) { - // If this doesn't spawn new work, complete the current work. - completeUnitOfWork(unitOfWork) - } else { - workInProgress = next - } - - ReactCurrentOwner.current = null -} -``` - -结合上文,`performUnitOfWork` 是在 while 循环中执行的,performUnitOfWork,分别进入 `beginWork`, `completeUnitOfWork 中调用 --> completeWork`。 - -这是一个递归的过程,`beginWork` 递,`completeWork` 归。’递‘ 到没有叶节点的子节点后,检查是否有兄弟节点 sibling,有的话进入兄弟节点继续递,否则进入 ’归‘ 阶段,’归‘ 到 fiberRoot,render 阶段结束。 - -举个例子: - -```js -function App() { - return ( -
- I am - yokiizx -
- ) -} - -ReactDOM.render(, document.getElementById("root")); - -/** - * fiberRoot beginWork - * App Fiber beginWork - * div Fiber beginWork - * 'I am' Fiber beginWork - * 'I am' Fiber completeWork - * span Fiber beginWork - * span Fiber completeWork - * div Fiber completeWork - * App Fiber completeWork - * fiberRoot completeWork - * / -``` - -> 之所以 'yokiizx' Fiber 没有进入 beginWork/completeWork,是因为针对**只有单一文本子节点的 Fiber**,React 进行了特殊的处理来进行性能优化。 - -### beginWork - -beginWork 源码比较长,这里简化一下主要逻辑: - -```js -function beginWork( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes -): Fiber | null { - - // current !== null,说明是 update,可以通过diff来获取可以复用的fiber - if (current !== null) { - const oldProps = current.memoizedProps; - const newProps = workInProgress.pendingProps; - - if ( - oldProps !== newProps || - hasLegacyContextChanged() || - (__DEV__ ? workInProgress.type !== current.type : false) - ) { - didReceiveUpdate = true; - // includesSomeLane 这个方法在 Scheduler 时再细看,这里是优先级不够就进入下面代码 - } else if (!includesSomeLane(renderLanes, updateLanes)) { - didReceiveUpdate = false; - switch (workInProgress.tag) { - // 省略处理 - } - - // 复用current,判断子树是否需要更新 - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderLanes, - ); - } else { - didReceiveUpdate = false; - } - } else { - didReceiveUpdate = false; - } - - // 根据 fiber.tag 的不同,创建不同的子Fiber节点 - switch (workInProgress.tag) { - case IndeterminateComponent: - // ...省略 - case LazyComponent: - // ...省略 - case FunctionComponent: { - const Component = workInProgress.type; - const unresolvedProps = workInProgress.pendingProps; - const resolvedProps = - workInProgress.elementType === Component - ? unresolvedProps - : resolveDefaultProps(Component, unresolvedProps); - return updateFunctionComponent( - current, - workInProgress, - Component, - resolvedProps, - renderLanes, - ); - } - case ClassComponent: { - const Component = workInProgress.type; - const unresolvedProps = workInProgress.pendingProps; - const resolvedProps = - workInProgress.elementType === Component - ? unresolvedProps - : resolveDefaultProps(Component, unresolvedProps); - return updateClassComponent( - current, - workInProgress, - Component, - resolvedProps, - renderLanes, - ); - } - case HostRoot: - return updateHostRoot(current, workInProgress, renderLanes); - case HostComponent: - return updateHostComponent(current, workInProgress, renderLanes); - case HostText: - // ...省略 - // ...省略其他类型 - } -} -``` - -入参的 current 即 currentFiber,通过 workInProgress Fiber 的 alternate 属性获取到,前文说过,mount 时,current 为空,所以在 `beginWork` 中通过判断 `current !== null` 来判断是 update 还是 mount 阶段 。 -在 update 阶段,可以调用 `bailoutOnAlreadyFinishedWork` 来复用 current 上的节点。 -在 mount 阶段,直接根据 tag 不同,创建不同的子 Fiber 节点。而根据 tag 不同来创建 Fiber 节点,对于常见的组件(FunctionComponent/ClassComponent/HostComponent)最终都会进入 `reconcileChildren` 这个方法: - -```js -export function reconcileChildren( - current: Fiber | null, - workInProgress: Fiber, - nextChildren: any, - renderLanes: Lanes, -) { - if (current === null) { - // If this is a fresh new component that hasn't been rendered yet, we - // won't update its child set by applying minimal side-effects. Instead, - // we will add them all to the child before it gets rendered. That means - // we can optimize this reconciliation pass by not tracking side-effects. - workInProgress.child = mountChildFibers( - workInProgress, - null, - nextChildren, - renderLanes, - ); - } else { - // If the current child is the same as the work in progress, it means that - // we haven't yet started any work on these children. Therefore, we use - // the clone algorithm to create a copy of all the current children. - - // If we had any progressed work already, that is invalid at this point so - // let's throw it out. - - // 这里将使用 diff 算法 创建新Fiber 并加上 flags(v16叫effectTag) - workInProgress.child = reconcileChildFibers( - workInProgress, - current.child, - nextChildren, - renderLanes, - ); - } -} -``` - -这里也是根据 current 是否为 null 来判断是 mount 还是 update 阶段的,不论走哪个逻辑,最终他会生成新的子 Fiber 节点并赋值给 workInProgress.child,作为本次 beginWork 返回值,并作为下次 performUnitOfWork 执行时 workInProgress 的传参。 - -mountChildFibers 和 reconcileChildFibers 逻辑基本相同,唯一不同的是:reconcileChildFibers 会为生成的 Fiber 节点带上 `flags(v16叫effectTag)` 属性。 - -`ReactFiberFlags.js` 这个文件中存储着 flags(v16叫effectTag) 对应的操作: - -```js -// DOM需要插入到页面中 -export const Placement = /* */ 0b00000000000010; -// DOM需要更新 -export const Update = /* */ 0b00000000000100; -// DOM需要插入到页面中并更新 -export const PlacementAndUpdate = /* */ 0b00000000000110; -// DOM需要删除 -export const Deletion = /* */ 0b00000000001000; - -``` - -> 通过二进制表示 flags(v16叫effectTag),可以方便的使用位操作为 fiber.flags(v16叫effectTag) 赋值多个 effect。 - -beginWork 的流程图: -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202211152222775.png) - -### completeWork - -从 "递" 出来后进入 "归"的 `completeWork`。 - -`completeWork` 的源码非常长,不过与 `beginWork` 一样,也是根据 fiber.tag 调用不同的处理逻辑,方法内就一个 `switch...case`,有些组件要处理的逻辑较多,下面只关注部分组件类型的主要逻辑。 - -```js -function completeWork( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -): Fiber | null { - const newProps = workInProgress.pendingProps; - - switch (workInProgress.tag) { - case IndeterminateComponent: - case LazyComponent: - case SimpleMemoComponent: - case FunctionComponent: - case ForwardRef: - case Fragment: - case Mode: - case Profiler: - case ContextConsumer: - case MemoComponent: - return null; - case ClassComponent: { - // ...省略 - return null; - } - case HostRoot: { - // ...省略 - updateHostContainer(workInProgress); - return null; - } - case HostComponent: { - // ...省略 - return null; - } - // ...其它的 case 类型 - } -} -``` - -先重点关注页面渲染所必须的 `HostComponent`(即原生 DOM 组件对应的 Fiber 节点): - -```js -case HostComponent: { - popHostContext(workInProgress) - const rootContainerInstance = getRootHostContainer() - const type = workInProgress.type - if (current !== null && workInProgress.stateNode != null) { - updateHostComponent( - current, - workInProgress, - type, - newProps, - rootContainerInstance - ) - - if (current.ref !== workInProgress.ref) { - markRef(workInProgress) - } - } else { - if (!newProps) { - // This can happen when we abort work. - return null - } - - const currentHostContext = getHostContext() - // 创建 DOM 节点 - const instance = createInstance( - type, - newProps, - rootContainerInstance, - currentHostContext, - workInProgress - ) - // 将子孙DOM节点插入刚生成的DOM节点中 - appendAllChildren(instance, workInProgress, false, false) - // DOM节点赋值给fiber.stateNode - workInProgress.stateNode = instance - - // Certain renderers require commit-time effects for initial mount. - // (eg DOM renderer supports auto-focus for certain elements). - // Make sure such renderers get scheduled for later work. - // 初始化 DOM 对象的事件监听器和属性 - if ( - finalizeInitialChildren( - instance, - type, - newProps, - rootContainerInstance, - currentHostContext - ) - ) { - markUpdate(workInProgress) - } - - if (workInProgress.ref !== null) { - // If there is a ref on a host node we need to schedule a callback - markRef(workInProgress) - } - } - return null -} -``` - -也是根据 current 是否为 null 来判断是 mount 还是 update;同时根据 workInProgress.stateNode 是否已存在对应 DOM 节点来判断是否进入更新还是去新建。 - -- 如果进入 update,则会走入 `updateHostComponent` 方法,这个方法最终会生成 `updatePayload` 挂载到 `workInProgress.updateQueue` 上,最后在 `commit` 阶段渲染到页面上。 - ```js - // updatePayload为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value - workInProgress.updateQueue = (updatePayload: any); - ``` -- 如果进入 mount,主要干了这么几件事 - - 为 fiber 创建对应的 DOM 节点,并赋值给 fiber.stateNode - - 把子孙 DOM 节点放入刚刚生成的 DOM 节点中(我们是”归“阶段所以能拿到所有子孙节点) - - 初始化 DOM 对象的事件监听器和属性 - -当 `completeWork` 归到 rootFiber 时,在内存中就已经有了一个构建好的 DOM 树了。 - -继续归--继续退栈,依次 `completeUnitOfWork`,`performUnitOfWork`,`workLoopSync`,`renderRootSync`,`performSyncWorkOnRoot`,最后执行 `performSyncWorkOnRoot` 的代码: - -```js - commitRoot(root); // 进入 commit 阶段 -``` - -注意:commit 阶段需要找到所有具备 flags(v16叫effectTag) 的 fiber 节点,并依次执行对应的操作,为了不再 commit 阶段再遍历一次 fiberTree 来提高性能,React 在 fiber 中设置了类似 UpdateQueue 对象的 fisrt/lastBaseUpdate 属性,为 firstEffect 和 lastEffect,通过 nextEffect 将具有 flags(v16叫effectTag) 的 Fiber 连接起来,这部分操作发生在 `completeUnitOfWork` 内执行完 `completeWork` 之后。 - -还是因为在 "归" 阶段,最终就是会形成从 rootFiber 到最后一个 fiber 的 effectList: - -```js - nextEffect nextEffect -rootFiber.firstEffect -----------> fiber -----------> fiber -``` - -这样,在 commit 阶段只需要遍历 effectList 就能执行所有 effect 了。 - -`completeWork` 大致流程图: -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202211170035027.png) - -## 总结 - -至少大概要清楚 render 阶段都做了什么: - -- beginWork - - mount:根据 tag(组件类型) 生成了新的 fiber 节点 - - update:根据 props 和 type 判断是否可以复用: - 1. 可以复用再判断子树是否检查更新,需要返回 workInProgress.child,不需要返回 null; - 2. 不可复用则根据 tag 不同做不同操作,然后调用 `reconcileChildFibers` 通过 diff 算法生成 flags(v16叫effectTag) 的新 fiber 节点 -- completeWork - 根据 tag 不同进入不同的组件处理逻辑: - - - mount - 1. createInstance 创建 DOM 实例,赋值给 fiber.stateNode - 2. 将子孙节点插入新生成的 DOM 节点 - 3. 初始化 DOM 对象的事件监听器和内部属性 - - update - - updateHostComponent 主要是 diff props,返回需要更新的属性名和值的数组,形式如 `[key1,value1,key2,value2,...]`,并把这个数组赋值给 workInProgress.updateQueue。 - -- completeUnitOfWork 内 completeWork 执行之后 - 最终把带有 flags(v16叫effectTag) 的 fiber 通过 nextEffect 连接形成单链表,挂载到父级 effectList 的末尾,并返回下一个 workInProgress fiber。 - -最后 `performSyncWorkOnRoot` 内调用 `commitRoot(root);` 进入 commit 阶段。 diff --git "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2066.md" "b/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2066.md" deleted file mode 100644 index 85e8196..0000000 --- "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2066.md" +++ /dev/null @@ -1,358 +0,0 @@ ---- -title: 'React基础原理6 - commit 阶段' -date: 2022-11-17T10:57:23+08:00 -tags: [React] -draft: true ---- - -render 通过 `commitRoot(root)` 进入此阶段。该阶段调用的主函数是 `commitRootImpl(root, renderPriorityLevel)`。 - -此时,`rootFiber.firstEffect` 上保存了一条需要执行副作用的 Fiber 节点的单向链表 effectList,这些 Fiber 节点的 updateQueue 上保存着变化了的 props。 - -这些副作用的 DOM 操作在 commit 阶段执行。 - -commit 阶段主要分为:before mutation,mutation,layout 这三个阶段。 - -开始三个阶段之前先看下 `commitRootImpl` 的主要内容: - -```js -// 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级 -const previousLanePriority = getCurrentUpdateLanePriority(); -setCurrentUpdateLanePriority(SyncLanePriority); - -// 将当前上下文标记为CommitContext,作为commit阶段的标志 -const prevExecutionContext = executionContext; -executionContext |= CommitContext; - -// 处理focus状态 -focusedInstanceHandle = prepareForCommit(root.containerInfo); -shouldFireAfterActiveInstanceBlur = false; -``` - -## before muatation(执行 DOM 操作前) - -遍历 effectList,进入主函数 `commitBeforeMutationEffects`: - -```js -function commitBeforeMutationEffects() { - while (nextEffect !== null) { - const current = nextEffect.alternate - - if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { - // ...focus blur相关 - } - - // flags(v16叫effectTag) - const flags = nextEffect.flags - - // 调用 getSnapshotBeforeUpdate - if ((flags & Snapshot) !== NoFlags) { - commitBeforeMutationEffectOnFiber(current, nextEffect) - } - - // 调度useEffect - if ((flags & Passive) !== NoFlags) { - // If there are passive effects, schedule a callback to flush at the earliest opportunity. - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true - scheduleCallback(NormalSchedulerPriority, () => { - // 触发 useEffect - flushPassiveEffects() - return null - }) - } - } - nextEffect = nextEffect.nextEffect - } -} -``` - -整体可以分为三部分,遍历 effectList,依次执行: - -- 处理 DOM 节点渲染/删除后的 autoFocus、blur 逻辑。 -- 调用 `getSnapshotBeforeUpdate` 生命周期钩子。 -- 调度 `useEffect`。 - -提一嘴生命周期,`getSnapshotBeforeUpdate` 是 React16 新增的 api,主要是因为 render 阶段可能被中断,然后再接着执行,而旧的 `componentWillXxx` 生命周期也是在 render 阶段执行,就可能会导致此类生命周期被触发执行多次。为此,React 提供了替代的生命周期钩子 `getSnapshotBeforeUpdate`,如上,它是在 commit 阶段 -- 确切说是 `commit before mutation` 也称为 `pre-commit` 阶段执行的,而 commit 是同步执行的,不会出现多次调用的问题。 - -[详细见关于 useEffect](./React-useEffect.md)。 -此处,只需要知道 `useEffect` 是异步调用的,为了防止阻塞浏览器渲染。 -`flushPassiveEffect` 根据优先级调用 `flushPassiveEffectsImpl`,然后在 `flushPassiveEffectsImpl` 内部遍历 `rootWithPendingPassiveEffects`。一开始 `rootWithPendingPassiveEffects` 为 `null`,它是在上一轮 layout 阶段之后把 `effectList` 赋给 `rootWithPendingPassiveEffects` 的。 - -## mutation(执行 DOM 操作) - -现在到了执行 DOM 操作的阶段: - -```js -// commitImpl -// 同样也是遍历 effectList, before mutation/mutation/layout 都类似 -nextEffect = firstEffect; -do { - try { - commitMutationEffects(root, renderPriorityLevel); - } catch (error) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } -} while (nextEffect !== null); -``` - -主函数 `commitMutationEffects`: - -```js -function commitMutationEffects(root: FiberRoot, renderPriorityLevel: ReactPriorityLevel) { - // 遍历 effectList - while (nextEffect !== null) { - setCurrentDebugFiberInDEV(nextEffect); - - const flags = nextEffect.flags; - - // 重置文字节点 - if (flags & ContentReset) { - commitResetTextContent(nextEffect); - } - // 更新 ref - if (flags & Ref) { - const current = nextEffect.alternate; - if (current !== null) { - commitDetachRef(current); - } - } - - // The following switch statement is only concerned about placement, - // updates, and deletions. To avoid needing to add a case for every possible - // bitmap value, we remove the secondary effects from the effect tag and - // switch on that value. - const primaryFlags = flags & (Placement | Update | Deletion | Hydrating); - switch (primaryFlags) { - // 插入 DOM - case Placement: { - commitPlacement(nextEffect); - nextEffect.flags &= ~Placement; // 与自身的`取反` 进行 `与`,变成了 NoFlags - break; - } - // 插入 DOM - case PlacementAndUpdate: { - commitPlacement(nextEffect); - nextEffect.flags &= ~Placement; - - const current = nextEffect.alternate; - commitWork(current, nextEffect); - break; - } - // SSR 省略 ... - - // 更新 DOM - case Update: { - const current = nextEffect.alternate; - commitWork(current, nextEffect); - break; - } - // 删除 DOM - case Deletion: { - commitDeletion(root, nextEffect, renderPriorityLevel); - break; - } - } - nextEffect = nextEffect.nextEffect; - } -} -``` - -从源码中可以看出,每个 Fiber 都要进行以下操作: - -- 重置文本节点 -- 更新 `ref` -- 根据对应的 `flag` 进行对应的操作:(Placement | Update | Deletion | Hydration(SSR 相关,暂不关注)) - -以上 `flag` 都在 `ReactFiberFlags.js` 中定义,全部为二进制值,是为了方便进行转换计算。 - -### Placement - -当 flag 标志为 Placement 时,意味着该 fiber 对应的 DOM 元素应该被插入到页面中。 - -`commitPlacement` 函数: - -```js -function commitPlacement(finishedWork: Fiber): void { - if (!supportsMutation) { - return; - } - // Recursively insert all host nodes into the parent. - const parentFiber = getHostParentFiber(finishedWork); // 获取父级 DOM 节点 - - // Note: these two variables *must* always be updated together. - let parent; - let isContainer; - const parentStateNode = parentFiber.stateNode; // 拿到父fiber节点对应的 DOM - // 根据父级 fiber 的tag对上方两个变量进行不同的赋值 - switch (parentFiber.tag) { - case HostComponent: - parent = parentStateNode; - isContainer = false; - break; - case HostRoot: - parent = parentStateNode.containerInfo; - isContainer = true; - break; - case HostPortal: - parent = parentStateNode.containerInfo; - isContainer = true; - break; - case FundamentalComponent: - if (enableFundamentalAPI) { - parent = parentStateNode.instance; - isContainer = false; - } - } - if (parentFiber.flags & ContentReset) { - // Reset the text content of the parent before doing any insertions - resetTextContent(parent); - // Clear ContentReset from the effect tag - parentFiber.flags &= ~ContentReset; - } - - const before = getHostSibling(finishedWork); // 获取兄弟节点 - - // 这里是根据是否为 rootFiber 来调用不同的方法 - // 每个方法内都会递归的查找子节点 - if (isContainer) { - insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); - } else { - insertOrAppendPlacementNode(finishedWork, before, parent); - } -} -``` - -> getHostSibling(获取兄弟 DOM 节点)的执行很耗时,当在同一个父 Fiber 节点下依次执行多个插入操作,getHostSibling 算法的复杂度为指数级。 -> 这是由于 Fiber 节点不只包括 HostComponent,所以 Fiber 树和渲染的 DOM 树节点并不是一一对应的。要从 Fiber 节点找到 DOM 节点很可能跨层级遍历。 - -```txt -// Fiber树 - child child child -rootFiber -----> App -----> div -----> p - | sibling child - | -------> Item -----> li -// DOM树 -#root ---> div ---> p - | - ---> li - -此时DOM节点 p的兄弟节点为 li,而Fiber节点 p对应的兄弟DOM节点为 fiberP.sibling.child -``` - -### Update - -节点更新调用 `commitWork`,会根据 fiber 的 tag 分别处理,重点关注 FunctionComponent 和 HostComponent。 - -- tag 为 FunctionComponent,调用 `commitHookEffectListUnmount`,顾名思义就是遍历 effectList,执行所有 useLayoutEffect hook 的清理函数。 - -- tag 为 HostComponent,会调用 `commitUpdate`,最终会调用 `updateDOMProperties`: - -```js -for (let i = 0; i < updatePayload.length; i += 2) { - const propKey = updatePayload[i]; - const propValue = updatePayload[i + 1]; - if (propKey === STYLE) { - setValueForStyles(domElement, propValue); - } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { - setInnerHTML(domElement, propValue); - } else if (propKey === CHILDREN) { - setTextContent(domElement, propValue); - } else { - setValueForProperty(domElement, propKey, propValue, isCustomComponentTag); - } -} -``` - -上方就是将 render 阶段 completeWork 中给 Fiber 节点赋值的 updateQueue 对应的内容渲染在页面上。 - -### Deletion - -递归的将 fiber 节点对应的 DOM 节点从页面中删除。 - -```js -function commitDeletion( - finishedRoot: FiberRoot, - current: Fiber, - renderPriorityLevel: ReactPriorityLevel, -): void { - if (supportsMutation) { - // Recursively delete all host nodes from the parent. - // Detach refs and call componentWillUnmount() on the whole subtree. - unmountHostComponents(finishedRoot, current, renderPriorityLevel); - } else { - // Detach refs and call componentWillUnmount() on the whole subtree. - commitNestedUnmounts(finishedRoot, current, renderPriorityLevel); - } - const alternate = current.alternate; - detachFiberMutation(current); - if (alternate !== null) { - detachFiberMutation(alternate); - } -} -``` - -- 递归调用 Fiber 节点及其子孙 Fiber 节点中 fiber.tag 为 ClassComponent 的 componentWillUnmount 生命周期钩子,从页面移除 Fiber 节点对应 DOM 节点 -- 解绑 ref -- 调度 `useEffect` 的清理函数 - -## layout(执行 DOM 操作后) - -这阶段在 DOM 渲染完成之后,所以该阶段触发的生命周期钩子和 hook 可以直接访问到已经改变后的 DOM。 -layout 阶段也是递归遍历 effectList。具体执行函数是 `commitLayoutEffects`。 - -```js -function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) { - // ... - while (nextEffect !== null) { - const flags = nextEffect.flags; - // 调度生命周期钩子和hook - if (flags & (Update | Callback)) { - const current = nextEffect.alternate; - // 原名为 commitLifeCycles, 引入后改了别名 - commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); - } - // 更新 ref - if (flags & Ref) { - commitAttachRef(nextEffect); - } - nextEffect = nextEffect.nextEffect; - } - // ... -} -``` - -首先,`commitLifeCycles` 也是根据 fiber.tag 去分别处理: - -- 对于 ClassComponent,他会通过 current === null?区分是 mount 还是 update,调用 componentDidMount 或 componentDidUpdate。`this.setState` 如果有第二个参数,也会在此时调用。 -- 对于 FunctionComponent 及相关类型[^1],他会调用 useLayoutEffect hook 的回调函数,调度 useEffect 的销毁与回调函数。 - > useEffect 是在 beforeMutation 阶段调度,其清理函数是在 mutation 调度,而他们的执行都是在 layout 阶段完成后才异步执行的。 - > useLayoutEffect 则是在 mutation 阶段 update 操作内执行上一轮更新的清理函数,在 layout 阶段执行它的回调函数。相比之下没有调度这一步。 -- 对于 HostRoot,即 rootFiber,如果赋值了第三个参数回调函数,也会在此时调用。 - ```JSX - ReactDOM.render(, document.querySelector("#root"), function() { - console.log("i am mount~"); - }) - ``` - -`commitLayoutEffects` 做的第二件事就是 `commitAttachRef`: 获取 DOM 实例,更新 ref。 -layout 阶段结束。 - -注意点: fiberRootNode 的 current 指针切换时机 -- mutation 和 layout 之间: - -```js -// 递归 mutation -root.current = finishedWork -// 递归 layout -``` - -> componentWillUnmount 在 mutation 阶段执行,此时 current Fiber 树还指向前一次更新的 Fiber 树,在生命周期钩子内获取的 DOM 还是更新前的; -> componentDidMount 和 componentDidUpdate 在 layout 阶段执行,此时 current Fiber 树已经指向更新后的 Fiber 树,在生命周期钩子内获取的 DOM 就是更新后的。 - -## 参考 - -- [React 生命周期图](https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/) - -[^1]: 相关类型指特殊处理后的 FunctionComponent,比如 ForwardRef、React.memo 包裹的 FunctionComponent diff --git "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2067-Diff\347\256\227\346\263\225.md" "b/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2067-Diff\347\256\227\346\263\225.md" deleted file mode 100644 index 8cc97b4..0000000 --- "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2067-Diff\347\256\227\346\263\225.md" +++ /dev/null @@ -1,509 +0,0 @@ ---- -title: 'React基础原理7 - Diff算法' -date: 2022-11-22T17:15:07+08:00 -tags: [React] -draft: true ---- - -在 render 阶段的 beginWork 如果是 update 最终走入 `reconcileChildFibers`,这个方法就是通过 diff 算法创建新 Fiber 并加上 flags,并尝试复用 currentFiber。 - -本文学习一下 React 的 diff 算法。 -diff 算法的本质是:JSX 对象和 current Fiber 对比,生成 workInProgress Fiber。 - -### 三个限制 - -首先,两棵树完全对比的时间复杂度是 O(n^3),是相当消耗性能的,为此,React 的 diff 算法预设了 3 个限制: - -- 只对同级元素进行 diff。如果 DOM 节点前后两次更新跨越了层级,不会复用它。 -- 不同类型的元素产生不同的树。如果 DOM 节点前后两次更新类型发生了变,会直接销毁它及子节点,并重新建树。 -- 可以通过 key 标识哪些子元素在不同的渲染中可能是不变的。 - -### reconcileChildFibers - -```js -function reconcileChildFibers( - returnFiber: Fiber, - currentFirstChild: Fiber | null, - newChild: any, - lanes: Lanes -): Fiber | null { - // 获取 newChild 类型 (JSX对象) - const isObject = typeof newChild === 'object' && newChild !== null - - if (isObject) { - switch (newChild.$$typeof) { - case REACT_ELEMENT_TYPE: - return placeSingleChild( - reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes) - ) - case REACT_PORTAL_TYPE: - return placeSingleChild( - reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes) - ) - case REACT_LAZY_TYPE: - if (enableLazyElements) { - const payload = newChild._payload - const init = newChild._init - // todo: This function is supposed to be non-recursive. - return reconcileChildFibers(returnFiber, currentFirstChild, init(payload), lanes) - } - } - } - // 调用 reconcileSingleTextNode 处理单节点 - if (typeof newChild === 'string' || typeof newChild === 'number') { - return placeSingleChild( - reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes) - ) - } - // 调用 reconcileChildrenArray 处理多节点 - if (isArray(newChild)) { - return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes) - } - - // ... 其它 case 省略 - - // 以上case都没有命中,就删除节点 - return deleteRemainingChildren(returnFiber, currentFirstChild) -} -``` - -根据 newChild 的类型进行了不同的处理: - -- 类型为 object,或 number,string,代表同级只有一个节点 -- 类型为 array,代表同级有多个节点 - -### 同级只有一个节点 - -单个节点,关注一下 `reconcileSingleElement` 这个方法: - -```js -function reconcileSingleElement( - returnFiber: Fiber, - currentFirstChild: Fiber | null, - element: ReactElement, - lanes: Lanes -): Fiber { - const key = element.key - let child = currentFirstChild - - // 上次更新时的 fiber 是否存在对应的 DOM 节点 - // 1. 存在,进入判断是否可以复用 - while (child !== null) { - // 先判断key是否相同 - if (child.key === key) { - // 再根据 fiber 的tag类型做处理 - switch (child.tag) { - case Fragment: { - /** ... */ - } - case Block: { - /** ... */ - } - default: { - // elementType 相同 表示可以复用 - if (child.elementType === element.type) { - deleteRemainingChildren(returnFiber, child.sibling) - const existing = useFiber(child, element.props) - existing.ref = coerceRef(returnFiber, child, element) - existing.return = returnFiber - return existing - } - break - } - } - // 代码执行到这里代表:key相同但是type不同 - // 将该fiber及其兄弟fiber标记为删除 - deleteRemainingChildren(returnFiber, child) - break - } else { - // key不同,将该 fiber 标记为删除 - deleteChild(returnFiber, child) - } - child = child.sibling - } - - // 2. 不存在 DOM 节点,直接创建新的 Fiber 并返回 - if (element.type === REACT_FRAGMENT_TYPE) { - const created = createFiberFromFragment( - element.props.children, - returnFiber.mode, - lanes, - element.key - ) - created.return = returnFiber - return created - } else { - const created = createFiberFromElement(element, returnFiber.mode, lanes) - created.ref = coerceRef(returnFiber, currentFirstChild, element) - created.return = returnFiber - return created - } -} -``` - -注意: - -- 当 key 不相同时,仅把当前 fiber 标记为删除。因为兄弟节点还有可能匹配。 -- 当 key 相同,但是 type 不同时,把当前 filber 和兄弟 fiber 都标记为删除。因为 key 确定了就是这个元素,它都没机会,其它的兄弟节点也可以直接干掉了。 - -例子: - -```txt -ul -> li * 3 - ↓ -ul -> p (变成了单个节点) -``` - -如果 p 没有 key,p 与第一个 li key 不同,还会与后面的两个 li 去比较; -如果有 key 且和第一个 li 的 key 相同,继续去判断类型,当类型都不一样的时候,直接 li 和后面的两个 li 都标记删除。 - -### 同级有多个节点 - -多个节点的比较,分为三种情况: - -1. 节点更新(属性和类型改变) -2. 节点新增/减少 -3. 节点位置变化 - -```js -function reconcileChildrenArray( - returnFiber: Fiber, - currentFirstChild: Fiber | null, - newChildren: Array<*>, - lanes: Lanes -): Fiber | null { - // This algorithm can't optimize by searching from both ends since we - // don't have backpointers on fibers. I'm trying to see how far we can get - // with that model. If it ends up not being worth the tradeoffs, we can - // add it later. - let resultingFirstChild: Fiber | null = null // 最终返回,其实就是workInProgress Fiber - let previousNewFiber: Fiber | null = null // 链表操作指针,穿针引线用于把一个个新的fiber串联起来 - - let oldFiber = currentFirstChild - let lastPlacedIndex = 0 // 新fiber对应的DOM在页面中的位置,用来处理节点位置的变化 - let newIdx = 0 - let nextOldFiber = null - // 第一次遍历, oldFiber单恋和newFiber数组进行比较 - for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { - // 修正 oldFiber 的位置和 newFiber 对齐 - if (oldFiber.index > newIdx) { - nextOldFiber = oldFiber - oldFiber = null - } else { - nextOldFiber = oldFiber.sibling - } - - // 比较 oldFiber 和 newChildren[newIdx] 可以复用就返回existing, 不可复用返回null - const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes) - if (newFiber === null) { - if (oldFiber === null) { - oldFiber = nextOldFiber - } - // updateSlot 中会判断key是否相同,如果不同直接返回null,进入这里直接退出循环 - break - } - - // 如果key相同,会进入updateElement,再判断type是否相同,不同则需要把oldFiber标记删除Deletion - if (shouldTrackSideEffects) { - if (oldFiber && newFiber.alternate === null) { - deleteChild(returnFiber, oldFiber) - } - } - - // 链表操作, 把fiber连接起来组成workInProgress Fiber - if (previousNewFiber === null) { - resultingFirstChild = newFiber - } else { - previousNewFiber.sibling = newFiber - } - previousNewFiber = newFiber - oldFiber = nextOldFiber - } - - /* ---------- 第一轮遍历结束后 ---------- */ - // 这种情况是 newChildren 遍历完了,把 剩余的oldFiber 都标记为 Deletion - if (newIdx === newChildren.length) { - // We've reached the end of the new children. We can delete the rest. - deleteRemainingChildren(returnFiber, oldFiber) - return resultingFirstChild - } - - // oldFiber 遍历完了, 从newIndex的位置继续遍历剩下的 newChildren - if (oldFiber === null) { - // If we don't have any more existing children we can choose a fast path - // since the rest will all be insertions. - for (; newIdx < newChildren.length; newIdx++) { - const newFiber = createChild(returnFiber, newChildren[newIdx], lanes) - if (newFiber === null) { - continue - } - lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx) - if (previousNewFiber === null) { - resultingFirstChild = newFiber - } else { - previousNewFiber.sibling = newFiber - } - previousNewFiber = newFiber - } - return resultingFirstChild - } - - // 剩下的情况是 oldFiber和newChildren都没有遍历完,需要进行位置交换的情况了 - // 所以使用了一个 map ,以 key 为键, 以 Fiber 为值,为了 O(1) 的查找 - // Add all children to a key map for quick lookups. - const existingChildren = mapRemainingChildren(returnFiber, oldFiber) - // Keep scanning and use the map to restore deleted items as moves. - for (; newIdx < newChildren.length; newIdx++) { - const newFiber = updateFromMap( - existingChildren, - returnFiber, - newIdx, - newChildren[newIdx], - lanes - ) - if (newFiber !== null) { - if (shouldTrackSideEffects) { - // newFiber是workInProgress Fiber, 如果复用了 oldFiber, 其alternate不为null - if (newFiber.alternate !== null) { - // 复用了 oldFiber 后, 需要将它从 map 中删除 - existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key) - } - } - // 插入 - lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx) - - // 指针操作 - if (previousNewFiber === null) { - resultingFirstChild = newFiber - } else { - previousNewFiber.sibling = newFiber - } - previousNewFiber = newFiber - } - } - - if (shouldTrackSideEffects) { - // map 遍历, 如果在上方删除后还有剩余说明这些都不会被用到了,需要被标记为删除 - existingChildren.forEach((child) => deleteChild(returnFiber, child)) - } - - return resultingFirstChild -} -``` - -进入方法,官方注释就告诉我们,fiber 是单链表没有反向指针,所以一个单链表的 currentFiber tree 和 JSX 对象组成的数组做对比是做不到数组常用的双指针从两端往中间遍历的。现象:** `newChildren[0]` 与 `fiber` 比较,`newChildren[1] `与 `fiber.sibling` 比较。** - -进入函数体内,当未知长度 oldFiber 单链表和未知大小的 newChildren 数组比较的时候,会出现四种情况: - -- oldFiber 单链表和 newChildren 数组同时遍历完了 -- oldFiber 单链表遍历完了,newChildren 数组没有遍历完 -- oldFiber 单链表没有遍历完,newChildren 数组遍历完了 -- oldFiber 单链表和 newCildren 数组都没有遍历完 - -`reconcileChildrenArray` 巧妙的处理了上方的各种情况,大概说下流程: - -1. 第一次进入 for 循环的时候,遍历比较 `oldFiber` 和 `newChildren[newIdx]` 查看是否可以复用,**优先处理的是更新操作**。 - - - 可以复用就返回 `existing`。 - - 不可复用返回 null。 - - `updateSlot` 会先判断 key 是否相同,不相同直接返回 null,会 break 退出循环 - - 如果 key 相同会再进入 `updateElement`等,这里会判断 type 是否相同,如果 type 不相同,会将 oldFiber 标记为 DELETION,并继续遍历 - -2. 当 oldFiber 为 null 或 `newIdx === newChildren.length` 时(即 oldFiber 单链表遍历完/newChildren 遍历完/同时遍历完),本轮循环结束,继续往下走~ - -3. 当从第一轮循环退出时,上方说的四种情况就出现了。 - - - 都遍历完了,diff 结束 - - - newChildren 遍历完, oldFiber 没有遍历完,把剩下的所有 oldFiber 标记为 `DELETION` - ```js - if (newIdx === newChildren.length) { - // We've reached the end of the new children. We can delete the rest. - deleteRemainingChildren(returnFiber, oldFiber); - return resultingFirstChild; - } - ``` - - oldFiber 遍历完,newChildren 没有遍历完,剩下的 newChildren 可以全部为插入 - ```js - // oldFiber 遍历完了, 从newIndex的位置继续遍历剩下的 newChildren, - // 剩下的全部是可以直接 PLACEMENT 的 - if (oldFiber === null) { - // for 循环,接着第一轮退出来时的 newIdx 位置开始, - return resultingFirstChild - } - ``` - - 都未遍历完,找到移动的节点,并插入正确的位置,就是 - - ```js - // 能走到这里,说明单链和数组都没有遍历完,那么一定是发生了位置的变换 - const existingChildren = mapRemainingChildren(returnFiber, oldFiber); - ``` - - 为了复用仅仅是移动了位置的 fiber,`mapRemainingChildren` 用 key 作为 map 的 key,用 fiber 作为 value,返回了 `existingChildren` 这个 map 对象,这样查询的时候,就能做到 O(1)的时间复杂度了。 - 接着对 newChildren 进行遍历, 通过`updateFromMap`找到位置变动但是可以复用的 fiber,若存在,同时从 `existingChildren` 中删除对应的 k-v 映射。 - - 找到可复用节点之后要插入正确的位置,是通过比较 `lastPlacedIndex` 和 `oldIndex`。 - - ```js - // placeChild 主要逻辑 - newFiber.index = newIndex; // 把新的位置给newFiber - const current = newFiber.alternate; - if (current !== null) { - const oldIndex = current.index; // 找到旧的索引 - if (oldIndex < lastPlacedIndex) { // 旧的索引和最后一个可复用节点的位置进行比较 - // This is a move. - newFiber.flags = Placement; // 需要移动到后面 - return lastPlacedIndex; - } else { - // This item can stay in place. - return oldIndex; // 无需移动, 同时返回oldIndex - } - } else { - // This is an insertion. - newFiber.flags = Placement; - return lastPlacedIndex; - } - ``` - - 最后,如果 `existingChildren` 不为空,说明剩下的 oldFiber 也都无用,需要被标记为 DELETION: - - ```js - existingChildren.forEach((child) => deleteChild(returnFiber, child)) - ``` - -多节点举例巩固一下: - -1. abcd --> acdb (字母表示 key) - -```js -===第一轮遍历开始=== -a(之后)vs a(之前) -key不变,可复用 -此时 a 对应的oldFiber(之前的a)在之前的数组(abcd)中索引为0 -所以 lastPlacedIndex = 0; - -继续第一轮遍历... - -c(之后)vs b(之前) -key改变,不能复用,跳出第一轮遍历 -此时 lastPlacedIndex === 0; -===第一轮遍历结束=== - -===第二轮遍历开始=== -newChildren === cdb,没用完,不需要执行删除旧节点 -oldFiber === bcd,没用完,不需要执行插入新节点 - -将剩余oldFiber(bcd)保存为map - -// 当前oldFiber:bcd -// 当前newChildren:cdb - -继续遍历剩余newChildren - -key === c 在 oldFiber中存在 -const oldIndex = c(之前).index; -此时 oldIndex === 2; // 之前节点为 abcd,所以c.index === 2 -比较 oldIndex 与 lastPlacedIndex; - -如果 oldIndex >= lastPlacedIndex 代表该可复用节点不需要移动 -并将 lastPlacedIndex = oldIndex; -如果 oldIndex < lastplacedIndex 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,代表该节点需要向右移动 - -在例子中,oldIndex 2 > lastPlacedIndex 0, -则 lastPlacedIndex = 2; -c节点位置不变 - -继续遍历剩余newChildren - -// 当前oldFiber:bd -// 当前newChildren:db - -key === d 在 oldFiber中存在 -const oldIndex = d(之前).index; -oldIndex 3 > lastPlacedIndex 2 // 之前节点为 abcd,所以d.index === 3 -则 lastPlacedIndex = 3; -d节点位置不变 - -继续遍历剩余newChildren - -// 当前oldFiber:b -// 当前newChildren:b - -key === b 在 oldFiber中存在 -const oldIndex = b(之前).index; -oldIndex 1 < lastPlacedIndex 3 // 之前节点为 abcd,所以b.index === 1 -则 b节点需要向右移动 -===第二轮遍历结束=== - - -``` - -最终 acd 3 个节点都没有移动,b 节点被标记为移动 - -2. abcd --> dabc (字母表示 key) - -```js -===第一轮遍历开始=== -d(之后)vs a(之前) -key改变,不能复用,跳出遍历 -===第一轮遍历结束=== - -===第二轮遍历开始=== -newChildren === dabc,没用完,不需要执行删除旧节点 -oldFiber === abcd,没用完,不需要执行插入新节点 - -将剩余oldFiber(abcd)保存为map - -继续遍历剩余newChildren - -// 当前oldFiber:abcd -// 当前newChildren dabc - -key === d 在 oldFiber中存在 -const oldIndex = d(之前).index; -此时 oldIndex === 3; // 之前节点为 abcd,所以d.index === 3 -比较 oldIndex 与 lastPlacedIndex; -oldIndex 3 > lastPlacedIndex 0 -则 lastPlacedIndex = 3; -d节点位置不变 - -继续遍历剩余newChildren - -// 当前oldFiber:abc -// 当前newChildren abc - -key === a 在 oldFiber中存在 -const oldIndex = a(之前).index; // 之前节点为 abcd,所以a.index === 0 -此时 oldIndex === 0; -比较 oldIndex 与 lastPlacedIndex; -oldIndex 0 < lastPlacedIndex 3 -则 a节点需要向右移动 - -继续遍历剩余newChildren - -// 当前oldFiber:bc -// 当前newChildren bc - -key === b 在 oldFiber中存在 -const oldIndex = b(之前).index; // 之前节点为 abcd,所以b.index === 1 -此时 oldIndex === 1; -比较 oldIndex 与 lastPlacedIndex; -oldIndex 1 < lastPlacedIndex 3 -则 b节点需要向右移动 - -继续遍历剩余newChildren - -// 当前oldFiber:c -// 当前newChildren c - -key === c 在 oldFiber中存在 -const oldIndex = c(之前).index; // 之前节点为 abcd,所以c.index === 2 -此时 oldIndex === 2; -比较 oldIndex 与 lastPlacedIndex; -oldIndex 2 < lastPlacedIndex 3 -则 c节点需要向右移动 - -===第二轮遍历结束=== -``` - -- TODO 和 vue 的 diff 比较 diff --git "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2068-Scheduler.md" "b/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2068-Scheduler.md" deleted file mode 100644 index d7d8aa2..0000000 --- "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2068-Scheduler.md" +++ /dev/null @@ -1,387 +0,0 @@ ---- -title: 'React基础原理8 - Scheduler' -date: 2022-11-28T23:23:21+08:00 -tags: [React] -draft: true ---- - -> Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。 - -- `Fiber`: 架构将单个组件作为 `工作单元`,使以组件为粒度的“异步可中断的更新”成为可能 -- `Scheduler`: 配合 `时间切片`,为每个工作单元分配一个 `可运行时间`,实现“异步可中断的更新” -- `lane` 模型: 控制不同 `优先级` 之间的关系与行为。比如多个优先级之间如何互相打断?优先级能否升降?本次更新应该赋予什么优先级? - -> 从源码层面讲,Concurrent Mode 是一套可控的“多优先级更新架构”。 - -## Scheduler - -主要两个功能: - -- 时间切片 -- 优先级调度 - -### 时间切片 - -先回顾一下,浏览器的事件循环机制(简约版): - -```txt -MacroTask --> MicroTask--> requestAnimationFrame--> 浏览器重排 / 重绘--> requestIdleCallback -``` - -> 时间切片的本质:就是模拟实现:`requestIdleCallback`,在当前帧还有空余时间时,执行回调。 - -时间切片选择使用 `MessageChannel` 实现,它的执行时机比 `setTimeout` 更靠前。 - -```js -// Scheduler 将需要被执行的回调函数作为 MessageChannel 的回调执行 -const channel = new MessageChannel(); -const port = channel.port2; -channel.port1.onmessage = performWorkUntilDeadline; - -requestHostCallback = function(callback) { - scheduledHostCallback = callback; - if (!isMessageLoopRunning) { - isMessageLoopRunning = true; - port.postMessage(null); - } -}; - -// 如果不支持 MessageChannel 会降级成 setTimeout -const _flushCallback = function() { - if (_callback !== null) { - try { - const currentTime = getCurrentTime(); - const hasRemainingTime = true; - _callback(hasRemainingTime, currentTime); - _callback = null; - } catch (e) { - setTimeout(_flushCallback, 0); - throw e; - } - } -}; -requestHostCallback = function(cb) { - if (_callback !== null) { - // Protect against re-entrancy. - setTimeout(requestHostCallback, 0, cb); - } else { - _callback = cb; - setTimeout(_flushCallback, 0); - } -}; -``` - -之前学习过 `workLoopSync`,是时候看看 `workLoopConcurrent`了: - -```js -function workLoopConcurrent() { - // Perform work until Scheduler asks us to yield - while (workInProgress !== null && !shouldYield()) { - performUnitOfWork(workInProgress); - } -} -``` - -唯一的不同就是多了个 `shouldYield` 是否暂停的判断,这个方法是从 `shceduler` 内部抛出来的。 - -```js -shouldYieldToHost = function() { - const currentTime = getCurrentTime(); - // deadline = currentTime + yieldInterval; - // yieldInterval 初始化为 5ms - if (currentTime >= deadline) { - // There's no time left. We may want to yield control of the main - // thread, so the browser can perform high priority tasks. The main ones - // are painting and user input. If there's a pending paint or a pending - // input, then we should yield. But if there's neither, then we can - // yield less often while remaining responsive. We'll eventually yield - // regardless, since there could be a pending paint that wasn't - // accompanied by a call to `requestPaint`, or other main thread tasks - // like network events. - if (needsPaint || scheduling.isInputPending()) { - // There is either a pending paint or a pending input. - return true; - } - // There's no pending input. Only yield if we've reached the max - // yield interval. - return currentTime >= maxYieldInterval; - } else { - // There's still time left in the frame. - return false; - } -}; -``` - -> 注释写的很明白,主要就是看是否有剩余时间是否用完,在 Schdeduler 中,为任务分配的初始剩余时间为 5ms,随着应用的运行,根据 fps 动态调整可执行时间。 - -```js -forceFrameRate = function(fps) { - if (fps < 0 || fps > 125) return - if (fps > 0) { - yieldInterval = Math.floor(1000 / fps); - } else { - // reset the framerate - yieldInterval = 5; - } -}; -``` - -启用 Concurrent Mode 后每个任务的执行时间大体都是多于 5ms 的一小段时间 —— 每个时间切片被设定为 5ms,任务本身再执行一小段时间,所以整体时间是多于 5ms 的时间 - -OK,到这里,`performUnitOfWork` 是怎么暂停的已经清除,主要是由于分配的剩余时间来决定的,那么任务怎么重启呢,这个就要看优先级调度了。 - -### 优先级调度 - -`Scheduler` 是独立于 React 的包,**它的优先级也是独立于 React 的优先级**。 - -```js -// SchedulerPriorities.js -export const NoPriority = 0; -export const ImmediatePriority = 1; -export const UserBlockingPriority = 2; -export const NormalPriority = 3; -export const LowPriority = 4; -export const IdlePriority = 5; - -function unstable_runWithPriority(priorityLevel, eventHandler) { - switch (priorityLevel) { - case ImmediatePriority: // 最高优先级会立即执行 - case UserBlockingPriority: - case NormalPriority: - case LowPriority: - case IdlePriority: - break; - default: - priorityLevel = NormalPriority; - } - - var previousPriorityLevel = currentPriorityLevel; - currentPriorityLevel = priorityLevel; - - try { - return eventHandler(); - } finally { - currentPriorityLevel = previousPriorityLevel; - } -} -``` - -可见,`Scheduler` 有 5 种优先级,默认是 `NormalPriority`,`ImmediatePriority` 是最高优先级,会立即执行。 - -```js -function commitRoot(root) { - // 返回 scheduler 中的 currentPriorityLevel - const renderPriorityLevel = getCurrentPriorityLevel(); - // 发起一个立即执行的任务,并指定这个任务的优先级 - runWithPriority( - ImmediateSchedulerPriority, - commitRootImpl.bind(null, root, renderPriorityLevel), - ); - return null; -} -``` - -优先级的意义 --- 赋予不同优先级不同的过期时间~ - -看一下 `scheduler` 的这个方法 `unstable_scheduleCallback`,对外抛出一般是 `schedulerCallback`,直译过来就是安排回调,也就可以理解为是调度任务: - -```js -// Times out immediately -var IMMEDIATE_PRIORITY_TIMEOUT = -1; -// Eventually times out -var USER_BLOCKING_PRIORITY_TIMEOUT = 250; -var NORMAL_PRIORITY_TIMEOUT = 5000; -var LOW_PRIORITY_TIMEOUT = 10000; -// Never times out -var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; - -function unstable_scheduleCallback(priorityLevel, callback, options) { - var currentTime = getCurrentTime(); - - var startTime; - if (typeof options === 'object' && options !== null) { - var delay = options.delay; - if (typeof delay === 'number' && delay > 0) { - startTime = currentTime + delay; - } else { - startTime = currentTime; - } - } else { - startTime = currentTime; - } - - var timeout; - switch (priorityLevel) { - case ImmediatePriority: - timeout = IMMEDIATE_PRIORITY_TIMEOUT; - break; - case UserBlockingPriority: - timeout = USER_BLOCKING_PRIORITY_TIMEOUT; - break; - case IdlePriority: - timeout = IDLE_PRIORITY_TIMEOUT; - break; - case LowPriority: - timeout = LOW_PRIORITY_TIMEOUT; - break; - case NormalPriority: - default: - timeout = NORMAL_PRIORITY_TIMEOUT; - break; - } - - // 过期时间 - var expirationTime = startTime + timeout; - - var newTask = { - id: taskIdCounter++, - callback, - priorityLevel, - startTime, - expirationTime, - sortIndex: -1, - }; - if (enableProfiling) { - newTask.isQueued = false; - } - - if (startTime > currentTime) { - // This is a delayed task. - newTask.sortIndex = startTime; - push(timerQueue, newTask); - if (peek(taskQueue) === null && newTask === peek(timerQueue)) { - // All tasks are delayed, and this is the task with the earliest delay. - if (isHostTimeoutScheduled) { - // Cancel an existing timeout. - cancelHostTimeout(); - } else { - isHostTimeoutScheduled = true; - } - // Schedule a timeout. - requestHostTimeout(handleTimeout, startTime - currentTime); - } - } else { - newTask.sortIndex = expirationTime; - push(taskQueue, newTask); - if (enableProfiling) { - markTaskStart(newTask, currentTime); - newTask.isQueued = true; - } - // Schedule a host callback, if needed. If we're already performing work, - // wait until the next time we yield. - if (!isHostCallbackScheduled && !isPerformingWork) { - isHostCallbackScheduled = true; - requestHostCallback(flushWork); - } - } - - return newTask; -} -``` - -首先,根据任务优先级得到了不同的任务过期时间,放到 `newTask` 中;`options` 可以设置 `delay` 时间,当设置了 `delay` 在下面入队列的时候就会进入 `timerQueue` 队列 `push(timerQueue, newTask);`,否则进入的是 `taskQueue` 队列。 - -上方的 push、peek 都是 scheculer 实现的优先队列的方法,之所以自己实现了一个小顶堆优先队列,是为了`O(1)`复杂度找到上方 `timerQueue` 和 `taskQueue` 中时间最早的那个任务。 - -继续往下走,任务的重启就在 `requestHostCallback` 这个方法,这个方法根据是否支持 `MessageChannel` 也有两种实现,暂且不关注,主要关注它后面的流程,`requestHostCallback` 调用了 `flushWork`,再调用 `workLoop`: - -```js -function workLoop(hasTimeRemaining, initialTime) { - let currentTime = initialTime; - advanceTimers(currentTime); - currentTask = peek(taskQueue); - while ( - currentTask !== null && - !(enableSchedulerDebugging && isSchedulerPaused) - ) { - if ( - currentTask.expirationTime > currentTime && - (!hasTimeRemaining || shouldYieldToHost()) - ) { - // This currentTask hasn't expired, and we've reached the deadline. - break; - } - const callback = currentTask.callback; // 注册任务的回调函数 - if (typeof callback === 'function') { - currentTask.callback = null; - currentPriorityLevel = currentTask.priorityLevel; - const didUserCallbackTimeout = currentTask.expirationTime <= currentTime; - if (enableProfiling) { - markTaskRun(currentTask, currentTime); - } - /* ---------- 关注这里 ---------- */ - const continuationCallback = callback(didUserCallbackTimeout); - currentTime = getCurrentTime(); - if (typeof continuationCallback === 'function') { - currentTask.callback = continuationCallback; - if (enableProfiling) { - markTaskYield(currentTask, currentTime); - } - } else { - if (enableProfiling) { - markTaskCompleted(currentTask, currentTime); - currentTask.isQueued = false; - } - if (currentTask === peek(taskQueue)) { - pop(taskQueue); - } - } - advanceTimers(currentTime); - } else { - pop(taskQueue); - } - currentTask = peek(taskQueue); - } - // Return whether there's additional work - if (currentTask !== null) { - return true; - } else { - const firstTimer = peek(timerQueue); - if (firstTimer !== null) { - requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); - } - return false; - } -} -``` - -重点是:如果 `continuationCallback` 即调度注册的回调函数,它的返回值为 `function` 时,会把 `continuationCallback` 作为当前任务的回调函数,否则 `pop(taskQueue);` 把当前执行的任务清除 `taskQueue`,而在 `render` 阶段 `performConcurrentWorkOnRoot` 函数的末尾有这么段代码: - -```js -if (root.callbackNode === originalCallbackNode) { - // The task node scheduled for this root is the same one that's - // currently executed. Need to return a continuation. - return performConcurrentWorkOnRoot.bind(null, root); -} -``` - -这里就是返回了一个函数 `continuation`。 - - - -## 参考 - -- [React 技术揭密](https://react.iamkasong.com/) -- [为什么 Scheduler 不使用 generator](https://github.com/facebook/react/issues/7942#issuecomment-254987818) -- [React Scheduler 为什么使用 MessageChannel 实现](https://juejin.cn/post/6953804914715803678) -- [window.requestAnimationFrame](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame) -- [IntersectionObserver API 使用教程](https://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html) - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202211291457300.png) diff --git "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2069-lane\346\250\241\345\236\213.md" "b/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2069-lane\346\250\241\345\236\213.md" deleted file mode 100644 index 3f228c1..0000000 --- "a/content/posts/react/React\345\237\272\347\241\200\345\216\237\347\220\2069-lane\346\250\241\345\236\213.md" +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: 'React基础原理9 - Lane模型' -date: 2022-11-30T11:59:50+08:00 -tags: [React] -draft: true ---- - -React 与 Scheduler 是两套不同的优先级机制。它们之间是可以转换的`lanePriorityToSchedulerPriority/schedulerPriorityToLanePriority`。 - -React 的优先级机制需要满足以下情况: - -- 可以表示优先级的不同 -- 可以表示 `批` 的概念,因为可能同时存在几个同优先级的更新 -- 便于进行优先级相关计算 - -针对第一点,React 采用一个 31 位二进制数来表示优先级,如 `lane:行车道` 的意思一样,每一个位置都表示一条行车道,位数越低,优先级越高 -针对第二点,有些优先级同时占用好几条车道,依次来表示`批` - -```TS -export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000; -export const NoLane: Lane = /* */ 0b0000000000000000000000000000000; - -export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001; -export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010; - -export const InputDiscreteHydrationLane: Lane = /* */ 0b0000000000000000000000000000100; -const InputDiscreteLanes: Lanes = /* */ 0b0000000000000000000000000011000; - -const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000100000; -const InputContinuousLanes: Lanes = /* */ 0b0000000000000000000000011000000; - -export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000; -export const DefaultLanes: Lanes = /* */ 0b0000000000000000000111000000000; - -const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000; -const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000; - -const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000; - -export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000; - -export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000; - -const NonIdleLanes = /* */ 0b0000111111111111111111111111111; - -export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000; -const IdleLanes: Lanes = /* */ 0b0110000000000000000000000000000; - -export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000; -``` - -> 从 SyncLane 往下一直到 SelectiveHydrationLane,赛道的优先级逐步降低。 - -针对第三点,那就是二进制有利于位运算,也就方便了优先级的相关计算: - -- 取交集(都拥有的赛道) - `a & b` -- 取并集(合并赛道) - `a | b` -- 从 a 的赛道 移除 b 的赛道 - `a & ~b` diff --git a/content/posts/react/hooks/React_hooks--useContext.md b/content/posts/react/hooks/React_hooks--useContext.md deleted file mode 100644 index f95c6db..0000000 --- a/content/posts/react/hooks/React_hooks--useContext.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: 'React hooks--useContext' -date: 2023-07-27T11:47:25+08:00 -tags: [] -series: [hooks] -categories: [React hooks] -weight: 4 ---- - -## useContext - -`const value = useContext(SomeContext)` - -简单理解就是使用传递下来的 context 上下文,这个 hook 不是独立使用的,需要先创建上下文。 - -### createContext - -`const SomeContext = createContext(defaultValue)` - -创建的上下文有 `Provider` 和 Consumer(过时): - -```tsx - - - - -// useContext 替代 Consumer -const value = useContext(SomeContext) -``` - -`useContext` 获取的是离它**最近**的 `Provider` 提供的 `value` 属性,如果没有 Provider 就去读取对应 `context` 的 `defaultValue`。 - -### 性能优化 - -当 `context` 发生变化时,会自动触发使用了它的组件重新渲染。因此,当 Provider 的 value 传递的值为**对象或函数**时,应该使用 `useMemo` 或 `useCallback` 将传递的值包裹一下避免整个子树组件的无效渲染(比如在 useEffect 中讲过的:函数在每次渲染中都是新的函数)。 - -## reference - -- [createContext](https://react.dev/reference/react/createContext) -- [useContext](https://react.dev/reference/react/useContext) diff --git a/content/posts/react/hooks/React_hooks--useEffect&useLayoutEffect.md b/content/posts/react/hooks/React_hooks--useEffect&useLayoutEffect.md deleted file mode 100644 index 973a003..0000000 --- a/content/posts/react/hooks/React_hooks--useEffect&useLayoutEffect.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -title: 'React hooks--useEffect&useLayoutEffect' -date: 2023-07-25T21:47:25+08:00 -tags: [] -series: [hooks] -categories: [React hooks] -weight: 2 ---- - -## useEffect - -`useEffect(() => { setup }, dependencies?)` - -### 执行时机 - -useEffect 是异步的: -`setup` 函数在 DOM 被渲染后执行。如果 setup 返回了 `cleanup` 函数,会`先执行 cleanup,再执行 setup`。 -当组件挂载时都会先调用一次 `setup`,当组件被卸载时,也会调用一次 `cleanup`。 - -> 值得注意,`cleanup` 里的状态是上一次的状态,即它被 return 那一刻的状态,因为它是函数嘛,类似快照。 - -关于 dependencies: - -- 无,每次都会执行 setup -- [],只会执行一次 setup -- [dep1,dep2,...],当有依赖项改变时(依据 Object.is),才会执行 setup - -### 心智模型--每一次渲染的 everything 都是独立的 - -一个看上去反常的例子: - -```tsx -// Note: 假设 count 为 0 -useEffect( - () => { - const id = setInterval(() => { - setCount(count + 1) // 只会触发一次 因为实际上这次渲染的count永远为 0,永远是0+1 - }, 1000) - return () => clearInterval(id) - }, - [] // Never re-runs -) -``` - -因此需要把 count 正确的设为依赖,才会触发再次渲染,但是这么做又会导致每次渲染都先 cleanup 再 setup,这显然不是高效的。可以使用类似于 setState 的函数式写法:`setCount(c => c + 1)` 即可。这么做是既告诉 React 依赖了哪个值,又不会再次触发 effect 。 - -> 并不是 dependencies 的值在“不变”的 effect 中发生了改变,而是 effect 函数本身在每一次渲染中都不相同。 - -然而,如果 setCount(c => c + 1) 变成了 `setCount(c => c + anotherPropOrState)`,还是得把 anotherPropOrState 加入依赖,这么做还是需要不停的 cleanup/setup。一个推荐的做法是使用 `useReducer`: - -```tsx -useEffect( - () => { - const id = setInterval(() => { - dispatch({ type: 'add_one_step' }) - }, 1000) - return () => clearInterval(id) - }, - [dispatch] // React会保证dispatch在组件的声明周期内保持不变。所以不再需要重新订阅定时器。 -) -``` - -### 使用场景 - -`useEffect` 在与`浏览器操作/网络请求/第三方库`状态协同中发挥着极其重要的作用。 - -着重讲一下在 useEffect 中请求数据的注意点: - -```tsx -// 借用Dan博客的例子 -function SearchResults() { - // 🔴 Re-triggers all effects on every render - function getFetchUrl(query) { - return 'https://hn.algolia.com/api/v1/search?query=' + query - } - - useEffect(() => { - const url = getFetchUrl('react') - // ... Fetch data and do something ... - }, [getFetchUrl]) // 🚧 Deps are correct but they change too often - - useEffect(() => { - const url = getFetchUrl('redux') - // ... Fetch data and do something ... - }, [getFetchUrl]) // 🚧 Deps are correct but they change too often -} -``` - -因为函数组件中的方法每次都是不一样的, 所以会造成 effect 每次都被触发, 这不是想要的。有两种办法解决: - -1. 如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,然后就可以自由地在 effects 中使用 - - ```tsx - // ✅ Not affected by the data flow - function getFetchUrl(query) { - return 'https://hn.algolia.com/api/v1/search?query=' + query - } - - function SearchResults() { - useEffect(() => { - const url = getFetchUrl('react') - // ... Fetch data and do something ... - }, []) // ✅ Deps are OK - - useEffect(() => { - const url = getFetchUrl('redux') - // ... Fetch data and do something ... - }, []) // ✅ Deps are OK - - // ... - } - ``` - -2. 使用 useCallback 包裹 - - ```tsx - function SearchResults() { - // ✅ Preserves identity when its own deps are the same - const getFetchUrl = useCallback((query) => { - return 'https://hn.algolia.com/api/v1/search?query=' + query - }, []) // ✅ Callback deps are OK - - useEffect(() => { - const url = getFetchUrl('react') - // ... Fetch data and do something ... - }, [getFetchUrl]) // ✅ Effect deps are OK - - useEffect(() => { - const url = getFetchUrl('redux') - // ... Fetch data and do something ... - }, [getFetchUrl]) // ✅ Effect deps are OK - } - ``` - -> 到处使用 `useCallback` 是件挺笨拙的事。当我们需要将函数传递下去并且函数会在子组件的 `effect` 中被调用(简而言之:参与数据流)的时候,`useCallback` 是很好的技巧且非常有用。 - -另一个注意点是:`因为要返回 cleanup,所以 setup 是不能用 async 来修饰的`。 - -## useLayoutEffect - -useLayoutEffect 和 useEffect 不同的地方在于 执行时机,`在屏幕渲染之前执行`。同时 setup 函数的执行会阻塞浏览器渲染。 - -## reference - -- [useEffect](https://react.dev/reference/react/useEffect) -- [A Complete Guide to useEffect](https://overreacted.io/a-complete-guide-to-useeffect/) -- [How to fetch data with React Hooks](https://www.robinwieruch.de/react-hooks-fetch-data/) diff --git a/content/posts/react/hooks/React_hooks--useMemo&useCallback.md b/content/posts/react/hooks/React_hooks--useMemo&useCallback.md deleted file mode 100644 index 3db50ba..0000000 --- a/content/posts/react/hooks/React_hooks--useMemo&useCallback.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: 'React hooks--useMemo&useCallback' -date: 2023-07-27T11:47:25+08:00 -tags: [] -series: [hooks] -categories: [React hooks] -weight: 5 ---- - -在之前的笔记中,讲了很多次的心智模型 -- 组件在每次渲染中都是它全新的自己。所以当对象或函数参与到数据流之中时,就需要进行优化处理,来避免不必要的渲染。 - -## useMemo - -`const cachedValue = useMemo(calculateValue, dependencies)` - -### memo - -`const MemoizedComponent = memo(SomeComponent, arePropsEqual?)` - -`useMemo` 是加在数据上的缓存,而 `memo` api 是加在组件上的,只有当 `props` 发生变化时,才会再次渲染。 - -没有`arePropsEqual`,默认比较规则就是 `Object.is` - -## useCallback - -`const cachedFn = useCallback(fn, dependencies)` - -## reference - -- [useMemo](https://react.dev/reference/react/useMemo) -- [useCallback](https://react.dev/reference/react/useCallback) diff --git a/content/posts/react/hooks/React_hooks--useRef.md b/content/posts/react/hooks/React_hooks--useRef.md deleted file mode 100644 index 28eb093..0000000 --- a/content/posts/react/hooks/React_hooks--useRef.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: 'React hooks--useRef' -date: 2023-07-26T11:47:25+08:00 -tags: [] -series: [hooks] -categories: [React hooks] -weight: 3 ---- - -## useRef - - - -`const ref = useRef(initialValue)` - -`ref` 就是引用,vue 中也有类似的概念,在 react 中 ref 是一个形如 `{current: initialValue}` 的对象,不仅可以用来**操作 DOM**,也可以**承载数据**。 - -### 与 useState 的区别 - -既然能承载数据,那么和 `useState` 有什么渊源呢?让我们看看官网的一句话:`useRef is a React Hook that lets you reference a value that’s not needed for rendering.`,重点在后半句,大概意思就是**引用了一个与渲染无关的值**。 - -在承载数据方面,这就是与 `useState` 最大的区别: - -- `useRef` 引用的数据在改变后不会影响组件渲染,类似于函数组件的一个实例属性 -- `useState` 的值改变后会引发重新渲染 - -> 函数式组件在每次渲染中的 props、state 等等都是那次渲染中所独有的,当需要在 useEffect 中访问`未来`的 props/state 时,可以使用 useRef 。[Demo](https://codesandbox.io/s/ox200vw8k9?file=/src/index.js:165-178): 随意输入并 send 后,再次输入,获取的是全部的输入。 - -### TS 环境下的使用 - -在 TS 环境下,往往你可能会遇到此类报错: -`Type '{ ref: RefObject; }' is not assignable to type 'IntrinsicAttributes'. - Property 'ref' does not exist on type 'IntrinsicAttributes'.ts(2322)` - -这就需要做一定的类型处理了。 - -前置先介绍两个 api: - -1. 因为函数不能直接接收 ref,所以在子组件中使用 `forwardRef` 这个 api 来传递进子组件。在类组件时代也常用来跨祖孙组件传递 ref, 比如 HOC。 -2. 父组件想要访问子组件的数据,需要使用 `useImperativeHandle` 这个 api 对外暴露。 - -```tsx -// 父组件 -const Demo: FC = () => { - // 给自定义组件添加 ref,需要使用 ElementRef 来获取 ref 的类型 - const SonRef = useRef>(null) - - const handleClick = () => { - SonRef.current?.test() - } - - return ( - <> - - - - ) -} - -// 子组件 两种方式设置 ref 类型 -// 1. 用泛型,注意与参数的位置是相反的 -const Son = forwardRef((props, ref) => { - useImperativeHandle(ref, () => { - return { - test: () => console.log('hello world') - } - }) - return ( - <> -
hello world
- - ) -}) - -// 2. 在参数中使用 Ref -// const Son = forwardRef((props, ref: Ref) => {}) -``` - -## reference - -- [useRef](https://react.dev/reference/react/useRef) -- [forwardRef](https://react.dev/reference/react/forwardRef) diff --git a/content/posts/react/hooks/React_hooks--useState&useReducer.md b/content/posts/react/hooks/React_hooks--useState&useReducer.md deleted file mode 100644 index 686d99a..0000000 --- a/content/posts/react/hooks/React_hooks--useState&useReducer.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: 'React hooks--useState&useReducer' -date: 2023-07-25T11:47:25+08:00 -tags: [] -series: [hooks] -categories: [React hooks] -weight: 1 ---- - -## useState - -React hooks 的出现一大作用就是让函数具有了 state,进而才能全面的拥抱函数式编程。 - -函数式编程的好处就是两个字:`干净`,进而可以很好的实现逻辑复用。 - -```tsx -const [state, setState] = useState(initialState) -``` - -### 心智模型(重要) - -在函数组件中所有的状态在一次渲染中实际上都是`不变的,是静态的`,你所能看到的 `setState` 引起的渲染其实已经是下一次渲染了。理解这一点尤为重要,感谢 Dan 的[文章讲解](https://overreacted.io/a-complete-guide-to-useeffect/#each-render-has-its-own-props-and-state)。 - -```tsx -const [number, setNumber] = useState(0) - -const plus = () => { - setNumber(number + 1) - setNumber(number + 1) - setNumber(number + 1) - console.log('plus ---', number) // 输出仍然为 0, 就像一个快照 -} - -useEffect(() => { - console.log('effect ---', number) // 输出为 1,因为setNumber(number + 1) 的number在这次渲染流程中始终都是 0 !!! -}, [number]) - -/* ---------- 如果依赖于前一次的状态那么应该使用函数式写法 ---------- */ -const plus = () => { - setNumber((prev) => prev + 1) // 更新函数 prev 准确的说是pending state - setNumber((prev) => prev + 1) - setNumber((prev) => prev + 1) - console.log('plus ---', number) // 输出为 0 -} - -useEffect(() => { - //因为更新函数会被保存到一个队列,在下一次渲染的时候每个函数会根据前一个状态来动态计算 - console.log('effect ---', number) // 输出为 3 -}, [number]) -``` - -两个小点: - -- 如果 setState 的值与前一次渲染一样(依据 Object.is),则不会触发重新渲染 - ```js - const obj1 = { num: 1 } - const obj2 = obj1 - obj2.num = 2 - Object.is(obj1, obj2) // true - ``` -- setState 的动作是批量更新的,想要立马更新使用 flushSync api - -### 初始化值为函数类型注意点 - -另一点是 `initialState`,可以是`任何类型`,当为函数类型时需要注意: - -- useState(initFn()),这是传了函数执行后的结果,每次渲染都会执行,效率较低 -- useState(initFn),这是把函数自身传过去 - ---- - -## useReducer - -这是 React 生态中状态管理的一种设计模式。 - -`const [state, dispatch] = useReducer(reducer, initialArg, init?)` - -`init` 不为空时,初始 state 为 `init(initilaArg)` 函数执行的返回值;否则就是 `initialArg` 自身。这么做的原因和不要在 useState 时传函数执行一个道理。 - -### 使用场景 - -当一个 state 的状态更新被多个不同的事件处理时,就可以考虑把它重构为 `reducer` 了。另一个场景是 useEffect 中依赖之间有相互依赖从而去计算下一个状态时,可以使用 reducer 来去掉 useEffect 的 deps。 - -`reducer` 函数在组件之外,接收两个参数 `reducer(state, action)`,必须返回下一个状态。 - -```tsx -function reducer(state, action) { - // like this - if (action.type === 'add') { - return { age: state.age + 1 } // 返回nextState - } - // 一般返回 {type: xxx, otherProps: ...} -} -``` - -与 useState 一致,reducer return 的 nextState 如果和前一次状态一致,那么就不会触发渲染,判断是否一致的规则是 `Object.is(a,b)`,所以是不能直接改 state 的,需要 `{...state, changeProps: val, ...}`。 - -> useReducer 除了可以更好的简化代码,也更方便 debug,但如果不是对状态多重操作的话,也没必要这么做。 - -## reference - -- [useState](https://react.dev/reference/react/useState) -- [useReducer](https://react.dev/reference/react/useReducer) \ No newline at end of file diff --git "a/content/posts/vue/Vue\345\223\215\345\272\224\345\274\217\345\216\237\347\220\206.md" "b/content/posts/vue/Vue\345\223\215\345\272\224\345\274\217\345\216\237\347\220\206.md" deleted file mode 100644 index 41597b3..0000000 --- "a/content/posts/vue/Vue\345\223\215\345\272\224\345\274\217\345\216\237\347\220\206.md" +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: 'Vue2 响应式原理' -date: 2022-04-26 -series: [] -categories: [Vue] -weight: ---- - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210261654753.png) - -## 核心 - -VUE2 的响应式原理实现主要基于: - -- [Object.defineProperty(obj, prop, descriptor)](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) -- [观察者模式](https://yokiizx.site/posts/js/%E8%A7%82%E5%AF%9F%E8%80%85%E5%92%8C%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85/) - -### init - reactive 化 - -Vue 初始化实例时,通过 `Object.defineProperty` 为 `data` 中的所有数据添加 `setter/getter`。这个过程称为 reactive 化。 - -所以: - -- vue 监听不到 data 中的对象属性的增加和删除,必须在初始化的时候就声明好对象的属性。 - 解决方案:或者使用 Vue 提供的 `$set` 方法;也可以用 `Object.assign({}, source, addObj)` 去创建一个新对象来触发更新。 - -- Vue 也监听不到数组索引和长度的变化,因为当数据是数组时,Vue 会直接停止对数据属性的监测。至于为什么这么做,尤大的解释是:解决性能问题。 - 解决方案:新增用 `$set`,删除用 splice,Vue 对数组的一些方法进行了重写来实现响应式。 - -看下 `defineReactive` 源码: - -```js -// 以下所有代码为简化后的核心代码,详细的见vue2的gihub仓库哈 -export function defineReactive(obj: object, key: string, val?: any, ...otehrs) { - const dep = new Dep() - Object.defineProperty(obj, key, { - enumerable: true, - configurable: true, - get: function reactiveGetter() { - if (Dep.target) dep.depend() - return value - }, - set: function reactiveSetter(newVal) { - const value = getter ? getter.call(obj) : val - if (!hasChanged(value, newVal)) return - val = newVal - dep.notify() - } - }) - return dep -} -``` - -函数 `defineReactive` 在 `initProps`、`observer` 方法中都会被调用(initData 调用 observe),目的就是给数据添加 `getter/setter`。 - -再看下 `Dep` 源码: - -```js -/** - * 被观察者,依赖收集,收集的是使用到了这个数据的组件对应的 watcher - */ -export default class Dep { - constructor() { - this.subs = [] // 收集订阅者(观察者) - } - addSub(sub: DepTarget) { - this.subs.push(sub) - } - removeSub(sub: DepTarget) { - this.subs[this.subs.indexOf(sub)] = null - } - depend() { - // Dep.target 是一个具有唯一id的 watcher 对象 - if (Dep.target) { - // 收集watcher,建议结合下面的Wather一起看 - Dep.target.addDep(this) - } - } - notify() { - for (let i = 0, l = subs.length; i < l; i++) { - const sub = subs[i] - sub.update() - } - } -} -``` - -结合起来看: - -- getter:当 getter 调用的时候,会 调用 wather 的方法,把 watcher 自身加入到 dep 的 subs 中 -- setter:当 setter 调用的时候,去 通知执行刚刚注册的函数 - -### mount - watcher - -先看下生命周期 `mountComponent` 函数: - -```js -// Watcher 在此处被实例化 -export function mountComponent(vm: Component, el: Element | null | undefined): Component { - vm.$el = el - let updateComponent = () => { - vm._update(vm._render() /*...*/) // render 又触发 Dep 的 getter - } - - // we set this to vm._watcher inside the watcher's constructor - // since the watcher's initial patch may call $forceUpdate - // (e.g. inside child component's mounted hook), - // which relies on vm._watcher being already defined - new Watcher(vm, updateComponent /* ... */) - - // ... - - return vm -} -``` - -再看看 `Watcher` 源码 - -```js -export default class Watcher implements DepTarget { - constructor(vm: Component | null, expOrFn: string | (() => any) /* ... */) { - this.getter = expOrFn - this.value = this.get() - // ... - } - - /** - * Evaluate the getter, and re-collect dependencies. - */ - get() { - // dep.ts 中 抛出的方法,用来设置 Dep.target - pushTarget(this) // Dep.target = this 也就是这个Watcher的实例对象 - let value - const vm = this.vm - // 调用updateComponent重新render,触发依赖的重新收集 - value = this.getter.call(vm, vm) - return value - } - - addDep(dep: Dep) { - // ...精简了 - dep.addSub(this) - } - // Watcher 的 update、run方法都会调用 get 来触发 getter 的执行,形成闭环 -} -``` - -结合 `mountComponent` 和 `Watcher` 的源码不能看出: - -- `mountComponent` 执行时创建了 `watcher` 对象,一个 vue component 对应一个 `watcher`。`new Watcher` 时,构造器中最终会调用 `updateComponent` 函数,这个函数会调用 `render` 函数重新渲染,再触发 dep 中的 getter,重新收集依赖 -- `Watcher` 中实例 this 被设置成了 Dep 的 target,同时该 watcher 对应的组件,只要用到了 data 中的数据,渲染的时候就会把这个 watcher 加入到 dep 的 subs 中 - -由此,`watcher` 把 vue 组件和 dep 依赖连接了起来。 - -### update - -当 data 中的数据发生改变时,就会触发 setter 函数的执行,进而触发 Dep 的 notify 函数。 - -```js -notify() { - for (let i = 0, l = subs.length; i < l; i++) { - const sub = subs[i] - sub.update() - } -} -``` - -subs 中收集的是每个 watcher,有多少个组件使用到了目标数据,这些个组件都会被重新渲染。 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202210270940977.png) - -现在再看开头官网的图应该就很清晰了吧~👻 - -### 小结 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202302221241366.png) - -简单小结一下: vue 中的数据会被 Object.defineProperty() 拦截,添加 getter/setter 函数,其中 getter 中会把组件的 watcher 对象添加进依赖 Dep 对象的订阅列表里,setter 则负责当数据发生变化时触发订阅列表里的 watcher 的 update,最终会调用 vm.render 触发重新渲染,并重新收集依赖。 - ---- - -至于 Vue3 的原理,由于目前还未使用过(我更倾向于使用 React,不香嘛~),只是大概了解是使用 `Proxy` 来解决 `Object.defineProperty` 的缺陷的。下面是他人写的总结,有时间可以看看 - -## 参考 - -- [这次终于把 Vue3 响应式原理搞懂了!](https://mp.weixin.qq.com/s/F2yYqXE_xTHl0d8j03I-UQ) diff --git "a/content/posts/vue/Vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246.md" "b/content/posts/vue/Vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246.md" deleted file mode 100644 index b992776..0000000 --- "a/content/posts/vue/Vue\346\272\220\347\240\201\344\270\255\347\232\204\350\256\276\350\256\241\347\276\216\345\255\246.md" +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: 'Vue2 源码中的设计美学' -date: 2022-04-30 -series: [] -categories: [Vue] -weight: ---- - -## 为什么能通过 this.xxx 就能访问到对应的 methods 和 data - -methods 能通过 this 访问比较简单,就是在初始化的时候,通过 bind 修正 this 指向 vue 的实例。 - -```js -class Person { - constructor(name) { - this.name = name - } -} - -/** - * 源码中实现 this.methodXxx 的主要部分 - */ -function initMethods(vm, methods) { - for (const key in methods) { - vm[key] = typeof methods[key] === 'function' ? methods[key].bind(vm) : () => {} - } -} - -const person = new Person('zhang san') -const methods = { - a: () => { - console.log('a') - }, - b: () => { - console.log('b') - } -} -initMethods(person, methods) -person.a() // a -person.b() // b -``` - -data 中的数据能通过 this.xxx 访问则需要数据拦截了。 - -```js -/** - * 模拟实现 initData, 源码里 data 是从 this.$options.data 获取的 - */ -function initData(vm, data) { - var data = (vm._data = typeof data === 'function' ? getData(data, vm) : data || {}) - // 代理拦截 - Object.keys(data).forEach(key => proxy(vm, '_data', key)) - // 监听数据 - // observe(data, true /** asRootData */) -} -function getData(data, vm) { - return data.call(vm, vm) -} - -var sharedPropertyDefinition = { - enumerable: true, - configurable: true, - get: function () {}, - set: function () {} -} -function proxy(vm, sourcekey, key) { - sharedPropertyDefinition.get = function getter() { - return this[sourcekey][key] - } - sharedPropertyDefinition.set = function setter(val) { - this[sourcekey][key] = val - } - Object.defineProperty(vm, key, sharedPropertyDefinition) -} - -const data = () => { - return { - abc: 'hello world' - } -} -initData(person, data) -console.log(person) -``` - -## 参考 - -- [为什么 Vue2 this 能够直接获取到 data 和 methods](https://juejin.cn/post/7112255428452417549) diff --git "a/content/posts/z-drafts/VsCode\346\240\274\345\274\217\345\214\226\345\273\272\350\256\256.md" "b/content/posts/z-drafts/VsCode\346\240\274\345\274\217\345\214\226\345\273\272\350\256\256.md" deleted file mode 100644 index c2bb725..0000000 --- "a/content/posts/z-drafts/VsCode\346\240\274\345\274\217\345\214\226\345\273\272\350\256\256.md" +++ /dev/null @@ -1,107 +0,0 @@ -······················--- -title: 'VsCode 格式化建议' -date: 2022-09-18T20:37:14+08:00 -tags: [tool, vscode] -series: [format] -categories: [tool] -draft: true - ---- - -## 前言 - -做项目时,独木难支,难免会有多人合作的场景,甚至是跨地区协调来的小伙伴进行配合。如果两个人有着不同的编码风格,再不幸两人同时修改了同一份文件,就极有可能会产生不必要的代码冲突,因此,一个项目的代码格式化一定要统一。这对于上线前的代码 codereview 也是极好的,能有效避免产生大片红大片绿的情况。。。废话不多说,上菜! - -## 插件 - -进行配置前,确保安装了以下两个插件: - -- ESLint -- Prettier - Code formatter - -## 配置 - -1. 项目根目录下创建`.vscode/settings.json`,用以覆盖本地的保存配置 - - ```json - { - "editor.formatOnSave": true, // 保存时自动格式化 - "editor.codeActionsOnSave": { - "editor.codeActionsOnSave": { - "source.fixAll": "always", // 保存时自动修复 explict只有手动显示保存才会自动修复 - "source.fixAll.eslint": "always", - "source.fixAll.stylelint": "always" - } - } - } - ``` - -2. 项目根目录下创建`.prettierrc`,配置如下(按需配置): - - ```json - { - "printWidth": 120, - "tabWidth": 4, - "useTabs": false, - "semi": false, // 末尾分号 - "singleQuote": true, - "trailingComma": "none", // 对象数组末尾逗号 - "bracketSpacing": true, - "arrowParens": "avoid", // 箭头函数只有一个参数 省略括号 - "htmlWhitespaceSensitivity": "ignore", - "endOfLine": "auto" - } - ``` - - > 当然了,如果你乐意也可以在本地 vscode 的 settings.json 中做一份配置,只是需要注意如果本地项目中有`.editorconfig` 文件,settings.json 中关于 prettier 的配置会失效。 - -3. 解决冲突 - prettier 是专门做代码格式化的,eslint 是用来控制代码质量的,但是 eslint 同时也做了一些代码格式化的工作,而 vscode 中,prettier 是在 eslint --fix 之后进行格式化的,这导致把 eslint 对格式化的一些操作改变为 prettier 的格式化,从而产生了冲突。 - - 好在社区有了比较好的解决方案: - - - `eslint-config-prettier` 让 eslint 忽略与 prettier 产生的冲突 - - `eslint-plugin-prettier` 让 eslint 具有 prettier 格式化的能力 - - ```sh - npm i eslint-config-prettier eslint-plugin-prettier -D - ``` - - 接着修改 `.eslintrc` - - ```js - "extends": ["some others...", "plugin:prettier/recommended"] - ``` - - 看看 `plugin:prettier/recommended` 干了什么 - - ```js - // node_modules/eslint-plugin-prettier/eslint-plugin-prettier.js - module.exports = { - configs: { - recommended: { - extends: ['prettier'], - plugins: ['prettier'], - rules: { - // 让代码文件中不符合prettier格式化规则的都标记为错误, - // 结合vscode-eslint插件便可以看到这些错误被标记为红色, - // 当运行eslint --fix 命令时,将自动修复这些错误。 - 'prettier/prettier': 'error', - 'arrow-body-style': 'off', - 'prefer-arrow-callback': 'off' - } - } - } - // ... - } - ``` - - > `arrow-body-style` 和 `prefer-arrow-callback`: 这两个规则在 eslint 和 prettier 中存在不可解决的冲突,所以关闭掉。 - - > 另外建议某些特殊的影响到代码格式化的 eslint 配置全部关闭,比如`vue/max-attributes-per-line`,否则就会产生下面这种尴尬 (如果你看不见图,用个梯子试试~) : - > ![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/20220919000042.png) - - 大功告成!妈妈再也不用担心我的代码一团乱麻 🌶 - - ps: `Error while loading rule 'prettier/prettier': context.getPhysicalFilename is not a function`, - 遇到这种错误,一开始是有点懵逼的,去 github 上看了下,[解决方案](https://github.com/prettier/eslint-plugin-prettier/issues/434)还挺多的。常见的是降级`eslint-plugin-prettier@3.1.4`,或者升级`eslint`,当然是与时俱进了。 diff --git "a/content/posts/z-drafts/Vuex&Redux\345\216\237\347\220\206.md" "b/content/posts/z-drafts/Vuex&Redux\345\216\237\347\220\206.md" deleted file mode 100644 index 4075bf0..0000000 --- "a/content/posts/z-drafts/Vuex&Redux\345\216\237\347\220\206.md" +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: 'Vuex&Redux原理' -date: 2022-09-20T17:14:39+08:00 -tags: [Vue, React] -draft: true ---- - -## Vuex - -## Redux - -## 参考 - -- [学习 vuex 源码整体架构,打造属于自己的状态管理库](https://juejin.cn/post/6844904001192853511#heading-2) -- [学习 redux 源码整体架构,深入理解 redux 及其中间件原理](https://juejin.cn/post/6844904191228411911) -- [前端:从状态管理到有限状态机的思考](https://cloud.tencent.com/developer/article/1829761) diff --git "a/content/posts/z-drafts/mac\344\270\212bash\345\222\214zsh.md" "b/content/posts/z-drafts/mac\344\270\212bash\345\222\214zsh.md" deleted file mode 100644 index 1020495..0000000 --- "a/content/posts/z-drafts/mac\344\270\212bash\345\222\214zsh.md" +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: 'Mac上bash/zsh' -date: 2022-11-25T10:27:55+08:00 -tags: [mac, shell] -draft: true ---- - -## 基本概念 - -- shell,就是人机交互的接口 -- bash/zsh 是执行 shell 的程序,输入 shell 命令,输出结果 - -在 mac 上,我们常用的就是 bash 和 zsh 了。其它还有 sh,csh 等。 - -### 查看本机上所有 shell 程序 - -```sh -cat /etc/shells -``` - -### 查看目前使用的 shell 程序 - -```sh -echo $SHELL -``` - -### 设置默认 shell 程序 - -```sh -# change shell 缩写 chsh -chsh -s /bin/[bash/zsh...] -``` - -### bash 和 zsh 的区别 - -都是 shell 程序,但是 zsh 基本完美兼容 bash,并且安装 oh-my-zsh 有自动列出目录/自动目录名简写补全/自动大小写更正/自动命令补全/自动补全命令参数的内置功能,还可以配置插件。 - -- bash 读取 `~/.bash_profile` -- zsh 读取 `~/.zshrc` - -> 在 `.zshrc` 中添加 `source ~/.bash_profile` 就可以直接使用 `.bash_profile` 中的配置,无需再配置一遍。 - ---- - -小技巧: -对于常用的命令,一定要配置别名,git 可以,所有 shell 命令都可以 - -```sh -# ~/.zshrc -alias cra="npx create-react-app" -alias nlg="npm list -g" -``` - -### oh-my-zsh - -都知道 zsh 推荐安装 [oh-my-zsh](https://ohmyz.sh/),很强大。 - -插件也有很多[插件仓库](https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins)。这里推荐两个: - -- 语法高亮插件 `zsh-syntax-highlighting` - ```sh - # 安装 - git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting - ``` -- 语法提示增强 `zsh-autosuggestions` (对输入过的命令进行提示,->选择) - ```sh - # 安装 - git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions - # .zshrc 配置 - plugins=( - git - zsh-syntax-highlighting - zsh-autosuggestions - ) - ``` - -> 基于 zsh-autosuggestions,说一个 shell 命令 ---> history,可以查看输入过的所有命令,想要清空可以使用 shell ---> history -c diff --git a/content/posts/z-drafts/webpack/webpack.md b/content/posts/z-drafts/webpack/webpack.md deleted file mode 100644 index 1153827..0000000 --- a/content/posts/z-drafts/webpack/webpack.md +++ /dev/null @@ -1,565 +0,0 @@ ---- -title: 'Webpack核心流程' -date: 2023-01-04T17:15:44+08:00 -tags: [tool] -series: [wp] -draft: true ---- - -**本文基于 webpack5,不说废话** - -> [webpack 官网](https://webpack.js.org/) - -## 调试 - -一般调试 npm 包,使用 `npm link` 创建软链的方式进行 debug。这里单纯调试 Node, 一般也有两种方式: - -- Chrome devtools -- [Node 官网 debugger](https://nodejs.org/dist/latest-v14.x/docs/api/debugger.html#debugger_debugger) - 1. terminal 输入命令: `node inspect xxx.js` - 2. chrome 浏览器输入:`chrome://inspect` - 3. 点击 `Open dedicated DevTools for Node` 就能进行 node 的调试了 -- VsCode debugger(推荐) -- [microsoft/vscode-js-debug](https://github.com/microsoft/vscode-js-debug);[VsCode 官网 debugger](https://code.visualstudio.com/docs/nodejs/nodejs-debugging) - 1. 禁用插件 `@builtin @id:ms-vscode.js-debug` - 2. 启用插件 `@id:ms-vscode.js-debug-nightly` - 3. `cmd + shift + p`:输入 `debug`,选择合适的 debug 策略即可 - ---- - -前期准备: - -```sh -# 克隆 webpack 的 main 分支到本地,cloneb 是我配置的别名 -g cloneb main https://github.com/webpack/webpack.git -# yarn 安装依赖 -yarn -# ------ 创建调试目录并初始化 ----- -cd webpack && mkdir debug_webpack1; cd $_ && npm init -y -# 进入文件夹创建测试文件和配置文件 -touch index.js a.js b.js debugger.js webpack.config.js -``` - -其中 `index.js` 为入口文件,`a.js`,`b.js` 都是平时写的代码,主要用来进行测试打包过程的,可以随意发挥。 - -`debugger.js`: - -```js -const webpack = require('../lib/webpack.js'); -const config = require('./webpack.config,js'); -// 创建一个complier对象 -const complier = webpack(config); -// 执行compiler.run方法开始编译代码,回调方法用于反馈编译的状态 -complier.run((err, stats) => { - // 如果运行时报错输出报错 - if (err) { - console.error(err); - } else { - // stats webpack内置的编译信息对象 - console.log(stats); - } -}); -``` - -`webpack.config.js`: - -```js -const path = require('path'); - -module.exports = { - mode: 'development', - devtool: 'eval-cheap-module-source-map', - entry: './index.js', - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'bundle.js', - publicPath: '/' - } -}; -``` - -在 webpack 源码任意你想了解的地方打断点,就可以进入调试流程了,需要注意的是,最好 watch 以下三个对象:`compiler`,`compilation`,`options`,帮助定位触发钩子的回调函数。 - -这是最最最基础的配置,主要关注核心流程,后续会根据需求逐步完善,Let‘s go🔥 - -## 核心流程 - -### 初始化阶段 - -`const complier = webpack(config)`,[./lib/webpack.js](https://github.com/webpack/webpack/blob/main/lib/webpack.js#L102): - -```js -// 部分代码省略 -const webpack = (options, callback) => { - const create = () => { - let compiler; - // 当callback为函数 且 watch为true 时 compiler.watch(watchOptions, callback); - // cli 配置为 webpack --watch,作用就是检测到文件变更就会重新执行编译 - let watch = false; - let watchOptions; - /* MultiCompiler 部分省略,只关注核心主流程 */ - const webpackOptions = options; - compiler = createCompiler(webpackOptions); - watch = webpackOptions.watch; - watchOptions = webpackOptions.watchOptions || {}; - return { compiler, watch, watchOptions }; - } - const { compiler, watch } = create() - return compiler; -} - - -const createCompiler = rawOptions => { - const options = getNormalizedWebpackOptions(rawOptions); // 初始化基础配置,默认格式,防止后续报错 - applyWebpackOptionsBaseDefaults(options); // 给 options 添加 context --> process.cwd() - const compiler = new Compiler(options.context, options); // 创建 compiler - /** - * NodeEnvironmentPlugin 插件:主要是把 node 的文件系统 fs 做了增强并挂载到 compiler 实例上 - * 绑定 compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin") 钩子执行 inputFileSystem.purge() - */ - new NodeEnvironmentPlugin({ - infrastructureLogging: options.infrastructureLogging - }).apply(compiler); - /** 加载自定义配置的插件 注意这就是为什么插件都有apply方法 */ - if (Array.isArray(options.plugins)) { - for (const plugin of options.plugins) { - if (typeof plugin === "function") { - plugin.call(compiler, compiler); - } else { - plugin.apply(compiler); - } - } - } - - applyWebpackOptionsDefaults(options); // 添加各种默认配置到options上 - compiler.hooks.environment.call(); // 触发这两个钩子,绑定的回调函数执行 - compiler.hooks.afterEnvironment.call(); // watchFileSystem 插件在这个时机添加到compiler上 - - /** - * 这里做的事情非常多:./lib/WebpackOptionsApply.js - * 主要用于 挂载内置插件 和 注册对应时机的钩子 - */ - new WebpackOptionsApply().process(options, compiler); - compiler.hooks.initialize.call(); - return compiler; -}; -``` - -简单小结:初始化阶段比较简单,就是整合配置参数,创建出 `compiler` 实例,并挂载插件,注册一系列的钩子回调等。 - -### 构建(make)阶段 - -`compiler.run()`,[./lib/Compiler](https://github.com/webpack/webpack/blob/main/lib/Compiler.js) - -```js -class Compiler { - constructor(context, options = {}) { - this.hooks = Object.freeze({ - initialize: new SyncHook([]), - beforeRun: new AsyncSeriesHook(["compiler"]), - run: new AsyncSeriesHook(["compiler"]), - emit: new AsyncSeriesHook(["compilation"]), - /* other hooks ... */ - }) - this.webpack = webpack; - this.root = this; - this.options = options; - this.context = context; - this.idle = false; - this.cache = new Cache(); - /* other props ... */ - } - run(callback) { - if (this.running) return callback(new ConcurrentCompilationError()); - - // 最终的回调,内部处理一些逻辑,如果callback存在,则会执行它并透传err和stats - const finalCallback = (err, stats) => { /** ... */} - - const startTime = Date.now(); - this.running = true; - - // this.compile 的回调函数 - const onCompiled = () => { /** todo */ } - - const run = () => { - this.hooks.beforeRun.callAsync(this, err => { - if (err) return finalCallback(err); - - this.hooks.run.callAsync(this, err => { - if (err) return finalCallback(err); - - this.readRecords(err => { - if (err) return finalCallback(err); - /** 调用 compile 并且把 onCompiled 作为回调函数 */ - this.compile(onCompiled); - }); - }); - }); - }; - - if (this.idle) { - this.cache.endIdle(err => { - if (err) return finalCallback(err); - - this.idle = false; - run(); - }); - } else { - run(); - } - } - /** 省略了错误处理和部分代码简化... */ - compile(callback) { - // 工厂函数给后续创建module使用 - const params = { - normalModuleFactory: this.createNormalModuleFactory(), - contextModuleFactory: this.createContextModuleFactory() - } - this.hooks.beforeCompile.callAsync(params, err => { - this.hooks.compile.call(params); - // 创建 compilation 最终调用的是 Compilation 构造器 - const compilation = this.newCompilation(params); - /** -------进入构建(make)阶段 ------ */ - // 触发 make 钩子 并且传入 compilation - this.hooks.make.callAsync(compilation, err => { - // 触发 finishMake 钩子 - this.hooks.finishMake.callAsync(compilation, err => { - process.nextTick(() => { - // 执行 compilation 实例上的finish方法 - compilation.finish(err => { - // 执行 compilation 实例上的seal方法 - compilation.seal(err => { - // 触发afterCompile函数钩子执行绑定的回调函,传入compilation实例 回调函数 - this.hooks.afterCompile.callAsync(compilation, err => { - // 执行传入的onCompiled回调函数,并且传入compilation实例,返回执行结果 - return callback(null, compilation); - }); - }); - }); - }); - }); - }); - }); - } - /* other funtions ... */ -} -``` - -目前知道:`compiler.run()` 触发了 `this.compile(onCompiled)`,在 `compile` 内先获取了两个围绕 `module` 的工厂函数存为 params 变量,接着创建了单次构建的 `compilation` 实例:`new Compilation(this, params)`,然后触发 make 钩子,把 `compilation` 实例传递下去。 - -这部分到现在还没有看到我们的入口在哪...不得不说,webpack 把回调函数真的是玩的炉火纯青,同时注册了大量的 hooks 钩子,关于 hooks 钩子的原理稍后详细记录一下。 - -从 VsCode 的调用栈来看,之后进入到了 `EntryPlugin`,在这里注册了 make 钩子的回调,这个是在初始化阶段 `webpack(config)` 内 `new WebpackOptionsApply().process(options, compiler)` 时就已经注册好的,当 make 钩子被触发时进入: - -```js -compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => { - compilation.addEntry(context, dep, options, err => { - callback(err); - }); -}); -``` - -来看看 `addEntry`,[./lib/Compilation.js](https://github.com/webpack/webpack/blob/main/lib/Compilation.js) - -```js -class Compilation { - constructor(compiler, params) { - this.hooks = Object.freeze({ - addEntry: new SyncHook(["entry", "options"]), - seal: new SyncHook([]), - /** other hooks ... */ - }) - this.compiler = compiler; - this.params = params; - - const options = compiler.options; - this.options = options; - - this.moduleGraph = new ModuleGraph(); // 存储各个module之间的关系,对于后面生成chunkGraph也要用到 - this.chunkGraph = undefined; // 用于储存 module、chunk、chunkGroup 三者之间的关系 - this.chunkGroups = []; - - this.modules = new Set(); // module 集合, module 是由 handleModuleCreation 生成的对象,对应的是各个文件 - this.chunks = new Set(); // chunk 集合,chunk 是由一个或者多个 module 组成 - /** other props ... */ - } - // 省略... - // 这里的 entry 是通过 EntryPlugin.createDependency 转为的 dep - addEntry(context, entry, optionsOrName, callback) { - // TODO webpack 6 remove - const options = - typeof optionsOrName === "object" - ? optionsOrName - : { name: optionsOrName }; - - this._addEntryItem(context, entry, "dependencies", options, callback); - } - /** - * 进入 addEntry 后之后的流程大致如下: - * compilation.addEntry => compilation._addEntryItem => compilation.addModuleTree - * => compilation.handleModuleCreation => compilation.factorizeModule => compilation._factorizeModule - * => NormalModuleFactory.create => compilation.addModule => compilation._addModule - * => compilation.buildModule => compilation._buildModule - * => normalModule.build => normalModule.doBuild => runLoaders(normalModule中的执行) => this.parser.parse(normalModule中的执行) - */ -} -``` - -进入 addEntry 后的流水线代码就不贴了,建议调试源码自己跑一遍才印象深刻。 - -简单小结:`compiler.run() `开始编译,创建 `compilation` 实例,触发 `compiler.make` 钩子让 `compilation` 开始工作;`compilation.addEntry` 将在初始化阶段通过 `EntryPlugin.createDependency` 生成的 dep 对象转成 dependencies 属性值,然后调用 `handleModuleCreation` 创建 `module`,接着 `addModule`、`buildModule`,`buildModule` 内调用 `module.build` 方法,此方法内先调用 `_doBuild` 选用合适的 loader,通过 `runLoaders` 运行相关 loader,最后执行 `this.parser.parse` 源码进行 AST 的转换,继续执行到 `this.processModuleDependencies(module, callback)` 对 module 递归进行依赖收集,循环执行 `handleModuleCreation`。 - -至此,make 核心就差不多了,可以看见,构建阶段主要就是围绕 module 来做一系列处理的,最终得到 `compilation.modules` 等信息。 - -### 生成(seal)阶段 - -构建阶段结束,我们可以得到 `compilation.modules` 了,接下来对这些 `modules` 进行组装变成 `chunks`,然后输出资源。 - -`compilation.seal` 是先封闭模块,再生成资源,这些资源保存在 `compilation.chunks` 和 `compilation.assets`。[./lib/Compilation.js](https://github.com/webpack/webpack/blob/main/lib/Compilation.js#L2780)。 -这部分代码比较长也是相当复杂的,感兴趣的去 dubug 以下最好,此处只记录重点。以下参考[compilation.seal(callback)](https://juejin.cn/post/6948950633814687758#heading-9) - -```js - // lib/compilation.js --- seal 简化代码 - class Compilation { - seal (callback) { - // 创建 ChunkGraph 实例 - const chunkGraph = new ChunkGraph(this.moduleGraph); - // 触发 compilation.hooks.seal 钩子 - this.hooks.seal.call(); - // ... - // 遍历 this.entries 入口文件创建 chunks - for (const [name, { dependencies, includeDependencies, options }] of this.entries) {/** ... */} - // 创建 chunkGraph moduleGraph - buildChunkGraph(this, chunkGraphInit); - // 触发优化钩子 - this.hooks.optimize.call(); - - // 执行各种优化modules钩子 - while (this.hooks.optimizeModules.call(this.modules)) { - /* empty */ - } - // 执行各种优化chunks钩子 - while (this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)) { - // 触发几个比较重要的钩子 - // 触发压缩 chunks MinChunkSizePlugin 插件 - compilation.hooks.optimizeChunks.tap( { name: "MinChunkSizePlugin", stage: STAGE_ADVANCED }, chunks => {}) - // 触发根据 split 切割 chunks 插件 - compilation.hooks.optimizeChunks.tap( { name: "SplitChunksPlugin", stage: STAGE_ADVANCED }, chunks => {}) - /* empty */ - } - // ...省略代码 - - // 优化modules树状结构 - this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => { - this.hooks.optimizeChunkModules.callAsync(this.chunks, this.modules, err => { - // 各种优化钩子 - - // 生成module的hash 分为 hash、contentHash、chunkHash - this.createModuleHashes(); - // 调用codeGeneration方法用于生成编译好的代码 - this.codeGeneration(err => { - // 生成chunk的Hash - const codeGenerationJobs = this.createHash(); - - // 执行生成代码方法 - this._runCodeGenerationJobs(codeGenerationJobs, err => { - // 执行 (NormalModule)module.codeGeneration 生成源码 - // 这个其中又会涉及到 大致5模板用于生成代码 - // Template.js - // MainTemplate.js - // ModuleTemplate.js - // RuntimeTemplate - // ChunkTemplate.js - this._codeGenerationModule(module, runtime, runtimes, hash, dependencyTemplates, chunkGraph, moduleGraph, runtimeTemplate, errors, results, callback) - - // 清除资源 - this.clearAssets(); - - // 创建module资源 - this.createModuleAssets() { - compilation.hooks.moduleAsset.call(module, fileName); - }; - - // 创建chunk资源 - this.createChunkAssets(callback) { - // 开始输出资源 - this.emitAsset(file, source, assetInfo); - } - }) - }) - }) - }) - } - } -``` - -在执行完成 `compilation.emitAsset` 后会回到 `compiler` 文件中执行代码如下: - -```js -// lib/compiler.js -class Compiler { - /** 最终执行这个方法 */ - emitAssets () { - let outputPath; - // 输出打包结果文件的方法 - const emitFiles = err => { - // ... - }; - // 触发compiler.hooks.emit钩子 - // 触发CleanPlugin中绑定的函数 - // 触发LibManifestPlugin中绑定的函数 生成lib包 - this.hooks.emit.callAsync(compilation, err => { - if (err) return callback(err); - // 获取输出路径 - outputPath = compilation.getPath(this.outputPath, {}); - // 递归创建输出目录,并输出资源 - mkdirp(this.outputFileSystem, outputPath, emitFiles); - }); - } - compile(callback) { - // 省略代码 - // 执行完成seal 代码封装,就要输出封装好的文件 - compilation.seal(err => { - // 触发钩子 - this.hooks.afterCompile.callAsync(compilation, err => { - // 执行run函数中传入的onCompiled - return callback(null, compilation); - }); - }); - } - run (callback) { - // 省略代码 - // emit入口 - const onCompiled = (err, compilation) => { - process.nextTick(() => { - // 执行shouldEmit钩子上的方法,若返回false则不输出构建资源 - if (this.hooks.shouldEmit.call(compilation) === false) { - // stats包含了本次构建过程中的一些数据信息 - const stats = new Stats(compilation); - stats.startTime = startTime; - stats.endTime = Date.now(); - // 执行done钩子上的方法,并传入stats - this.hooks.done.callAsync(stats, err => { - if (err) return finalCallback(err); - return finalCallback(null, stats); - }); - return; - } - // 执行 Compiler.emitAssets 输出资源 - this.emitAssets(compilation, err => { - // 执行shouldEmit钩子上的方法,若返回false则不输出构建资源 - if (compilation.hooks.needAdditionalPass.call()) { - // compilation上添加属性 - compilation.needAdditionalPass = true; - compilation.startTime = startTime; - compilation.endTime = Date.now(); - // 实例化Stats类 - const stats = new Stats(compilation); - // 触发compiler.hooks.done钩子 - this.hooks.done.callAsync(stats, err => { - this.hooks.additionalPass.callAsync(err => { - this.compile(onCompiled); - }); - }); - } - // 输出构建记录 - this.emitRecords(err => { - const stats = new Stats(compilation); - // 执行compiler.hooks.done钩子 - this.hooks.done.callAsync(stats, err => { - if (err) return finalCallback(err); - this.cache.storeBuildDependencies( - compilation.buildDependencies, - err => { - if (err) return finalCallback(err); - return finalCallback(null, stats); - } - ); - }); - }); - }); - }); - }; - // 运行compiler.run - const run = () => { - this.compile(onCompiled); - } - } -} -``` - ---- - -## 总结 - -三个阶段: - -1. 初始化阶段 - - - `webpack(config)` 接收配置参数(来自配置文件或 shell 传参,用 yargs 解析并合并) 来创建 `compiler` 实例 - - 挂载 NodeEnvironmentPlugin 插件,fs 文件系统到 `compiler` 实例上 - - 挂载 options 中的 **自定义配置** 的插件到 `compiler` 实例上 - - 挂载 webpack 的基础内置插件,同时注册一系列的钩子回调,比如在 `EntryPlugin` 中注册了 `make` 钩子。详细见`new WebpackOptionsApply().process(options, compiler);` - -2. 构建(make)阶段 - - - `compiler.run()` 进入模块编译阶段 - - 创建 `compilation` 实例,触发 `hooks.make` 钩子,`compilation` 开始工作,调用 `compilation.addEntry` 从入口文件依赖开始构建 module - - 通过 `handleModuleCreation` 来创建的 `module` - - 有了 `module` 后调用工厂函数的 `build` 方法,期间执行 `doBuild` 调用 loader 解析模块为 js 脚本,然后调用 `parser.parse` 转为 AST 并进行依赖收集 - - 在 `HarmonyExportDependencyParserPlugin` 插件监听 `exportImportSpecifier` 钩子(识别 require/import 语句),解读 JS 文本对应的资源依赖 - - 调用 module 对象的 addDependency 方法将依赖对象加入到 module 依赖列表中 - - 继续执行到 `this.processModuleDependencies(module)` 看 module 是否还有其他的依赖,如果有,递归执行 `handleModuleCreation` - -3. 生成(emit)阶段 - 围绕 chunk - - `compilation.seal` 生成 chunk - - 构建 `ChunkGraph` 对象 - - 遍历 `compilation.modules` 集合,记录 module 与 chunk 的关系,按照 `entry/动态引入` 的规则把 module 分配给不同的 chunk 对象 - - 调用 `createModuleAssets/createChunkAssets` 分别遍历 `module/chunk` 把 `assets` 信息记录到 `compilation.assets` 对象中 - - 触发 seal 回调后,调用`compilation.emitAsset`,根据配置路径和文件名,写入文件系统 - ---- - -附上两张网上的图,便于理解: - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202301031450002.png) - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202301171730205.png) - -用于总结 module 和 chunk - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202301021511242.png) - -## 补充:易混淆知识点 - -1. module, chunk, bundle - - - module:构建阶段,通过 `handleModuleCreation` 创建的,对应的是每个文件 - - chunk:打包阶段生成的对象,遍历 `compilation.modules` 后,每个 chunk 都被分配了相应的 module - - bundle:最终输出的代码,是可以直接在浏览器中执行的 - ![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202301030941740.png) - - > 一般来讲,一个 chunk 产生一个 bundle,产生 chunk 的途径: - > - > 1. entry,注意数组的 entry 只会产生一个,以对象形式,一个入口文件链路一个 chunk - > 2. 异步加载模块(动态加载) - > 3. 代码分割 - > - > Webpack 5 之后,如果 entry 配置中包含 runtime 值,则在 entry 之外再增加一个专门容纳 runtime 的 chunk 对象,此时可以称之为 runtime chunk。 - -2. filename, chunkFilename - - output.filename:列在 entry 中,打包后输出的文件的名称,是根据 entry 配置推断出的 - - output.chunkFilename:未列在 entry 中,却又需要被打包出来的文件的名称,如果没有显示指定,默认为 chunk 的 id,往往需要配合魔法注释使用,如: - `import(/* webpackChunkName: "lodash" */ 'lodash')` -3. hash, chunkhash, contenthash - 这个可以顾名思义。首先 hash 是随机唯一的,它的作用是一般是用来结合 CDN 处理缓存的,当文件发生改变,hash 也就变化,触发 CDN 服务器去源服务器拉取数据,更新本地缓存。它们三个就是触发文件 hash 变化的条件不同:`[name].[hash].js` 计算的是整个项目的构建;chunkhash 计算的是 chunk;contenthash 计算的是内容。 - -## 补充: Tapable 见 [webpack 之 plugin](https://yokiizx.site/posts/tool/webpack%E4%B9%8Bplugin/) - -## 参考 - -- [webpack 官网](https://webpack.js.org/) -- [webpack5 知识体系图谱](https://gitmind.cn/app/docs/m1foeg1o) -- [webpack 中容易混淆的 5 个知识点](https://mp.weixin.qq.com/s/kPGEyQO63NkpcJZGMD05jQ) -- [深度剖析 VS Code JavaScript Debugger 功能及实现原理](https://juejin.cn/post/7109006440039350303#heading-4) -- [yargs](https://github.com/yargs/yargs) -- [webpack 编译流程详解](https://juejin.cn/post/6948950633814687758) -- [webpack 总结](https://xie.infoq.cn/article/ddca4caa394241447fa0aa3c0) diff --git "a/content/posts/z-drafts/webpack/webpack\344\271\213loader.md" "b/content/posts/z-drafts/webpack/webpack\344\271\213loader.md" deleted file mode 100644 index 9b86b24..0000000 --- "a/content/posts/z-drafts/webpack/webpack\344\271\213loader.md" +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: 'Webpack之loader' -date: 2023-01-05T22:12:13+08:00 -tags: [tool] -series: [wp] -draft: true ---- - -Loader 就像一个翻译员,能将源文件经过转化后输出新的结果,并且一个文件还可以链式地经过多个翻译员翻译。 - -## 基础 - -打包非 JS 和 JSON 格式的文件,需要使用 `loader` 来转换一下,在构建阶段,所有 module 都会被对应的 loader 转成可以被 `acorn` 转译的 JS 脚本。 - -也就很好理解为什么 loader 的配置是在 module 内的: - -```js -module.exports = { - module: { - rules: [ - { - test: /\.js$/, - use: ['loaderA', 'loaderB', 'loaderC'] - } - ] - } -} -``` - -一个小知识点,loader 总是从右往左调用的,但是,在实际执行之前,会先**从左到右**调用 loader 的 `pitch` 方法,如果某个 loader 在 pitch 方法中给出一个结果,那么这个过程会回过身来,并跳过剩下的 loader,详细见[Loader Interface](https://webpack.docschina.org/api/loaders/)。 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202301051444588.png) - -## 自定义 loader - -开发一个 loader 的基本形式: - -```js -module.exports = function (source ) { - // 做你想做的~ - return source; -} -``` - -既然是一个 node.js 模块,那就有很大的发挥空间了,几乎想做什么就做什么,webpack 还有一些内置接口,见[Loader Interface](https://webpack.js.org/api/loaders/) - -## 参考 - -- [手把手教你撸一个 Webpack Loader](https://juejin.cn/post/6844903555673882632#heading-8) -- [loader 配置详解](https://juejin.cn/post/6847902222873788430#heading-2) diff --git "a/content/posts/z-drafts/webpack/webpack\344\271\213plugin.md" "b/content/posts/z-drafts/webpack/webpack\344\271\213plugin.md" deleted file mode 100644 index 65395e0..0000000 --- "a/content/posts/z-drafts/webpack/webpack\344\271\213plugin.md" +++ /dev/null @@ -1,159 +0,0 @@ ---- -title: 'Webpack之plugin' -date: 2023-01-05T22:12:22+08:00 -tags: [tool] -series: [wp] -draft: true ---- - -## Tapable - -Tapable 是 webpack 实现的一个包,webpack 打包全流程都有它的影子,是 webpack 的核心库,webpack 的插件系统离不开它。 - -```js -const { - SyncHook, - SyncBailHook, - SyncWaterfallHook, - SyncLoopHook, - AsyncParallelHook, - AsyncParallelBailHook, - AsyncSeriesHook, - AsyncSeriesBailHook, - AsyncSeriesWaterfallHook -} = require('tapable') -``` - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202301301354638.webp) - -| 钩子名称 | 执行方式 | 使用要点 | -| ------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| SyncHook | 同步串行 | 不关心监听函数的返回值 | -| SyncBailHook | 同步串行 | 只要监听函数中有一个函数的返回值不为 null,则跳过剩余逻辑 | -| SyncWaterfallHook | 同步串行 | 上一个监听函数的返回值将作为参数传递给下一个监听函数 | -| SyncLoopHook | 同步串行 | 当监听函数被触发的时候,如果该监听函数返回 true 时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环 | -| AsyncParallelHook | 异步串行 | 不关心监听函数的返回值 | -| AsyncParallelBailHook | 异步串行 | 只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到 callAsync 等触发函数绑定的回调函数,然后执行这个被绑定的回调函数 | -| AsyncSeriesHook | 异步串行 | 不关心 callback()的参数 | -| AsyncSeriesBailHook | 异步串行 | callback()的参数不为 null,就会直接执行 callAsync 等触发函数绑定的回调函数 | -| AsyncSeriesWaterfallHook | 异步串行 | 上一个监听函数的中的 callback(err, data)的第二个参数,可以作为下一个监听函数的参数 | - -我们从命名就能看出来大致的区别,分为同步/异步,串行/并行/瀑布流/循环类型等。钩子的目的是为了显示的声明触发监听事件时传入的参数,以及订阅该钩子的 callback 函数所接收到的参数,举个简单例子: - -```js -const demo = new SyncHook(['hello']) // hello 字符串为参数占位符 -demo.tap('Say', (str1, str2) => { - console.log(str1, str2) // hello-world, undefined -}) -demo.call('hello-world') -``` - -注意:call 传入的参数与定义的参数需要与实力化时传给钩子的数组长度保持一致。 - ---- - -其实本质上来看,就是一个发布-订阅模式,订阅事件,当事件被触发时,执行绑定的回调函数,分别来看。 - -订阅事件有三个方法: - -- tap: (name: string | Tap, fn: (context?, ...args) => Result) => void, -- tapAsync: (name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void, -- tapPromise: (name: string | Tap, fn: (context?, ...args) => Promise) => void, - -其中只有 tap 可以用与 Sync 开头的钩子,tapAsync 相比 tap 的回调参数多了个 callback 入参,执行 callback 才能确保流程会走入到下一个插件中去。 - -触发事件对应的也有三个方法: - -- call: (...args) => Result, -- callAsync: (...args, callback: (err, result: Result) => void) => void, -- promise: (...args) => Promise, - -## 插件与 tapable - -说了一堆 Tapable 的概念,插件到底咋整呢?看官网示例: - -```js -// ./CustomPlugin.js -class HelloWorldPlugin { - apply(compiler) { - compiler.hooks.done.tap( - "Hello World Plugin", - ( - compilation /* compilation is passed as an argument when done hook is tapped. */ - ) => { - console.log("Hello World!"); - } - ); - } -} - -module.exports = HelloWorldPlugin; - -// Webpack.config.js -const HelloWorldPlugin = require("./CustomPlugin"); -module.exports = { - // ... - plugins: [ - new HelloWorldPlugin() - ] -} -``` - -在之前学习 webpack 核心流程时,`createCompiler` 方法体内有这么一段代码: - -```js -/** 加载自定义配置的插件 注意这就是为什么插件需要一个 apply 方法 */ -if (Array.isArray(options.plugins)) { - for (const plugin of options.plugins) { - if (typeof plugin === "function") { - plugin.call(compiler, compiler); - } else { - plugin.apply(compiler); - } - } -} -``` - -这就是为什么自定义插件都要带有一个 apply 方法并传入了 compiler。 - -apply 虽然是一个函数,但是从设计上就只有输入,webpack 不 care 输出,所以在插件中只能通过调用类型实体的各种方法来或者更改实体的配置信息,变更编译行为。例如: - -- compilation.addModule:添加模块,可以在原有的 module 构建规则之外,添加自定义模块 -- compilation.emitAsset:直译是“提交资产”,功能可以理解将内容写入到特定路径 -- ... - -apply 函数运行时会得到参数 compiler ,以此为起点可以调用 hook 对象注册各种钩子回调,例如:compiler.hooks.make.tapAsync ,这里面 make 是钩子名称,tapAsync 定义了钩子的调用方式,webpack 的插件架构基于这种模式构建而成,插件开发者可以使用这种模式在钩子回调中,插入特定代码。webpack 各种内置对象都带有 hooks 属性,比如 compilation 对象: - -```js -class SomePlugin { - apply(compiler) { - compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => { - compilation.hooks.optimizeChunkAssets.tapAsync('SomePlugin', ()=>{}); - }) - } -} -``` - -> Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例; -> Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。 -> —— 摘自「深入浅出 Webpack」 - -### 常用基础插件 - -- [clean-webpack-plugin](https://github.com/johnagan/clean-webpack-plugin),每次打包前先清空上一轮的打包,防止有缓存干扰。 -- [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) -- [speed-measure-webpack-plugin](https://github.com/stephencookdev/speed-measure-webpack-plugin) 量化构建速度 -- [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) 压缩 JavaScript -- [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) `require('webpack-bundle-analyzer').BundleAnalyzerPlugin` 打包分析工具 -- [mini-css-extract-plugin]() 抽离 css -- [optimize-css-assets-webpack-plugin]() 压缩 css -- [SplitChunksPlugin](https://webpack.docschina.org/plugins/split-chunks-plugin) 代码分割 -- [page-skeleton-webpack-plugin](https://github.com/ElemeFE/page-skeleton-webpack-plugin) 饿了么开源的自身生成骨架屏的 webpack 插件 -- ... 见官网吧~ - -## 参考 - -- [webpack/tapable](https://github.com/webpack/tapable) -- [Webpack 的插件机制 - Tapable](https://mp.weixin.qq.com/s/qWq46-7EJb0Byo1H3SDHCg) -- [Tapable 源码实现](https://juejin.cn/post/7040982789650382855#heading-24) -- [手把手入门 webpack 插件](https://mp.weixin.qq.com/s/sbrTQb5BCtStsu54WZlPbQ) diff --git "a/content/posts/z-drafts/webpack/webpack\344\271\213\347\220\220\347\242\216\347\237\245\350\257\206\347\202\271.md" "b/content/posts/z-drafts/webpack/webpack\344\271\213\347\220\220\347\242\216\347\237\245\350\257\206\347\202\271.md" deleted file mode 100644 index 1d57bf0..0000000 --- "a/content/posts/z-drafts/webpack/webpack\344\271\213\347\220\220\347\242\216\347\237\245\350\257\206\347\202\271.md" +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: 'Webpack之琐碎知识点' -date: 2023-01-30T10:54:44+08:00 -tags: [tool] -series: [wp] -draft: true ---- - -## HMR - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202301301743084.png) - -webpack-dev-server 启动服务后,当文件发生了变动,会触发重新构建,让我们专注于 coding,但是如果不做任何配置,它会刷新页面导致丢失掉应用状态,为此,webpack 提供了 hot module replacement 即 HMR 热更新。 - -1. 当文件发生变化后,`webpack` 会重新打包,打包完成后,发布 `done` 事件。 -2. done`回调函数执行,通过服务端与客户端建立的长连接发送`hash` 值到客户端。 -3. 客户端收到 `hash` 值之后,确认是否要更新。如果更新,则会通过 `Ajax` 去请求 `manifest.json` 文件,该文件记录了所有发生变动的模块。 -4. 通过 `manifest.json` 文件,客户端使用 `jsonp` 方式去拉取每一个变动模块的最新代码。 -5. 客户端更新模块,加入了 3 个属性:`parents、children、hot`。 -6. 通过模块 `id` 找到父模块中所有依赖该模块的回调函数并执行。 -7. 页面自动更新,热替换完成 - -> [manifest](https://webpack.docschina.org/concepts/manifest/#manifest) -> [模块热替换(hot module replacement)](https://webpack.docschina.org/concepts/hot-module-replacement/) - -## split chunk - -讲代码分割的方式之前,先回顾一下 chunk 形成的途径,一共有三种: - -- entry 配置,一个入口文件链路对应一个 chunk -- 动态引入 `import()` -- optimization.splitChunks - -我们平时说的代码分割优化指的就是第三种 `optimization.splitChunks`,一般配置长这样: - -```js -module.exports = { - optimization:{ - splitChunks:{ - cacheGroups:{ - vendors:{ - chunks:'initial',//指定分割的类型,默认3种选项 all async initial - name:'vendors',//给分割出去的代码块起一个名字叫vendors - test:/node_modules/,//如果模块ID匹配这个正则的话,就会添加一vendors代码块 - priority:-10 //优先级 - }, - commons:{ - chunks:'initial', - name:'commons', - minSize:0,//如果模块的大小大于多少的话才需要提取 - minChunks:2,//最少最几个chunk引用才需要提取 - priority:-20 - } - } - } - } -} -``` - -> 详细配置见官网[split-chunks-plugin](https://webpack.docschina.org/plugins/split-chunks-plugin) - -## tree shaking - -tree shaking 基于 ESM,因为 CJS 是动态的,无法在代码执行前确认代码是否使用到,而 ESM 则是完全静态的,可以判断到底加载了那些模块,判断哪些模块和变量未被使用或引用,进而删除对应的代码。大致有两种优化方式: - -1. 在 `package.json` 中 添加 `sideEffects: false | [modules...]` 告知 webpack 的 compiler 此模块的代码无副作用,可以安全删除未用到的 export。 -2. `optimization.usedExports` 和 TerserWebpackPlugin 结合: - ```js - module.exports = { - // ... - optimization: { - usedExports: true, // 标识出未使用的export - minimize: true // 开启代码压缩, 会删除掉未使用代码 - } - } - ``` - -> 注意:生产模式是默认开启 tree shaking 的,不用做上面的配置。 -> 关于 sideEffects 配置和 usedExports 见官网介绍[Tree shaking](https://webpack.docschina.org/guides/tree-shaking/) - -## externals - -抽离框架、库之类的依赖到 CDN,相比抽离成 dll 文件,CDN 更加优秀。 - -## source map - -通过 devtool 属性来控制的,注意 v4 和 v5 版本的字段略有差别。 - -开发环境下推荐 `eval-cheap-module-source-map` 即可; -生产环境下根据业务来做选择~ - -## 参考 - -- [HMR 机制](https://mp.weixin.qq.com/s/GlwGJ4cEe-1FgWW4EVpG_w) -- [split chunk 分包机制](https://mp.weixin.qq.com/s/YjzcmwjI-6D8gyIkZF0tVw) -- [tree shaking 原理](https://mp.weixin.qq.com/s/XVBFZ9fHBmcfNN6kgSLshw) -- [source map 原理](https://mp.weixin.qq.com/s/pOBKras7skY2efsY0tRB0A) -- [webpack 优化](https://github.com/jantimon/html-webpack-plugin) -- [webpack 优化](https://mp.weixin.qq.com/s/Rv1O4oFvj6rVpijUXtfyCA) -- [深入理解 webpack 的 require.context](https://mp.weixin.qq.com/s/wEAXLtIpE9AN7ZyCjnfBEg) -- [读懂这些题,你就是 webpack 配置工程师](https://juejin.cn/post/6844903890429673480) diff --git "a/content/posts/z-drafts/\344\270\200\347\202\271\347\273\217\351\252\214.md" "b/content/posts/z-drafts/\344\270\200\347\202\271\347\273\217\351\252\214.md" deleted file mode 100644 index 9574262..0000000 --- "a/content/posts/z-drafts/\344\270\200\347\202\271\347\273\217\351\252\214.md" +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: '积累经验' -date: 2022-09-20T16:49:09+08:00 -tags: [exp] -draft: true ---- - -### 禁用下拖拽 - -百度 UEeditor 富文本被禁用后,无法拖拽,因此想到覆盖一层 div 去实现禁用状态下的拖拽。 - -### 组件封装 - -经验,需要再 dropdown 的下拉面板中添加一个 input 输入框,利用 devtool 的事件监听来排查 dropdown 的下拉面板内聚集输入框就触发面板收回的问题。 - -### 灵活运用 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/issue_import.png) - -测试嫌手动输入和选择的与导入的不区分,以及是否是保存状态,我就使用了 git 中 untracked, unmodified, unstaged 的概念来类比区分情况。 - -### tab 接口多次调用 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/20221009143455.png) - -如上图:公司的组件库,开发时发现,这个 tab 有多少个,在初始化的时候,接口就会被调用多少次。 - -网上查了下,有的说加 `router-view` 去包裹动态组件,感觉不太靠谱。分析了一下,感觉应该因为 tab 下的组件每个都在被初始化,所有有多少个就经历了多少次,那么只要做到让切换到 tab 下时,再去初始化不就好了? - -所以给 `component` 加上了 `v-if` 的判断,试了一下,完美解决,如图。 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/20221009144306.png) diff --git "a/content/posts/z-drafts/\345\245\275\346\226\207\346\224\266\351\233\206.md" "b/content/posts/z-drafts/\345\245\275\346\226\207\346\224\266\351\233\206.md" deleted file mode 100644 index d95402a..0000000 --- "a/content/posts/z-drafts/\345\245\275\346\226\207\346\224\266\351\233\206.md" +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: '朝花夕拾' -date: 2022-08-18T01:28:06+08:00 -tags: [life] -categories: [My Reading] -weight: 1 -draft: true ---- - -世间美好的事物总是忍不住多看几眼,文章也一样 🌹 - -特开此文,主要记录一些我认为还不错的文章和博客,时常回来看看。 - -持续更新~~~ - -## blog/tutorial - -- [overreaDan Abramovcted](https://overreacted.io/) -- [Kent C. Dodds](https://kentcdodds.com/) -- [web.dev blog](https://web.dev/blog/) -- [Patterns](https://www.patterns.dev/posts/),更适合前端的各种设计模式 🔥 -- [张鑫旭](https://www.zhangxinxu.com/) -- [ChokCoco](https://www.cnblogs.com/coco1s/),css 奇淫巧技~ -- [美团技术团队](https://tech.meituan.com/) -- [前端知识库](https://www.html5iq.com/index.html) -- [TopK](https://osjobs.net/topk/),海内外大厂真实面经 -- [极简 node.js](https://www.yuque.com/sunluyong/node/what-is-node) -- [fettblog](https://fettblog.eu/guides/) -- [Libraries.io](https://libraries.io/),全网开源搜索~🐂 -- [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) - -## html - -- [HTML5 新增特性](https://www.cnblogs.com/sarah-wen/p/10767178.html) -- [HTML5 新增的 API](https://www.cnblogs.com/yangpeixian/p/11367193.html) -- [深度解读:浏览器工作原理](https://segmentfault.com/a/1190000022633988?_ea=44436475) -- [预加载 preload 及类似的技术](http://eux.baidu.com/blog/fe/link-preload-%E6%A0%87%E7%AD%BE) -- [深入研究 Chrome:Preload 与 Prefetch 原理,及其优先级](https://mp.weixin.qq.com/s?__biz=MzUxMzcxMzE5Ng==&mid=2247485614&idx=1&sn=b25bac7cfbb02bdcab76b41f10a4bffb&source=41#wechat_redirect) -- [面试官:前端跨页面通信,你知道哪些方法?](https://juejin.cn/post/6844903811232825357) -- [自身生成骨架屏的方案](https://mdnice.com/writing/bb3aaf5c613d4e0a9cc86ee2244754df) -- [How browsers work](https://web.dev/howbrowserswork/) - -## css - -- [Animation Principles for the Web](https://cssanimation.rocks/principles/) -- [前端基础篇之 CSS 世界](https://juejin.cn/post/6844903894313598989) -- [一文梳理 CSS 必会知识点](https://juejin.cn/post/6854573212337078285) -- [Sass/Scss 和 Less 的区别](https://www.cnblogs.com/wangpenghui522/p/5467560.html) -- [github 30-seconds-of-css](https://github.com/30-seconds/30-seconds-of-css) -- [你所不知道的 CSS 负值技巧与细节](https://juejin.cn/post/6844903908440014861) -- [怎么画一条 0.5px 的边](https://zhuanlan.zhihu.com/p/34908005) -- [你应该知道的简单易用的 CSS 技巧](https://mp.weixin.qq.com/s/TknFflTIdtes8-khLUN35A) -- [你不知道的 CSS 开发技巧](https://mp.weixin.qq.com/s/tl4YJDq-xfhdPaJV6O5pxw) -- [未必知道的 49 个 css 知识点](https://github.com/qdlaoyao/css-gif) -- [大厂是怎么做移动端适配的?](https://mp.weixin.qq.com/s/uxHEIupg-sYbqtmSnFiOnQ) -- [不常见缺非常有用的 css 属性](https://segmentfault.com/a/1190000022851543) -- [6 种移动端 1px 解决方案(完整版)](https://mp.weixin.qq.com/s/IrV0-v3v5Cl969yFCI58Rg) -- [实现轮播图就是这么简单!!!](https://mp.weixin.qq.com/s/ECSACv3Vmr0LMItkUd1LaA) -- [根据背景色自适应文本颜色](https://github.com/wsafight/personBlog/issues/27) -- [有趣且实用的 CSS 小技巧](https://zhuanlan.zhihu.com/p/468983073) -- [5 种回到顶部的写法从实现到增强](https://www.cnblogs.com/xiaohuochai/p/5836179.html) - -## js - -- [github 上 JS 基础进阶自查题](https://github.com/lydiahallie/javascript-questions) -- [前缀树在前端路由系统中的应用](https://mp.weixin.qq.com/s/8G8CvZAzRNnhsfF6WZoKWg) -- [为什么 Proxy 一定要配合 Reflect 使用?](https://juejin.cn/post/7080916820353351688) - - - [ES6 的 Proxy 中,为什么推荐使用 Reflect.get 而不是 target[key]?--这篇更简短](https://juejin.cn/post/7050489628062646286) - - Proxy 中接受的 Receiver 形参表示代理对象「本身」或者「继承与代理对象」的对象。 - - Reflect 中传递的 Receiver 实参表示修改执行原始操作时的 this 指向。 - -- [前端虚拟列表的实现原理](https://mp.weixin.qq.com/s/gkPOmKKD2-4TQz3TnmWbSw) -- [虚拟列表,我真的会了!!!](https://juejin.cn/post/7085941958228574215) -- [装饰器模式](https://zhuanlan.zhihu.com/p/115402372) -- [Web Component 探索之旅](https://mp.weixin.qq.com/s/mLXre4hdwcUX19Xq0qHGVw) -- [一文彻底了解 Web Worker,十万条数据都是弟弟](https://juejin.cn/post/7137728629986820126) -- [setImmediate 方法应用](https://blog.csdn.net/weixin_47450807/article/details/124098448) -- [RegExp 实例方法和字符串的模式匹配方法的总结](https://www.cnblogs.com/guorange/p/6693605.html) -- [Service Worker 运用与实践](https://mp.weixin.qq.com/s/3Ep5pJULvP7WHJvVJNDV-g) -- [不使用第三方库怎么实现【前端引导页】功能?](https://juejin.cn/post/7142633594882621454) -- [axios CancelToken 取消频繁发送请求的用法和源码解析](https://blog.csdn.net/sinat_38959166/article/details/104173187) -- [JS 中的 class](https://www.cnblogs.com/hencins/p/15408204.html) -- [前端网红框架的插件机制全梳理(axios、koa、redux、vuex](https://mp.weixin.qq.com/s/MuohDtMBrmIHOe8KrS_0ew) -- [如何实现准时的 setTimeout](https://mp.weixin.qq.com/s/ENU93_jSUaAONCkfTQTK-Q) -- [别再说你不懂 Top K 问题了](https://blog.51cto.com/u_15127654/2782684) -- [H5 唤起 APP 指南(附开源唤端库)](https://juejin.cn/post/6844903664155525127) - ---- - -## react/Vue - -- [深度解析 React 性能优化 API](https://mp.weixin.qq.com/s/svGYB3HvmLDMerlM50BhAg) -- [一文吃透 React v18 全部 Api](https://juejin.cn/post/7124486630483689485) -- [React17 事件机制](https://mp.weixin.qq.com/s/DI0oQI7Q-v5vrySRkD1ckw) -- [谈一谈 HOC、Render props、Hooks](https://mp.weixin.qq.com/s/UIAAg4qpg1YTebSEa1V_PQ) -- [自查,你的 React Hooks 够优雅吗?](https://mp.weixin.qq.com/s/y9b8Xv4zhVDdZAQmU6KwLQ) -- [When to useMemo and useCallback](https://kentcdodds.com/blog/usememo-and-usecallback#so-when-should-i-usememo-and-usecallback) -- [前端 sharing - React 栏](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MjMxMzY5Mw==&action=getalbum&album_id=1719725878131163146&scene=173&from_msgid=2247484634&from_itemidx=1&count=3&nolastread=1#wechat_redirect) -- [vue3 解构赋值失去响应式引发的思考](https://juejin.cn/post/7114596904926740493) -- [基于 react 的组件库主题设计方案](https://cloud.tencent.com/developer/article/1663404) - -## node - -- [Node.js 资源大全中文版](https://github.com/jobbole/awesome-nodejs-cn) -- [Useful Built-in Node.js APIs](https://www.sitepoint.com/useful-built-in-node-js-apis/) -- [项目中常用的 .env 文件原理是什么?如何实现?](https://juejin.cn/post/7045057475845816357) -- [在 Node.js 中如何通过子进程与其他语言(Go)进行 IPC 通信](https://mp.weixin.qq.com/s/J5mtYjKsNXkg4P0gWdS9Hg) -- [一文教你搞定所有前端鉴权与后端鉴权方案,让你不再迷惘](https://juejin.cn/post/7129298214959710244) -- [Puppeteer doc](https://pptr.dev/) -- [通过 Node.js 的源码彻底理解 EventLoop](https://mp.weixin.qq.com/s/B6Wv1lIPUoX7IHOgMF7t7g) -- [前端登录,这一篇就够了](https://juejin.cn/post/6845166891393089544) -- [傻傻分不清之 Cookie、Session、Token、JWT](https://juejin.cn/post/6844904034181070861),文章不错 -- [关于 JWT 这一点很多人都搞错了!](https://mp.weixin.qq.com/s/fKCQZSYybYrPf9E1-kaTgA) -- [关于无感刷新 Token,我是这样子做的](https://mp.weixin.qq.com/s/k-UTRUPsyq3xv5mOwuU7ZQ) -- [亚马逊-什么是 SSO](https://aws.amazon.com/cn/what-is/sso/) -- [说一下 SSO 单点登录和 OAuth2.0 的区别](https://mp.weixin.qq.com/s?__biz=Mzg3NzgyMzIyNw==&mid=2247485964&idx=1&sn=c441fe31dd94cbccc3bf241b6657007f&chksm=cf1c5f98f86bd68e0a75cae22dbaf1459b23887e2d8facb8597f1da482e7a300a27bf32eb387&scene=132#wechat_redirect) -- [你学 BFF 和 Serverless 了吗](https://juejin.cn/post/6844904185427673095) -- [Node.js 如何创建软链接,与硬链接有什么区别](https://toutiao.io/posts/freqyei/preview) - -## 工程化 - -- - [NPM 依赖管理的复杂性](https://mp.weixin.qq.com/s/UyOX30WSXh-LjvrIM9wa0A) -- [打包 JavaScript 库的现代化指南](https://github.com/frehner/modern-guide-to-packaging-js-library/blob/main/README-zh_CN.md) -- [前端亮点 or 提效?从开发一款 Node CLI 开始!](https://juejin.cn/post/7178666619135066170) -- [webpack 中容易混淆的 5 个知识点](https://mp.weixin.qq.com/s/kPGEyQO63NkpcJZGMD05jQ) -- [字节的一个小问题 npm 和 yarn 不一样吗?](https://juejin.cn/post/7060844948316225572) -- [前端埋点实现方案 ✔](https://juejin.cn/post/7094146488439144455) -- [这样用 lerna 也太爽了吧!](https://juejin.cn/post/7134646424083365924) -- [微前端究竟是什么?微前端核心技术揭秘!](https://cloud.tencent.com/developer/article/1946575) -- [微前端架构的几种技术选型](https://juejin.cn/post/7113503219904430111) -- [深入浅出 package.json](https://juejin.cn/post/7099041402771734559),结合官网[package.json](https://docs.npmjs.com/cli/v9/configuring-npm/package-json) -- [说不清 rollup 能输出哪 6 种格式 😥 差点被鄙视](https://juejin.cn/post/7051236803344334862) -- [一文入门 rollup🪀!13 组 demo 带你轻松驾驭](https://juejin.cn/post/7069555431303020580) -- [ESBuild 构建优化](https://mp.weixin.qq.com/s/7MR1raMmafEELiC9qTSaYQ) - -## other - -- [扒一扒前端埋点,聊聊 sdk 设计](https://juejin.cn/post/7104893385944596511) -- [前端性能优化之自定义性能指标及上报方法详解](https://mp.weixin.qq.com/s/DJ8Fdq1_cIoW0_NYekZwFw) -- [如何进行 web 性能监控](http://www.alloyteam.com/2020/01/14184/) -- [深入浅出前端监控](https://mp.weixin.qq.com/s/xXn8FnBuBXQQE93nHyjCXg) -- [关于移动端适配方案都在这里](https://mp.weixin.qq.com/s/hnJqHd-cWzdFbj0QH1e7UQ) -- [亲,你的防盗链钥匙,在我手上](https://juejin.cn/post/6844903596937461773),对比 host & referer -- [一文了解文件上传全过程(长文深度解析,进阶必备)](https://mp.weixin.qq.com/s/omxy6C6JXSM9fRa_xx5CPg) -- [还在看那些老掉牙的性能优化文章么?这些最新性能指标了解下](https://mp.weixin.qq.com/s/y7EqNlJ9Bm6vZKxYwJ090Q) -- [Chrome DevTools 全攻略!助力高效开发](https://cloud.tencent.com/developer/article/1692503) -- [零距离接触 websocket](https://juejin.cn/post/6876301731966713869) -- [搞懂 Nginx 一篇文章就够了](https://blog.csdn.net/yujing1314/article/details/107000737) -- [MVC、MVP、MVVM](https://www.manster.me/?p=857) - -## 源码学习 - -- [学习 vuex 源码整体架构,打造属于自己的状态管理库](https://juejin.cn/post/6844904001192853511#heading-2) -- [学习 redux 源码整体架构,深入理解 redux 及其中间件原理](https://juejin.cn/post/6844904191228411911) -- [最精简的 Redux 源码解析](https://mp.weixin.qq.com/s/uC7zkq2sqbhiLwW3RRGfWw) -- [通过 React Router V6 源码,掌握前端路由](https://mp.weixin.qq.com/s/3DxZ0UdH9CKOMzfAo_x0XQ) -- [几个 React 的开源项目](https://mp.weixin.qq.com/s/0M5nrqmhbJhRrUmN_fqRBA) diff --git "a/content/posts/z-drafts/\346\262\241\344\272\213\345\204\277\347\234\213\347\234\213\345\211\247.md" "b/content/posts/z-drafts/\346\262\241\344\272\213\345\204\277\347\234\213\347\234\213\345\211\247.md" deleted file mode 100644 index 6c0c7bf..0000000 --- "a/content/posts/z-drafts/\346\262\241\344\272\213\345\204\277\347\234\213\347\234\213\345\211\247.md" +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: 没事儿看看剧~ -date: 2022-09-18T01:28:06+08:00 -tags: [life] -draft: true ---- - -### 美剧 - -- 老友记 -- 破产姐妹 -- 生活大爆炸 -- 越狱 -- 福尔摩斯 -- 绝命毒师 -- 哥谭 - ---- - -### 电影 - -- 阿甘正传 -- 辛德勒的名单 -- 绿皮书 -- 当幸福来敲门 diff --git a/content/posts/z-study/java/base/java_1.md b/content/posts/z-study/java/base/java_1.md deleted file mode 100644 index 9496c3a..0000000 --- a/content/posts/z-study/java/base/java_1.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: 'Java_1_安装与IDEA配置' -date: 2023-06-14T17:52:02+08:00 -tags: [] -series: [java] -categories: [study notes] -weight: 1 -draft: true ---- - -如今只会前端让我感到处处受制于人,很多自己的想法难以实现,严重依赖于后端,为了改变现状,我决定今年另一个小目标就是掌握 java,以及它的相关技术栈,不折腾一下,做什么程序员?Let's go! - ---- - -## 三个基础概念: - -- JVM: Java 虚拟机,实现 JAVA 跨平台 -- JRE: Java 程序运行时环境,包含 JVM 和各种核心类库 -- JDK: 包含各种工具,包含 JRE 和开发人员使用工具 - -## 安装 JDK - -点击 [Oracle 官网](https://www.oracle.com/java/technologies/downloads/archive/) 下载,默认都会安装到 `/Library/Java/JavaVirtualMachines` 这个目录下。 - -由于我一开始不懂,下载的 JAVA20,结果现在主力开发用的都是 JAVA8(Oracle 无 ARM 版) 或 JAVA11(支持 ARM),于是乎我就都下载了,这就面临了和切换 node 一样的问题,查了下,配置变量和命令别名来切换即可,如下: - -```sh -# .zshrc 配置java环境 -# JAVA config -export JAVA_8_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_361.jdk/Contents/Home -export JAVA_8_HOME_zulu=/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home -export JAVA_11_HOME=/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home - -# default jdk -export JAVA_HOME=$JAVA_8_HOME_zulu - -# alias command for exchange jdk -alias jdk8="export JAVA_HOME=$JAVA_8_HOME" -alias jdkz="export JAVA_HOME=$JAVA_8_HOME_zulu" -alias jdk11="export JAVA_HOME=$JAVA_11_HOME" - -# add $JAVA_HOME/bin -export PATH=$HOME/bin:/usr/local/bin:$JAVA_HOME/bin:$PATH -``` - -补充:另外一个下载地址 [zulu](https://www.azul.com/downloads/?os=macos#zulu),可以下载 ARM 版本的 JAVA8~ - -试一试: - -```sh -javac xxx.java # 编译为 xxx.class 文件(JVM可识别的字节码文件) -java xxx # 执行 xxx.class 文件(本质是把.class文件装载到JVM机执行) -``` - ---- - -## 绕不开的 HelloWorld - -```java -// HelloWorld.java -public class HelloWorld { - public static void main(String[] args) { - System.out.println("Hello World"); - } -} -``` - -> 其他的都还行,作为前端开发需要注意的是: -> -> 1. 字符串用**双引号**! -> 2. 语句结束必须加上**分号** -> 3. 一个文件只能有一个 public 类,其他类不限(每一个类编译后对应一个 .class 文件) -> 4. 如果有 public 类,则源文件必须和该类同名 -> 5. main(psvm)方法可以写在非 public 的类中,指定编译后的 class 文件执行即可 - -## IDEA - -YYDS,注意:断点调试时,是运行状态,是按照对象的 `运行类型` (见多态)来执行的。 - -### 常用快捷键 - -只记录一下常用的快捷键,改了一点: - -- `⌃ + space`,代码建议 -- `⌥ + ↩︎`,操作建议 -- `⌘ + J`,代码片段 -- `⌘ + N`,创建声明代码 -- `F19 + S`,包裹代码块,if/else,try/catch/finally 等 - -- `⌃ + ↩︎`,创建新文件 - -- `^ + I`,可以实现的方法 -- `^ + O`,可以重写的方法 -- `⌃ + H`,查看类的继承层级关系 -- `⌃ + U`,查看父类 - -- `⌘ + O`,查找文件 -- `⌘ + G`,查找下一个 -- `⌃ + G`,选中相同的文本 -- `⌃ + M`,在对应括号之间切换, -- `⌘ + L`,跳转到行 -- `⌘ + D`,向下复制 - -- `⌃ + R`,运行 -- `F19 + D`, debugger (`^ + D` 与 mac 自带的 Del 冲突) -- `⌃ + D`,Del -- `^ + ;`,格式化 - -- `shift + enter` 和 `⌘ + shift + enter`,为了与 vscode 统一,把直接新开一行改为这两个键了~ - -智能补全比较 6 了,VsCode 里没怎么用过... - -- `.field`,快速定义成员变量 -- `.var`,快速定义局部变量 -- `.notnull/.null`,判空 -- `.not`,取反 -- `.if`,判断语句 -- `.lambda`,箭头函数 😂 -- `.opt`,Optional 语句 - -### Debugger 设置 - -在设置里 `build->stepping->Do not step into the classes` 中把 `java.* `和 `javax.*` 去掉 - -快捷键添加: - -- `ctrl + [`,断点前进 -- `ctrl + ]`,行前进 -- `ctrl + ↩︎`,进入方法 -- `ctrl + del`,跳出方法 - ---- diff --git a/content/posts/z-study/java/base/java_10.md b/content/posts/z-study/java/base/java_10.md deleted file mode 100644 index dee43b8..0000000 --- a/content/posts/z-study/java/base/java_10.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: 'Java_10_泛型&单元测试 JUnit' -date: 2023-10-19 -tags: [] -series: [java] -categories: [study notes] -weight: 10 -draft: true ---- - -前端学过 TypeScript,对泛型是一定的理解,来看看 java 的泛型吧。 - ---- - -## 作用 - -不使用泛型会有以下比较明显的问题: - -1. 不能对类型进修约束,不安全 -2. 比如 ArrayList,遍历的时候需要进行类型转换,如过集合的数据量过大,对效率有影响 - -```java -// 简单使用 demo -ArrayList list = new ArrayList(); -``` - -> 其实还是 TS 的那种理解,无非就是一个可变类型。声明接口、类等等时占位,然后在实例化的时候给定准确类型罢了。哈哈哈~ - -## 注意点 - -- Java 中的泛型只能是引用类型,比如 `new ArrayList`,而不能是 `int`。 -- 制定类型后,可以传入指定类型的子类型。 -- 一般简写即可,`ArrayList = new ArrayList<>()` -- 泛型如过不传,默认为 Object 类型 - -## 自定义泛型 - -注意点: - -- 使用泛型的数组不能初始化 -- 静态方法中不能使用类的泛型 - -```java -class LeiName { - T[] name; // 因为未确定类型,不知道开辟多少空间 - - // 如过加上 static 就会抛错,因为类先加载,这时候还不能确定类型 - public String getName(X m) { - // .... - } - - // 泛型方法 - public void setName(K k, V v) { - // .... - } -} -``` - -## 通配符&约束 - -```java -// ,支持任意类型 -// ,支持 A 及 A 的子类,和 TS 中的约束类似哦~ -// ,支持 B 及 B 的父类 -``` - -## 单元测试 - -个人感觉 Java 的单元测试做起来比前端的单元测试要简单的多。 - -为了便于对某个方法做测试,只需要在方法上添加注解 `@Test`,然后 `option + enter` 既可添加 JUnit 不同版本,现在一般使用 v5+,之后就可以进行单独的运行方法了。 - -```java -@Test -public void m1() { - System.out.println("单元测试"); -} -``` diff --git a/content/posts/z-study/java/base/java_11.md b/content/posts/z-study/java/base/java_11.md deleted file mode 100644 index 39f72dc..0000000 --- a/content/posts/z-study/java/base/java_11.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -title: 'Java_11_java 绘图基础&线程' -date: 2023-10-31T14:41:26+08:00 -lastmod: -tags: [] -series: [java] -categories: [study notes] -weight: 11 -draft: true ---- - -## JFrame & JPanel - -基础 demo,打开窗口,绘制一个小圆: - -```java -import javax.swing.*; -import java.awt.*; - -// 继承 JFrame,用于创建窗口 -public class Demo1 extends JFrame { - - public MyPanel mp = null; - - public static void main(String[] args) { - new Demo1(); - } - - // 构造器 - public Demo1() { - mp = new MyPanel(); // 创建画板 - this.add(mp); // 画板加入窗口 - this.setSize(400, 300); - this.setVisible(true); - // 关闭窗口的时候退出程序 - this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - } -} - - -// 继承 JPanel,用于创建画板 -class MyPanel extends JPanel { - @Override - public void paint(Graphics g) { - super.paint(g); // 调用父类方法,完成初始化 - System.out.println("执行了~~~~"); - // Graphics 画笔,对应绘制不同图形 - g.drawOval(10, 10, 100, 100); - } -} -``` - -## java 事件 - -对于前端来说挺好理解的,各种 event 很多,直接查文档。 - -```java -// 以监听键盘事件为例 -// 1. 面板 需要继承 JPanel 并实现 KeyListener:同时实现接口的方法 -class MyPanel extends JPanel implements KeyListener -// 2. 窗口 内需要把 面板加上事件监听: this.addKeyListener(mp); -this.addKeyListener(mp); -// 3. 需要重绘图形时,调用 repaint 方法 - @Override -public void keyPressed(KeyEvent e) { - System.out.println((char)e.getKeyCode()); - // this.repaint(); -} -``` - -## 线程与进程基础 - -- 进程,是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发 -- 线程,是进程的子任务,是 CPU 调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发 - -> [进程和线程的概念、区别及进程线程间通信](https://cloud.tencent.com/developer/article/1688297) - -- 并发:同一时刻,单核 CPU 让多个任务交替执行,“貌似同时” -- 并行:同一时刻,多核 CPU 让多个任务同时执行 - ---- - -#### 开辟线程 - -- 继承 `Thread` 类,重写 run 方法 -- 实现 `Runnable` 接口,Thread 也是实现了这个接口的,重写 run 方法 - -##### 为什么是 start - -如下代码: - -```java -public class ThreadDemo { - public static void main(String[] args) throws InterruptedException { - int i = 0; - - Dog dog = new Dog(); - dog.start(); - // dog.run(); - while (i++ < 10) { - System.out.println("hello world~~~"); - Thread.sleep(500); - } - } -} - -class Dog extends Thread { - int i = 0; - - @Override - public void run() { - super.run(); - while (i++ < 10) { - try { - System.out.println("wang wang wang~~~"); - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } -} -``` - -`dog.start()` 会发现 main 方法和 run 方法交替输出,如果是 `dog.run()` 则会先输出 run 的。 - -这是因为 `Thread` 内部的 `start` 方法最终调用的是 `start0` - -```java -public synchronized void start() { - start0(); -} - -// 这是jvm调用,底层是c/c++实现。 -// 它的作用也只是将线程变为可执行状态,具体什么时候执行,取决于cpu -public native void start0(); -``` - -##### 为什么有了 Thread 还需要 Runnable - -java 中有很多这种情况,主要是由于 java 的 `单继承机制`。假如一个类已经继承了某个父类,那就不能继承 Thread 了,但是,Runnable 是接口呀,接口是可以多继承的~ - -需要注意的是:Runnable 上没有 start 方法。需要通过 Thread 代理一下,这里是典型的静态代理模式。 - -```java -public class ThreadDemo { - public static void main(String[] args) throws InterruptedException { - int i = 0; - - Dog dog = new Dog(); - Thread thread = new Thread(dog); // 代理一下 - thread.start(); - while (i++ < 10) { - System.out.println("hello world~~~"); - Thread.sleep(500); - } - } -} - -class Dog extends Animal implements Runnable { - int i = 0; - - @Override - public void run() { - while (i++ < 10) { - try { - System.out.println("wang wang wang~~~"); - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } -} -``` - -> 另外,通过实现 Runnable 的方式创建线程,实现多个线程共享一个资源的情况更加优雅。 比如上例可以把 Dog 实例放到多个 new Thread(dog);中,`把Dog中需要共享的资源使用 **static** 修饰`。共享资源就会产生线程竞争的问题,就需要 `synchronized` 关键字来上锁处理。 - -##### synchronized - -为了避免多个线程抢占产生的问题,java 中使用 `synchronized` 关键字来处理。 - -```java -// 1,得到对象的锁,才能操作被锁的代码 -synchronized(对象) { - // 上锁代码 -} -// 2. 给方法声明加锁 -public synchronized void m () { - // 方法体 -} -``` - -上锁的时候就体现出继承 Thread 和实现 Runnable 的区别了,前者多次 new 是多个对象,后者始终是一个对象,只不过多个代理。 - -- 非静态方法的锁可以是 this,也可以是其他对象(必须是同一个对象) -- 静态方法的锁是当前类本身 - -```java -// m1 的锁加在了 类名.class 上 -public synchronized static void m1(){} -// m2 的内部代码块的锁在 this,可以设置其他的对象(多个线程中必须唯一) -public void m2 () { - synchronized(this) { - // ... - } -} -``` - -#### 终止线程 - -- 执行完毕,自动终止 -- 通过变量控制 run 方法退出,来终止线程 - -#### 其他方法 - -- `interrupt`,中断线程,往往在线程休眠的时候,可以帮助提前结束休眠~ -- `yield`(static),线程礼让,但是不一定礼让成功,看系统资源情况 -- `join`,线程插队,t1 线程执行中一旦调用了 t2.join(),则一定是 t2 全部执行完之后才接着执行 t1 线程 - -#### 用户线程和守护线程 - -- 用户线程:也叫工作线程,当线程执行完或被通知结束 -- 守护线程:当所有用户线程结束,自动结束,如:垃圾回收机制 - -把线程设置成守护线程的方式:在 start 之前,`t.setDaemon(true)` 即可。 - -#### 死锁 - -```java -public class LockDemo { - public static void main(String[] args) { - Cat cat1 = new Cat(true); - Cat cat2 = new Cat(false); - cat1.start(); - cat2.start(); - } -} - -class Cat extends Thread { - // 保证多线程共享,使用 static - static Object o1 = new Object(); - static Object o2 = new Object(); - boolean flag; - - public Cat(boolean flag) { - this.flag = flag; - } - - @Override - public void run() { - // 最简单的死锁~~~ - if (flag) { - synchronized (o1) { - System.out.println("o1---"); - synchronized (o2) { - System.out.println("o2---"); - } - } - } else { - synchronized (o2) { - System.out.println("o2~~~"); - synchronized (o1) { - System.out.println("o1~~~"); - } - } - } - } -} -``` - -#### 释放锁 - -- 线程中同步代码块执行完毕 -- 同步代码块、方法中遇到 break、return -- 同步代码块、方法中抛出 Error 或 Exception -- 同步代码块、方法中执行了线程对象的 wait() 方法 - -注意: - -- `Thread.sleep()`、`Thread.yield()` 不会释放锁~~~ -- 线程执行同步代码块时,其他线程调用了该线程的 `suspend()` 方法将该线程挂起,该线程不会释放锁 - > 应尽量避免 `suspend()` 和 `resume()` 来控制线程,方法不再推荐使用。 diff --git a/content/posts/z-study/java/base/java_12.md b/content/posts/z-study/java/base/java_12.md deleted file mode 100644 index ff61218..0000000 --- a/content/posts/z-study/java/base/java_12.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: 'Java_12_IO流' -date: 2023-11-13T11:50:11+08:00 -lastmod: -series: [java] -categories: [study notes] -weight: 12 -draft: true ---- - -## IO 流 - -### 文件 - -图片、视频、文档等文件在程序中是以流的形式操作的。 - -在 java 中,输入、输出是以 java 内存为视角来命名的。 - -```java -// 内存中创建 file -new File(String pathname); -new File(File parent, String child); -new File(String parent, String child); - -// 输出 file 到固态存储 -file.createNewFile(); - -file.delete(); // 删除文件 -file.mkdir(); -file.mkdirs(); // 创建多级目录(不存在的) -``` - -其他常用方法:exists, isFile, isDirectory, length, getName, getParent, getAbsolutePath 等等 - -### IO 流原理及流分类 - -以下四个抽象类 - -| | 输入流 | 输出流 | 场景 | -| ------ | ----------- | ------------ | ---------------------------------------------- | -| 字节流 | InputStream | OutputStream | 二进制文件,无损,一次读取一个字节,中文会乱码 | -| 字符流 | Reader | Writer | 处理文本文件,效率更高 | - -### 输入流 - -- FileInputStream,`fileInputStream.read()` 默认单个字节读取,效率比较低,读取完毕返回 -1;可以传入 byte[] buff = new Byte[8],来表示一次读取多少个字节,提高效率 -- BufferInputStream,带缓冲 -- ObjectInputStream,对象字节输入流,能够将数据进行序列化和反序列化,简而言之就是数据和数据类型一起保存着,相应的需要使用对应的方法。注意:读取与输入的顺序必须一致。 - > 需要让某个对象支持序列化,必须实现:`Serializable`(推荐,标记接口,内部没有方法) 或 `Externalizable` 接口之一。注意点:1.序列化类中建议添加 SerialVersionUID,为了提高版本兼容性;2.序列化对象中,static 和 transient 修饰成员不会被序列化;3. 序列化对象,要求里面属性的类型也要实现序列化接口;4. 序列化具备可继承性,一个类实现了序列化,则它的所有子类也默认实现了序列化。 - -> 注意每个流最后一定要 close()。 - ---- - -- FileReader -- BufferedReader,带缓冲,按行读取 br.readLine(),读取完毕返回 null - -### 输出流 - -- FileOutputStream,new FileOutputStream(path, boolean); ,boolean 为 true 时,file.write(something) 是追加,否则为覆盖。与 FileInputStream 配合可以实现文件拷贝的功能,一边读,一边写,需要注意的是写的过程中,注意写的大小要与 FileInputStream 读到的大小一致:fileOutputStream.write(buff, 0, readLen) -- 其他子类... - ---- - -- Writer - -### 节点流和处理流 - -- 节点流就是从数据源读写数据,比如 FileReader、FileWriter 等。 -- 处理流(包装流)是“连接”在已存在流之上,为程序提供更强大的读写功能,比如 BufferedReader、BufferedWriter 等,内部有属性包装节点流/其他处理流(使用装饰器模式实现,简单说就是大盒子把小盒子装进去,大盒子可以使用小盒子的方法,同时大盒子还能用自己的方法,看上去就是对小盒子进行了拓展)。 - -### 标准输入输出流 - -```java -// 标准输入 -- 键盘 -System.in // 编译类型 InputStream 运行类型 BufferedInputStream -new Scanner(System.in); - -// 标准输出 -- 显示器 -System.out // 编译类型 PrintStream 运行类型 PrintStream -System.out.println("dd"); -``` - -### 转换流 - -用来处理乱码 - -- InputStreamReader,可以指定 charset,将字节流包装成字符流 - ```java - // demo - BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filepath), "gbk")); - br.readLine(); - ``` -- OutputStreamWriter - -### Properties 类 - -一般用来读取配置文件,比如:xxx.properties,配置文件的格式 `key=value`,主要不需要空格。value 默认为 String 类型,不需要双引号。 - -Properties 类常用方法 - -```java -Properties p = new Properties(); -// load 加载配置文件 -p.load(Reader||InputStream); -// list k-v x显示 -p.list(System.out); - -// get -p.getProperty("user"); -// set -p.setProperty("user", "Eric"); - -// store 存储到配置文件 -p.store(Writer||OutputStream); -``` diff --git a/content/posts/z-study/java/base/java_13.md b/content/posts/z-study/java/base/java_13.md deleted file mode 100644 index ee6db0c..0000000 --- a/content/posts/z-study/java/base/java_13.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: 'Java_13_网络编程' -date: 2023-11-15T17:50:14+08:00 -lastmod: -series: [java] -categories: [study notes] -weight: 13 -draft: true ---- - -作为前端开发,对网络这块大体还是比较熟悉的,如需要深度学习网络,可以后续深度学习~ - ---- - -java.net 包下提供了一系列的类和接口,来完成网络通讯。 - -### InetAddress - -```java -/* ---------- InetAddress ---------- */ -// 获取本机主机名和 ip 地址 -InetAddress localHost = InetAddress.getLocalHost(); - -// 通过域名获取远程 ip -InetAddress baidu = InetAddress.getByName("www.baidu.com"); -System.out.println(baidu); -``` - -### socket - -通信的两端都要有 socket(套接字)。 - -TCP 编程简单 demo: - -```java -// 客户端 -public class SocketClient { - public static void main(String[] args) throws IOException { - // 请求连接 proxy, port - Socket socket = new Socket(InetAddress.getLocalHost(), 9999); - - OutputStream outputStream = socket.getOutputStream(); - outputStream.write("hello server ~".getBytes()); - socket.shutdownOutput(); // 设置本轮发送结束标记 - // 当使用 字符流 时,也可以使用 writer.readLine() 设置结束标记,对应的读取需要使用 readLine() - - // 字符流接收服务端的消息 - InputStream inputStream = socket.getInputStream(); - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); - System.out.println(bufferedReader.readLine()); - - bufferedReader.close(); - outputStream.close(); - socket.close(); - } -} - -// 服务端 -public class SocketServer { - public static void main(String[] args) throws IOException { - // ServerSocket 通过 accept 可以返回多个 socket【多个客户端连接服务器的并发】 - ServerSocket serverSocket = new ServerSocket(9999); - System.out.println("正在监听 9999 端口服务。。。"); - Socket socket = serverSocket.accept(); - InputStream inputStream = socket.getInputStream(); - byte[] buff = new byte[1024]; - int readLen = 0; - while ((readLen = inputStream.read(buff)) != -1) { - System.out.println(new String(buff, 0, readLen)); - } - - // 字符流发送给客户端消息 - OutputStream outputStream = socket.getOutputStream(); - BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream)); - bufferedWriter.write("I got you, client~"); - bufferedWriter.newLine(); - bufferedWriter.flush(); // 必须刷新一下,否则进入不到通道 - - bufferedWriter.close(); // 关闭外层流 - inputStream.close(); - socket.close(); - } -} -``` - -> 注意:使用 字符流,需要使用转换流 `OutputStreamWriter(outputStream)` 和 `InputOutputStreamReader(inputStream)`,写入后需要 `flush()` 一下,最后必须关闭外层流。 - -### netstat - -这个指令可以查看当前主机网络情况包括端口情况和网络情况。 - -```shell -netstat -an -netstat -an | more # 分页 -``` - -#### UDP - -众所周知,tcp 需要连接,UDP 不需要,UDP 的每个包中都有完整的信息。 - -在 java 中核心的两个类:`DatagramSocket`和`DatagramPacket`,DS 对象有 send 和 receive 两个方法,发送端发送时将数据装包成 DP 对象,接受端接收到 DP 对象后,进行拆包。 DS 可以指定在哪个端口接收数据。一个数据包最大 64k。 diff --git a/content/posts/z-study/java/base/java_14.md b/content/posts/z-study/java/base/java_14.md deleted file mode 100644 index a1e3d39..0000000 --- a/content/posts/z-study/java/base/java_14.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: 'Java_14_反射' -date: 2023-11-24T15:08:33+08:00 -lastmod: -series: [java] -categories: [study notes] -weight: 14 -draft: true ---- - -## 场景 - -反射机制是 java 的灵魂。 - -比如,需要通过外部文件配置,在不修改源码的情况下,来控制程序。这也符合设计模式的 ocp 原则(开闭原则)。 - -简单 demo:`根据配置文件,控制程序` - -```java -// 常规实例化一个对象,并调用它的方法 -public class Demo { - public static void main(String[] args) { - Car car = new Car(); - car.sayHello(); - } -} -/* ---------- 根据配置文件控制程序 ---------- */ -// Car.properties -classPath=com.yk.Car -method=sayHello - -// 1. 读取配置文件 -public class Demo { - // Exception 省略。。。 - public static void main(String[] args) { - // 1. 读取配置 - String configPath = "/Users/yokiizx/Desktop/Java_Demo/untitled/src/com/yk/Car.properties"; - Properties properties = new Properties(); - properties.load(new FileReader(configPath)); - String classPath = properties.getProperty("classPath"); // 得到配置的类路径 - String methodName = properties.getProperty("method"); // 得到具体方法名 - - // 2. 反射魔法 - Class aClass = Class.forName(classPath); // 真正获取到 “类对象” - - /* ---------- Object o = aClass.newInstance(); 无参构造器可以直接实例化 ---------- */ - Constructor constructor1 = aClass.getConstructor(); // 获取无参构造器 - Constructor constructor2 = aClass.getConstructor(String.class); // 获取有参构造器 - Object o = constructor1.newInstance(); // 创建对象实例 - - // 反射获取非私有属性 - Field name = aClass.getField("name"); - System.out.println(name.get(o)); // god is a girl - - // 反射执行方法 - Method method = aClass.getMethod(methodName); - method.invoke(o); // hello world - } -} -``` - -## 反射机制 - -Java 反射机制可以完成: - -- 在运行时判断任意一个对象所属的类 -- 在运行时构造任意一个类的对象 -- 在运行时得到任意一个类所具有的成员变量和方法 -- 在运行时调用任意一个对象的成员变量和方法 -- 生成动态代理 - -反射的优缺点: - -- 优点:可以动态创建和使用对象(框架底层的核心),使用灵活。 -- 缺点:使用反射基本是解释执行,对执行速度有影响。可以通过 ·禁用访问安全检查· `setAccessible(true)`来优化,Method、Field 和 Constructor 对象都有这个方法。 - > setAccessible(true),也称为爆破,可以使得访问私有的成员~ - -> Class, Method, Field, Constructor 相关 api 建议查文档,用多了就熟悉了,不赘述 - ---- - -### Java 程序在计算机的三个阶段 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202311291125177.png) - -由此可见,类对象是反射的核心,关于类对象需要知道以下内容: - -1. Class 也是一个类,继承自 Object -2. Class 类对象不是 new 出来的,是系统创建的 -3. Class 类对象在内存只有一份,因为类只会加载一次 -4. 每个类的实例都会记得自己是由哪个 Class 类对象所生成 -5. Class 类对象是存放在堆的 -6. 类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括方法代码,变量名,方法名,访问权限等) - -### 获取 Class 类对象的方式 - -- `Class.forName("类的全路径")`,场景:多用于通过配置文件加载类 -- `类.class`,已知具体类,该方式最为安全可靠,程序性能最高,场景:多用于参数传递,比如上例通过反射获取构造器时,传递参数 `String.class` -- `对象.getClass()`,场景:通过实例对象获取类对象 -- 源码中的方式:`ClassLoader cl = 对象.getClass().getClassLoader(); cl.loadClass("类的全类名")` -- 基本数据获取 Class 类对象:`基本数据类型.class` -- 基本数据类型的包装类获取 Class 类对象:`包装类.TYPE` - -#### 那些类型有 Class 对象 - -1. 类 -2. 接口 -3. 数组 -4. 枚举 -5. 注解 -6. 基本数据类型 -7. void - -### 类加载时机 - -#### 静态记载&动态加载 - -- 静态加载,编译的时候就加载相关类了,如果没有对应的类,则会报错 -- 动态加载,代码执行到才会加载相关类,如果没有执行到,即使没有对应的类,也不会报错 - -动态加载优点像前端的懒加载的概念,用到了才会去引入。 - -```java -// 静态加载 -public static void test() { - if (true) { - System.out.println(true); - } else { - Car car = new Cat(); // 即使肯定执行不到这里,但是编译的时候会加载Cat类,由于没有此类,所以报错 - System.out.println(car); - } -} -// 反射 动态加载 -public static void test() throws ClassNotFoundException, InstantiationException, IllegalAccessException { - if (true) { - System.out.println(true); - } else { - // 使用反射 - 则可以顺利 javac 和执行,因为代码始终走不到这里,就不会去加载Cat类 - Class aClass = Class.forName("Cat"); - Object o = aClass.newInstance(); - System.out.println(o); - } -} -``` - -四种会加载类的情况: - -- new 创建对象 -- 子类被加载时,父类也被加载 -- 调用类中的静态成员 -- 反射。(上面 3 个都是静态加载,此处为动态加载) - -#### 类加载的三个阶段 - -前面说的 Class 类加载阶段又可以详细分为三个阶段: - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202311301513296.png) - -补充: - -1. 连接 - 验证阶段:包括文件格式验证(是否以魔数 oxcafebabe 开头)、元数据验证、字节码验证和符号引用验证。可以考虑使用 `-Xverifyinone` 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。 -2. 连接 - 准备阶段:需要注意这里**默认初始化**的是 `静态变量`,且初始化的是**默认值**,真正的初始化赋值是在初始化阶段的。 - ```java - public int a = 10; // 不是静态变量,准备阶段不会分配内存和初始化 - public static b = 20; // 准备阶段初始化为 0 - public static final c = 30; // 因为是final,所以准备阶段直接初始化为 30 - ``` -3. 连接 - 解析阶段:直接引用就是具体引用地址的指针,符号引用就是字符串符号的形式来表示引用 diff --git a/content/posts/z-study/java/base/java_2.md b/content/posts/z-study/java/base/java_2.md deleted file mode 100644 index 7b31a62..0000000 --- a/content/posts/z-study/java/base/java_2.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -title: 'Java_2_类型&修饰符&运算符' -date: 2023-06-15T20:12:28+08:00 -tags: [] -series: [java] -categories: [study notes] -weight: 2 -draft: true ---- - -## JAVA 基本数据类型 - -java 中有八大基础数据类型,和 js 的略有不同,Java 是强类型语言,每种类型在声明的时候就需要开辟出空间(需要注意每种类型的占空间大小): - -- byte:1 个字节,最小整数类型 -- boolean:1 个字节,只能是 true 或 false,不能为 0、1 啥的 -- char:2 个字节,单个字符,注意使用`单引号` - 也可以是数字 -- 输出对应的 `ASCII` 码,本质上是数字,可以参加运算 -- short:2 个字节 -- int:4 个字节,(默认) -- long:8 个字节,_可以数字后加 'l' 或 'L'_ -- float:4 个字节,单精度,[-3.403E38, 3,403E38],_可以在数字后加 'f' 或 'F'_ -- double:8 个字节,双精度,[-1.798E308, 1.798E308],(默认) - 浮点数 = 符号位 + 指数位 + 尾数位; 尾数部分可能丢失,造成精度损失。当计算出的浮点数和声明的浮点数比较时,应当是在一定范围内比较。 - -> 注:`1 byte = 8 bit`,也就是对应有 8 位数字的二进制数,所以 1 个字节可以存储的数据范围是 `[-128, 127]`,即 `[1000 0000, 0111 1111]`。PS:计算机世界负数按照补码来取的,`1000 0000(原码)` -> `1111 1111(反码)` ---> +1 `1000 0000(补码)` -> `-0`,`-0` 无意义,被利用起来规定为 `-128`。 - -### 基本类型转换 - -#### 自动类型转换 - -java 程序进行赋值或者运算时,精度小的会自动转换为精度大的。 - -两条线路: - -- `char -> int -> long -> float -> double` -- `byte -> short -> int -> long -> float -> double` - -```java -int a = 'a' // char -> int -double b = 97 // int -> double -``` - -> 注意:char, byte, short 之间不能自动转换,但是可以参与计算,计算时会首**先转换到 int 类型** - -另,long 和 float 类型之后需要加 l 或 f 往往是因为在计算过程中,数字会被转换成容量最大的数据类型再进行计算。 - -#### 强制类型转换 - -数据的类型从大转小,就需要强制转换,一般不推荐,会产生精度损失或数据溢出的问题,除非很确定需要的是什么。 - -```java -int a = (int)8.88 -``` - -在复合赋值运算符中,就会进行强制类型转换,如: - -```java -byte a = 1 -a += 1 // 实际上 a = (byte)(a + 1) -a++ // 也会强制转换 -``` - -## JAVA 引用类型 - -- 类 class:Java 面向对象的基石,可以看成是自定义的数据类型 -- 接口 interface: TODO -- 数组 []:与 js 数组不同,Java 数组存放的是`同一类型的数据` - -> 与 JS 一致,基本类型也是值拷贝,引用类型是引用传递,拷贝的是物理地址。 - -### 类 - -#### 成员变量(属性) - -> [Java 基础——成员变量、局部变量和静态变量的区别](https://blog.csdn.net/haovip123/article/details/43883109) - -> 关注一下通过类创建对象时,JVM 的内存状态: -> ![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202306262325036.png) - -可见,JVM 内存管理主要分为 3 个部分: - -- 栈:基础类型 -- 堆:引用类型 -- 方法区:加载类信息和常量池,详细了解--[你知道 JVM 的方法区是干什么用的吗?](https://zhuanlan.zhihu.com/p/166190558) - -这篇文章也很不错:[String 在内存中如何存储(Java)](https://blog.csdn.net/iceyung/article/details/106202654) - -#### 成员方法(方法) - -其实和 JS 没啥太大的区别,函数(方法)在执行的时候都会开辟栈空间,形参会在栈空间内创建变量与入参的联系,需要注意的是内存中的关系,区分基础类型和引用类型,老生常谈了,注意下一点就理解的差不多了: - -```js -// 为了方便用JS演示 -const obj = { - age: 200 -} - -function test(obj) { - // obj.age = 100 // is ok - obj = null // what happen? -} - -test(obj) - -console.log('📌📌📌 ---', obj.age) // 会报错吗?---不会! -// obj = null 只是打断了栈空间的obj与入参即堆中obj的联系 -``` - ---- - -##### 重载(overload) - -没啥好说的,JS 中形参 `1. 不在乎类型,2. 不在乎数量`,很随意,JAVA 恰恰都把控很严。 - -因此为了防止重复起名的麻烦,JAVA 引入了重载的概念,允许同一个类中,有多个同名方法,但是参数列表不一致。 - ---- - -###### 可变参数 - -重载中也难免会有冗余的情况,比如就只是计算 n 个数的和,那么入参可以是 n 个,每个都重载一个方法会很 low~,那么就可以使用`可变参数`了 - -```java -public int sum(int...nums) { - // ... 与js的剩余参数类似,也必须在最后~ -} -``` - -#### 构造器 - -JAVA 中的构造器不是用来创造对象,是用来`完成对象的初始化`。有以下特点: - -1. `方法名与类名一致` -2. 无返回值(不需要返回类型) -3. 构造器也可以重载 - -#### this - -JAVA 的 this 和 JS 的 this 还是略有不同的~,其实我感觉比 JS 还好理解一点。 - -> this 就是指当前类型的实例对象。可以想象成实例对象内部有个隐藏的属性 this 指向自身的内存地址。可以通过 hashCode(物理地址的) 来简单证明 - -一个注意点:构造器中调用另一个构造器的语法 `this(...args)`,且必须在第一行。 - -```java -public Cons (){ - this(18); -} -public Cons(int age) { - this.age = age; -} -``` - -### 接口 - -TODO - -### JAVA 的数组 - -#### 两种创建方式: - -```java -// 动态创建 -数据类型[] name = new 数据类型[容量] // 或者 数据类型 name[] = new 数据类型[容量] -// 静态创建 -int[] name = {...} -``` - -#### 扩容注意 - -与 JS 不同,java 先申请空间,所以不能直接在超出空间大小的范围外直接修改数组 - -```java -int[] a = new int[3]; -a[3] = 1; // wrong 越界了 -``` - -因此,要想扩容就得先开辟空间,复制原数组,然后填入新数据。在 java 中,这是比较低效的,后续会学习 链表 --- TODO. - -## 引用类型属性默认值 - -分配空间的数组根据类型不同有不同的默认值,比如 int 默认为 0,float 默认为 0.0,需要注意以下三种: - -- boolean 默认为 `false`, -- char 默认为 `\u0000`, -- string 默认为 `null` - -> 注意:局部变量必须设置初始值,且局部变量不能加修饰符 - ---- - -## 算数运算符注意点 - -Java 部分算数运算符和 JS 中的略有不同。 - -1. `/`,根据上面,按照最大精度类型来参与运算,如: - ```java - System.out.println(10 / 4); // 2 - System.out.println(10.0 / 4); // 2.5 - ``` -2. `%`,取余当被取余数为负数时,注意按照公式 `a % b = a - a / b * b` 来算 - ```java - System.out.println(10 % -3); // 1 <== 10-10/-3*-3 - ``` - ---- - -## 包 - -作用:区分相同名字的类和接口。 -本质:就是创建不同的文件夹来保存类文件。 - -```java -// 在包内的java文件顶部需要声明,当然了用ide的话,都会自动做好 -package 包名 -``` - -包名不能用关键字或保留字,一般小写字母就行了,形如:`com.公司名.项目名.业务模块名`。 - -常用内置包: - -- java.lang.\*,基本包,默认引入 -- java.util.\*,系统提供的工具包 -- java.net.\*,网络包 -- java.awt.\*,界面开发,GUI - -## 访问修饰符 - -- `public`:公开 -- `protected`:对`子类`和`同一个包中的类`公开 -- `无修饰符`:对`同一个包中的类`公开,`不在同一个包内,即使继承,也不能访问` -- `private`:只能类自身访问 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202307042238681.png) - ---- diff --git a/content/posts/z-study/java/base/java_3.md b/content/posts/z-study/java/base/java_3.md deleted file mode 100644 index 2c5ef1c..0000000 --- a/content/posts/z-study/java/base/java_3.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: 'Java_3_三大特性' -date: 2023-06-30T10:18:33+08:00 -tags: [] -series: [java] -categories: [study notes] -weight: 3 -draft: true ---- - -面向对象编程三大特性:`封装、继承和多态`。 - -## 封装 - -封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。 - -### 基础形式 - -- 属性私有化 `private` -- 提供公开的 getter/setter 方法 - -## 继承 - -与 JS 一样,使用 `extends` 来实现继承。 - -### 注意细节 - -有以下注意点: - -- 子类总是继承了所有的属性和方法,只是私有属性和方法不能在子类中访问,可以通过调用父类的公共方法间接访问 -- 父类的构造方法不能被继承(因为构造方法比类名相同),但子类初始化时,总是默认会先调用父类的无参构造器,在子类构造器中默认会有一个 `super()` 的动作。这种机制一直延伸到顶级类 `Object` 类 -- 如果父类没有提供无参构造器,则子类必须要有构造器,通过调用 `super(xxx)` 指明是哪个构造器 -- `super()` 和 `this()` 都必须在构造器第一行,所以不能共存,只能二选一 -- 多级继承,`super` 关键字遵循 `就近原则` -- java 继承是`单继承机制`,子类只能继承一个类 - -### 继承本质 - -这里借用韩顺平老师的图: - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202307042055445.png) - -> 小结:在 new 一个对象的时候,JVM 先在方法区加载类(在这里会加载类的所有祖先类),然后再在堆中分配内存,最后在主栈中创建变量引用。 - -### 重写 - -子类中的某个方法和祖先类的某个方法完全一致(返回类型,名称,参数列表),这就叫做方法的重写。 - -注:1. 父类中方法的返回类型 可以为 子类中重写方法的返回类型的父类; 2. 子类重写方法不得缩小父类方法的访问权限(即修饰符权限不得缩小) - -## 多态 - -方法或对象具有多种形态,是建立在封装和继承之上的。主要目的:`提高代码复用性`,比如方法,可以把入参的参数类型提升到父类,这样就可以避免写一些不必要的重载。 - -### 方法的多态 - -- 重载,体现在实例对象调用同名但返回类型或参数不同的方法 -- 重写,体现在父子不同对象调用同名方法 - -### 对象的多态 - -重点: - -- 一个对象的编译类型和运行类型可以不一致 -- 编译类型在定义对象时,就确定了,不能改变 -- 运行类型是可以变化的 -- 编译类型看定义时 `=` 的左边,运行类型看 `=` 的右边 - -```java -// 基本演示 -Animal animal = new Dog() -animal = new Cat() // animal 仍然是 Animal 类型,但是运行类型从 Dog 变成了 Cat -``` - -> `A instanceof B`,这里判断的是 A 的运行类型是否是 B 类型或 B 类型的子类型 - -转型细节: - -- 向上转型,`父类 xxx = new 子类()`: - - 1. xxx 可以访问父类的所有成员(遵守访问权限),但访问不了子类特有的成员。因为在编译阶段,能调用哪些成员是由编译类型决定的(javac) - 2. 然而,最终的执行结果还是看子类的具体实现(java) - - ```java - // 简而言之: 属性访问看编译类型, 方法访问看运行类型 - class A { - int num = 10; - public void log() { - System.out.println(this.num); - } - } - class B extends A { - int num = 20; - public void log() { - System.out.println(this.num); - } - public void unique() { - // ... - } - } - - // 测试 - B b = new B(); - System.out.println(b.num); // 20 - b.log(); // 20 - - A a = b; - System.out.println(a.num); // 10 看编译类型 - a.log(); // 20 看运行类型 - ``` - -- 向下转型,`子类 xxx = (子类) 父类引用`,可以看成是引用类型的强转: - - 1. 解决了向上转型不能访问子类型特有成员的问题 - 2. 注意`父类引用`创建时的运行类型必须和这里的编译类型一致 - - ```java - ((B) a).unique(); - ``` - -### 动态绑定机制(重要) - -- 对象调用方法时,方法会和该对象的运行类型绑定 -- 但属性没有动态绑定机制,哪里声明哪里用 - -例子: - -```java -class A { - public int i = 10; - - public int sum() { - return getI() + 10; - } - - public int sum1() { - return i + 10; - } - - public int getI() { - return i; - } -} - -class B extends A { - public int i = 20; - -// public int sum() { -// return i + 20; -// } -// public int sum1() { -// return i + 20; -// } - - public int getI() { - return i; - } -} - -// 向上转型,假如子类中的方法没有注销,那么直接调用运行类型的方法,下述语句应该输出都是40 -A a = new B(); -// 注释后,sum中的getI方法跟随运行类型 -System.out.println(a.sum()); // 30 -// sum1中的i不会跟随运行类型,在哪里,用哪里 -System.out.println(a.sum1()); // 20 -``` - ---- diff --git a/content/posts/z-study/java/base/java_4.md b/content/posts/z-study/java/base/java_4.md deleted file mode 100644 index 8365b4e..0000000 --- a/content/posts/z-study/java/base/java_4.md +++ /dev/null @@ -1,285 +0,0 @@ ---- -title: 'Java_4_类&代码块' -date: 2023-07-08T21:11:35+08:00 -tags: [] -series: [java] -categories: [study notes] -weight: 4 -draft: true ---- - -## Object 类 - -### `==` 和 `equals` - -- `==`,判断基本类型,判断的是值;判断引用类型,判断的是引用地址 -- `equals`,只判断引用类型,(JAVA 中字符串是引用类型) -- 子类中往往会重写该方法,用于**比较内容**是否相等,比如 Integer、String 类就不太相同,直接看源码: - - ```java - // Object类中 -- 很简单 - public boolean equals(Object obj) { - return (this == obj); - } - - // String - public boolean equals(Object anObject) { - if (this == anObject) { // 和自己比直接返回真 - return true; - } - if (anObject instanceof String) { - String anotherString = (String)anObject; // 向下转型,需要得到作为String内的各个属性 - int n = value.length; - if (n == anotherString.value.length) { - char v1[] = value; - char v2[] = anotherString.value; - int i = 0; - while (n-- != 0) { - if (v1[i] != v2[i]) - return false; - i++; - } - return true; - } - } - return false; - } - - // Integer - public boolean equals(Object obj) { - if (obj instanceof Integer) { - return value == ((Integer)obj).intValue(); - } - return false; - } - ``` - -### hashCode() - -返回对象的哈希码值。往往也需要被重写。 - -作用:提高具有哈希表结构数据类型的性能 - -小结: - -- 对于对象,两个引用一致的对象返回的 `hashCode` 一定是一样的 -- 哈希值与内存地址对应,并不是真正的内存地址 -- [ ] 学习到 hashMap hashSet 时再深入 - -### toString() - -```java -public String toString() { - // getClass().getName() 返回全类名(包名+类名) - return getClass().getName() + "@" + Integer.toHexString(hashCode()); -} -``` - -> 直接输出对象,默认就会调用 toString() 方法 - -### finalize() - -当对象被回收时,系统自动调用对象的 `finalize` 方法。一般是需要重写的。 - -> 触发回收: 1. `引用 = null`,等待回收; 2. `System.gc()`,主动回收 - -## 类变量/类方法(静态变量/静态方法) - -### 静态变量 - -当某个变量需要被`该类的所有实例对象共享`的时候,就需要使用到类变量了。推荐通过类名直接访问。 - -两个点: - -1. static 静态变量在类加载时,就生成了 -2. statci 静态变量被该类所有对象实例共享 - -```java -class People { - public static int count; - - private String name; - public People(String name) { - this.name = name; - } -} -``` - -> 类变量的内存在 JDK8 以前是在方法区内,JDK8 及以后是在堆里面的类对应的 class 对象。(这个对象是通过反射机制,在类加载的时候在堆内生成的) - -推荐阅读: - -- [Java 类的静态变量存放在哪块内存中](https://blog.51cto.com/u_15061941/2591637) -- [Java static 变量保存在哪](https://blog.csdn.net/x_iya/article/details/81260154/) - -### 静态方法 - -当方法中不涉及到任何和对象相关的成员,就可以使用静态方法。 - -简单说就是`不需要实例化对象就访问`,比如 `Math.xxx()` 之类的`工具类`就是如此。 - -```java -class MyTools { - public static void myFn() { - // xxx - } -} -``` - -注意: - -1. 静态方法中无法使用 `this/super` -2. 静态方法只能访问静态属性/方法;普通方法则无限制 - -### main 方法 - -`public static void main(String[] args)` - -1. main 方法是由 jvm 虚拟机调用的,所以方法必须是 public 的 -2. jvm 虚拟机在调用的时候不创建对象,所以必须是 static 的 - - 因此,main 方法中不能直接访问非静态变量或方法,必须在方法内实例化一个该类对象,通过这个对象再去访问 -3. `String[] args` 是执行 java 命令时,传递给所运行的类的参数 - ```sh - java xxx arg1 arg2 arg3 # arg1 arg2 arg3 就处理为 String[] args 传给类 - ``` - -## 代码块(初始化块) - -没有方法名,没有返回,没有参数,只有方法体,在加载类时或创建对象时隐式调用: - -```java -[static] { - // ... -}; -``` - -场景:多个构造器中有重复的语句,就可以抽取到初始化块中,提高代码的复用性。 - -### 细节 - -1. 类加载时机 - - new - - 创建子类实例对象,父类也会被加载 - - 访问类的静态成员 -2. 代码块执行时机 - - `static` 代码块在类加载时就执行,所以只会执行一次 - - 普通代码块在创建对象时执行,创建一次执行一次;如果只是访问类的静态成员,则不会执行 -3. 对象创建时,类中的代码块和属性调用顺序(一个类中) - - 调用静态代码块和静态属性初始化 - - 调用普通代码块和普通属性初始化 - - 调用构造器。若是有继承关系,创建一个子类对象顺序如下 - 1. 父类静态代码块和静态属性初始化 - 2. 子类静态代码块和静态属性初始化 - 3. 父类普通代码块和普通属性初始化 - 4. 父类构造方法 - 5. 子类普通代码块和普通属性初始化 - 6. 子类构造方法 -4. 静态代码块只能调用静态成员 - -## 单例模式 - -这是学习 Java 过程中的遇到的第一个设计模式。 - -记得在 JS 中就是判断是否已经存在实例对象,如果存在就返回来实现简单的单例模式的。Java 相比而言更加的专业~ - -> 简而言之:保证类的实例对象只有一个,该类提供一个访问对象实例的方法 - -### 饿汉式 - -1. 将构造器私有化 -2. 在类内部创建静态对象实例 -3. 暴露静态方法,该方法访问在类内部创建的对象实例 - -```java -class Wife { - public String name; - private Wife(String name) { // 私有化构造器,方式通过new访问 - this.name = name; - } - - private static Wife wife = new Wife("hyl"); // 饿汉,类加载时就着急的创建出实例了 - public static Wife getInstance() { // 因为不能通过new访问,所以得使用静态方法 - return wife; - } -} -``` - -`java.lang.Runtime` 就是经典的饿汉单例。 - -### 懒汉式 - -饿汉式在累加载时创建出来的对象可能压根没有用到,造成了资源浪费,所以就需要懒汉式的单例模式,在使用才创建对象实例。 - -```java -class Son { - public String name; - private Son(String name) { - this.name = name; - } - - private static Son son; - public static Son getInstance() { - if (son == null) { - son = new Son("my-son"); - } - return son; - } -} -``` - -看着花里胡哨,其实这就和 js 的防抖节流是否先执行一次有异曲同工之妙~so easy👻 - -> But! 懒汉式存在线程安全,比如同时三个线程进入就会创建三个对象实例了,破坏了单例模式。怎么解决?TODO - -## final - -1. 修饰类,禁止类被继承 -2. 修饰方法,禁止方法被重写,但是可以继承 -3. 修饰成员变量和局部变量,则变量不能被修改 - -细节: - -1. 与 JS 中的 const 类似,声明时就得赋初始化值,变量名形式一般为:`xx_xx_xx` -2. 赋初始化值的位置可以是: - - 直接定义 - - 代码块 - - 构造器 -3. 特别,如果修饰的是静态属性,则不能在构造器中赋值 -4. 一般,一个类已经是 final 类了,则没必要把方法再修饰成 final -5. final 和 static 往往搭配使用,效率更高,底层编译器做了优化处理,比如访问同时 final static 修饰的属性,不会导致类的加载 -6. 包装类(Integer、Double、Boolean)都是 final 类,String 也是 final 类,不能被继承~ - -## 抽象类 - -父类中需要声明但是又不确定如何实现,往往需要子类去重写的方法,可以声明为抽象方法,这个类声明为抽象类。 - -关键字: `abstract`,只能修饰类和方法。 - -```java -abstract class Animal { - public abstract void eat(); // 不能有 {} -} -``` - -注意: - -- 抽象类不能被实例化 -- 继承了抽象类的类必须重写抽象类的所有抽象方法,除非这个子类也为抽象类 -- 抽象方法不能使用 private, final, static 关键字,因为和重写相违背 - -### 模板模式 - -当多个类中有相同的方法时,可以考虑提取到一个父类抽象类中。封装不变部分,扩展可变部分: - -- 相同的方法一般使用 `final` 关键字修饰 -- 不同的方法设为抽象方法,需要子类去重写 - -> [菜鸟教程的简单 demo](https://www.runoob.com/design-pattern/template-pattern.html) - -## 出租系统-简单面向对象应用 - -TODO - -## 拓展阅读 - -- [Java HashCode 详解](https://blog.csdn.net/tanggao1314/article/details/51505705) diff --git a/content/posts/z-study/java/base/java_5.md b/content/posts/z-study/java/base/java_5.md deleted file mode 100644 index 76c7841..0000000 --- a/content/posts/z-study/java/base/java_5.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: 'Java_5_接口&内部类' -date: 2023-07-16T08:50:57+08:00 -tags: [] -series: [java] -categories: [study notes] -weight: 5 -draft: true ---- - -## 接口 - -### 基本概念 - -接口就是给出一些没有实现的方法,封装到一起,在某个类需要使用的时候,再根据具体情况把这些方法实现。 - -```java -interface XxxXxx { - // 1. 抽象方法,接口中有无a bstract 关键字都行,默认会加上 - // 2. jdk1.8后 default 方法, 具体实现 - // 3. jdk1.8后 static 方法 -} - -class LeiName implements XxxXxx { - // 必须实现接口的抽象方法 -} -``` - -### 应用场景 - -制定规格规范,方便统一。 - -```java -// 定义数据库连接规范,这样对于不同数据库连接, -// 即使是不同人来写,也得遵守方法规范,减轻心智模型 -interface DBInterface { - public void connect(); - public void close(); -} - -public class Mysql implements DBInterface { - public void connect() { - System.out.println("connect mysql..."); - } - - public void close(){ - System.out.println("close mysql..."); - } -} - -public class Oracle implements DBInterface { - public void connect() { - System.out.println("connect oracle..."); - } - - public void close(){ - System.out.println("close oracle..."); - } -} -// .... other db - -// use -public class Test { - public static void main(String[] args) { - Mysql mysql = new Mysql(); - Oracle oracle = new Oracle(); - - db(mysql); - db(oracle); - } - - // 形参体现接口的多态 - public static void db(DBInterface dbInterface) { - dbInterface.connect(); - dbInterface.close(); - } -} -``` - -### 细节 - -- 接口不能被实例化,都是需要类来实现的,可以看成某一类型的规范 -- 接口中所有方法是 `public abstract`(可省略) 的 -- 普通类必须实现接口中的所有抽象方法;抽象类可以不用实现接口的方法 -- 一个类可以同时实现多个接口;一个接口可以继承多个接口 -- 接口中的属性都是 `public static final`(可省略) 的,且必须初始化 - -### 比较 - -实现接口可以看出是继承类的补充。 - -比如:小猴子继承老猴子,天生就会爬树,但是想要飞翔就得实现飞行的接口,想要游泳就得实现游泳的接口,因为老猴子本身并不具备这两种能力。 - -```java -interface Swimming { - void swmiiing(); -} -interface Fly { - void fly(); -} - -class LittleMonkey extends Monkey implements Swimming,Fly { - // ... -} -``` - -- 类继承价值在于`复用性`和`可维护性` -- 接口价值在于`制定规范,让其他类实现,更加灵活` -- 接口在一定程度上实现代码解耦 [接口规范性+动态绑定] - -> [接口与其他概念比较](https://www.runoob.com/java/java-interfaces.html) - ---- - -## 四种内部类 - -### 介绍 - -顾名思义,一个类中包含了另一个类。 - -```java -class Outer{ // 外部类 - class Inner { // 内部类 - // ... - } -} -``` - -> 当内/外部类有重名的属性遵循就近原则,访问外部需要 `外部类名.this.xxx` - -内部类的最大特点是:可以直接访问外部类的私有属性或方法,并且可以体现类与类之间的包含关系。 - -### 分类 - -- 局部内部类,定义在方法中,不能添加访问修饰符,但是可以加 final - - ```java - public class Outer { - - public static void main(String[] args) { - new Outer().part(); - } - - public static int num = 1; - - public void part() { - class Inner { - private String num = "yy"; - - public void log() { - System.out.println(num + "---" + Outer.this.num); - } - } - // 访问内部类 实例化对象即可 - Inner inner = new Inner(); - inner.log(); - } - } - ``` - -- 匿名内部类,无类名的局部内部类 - `new 接口/类名(参数) { ... };` - - ```java - // 场景: 传统接口一定要先被类实现,再重写方法,但是这个实现的类假如只使用一次呢? - // 那么每次创建类再实现接口就有点浪费了,可以直接使用匿名内部类来完成 - class AnonymousOuter { - public void demo() { - // 语法 直接 new 接口/类名() { // ... }; - // 编译类型是 Animal; 运行类型是 AnonymousOuter$1, 如有多个多个就外部类$1..n - Animal tiger = new Animal() { - @Override - public void cry() { - System.out.println("tiger cry..."); - } - }; - - tiger.cry(); // tiger一直存在,但是匿名内部类只使用一次 - } - } - - public static void main(String[] args) { - AnonymousOuter anonymousOuter = new AnonymousOuter(); - anonymousOuter.demo(); - } - - // 实践,可以直接作为参数传递给方法。 - ``` - -- 成员内部类,无 static,不在方法里而再在类成员位置写类,可以用所有修饰符 - - ```java - class MemberOuter { - public class MemberInner { - public void log() { - System.out.println("访问成员内部类..."); - } - } - } - // 外部其他类使用内部类的情况 - public static void main(String[] args) { - MemberOuter memberOuter = new MemberOuter(); - MemberOuter.MemberInner memberInner = memberOuter.new MemberInner(); - memberInner.log(); - } - ``` - -- 静态内部类,有 static,限制了不能访问非静态的成员 - - ```java - // 把上方成员内部类变成静态成员内部类后访问方式 - public static void main(String[] args) { - MemberOuter.MemberInner memberInner = new MemberOuter.MemberInner(); - } - - // 区别前面三个,访问外部类重复属性名的时候是 “外部类名.静态属性” 没有this了.. - ``` - -> 入门视频更详细 ---[B 站](https://www.bilibili.com/video/BV1fh411y7R8/?p=414&spm_id_from=pageDriver&vd_source=fbca740e2a57caf4d6e7c18d1010346e) diff --git a/content/posts/z-study/java/base/java_6.md b/content/posts/z-study/java/base/java_6.md deleted file mode 100644 index 526286c..0000000 --- a/content/posts/z-study/java/base/java_6.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: 'Java_6_枚举&注解' -date: 2023-07-19T13:30:33+08:00 -tags: [] -series: [java] -categories: [study notes] -weight: 6 -draft: true ---- - -## 枚举 - -比如季节只有四个,传统创建季节类,可能 new n 个不同季节,不合理,所以就需要枚举类来实现。 - -枚举是一组常量的集合,可以理解为是一种特殊的类,包含了一组有限的特定的对象。 - -```java -public enum Season { - // 定义的常量 必须写在最前面 - SPRING("春天", "温暖"), SUMMER("夏天", "炎热"), AUTUMN("秋天", "凉爽"), WINTER("冬天", "寒冷"); - // 等价于 public static final SPRING = new Season("春天", "温暖"); - private String name; - private String prop; - - // 构造器得是 private 的 - private Season(String name, String prop) { - this.name = name; - this.prop = prop; - } -} -``` - -- enum 类继承自 Enum,且 enum 声明的是 final 类,可以通过 `javap` 反编译查看 -- enum 类也可以作为内部类中使用,也可以有成员属性和方法 -- 定义的常量,必须写在最前面,且需要知道使用的是哪个构造器;如果是无参构造器,则括号可以省略,多个常量之间使用 `,` 号分割 - -### values(), ordinal(), valueOf() - -```java -for (Season season : Season.values()) { - System.out.println(season); // 默认调用 Enum 的 toString方法 - System.out.println(season.ordinal()); // 常量的索引 -} - - System.out.println(Season.valueOf("SPRING")); // 根据字符串拿到常量 -// 其他具体方法可以去 Enum 类中查看 -``` - -### 枚举实现接口 - -枚举不能继承,因为底层已经继承了 Enum 了,但是仍然可以实现接口。 - -```java -interface Job { - public void work(); -} - -public enum Gender implements Job { - MAN, WOMAN; - - @Override - public void work() { - if (this == MAN) { - System.out.println("男人需要工作"); - } else { - System.out.println("女人也需要工作"); - } - } -} -// psvm -Gender.MAN.work(); -Gender.WOMAN.work(); -``` - ---- - -## 注解 - -### JDK 内置基本注解 - -- `@Override`,修饰重写方法,如果加上了编译器就会去检查是否真的重写了父类的方法 - ```java - // IEDA查看源码 - @Target(ElementType.METHOD) - @Retention(RetentionPolicy.SOURCE) - public @interface Override { // 这里的 @interface 表示一个注解类 和接口无关 - } - ``` -- `@Deprecated`,修饰类/属性/方法/包/参数等,表示已经过时,(不推荐使用但并不是不能用),往往用来做新旧版本兼容过度 -- `@SuppressWarnings`,抑制编译器警告,类型很多,不用背,idea 会给出 - -### JDK 元注解: 对注解进行注解(了解,辅助看源码) - -- `@Retention`,指定注解作用范围,保留时间 - - RetentionPolicy.SOURCE,记录在源码层面,编译器 javac 使用后,直接丢弃这种策略的注释 - - RetentionPolicy.CLASS,编译器把注释记录在 class 文件中,JVM 不会保留注释,(默认) - - RetentionPolicy.RUNTIME,编译器把注释记录在 class 文件中,java 程序运行时,JVM 会保留注释 -- `@Target`,指定注解在哪些地方可以使用 -- `@Documented`,指定注解是否在 javadoc 中体现 -- `@Inherited`,子类会继承父类注解 diff --git a/content/posts/z-study/java/base/java_7.md b/content/posts/z-study/java/base/java_7.md deleted file mode 100644 index 48c5b3c..0000000 --- a/content/posts/z-study/java/base/java_7.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: 'Java_7_异常' -date: 2023-07-20T13:40:31+08:00 -tags: [] -series: [java] -categories: [study notes] -weight: 7 -draft: true ---- - -## 异常 - -老生常谈了,为了程序的健壮性,不能因为一个不算致命的问题,就导致整个系统崩溃。 - -### 分类 - -程序执行过程中的异常可以分为两大类: - -- `Error`: JVM 虚拟机无法解决,比如 StackOverflowError、OOM 等,程序会崩溃 -- `Exception`: 因为变成错误或偶然的外在因素导致的一般性问题,比如 空指针,读取不存在的文件等,又分为: - - 运行时异常,java.lang.RuntimeException 及其子类 - - 编译时异常 - -> 对于异常,多写代码就有经验了。 - -### 类图 - -进入 `Throwable` 源码,右键 `Diagram` 可以查看/操作类图。 - -### 处理方式 - -与 JS 基本类似,有两种: - -- tray-catch-finally,捕获异常,自行处理 -- throws,抛出异常,交给调用者(方法)来处理,最顶级的处理者就是 JVM - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202307222056115.png) - -> 如果没有 try,那么默认就是 throws 抛出 - -### t-c-f 注意点 - -当 catch 和 finally 中都有 return 的时候,最终返回的是 finally 的; -当只有 catch 中有返回变量的时候,这个变量是在内存中暂存的,finally 执行完之后会返回。 - -```java -public class Demo { - - public static int method() { - String[] names = new String[2]; - int i = 1; - try { - if (names[10] == null) { // 数组越界 - i++; - } - return 1; - } catch (Exception e) { - return ++i; // i 的值会存到一个临时变量里, 此时为 2; - } finally { - // return ++i; // 这里有 return 那么最终返回的就是 --- 3 - ++i; // 没有 return 那么返回的就是 catch 的中临时变量 --- 2 - } - } - - public static void main(String[] args) { - System.out.println(method()); - } -}; -``` - -### 自定义异常 - -一般继承自 RuntimeException. - -```java - -public class CustomException { - public static void main(String[] args) { - int age = 180; - if (age > 160) { - throw new AgeException("不可能活到这么大~"); - } - System.out.println("年龄正常"); - } -} - -class AgeException extends RuntimeException { - public AgeException(String message) { - super(message); - } -} -``` - -### throw & throws - -- throws,异常处理的一种方式,后面跟异常类型,在方法声明处 -- throw,手动生成异常对象关键字,后面跟异常对象,在方法体中 - -### IDEA 快捷操作 - -选中需要被 `try` 的代码,`F19 + S` 即可。 diff --git a/content/posts/z-study/java/base/java_8.md b/content/posts/z-study/java/base/java_8.md deleted file mode 100644 index 4a19dcf..0000000 --- a/content/posts/z-study/java/base/java_8.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -title: 'Java_8_包装类&String类' -date: 2023-07-23T11:47:25+08:00 -tags: [] -series: [java] -categories: [study notes] -weight: 8 -draft: true ---- - -## 包装类 - -包装类 -- 针对八种基本数据类型有相对应的引用类型。 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202307231151103.png) - -> Character, Boolean 的父类是 Object,其他的父类是 Number - -### 包装类与基本类型相互转换 - -JDK5 之前装箱拆箱的动作都是手动的,现在都是自动的了,但是知道都干了什么。 - -- 装箱,可以通过 `new 包装类(基本类型数据)` or `包装类.valueOf(基本类型数据)` - ```java - int i = 666; - Integer ii = new Integer(i); - Integer iii = Integer.valueOf(i); - ``` -- 拆箱,调用对应类型的 `xxxValue` 方法即可, xxx 为基本数据类型 - ```java - int iiii = iii.intValue() - ``` - > [深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html) - -### 经典题 - -```java -Object a = true ? new Integer(1) : new Double(2.0) -System.out.println(a) // 1.0 -``` - -{{< admonition>}} -三元运算符要看成一体,最终变量的精度为整体的最高精度 -{{}} - -## String 类 - -字符串使用 Unicode 字符编码,一个字符占两个字节(部分字母还是汉字)。与 JS 不同,必须使用双引号。 - -### 实现接口 - -`String`类实现了: - -```java -public final class String - implements java.io.Serializable, Comparable, CharSequence{ - // ... - } -``` - -- `Serializable` 接口,说明 string 可以串行化:可以在网络传输,可以保存到文件 -- `Comparable` 接口,说明 string 可以比较 -- `charSequence` 接口 - -### 本质 - -1. `String` 是 `final` 类 -2. 内部使用 `final char value[]` 来存储字符数组 - - ```java - private final char value[]; // final类型, 不可修改(指的是地址) - - // 经典题:下面创建了几个对象? - String a = 'hello'; - a = 'world'; - // 2 个,因为final修饰字符串是不可变的,因此常量池中创建了hello 和 world 两个字符串对象,只是改变了 a 变量的引用 - String b = "hello" + "world"; // 1 个对象,编译器不傻,会判断常量池中对象是否被引用 - String c = a + b; // a-hello b-world 呢? 3 个 - // 1. 创建 StringBuilder 实例对象 sb (堆上) - // 2. 再分别把 a 和 b append 到 sb 上 - // 3. 返回 sb.toString(), 注意 c 指向的是堆中地址 - // 因此: System.out.println(b == c); // false - ``` - -### 两种创建方式 - -1. `String xxx = "xxx"` -2. `String xxx = new String(...)`,构造器很多,查手册 - -这两种创建方式的本质区别是: - -- 方式一:在常量池里寻找是否存在"xxx",存在就直接指向,否则就在常量池中创建。变量最终指向的是常量池中的空间地址 -- 方式二:先在堆中开辟空间,维护了 value 属性,如果常量池中存在 value 就指向常量池空间,否在在常量池中创建。变量最终指向的是堆中的空间地址 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202308152311173.png) - -```java -// 经典题 -String a = "abc"; -String b = "abc"; -System.out.println(a.equals(b)); // true -System.out.println(a == b); // true -/* ---------- 第二种 ---------- */ -String a = "abc"; -String b = new String("abc"); -System.out.println(a.equals(b)); // true -System.out.println(a == b); // false -``` - -### intern() - -这个方法就是根据 String 的“模样”,去常量池中找,如果有“长得一样的”,就返回这个地址;否则就在常量池中新创建该字符串并返回常量池中的地址。 - -```java -String a = "hello"; -String b = "world"; -String c = "helloworld"; -System.out.println((a + b).intern() == c); // T -``` - ---- - -## StringBuffer - -String 类的效率比较低,每次更新都需要重新开辟空间,所以 java 中设计了 StringBuilder 和 StringBuffer 来提高效率。 - -### 继承&实现 - -`StringBuffer` 也实现了 `Serializable` 接口 -`StringBuffer` 继承自 `AbstractStringBuilder`,该类内部有 `char[] value` 非 final 属性。 -`StringBuffer` 也是 final 类 - -> 相比较 String,在扩大字符串的时候,是修改的在堆中的 value 的内容,只有当扩容的时候才会修改地址。而 String 每次都是在修改地址。 - -## StringBuilder - -与 StringBuffer 类似,但不保证同步(不是线程安全的,看源码可以看到它的方法都没有 synchronized 关键字修饰的。)。被当做 StringBuffer 的简易替换,**用作字符串缓冲区被单个线程使用的时候**。它比 StringBuffer 要快。 - -## 比较 - -- String:效率低,复用率高 -- StringBuffer:效率较高,线程安全 -- StringBuilder:效率最高,线程不安全。`Array.toString()` 就是用这个拼接的~ - -## 其它内部类,直接查手册吧~ diff --git a/content/posts/z-study/java/base/java_9.md b/content/posts/z-study/java/base/java_9.md deleted file mode 100644 index e040610..0000000 --- a/content/posts/z-study/java/base/java_9.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: 'Java_9_集合' -date: 2023-09-14 09:53:06 -tags: [] -series: [java] -categories: [study notes] -weight: 9 -draft: true ---- - -Java 种数组需要手动扩容,使用起来很不方便,因此,更常用的是集合。 - -## Collection 接口 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202309161520534.png) - -### List - -- 存储有序 -- 有索引 -- 可重复 - -#### 三种常用的 List 实现类 - -- `ArrayList`,线程不安全,内部是一个 `Object[]` 数组: `elementData`。 - - 扩容机制:使用 `Arrays.copyOf()`, 1. `new ArrayList()`,初始大小为 0,加入一个元素后变成 10,装满后再装就变成 1.5 倍; 2. `new Array(int)`,初始为 int,装满后也是扩容到 1.5 倍。 -- `Vector`,线程安全,操作方法都带有 `synchronized` 修饰。另外它的扩容是按照 2 倍扩容。 -- `LinkedList`,线程也不安全,底层使用双向链表实现,因此,增删的效率高,改查的效率低。 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202310072214653.png) - -### iterator 和 增强 for - -学过 js 的 generator 函数应该很容易理解这一部分内容。 - -```java -ArrayList test = new ArrayList(); -test.add("111"); -test.add("222"); -test.add("333"); - -Iterator iterator = test.iterator(); -while (iterator.hasNext()) { - String next = iterator.next(); - System.out.println(next); -} - -// 增强 for 的底层使用的也是迭代器 -for (String s : test) { - System.out.println(s); -} - -// 另外一种就是普通for循环了~ -``` - -### Set - -- 存储无序 -- 无索引 -- 不允许重复,至多一个 null - -#### 两种常用的 Set 实现类 - -- `HashSet`,底层实际上是`HashMap`,而`HashMap`的底层是`数组+链表`,根据容量会树化为红黑树 - - - 当 Set 小于 64(MIN_TREEIFY_CAPACITY) 的时候,数组+链表(邻接表---数组存储链表头)就够了 - - 当 Set 大于等于 64(MIN_TREEIFY_CAPACITY) 且某条链表容量到达 8(TREE_THRESHOLD) 时,整个表会进行树化(红黑树)。单个链表到达 8 的时候如过不能树化也会扩容。 - - ```java - // 经典题 - set.add(new String('hello')); - set.add(new String('hello')); // 可以加入吗? 答案是:NO! - // 底层添加的原理 hash + equals - // 添加一个元素时,先得到hash值,回转成索引值,根据索引值找到链表,当链表不为空时,调用 equals 方法进行比较,如果相同就放弃添加。String的equals是比较字符串内容,所以上述不能添加进去。 - - /* ---------- new HashSet ---------- */ - public HashSet() { - map = new HashMap<>(); - } - /* ---------- hashSet.add(E e) ---------- */ - return map.put(e, PRESENT)==null; - return putVal(hash(key), key, value, false, true) - /* ---------- hash(key) ---------- */ - (h = key.hashCode()) ^ (h >>> 16) - ``` - - - LinkedHashSet,是 HashSet 的子类,底层是 `数组+双向链表`,所以能确保便利顺序和插入顺序一致。 - -- `TreeSet`. - -## Map - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202309161519964.png) - -### HashMap - -之前 Set 底层存储的 Node [k, v] 的 v 是常量 PRESENT,在 Map 中 v 就是 `map.put(k,v)` 的 v。当遇到 k 相同时,map 的策略是后来居上,直接替换 v。 - -```java -Map map = new HashMap(); - -map.put("hello", 1); -map.put("hello", 2); // map 中最后只剩下 hello-2 这个值 - -// k-v 最终是 HashMap$Node node = newNode(hash, key, value, null); -// 为了方便遍历,还会创建 EntrySet 集合,该集合村房的元素类型是 Entry,EntrySet>,注意:这里的 k-v 是创建的对 newNode 中 key,value 的引用:transient Set> entrySet; 可以通过 map.entrySet(); 获取到这个 Set -``` - -### HashTable - -与 HashMap 不同的点: - -- k、v 都不能为 null -- HashTable 是线程安全的,HashMap 则不是 - -#### Properties (HashTable 的子类) - -常用于从 `xxx.properties` 文件中,加载数据到 `Properties` 类对象,并进行读取和修改。 - ---- - -## 选择合适的数据结构 - -- 单列(obj) -- Collection - - 可重复 -- List - - 增删多:LinkedList,底层双链表 - - 改查多:ArrayList,底层 object 类型的可变数组 - - 不可重复 -- Set - - 无序:HashSet,底层是 HashMap - - 排序:TreeSet,构造器使用一个比较方法 - - 插入/取出顺序一致:LinkedHashSet -- 双列(k,v) -- Map - - 键无序:HashMap - - 键有序:TreeMap - - 插入/取出顺序一致:LinkedHashMap - - 读取文件:Properties - ---- diff --git a/content/posts/z-study/java/mybatis/image-1.png b/content/posts/z-study/java/mybatis/image-1.png deleted file mode 100644 index ed55ab8..0000000 Binary files a/content/posts/z-study/java/mybatis/image-1.png and /dev/null differ diff --git a/content/posts/z-study/java/mybatis/image-2.png b/content/posts/z-study/java/mybatis/image-2.png deleted file mode 100644 index 4240a82..0000000 Binary files a/content/posts/z-study/java/mybatis/image-2.png and /dev/null differ diff --git a/content/posts/z-study/java/mybatis/image.png b/content/posts/z-study/java/mybatis/image.png deleted file mode 100644 index 6ed65a0..0000000 Binary files a/content/posts/z-study/java/mybatis/image.png and /dev/null differ diff --git a/content/posts/z-study/java/mybatis/mybatis.md b/content/posts/z-study/java/mybatis/mybatis.md deleted file mode 100644 index 37d5756..0000000 --- a/content/posts/z-study/java/mybatis/mybatis.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: 'mybatis' -date: 2023-12-12T19:30:05+08:00 -lastmod: -tags: [] -categories: [study notes] -draft: true ---- - -### MVC 和三层架构 - -- 著名 MVC 架构 - ![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202312121932695.png) -- 三层架构 - ![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202312122258621.png) - -其中三层架构中的`表示层`就是 MVC 的 `Controller+View` 层;而 Service 业务逻辑层和 Dao 持久化层是 MVC 的 model 和 db 之间的层。 - -### ORM 思想 - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202312141810437.png) - -### 入门基础 - -mybatis 中的两种重要配置文件 - -1. [mybatis-config].xml,这个文件用来核心配置,比如连接数据库信息 - - ```java - // 使用 mybatis io 自带的 Resources.getResourceAsStream() 接口来获取配置文件的输入流 - InputStream is = Resources.getResourceAsStream("mybatis.xml"); // 第二个参数可以指定环境,详细看下面的配置文件教学 - // 底层就是 InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis.xml") - // 极简配置 - SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); - SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); - // 开启会话(底层会开启事务) - SqlSession sqlSession = sqlSessionFactory.openSession(); - // 执行sql语句 - int count = sqlSession.insert("insertUser"); // 配置sql xml 内语句的 id - sqlSession.commit(); // 默认不是自动提交 - ``` - -2. [XxxMapper].xml,这个文件专门用来编写 sql 语句,一个表对应一个 xml - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202312181356370.png) - -> PS: resources 文件夹是类根目录。 - ---- - -#### insert - -回顾一下 jdbc 的做法: - -1. 获取 connection 对象,DriverManager.getConnection() -2. 获取 ps 对象 connection.prepareStatement(sql) -3. ps 执行操作 - -```java -// PreparedStatement -String sql = "insert into tb_name (columns...) values(null, ?,?,?,...)"; // 通过 ?占位 -PreparedStatement ps = connection.prepareStatement(sql); -ps.setString("hello"); // 设置值 -// ... -int count = preparedStatement.executeUpdate(); -``` - -硬伤:sql 在代码中硬编码,而且很烦琐。 - ---- - -在 Mybatis 中通过 `#{}`来占位,括号内填入需要调用的 bean 的属性,底层还是要去调用 bean 对应属性的 getter 方法。 - -```xml - - - - - insert into tb_name(columns...) values(null, #{id}, #{name},...); - - -``` - -```java -// sqlSession可以封装一个工具类来简化操作 - -SqlSession sqlSession = SqlSessionUtil.openSession(); - -// sqlSession.insert(id, Map); -// 第一个参数为 xml 中 sql 的id,第二个参数最原始的做法是创建个 Map, map put的key与表字段一一对应。 -/* ---------- 在平时开发中,一般创建实体类来与表一一对应 ---------- */ - -User user = new User(007, "Tom"); - -sqlSession.insert("insertUser", user); - -sqlSession.commit(); -sqlSession.close(); - -// UserDo 实体类 -class UserDao { - private Long id; - private String name; - - getter - setter - - constructor - toString -} -``` - -#### select - -update 和 delete 省略了,比较简单,着重看看 select 查询语句。 - -在 jdbc 时,查询出的结果集需要通过 while(rs.next()) 遍历出每一行,然后塞到一个 list 内。 - -mybatis 提供了两个 api,`selectOne` 和 `selectList`, - -> 需要注意的是:mysql 中,字段默认是下划线命名,而 java 中一般是小驼峰,因此,sql 语句查询的时候需要 as 别名与实体类一一对应,并且在 `` - -```xml - - - -``` - -```java -// sqlSession.selectOne(beanId, params); -sqlSession.selectOne("queryUser", 007); -List users = sqlSession.selectList("queryUsers"); // 很简单 不需要手动遍历了 -``` - -##### namespace - -mapper 上的 namespace 猜也能猜到是为了防止多个 bean 具有相同的 id 的问题了。 - -在 java 中就需要写全了:`namespace.beanId` - -### mybatis 配置文件 - -- environments - ![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202312181356370.png) -- transactionManager - ![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202312201340544.png) -- dataSource - ![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202312201339872.png) - - - 关于连接池配置参数,详细看文档,常用的就那几个,最大连接数量,超时时间,最大空闲数量等 - -- properties - ![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202312210923360.png) - 不过一般不会这么写,而是通过 `` 来加载类路径下的配置文件,或者通过 url 加载绝对路径下的配置文件。 - -- [mybatis 核心配置文件](https://www.bilibili.com/video/BV1JP4y1Z73S?p=24&spm_id_from=pageDriver&vd_source=fbca740e2a57caf4d6e7c18d1010346e) - ---- - -### B 站课程 - -- [MyBatis 教学](https://www.bilibili.com/video/BV1JP4y1Z73S) -- [MyBatisPlus 教学](https://www.bilibili.com/video/BV1Xu411A7tL) diff --git a/content/posts/z-study/java/mybatis/mybatis_plus.md b/content/posts/z-study/java/mybatis/mybatis_plus.md deleted file mode 100644 index 4ced999..0000000 --- a/content/posts/z-study/java/mybatis/mybatis_plus.md +++ /dev/null @@ -1,204 +0,0 @@ ---- -title: 'mybatis-plus' -date: 2023-12-21T13:27:55+08:00 -lastmod: -tags: [] -# series: [mybatis] -categories: [study notes] -# weight: 1 -draft: true ---- - -mybatis 写 sql 的时候仍然有一些不便,mybatis-plus 对它进行了增强。 - -## 入门 - -### 用它 - -简单两步骤: - -1. pom.xml 引入 mp 的依赖,可以去 maven 官网查 -2. 接口 xxxMapper 继承 mp 提供的 BaseMapper,即可完成简单的 crud。 - -```java -public interface UserMapper extends BaseMapper { - /** - * 1. BaseMapper 是 mp 提供的,内置了基础的crud - * 2. BaseMapper 的泛型得是要操作的 实体类 - */ -} -``` - -#### 实体类的约定 - -mp 基于反射获取实体类信息作为数据表信息 - -```java -@Data -public class User { // 类名 驼峰转下划线 作为表名 - private Long id; // 名为 id 的字段作为 主键 - private String username; - private String password; - private String workStatus; // 变量名 驼峰转下划线 作为表的字段名 - private boolean isMan; // is 开头命名的布尔变量在 mp 中是特殊的,比如这里会被解析为 man -} -``` - -### 注解 - -上述两步就能搞定与数据库打交道的前提是遵守了 mp 的约定,当与约定冲突的时候,就需要注解来辅助了。 - -#### 常用注解 - -全部注解在 mp 的官网是可以看到的,这里先学习 3 个常用的注解。 [全部注解](https://baomidou.com/pages/223848/)。 - -- @TableName(),指定表名 -- @TableId(),指定主键 -- @@TableField(),指定表字段。 - - 成员变量名与数据库字段名不一致 - - 成员变量名以 is 开头,且是布尔值 - - 成员变量名与数据库关键字冲突 - - 成员变量不是数据库字段 - -```java -@Data -@TableName("sys_user") -public class User { - // AUTO:数据库自增长; - // INPUT:通过set方法自行输入; - // ASSIGN_ID :mp 分配ID,接口ldentifierGenerator的方法nextld来生成id,默认实现类为DefaultldentifierGenerator雪花算法 - @TableId(value="id", type=IdType.AUTO) - private Long id; - private String username; - private String password; - @TableField(exists=false) // 表中不存在 - private String workStatus; - @TableField("isMan") - private boolean isMan; -} -``` - -### 常用配置 - -[直接看官网,一般 ide 也会有提示建议的](https://baomidou.com/pages/56bac0/#%E5%9F%BA%E6%9C%AC%E9%85%8D%E7%BD%AE) - -## 核心功能 - -### 条件语句 - -mp 的很多删改查的接口的参数为 `Wrapper`,这个 Wrapper 就是用来构造条件 where 语句的。 - -关系如下: - -`Wrapper -> AbstractWrapper : - 1. QueryWrapper 2. UpdateWrapper 3. AbstractLambdaWrapper : - 3.1 LambdaUpdateWrapper 3.2 LambdaQueryWrapper` - -> LambdaXxx 是为了解决 sql 字符串硬编码的问题。UpdateWrapper 只有在 set 语句比较特殊的时候才会使用。 - ---- - -QueryWrapper demo: - -```java -// query -// select id, username, balance from user where username like ? and balance >= ? -void test() { - // 构建查询条件 - QueryWrapper wrapper = new QueryWrapper() - .select("id", "username", "balance") - .like("username", "o") - .ge("balance", 1000) - // 查询 - List users = userMapper.selectList(wrapper); - users.forEach(System.out::println); -} -// update -// update user set balance = 2000 where (username = "jack") -void test() { - // 更新数据 - User user = new User(); - user.setBalance(2000); - // 更新条件 - QueryWrapper wrapper = new QueryWrapper() - .eq("username", "jack"); - // 更新 - userMapper.update(user, wrapper); -} -``` - -UpdaterWrapper demo: 给 id 为 1,2,4 的用户余额 -2000 - -```java -// update user set balance = balance - 2000 where id in (1,2,4); -// 这种情况,用queryWrapper就不太好处理 -void test() { - List ids = List.of(1L,2L,4L); - UpdateWrapper wrapper = new UpdateWrapper() - .setSql("balance = balance - 2000") - .in("id", ids); - userMapper.update(null, wrapper); -} -``` - -**推荐 Lambda**: - -```java -// lambda 解决硬编码 -void testLambda() { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper() - .select(User::getId, User::getUsername, User::getBalance) - .like(User::getUsername, "o") - .ge(User::getBalance, 1000); - // 查询 - List users = userMapper.selectList(wrapper); - users.forEach(System.out::println); -} -``` - -### 自定义 sql - -上方 UpdateWrapper 的一个问题是:setSql("balance = balance - 2000"); 属于直接在业务层写 sql 了,有些公司要求,只能在 xml 里写 sql,怎么办呢? - -通过自定义 sql 方法 -- 用 mp 的 Wrapper 生成 where 条件(这更方便呀!),然后把 wrapper 传递到自定义的 sql 中。 - -```java -void test() { - List ids = List.of(1L, 2L, 4L); - int amount = 200; - - LambdaWrapper wrapper = new LambdaWrapper() - .in(User::getId, ids); - userMapper.updateBalanceByIds(wrapper, amount); // updateBalanceByIds 是在 UserMapper 内的自定义方法。 -} - -class UserMapper extends BaseMapper { - // 注意 这里 Constants.WRAPPER 为 "ew", wrapper的注解必须为这个! - void updateBalanceByIds(@Param(Constants.WRAPPER) LambdaQueryWrapper wrapper, @Param("amount") int amount); -} - -// 最后自定义sql可以在 updateBalanceByIds 上用注解写,也可以在xml中写,如: - - Update user set balance = balance - #{amount} ${ew.customSqlSegment} - -``` - -### service 接口 - -在上面继承了 BaseMapper 后,增删改查都交给 mp 了。 - -mp 提供了 `IService` 接口和对应的实现类 `ServiceImpl`,以此来简化 service 层的 crud 操作(底层还是调用的 mapper)。 - -比如我们有自定义 UserService 接口,和 UserServiceImpl 实现类,一般都会让 UserService 继承 IService,再 UserServiceImpl 继承 ServiceImpl。 - -```java -// 1. 接口继承 -public interface UserService extends IService{} - -// 2. 为了不实现所有方法直接继承 ServiceImpl -public class UserServiceImpl extends ServiceImpl implements UserService {} -``` - -> IService 提供的 LambdaQuery 和 LambdaUpdate 平时更加常用于复杂的查询和更新。 - -剩下的就需要赶紧进行实操啦~~~ diff --git a/content/posts/z-study/java/spring/spring_1.md b/content/posts/z-study/java/spring/spring_1.md deleted file mode 100644 index bef7de5..0000000 --- a/content/posts/z-study/java/spring/spring_1.md +++ /dev/null @@ -1,216 +0,0 @@ ---- -title: 'spring_1' -date: 2023-12-15T09:34:51+08:00 -lastmod: -tags: [] -categories: [study notes] -draft: true ---- - -### 核心 - -spring 是一个轻量级的开源框架 -spring 是为了解决企业级应用开发业务逻辑层和其他各层耦合的问题 -spring 是一个 IOC 和 AOP 的容器框架。容器:包含并管理应用对象的生命周期。 - ---- - -spring 的两大核心是: - -- **控制反转(Inversion of Control,IOC)** -- **面向切面编程(aspect- oriented programming,AOP)** - ---- - -#### IOC - -> IOC 与大家熟知的**依赖注入**同理,指的是对象仅通过构造函数参数、工厂方法的参数或在对象实例构造以后或从工厂方法返回以后,在对象实例上设置的属性来定义它们的依赖关系(即它们使用的其他对象). 然后容器在创建 bean 时注入这些需要的依赖。 这个过程基本上是 bean 本身的逆过程(因此称为 IOC),通过使用类的直接构造或服务定位器模式等机制来控制其依赖项的实例化或位置。 - -简单讲,IOC 是一种思想,把设计好的对象交给容器控制,而不是显式地用代码创建对象。 - -把创建和查找依赖对象的控制权交给 IOC 容器,由 IoC 容器进行注入、组合对象之间的关系。这样对象与对象之间是松耦合、功能可复用(减少对象的创建和内存消耗),使得程序的整个体系结构可维护性、灵活性、扩展性变高。所谓 IOC,就简短一句话:**对象由 spring 来创建、管理,装配!** - -##### 设计五大原则 (solid) - -1. 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。换句话说,一个类应该只负责一种功能。 -2. 开放封闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着当需要添加新的功能时,不应该修改现有的代码,而是通过扩展来实现。 -3. 里氏替换原则(Liskov Substitution Principle,LSP):子类应该能够替换其父类并出现在父类能够出现的任何地方,而不引起任何错误或异常。 -4. 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该被迫依赖它们不使用的接口。这意味着应该创建多个特定于客户端的接口,而不是一个大而全的接口。 - - 人话 1:这个原则的目的是把大接口替换为多个小接口,更加专一。 - - 人话 2:这个原则的前提是模块间不要使用类强耦合,而是使用接口进行分离 -5. 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。 - -> IOC 就是接口隔离原则和依赖倒置原则的最佳实践。 - -举例:电脑,鼠标。 - -- 如果类与类之间强耦合,那就等于是把鼠标和电脑焊死了~ -- 如果用的是那种 ps/2 的接口,电脑鼠标是分离了,但是不能热插拔~ -- 所以最佳使用 usb 接口。电脑为上层,鼠标为下层,下层依赖上层,而不是上层依赖下层,控制反转 - -##### IOC 引入方式 - -- 入门学习:导入 jar 包 + `xml` -- 一般配置:maven + `注解 + xml` -- 高阶方式:springboot + `javaconfig` - -> 关于 Maven 配置参考:[1. Maven 之配置文件](https://www.jianshu.com/p/06f73e8cbf78),[2. Maven 配置多仓库的方法](https://www.jianshu.com/p/06f73e8cbf78) - -##### 未使用 IOC 的分层 - -按照 实体类 --> (controller 层 --> service 层 --> dao 层)(括号部分反过来也 ok) 的顺序编写代码,看个人习惯。 - -- 实体类 - -```java -public class UserDO { - private Integer id; - private String Username; - - public Integer getId() { - return id; - } - - public String getUsername() { - System.out.println("查询 mysql 数据库"); - return Username; - } - - @Override - public String toString() { - return "UserDO{" + - "id=" + id + - ", Username='" + Username + '\'' + - '}'; - } -} -``` - -- dao 持久层,查询数据库 - -```java -// 接口 -public interface IUserDao { - String getUser(); -} -// 实现 -public class UserDaoImpl implements IUserDao { - @Override - public String getUser() { - // Dao层 模拟查询数据库 - System.out.println("查询数据库~"); - return "mysql"; - } -} -``` - -- service 层调用 dao 层 - -```java -public class UserServiceImpl implements IUserService { - - IUserDao userDao = new UserDaoImpl(); - - @Override - public String getUser() { - return userDao.getUser(); - } -} -``` - -- controller 层调用 service 层,这里就简单测试一下代码 - -```java -public class Test { - public static void main(String[] args) { - UserServiceImpl userService = new UserServiceImpl(); - String user = userService.getUser(); - System.out.println("user" + user); - } -} -``` - -> controller 调用 service,service 调用 dao,实现了分层,但是,上述代码调用过程中,是把对象实例化到上层中的,造成代码强耦合!一旦代码逻辑变动,需要修改到实例对象,那么有多少个地方引用,就得改多少个地方,非常繁琐。 - -举例: - -```java -// 假如换了数据库查询,那么重新写个实体类 -public class UserDaoOracleImpl implements IUserDao { - @Override - public String getUser() { - System.out.println("查询数据库"); - return "oracle"; - } -} -// 之前的 service 层对 dao 的调用就得调整 -// IUserDao userDao = new UserDaoImpl(); // new UserDaoImpl() --> new UserDaoOracleImpl() -``` - ---- - -##### 使用 IOC 后 - -- 在 resource 目录下创建 spring.xml 配置文件 - -```xml - - - - - -``` - -- 把 UserDaoImpl、UserServiceImpl (再 spring 中称为 bean) 进行分离 - -```xml - - - - - - - -``` - -- 实例化对象的方式 --> 改为注入的方式,xml 配置的 bean 需要同步设置 getter/setter - -```java -// IUserDao userDao = new UserDaoImpl(); // 替换为: -public IUserDao getDao() { - return dao; -} - -public void setDao(IUserDao dao) { - this.dao = dao; -} - -IUserDao dao; // 与 property name 对应 -``` - -- spring.xml 通过配置 bean 实现 IOC,还需要设置 property 依赖注入: - -```xml - - - - -``` - -- 最后需要再 controller 中加载 spring 上下文,加载 ioc 容器 - -```java -// 加载spring上下文,加载ioc容器 -ApplicationContext ioc = new ClassPathXmlApplicationContext("spring.xml"); -IUserService service = ioc.getBean(IUserService.class); -String user = service.getUser(); -System.out.println("user" + user); -``` - -完成。 - -这样的好处是,当需要修改 UserDaoImpl 时,直接修改 bean 即可,比如 ``,就很灵活。 diff --git a/content/posts/z-study/lerna.md b/content/posts/z-study/lerna.md deleted file mode 100644 index 998e14e..0000000 --- a/content/posts/z-study/lerna.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: 'Lerna' -date: 2022-09-21T17:16:09+08:00 ---- - -> Lerna is a fast, modern build system for managing and publishing multiple JavaScript/TypeScript packages from the same repository. - -![](https://cdn.jsdelivr.net/gh/yokiizx/picgo@main/img/202303171022701.png) - -## lerna 解决了哪些问题 - -1.
- 调试问题 - 以往多个包之间需要通过 npm link 进行调试,lerna 可以通过动态创建软链直接进行模块的引入和调试。 - - ```JS - function createSymbolicLink(src, dest, type) { - return fs - .lstat(dest) - .then(() => fs.unlink(dest)) - .catch(() => { - /* nothing exists at destination */ - }) - .then(() => fs.symlink(src, dest, type)); - } - ``` - - > PS: linux 创建软链 `ln -s source target`,注意 source 的路径如果为相对路径相对的是目标文件的路径 - -
- -2. 资源包升级。 - 一个项目依赖了多个 npm 包,当某一个子 npm 包代码修改升级时,都要对主干项目包进行升级修改。 - -## 常用命令 - -```sh -# 仓库初始化 -lerna init -# 创建子包 -lerna create -# 将本地包交叉链接在一起并安装剩余的包依赖项 -lerna bootstrap -# 增加依赖 package 到最外层的公共 node_modules -lerna add -# 增加依赖 package 到指定子包 subpackage -lerna add --scope= -# lerna 执行传递的 shell 命令,与 npm 类似 -lerna exec -- -leran exec --scope # 只对某个包执行 shell -# 执行 npm 脚本 -lerna run