经典优秀题目

2022.02.15

Personal

经典的题目。 记录一些解决思路比较巧妙,具有参考价值的题目。 逃离黑洞 题目 【 逃离黑洞 】 你驾驶一艘飞船正在宇宙中遨游,宇宙可简化为一个二维平面,你所在的位置坐标是[0,0],你想要到达的目标坐标是[a,b]。 宇宙中有若干个黑洞,它们的坐标在holes数组中给出,每一个坐标为一对正整数值,代表黑洞中心的横、纵坐标位置[x,y],给出的黑洞中心点都位于平面内(含边界)。 假设每个黑洞的半径r都是相同的,当你的位置与黑洞中心距离l <= r时,你会被立即吸入黑洞。 求你有机会到达目标坐标的条件下,最大的黑洞半径r。 你可以在宇宙中沿着任意连续路线行驶,且位置不必在整数坐标点上。 思路 黑洞半径越小,通过越容易,半径越大,通过越困难。则问题的答案r具有单调性,可以考虑使用二分查找。假设我们判断黑洞半径r是否可以通过的函数为can(r),下面思考如何实现; 黑洞的最小半径为r = 0,最大半径为r = Math.min(a,b)。此外,当某一个黑洞覆盖了目标位置[a,b]时,目标位置也不再可到达,因此最大半径还需要考虑距离目标位置最近的黑洞中心点,到目标位置的距离l。r需要取a,b,l中的最小值,此时我们可以知道目标点一定不会被黑洞覆盖; 当半径变大时,某些黑洞会相交,它们中间没有间隙,因此可以被看做是一个黑洞。这与集合的合并有关,考虑使用并查集维护; 因为当前位置为[0,0]。当目标位置不可到达时,目标位置要么被黑洞与上、下边界形成的隔离带隔开,要么被黑洞与左、下边界形成的隔离带隔开; 对于每一个黑洞半径r,我们用并查集维护连在一起的黑洞集合,并且记录集合中黑洞的最上、下、左位置的坐标,用来判断是否与上述边界形成隔离带; 如果连在一起的黑洞集合与左、下边界或上、下边界形成了隔离带,我们就无法通过,此时r过大,can(r)应该返回false; 否则,有缝隙,can(r)应该返回true。 代码 function blackHole(a, b, holes = []) { let l = 0, r = Infinity; for (let [x,y] of holes) { r = Math.min(r, Math.floor(Math.sqrt((x-a)**2 + (y-b)**2))); } r = Math.min(r, a, b); function touched(c1, c2, r) { // 检查是否触碰 let cur = Math.sqrt((c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2); return cur <= 2 * r; } function can(r) { // judge if radius = r can pass. let set = new UnionFind(holes); for (let i = 0; i < holes.length - 1; i++) { for (let j = i + 1; j < holes.length; j++) { if (touched(holes[i], holes[j], r)) set.merge(i, j); } } for (let i of set.set) { let { top, left, down } = set.getValue(i); if (down - r <= 0) { if (top + r >= b || left - r <= 0) return false; } } return true; } while (l < r) { // 二分查找 let m = Math.ceil((l + r) / 2); if (can(m)) l = m; else r = m - 1; } return r; } class UnionFind { // 并查集 constructor(holes = []) { this.holes = holes; let map = new Map(); let set = new Set(); for (let i = 0; i < holes.length; i++) { map.set(i, { parent: i, rank: 1, top: holes[i][1], down: holes[i][1], left: holes[i][0], }); set.add(i); } this.map = map; this.set = set; } isSame(e1, e2) { return this.getParent(e1) === this.getParent(e2); } getParent(e) { if (this.map.get(e).parent === e) return e; let res = this.getParent(this.map.get(e).parent); this.map.get(e).parent = res; return res; } getSize() { return this.set.size; } add(e) { if (this.map.has(e)) return; this.map.set(e, { parent: e, rank: 1, }); this.set.add(e); } refreshValue(p, c) { this.map.get(p).top = Math.max(this.map.get(p).top, this.holes[c][1]); this.map.get(p).left = Math.min(this.map.get(p).left, this.holes[c][0]); this.map.get(p).down = Math.min(this.map.get(p).down, this.holes[c][1]); } getValue(e) { return this.map.get(e); } merge(e1, e2) { let p1 = this.getParent(e1), p2 = this.getParent(e2); if (p1 === p2) return; let r1 = this.map.get(p1).rank, r2 = this.map.get(p2).rank; let np = r1 >= r2 ? p1 : p2; let nc = r1 >= r2 ? p2 : p1; this.map.get(nc).parent = np; // refresh the top, left, down value of new parent. this.refreshValue(np, nc); if (r1 === r2) this.map.get(np).rank += 1; this.set.delete(nc); } } // test let holes = [ [5, 5], [10, 10], [15, 3], ]; console.log(blackHole(20, 10, holes)); // 4 所有子数组的最小值之和 题目 给定一个整数数组arr,找到min(b)的总和,其中b的范围为arr的每个子数组(连续)。 由于答案可能很大,因此 返回答案模10^9 + 7。 输入:arr = [3,1,2,4] 输出:17 解释:子数组为: [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]。 最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。 数据量大,需要O(n)解法。 思路 暴力枚举不可行,因为复杂度为O(n2); 每个子数组,都一定含有一个最小值。那么反过来思考,对于每个元素arr[i],都对应着若干以它作为最小值的子数组; 可以枚举每个元素arr[i],找到以它作为最小值的子数组数目n,那么这个元素对于结果(最小值和)的贡献为n * arr[i]; 问题转化为:如何确定以arr[i]为最小值的子数组数目?如果一个子数组以arr[i]为最小值,那么在arr[i]两侧,一定有若干值>= arr[i]的元素; 假设arr[i]左侧有m个大于等于其值的元素,右侧有n个,那么加上arr[i]自身,它们一共形成了m * n + m + n + 1个子数组,这些子数组都是以arr[i]为最小值的; 我们找到arr[i]左侧第一个小于它的元素位置l,和右侧第一个小于它的元素位置r,那么用上面的计算方法就可以计算出最终结果; 找到第一个小于自身的元素位置,标准方法是单调栈。此时还有一个很重要的注意点:如果存在相同的值的元素怎么办?[2,2,2,2]这样的数组,岂不是中间会重复计算很多次?; 让左、右区间一开一闭就可以了!比如在左侧寻找<= arr[i]的第一个元素位置,而在右侧寻找< arr[i]的第一个元素位置。 代码 var sumSubarrayMins = function(arr) { let mod = 1e9 + 7; let res = 0; let stk = []; let after = arr.slice().fill(0); let before = arr.slice().fill(0); arr.forEach((i,idx) => { while (stk.length && arr[stk[stk.length-1]] > i) after[stk.pop()] = idx; stk.push(idx); }); while (stk.length) after[stk.pop()] = arr.length; for (let i=arr.length-1; i>=0; i--) { while (stk.length && arr[stk[stk.length-1]] >= arr[i]) before[stk.pop()] = i; stk.push(i); } while (stk.length) before[stk.pop()] = -1; for (let i=0; i<arr.length; i++) { let left = i - before[i] - 1; let right = after[i] - i - 1; res = res % mod + (arr[i] % mod) * ((1 + left + right + left * right) % mod) % mod; } return res; }; 最大宽度坡 题目 给定一个整数数组A,坡是元组 (i, j),其中 i < j 且 A[i] <= A[j]。这样的坡的宽度为 j - i。 找出 A 中的坡的最大宽度,如果不存在,返回 0 。 思路 暴力方法时间复杂度是O(n^2); 观察发现:如果找到了这样一对元素i < j满足要求,我们希望j和i相距尽可能远,也就是说对于每个j,我们希望用尽可能小的i和它匹配,这样宽度更大; 考察i位置元素A[i]和i之后区间的元素A[t],如果A[t] >= A[i],那么能与A[t]形成有效数对的元素A[j],都能与A[i]形成有效数对,且区间[i,j]更宽。因此无需对位于i之后,且值更大的元素进行考虑; 我们分别对i位置的元素、j位置的元素性质进行考察:假设一对元素i < j满足要求,对于[i+1, j-1]之间的元素t,如果也满足A[t] >= A[i],那么: 考察元素对(i,t):因为t-i < j-i,宽度更小,因此对结果没有影响; 如果A[t] <= A[j],考察元素对(t,j):因为j-t < j-i,宽度更小,因此对结果没有影响; 综上,如果存在元素i < j满足要求,我们只需要考虑[i,j]之外的元素匹配情况,无需对[i,j]区间再作考虑。 基于以上结论,使用单调栈实现高效算法: 从前向后遍历,维护一个单调递减的单调栈,栈底元素为A[0]; 从后向前遍历,假设当前遍历的元素为A[j],栈顶元素在A中的索引为top: 如果A[j] >= A[top]: 则记录区间宽度j - top到结果(取最大值),并在栈中弹出top; 如果A[j] < A[top],继续向前移动j,不做任何操作。 代码 var maxWidthRamp = function(nums) { let stk = []; // 单调栈 let res = 0; for (let i=0; i<nums.length; i++) { if (stk.length === 0 || nums[i] < nums[stk[stk.length-1]]) stk.push(i); } for (let i=nums.length-1; i>=0; i--) { // 从后向前遍历,找最大宽度 while (stk.length && nums[i] >= nums[stk[stk.length-1]]) res = Math.max(res, i - stk.pop()); } return res; }; 在二叉树中分配硬币 题目 979. 在二叉树中分配硬币 思路 叶子节点如果有多余的硬币,只能向父节点转移。同理,如果叶子节点缺硬币,只能由父节点转移给它; 那么考察两个叶子节点的父节点,它的硬币转移情况可能有: 左边硬币多、右边硬币少(或者相反),那么需求可以内部消化(左手倒右手); 左右硬币都多,那么左右子节点都需要向父节点转移硬币; 左右硬币都少,那么父节点需要向左右子节点输送硬币; 我们将子节点向父节点输出n个硬币记为+n,将从父节点索取n个硬币记为-n,那么: 左子节点向父节点输送L(索取则L为负值)个硬币; 右子节点向父节点输送R(索取则R为负值)个硬币; 父节点此时的硬币总数为node.val + L + R; 父节点只需要保留一个给自己,其余要向上输出(或者索取),数目为node.val + L + R - 1。 对于父节点,我们可以按子节点同样的方式递归处理,将它的硬币需求向上传递,直到根节点; 对于每个节点,它与左子节点交换的硬币数为abs(L),它与右子节点交换的硬币数为abs(R),我们需要对每个节点将它们加和起来作为结果; 即使是左右倒右手(L和R一正一负),也是需要计入结果的。 代码 var distributeCoins = function(root) { let res = 0; function dfs(node = root) { if (node === null) return 0; let left = dfs(node.left); let right = dfs(node.right); res += Math.abs(left) + Math.abs(right); return node.val - 1 + left + right; } dfs(); return res; };

计算机硬件组成笔记

2022.01.15

Computer Science

计算机硬件组成 CPU结构及工作原理 进程和线程 程序平时储存在硬盘中,当我们执行程序,程序就从硬盘中被加载到内存。 在内存中的程序就叫做进程。每一个进程都被分配了独立的资源(内存空间、网络端口等),进程是系统分配资源的最小单位。 进程在开始运行的时候,操作系统会帮助找到第一行待执行代码,叫做主线程。一颗CPU内核,同时只能运行一个线程。 多线程一定更快吗? 对于单核CPU来说,运行多线程程序需要在线程间进行切换。线程切换是需要花费额外时间的,因此并不是多线程一定速度更快。 CPU的组成 程序计数器PC(Program Counter); 寄存器(Register); 算术逻辑单元ALU(); 缓存 (Cache); CPU缓存 CPU寄存器和内存读取数据速度的比较 CPU从自身的寄存器读取数据的速度,大约比从内存读取数据速度快100倍。 多级缓存 CPU的多级缓存 对数据进行缓存,是解决这类问题的通用思路。 通过对从内存中读取的数据进行缓存,减少CPU下次读取数据的耗时。缓存可以设置若干级,距离CPU从近到远,读取速度从快到慢。 现在CPU一般设置三级缓存,各级缓存的功能有所不同: L1级缓存:CPU的指令缓存,用于缓存CPU的指令; L2级缓存:数据缓存,每个内核独立,用于缓存这个CPU内核的数据; L3级缓存:数据缓存,各内核共用,用于CPU内核间交换数据。 CPU缓存的工作顺序: 从L1调取指令; 从L2读取数据,如果没有则从L3尝试读取数据; 如果没有,从内存读取数据。 各级缓存的存取时间估计: L1:4个CPU时钟周期; L2:11个CPU时钟周期; L3:39个CPU时钟周期; 内存:107个CPU时钟周期; 缓存行 单独按字节访问内存,开销过大。现代CPU是按块访问内存的,一般是64Bit,称为一个缓存行。 内存与缓存的映射关系 从内存中读取的数据,该存放到哪一块缓存区域呢?这个问题也就是内存与缓存的映射关系问题。 一般有三种形式: 直接映射:类似于将地址取模。固定的单个内存地址对应固定的单个缓存位置。 完全关联:任意对应。任意内存地址,对应任意缓存位置。 N路组关联:将缓存地址分为N组,每个内存地址对应单独的一组缓存地址。查找缓存时先找到对应的组别,然后在组内遍历。 直接映射查找迅速,但是因为映射关系太过死板,缓存又不像哈希表一样可以使用拉链法等,缓存中后来的数据会替代先前的数据,导致缓存被频繁更替,命中率不高。 完全关联映射太过宽松,虽然可以保证最大程度地缓存数据,但是查找时必须完全遍历整个缓存,查找效率低。 因此现代CPU一般采用折中方案N-Ways,即N路关联。

JS:数据结构与算法

2022.01.12

JavaScript

Algorithm

Data Structure

记录一些算法 & 技巧 Preview 算法的复杂度 基本操作 在普通的计算机上,加减乘除、访问变量(访问基本数据类型的变量)、给变量赋值等都可以看作基本操作。 对基本操作的次数统计或是估测,可以作为评判算法用时的指标。 不同的基本操作,其实执行用时是不同的(除法比加法用时长),这种不同在计算时间复杂度的时候因过小被忽略。 数据规模 衡量一个算法的快慢,一定要考虑数据规模的大小。 所谓数据规模,一般指输入的数字个数、输入中给出的图的点数与边数等等。 一个数据规模,一般用一个字母来表示(m、n等)。 时间复杂度 程序执行的用时随数据规模而增长的趋势,叫做时间复杂度。 时间复杂度分为最坏时间复杂度和平均时间复杂度。 最坏时间复杂度,即每个输入规模下用时最长的输入对应的时间复杂度。在算法竞赛中,由于输入可以在给定的数据范围内任意给定,我们为保证算法能够通过某个数据范围内的任何数据,一般考虑最坏时间复杂度。 平均(期望)时间复杂度,即每个输入规模下所有可能输入对应用时的平均值的复杂度(随机输入下期望用时的复杂度)。 算法如何高效 计算机解决问题,本质上都是穷举出所有可能结果,然后进行比较选择; 计算的问题可能性越多,就从本质上决定了计算的复杂度越大; 提升算法效率,一般只有以下几个途径: 通过人进行逻辑分析,充分避免无意义的计算步骤(剪枝); 通过充分利用已经计算的结果,避免重复计算(记忆化); 通过合并操作,减少高成本的操作次数。 思路相关 一些思考方向 查看解本身,或者与解有关的因变量,是否具有单调性。具有单调性的问题可以用二分查找求解; 求解空间上连续子区间的问题,考虑滑动窗口法; 自变量与因变量之间存在负相关(一增一减),考虑从两端开始相向而行的双指针法; 问题明显与状态相关,可以清晰地用几个参数定义问题的状态,而且前后状态之间存在转移关系,考虑动态规划。特别地,当状态空间非常小时(大约数万),考虑用二进制进行状态压缩; 对比分析每一步的几种决策,可以消除掉一些非优策略。当问题存在唯一的局部最优策略,那么整体最优结果一定由这个局部最优策略得来,可以使用贪心算法; 问题可以分解为两个或多个子问题,且问题小到一定规模结果是显然可得的,考虑分治法; 问题状态空间较小(例如只有<= 16位的长度),可以直接进行枚举。特别地,可以用二进制数表示每一个状态,从而用二进制数代表状态,进行枚举; 问题数据量有关的提示信息: ≤ 20: 可以是指数级算法。如:回溯等; ≤ 2*10^4:复杂度至少为O(n2)的方法,如:动态规划等。; ≥ 10^5:复杂度至少为O(n*logn)的方法。如:二分查找、滑动窗口、贪心等。 关注点 达成目标的条件:题目的目标是什么,什么情况下就达成了目标,达成目标的条件是否可以等价转化; 不变量:如果在变化过程中存在恒定不变的变量、点或序列等,可能是求解的关键; 转移关系:随着变量的改变,前后二者是否存在可以互相转移的关系; 多个约束条件的耦合性:如果同时存在多个约束条件,考察它们之间是否存在耦合性,如果无耦合可以将它们分开。例如:左右两侧分别进行一次遍历; 可操作的方式:如果题目给你一定的权限进行某种操作,尽可能枚举所有操作,观察它们对结果的影响; 选择唯一的情况:当可以进行多种选择时,关注选择情况受限(或只能进行唯一的选择)的情况,它可能是动态规划等方式解决问题的起点; 映射不变性:给定的元素编号、索引等,在经过一次变换后,如果仍保持一一映射关系,且某些基本性质并未发生改变,可以考虑将利用映射关系,分解问题为子问题; 覆盖:状态之间是否存在覆盖,从而可以去掉一些情况; 操作是否有效: 每种操作,需要观察其后果,是否等价于其他操作,或者是无效操作; 注意事项 正确可行的思路是核心。在没有形成完整的思路之前,不要开始写程序; 读题。读题。读题。(充分理解题目要求); 充分构造边界条件下的测试用例,全通过后再提交。 雷点&坑点 关注是否存在负数节点,出现负元素,很多算法都有所限制,尤其和最优化相关; 二分查找的数据范围。一般不要使用((l+r) >> 1)这种找中点的方法,容易溢出。要使用Math.floor((l+r)/2); 栈、队列(优先队列)和哈希表 栈 栈是只能在一端进出,有”先进后出(FIFO)”特性的数据结构。 ★单调栈 单调栈:栈内的元素索引值单调递增,且元素值从底向顶也单调递增或单调递减。 单调栈同普通栈一样,元素只能在栈顶进出。 单调栈的目的是,随着原序列的遍历,维护一个局部最优的子序列。属于贪心算法的工具。 单调栈可以用来解决: 查找数组中每个元素下一个大于(或小于)自身的值; 按索引顺序,查找具有最大跨度的上升(下降)元素对; △查找最小或最大子序列。 有关用法2的详细解释,见。 找出最具竞争力的子序列 移掉K位数字 907. 子数组的最小值之和 ★962. 最大宽度坡 1124. 表现良好的最长时间段 队列 队列是”先进先出(FIFO)”的数据结构。 单调队列 单调队列:队列中元素的索引值单调递增,且队列中的元素值单调也递增(或递减)。 单调队列与普通队列不同,一般为双端队列。元素可以在两侧出队,而只能在队尾入队。 Out: Head <- [a,b,c,d] -> Tail; In: [a,b,c,d] <- Tail 单调队列在滑动窗口中应用,可以获取窗口内的最大、最小值。操作如下: 设置单调队列queue,初始值为空;(假设我们要保存的是窗口的最大值,queue为单调递减队列) 假设窗口左指针为left,右指针为right。当窗口扩大时,right向右移动,此时判断新加入窗口的元素arr[right+1]与queue队尾的元素大小关系: 如果arr[right+1] > queue[queue.length-1],说明有新的最大值加入,这里因为queue中的元素都比新加入的元素更靠前,因此在left到达right+1位置之前queue[queue.length-1]绝不可能再成为最大值,因此可以将它在队尾删除; 同理,删除后还需要对下一个队尾值进行比较删除操作,直到队空或队尾元素值大于等于新值,将新值从队尾加入队列;(等于的情况需要保留,因为之前加入的值仍然是最大值。) 这样就保证了单调队列的两个特性:索引和值都单调递增。 当窗口需要缩小,left向右移动,事项如下: 在窗口中不存在的元素,需要从队列中删除; 因为left <= right,当前要删除的left元素最先加入队列,因此它要么在队列的最前面,要么已经被后来的最大值淘汰,被从队尾删除掉; 此时只需判断队首元素是否是left对应的元素,如果是则将它从队首出队,如果不是则不用做任何操作。 每次窗口移动,队首的元素就是当前窗口内的最大值。 窗口中的最大值 绝对差不超过限制的最长连续子数组 哈希表 适用于:计数类问题。哈希表可以快速判断一个值是否出现在集合中,而避免了每次都要遍历查找。 缺点是空间复杂度高,是以空间换时间的方法。 哈希结构:数组、对象、Set、Map 哈希表计数去重 使用哈希表计数两数组合,如何能不重复? 给出一个数组arr,求数组中子序列 [a,b] 加和 a+b=target 的组合数。 使用暴力解法时间复杂度为O(n^2),可以使用两个嵌套for循环,通过索引来实现无重复计数。 使用一个哈希表map,可以实现O(n)复杂度的算法。但是如果采用先将数组元素添加到map统计数目,后遍历map计算组合数的方式,计算的组合数将包含重复情况: 例如: map = {1: 3, 2: 3} ,target = 3,遍历map时,会得到[1,2]+[2,1] 共计18种组合,实际上只有9种。 可以采用边统计,边计数的方式,避免这种重复计数的情形: 每次将元素i放入map前,先计算 map 中 target-i 的数目num,将其加入计数结果count; 将元素放入map,它对应在map中的数目+1. 这样做可以避免重复计数([a,b]和[b,a])的原因是: 如果a和b是一对待计入组合,那么按照这种依次加入的方式,他们加入map一定有先后顺序。我们只在二者中后加入map的一方加入map的时候计数。因为所有元素最终一定都被添加到map,他们之间两两存在先后顺序,也就是说如果存在任意组合需要被计入,只会在组合元素最后一次被添加的时候计数,也就是只会计数一次。 ST表 ST表的功能与复杂度 ST表(Sparse Table,稀疏表),是一种基于倍增思想的数据结构。 ST表经常用于解决高效查询区间最值问题(RMQ)。 ST表可以在O(n*log_n)时间复杂度进行建立,然后在O(1)时间复杂度内实现对[l,r]区间内最值的查询。 实际上,ST表可以解决的问题叫做可重复贡献性问题。 任何一个操作opt,如果x opt x === x,则opt叫做可重复贡献操作,也就是对同一参数多次执行,不影响计算结果。 ST表的原理与结构 ST表基于以下原理实现: 任何一个区间[l,r],都能找到两个长度len = 2^i (2^i ∈ [1, r-l+1])的子区间[l,a]和[b,r] (a,b在[l,r]区间内); 我们可以以2的幂为区间长度,预先计算出各个区间的最值; [l,r]区间内的整体最值结果,与分别对子区间[l,a]和[b,r]求最值,然后再联合求最值的结果相同。 ST表的处理过程: 预处理,建立ST表: 设d[i][j]为从i开始的,长度为2^j的闭区间[i, i+2^j-1]的最值结果; 使用动态规划,计算出原区间[l,r]的全部d[i][j]值; 对于区间[l,r],i的取值范围为[1,r],j的取值范围为[0, log(r-l+1)]。 查询[l,r]区间的最值: 将[l,r]区间分为两个子区间[l,a]和[b,r]。为了让子区间最大程度地重叠,子区间长度尽可能靠近r-l+1,因此len = 2^j的指数j选取为j = ⌊ log(r-l+1) ⌋; 获取d[l][j]和d[r-2^j+1][j]的值,取二者的最值即为结果。 ST表的实现 class ST { constructor(arr = []) { let JMAX = Math.floor(Math.log2(arr.length)) + 1; let d = new Array(arr.length).fill(0).map(i => new Array(JMAX).fill(0)); arr.forEach((i, idx) => { d[idx][0] = i; }); for (let j = 1; j <= JMAX; j++) { for (let i = 0; i + (1 << j) - 1 < arr.length; i++) { // [i, i+2^j-1] --> [i, i+2^(j-1)-1] + [i+2^(j-1), i+2^j-1] // d[i][j] --> d[i][j-1] + d[i+2^(j-1)][j-1]; // 动态规划:[i,j]用到j-1列的数据,因此计算顺序是先列后行。 d[i][j] = Math.max(d[i][j - 1], d[i + (1 << (j - 1))][j - 1]); // 这里执行对应的可重复贡献逻辑: max, min, gcd... } } this.d = d; } query(l, r) { let j = Math.floor(Math.log2(r - l + 1)); return Math.max(this.d[l][j], this.d[r - (1 << j) + 1][j]); } } 堆(优先队列) 堆相关 数组 差分数组 定义和适用情况 差分数组:对于一个源数组arr,差分数组diff[i]定义如下: diff[0] = 0; diff[i] = arr[i] - arr[i-1]; (i > 0) 差分数组主要用于对原数组子区间内元素,进行统一增减操作的情况,将区间内的统一变化,转化为两个端点的变化。 可以将数组操作转化为哈希表记录端点变化的操作,降低复杂度。 差分数组的性质 差分数组是什么 对于一个数组arr,计算它的差分数组diff,如果它的子区间[a,b]进行统一的增减n的操作,diff数组的变化如下: 统一增大n:diff[a]增大n,diff[b+1]减小n,其他不变; 统一减小n: diff[a]减小n,diff[b+1]增大n,其他不变; 也就是说,区间[a,b]同时增减操作,只影响差分数组的区间头位置a和尾位置+1位置b+1,diff[a]与变化方向相同,diff[b+1]与变化方向相反,变化值绝对值相同都为n。 从另一个角度考虑,在对区间[a,b]进行了统一增加n操作后,diff的[a,b]区间前缀和统一增加了n,而其他位置前缀和不变。 因为+n发生在diff[a]位置,-n发生在diff[b+1]位置,而中间的元素不变。 所以[a,b]区间内前缀和都统一增加了n,而到b+1位置恢复,b+1之后的前缀和没有变化。 差分数组的好处 差分数组将一个区间内的操作,转化为在两个端点的操作,省去了遍历整个区间的过程,减少了时间复杂度。 差分数组应用实例: 1893.区间是否被全覆盖 ★ 1674.使数组互补的最少操作次数 前缀和 对于一个数组arr的索引i,前缀和也就是求区间[0,i]中数组所有元素的和。 对于一个子数组(连续)的所有元素之和,等于它首尾位置前缀和之差。这有时可以用于降低复杂度。 和为K的子数组 子数组 子数组是一个数组arr中索引连续的元素组成的数组。 子树组类题目,一般情况是需要统计符合某种规则的子数组数目。 一个长度为n的数组,它的全部子数组数目为n *(n+1)/ 2,为n^2量级。因此当数据量较大(一般大于30000)时,暴力法无效。 当暴力法无效时,就需要一些特殊的判断技巧,以降低复杂度。这时可以观察题目要求规则的特点,枚举子数组中比较有特点的某个元素位置,从而寻找子数组数目。 一般有以下两种情况: 枚举子数组左、右边界:比如统计以某个字符作为结尾的子数组数目,我们枚举找到这个字符的位置,以它为结尾的子数组数目,等于它前面元素的数目n+1; 枚举子数组中某个特殊元素位置,可以依此将子数组分为左右两部分:比如约定了子数组的最大值、最小值区间范围时,我们直接枚举以某个元素为最大、最小值的子区间数目。 最值在某一区间范围[l,r]的子数组个数 现在给定我们一个区间[l,r],让我们求出最大(小)值落在这个区间的,arr的子数组的个数。 基于上节思路,现在再多考虑一些细节: 枚举哪个位置?如果我们选择从左到右遍历原数组,因为我们已遍历的区间在当前位置i的左侧,要实现O(n)的算法,只能认为我们当前枚举的位置是区间的右边界; **如何判断符合要求子区间的数目,以及取舍? 要实现O(n)的算法,我们每次只能考虑当前正在遍历的这个元素arr[i],利用它的值来判断以arr[i]为右边界的一系列区间的保留与舍弃**,因此要设定一个对当前元素的判定条件; 已经遍历的部分,我们记录符合条件的元素出现的长度len,以当前位置arr[i]为结尾的符合要求子数组个数,就等于len+1。(见上节图); 对于取区间最大值的问题,符合要求的元素为arr[i] <= MAX,我们必须保证计入len的元素都满足这一条件,因此我们将原区间[l,r]划分为[~,l-1]和[~,r]两个向左的区间差; 对于取区间最小值的问题,符合要求的元素为arr[i] >= MIN,我们必须保证计入len的元素都满足这一条件,因此我们将原区间[l,r]划分为[l,~]和[r+1,~]两个向左的区间差; 代码实现: // 求子数组最小值满足[l,r]的个数: function minValueBetweenCnt(arr, l ,r) { // [1,3,2,4,5,10,6,3,1,5,4,9,12,1] // [3,6] // min >= 3 && min <= 6 // (min >= 3) - (min >= 7) 区间取最小值,划分为两个向右的区间差 function cntMinValueLargerThan(e) { let cur = 0; let res = 0; for (let i of arr) { // 枚举子数组右边界位置 if (i >= e) cur += 1; // 当满足要求,连续出现的符合要求元素数 +1 else cur = 0; // 不满足要求,连续出现的符合要求元素数 归0 res += cur; } return res; } return cntMinValueLargerThan(l) - cntMinValueLargerThan(r+1); } // 求子数组最大值满足[l,r]的个数:(原理相同) function maxValueBetween(arr, l, r) { // [l,r] // [~,r] - [~,l-1] function cntMaxValueSmallerThan(e) { let res = 0; let cur = 0; for (let i of arr) { if (i > e) cur = 0; else cur += 1; res += cur; } return res; } return cntMaxValueSmallerThan(r) - cntMaxValueSmallerThan(l-1); } 动态前缀和:树状数组 树状数组(Binary Indexed Array) 树状数组(Binary Indexed Array,BIT)的功能,是求解某一数组的动态前缀和。 它巧妙地利用了数组索引的二进制表示信息,实现了在O(log_n)时间复杂度,实现对数组arr的任意位置前缀和的修改和查询操作。 同样,区间和的查询,相当于两次前缀和的查询,因此也可以在O(log_n)时间复杂度实现区间和的查询操作。 树状数组建立的过程,复杂度是O(n * log_n)。 动态前缀和有什么用? 动态前缀和可以随着原数组的遍历,对前缀数组进行动态修改,从而更快找到我们想要的信息。 315. 计算右侧小于当前元素的个数 2179. 统计数组中好三元组数目 树状数组的结构 树状数组的索引从1开始; 树状数组的每一个位置,都保存着原数组arr某一区间的加和; bit[i]具体保存的是哪一个区间的信息,与索引i有关; 从索引i二进制表示最末位开始,一直向前直到找到第一个1位,区间所表示的二进制数len = lowbit(i),就是bit[i]所表示的向前区间长度(包含当前位置); 某一位置i所表示区间的前一个区间元素位置:i - lowbit(i); 某一位置i所表示区间的上层覆盖区间的元素位置:i + lowbit(i)。 树状数组的功能 [1, i]区间前缀和的查询query(i); 原数组i位置数据的更新update(i, diff);(diff为更新的增量) lowbit(i),计算i位置的区间长度len; 树状数组的实现 // Mars 2022.02 class BIT { constructor(arr = []){ // arr[i] -> bit[i+1] this.bit = new Array(arr.length+1).fill(0); arr.forEach((i,idx) => this.update(idx, i)); } lowBit(i) { return i & (-i); } update(i, diff) { // update arr[i] with delta <diff>. let c = i + 1; while (c < this.bit.length) { // 不断向上寻找覆盖区间,并更新; this.bit[c] += diff; c += this.lowBit(c); } } query(i) { // query prefix sum of [0, i] if (i >= this.bit.length-1) { console.warn(`index exceed the limit.`); return; } let c = i + 1; let r = 0; while (c > 0) { // 不断向前寻找区间,并加和; r += this.bit[c]; c -= this.lowBit(c); } return r; } } 线段树 线段树将一个数组的区间计算信息,预先计算并保存在一个树状结构中,降低后续数组区间查询和更新数据的复杂度。 线段树可以在O(n*log_n)时间复杂度内建立,然后在O(log_n)的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。 线段树适用于需要频繁对数组进行区间信息查询,且频繁对数组进行更新的情况。 线段树的结构 线段树是二叉树形式的数据结构,可以用Node或数组形式表示; 线段树的每一个节点,代表原数组一个区间的信息。例如:节点A可以表示[0,3]区间内,原数组的元素加和结果; 线段树节点的子节点,记录父节点区间,从中间点拆分而成的子区间信息,中间点属于左子节点还是右子节点均可,可以自行约定。 例如:节点A表示[0,3]区间,我们约定取中间点为m = Math.floor((l+r)/2),且左子节点保存[left, m]的区间,右子节点保存[m+1, right]区间,那么A的左子节点保存[0,1]区间信息,右子节点保存[2,3]区间信息。 线段树的叶子节点,就是区间长度为1的结果,也就是保存了原数组的各个元素值。 constructor(arr = []) { this.arr = arr; this.tree = new Array(4 * this.arr.length).fill(null); this.build(); } 线段树的生成 build 使用递归的方式生成。 线段树需要的数组空间不好计算,根据渐进公式可以设其长度为4 * n(n为原数组长度); 与建堆一样,i的左子节点索引为2i+1,右子节点为2i+2; 递归函数需要记录的参数:当前树节点的索引idx,当前节点代表的区间左边界left,右边界right; 递归出口条件: left === right: 叶子节点,返回arr[left] ; left > right: 空节点,返回null。 函数体内,将区间按约定的中点分隔方式,分隔并递归调用即可。 build(idx = 0, start = 0, end = this.arr.length - 1) { if (start === end) { this.tree[idx] = arr[start]; return; } let leftIdx = 2 * idx + 1; let rightIdx = 2 * idx + 2; let m = Math.floor((start + end) / 2); // [start, m] [m+1, end] this.build(leftIdx, start, m); this.build(rightIdx, m + 1, end); this.tree[idx] = this.tree[leftIdx] + this.tree[rightIdx]; } 线段树的更新 update 更新arr[i] = val,只需要对线段树的arr[i]所处的叶子节点,及其各祖先节点进行更新即可。其他节点不受影响。 判断更新前后的值是否相同,相同则无需更新,直接结束; 计算要更新的差值,delta = val - arr[i]; 递归遍历线段树,判断更新的位置i是否位于[left, right]区间内,是则在当前节点的值上加上差值delta,并对子区间递归调用。不在区间内则直接返回; update(arrIdx, val) { if (val === this.arr[arrIdx]) return; let delta = val - this.arr[arrIdx]; this.arr[arrIdx] = val; // refresh arr self // refresh tree this._treeUpdate(arrIdx, delta); } _treeUpdate(arrIdx, delta, idx = 0, start = 0, end = this.arr.length - 1) { if (arrIdx < start || arrIdx > end) return; if (start === end) { this.tree[idx] += delta; return; } this.tree[idx] += delta; let leftIdx = 2 * idx + 1; let rightIdx = 2 * idx + 2; let m = Math.floor((start + end) / 2); this._treeUpdate(arrIdx, delta, leftIdx, start, m); this._treeUpdate(arrIdx, delta, rightIdx, m + 1, end); } 线段树的查询 query 对原数组某个子区间[l,r]内的结果进行查询: 分治思想,递归将原数组拆分,直到区间两端节点与线段树某一节点的两个端点相同,将线段树中结果加和计入结果中; 返回加和结果。 注意:这里在递归对区间进行拆分的时候,需要按照线段树节点区间的中点m位置来分情况讨论: 当m位于查询区间左侧,只需要对右子节点进行递归查询,且区间不用分割; 当m位于查询区间右侧,只需要对左子节点进行递归查询,且区间不用分割; 当m位于查询区间内部,需要将区间分割成[l, m]和[m+1, r],对左右子节点分别进行递归查询、求和。 query(start = 0, end = this.arr.length - 1) { if (start > end) { console.warn(`Start must be smaller than or equal to End.`); return null; } return this._query(start, end); } _query(qStart, qEnd, idx = 0, start = 0, end = this.arr.length - 1, res = [0]) { if (qStart === start && qEnd === end) { res[0] += this.tree[idx]; return res[0]; } let leftIdx = 2 * idx + 1; let rightIdx = 2 * idx + 2; let m = Math.floor((start + end) / 2); // [start, m] [m+1, end] if (m >= qEnd) { this._query(qStart, qEnd, leftIdx, start, m, res); } else if (m+1 <= qStart) { this._query(qStart, qEnd, rightIdx, m+1, end, res); } else { this._query(qStart, m, leftIdx, start, m, res); this._query(m+1, qEnd, rightIdx, m+1, end, res); } return res[0]; } 哈希数组 1146. 快照数组 哈希数组,数组的每一个元素为一个哈希表,适用于需要保存数组元素各个变化状态的情况。 数组元素arr[i]每次变化,将变化存储在i对应位置的哈希表中,需要时从哈希表中取出对应状态的值即可。这样没有变化的元素不会被额外存储,造成空间浪费。 树:二叉树、BST、Trie 二叉树 Binary Tree 二叉树的特点 二叉树是一种特殊的图,可以将二叉树看成有向图: 除了根节点外,它的每个节点入度都是1; 除了叶子节点外,它的每个节点出度都为2; 全部节点的入度加和,等于出度加和。 二叉树的前、中、后序遍历 二叉树前中后序遍历的迭代的统一写法思路: 使用一个栈stk来保存遍历的路径,使用Res数组记录结果; 因为栈的入栈与出栈顺序相反,所以入栈顺序需要与前、中、后序遍历的顺序相反(前序: 中→左→右,入栈: 右→左→中); 只有在中间元素出栈的时候才记录结果到res数组; 在中间元素后面添加一个null元素,作为它出现在中间位且没被res数组记录的标志,一旦在出栈过程中出现null,说明此时应该向res添加记录下一个元素; 直到栈stk为空,结束迭代。 例如: // 后序遍历: // 遍历:左右中 // 入栈:中右左 function postOrderIterate(root){ let stk = [root]; let res = []; while(stk.length !== 0){ let cur = stk.pop(); if(cur === null){ res.push(stk.pop().value); continue; } stk.push(cur, null); // 中 (记录标志为null) if(cur.right) stk.push(cur.right); // 右 if(cur.left) stk.push(cur.left); // 左 } return res; } 二叉树的序列化与反序列化 什么是序列化 序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。 通俗地讲,序列化就是将一种数据结构,转化为字符串等不同环境通用的中间格式的过程。需要注意的是,序列化后的结果,在另一个执行环境中进行复原后(反序列化),其复原结果必须与原数据一致,没有歧义。 二叉树该如何序列化 如果树中不存在重复的元素,那么知道了二叉树的三种遍历结果中的任意两种,即可还原出原有的树结构。 如果存在重复元素,则需要对二叉树的空节点也进行输出,用一个特殊符号表示(将二叉树转化为扩充二叉树),然后进行先序或后序遍历(中序无法确定根节点位置,故不行)。 扩充二叉树的先序、后序遍历结果,与二叉树的节点是一一对应的,因此恢复的结果也是唯一的。 二叉树的反序列化 如何对含空节点标记的二叉树的先序遍历序列化结果,进行反序列化(复原)? 以'1234###5###'为例: 先序遍历结果以中-左-右顺序出现,所以最左侧的值一定是当前根节点的值; 先序遍历会沿着一条路径一直向左探索,直到遇见叶子节点; 我们用一个栈stk保存路径,每次从序列拿到新的节点值val,我们根据val的值新建一个节点Node(可能是空节点null): 如果栈顶节点top的左子节点为空,则将val填充到top的左子节点; 如果栈顶节点top的左子节点不为空,则将val填充到top的右子节点; 如果当前值val不是代表空节点的占位符#,则将新插入的节点入栈,记录下来; 循环判断栈顶元素的左右子节点是否都被填充过,如果是,则对栈顶元素进行出栈,以保证栈顶是下一个待填充的节点; 最后返回根节点即可。 function _buildFromPreorderSerialzation(str) { // Mars 2022.02 if (str.length === 1 && str[0] === '#') return null; str = str.split(''); let res = new Node(+str[0], '$', '$'); let stk = [res]; for (let i = 1; i < str.length; i++) { let cur = str[i] === '#' ? null : new Node(+str[i], '$', '$'); if (stk[stk.length - 1].left === '$') stk[stk.length - 1].left = cur; else stk[stk.length - 1].right = cur; if (cur !== null) stk.push(cur); while (stk.length > 0 && stk[stk.length - 1].right !== '$' && stk[stk.length - 1].left !== '$') stk.pop(); } return res; } 字典树 Trie 字典树由字符串生成,形式是嵌套的哈希表形成的树结构。 字典树以输入字符串的每位字母为键名, 字典树可以用于文本推断、自动填充等场景。 时间复杂度: 构建、插入: O(n) 查询: O(n) 空间复杂度: 构建、插入: O(n) 查询: O(1) 构建字典树:前缀、后缀 构建前缀、后缀字典树Trie的步骤: 初始化:设置根节点为一个空哈希表(Map或空对象),也可以在内部设置各种结尾的标志符; 插入操作:从插入字符串的0位置(前缀)或末尾位置(后缀)开始,向后(前)遍历,设置初始哈希表cur为根节点,对于每一位的字符letter,执行如下操作: ① 在当前哈希表中查找letter键名,如果不存在,则在cur中为letter创建一个新哈希表; ② 将cur指向letter对应的哈希表,在letter哈希表设置对应的结束标志为true(如果构建前缀树,每步都设置前缀结束标志为true,最后词尾设置词汇结束标志true); ③ 继续遍历,直到字符串尽头,完成插入; 查询操作:从插入字符串的0位置(前缀)或末尾位置(后缀)开始,向后(前)遍历,设置初始哈希表cur为根节点,对于被查询字符串query每一位的字符letter,执行如下操作: ① 在当前哈希表中查找letter键名,如果不存在,则说明没有此查询字符串,返回false结束; ② 将cur指向letter对应的哈希表,继续查询; ③ query全部查询完毕后,判断此时cur中各结束标志是否为true(比如查询前缀query,那么此时前缀结束标志应该是True),如果为true则返回ture,否则返回false。 并查集 定义 并查集是一种抽象数据类型,它操作一组互不相交的集合,可进行高效集合合并和查询两个元素是否在同一集合的操作。 并查集解决的是图的动态连通性问题。即在动态过程中,判断一个图中存在几个连通分量。 应用 判断一个图中连通分量的个数; 基本思路 一个集合用一个代表元素来表示,称为代表元。判断两个集合是否相同,被简化为判断两个集合的代表元是否相同; 一个集合被组织成一个树形结构,代表元是这个树的根元素; 集合中的每个元素x,具有根据x本身访问它父元素的途径parent(x); 根元素的parent,指向它自己,也就是parent(x) = x; 查找一个元素x的代表元,只需要不断迭代查询元素的parent(x),即可最终找到集合的根元素; 并查集的方法 初始化:每个元素的父节点各自指向自身,因为它们独立组成集合,还没有合并; 查询:两个元素是否在同一集合中; 合并:将两个集合合并成一个; 获取集合中子集合个数:获取当前并查集中子集合的个数。 并查集的优化 记录集合高度 对于每个集合,记录集合树的高度rank,在合并的时候,将较小rank的集合连接到较大rank的集合树上,可以减小合并后树的高度。 路径压缩 对于每次查询,如果查询到元素a所在集合的根元素是root_A,那么直接把a连接在root_A上,这样下次查询可以省去多级遍历的过程。 路径压缩后,集合树的高度可能有所变化,但是一般为简单起见,路径压缩后树的高度rank保持不变。 并查集的实现 可以用数组实现,也可以用哈希表。 用数组实现,是每个数组位置代表一个元素,同时定义parent数组和rank数组,记录每个位置的元素的父元素和它为根节点树的高度。 用哈希表实现,是每个元素的值作为键名,键值为一个对象,将parent和rank储存在其中。 // HashMap Implementation class BingChaJi { constructor(vals = []) { this.map = new Map(); for (let v of vals) { this.map.set(v, { rank: 1, parent: v, }); } } findParent(v) { let m = this.map; let p = m.get(v)['parent']; if (p === v) return v; else { let root = this.findParent(p); this.map.get(v).parent = root; console.log(`${v}'s parent is set to ${root}`); return root; } } isSameSet(v1, v2) { let root1 = this.findParent(v1); let root2 = this.findParent(v2); return root1 === root2; } merge(v1, v2) { let root1 = this.findParent(v1); let root2 = this.findParent(v2); if (root1 === root2) return; let r1 = this.map.get(root1).rank; let r2 = this.map.get(root2).rank; if (r1 < r2) { this.map.get(root1).parent = root2; console.log(`root is ${root2}`); } else { this.map.get(root2).parent = root1; console.log(`root is ${root1}`); } if (r1 === r2) { this.map.get(root1).rank += 1; console.log(`rank is set to ${this.map.get(root1).rank}`); } } } 图 DFS深度优先搜索 记忆化DFS搜索 记忆化DFS搜索,就是在DFS搜索的基础上,添加一个哈希表(或数组),用于保存计算过的结果(备忘录)。 当备忘录中存在结果时,直接返回。否则再进行计算+存储。 记忆化搜索是以空间换时间,可以降低时间复杂度。 638. 大礼包 基本思路: 先对原大礼包数组进行筛选,不划算的或礼包中某一物品数量多于所需数量的,都要筛除; 搜索从当前需求need = needs开始,对于每一个need数组,它都对应着一个最低购买价格,我们把求出这个最低价格的函数叫做getMinPrice(),则对于需求need的最低价格为min = getMinPrice(need); 下面是getMinPrice()函数的实现: 最坏情况是,我们的需求,一个大礼包也买不了,这时需要原价买入所有的物品,假设此时价格为originalPrice; 对于每一个大礼包,我们现在已知买入它们都是划算的(因为筛选过,但它们划算程度不同)。我们要找到所有大礼包中,买入后能满足当前需求,且总价格最低的; 对于每一个大礼包,如果礼包中各物品的数量,都≤当前需求物品数量,则大礼包可以买入。反之,则不能买入。 我们遍历所有大礼包,对于一个大礼包s,它内部物品数目列表为sn,如果它可以买入,那么它买入后,下次我们的需求就变成了need - sn(数组各位分别减去对应物品数目)。那么它买入后需求的最低购入价格就是getMinPrice([need - sn]),因此,买了这个礼包后,我们的购入价格是price_need = getMinPrice([need - sn]) + price_s; 遍历所有大礼包,对每一个能买入的礼包s,都计算price_need = getMinPrice([need - sn]) + price_s,则达成当前需求need的最低费用是: 原价购买价格originalPrice与所有price_need 中的最小值; 每次找到当前need的最低购入价格,就把它放在备忘录map中,键名是当前need形成的字符串need.join('-'),键值是价格; 如果当前need的最低购入价格在备忘录中存在,则直接返回,省去计算步骤; 要找的结果是最后备忘录中初始需求needs对应的价格。 BFS广度优先搜索 广度优先搜索从几个节点出发,这些节点本身组成一层。他们可到达的节点组成新的一层,在本层遍历完成后遍历,依次这样分层遍历,直到全部遍历完成。 广度优先搜索一般借助一个队列q来实现,每次从队列头部取出一个元素n,然后把这个元素n的下一层元素添加到队列末尾,这样可以保证元素按序分层遍历。 地图分析 JS广度优先搜索的优化 因为JS中没有标准的队列数据类型,需要用数组的shift()进行模拟,而数组的shift()是一个比较费时的操作,经常造成TLE。 解决方法是: 每一层用一个next数组存放下一层的结点,将q中的元素一一pop()出来,然后将下一层的元素push()到next数组,最后用next代替q。 这样就避免了shift()操作造成超时。(但是会造成搜索顺序变化) 地图中的最高点 // template let q = [init]; while (q.length) { let next = []; // 新next数组 while (q.length) { let cur = q.pop(); // 从原队列pop() let n = cur.next; // 找到下一层结点 next.push(n); // push到next } q = next; // 用next代替q } 拓扑排序 OI Wiki拓扑排序 定义 拓扑排序要解决的问题,是给一个有向无环图(DAG:directed acyclic graph)的所有节点进行一维排序,使得对于存在的任何u到v的有向边u -> v, 都可以保证u在v的前面。 实现 拓扑排序的实现,一般有两种形式:Kahn算法 和 DFS。 其区别是: Kahn算法:从入度为0的节点开始,正序记录(在入度为0的时候记录数据); DFS:在出度为0的时候记录数据,记录顺序为逆序。 Kahn 算法 Kahn算法基本过程: 遍历有向无环图,记录每个节点的下一相邻节点集合,以及节点的入度(进入该节点的边数); 遍历所有节点,找到初始入度为0的节点,组成初始队列q; 每次从队列q中出列一个节点n,然后执行如下操作: 将n添加到结果数组res尾部; 找到n全部的相邻下一节点t,将t的入度-1(等同于将n从图中删除); 如果t的入度此时为0,则将t加入队列q尾部; 直到队列q为空,停止遍历,res为拓扑排序结果。 时间复杂度:O(N+E) (N为节点总数、E为边总数) function kahnSort(g) { let entryNum = new Map(); let tos = new Map(); for (let [f, t] of g) { if (!entryNum.has(f)) entryNum.set(f, 0); if (!entryNum.has(t)) entryNum.set(t, 0); entryNum.set(t, entryNum.get(t) + 1); if (!tos.has(f)) tos.set(f, new Set()); tos.get(f).add(t); } let q = []; let res = []; for (let [k,v] of entryNum.entries()) { if (v === 0) q.push(k); } while (q.length) { let i = q.shift(); res.push(i); if (tos.has(i)) { for (let t of tos.get(i)) { entryNum.set(t, entryNum.get(t)-1); if (entryNum.get(t) === 0) q.push(t); } } } return res; } DFS 实现拓扑排序 DFS实现拓扑排序基本过程: 用一个数组visited,标记节点是否被记录; 遍历图,记录每一节点n的下一相邻节点集合,集合的元素数目就是n的出度; 从任意节点开始,遍历全部节点,对任一节点n,如果其未被遍历: 如果出度为0,则pushn到结果数组res; 如果出度不为0,则递归对每一相邻下一节点t执行dfs(t); 记录visited[n]为true; 执行完毕,res为倒序的拓扑遍历结果。 function dfsSort(g) { let m = new Map(); // 记录下一邻接节点集,以及出度 let nodes = new Set(); // 记录全部节点集合 for (let [f,t] of g) { if (!m.has(f)) m.set(f, new Set()); m.get(f).add(t); nodes.add(f); nodes.add(t); } let res = []; let visited = new Set(); // 已访问节点集合 function dfs(n) { if (!visited.has(n)) { visited.add(n); if (!m.has(n)) { res.push(n); return; } for (let c of m.get(n)) { dfs(c); } res.push(n); } } for (let i of nodes) { dfs(i); } return res.reverse(); } 特殊的图结构 二分图 一个图的节点能够被分成两组,并且每条边连接的两个顶点,都分别属于不同的组,那么这个图叫做二分图。 另一个等价说法:二分图是一个不包含由奇数条边组成的环的图。 最小生成树 什么是最小生成树 无向连通图的最小生成树(Minimum Spanning Tree,MST),为边权和最小的生成树。 一个由n个节点构成的图,它的最小生成树的边数为n-1。 最小生成树:Kruskal算法 Kruskal算法属于贪心算法。它的流程如下: 用并查集维护节点的连通性关系; 维护已添加的边的权重加和sum,以及添加的边数num; 从所有边中,选出权值最小的边a -- b,查看a和b是否在同一个连通分量中: 如果不在,则将这个边的权值计入结果sum,并将边数num += 1; 如果在同一个连通分量,则跳过,寻找下一个权值最小边长; 直到添加了n-1条边,结束返回。 动态规划(Dynamic Programming) 动态规划是一种思想,将问题分解为一个个状态,利用记忆化的方法储存计算过状态的结果,并利用现有状态结果转移计算新状态的方法。 因此要成功应用动态规划,有几个重要步骤: 区分操作和状态:操作是对状态进行转移的动作。首先要区分题目中哪些是可进行的操作,哪些是问题的状态; 定义状态:找到问题所描述的解的状态,从而确定定义一个什么样的状态,可以由状态转移逐步推导出问题的解; 寻找状态变量:找寻合适的参数变量集合,使得一组确定的参数变量,可以确定问题唯一的状态; 给定初始状态或状态的边界: 无法再分割的最小状态,往往对应着单一且显而易见的结果,直接将结果赋值,然后从这些边界状态开始递推; 找到状态转移方式: 找到由旧状态递推新状态的方法。 以上都成功应用的话,找到解只是向目标状态进行转移的过程,最终返回解所在的状态结果就可以了。 背包(零钱)问题 背包问题有两种形式: ① 0-1背包问题: 背包内的东西只能取一次; ② 完全背包问题: 背包内的东西是无限的。 0-1背包问题: 加减的目标值 1049. 最后一块石头的重量 II 完全背包问题:零钱问题2 0-1背包问题 详解0-1背包问题 主要思想是建立一个二维DP数组,行坐标代表物品集合区间,列坐标代表背包的剩余容量。 dp[r][c]的含义是: 背包剩余容量为c的情况下,仅由[0,r]中的物品组成,能装下的最大价值。 这样对于一个[r,c]组合,有两种选择: 选择装下这个物品r:假设物品r本身重量为wr,价值为vr,那么背包剩余容量为c-wr,物品r已经装入,只能从[0,r-1]物品区间继续装入,此时能装下的最大价值为dp[r-1][c-wr]。 因此这种情况的最大价值为vr+dp[r-1][c-wr]; 放弃装这个物品r:最大价值与r-1个物品时一样。为dp[r-1][c]。 每次选二者之中较大者。 因为每次值只用到了r-1和r两行数据,可以对dp数组进行压缩。 最大可以压缩为一行。每次需要从右到左更新数据。(见上面的详解最下方。) 完全背包问题 0-1背包问题中,每个物品只能选择一次。而完全背包问题中,每个物品的数目是无限的,可以重复选择。 经典完全背包问题:凑零钱。给出零钱种类和目标值,求组成目标值的方法数。(比如由1,2,5组成10元有几种方法) 对于完全背包问题,存在一个for循环先后顺序导致的,排列数和组合数的问题。 详细说明: 动态规划解题,可以想到将钱数amount和钞票面值note联系起来: 组成一个目标值amount的方法数,等于所有钞票面值note对应的amount-note的方法数加在一起。dp[amount] = sum( dp[amount-note] ) 比如: 如果给出钞票[3,10],那么组成目标钱数20的方法数,应该等于组成20-3=17和20-10=10的方法数之和。因为组成17的每一个方法,再加上一张3元的钞票,就都可以组成目标值20,另一个例子同理。 可以用两层for循环分别遍历amount数组和notes数组,动态更新amount数组的值: amount[i] += amount[i-note]。 但是这里存在一个遍历顺序问题: ① 如果先遍历amount,后遍历notes,则意味着:对于每个目标值,都直接穷举出所有当前可能的钞票组合,之后再进入下一个目标值进行更新。 ② 如果先遍历notes,后遍历amount,则意味着:对于每个钞票note,先计算并更新单独由这个note组成各个目标值的方法数,然后再进入下一个面额note,再更新这个新面额和之前所有面额组成的方法数,保证了每个方案中的钱的排布顺序,是notes的顺序。 对于情况①,计算出来的是所有的无重复排列数。对于情况②,计算出来的是无重复组合数。 本题不同排列同一组合,视为同一个方法,因此应该使用第二个遍历方式。 股票买卖 股票买卖 给你一个每日股票价格组成的数组prices(索引是日期,值是价格),和一个可交易次数k。编写返回可实现的最大收益的函数。 股票同时期最多只能持有一份,一次也只能交易一份。也就是如果持有,必须先卖出才能再买入。 思路分析: 一天可以有三种选择:买入、卖出、不操作; 只有卖出才能获得收益; 卖出必须先买入; 如果不操作,则总收益和之前一天的收益相同。 基于以上四点,可以得出下面的算法: 对于某一天,只关注卖出操作。我们可以选择卖出或不卖出。 如果卖出, 则之前必定有一天进行了买入。此次交易赚取收益为price[sell]-price[buy]; 如果不卖出,则总收益与前一天收益相同; 设置一个矩阵,每一行代表最大可交易次数k,每一列代表交易日期day。每一个元素代表最大交易次数为k时,当前日期所能达到的最大收益。 则有如下关系: function maxProfitWithKTransactions(prices, k) { if(prices.length < 2) return 0; let dp = new Array(2); dp[0] = new Array(prices.length) .fill(0); dp[1] = new Array(prices.length); let trans = 1; while(trans <= k){ for(let i=0; i<prices.length; i++){ if(i === 0) dp[1][i] = 0; else { let notSellMaxProfit = dp[1][i-1]; let sellMaxProfit = -Infinity; for(let buyDay=i-1; buyDay>=0; buyDay--){ sellMaxProfit = Math.max(sellMaxProfit, prices[i]-prices[buyDay]+dp[0][buyDay]); } dp[1][i] = Math.max(notSellMaxProfit, sellMaxProfit); } } dp[0] = dp[1]; dp[1] = new Array(prices.length); trans++; } return dp[0][prices.length-1]; } 三个无重叠子数组最大和 三个无重叠子数组最大和 使用动态规划方法解题。步骤如下: 先遍历数组,用一个map记录以i位置为结束位置的长度为k的子数组的加和,i<k-1的值都为0,因为元素数目不够。(map.get(i)是[i-k+1, i]区间的加和); 使用一个二维dp数组保存计算结果。 dp的行n取值从0到3,代表可以选取的不相交子数组个数; dp的列i取值从0到nums.length-1,代表结束位置; 因此,dp[n][i]代表可以取n个子数组时,[0,i]区间内可以取到的n个子数组的最大和。 当n=0时,dp[n][i]全部为0;其他情况下,当进行dp[n][i]的取值时,分为以下两种情况: 不使用当前位置i构成的子数组map.get(i):dp[n][i] = dp[n][i-1] 使用当前位置i构成的子数组map.get(i): 则子数组[i-k+1,i]一定被使用了,当前dp值应该为 dp[n][i] = dp[n-1][i-k] + map.get(i) 当n=3,保留结果中的最大值和它对应的索引值index; 反向查找dp表,获得各子数组起始索引: 对于一个值dp[n][i],如果它使用了i位置的子数组,那么dp[n][i]一定大于dp[n][i-1]; 从n=3开始找,一直到n=1,保存结果值返回即可。 var maxSumOfThreeSubarrays = function(nums, k) { let map = new Map(); let first = 0; for(let i=0; i<k; i++){ first += nums[i]; map.set(i,0); } map.set(k-1, first); for(let i=k; i<nums.length; i++){ let cur = nums[i]; let prev = nums[i-k]; let sum = map.get(i-1) + cur - prev; map.set(i, sum); } let dp = new Array(4).fill(0).map(i => new Array(nums.length).fill(0)); let max = -Infinity; let index = 0; for(let n=1; n<4; n++){ for(let i=0; i<nums.length; i++){ let notUsed = i-1 >= 0 ? dp[n][i-1] : 0; let used = i-k >= 0 ? map.get(i) + dp[n-1][i-k] : map.get(i); dp[n][i] = Math.max(notUsed, used); if(n === 3 && dp[n][i] > max){ max = dp[n][i]; index = i; } } } function findIndex(i){ let n = 3; let res = [0,0,0]; while(n > 0){ while(i >= 0 && dp[n][i] === dp[n][i-1]){ i--; } res[n-1] = i-k+1; i = i-k; n--; } return res; } return findIndex(index); }; 最长回文子序列 Leetcode516: 最长回文子序列 思路: 回文序列首尾元素一定相同; 设置一个二维dp数组,dp[i][j]代表从[i,j]区间的最长子序列长度; dp[i][j]递推根据首尾元素是否相同判断: 如果首尾元素相同,则dp[i][j] = dp[i+1][j-1] + 2; 如果首尾元素不同,则它们必定不能同时作为最长子序列的首尾,dp[i][j] = max (dp[i+1][j], dp[i][j-1])。 需要注意dp遍历顺序问题,如果j从0到字符串末尾,那么i需要从j-1递减到0,这样才能正确利用之前的计算结果。 从1到n的BST的种类数 不同的二叉搜索树 给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。 思路: 对于一个数字n,那么从1到n,每一个数都可以选取作为根节点; 根据BST的性质,如果选择了i作为根节点,那么: [1,i-1]的元素都在左子树; [i+1,n]的元素都在右子树。 根据BST的规则,[1,n]的BST种类和[1+K,n+K]的BST种类相同; 设dp[i]为[1,i]区间可构建的BST种类数,那么: [1,i-1]的元素都在左子树,构建的BST种类数为dp[i-1]; [i+1,n]的元素都在右子树,构建的BST种类数为dp[n-i]。 对于一个根节点,总种类数为左右子树种类数相乘,则有状态转移方程:dp[i] = dp[i-1] * dp[n-i]; 从1到n遍历n,对每一个n遍历i = [1,n]选取作为根节点,计算其种类数加和做为dp[n]; 返回dp[n]即可。 区间DP 状态与区间相关,且可以由区间的切分等进行状态转移。 区间DP的基本流程是: dp[l][r]代表闭区间[l,r]范围内的结果; 对[l,r]区间进行某种拆分,常见的是在[l+1,r-1]区间枚举切分点i,将区间分为[l,i]和[i,r]两部分,并对每个切分点结果进行比较取最值,记录到dp[l][r]; 注意区间DP的顺序问题:因为大区间要用到小区间的信息,所以遍历的时候要先以区间长度len为条件(从小到大),再枚举区间的起点l(从小到大、从大到小均可),r的位置此时可以确定为l+len; 1039. 多边形三角剖分的最低得分 假设dp[l][r]为[l,r]区间切分的最小得分,那么整体的最小得分就是dp[0][len-1]; 在[l+1,r-1]区间内可以枚举切分点,将区间分为[l,i]和[i,r]两部分; dp[l][r] = min(dp[l][i] + dp[i][r] + v[l]*v[r]*v[i]); var minScoreTriangulation = function(values) { // 区间DP // dp[l][r]: [l,r] 区间的最低分。 // dp[l][r] = min(v[l]*v[r]*v[k] + dp[l][k] + dp[k][r]), k -> [l+1, r-1]; let dp = new Array(values.length).fill(0).map(i => new Array(values.length).fill(Infinity)); for (let len = 1; len <= values.length-1; len += 1) { for (let l=0; l+len<values.length; l++) { if (len === 1) { dp[l][l+len] = 0; continue; } for (let i=l+1; i<=l+len-1; i++) { dp[l][l+len] = Math.min(dp[l][l+len], values[l]*values[l+len]*values[i] + dp[l][i] + dp[i][l+len]); } } } return dp[0][values.length-1]; }; 贪心算法 分治算法 平面最近点对 给出一系列二维平面内的点[p1,p2,p3,p4...],每个点由[x,y]坐标构成,请寻找最近的一对坐标点之间的欧氏距离。 思路 直接暴力求解,问题复杂度为O(n^2),复杂度高的原因是进行了大量的无意义计算(比如相隔非常远的点间距离); 可以用一条线将平面分割成两部分(假定选择垂直于X轴的竖线),发现问题可以被分为子问题: 左边部分点集的最小距离l_min; 右边部分点集的最小距离r_min; 横跨左右点集的最小距离m_min; 对于任何一个点集的最终结果,都是上述三个子区间各自结果中的最小值min(l_min, r_min, m_min); 采用分治算法: 先对点集进行按x坐标从小到大排序; 递归出口:如果区间内的元素小于3个,直接进行计算返回; 按上述划分法,对每一个区间[l,r],取中间点m = Math.floor((l+r)/2);,分割成左右两部分; 对左右两部分分别递归求最小距离,假设结果为l_min和r_min,我们取它们二者中的最小值为delta; 对m位置的左右两侧,只对± delta范围内的元素进行求距离比对,找出这个范围内的最小距离m_min; 返回三者中的最小值。 实现 function getDist(p1, p2) { return Math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2); } function closestPair(points, l = 0, r = points.length - 1) { if (r - l <= 2) { if (r - l === 1) return getDist(points[r], points[l]); let a = getDist(points[l], points[l + 1]); let b = getDist(points[l + 1], points[r]); let c = getDist(points[l], points[r]); return Math.min(a, b, c); } let m = Math.floor((l + r) / 2); let lmin = closestPair(points, l, m); let rmin = closestPair(points, m+1, r); let delta = Math.min(lmin, rmin); // +- delta let leftSet = new Set(); let rightSet = new Set(); let c = m; while (c >= 0 && Math.abs(points[c][0] - points[m][0]) <= delta) { leftSet.add(points[c]); c -= 1; } c = m + 1; while (c < points.length && Math.abs(points[c][0] - points[m][0]) <= delta) { rightSet.add(points[c]); c += 1; } for (let p1 of leftSet) { for (let p2 of rightSet) { lmin = Math.min(lmin, getDist(p1, p2)); } } return Math.min(lmin, rmin); } // test let p = [[1,2],[2,1],[3,200]]; p.sort((a,b) => a[0] - b[0]); console.log(closestPair(p)); // 1.414.. 为运算表达式设计优先级 241. 为运算表达式设计优先级 采用分治算法+dfs: 设置一个dfs(l,r)函数,用来计算原表达式从l到r闭区间内子表达式的计算结果; 原表达式可以从任一操作符一分为二,可知左右两侧仍是一个表达式; 对两侧表达式重复上述分割步骤,直到两侧为不含操作符的数字; 每一个表达式因计算优先级的不同,可能有多种计算结果,因此返回的应该是一个数组,里面保存了所有表达式可能的计算结果; 每次用当前运算符,对左右两个表达式的计算结果进行遍历计算,合并结果集,最终返回整个表达式的结果。 var diffWaysToCompute = function(expression) { // 测试字符串是否是纯数字 function isNumber(str) { return /^\d+$/i.test(str); } // 执行单次计算 function cal(l, r, o) { switch (o) { case '+': { return l+r; } case '-': { return l-r; } case '*': { return l*r; } } } // 按操作符分隔表达式,取结果集合。 function dfs(l = 0, r = expression.length-1) { if (isNumber(expression.slice(l,r+1))) { return [+expression.slice(l,r+1)]; } let res = []; for (let i=l; i<=r; i++) { if (!isNumber(expression[i])) { let left = dfs(l, i-1); let right = dfs(i+1, r); let operater = expression[i]; for (let i of left) { for (let j of right) { res.push(cal(i, j, operater)); } } } } return res; } return dfs(); }; 回溯算法 算法基本思路 回溯算法是遍历决策树的过程。决策过程可以表示为当前路径、当前决策空间和结束条件。 当前路径:是由已经进行过的所有决策组成的。下一步决策依赖于当前走过的决策路径。(比如:有三张牌ABC,第一次选择B,第二次选择C,则当前第三次选择的路径为B-C); 当前决策空间:根据当前路径,本次决策剩余的选择空间。(ABC三张牌,前两次选择BC,则第三次当前选择空间只有一张牌A); 结束条件:决策空间中没有任何选项时,则代表决策树遍历到尽头,需要用结束条件处理这次决策的结果。(比如将当前决策路径B-C-A保存起来) 回溯算法一般是基于递归调用实现,递归主要负责实现决策树的遍历。回溯算法的主要过程如下: 先判断当前路径是否满足结束条件,满足则执行结束操作; 在当前决策空间B内进行决策,然后在当前路径A中添加当前决策(结果为A1),在当前决策空间B中去掉当前决策(结果为B1); 以A1为下一决策的路径,以B1为下一决策的决策空间,递归调用自身; 调用完成后,将决策路径B1恢复为B,路径A1也恢复为A。(这一步称为回溯过程) 回溯算法的应用:DFS深度优先搜索、全排列问题、组合问题等。 注意: 理论上使用回溯算法,执行决策后的回溯过程(恢复空间),应该与决策前的顺序正好相反,代码在结构上完全镜像对称。否则容易出现Bug。 回溯的注意事项 回溯算法复杂度高(O(n!)),一般数据量要小于20; 回溯算法本质上是递归,一般都可以应用记忆化方法,记录计算结果,防止重复计算; 回溯计算过程中,如果找到了想要的结果,要及时提前退出递归,终止后续计算。 回溯算法:全排列问题 元素互不相同的全排列 给定一个由字母组成的数组arr,数组中元素互不相同,请返回由全部数组元素组成的所有排列。 例如: ['a','b','c'] 的全排列是[['a','b','c'],['a','c','b'],['b','a','c'],['b','c','a'],['c','b','a'],['c','a','b']] 考虑使用回溯算法: 当前路径就是已经选择过的字母序列,当前决策空间就是剩余的可被选择的字母集合; 每次决策,在剩余字母中选择一个(比如’a’),添加到路径中,然后在可选择字母集合中删除掉这个已经选过的字母a; 使用新路径和新决策空间,递归调用执行下一次决策; 结束条件:当可选择字母集合为空,说明排列已经完成,将结果添加到结果集合,然后返回; 递归调用完成后,恢复路径(删掉添加的字母a)和决策空间(恢复字母a)。 代码如下: // 方法1: function getArrangement(arr, path=[], res=[]){ // arr是当前决策空间,path是当前路径 // 1.判断结束条件:决策空间内无元素可选; if(arr.length === 0){ res.push(path.slice()); return; } for(let i=0; i<arr.length; i++){ // 2.进行决策; let selected = arr[i]; // 3. 修改决策空间和路径,用来进行下一次决策; path.push(selected); arr.splice(i, 1); // 4. 递归调用,执行下一次决策; getArrangement(arr, path, res); // 5. 恢复 决策空间 和 路径。 arr.splice(i, 0, selected); path.pop(); } return res; } // 方法2: 使用一个used记录选择的路径 function allArrangement2(arr){ let path = [], used = new Array(arr.length).fill(false); let res = []; function backtrack(){ if(path.length === arr.length){ res.push(path.slice()); return; } for(let i=0; i<arr.length; i++) { if (!used[i]) { path.push(arr[i]); used[i] = true; backtrack(); used[i] = false; path.pop(); } } } backtrack(); return res; } 元素存在重复的全排列 元素存在重复的全排列 当给定数组中存在重复元素时,进行全排列会出现重复结果。 比如: [1,1,2,3]的全排列中,[1,1,2,3]本身会出现两次,因为存在两个1。 解决方法是:在相同决策空间进行选择的时候,进行一个判断。如果前后两次选择的元素值相同,则后续排列必重复,直接跳过此次选择即可。 例如在某次决策过程中,决策空间为[1,1,2,3],选择从左到右进行。本轮选择为第一个1元素,则下一次决策的空间为[1,2,3]。 本轮回溯过程执行完毕后,下次决策选择了第二个元素,此时发现同样为1元素,剩余空间还是[1,2,3]。那么这两次决策结果一定相互重复,第二轮直接跳过即可。 function backtrack(path=[], space=nums, res=[]){ if(space.length === 0){ res.push(path.slice()); return; } // 为每一轮回溯决策过程,设置一个Set,用来保存已经选择过的元素值。 let set = new Set(); for(let i=0; i<space.length; i++){ // 当该决策空间中已经选取过当前元素,直接跳过,防止重复。 if(set.has(space[i])) continue; else { set.add(space[i]); let cur = space[i]; path.push(cur); space.splice(i,1); backtrack(path, space, res); space.splice(i,0,cur); path.pop(); } } return res; } 全部子集(组合问题) 子集 求子集的回溯算法,思路如下: 设置一个path,代表已经选择的元素路径; 对于每一个path,设置一个start值,代表下次选择的起始位置;(含义是:path条件下,index < start的全部元素子集已经处理完毕) 每次从start到数组结尾遍历剩余选择空间,将元素放入path然后记录到结果数组,最后返回结果数组。 子集(组合)问题,每次都要进行判断,记录结果,而不是在最后判断返回。 function allCombination(arr){ function backtrack(path = [], start = 0, res = []){ // 对于每一个path,选择空间是[start, arr.length-1],[0,start) 全部子集都处理完成,不用考虑。 for(let i=start; i<arr.length; i++){ path.push(arr[i]); // 对于每一个可选择元素,我们可以将其加入组合,也可以选择跳过(不加入)。 res.push(path.slice()); backtrack(path, i+1, res); path.pop(); // 回溯,当前元素arr[i]被选择的情况已经遍历完成,将其弹出(表示当前元素被跳过),继续下一个元素的决策。 } return res; } return backtrack(); } 组合的去重 如果一个集合arr中有重复的元素,那么在上述求组合的结果中,会出现重复的组合。 重复组合: 组合中的元素种类,及各种类的数目都相同。 例如: [1,2,3] 和 [3,2,1] 去除重复组合的方法: 先将数组排序,这样重复的元素位置会相邻分布; 在回溯寻找组合的过程中,对于某次元素选择arr[i],查看其上一元素arr[i-1]是否与它相同,如果相同则跳过。 var subsetsWithDup = function(nums) { nums.sort((a,b) => a-b); // 先排序,让相同元素凑在一起。 function backtrack(path = [], start = 0, res = []){ for(let i=start; i<nums.length; i++){ if (i > start && nums[i] === nums[i-1]) continue; // 这里判断当前元素与前一元素是否相同,相同则跳过。(同一深度,添加相同的元素会造成重复) path.push(nums[i]); res.push(path.slice()); backtrack(path, i+1, res); path.pop(); } return res; } return backtrack(); }; 回溯算法:括号匹配 括号生成 回溯算法,解法等同于div标签生成. var generateParenthesis = function(n) { function backtrack(path=[], leftleft=n, rightleft=n, res=[]){ // 结束条件 if(leftleft === 0 && rightleft === 0){ res.push(path.join('')); return; } // 回溯:选择 if(leftleft > 0){ path.push('('); backtrack(path, leftleft-1, rightleft, res); path.pop(); } // 这里可以保证:右括号只有在有左括号能匹配的情况下,才会被添加。 if(rightleft > leftleft){ path.push(')'); backtrack(path, leftleft, rightleft-1, res); path.pop(); } return res; } return backtrack(); }; 回溯算法:优美的排列 优美的排列 回溯算法的路径,如果每次选择只涉及两种情况,则可以使用一个与原空间等长度的数组,用true和false代表选择的与否。 递归 递归的特点和基本思想 递归是一种解空间的遍历手段,它不同于for/while循环,递归遍历的空间可以是非线性的。(图、树等) 递归通过在函数内调用自身实现。递归在内存中是通过函数调用栈来实现的,每次调用函数在调用栈中压入函数,函数执行完毕后弹出,恢复上层函数的执行环境。 因为这个原因,递归在内存中需要占用空间,空间复杂度较高。(占用空间与函数的最大调用次数n正相关。) 递归的注意事项: 设置合理的退出条件; 递归深度影响空间复杂度,能用迭代实现尽量不用递归。 递归复杂度分析 递归问题的复杂度可以画递归树分析: 时间复杂度等于:每次函数调用的时间复杂度 × 递归调用次数。 空间复杂度等于:每次调用函数所需空间复杂度 * 最大递归深度。 典型递归问题 生成DIV标签 思路: 回溯算法,递归实现。 控制可用的开始和结束标签数目,当开始标签可用数>0时,向后面添加一个开始标签; 然后检查结束标签可用数,如果发现结束标签可用数大于开始标签可用数,说明有开始标签没有被正确关闭,此时向字符串后添加一个结束标签; 当开始标签和结束标签可用数都为0,则向结果数组中保存结果。 function generateDivTags(numberOfTags) { let res = []; function getStrings(prefix, openings, closings, result){ if(openings === 0 && closings === 0){ result.push(prefix); } if(openings > 0){ let newPrefix = prefix + '<div>'; getStrings(newPrefix, openings-1, closings, result); } if(closings > openings){ let newPrefix = prefix + '</div>'; getStrings(newPrefix, openings, closings-1, result); } } getStrings('', numberOfTags, numberOfTags, res); return res; } 排序算法 各排序算法基本思想与JS实现 堆排序 构建堆(也叫优先级队列) 构建堆 适用于:在对一个集合的遍历过程中,动态获取最大值或最小值的问题。 例如: 获取一个数组中出现次数最高的前N个元素。 为集合元素构建堆(大顶堆、小顶堆),可以通过堆排序,方便获取某一属性优先的前N个元素。 1792. 最大平均通过率 字典序 字典序的比较优先级从高到低(以字典序从小到大为例): 从前到后逐位比较,同一位置上字符值更大的,字典序更靠后; 前缀相同的两个字符串,长度更长的,字典序靠后; 也就是说,在字典序中比较两个字符串a和b,相当于比较它们最靠前的不同字符a[i]和b[i]。 一个给定的字典序排列[a,b,c,d,e...],假设长度为n,则: 它可以按照给定的字母顺序,形成一个n叉字典树; 将字符串按字典序排列,等同于将字符串加入这个字典树,然后对这个n叉树进行先序遍历。 搜索算法 各种搜索问题,本质上都是树的遍历问题。 二分查找 复杂度分析 时间复杂度: O(logN),N为原数组长度。 空间复杂度: O(1) 二分查找适用情况 应用二分查找的必要条件是: ①顺序存储:数组结构; ②有序,可以缩小结果范围。 一般适用于:按序排列的数组元素搜索。 二分查找的关键信息 成功应用二分查找,必须要找到一个根据中间点,能对左右两侧区间进行取舍的判据; 二分查找的最终时间复杂度为O(log_n); ★二分查找的区间情况和对应的代码 假设二分查找应用的数据是升序排列的,假设它的区间是[a,b]。将它以目标值target分成两部分,一定是如下两种情况之一: [a,target)和[target,b]; [a,target]和(target,b]。 对于一个target,我们进行查找的目标的情况可以是: ≤target的最后一个元素; <target的最后一个元素; ≥target的第一个元素; >target的第一个元素。 需要根据上述四种查找的目标情况,对区间进行正确切分,注意我们需要保证[left,right]区间内元素是我们想要的元素,而且最后left和right要重合,否则会出现数组越界。 查找≤target的最后一个元素时: mid ≤ target: left = mid; mid > target: right = mid - 1; mid的取整情况:向上取整。 查找<target的最后一个元素: mid < target: left = mid; mid ≥ target: right = mid - 1; mid的取整情况:向上取整。 查找≥target的第一个元素: mid < target: left = mid + 1; mid ≥ target: right = mid; mid的取整情况:向下取整。 查找>target的第一个元素: mid ≤ target: left = mid + 1; mid > target: right = mid; mid的取整情况:向下取整。 这里有几个技巧(对于升序排列): 先正确切分区间(哪边闭哪边开),然后判断查找的目标区间在哪一侧,目标区间边界在收缩的时候一定是闭的(在左侧:left = mid 或 在右侧:right = mid); right只能向左缩小,left只能向右缩小。也就是说,left、right取值只有以下两种情况: left = mid, right = mid - 1; left = mid+1, right = mid; 向上还是向下取整,取决于哪边是开区间: 当存在left = mid + 1,向下取整; 当存在right = mid - 1, 向上取整。 二分查找过程中,mid的判断条件,和区间的切分情况一定完全相同。需要做的只是根据区间的切分情况,对mid进行开闭取舍; mid位于目标值target左侧,动左区间。mid位于目标值右侧,动右区间。 此外,二分查找的取中点操作,一定要用Math.floor((l+r)/2)形式,而不是位运算(l+r) >> 1! 因为JS对位运算参与的数,当做32位整数处理,而不是64位。这样做在数值大于2^32-1的时候会造成计算错误。 Math.floor(((2 ** 31) - 1)/2) // 1073741823 ((2 ** 31) - 1) >> 1 // 1073741823 Math.floor(((2 ** 32) - 1)/2) // 2147483647 ((2 ** 32) - 1) >> 1 // -1 基本代码实现 二分查找实现的两种方式:1.迭代 2.递归。 // 递归实现:升序数组nums中,查找≤target的最后一个元素。 const search = function(nums,target) { function find(left, right){ if(left < right){ let mid = Math.ceil((left+right)/2); if(nums[mid] <= target) return find(mid, right); else return find(left, mid-1); } return left; } return find(0,nums.length-1); }; // 迭代实现 var search = function(nums,target) { let left = 0, right = nums.length-1; while(left < right){ let mid = Math.ceil((left+right)/2); if(nums[mid] <= target) left = mid; else right = mid-1; } return left; }; 二(多)维空间的二分查找 对于一个二(多)维数组,如果它的元素按照各维度的增长方向,已经是有序排列,那么它就满足了二分查找的条件:可以对任意一个中间值mid,查找比mid大或小的元素个数。 方法如下: 先确定元素的左右极值:因为为有序排列,极值位于维度增长方向的两个端点; 例如: 对于二维数组arr,如果按行按列都是递增的,那么极小值在arr[0][0],极大值在arr[arr.length-1][arr[0].length-1],也就是左上角、右下角; 对于一个k维有序数组,我们可以在O(nk-1)时间复杂度完成一次查询,方法是:固定其他维度,将多维降为二维,然后找到二维数据某一递增轴的末端点,作为起点,向另一个轴方向进行计数; 然后利用左右极值,进行常规二分查找即可。 滑动窗口 适用于:在一个大的连续空间,寻找满足特定要求的连续子区间。 滑动窗口中用到了左右两个指针,它们移动的思路是:以右指针作为驱动,拖着左指针向前走。右指针每次只移动一步,而左指针在内部 while 循环中每次可能移动多步。右指针是主动前移,探索未知的新区域;左指针是被迫移动,负责寻找满足题意的区间。 — 《挑战程序设计竞赛》 滑动窗口的基本过程是: 通过左右边界维护一个窗口:left和right,初始都在0位置; 窗口的有效区间范围是[left, right]双闭区间; 右边界是主动前进的,它负责向右扩展窗口探索新区域,每次只前进一步; 左边界是被动前进的,因为每次右边界都会前进,窗口内的值可能不再符合要求,需要收缩左边界使得当前窗口内的值继续满足要求,左边界每次可以前进多步; 直到右边界到达末尾length-1位置,停止搜索。 // 滑动窗口的伪代码 let left = 0, right = 0; let target = 0, res = 0; while (right < arr.length) { target += arr[right]; while (/* target do not fit the condition */) { target -= arr[left]; // decrease length of the window, change the target value. left += 1; } res = target // record the found answer here. right += 1; } return res; 枚举 当问题解的空间不大时(一般小于 2^16),可以直接进行枚举,找出所有的解。 特殊情况下,可以用二进制数字表示每个状态,通过二进制数字进行解空间的枚举。 6029. 射箭比赛中的最大得分 双指针 快慢指针 快慢指针:同一方向,不同前进速度的两个指针。 快慢指针的用途: 链表寻环: 使用快慢指针,快指针比慢指针每次多走1步,如果有环存在,则快慢指针一定会相遇; 寻找链表的中点; 使用快慢指针,慢指针每次走1步,快指针每次走2步,则快指针到达尾部,慢指针正好在链表中间位置; 有序集合 跳表 SkipList Redis为什么用跳表而不用平衡树? - 张铁蕾的文章 - 知乎 跳表是有序链表的扩展,是有序集合的一种。它可以实现O(logN)时间复杂度的集合元素添加、删除和查询操作。 相比于各种平衡树,跳表的平均时间复杂度相当,且在原理和实现上更为简单。 跳表为什么能够降低有序链表查询的时间复杂度? 跳表为有序链表的每个结点,额外增加了多个指针,用于指向其他节点。这些指针被分成多层,层数从低到高,每层的指针数目递减。查询时,从最顶层开始查找,这样可以大步长快速定位到查询节点附近,然后逐层降低层数,直到层数为0,返回查询到的结果(或未找到)。 因为跳表涉及到频繁插入、删除等操作,无法使用固定的递减模式进行层数设计。跳表一般使用一个概率值p确定节点指针的层数,并设置一个最大层数M,防止层数过高。 根据Redis的实现,M = 32, p = 1/4是合适的。 // Marswiz @2022 // Skiplist in JS. class SkipList { MAX_LAYER = 32; P = 1 / 4; constructor(data = []) { this.tail = new Node(Infinity, new Array(this.MAX_LAYER).fill(0).map(i => null)); this.head = new Node(-Infinity, new Array(this.MAX_LAYER).fill(0).map(i => this.tail)); for (let i of data) { this.addNode(i); } } addNode(val) { let p = this.P; let nextLayer = [null]; while (nextLayer.length < this.MAX_LAYER && Math.random() <= p) nextLayer.push(null); let curNode = new Node(val, nextLayer); let curLayer = this.MAX_LAYER - 1; let cur = this.head; while (curLayer >= 0) { while (this.getNext(cur, curLayer).val <= val) { cur = this.getNext(cur, curLayer); } if (curLayer < nextLayer.length) { curNode.next[curLayer] = cur.next[curLayer]; cur.next[curLayer] = curNode; } curLayer -= 1; } } getNext(node, layer) { return node.next[layer]; } show() { let cur = this.head; let res = []; while (cur !== null) { res.push(cur.val); cur = cur.next; } console.log(res); } } class Node { constructor(val = 0, next = [null]) { this.val = val; this.next = next; } } 一些操作 概念定义 子序列: 由原数组中部分或全部元素组成的新数组,子序列中元素的先后顺序必须与原数组相同,但是元素在原数组中不必相邻。 子数组: 由原数组中部分或全部元素组成的新数组,子数组中元素的先后顺序必须与原数组相同,而且元素在原数组中必须相邻。 字符串操作 字母转化为数值:a.charCodeAt(0) - 97; (97是字母a的ASCII码) 位运算 补码: 正数的补码是它本身; 负数的补码,是符号位保持为1不变,其他位取反,然后整体再加1。 AND OR XOR都满足交换律和结合律; (a&b)^(a&c) = a&(b^c) x^0 = x x^x = 0 >>是带符号右移,表示移动过程中左侧空出来位置用符号位的值来填充(正数补0,负数补1); >>>是无符号右移,表示移动过程中左侧空出来位置,始终用0来填充; a & (-a) 可以找出数字a的最低非0位; BigInt 除了比较数值大小外(>/</>=/<=),BigInt只能与BigInt类型进行计算; BigInt基本可以表示任意长度的整数,但仍有上界,只是非常大; BigInt的位运算: BigInt只有带符号左右移操作符<<和>>; BigInt在<<和>>的时候,不会被视作32位整数,而是它本身。 因此1 << 32 和1n << 32n结果是不同的,1n << 32n会返回正确的结果。 数组操作 数组中随机取一个元素 let randPos = Math.floor( Math.random() * arr.length ) let res = arr[randPos] 数组中删除一个元素 arr.splice(position, 1) 数组中动态删除元素,考虑从右到左遍历 从右到左遍历,指针左侧的元素不会被动态修改,指针右侧的元素删除不影响指针的下一位置。 浅拷贝数组 // 1. Array.from(arr); // 2. arr.slice(); // 3. [].concat(arr); 数组sort排序 // 字符串升序 arr.sort() // 字符串降序 arr.sort().reverse() // 数字升序 arr.sort( (a,b) => a-b ) // 数字降序 arr.sort( (a,b) => b-a ) 判断变量类型 // 精确返回变量类型,首字母大写 Object.prototype.toString.call(arg).slice(8,-1) 链表操作 链表的前序、后序遍历 function iterate(nodeHead){ if (nodeHead) { // 这里写是前序; iterate(nodeHead.next); // 这里写是后序。 } } 数值操作 辗转相除找最大公约数 function gcd(num1, num2) { return num2 === 0 ? num1 : gcd(num2, num1 % num2); } 找两数最小公倍数 let LCM = num1 * num2 / gcd(num1,num2); 寻找1 ~ n范围内每个数的约数 这个算法的复杂度为O(n * log_n)。 因为内层循环执行了n/1 + n/2 + ... + 1次,其级数的极限为log_n。而外层循环执行n次,所以总时间复杂度为O(n * log_n)。 function findDividers(n) { let res = new Array(n+1).fill(0).map(i => new Array(0)); for (let i=1; i<=n; i++) { for (let j=i; j<=n; j+=i){ res[j].push(i); } } } ABC集合的合集元素数目 |A∪B∪C| = |A| + |B| + |C| - |A∩B| - |A∩C| - |B∩C| + |A∩B∩C| 合并区间 对于一系列闭区间组成的数组arr:[[l1,r1],[l2,r2],[l3,r3]...[ln,rn]],它们之间可能无序,也可能存在嵌套,合并它们的策略是: 先按起始位置l1,l2,l2.3...对区间进行升序排序; 声明一个left和一个right变量,用来保存当前区间的左右边界值,以及一个数组res用来保存结果; 从左到右遍历区间数组(索引index从0到n-1),对每个区间[l1,r1]和它下一个区间[l2,r2],因为排过序可知此时存在l2 >= l1: 如果l2 > r1,说明下一个区间和现在的区间不重合,直接把现在的结果区间[left,right]放入结果数组,并更新left = l2, right = r2; 否则l2 <= r1,说明区间存在重合: 此时如果r2 <= r1,说明[l2,r2]区间被完全覆盖,此时直接跳过; 如果r2 > r1,此时更新新的区间右边界值right = r2; 最大值 <= M 的子数组个数 给你一个数组arr,请你找到内部元素最大值<= M的子数组个数? 例如:[1,5,2,3,4]中最大值<= 3的子数组有[1]、[2]、[3]、[2,3],共四个。 注意,子数组是连续的。 思路: 每一个子数组都有一个结尾,我们可以通过枚举子数组结尾元素位置,来计数; 对于以arr[i]为结尾元素的一系列子数组: 如果arr[i]本身> M,则没有满足要求的子数组; 如果arr[i]本身<= M,则以arr[i]为结尾的满足要求的子数组个数,等于从i位置向前(包含i)满足<=M的连续元素个数. function cal(arr, M) { let res = 0; let cur = 0; for (let i of arr) { cur = i > M ? 0 : cur+1; res += cur; } return res; } // cal([1,5,2,3,4],3); // 4 矩阵旋转 一个矩阵旋转(顺时针、逆时针),可以转化成水平、垂直翻转或沿对角线翻转的组合。 如果旋转90度,可以转化为一次沿水平、垂直的翻转+一次沿对角线的翻转; 如果旋转180度,可以转化为两次沿水平、垂直的翻转。 消除相同的数问题 消除相同的两个数 可以考虑使用位运算中的异或运算,对完全相同的两个数进行消除。 异或: 位相同则返回0, 否则返回1。 如果两个数 a === b,那么a ^ b === 0; 任何一个数a,它与0异或运算后,仍然是它本身,a ^ 0 === a。 消除相同的多个数 问题类型: 一个数组中,只有一个数x出现的次数为1,其他数都重复出现n次,请找出这个单独的数。 基本思路: 按位思考,对于每一个数的每一位,因为只能是1或0,对于重复出现的n个数,它们在这一位上的加和只能是n或0; 所以对于除单独的数x外,其他的数在每一位上的加和只能是nk (k >= 0 && k <= n); 考虑单独的数x,全部数在每一位上,加起来的和只能是nk+1或nk; 因此,对全部数每一位进行加和,然后用n取余数,余数就是要找的单独数x在该位上的实际值。 // 在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。 var singleNumber = function(nums) { let res = 0; let cur = 1; for (let i=0; i<31; i++) { let s = 0; for (let n of nums) { if ((n & cur) !== 0) { s += 1; } } res += cur * (s % 3); cur <<= 1; } return res; }; 位运算判断纯字母字符串是否含有重复字母 318. 最大单词长度乘积 因为字母一共只有26个,使用二进制数字的每一位(0~25)表示一个字母,最大为226-1不会超出JS数字上限。 设初始掩码为mask=0(全0),当字母存在时置对应位为1。具体操作为获取字母在字符串abcdefghijklmnopqrstuvwxyz的索引i,然后执行以下操作: let cur = 1 << i; mask |= cur; 例如:acde对应的二进制掩码后五位为11101(其余全为0) 当判断字符串a与b是否有重复字母时,取a、b的掩码mask_a和mask_b,对它们执行与操作&: 如果为0,则没有重复字母; 否则含有重复字母。 取余操作 对较大的幂值进行取余操作 求a ** b % mod,假设 a ** b 是一个较大的值,超出了JS数字的表示范围。 使用一个for循环即可。 let res = 1; for (let i=0; i<b; i++) { res *= a; res %= mod; } 概率问题 按照权重随机选取元素 给定一个权重数组w,每个位置i上的值w[i]代表i元素的权重值,目标是按照权重的大小,随机选取一个元素位置t。需要保证权重值越大,被选取的概率越大。 思路: 要随机选取,一定要用到随机数APIMath.random(),但是它只能在一个区间内均匀选取; 要按照权重选取,我们需要将这些权重转化为区间长度,这样在区间内均匀随机选取一个位置,判断其在哪一个区间内,就可以实现按权重选取一个元素了; 具体地: 从0开始,我们将w[i]转化为区间长度,则i位置对应的区间右边界为[0,i]区间内w[i]的前缀和,进行预计算,w[i]前缀和组成的数组假设为pre[i]; 设w[i]的总和为s,则我们在[0,s]范围内随机均匀抽样,选取一个随机数t; 二分查找,找到第一个pre[i] >= t的位置i,则i是我们按权重随机选择的结果。 使用rand7生成rand10 如何使用一个随机生成[1,7]的范围内整数的函数rand7(),实现一个在[1,10]均匀采样的函数rand10() ? 这个问题更精确的描述是:使用一个较小范围的均匀随机离散点生成器,实现一个更大范围的均匀随机离散点生成器? 解决思路: 可以多次执行提供的随机函数,生成更多的随机结果; 为保证结果是均匀的,我们不能让两次随机产生的结果互相有叠加,因为这样会让概率值发生变化; 解决方法: 使用rand7()生成一个二维坐标点。 调用两次rand7(),分别当做横坐标r和纵坐标c; r和c一共可以表示7*7 = 49个二维格子位置,将格子从上到下、从左到右进行编号1 ~ 49; 计算出抽到的格子编号idx; 因为49无法整除10,对于后面的[41, 49],如果抽到,我们拒绝采样,直接重新再采样一次,直到抽到的数idx <= 40; 返回(idx - 1) % 10 + 1。(为了让取模后的起始点为1而不是零) 小技巧: Tips 集合中选取元素组成目标和tar的问题,可以转化为给集合中的元素添加符号的问题: 抽取若干元素凑成目标和:转化为在原集合中的每个元素前面添加+1或0因子; 将集合中的元素,分为各自加和为目标和的两组:转化为在原集合中的每个元素前面添加+1或-1因子,使得加和结果为0; 选取集合中的部分元素,分为各自加和相同的两组:转化为在原集合中的每个元素前面添加+1、-1或0因子,使得加和结果为0; 判断数组的前n个元素arr[0] ~ arr[n-1]是否与索引值一一对应(也就是元素取值范围是[0, n-1]): 从前向后遍历原数组,记录最大值max,如果max === i,则[0,i]区间满足索引与元素一一对应。

数学、博弈问题

2022.01.01

Personal

Math

记录一些数学定义、公理和定理。 数论 余数 同余定理 两个整数a,b,如果他们同时对一个自然数m求余所得的余数相同,则称a,b对于模m同余。记作a≡b(mod m)。读为:a同余于b模m。在这里“≡”是同余符号。 模m的两个同余数a和b的差diff = a - b,被它们的模数m整除:(a-b) % m === 0; 模m的两个同余数a和b的和、差、积,与它们模m后余数的和、差、积同余:(a + b) % m === (a % m + b % m) % m 和、差、积、商取模 (a + b) % m === (a % m + b % m) % m; (a - b) % m === (a % m - b % m + m) % m; (a * b) % m === ((a % m) * (b % m)) % m; (a / b) % m === (a * B) % m = (a * b^(m-2)) % m; (其中B是b关于m的乘法逆元。) 最大公约数、最小公倍数 辗转相除找最大公约数 function gcd(num1, num2) { return num2 === 0 ? num1 : gcd(num2, num1 % num2); } 找两数最小公倍数 let LCM = num1 * num2 / gcd(num1,num2); 图相关 两点之间距离 博弈问题 博弈问题分类 无偏博弈(Impartial Game): 游戏有两个人参与,二者轮流做出决策,双方均知道游戏的完整信息,且可做出的决策集合相同; 任意一个游戏者在某一确定状态可以作出的决策集合只与当前的状态有关,而与游戏者无关; 游戏中的同一个状态不可能多次抵达,游戏以玩家无法行动为结束,且游戏一定会在有限步后以非平局结束; 总而言之,参与游戏的双方除了有先手后手外,其他毫无区别。 非无偏博弈:与无偏博弈的区别主要是可作出的决策集合与游戏者有关(例如棋牌游戏,双方互相不能使用对方的棋子); 反常博弈:与无偏博弈的区别主要是,反常游戏的胜者是最后第一个无法行动的玩家,而败者是最后行动的一方; 状态与必胜、必败状态 状态就是游戏的局面信息,游戏的进行由状态之间转移表示。 假设当前状态为S,当前的先手游戏者做出了决策P,那么游戏状态可能变为T,那么我们称T为S的后继状态,代表状态T可以通过状态S经由某种决策而得到。 必胜状态:某一个游戏状态S,对于当前先手的游戏者必胜; 必败状态:某一个游戏状态S,对于当前先手的游戏者必败; 有关必胜、必败状态的几个定理: 如果一个状态S没有任何后继状态,它是必败状态; 如果一个状态S的后续状态集合中,存在一个必败状态,那么S为必胜状态; 如果一个状态S的后续状态集合中,全部都是必胜状态,那么S为必败状态; 定理2和3是显然的,定理1的解释是:S没有后续状态,则当前游戏者无法做决策(无法行动)。根据游戏定义,这时它是败者。 著名IG游戏 Nim 游戏 有n堆石子[a1, a2, ... an],两名玩家每次从这些石子堆中移除若干石子,规则如下: 每次只能选择一堆,只能在这一堆中进行移除; 每次移除的石子数量在[1, ai]之间; 最先无法移除石子的人,为败者。 Nim 游戏的结论 结论:Nim游戏在所有石子堆a1,a2 ... an的异或和为0的时候,先手必败,否则先手必胜。 Nim游戏结论解释 定义Nim和为: a1 ^ a2 ^ a3 .... ^ an,Nim和为0的状态称为Nim平衡。 根据上节定义1,没有后继状态的状态为必败状态。这里当所有石子堆的数目全为0,为必败状态,此时Nim和为0; 当前状态的Nim和如果不为0:则当前先手总可以找到一种移除方法,使得Nim和归0; 当前状态的Nim和如果为0:则当前先手的任何移除方法,都使得Nim和不再为0; 对于2和3的解释: Nim平衡的本质是:a1、a2 ... an这n个数的二进制表示中,二进制的每一位上,都有偶数个1; 如果当前Nim不平衡:此时可知当前Nim和N一定有奇数个1的数位。那么我们从最高位开始,选择第一个具有奇数个1的位置t,假设它对应的数为ai,那么我们可以将这个数位-1,然后利用移除的这个最高位数,将t之后的非Nim平衡位,“配平”为偶数(类似借位的思想)。具体地,我们可以将ai替换为ai ^ N; 如果当前Nim平衡:此时玩家在a1、a2 ... an中选择一个数ai进行减小操作,一定会将ai的某个数位t由0变1(或由1变0),之后的Nim和一定不为0; 因此,Nim和总是在0与非0之间变换: 当初始Nim和为0时,因为先手一定会将其变为非0,而后手之后又可以将其归0,所以先手将一直面临Nim和为0的状态,直到到达石子数全为0的必败状态,先手败; 当初始Nim和不为0时,先手可以选择将其变为Nim平衡,后手一定会打破Nim平衡,因此后手一直面临Nim和为0的状态,直到到达石子数全为0的必败状态,先手胜; Nim游戏的启示:SG定理 SG定理由Sprague-Grundy提出,它定义了一个SG函数: 任意一个状态x:SG(x) = mex(S),其中S是集合S = { SG(y) },y是x的后继状态; mex(S)表示不在集合S内的最小非负整数: 例如mex({0,1,3,4}) === 2,因为2是不在集合{0,1,3,4}中出现的最小非负整数; 对于必败状态s,因为后续状态集合为空,因此SG(s) === 0; SG定理:游戏整体的SG函数值,等于各子游戏SG函数值的Nim和(异或和)。 游戏的SG函数值为0,则先手必败,否则先手必胜。

经典算法集

2021.12.19

Algorithm

经典的算法。 1. Kadane 算法 Kadane算法,用于计算数组中连续子数组的最大或最小和。 1.1 计算连续子数组最大和、最小和 【1.求子数组最大和】 求一个非空数组A中的一个子数组SubA,使得SubA中元素的加和最大。 (子数组:在原数组中必须是连续的,长度最小为1。单个元素构成的数组和数组A本身也是A的子数组。) Kadane算法,基于以下事实: 最终求得的子数组SubA一定以数组A中某一个元素Ai作为结尾; SubA在原数组A中是连续的。 假设在位置i的元素为Ai,则位置i+1的元素为Ai+1。 以Ai结尾的最大加和子数组SubAi,要么只是它本身自己构成,要么是和它相连的前面若干元素一同构成。设它前面的若干元素加和为K,那么此时加和Sum(Ai)=K+Ai。 如果SubAi只由自己本身构成,那么K=0。 这时考察以Ai+1结尾的子数组SubAi+1。它有两个选择: ① 继续使用Ai结尾的最大子数组,SubAi+1的加和可以表示为K+Ai+Ai+1,也就是 Sum(Ai+1)= Sum(Ai)+Ai+1。 ② 不继续沿用之前Ai的最大子数组,而是自己单独成为一个子数组,如果之前的子数组SubAi的加和Sum(Ai)对它而言是个累赘(为负)。这时Sum(Ai+1) = Ai+1; 这两种情况的取舍,取决于二者所得子数组加和哪个更大。也就是Sum(Ai+1)=Math.max(Sum(Ai)+Ai+1, Ai+1)。 因此得出Kadane算法如下: 初始化 maxSumHere = A[0] 和 max = A[0]; 对原数组A从左到右进行遍历,计算以每个数组元素位置i为结尾的子数组加和值maxSumHere,计算方法为:Sum(Ai+1)=Math.max(Sum(Ai)+Ai+1, Ai+1); 当前元素i计算完成后,更新max值。(最终结果为各个元素为结尾位置,而得到的子数组加和集的最大值。) 返回max值即为结果。 // Kadane's Algorithm function kadanesAlgorithm(array) { let lastSubArrayMax = array[0]; let totalMax = array[0]; for(let i=1; i<array.length; i++){ // Sum(Ai_+_1)=Math.max(Sum(A_i)+A_i_+_1, A_i_+_1); if(lastSubArrayMax + array[i] < array[i]) lastSubArrayMax = array[i]; else lastSubArrayMax = lastSubArrayMax + array[i]; // Refresh the total Max Value. totalMax = Math.max(lastSubArrayMax, totalMax); } return totalMax; } 计算连续子数组的最大和或最小和问题。 // Kadane's Algorithm // Max / Min function kadane(arr){ let curMax = arr[0]; // let curMin = arr[0] let max = arr[0]; // let min = arr[0]; for(let i=1; i<arr.length; i++){ curMax = Math.max(curMax+arr[i], arr[i]); // curMin = Math.min(curMin+arr[i], arr[i]); max = Math.max(max, curMax); // min = Math.min(min, curMin); } return max; } 1.2 计算环形数组的连续子数组最大和、最小和 环形子数组的最大和 如果原数组首尾连接,那么有两种情况: 最大子数组包含首尾两个元素(覆盖了首尾连接成环的间隙); 最大子数组出现在原数组内部(不覆盖首尾连接成环的间隙)。 对于2情况,等同于一般的Kadane算法计算连续子数组最大和。 对于1情况,最大子数组包含了数组首尾连接点,则最小和一定出现在数组之中。此时最大和 = 数组全部元素和 - 最小和。 因此,可以使用Kadane算法,分别计算数组内(首尾不连接成环)的全部元素总和sum、最大和max和最小和min,然后返回sum-min和max中的最大值。 这里有一种特殊情况:当全部元素为负,min=sum,此时sum-max=0。此时max为元素中的最大值,应该返回max而非0。 var maxSubarraySumCircular = function(nums) { let sum = nums[0]; let curMin = nums[0], curMax = nums[0]; let min = curMin, max = curMax; for(let i=1; i<nums.length; i++){ sum += nums[i]; curMax = Math.max(curMax+nums[i], nums[i]); curMin = Math.min(curMin+nums[i], nums[i]); max = Math.max(max, curMax); min = Math.min(min, curMin); } // 全部元素为负,返回max而非0. return max < 0 ? max : Math.max(max, sum-min); }; 1.3 计算连续子数组乘积的最大值、最小值 乘积最大子数组 乘积不同于加和,它的特点是: 如果下一个元素为正,则包含下一个元素数组的最大乘积max[i] = max[i-1] * num[i],最小乘积min[i] = min[i-1] * num[i]; 如果下一个元素为负,则包含下一个元素数组的最大乘积max[i] = min[i-1] * num[i],最小乘积min[i] = max[i-1] * num[i]。 因此,必须同时记录当前状态i的最大乘积max和最小乘积min,才能用于下一个状态i+1的计算。 var maxProduct = function(nums) { let curMax = nums[0]; let curMin = nums[0]; let max = curMax, min = curMin; for(let i=1; i<nums.length; i++){ // 无论情况如何,乘积最大值应该是这三者中的最大者,乘积最小值应该是最小者。 let a1 = curMax*nums[i], a2 = curMin*nums[i], a3 = nums[i]; curMax = Math.max(a1, a2, a3); curMin = Math.min(a1, a2, a3); max = Math.max(max, curMax); min = Math.min(min, curMin); } return max; }; 2. KMP算法 KMP算法,用于解决查询字符串str1中是否存在子字符串str2的问题。 2.1 算法内容 function knuthMorrisPrattAlgorithm(string, substring) { function getSuffixPosition(str){ let suffixPosition = new Array(str.length); suffixPosition.fill(-1); let j = 0; for(let i=1; i<str.length; i++){ if(str[i] === str[j]) suffixPosition[i] = j++; else j=0; } return suffixPosition; } let suffixPosition = getSuffixPosition(substring); let i=0, j=0; while(i < string.length){ if(string[i] === substring[j]){ if(j === substring.length-1) return true; j++; i++; } else { if(j === 0) i++; else j = suffixPosition[j-1]+1; } } return false; } 2,2 复杂度分析 时间: O(m+n) 空间: O(m) m为查询子串长度,n为被查询原字符串长度。 原字符串被遍历一次,查询子字符串也被遍历一次(用来建立相同前后缀的位置数组suffixPosition)。 原字符串的每次遍历,最多与子字符串执行两次比较。因此,总时间复杂度为O(2n+m) = O(m+n)。 3. 随机抽样算法 3.1 打乱数组:Fisher–Yates shuffle 洗牌算法 3.1.1 算法内容 对于一个待打乱数组arr: 指针i从末尾位置开始,选取从起始位置到i的闭区间[0,i]; 在该区间内随机抽取一个数pick,将pick与i位置的元素进行交换,此时i位置元素确定,将i向前移动1个位置; 重复1-2步骤,直到i到达起始位置0,结束打乱数组; function rand(arr){ for(let i=arr.length-1; i>0; i--){ let pick = Math.floor(Math.random() * (i+1)); [arr[pick], arr[i]] = [arr[i], arr[pick]]; } return arr; } 该算法可以保证打乱的随机性。 3.1.2 复杂度分析 时间复杂度: O(n); 空间复杂度: O(1); 3.2 大数据流中的均匀随机抽样:蓄水池抽样算法 假设有一个很大的数据流(长度为n),无法一次性加载完成(即不能知道n的具体数值),任务是在数据动态加载的过程中,对它进行随机抽样,且需要保证抽样的均匀性(1/n)。 3.2.1 算法内容 用一个变量n记录当前读取的数据长度,变量cur记录当前选择的元素; 对于下一个读取到的元素n = i,作为第i个读取到的元素,我们进行如下选择: 选择它的概率为1/i; 否则,继续保留上一次选择的元素,概率为1 - 1/i; 4. 寻找下一个更大排列 下一个排列 从右到左找到[i,i+1],使得arr[i] < arr[i+1]; 从末尾向前搜索,找到第一个p,使得arr[p] > arr[i]; 调换arr[i]和arr[p]; 将i+1到数组末尾的部分反转(降序变升序),即找到下一个更大排列。 5. 颜色分类(荷兰国旗) 颜色分类 将三个数0,1,2组成的随机数组,通过一次遍历,完成从小到大排列。 设定左右指针l,r,分别代表下一个0和下一个2的位置(初始:l=0, r=length-1); 设置游标cur,时刻保证两个条件: l <= cur <= r; l的左边都是0,r的右边都是2; 每次cur到达一个位置,都保证它要么触及左边界l,要么触及右边界r,要么为1才向后移动。 const sortColors = function(nums) { let l = 0, r = nums.length-1; let cur = 0; function swap(i,j){ [nums[i],nums[j]] = [nums[j],nums[i]]; } while(cur <= r){ while(cur >= l && cur <= r && nums[cur] !== 1){ if(nums[cur] === 0) swap(cur,l++); else if (nums[cur] === 2) swap(cur, r--); } cur++; } return nums; }; 6. 寻找多于lenght/n的元素() —— Boyer-Moore 投票算法 169. 多数元素 229. 求众数 II 投票算法用来寻找一个数组中,数目严格大于数组长度length/n的元素。 6.1 投票算法基本流程 对于找到数组中个数大于length/2的元素: Boyer-Moore 投票算法基本过程如下: 设置一个候选人变量candidate,设置一个投票数变量count=0; 对原始数组nums进行遍历,对每个nums[i]: 如果count=0,赋值candidate = nums[i]; 然后,判断: 如果candidtate === nums[i]: count增加1; 否则,count减小1; 最终,candidate就是寻找的众数。 Moore 投票算法也可以用于找数目大于 length/n 的元素: 数目大于length/n的元素,最多只能有n-1个。Moore算法步骤如下: 设置n个候选人位置candidate,初始化为任意值; 设置n个计票器count,初始化票数为0; 遍历原数组nums的每个值i,对任意一个候选人t: 如果: nums[i] === candidate[t],且 count[t] > 0, 则count[t]增加1; 如果:count[t] === 0,更换候选人candidate[t] = nums[i]; 否则: 让各候选人的投票数count[t]都 -1。 最后留下来的,其中一定有要寻找的数目大于 length/n 的元素(但并非全部都是); 再对原数组进行一次遍历,查看每个找到的元素,出现次数是否大于 length/n。 6.2 投票算法的证明 Boyer-Moore 投票算法的证明: 假设nums中,众数x一定存在,那么x的数目一定大于nums.length/2。 假设我们从左到右遍历数组nums: 如果全程count都没有变为0: 那么,可知此时candidate一定为众数。 因为如果candidate不是众数,因为众数数目占优,遍历过程中count一定会被众数变成负值,从而经过变0的过程。 对于任意位置i遍历之后,如果count=0: 如果candidate本身就是要寻找的众数x,那么可以知道包含i的左侧区间,非众数的数目n1等于众数x的数目n2,因为众数数目大于数组长度的一半,因此i右侧剩余数组众数依然为x; 如果candidate本身不是要寻找的众数x,那么可以知道包含i的左侧区间,众数x和其他非当前candidate的数之和,等于当前candidtate的数目,因此左侧区间众数x的数目小于当前candidate的数目,右侧区间众数依然为x; 因为右侧区间众数始终为x,所以对于最后一段区间,count一定不会再变为0,并且count最后一定不会是0(一定大于0),这等同于第一种情况。 因此,最终candidate一定是众数。 const majorityElement = function(nums) { let candidate; let count = 0; for (let i of nums) { if (count === 0) candidate = i; if (candidate === i) count += 1; else count -= 1; } return candidate; }; 7. 约瑟夫环问题 剑指 Offer 62. 圆圈中最后剩下的数字 约瑟夫环问题: 假设有n个人围成一圈,编号为0 ~ n-1。给定一个数值m,从编号0开始起(作为第1个),每一轮向后数m个,将当前位置的人驱逐出去,然后下一轮从他的下一位开始数,以此类推,直到圈中只剩一个人,求最终剩下这个人的编号。 基本思路: 每次从驱逐出去一个人,然后下一轮从他的下一位开始数,所以每轮圈中的人编号会发生变化; 假设当前轮剩余n人,f(n,m)表示计算圈中还剩n人的时候,按这种每轮淘汰第m位的规则,最终会留下的人员编号; 那么在当前轮之后,因为要淘汰掉一个,所以下一轮剩余n-1人,下一轮淘汰掉的人员编号为f(n-1, m); 但是,因为一轮之后,大家的编号要发生变化,必须找出前后两轮编号之间的对应关系: 因此,f(n,m) = (f(n-1,m) + m) % n ,当n = 1的时候,说明圈内只有一人,此时应返回他的编号0。 var lastRemaining = function(n, m) { if (n === 1) return 0; return (lastRemaining(n-1, m) + m) % n; }; 8. Brian Kernighan 算法 201. 数字范围按位与 Brian Kernighan 算法用来消除一个数字二进制表示中,最右边的1(将其变为0)。 算法实现: 对于一个数n:将其进行n = n & (n-1)操作。 例如: n = 5时, n = (110)2。进行n = n & (n-1)操作后,n = 4 = (100)2 消除了最右边的一个1. 9. LRU 缓存算法 LRU 缓存算法 描述: 设计一套缓存机制,容量最大为n。其中有put(key, value)和get(key)两个方法,put添加一条记录,get获取记录。 put和get都算作一次操作,当记录数目超过容量n时,删除最久未使用的记录。 算法实现: 通过JS的Map数据结构可实现,它是有序的(遍历时是按照加入的顺序); get(key)操作时,如果查询到key,则将key从map中删除,然后将其重新添加到map,来更新key的记录顺序到最新; put(key,val)操作时: 如果map中存在对应key,则同上面一样,先删除key,再添加key记录到最新的值val; 如果map中不存在对应key,则直接添加key,然后判断是否容量超限,如果超限则删除掉Map中遍历出来的第一个key记录(因为遍历是按照插入顺序,第一个是最早插入的)。 var LRUCache = function(capacity) { this.map = new Map(); this.capacity = capacity; }; LRUCache.prototype.get = function(key) { if (this.map.has(key)) { let val = this.map.get(key); this.map.delete(key); this.map.set(key, val); return val; } return -1; }; LRUCache.prototype.put = function(key, value) { if (this.map.has(key)) { this.map.delete(key); } this.map.set(key, value); if (this.map.size > this.capacity) { let oldest = this.map.keys().next().value; this.map.delete(oldest); } }; 10. 素数筛 用于筛选从2~n范围内的质数。 10.1 埃氏筛选法 基本思路: 假设要找的素数范围range是[2,n]; 维护一个数组deleted,长度为n+1,初始时任何数都没有被筛除,数组值全部为false(表示未筛除); 从i = 2开始,每次取deleted中第一个未被筛除的数(索引为i),将它在range内的全部倍数都筛除掉(deleted对应位置n*i设为true); 因为:如果一个数n能被分解为因数之积a*b,那么只有两种情况: a = b = sqrt(n); a和b一个大于sqrt(n),另一个小于sqrt(n)。 所以只要选取i <= sqrt(n)范围内的数,进行上述筛选,就可以完成全部范围内的筛选工作; 返回全部deleted[i] = false的i的集合,即是要求的素数集。 // 素数筛:埃氏筛 // O(n * ln(ln n)) // Mars 2021.11 function getPrime(n = 100) { let deleted = new Array(n + 1).fill(false); let primes = []; // 筛选到sqrt(n)即可 let e = Math.sqrt(n); for (let i = 2; i <= e; i++) { if (!deleted[i]) { // -- 为何是 j=i 开始? // 只需要从当前找到的质数i之后进行筛除即可,之前的数已经被筛除过了。 // 例如: 当前找到的数是5,那么不需要再对2*5, 3*5, 4*5进行筛除了,因为之前已经筛选过了,只需要从5*5开始。 for (let j = i; i * j <= n; j++) { deleted[i * j] = true; } } } deleted.forEach((i, index) => { if (!i && index >= 2) primes.push(index); }); return primes; } 10.2 线性筛选法(欧拉筛) 埃氏筛选法中,一个合数可能会被多个素数筛除: 例如12会被2和3筛除,从而多次执行delete[12] = true操作。 线性筛选法降低时间复杂度到O(n),其中主要是避免了这种情况。它的基本思想: 对于一个合数,只被它最小的质因数筛除。 在线性筛中,12这个数只被2筛除一次,不会被3筛除。 基本思路: 与埃氏筛相同,假设要找的素数范围range是[2,n]; 维护一个数组deleted,长度为n+1,初始时任何数都没有被筛除,数组值全部为false(表示未筛除); 同时维护一个被筛选出的质数数组primes,初始为空数组; 从i = 2开始遍历deleted(不论其是否已被筛除掉),如果deleted[i] = false(没被筛除),则将i加入到primes数组中; 然后遍历数组primes,对每一个已查找到的质数s = primes[j]: 如果i % s === 0,说明当前的数i不是最小的质因数,应该停下当前的筛选过程(停止遍历primes); 否则,如果s*i <= n,筛除掉s * i这个数:deleted[s*i] = true; 当全部筛选完毕,primes中即是结果素数集。 // 素数筛:线性筛 // O(n) function getPrime2(n = 100) { let deleted = new Array(n + 1).fill(false); let primes = []; for (let i = 2; i <= n; i++) { if (!deleted[i]) primes.push(i); for (let j of primes) { if (j * i > n) break; // 只筛选范围内的数 deleted[j * i] = true; // 利用当前已找到素数j,对i*j进行筛除 if (j % i === 0) break; // 如果当前素数j是i的因数,那么直接结束j后续的循环,因为i=n*j,后续的数应该用j进行删除而不是j之后的 } } return primes; } 11. 模拟退火算法 模拟退火 模拟退火是一种随机化算法。当一个问题的方案数量极大(甚至是无穷的)而且不是一个单峰函数时,我们常使用模拟退火求解。 模拟退火时我们有三个参数:**初始温度 ,降温系数 ,终止温度 **。 12. 快速幂、快速乘法 12.1 快速幂 快速幂可以以O(log_n)复杂度求出一个幂。 基本思路: 将ab中的b转化为二进制; 例如13 = (1101)_2表示8+4+1,则: ; 从右到左遍历b的二进制表示,判断其位上是否为1,是则执行对应乘法,将当前a乘入结果。 function fastPow(a,n) { if (n === 0) return 1; let res = 1; while (n > 0) { if (n & 1) { res *= a; } a *= a; n >>= 1; } return res; } 12.2 快速乘法 同快速幂一样,快速乘法用于快速计算m * n。 基本思路: 将m * n中的n转化为二进制; 例如13 = (1101)_2表示8+4+1,a * (8+4+1) = a*8 + a*4 + 0*2 + a*1; 从右到左遍历b的二进制表示,判断其位上是否为1,用一个c表示当前位对应的二进制数值,如果是1则在结果中加上c。 function fastMultiply(m,n) { let r = 0; let c = m; for (let i=0; i<31; i++) { if ((n & 1) !== 0) r += c; c += c; n >>= 1; } return r; } 13. 最长递增子序列 (LIS) 求一个序列的最长递增子序列(LIS),这样的子序列是允许中间越过一些字符的,即留“空”。 例如:[4, 2, 3, 1, 5] 的最长递增子序列为 [2, 3, 5],长度为 3 。 13.1 求最长递增子序列(LIS)的长度 算法基于以下几个事实: 需要保留元素的相对顺序,也就是只有后面的元素可以和前面的形成递增子序列;(想到动态规划) 如果要形成最长的递增子序列,需要让子序列增长得尽可能慢,这样后面的元素才更有可能形成更长的递增子序列。(想到贪心) 基本思路: 结合动态规划和贪心的思想; 用一个dp数组表示找到的最长递增子序列: dp[i]表示:已找到长度为i+1的递增子序列,且序列末尾的最小值为dp[i]; 可知,此时的dp也是一个严格递增序列。(因为长度更长的递增子序列,末尾元素一定更大) 从左到右按序遍历原数组,对每一个元素e: 如果e大于dp的最后一个元素: 直接将epush到dp末尾; 否则,二分查找dp数组,找到第一个【大于等于e】的元素dp[i],并把它替换为e。 (因为保持严格递增,即使相同的值也用后面的替换掉前面的) 最后,dp的长度,就是找到的最长递增子序列长度。 function findLIS(arr) { let dp = []; for (let e of arr) { if (dp.length === 0 || e > dp[dp.length-1]) dp.push(e); else { // binary search: find first > e let l = 0, r = dp.length-1; while (l < r) { let m = Math.floor((l+r)/2); if (dp[m] >= e) r = m; else l = m + 1; } dp[l] = e; } } return dp.length; } 13.2 求最长递增子序列(LIS) 13.2.1 基本思路 上述方法只能求出最长递增子序列的长度,无法得到序列本身,因为在遍历的过程中,dp数组的元素可能被替换,导致最后的dp数组不是要求的最长递增子序列。 那么如何得到序列本身呢? 观察其他事实: dp数组的最后一个元素,一定是要找的最长递增子序列的末尾元素,因为它没有机会被替换; 在更新dp数组的过程中,如果dp[i]被更新为e,则e一定与当时的dp[i-1]形成递增关系,并一同构成以dp[i]为结尾的递增子序列; 基本思路: 用一个数组p,p[t]记录每次用e = arr[t]更新dp[i]时,当前的前一个元素dp[i-1](如果没有则记录为一个特殊标志,如null); 从dp末尾元素开始,找到其在原数组arr的位置arr[t],然后找到对应的p[t],则p[t]是最长递增子序列的上一个元素; 迭代查询,直到p[t]为空,则找到了最长递增子序列。 function findLIS(arr) { let dp = []; let p = []; for (let e of arr) { if (dp.length === 0 || e > dp[dp.length-1]) { let pre = dp.length === 0 ? null : dp[dp.length-1]; p.push(pre); dp.push(e); } else { // binary search: find first > e let l = 0, r = dp.length-1; while (l < r) { let m = Math.floor((l+r)/2); if (dp[m] >= e) r = m; else l = m + 1; } let pre = l === 0 ? null : dp[l-1]; p.push(pre); dp[l] = e; } } let lis = []; let cur = dp[dp.length-1]; while (cur !== null) { lis.push(cur); cur = p[arr.indexOf(cur)]; } return lis.reverse(); } 13.2.2 存在的问题 上述方法存在两个问题: 原数组arr中可能存在重复元素,在回溯查找的时候无法确认对应哪个p[i]; 回溯查找需要遍历原数组,时间复杂度高。 基本解决思路: 用索引代替具体值,无论是在dp还是在p中,这样可保证唯一性,而且无需遍历查找。 完美解决方案: // 查找最长递增子序列 LIS function findLIS(arr) { let dp = []; let p = []; for (let i=0; i<arr.length; i++) { let e = arr[i]; if (dp.length === 0 || e > arr[dp[dp.length-1]]) { let pre = dp.length === 0 ? null : dp[dp.length-1]; p.push(pre); dp.push(i); } else { // binary search: find first > e let l = 0, r = dp.length-1; while (l < r) { let m = Math.floor((l+r)/2); if (arr[dp[m]] >= e) r = m; else l = m + 1; } let pre = l === 0 ? null : dp[l-1]; p.push(pre); dp[l] = i; } } let lis = []; let cur = dp[dp.length-1]; while (cur !== null) { lis.push(arr[cur]); cur = p[cur]; } return lis.reverse(); } 14. 最长公共子序列(LCS) 给定两个序列A和B,求它们共同含有的最长的子序列。 14.1 最长公共子序列长度 基本思路: 对于序列A和序列B,使用动态规划方法,用一个二维数组dp记录找到的最长公共子序列长度,dp[i][j]含义是:第一个序列的[0, i-1]部分,和第二个序列的[0, j-1]部分,能找到的最长公共子序列长度 dp[0][x]和dp[y][0]都是0,因为0位置之前没有元素可以形成子序列; 对于任一dp[i][j]: 如果a[i-1] 与 b[j-1] 相同,那么长度等同于第一个序列的[0, i-2]部分和第二个序列的[0, j-2]部分构成的LCS长度加1: dp[i][j] = dp[i-1][j-1] + 1`; 如果不同,那么长度等同于以下二者的较大值:dp[i][j] = max (dp[i-1][j-2], dp[i-2][j-1]); 第一个序列的[0, i-1]部分和第二个序列的`[0, j-2]部分构成的LCS长度 ; 第一个序列的[0, i-2]部分和第二个序列的`[0, j-1]部分构成的LCS长度 ; dp[a.length][b.length],即为最长公共子序列长度。 14.2 求最长公共子序列 要求出最长公共子序列的值,需要在dp中进行回溯搜索。方法如下: 因为只有在a[i-1] 与 b[j-1] 相同条件下,才会更新dp[i][j]长度+1,此处一定是最长递增子序列的字符; 从右下角开始,每行向左寻找,找到dp[i][j] === dp[i-1][j-1] + 1则记录下当前的a2[j-1]; 向上搜索(r-1),直到dp为0。 function findLCS(a1, a2) { let dp = new Array(a1.length + 1).fill(0).map(i => new Array(a2.length + 1).fill(0)); for (let r = 1; r <= a1.length; r++) { for (let c = 1; c <= a2.length; c++) { if (a1[r - 1] === a2[c - 1]) dp[r][c] = dp[r - 1][c - 1] + 1; else dp[r][c] = Math.max(dp[r - 1][c], dp[r][c - 1]); } } let res = []; let cur = dp[dp.length-1][dp[0].length-1]; let r = dp.length-1, c = dp[0].length-1; while (cur > 0) { while (dp[r][c] !== dp[r-1][c-1]+1) c -= 1; res.push(a2[c-1]); cur -= 1; c -= 1; r -= 1; } r 15. Rabin-Karp 字符串编码 1044. 最长重复子串 Rabin-Karp 字符串编码 Rabin-Karp 字符串编码可以将一个字符串哈希化为一个数值。 常用来对一个长字符串的子串进行哈希化处理,方便子串的比较,降低时间复杂度。 假设字符串str的长度为length,Rabin-Karp 字符串编码的过程如下: 选取一个进制数base,和一个模数mod;(一般base取大于字符空间大小的质数(如31),mod取1e9+7) 预处理,从前到后将str的每一位转化为一个数值(例如a-z对应0-25); 相当于把字符串看做一个base进制数,同base进制计算方式相同,字符串最右侧(最低位)的基为base^0,最左侧(最高位)的基为base^(length-1),依次将字符串各位对应的值乘上各自的基,然后加和在一起得到值res; 将res对mod取模,得到Rabin-Karp编码值。 Rabin-Karp 字符串编码的过程很好理解,但是计算时因为涉及到较大结果数的取模,有一些细节: function RabinKarp(str, R = 11, mod = 1e9+7) { let res = 0; for (let i = 0; i<str.length; i++) { // 分步计算、取模 // 1. 从最高位开始,从左向右算; // 2. res * base 相当于将当前计算结果左移一位,然后加入当前位的结果 str[i].charCodeAt(0) - 97; // 3. 多个结果的加和取模,相当于在每一步取模,因此每步之后都进行取模操作,防止超出范围。 res = ((res % mod) * (R % mod)) % mod + i.charCodeAt(0) % mod; } return res; } 使用 Rabin-Karp 对子字符串进行编码,可以很方便地在原始字符串中左右移动,并计算出相邻字符串的编码。 假设[abc]d向右移动到a[bcd],只需要: 在原编码res的基础上,减去a占有的部分res -= num(a) * base^(length-1); 这里注意:因为进行过取模操作,所以res可能会比num(a) * base^(length-1)小,因此结果可能为负。我们需要保证编码为正值,因此需要在为负的时候,再加上一个mod。 实际操作为: res = ((res + mod) - (num(a) * base^(length-1) % mod)) % mod 将原编码左移一位: res *= base; 加上新字符d的编码: res += num(d); 取模: res %= mod。 Robin-Karp编码映射的哈希表大小为mod,理论上存在哈希冲突的可能。 但是,Robin-Karp证明:因为哈希冲突而导致错误匹配的概率,与mod的大小成反比,因此选取一个较大的mod值可以避免这个问题。(这称为蒙特卡洛算法,利用较小的概率避免冲突) Robin-Karp 字符串匹配(指纹匹配) function RobinKarpSearch(str = '', ptn = '') { // Mars 2022.03 if (ptn.length > str.length) return false; if (ptn === '') return true; let code = 0, ptnCode = 0; let len = ptn.length; let R = 11; let mod = 1e9+7; for (let i=0; i<len; i++) { code = ((code % mod) * (R % mod)) % mod + str[i].charCodeAt(0) % mod; ptnCode = ((ptnCode % mod) * (R % mod)) % mod + ptn[i].charCodeAt(0) % mod; } // [a,b,c],d,e -> a,[b,c,d],e // ( code - code[i] * R^(len-1) ) * R + code[i+len] let RPow = 1; for (let i=0; i<len-1; i++) RPow = ((RPow % mod) * (R % mod)) % mod; for (let i=0; i+len-1<str.length; i++) { if (code === ptnCode) return true; if (i+len < str.length) { let a = (str[i].charCodeAt(0)* RPow) % mod; let b = (code + mod - a) % mod; code = ((b % mod) * (R % mod)) % mod + str[i+len].charCodeAt(0) % mod; } // code = (code + R - str[i].charCodeAt(0) * RPow) * R + str[i+len].charCodeAt(0); } return false; } 16. Floyd 判圈算法 Floyd判圈算法,用于寻找一个链表中的环的入口。它的时间复杂度是O(n),空间复杂度是O(1)。 算法过程 算法如下: 使用快慢指针法。慢指针s初始在链表首节点,每次向后移动一步,快指针f初始在链表首节点,每次向后移动两步; 向后移动快慢指针,直到二者相遇(重合); 初始化慢指针s为链表首节点,然后快慢指针同时向后移动,每次均只移动一步; 直到二者再次相遇,相遇位置就是链表环的入口节点。 原理分析 如图,假设快慢指针第一次在图中紫色节点相遇,那么根据算法描述(其中n为环的总长): 慢指针走过的路径长度为:s1 = m + An + x; 快指针走过的路径长度为:s2 = m + Bn + x; 快指针走过的路径长度s2,应为慢指针走过长度的2倍,因此s2 = 2 * s1,那么: s1 = s2 - s1 = (B - A)n 也就是说s1和s2都是环路径总长n的倍数。初始化s = head后,当s再次走过m到达环的入口: s从起点走过的长度: m; f从起点走过的总长度:s2 + m; 也就是说,f相当于从起点走过m,到达环的入口,又在环中走过了若干整圈,此时仍然在环的入口位置。 那么二者必定在环的入口相遇。 17. Manacher 算法 OI-WIKI: Manacher算法 Manacher算法,俗称“马拉车算法”。用于在线性时间复杂度O(n)内,寻找一个字符串的最长回文子字符串。 Manacher算法中的一些基本概念定义 回文中心:回文字符串的中间位置。注意这里的中间位置既可以是一个元素(奇数长度回文串),也可以是两个元素间的空位(偶数长度回文串)。对于一个位置i,它可以标记两种回文中心,它标记的奇数长度回文字符串中心是i所在的元素arr[i],它所标记的偶数长度回文字符串中心是arr[i]前方的间隙; i位置的最长回文子字符串:从i标记的回文中心向两侧扩展的,具有最长回文半径的子字符串; i位置的最长回文半径p[i]:从i所标记的回文中心,到最长回文字符串最右侧字符r为止,这个[i, r]闭区间内的字符总数; 最长回文区间:i位置标记的回文中心扩展而成的最长回文子序列所在的区间范围; 中心扩展法:从某一回文中心向两侧扩展,对比两侧的字符是否相同,直到两侧不同则找出了最长回文子字符串。这个方法找到全部回文子字符串的总时间复杂度是O(n^2); Manacher 算法 基本算法过程 核心思路是: 充分利用前面已经计算过的回文字符串信息,在一般情况下做到O(n)复杂度寻找当前位置的最长回文子字符串; 在无法利用前面的信息处理的部分,使用中心扩展法处理。 需要对奇数和偶数回文子字符串进行分别处理。使用后面的加井号统一处理方法,可以合并两种情况。 算法过程: 从左到右遍历原字符串,我们的目的是对于每个位置i,找到其标记的回文中心,所对应的最长回文半径p[i]; 遍历过程中,维护已经找到的最靠右(r最大)的最长回文子字符串边界值l和r,区间[l,r]为当前已找到的最靠右最长回文子字符串所在区间,假设其回文中心为id; 假设当前遍历到的回文中心位置为i,且id < i <= r,我们可以利用已找到的id最长回文区间信息,快速获取i的最长回文区间,方法是: 找到i关于回文中心id的对称位置j = 2 * id - i; 可知j一定小于id,j位置的最长回文半径p[j]已经计算过,我们找到j的最长回文子字符串左边界l[j] = j - p[j] + 1; 如果: 其位于id的最长回文区间内,即l[j] >= l,那么根据回文特性,此时p[i] = p[j]; 如果其超出了id的最长回文区间,即l[j] < l,那么我们只能确定在区间内的部分是回文的,也就是p[i]至少为j-l+1(等同于r-i+1`)。区间外的部分无法确定,需要继续用中心扩展法探索; 综上,我们可以将i位置的回文半径初始化为二者中的较小值:Math.min(p[j], r-i+1) 每次找到新的最右侧回文字符串,更新对应的id、l和r。 让奇偶回文子串统一处理的方法 可以看到,偶数长度回文字符串的回文中心是两个字符中间的间隙。 如果用一个占位字符填充每一个字符间隙,那么偶数回文串长度将变为奇数,同时奇数回文串仍保持为奇数回文串。 这样,可以将奇偶回文子串的情况统一起来,进行一次遍历即可。 abcdcba –> #a#b#c#d#c#b#a# 这样变更后,回文半径p[i]将变得更长。它与原回文半径的对应关系为: 实际半径 = Math.floor( p[i] / 2 ) 实际回文子串总长度 = p[i] - 1 Manacher算法代码 奇偶统一处理 function manacherUnified(str) { // 初始化字符串: // 'abc' --> '#a#b#c#' // 空位用#占位,变偶数回文子串为奇数。 let arr = ['#']; for (let i of str) { arr.push(i, '#'); } // manacher 算法 let l = 0, // 已找到的最右侧回文字符串的左右区间。 r = -1; // 初始化 r = -1是因为要让第一次的匹配i = 0达成i > r的条件,从而使用中心扩展法更新l和r。 // i位置对应的奇数最长回文子串的回文半径,因为插入了#,与实际的半径长度对应关系为: // 实际半径 = Math.floor( p[i] / 2 ) // 实际回文子串长度 = p[i] - 1 let p = new Array(arr.length).fill(0); for (let i = 0; i < arr.length; i++) { // 充分利用已找到的回文区间,对新位置的回文半径进行初始化。 let cur = i > r ? 1 : Math.min(p[l + r - i], r - i + 1); // 中心扩展法,探索未知部分,扩大回文半径 while (i - cur >= 0 && i + cur < arr.length && arr[i - cur] === arr[i + cur]) cur += 1; // 记录当前位置的回文半径 p[i] = cur; // 如果找到了更靠右的回文子串,更新l和r为最新值 if (i + cur - 1 > r) { r = i + cur - 1; l = i - cur + 1; } } return Math.max(...p) - 1; // 根据需要进行修改,此处是返回了最长的回文 } 奇偶子字符串分开处理 下面是奇偶子字符串分开处理的Manacher算法代码。 function manacher(str) { // find longest palindrome substring. // abcabcabca // p[i] === cur --> [i-cur+1, i+cur-1] // odd substring. let l = 0, r = -1; let p = new Array(str.length).fill(0); for (let i = 0; i < str.length; i++) { // 利用已有的信息初始化回文半径 cur let cur = i > r ? 1 : Math.min(p[l + r - i], r - i + 1); while (i - cur >= 0 && i + cur < str.length && str[i - cur] === str[i + cur]) { cur += 1; } p[i] = cur; if (i + cur - 1 > r) { // 找到了新的最右侧回文串 l = i - cur + 1; r = i + cur - 1; } } // even substring. l = 0, r = -1; let pp = new Array(str.length).fill(0); for (let i = 0; i < str.length; i++) { let cur = i > r ? 0 : Math.min(pp[l + r - i + 1], r - i + 1); while (i-1-cur >= 0 && i+cur < str.length && str[i-1-cur] === str[i+cur]) { cur += 1; } pp[i] = cur; if (i + cur - 1 > r) { // 找到了新的最右侧回文串 l = i - cur; r = i + cur - 1; } } pp.forEach((i,idx) => { if (i > p[idx]) p[idx] = i; }); return p; } 18. 字符串压缩算法 LZW 压缩算法 LZW压缩算法是利用字符串自身重复出现的子串,进行编码压缩的方法。它不需要额外储存字典,解码只需要编码本身。 具体实现见《数据结构》对应章节。 霍夫曼编码算法 霍夫曼编码的基本原理如下: 将字符(8bit)用更短的二进制串来表示; 统计字符出现的频率,出现频率越高,则分配更短的二进制串给它,目的是尽量提高压缩率; 为了能从左到右依次恢复原字符,而不造成歧义,所有用于替代表示字符的二进制串,互相都不是彼此的前缀; 将字符的字典和编码后的二进制串放在一起即可。 霍夫曼树 为了实现霍夫曼编码,找到每个字符应该对应的二进制串,需要建立霍夫曼树。 对于一个给定的字符串str,霍夫曼树的建立方式如下: 统计每个字符的出现频次,以频次值建立节点,组成集合set; 从当前set找到出现频次最小的两个字符节点,将它们移出。同时以它们作为左右子节点,创建一个父节点,值为它们二者之和。将这个父节点加入set; 循环进行,直到set中只有一个元素,此时为建立的霍夫曼二叉树的根节点; 确定每个字符的二进制串,方法如下: 从根节点开始向下寻找字符所在叶子节点的路径,向左前进一步则记录一个0,向右前进一步则记录一个1; 到达字符所在的叶子节点,路径记录的二进制串就是这个字符的表示串。 在实现时,因为每次需要找到集合中的最小频次值和次小频次值,可以建立小根堆实现。 代码实现 function huffmanEncode(str) { // 1. count the number of each ltr. let cnt = new Map(); for (let i of str) { if (!cnt.has(i)) cnt.set(i, 1); else cnt.set(i, cnt.get(i) + 1); } // 2. build min heap. let heap = new MinHeap(); for (let [k, v] of cnt) { heap.add(new Node(v, k)); } // 3. pick & build. while (heap.getSize() > 1) { let a = heap.pick(), b = heap.pick(); let root = new Node(a.val + b.val, null, a, b); heap.add(root); } // 4. get code of each ltr. let dict = new Map(); let root = heap.pick(); function dfs(node = root, path = []) { if (node === null) return; if (node.ltr !== null) { dict.set(node.ltr, path.join('')); return; } path.push(0); dfs(node.left, path); path[path.length-1] = 1; dfs(node.right, path); path.pop(); } dfs(); // 5. merge result. // [ltr_number, {ltr,code...}, `_`, stringEncoded] let res = [dict.size]; for (let [k,v] of dict) { res.push(k,v); } res.push('_'); for (let i of str) { res.push(dict.get(i)); } return res.join(''); } class Node { constructor(val, ltr = null, left = null, right = null) { this.val = val; this.ltr = ltr; this.left = left; this.right = right; } } class MinHeap { constructor(arr = [], size = Infinity) { this.heap = arr; this.size = size; for (let i = Math.floor(this.heap.length / 2) + 1; i >= 0; i--) { this.down(i); } while (this.heap.length > this.size) this.heap.pop(); } left(i) { return 2 * i + 1; } right(i) { return 2 * i + 2; } parent(i) { return Math.floor((i - 1) / 2); } swap(i, j) { [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]; } down(i) { let l = this.left(i); let r = this.right(i); let smaller = i; if (l < this.heap.length && this.heap[l].val < this.heap[smaller].val) smaller = l; if (r < this.heap.length && this.heap[r].val < this.heap[smaller].val) smaller = r; if (smaller !== i) { this.swap(smaller, i); this.down(smaller); } } up(i) { let p = this.parent(i); if (p >= 0 && this.heap[p].val > this.heap[i].val) { this.swap(p, i); this.up(p); } } add(e) { this.heap.push(e); this.up(this.heap.length - 1); while (this.heap.length > this.size) this.heap.pop(); } top() { return this.heap.length > 0 ? this.heap[0] : undefined; } getSize() { return this.heap.length; } pick() { this.swap(0, this.getSize() - 1); let r = this.heap.pop(); this.down(0); return r; } } // test let dict = huffmanEncode('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'); console.log(dict); // 28,000000D00000100h00000101b0000011p00001l0001i001r0100q010100E01010100L010101010U010101011x0101011g0101100.0101101v0101110f0101111n0110u0111a1000o1001t1010c10110m10111 110s11100d11101e1111_0101010101001010011111011111000100001111000111101111101110110010001100101001101110000110101101000101111111101000000011010110100101101110011111011010101111101001110100110100011101001000010011110000110110001011001011001101111000100110100000001101110011111110111011101100111011110010111111001011110011110111010101111101110000110010100110001011010110001111010011110101110110101011001111010110000110000000011100101001111110111110101101110110010001100101001111110101111000010110001101000110100000010010101000111100001011011100101010111010110111101100011011111010001110111010111001011000110111110010111011110110001100010111000000110010100011100111100110011010011110010100100011111101110111101010111111010010110001101010001010001100101101100111000100011000101111011010011100001100000000111001010000111100110011000111100001110011110101101000000100101010001110010000111011110101011110111110001101011010011011110111100111101100111010110100101101110011110101000111100010100101101110000001000111001111001101000011110101111110001010001110100111111011101100100011001010011000101101100100111100001010011110000010111110110111011111010000110101100010110110010111010010001011100001101010001010111111001011101111000100110101101111111001110011111101011000100010001011110111110111011001000110010100111111011110111110010111101110101100001100010101100110011100010001100011000001100001000011000101001110100010110111001010100010101110110111100001101011110111010011011100001011010101101001101101011010001111101101000101011010110011100001001111011000101010001010110011010010110110000010100100100111101111101101010000000110111000111011010101100010110110101100111000100001100011001010001110011101001010111101011110011011000110001101110111111110011110100011101101010110101111001000100010011010110100001100011011111000111101110111111100101011000011000000001110010100011110111 19. 格雷编码 格雷编码是这样一串二进制序列:b1,b2,...,bn,它的特点是: 相邻两个二进制串,仅有一位不同; 首尾两个二进制串,也仅有一位不同。 计算单个元素二进制长度为n的格雷码(元素值在0 ~ 2^n - 1范围),方法如下: 长度为0的格雷码序列为[0],长度为1的格雷码序列为[0,1]; 现在尝试用长度为1的格雷码,构造长度为2的格雷码: 先将长度为1的格雷码序列反转,接在它自身后面; 然后从末尾开始,对反转的后半部分,进行布尔或1 << (2-1)操作。 按同样的方式可以获取单个元素二进制长度为n的格雷码序列G(n): 先将长度为G(n-1)的格雷码序列反转,接在它自身后面; 然后从末尾开始,对反转的后半部分,进行布尔或1 << (n-1)操作。 var grayCode = function(n) { let res = [0]; for (let c=1; c<=n; c++) { let l = res.length-1; for (let i=l; i>=0; i--) { res.push((res[i] | (1 << (c-1)))); } } return res; }; 20. 最短路径算法 在有向图中寻找两点间最短路径的算法,一般有BFS、Floyd算法、Dijkstra算法、A*搜索等。 最短路径 Floyd算法 Floyd算法可以解决多源最短路问题(一次性解决所有节点间的最短路)。 它的特点是容易理解,写法简单(for-for-for算法),缺点是复杂度较高(O_N^3)。 算法流程 Floyd算法采用动态规划方法,通过逐一选取中间节点,判断任意两点经由中间节点的最短路径,来动态更新各点间的最短路径值,从而最后得出任意两点间的最短路径; Floyd算法用一个二维dp[i][j]数组来表示节点i和节点j之间的最短路径长度; 从第一个节点开始,每次选取一个中间节点k,更新任意两点间的最小路径:遍历起点i和终点j,计算i经由k到达j的最短路径,那么dp[i][j] = min(dp[i][j], dp[i][k]+dp[k][j]); k遍历完成后,dp中保存的就是任意两点间的最短路径长度。 算法实现 function floyd(n = 10, edges = [], query = [0, 0]) { let dp = new Array(n).fill(0).map(i => new Array(n).fill(Infinity)); for (let i = 0; i < n; i++) dp[i][i] = 0; for (let [f, t, cost] of edges) dp[f][t] = cost; for (let k=0; k<n; k++) { for (let i=0; i<n; i++) { for (let j=0; j<n; j++) { dp[i][j] = Math.min(dp[i][j], dp[i][k]+dp[k][j]); } } } return query.map(i => dp[i[0]][i[1]]); } Dijkstra 算法 Dijkstra算法用于解决权值非负的有向图中,给定起点s的单源最短路径问题(s到其他点的最短路径)。 算法流程 将节点分为已经确定最短路径的集合S和未确定最短路径的集合U,初始时S集合中只有起点s一个(路径长度为0); 用一个数组dist[i]记录s到i的最短路径长度。初始时,dist[s] = 0,与s直接邻接的结点dist[n]值为s -> n的边权值,其他节点的dist设为正无穷; 每次从集合U中选择出一个路径长度最短的节点t,将其转移到S中,同时更新t的所有邻接节点n路径长度:dist[n] = min(dist[n], dist[t] + val(t -> n)); 循环直到U中无节点,则dist中保存了到达各节点的路径最小值。 实现细节 可以用一个小顶堆来保存所有已找到路径,每次从堆顶取出一个不在集合S中的元素,因为小顶堆的性质,它一定是我们要找的不在S中的最短路径元素; 每次从小顶堆取出不在S中的最短路径元素,更新的路径信息直接加入堆中,无需对堆中的元素进行修改,因为我们只选择不在S的元素,已确定的S中的元素我们会跳过而不会选取; 代码实现 // Mars 2022 function dijkstra(n = 0, edges = [], s = 0) { let e = new Map(); for (let i = 0; i < n; i++) e.set(i, []); for (let [f, t, c] of edges) e.get(f).push([t, c]); let dist = new Array(n).fill(Infinity); dist[s] = 0; let seen = new Set(); // 集合S,已确定最短路的节点 let heap = new MinHeap([ [s, 0] ]); while (heap.getSize() > 0) { let cur = heap.pick()[0]; if (seen.has(cur)) continue; // 只选择没有被确定最短路径的节点 seen.add(cur); for (let [t, c] of e.get(cur)) { if (!seen.has(t) && dist[t] > dist[cur] + c) { dist[t] = dist[cur] + c; // 更新,并直接加入堆中 heap.add([t, dist[t]]); } } } return dist; }

JS执行环境、作用域链和this指向

2021.11.08

JavaScript

作用域(Scope)、闭包(Closure)、作用域链和上下文(Context) JS函数词法环境和作用域问题,还有var和let的区别问题。 一、执行环境(Execution Context) 执行环境,也叫执行上下文,是JavaScript在运行时,用于控制程序执行流程的一种手段。 执行环境记录了当前执行流条件下,可访问的变量空间、访问顺序(作用域链)和上下文(this指向)。 只有以下三种情况会创建新的执行环境: 全局执行环境: 程序开始运行时,创建全局执行环境Global; 函数执行环境(局部执行环境): 函数开始被执行时,会创建自己的执行环境,也叫本地执行环境; 使用eval()函数,也会创建自己的执行环境。(比较少用,不建议使用) 每个执行环境中,都存在一个变量对象VO(或活动对象AO),它的内部属性包含三部分: 当前执行环境内部的各变量(local variables:形参、内部声明变量等); 当前执行环境的this属性指向; 当前执行环境的作用域链(栈结构数据,全局作用域在最底层0位置); 因此,在当前执行环境下声明、修改变量,实际上是在修改当前执行环境下VO对象的属性值,或通过作用域链找到的闭包或全局执行环境对象内的属性值。 执行环境的VO对象中,记录了当前环境的作用域链: 在某一JS执行环境的VO对象上,存有当前环境的作用域链属性。任何执行环境下,作用域链的最底端是全局变量对象window(global); 如果在当前环境执行过程中,遇见函数声明,会对声明的函数func内部进行词法分析(静态分析),分析其内部是否引用了当前环境变量对象VO的属性,或当前环境下作用域链上可访问的某个对象的属性: 如果没有引用任何外部变量:当前函数的[[Scopes]]属性上只有一个全局变量对象Global,位于栈最底部0位置; 如果引用了一个或多个外部变量:则按照当前环境作用域链的先后顺序(最前方是当前执行环境变量对象),以作用域链节点为单位,分别打包成闭包Closure,按作用域链先后顺序压入当前声明函数的[[Scopes]]属性中; 当func被在某个执行环境执行时,会为他创建新的函数执行环境,压入执行环境栈: 取出存放在func内部的[[Scopes]]属性值(栈结构,全局变量对象在最下方0位置),作为当前func函数执行环境的VO对象的作用域链,func内部声明、赋值的属性都在当前环境的VO对象上添加、修改; 当前执行环境下,如查找某一变量var1,先在当前VO对象属性中查找,如果找不到,沿着作用域链(栈)从上到下的顺序一致查找到全局变量对象,如果找不到,则报错找不到变量。 JS引擎会把内存分为栈内存和堆内存。其中: 栈内存:包含全局作用域和函数执行环境栈。 堆内存:存放引用类型数据本体。栈内存中如果有变量引用了堆内存中的引用类型数据(比如对象),在栈内存中实际上是保存了这个对象的内存地址。 执行环境在内存中以栈的形式存储(执行环境栈): JS执行流每进入一个函数,就创建一个新的执行环境,将其压入函数执行环境栈顶部。函数执行完毕,再将执行环境弹出,恢复上层执行环境继续执行。 创建执行环境栈AO的实际操作步骤: 创建AO对象; 寻找形参和函数体内声明的变量名,设置为AO键名,键值为undefined; 传入实参值,替换掉AO中形参的键值undefined; 寻找函数体内直接声明的函数function A(){}...,设置函数名为AO键名,值为对应声明的函数体。 二、作用域 Scope、作用域链、闭包 作用域是针对变量访问这一操作而言的,它是一个变量集合。 如果一个作用域在当前执行环境的作用域链上,那么该作用域中的变量都可以在当前执行环境,沿着作用域链被访问(前提是不与作用域链上方的其他作用域中变量重名)。 JS使用词法作用域。也就是说,作用域是根据代码的静态书写顺序决定的,与如何执行、在哪执行无关。 JS中存在四种作用域: 全局作用域Global: JS程序开始执行就生成的初始作用域; 函数作用域Closure: 函数内部生成的作用域; 块级作用域Block: 使用{}括起来的部分,如果不是函数,形成的作用域是块级作用域; 模块作用域Module: 使用ES6模块引入的变量汇总在一起,单独存在于一个作用域中。 作用域链通过函数声明时内部记载的[[Scopes]]内部属性(一个栈),和函数执行环境中变量对象VO记载的作用域链属性来实现,它的原理是: JS程序执行过程中,如果遇见函数声明或函数表达式(具名或匿名): 如果当前执行环境是全局:则函数的[[Scopes]]内部属性被设置为只有全局变量对象window/global; 否则,新声明的函数可能存在对其他函数作用域内变量的引用: 对新创建的函数内部进行静态词法分析,找到它引用的外部作用域变量(LHS查询或RHS查询都有效); 除最末端全局作用域外,对当前执行环境作用域链内其他作用域中变量进行一轮筛选,只保留被新声明函数引用的变量; 将筛选后的被引用外部变量,按各自作用域分组,分别组成新的对象叫做闭包Closure(保持原链中的位置关系),放入堆内存中; 将闭包按原作用域链顺序(栈顺序),组成新的作用域链,保存在新声明函数的内部[[Scopes]]属性中。 如果没有任何对外部变量的引用,新声明的函数[[Scopes]]属性上只有全局变量对象window/global; 当声明的函数fn被执行: 创建新的函数执行环境,包含新的活动对象AO; 将函数fn的内部[[Scopes]]属性值取出,作为当前执行环境变量对象VO的初始作用域链; 将当前函数执行环境,压入函数执行栈,继续执行; 执行过程中,当前函数作用域始终在作用域链的最顶部。查找变量先在当前变量对象VO上查找,如果找不到,则沿着作用域链向下在各作用域中查找,一直到全局作用域,找不到则报错。 2.1 块级作用域 一对花括号会创建一个块级作用域。if{}、for(){}、while(){}和普通的{}都会创建块级作用域。 for{}和while{}循环,相当于多次执行了块级作用域创建,并在新的块作用域内修改变量。 函数内部是函数作用域,全局环境是全局作用域。 2.1.1 for、while循环内声明函数 for{}和while{}循环,相当于多次创建了块级作用域。如果使用let声明变量,每次是在新的块作用域内声明、修改变量。 因此每一个在for、while循环内声明的函数,都通过[[Scopes]]记录下当前的外部作用域,也就是不同的块级作用域,因此当它们在执行的时候,引用的也是不同的块级作用域内的不同变量。这也就解释了如下的代码: for (let i = 0; i < 10; i++){ setTimeout(() => { console.log(i) }) } // 0,1,2,3,4,5,6,7,8,9 // let声明的i,绑定在块级作用域内,相当于有如下块级作用域{i:0}、{i:1}、{i:2}、{i:3}... // setTimeout内部声明的箭头函数的[[Scopes]],记录了每次迭代时产生的外部的块级作用域里的i。 // 当后续这个箭头函数按序执行的时候,创建函数执行环境,作用域链引用的是创建时[[Scopes]]记录的各自外部块级作用域,因此log的是每个块级作用域下的不同i,也就是0-9 // 详细解释如下 —— Mars 2021.08.31: // ---------------------------------- // 1. 函数是引用类型,匿名箭头函数在作为参数传递的时候,相当于在外部先声明、再引用(传递的是内存地址); // 2. 在外部声明的时候,[[Scopes]]属性里记录了箭头函数的作用域链:Block{i: 0/1/2...} -> Global; // 3. for循环相当于是多次创建了块级作用域{},并在每一个块级作用域中使用let单独声明了变量i,并在每个块级作用域中独立执行内部函数setTimeout; //----------------------------------- // Arrow function is declared outside of setTimeout func. for (let i=0; i<2; i++) { let arrow = function () { console.log(i); } setTimeout(arrow, 0); } // same to for (let i=0; i<2; i++) { setTimeout(() => { console.log(i); },0); } // same to { // Block Scope 0 let i = 0; setTimeout(() => { console.log(i); },0); } { // Block Scope 1 let i = 1; setTimeout(() => { console.log(i); },0); } 三、上下文 Context: this属性指向 对于当前执行环境,它的上下文,指的是当前执行环境中变量对象VO的this属性值 3.1 哪里有this? 什么时候才会改变this? this存在于执行环境的变量对象VO中。 也就是说,以下两种执行环境内部,都存在this属性。 全局执行环境:this指向全局对象window(或global); 非严格模式,浏览器全局执行环境中this 指向 window对象(Node是global对象); 严格模式,全局作用域下this为undefined。 函数执行环境:函数被执行,为它新建了执行环境压入环境栈。其中的this属性指向,与调用函数的对象或new操作符等有关。 函数作用域中的this指向,只有函数被调用的时候才被决定。 改变函数fn执行时的执行环境内this指向的几种情况: 函数直接裸执行; 函数作为对象的方法,被执行; 使用了new 操作符; 使用了fn.call、fn.apply等显式设置this的方法; 使用了fn.bind强行绑定fn的this。 3.2 普通函数内部的this 使用函数声明或函数表达式声明的函数,内部的this: 指向将这个函数作为方法调用的对象,也就是函数作为方法被调用时,点号前面的对象。 如果函数被直接裸调用,而不是作为对象的方法(不使用’obj.func’形式,而是func()形式),那么浏览器下默认情况函数的this为window,严格模式为undefined。 3.3 箭头函数内的this 箭头函数内部作用域的没有自己的this,它始终引用它被声明的时刻,执行环境变量对象的this。 翻译成人话: 全局作用域中定义的箭头函数,this值始终指向window,永远不会改变。 一个函数A中定义的箭头函数,箭头函数里的this就等同于函数A执行时的this。 只要A的this不变,箭头函数在任何地方以任何形式调用this都不变。 call、apply和bind对箭头函数都无效。 function outer(){ // 这里inner是箭头函数,它内部的this和它定义上下文outer的this是一致的。 let inner = ()=>{ console.log(this) } return inner } // 这里outer()被裸调用,outer的this是window。 // 返回内部箭头函数inner,this与outer一致,因此也为window,赋值给了变量a。 let a = outer() a() // window 3.4 new 操作符会改变函数执行环境this指向 使用new操作符 + 构造函数语法,会改变新创建的构造函数执行环境内部的this指向。 构造函数执行环境内的this,被修改为指向当前环境 new 操作符左侧被赋值的变量(新创建的实例); 即使是被bind(context)绑定过上下文的函数,在使用new的时候,其内部的this仍然会被修改指向为新创建的实例。 class People { constructor (name) { this.name = name; } }; let p1 = new People('Mars'); // People函数执行环境,被修改为p1; // 因此,People中进行的this.name = name操作,相当于是p1.name = name操作。 3.5 call、apply和bind修改this call、apply和bind只对普通函数有效,箭头函数无效。 它们的作用是显式修改函数执行时,执行环境中this的指向。 bind的修改是长期的,call和apply是一次性调用。 四、var和let的区别 4.1 是否有块级作用域 var声明的变量只能存在于函数作用域和全局作用域,没有块级作用域。在块级作用域内部声明的var,会穿透块级作用域泄漏到外部。 let声明的变量可以存在于块级作用域,块级作用域内部用let声明的变量,只能在块内部访问,外部无法访问。 4.2 变量提升、暂时性死区 var声明的变量,声明会被提升到块级作用域的头部,而赋值还是在本地。 // other js code.. var a = 2; 其实是发生了下面的事情: var a; //此时a是undefined. // other js code.. a = 2; 因此var声明的变量可以提前使用,只是此时为undefined。 而let声明的变量,在当前代码块开始位置直到let声明之前区域,叫做暂时性死区,不可访问,否则报错。 4.3 var可以重复声明 var可以重复声明同名的变量,后面的覆盖前面的。而let不可以(报错)。 4.4 是否会变成全局变量 let 在全局作用域下声明变量,不会变成全局变量,也就是不会出现在window的属性中; var在全局作用域下声明变量,会默认变成window的属性。 五、箭头函数和普通函数的区别 箭头函数: 执行环境中没有自己的this,this引用定义时执行环境的this; 不能使用new操作符; 没有prototype属性; 内部没有arguments对象; call和apply、bind无效; 内部不能使用yield关键字,因此不能用作generator。 普通函数 与之相反。 箭头函数不适用的场景 箭头函数本身不能用作: 构造函数; Generator函数; 如果箭头函数内部含有this,它不适用于: 定义对象的方法; 动态上下文的回调函数;

数据结构基础

2021.11.04

Data Structure

基础数据结构 一、基本常见数据结构 1. 队列、栈、背包 队列:先进先出; 栈:先进后出; 背包:集合类型。只收集元素,无法按顺序遍历,也无法删除元素。(可以判断是否为空,也可以迭代所有收集到的元素) 2. 数组与链表 2.1 数组 数组是长度在创建时就固定的一种数据格式,每个元素类型统一,因此每个数组元素占用的内存空间相同。 一般会为数组分配一块连续的内存空间,这样只需要一个起始内存地址,就可以利用【起始地址 + 元素大小 * 索引】快速访问数组内任意索引位置的元素. 2.1.1 数组的优缺点 优点:访问一个固定位置的数据很迅速; 缺点:插入数据、删除数据都很慢(因为影响到操作位置后面的元素)。 2.1.2 数组各操作的时间复杂度 对于一个数组Array: 获取元素:O(1); 从非尾部删除元素:O(n); 从尾部删除元素: O(1); 更新元素:O(1); 非尾部插入元素:O(n); 尾部插入元素: 静态数组:O(n); 动态数组:平均为O(1); 复制数组:O(n) 2.2 链表 链表由一个个节点组成,每个节点都储存有自己的数据data和下一个节点的引用地址next。 链表默认是单向的,只能从头到尾。当然特殊情况也可以选择双向链表。 如果链表中的元素,从前到后按序排列(从大到小、从小到大),则称为有序链表。 2.2.1 链表的优缺点 优点:便于插入、删除数据; 缺点:访问数据比较慢,访问任何数据都需要从头遍历。 2.2.3 链表的各操作时间复杂度 对于一个双向链表: 获取头部:O(1); 获取尾部:O(1); 获取中间结点:O(n); 插入、删除头部:O(1); 插入、删除尾部:O(1); 插入中间结点:查找O(n) + 插入O(1) 查找结点:O(n) 3. 哈希表 (Hash Table) 3.1 什么是哈希表 哈希表也叫散列表。 本质上,哈希表是将字符串或其他数据类型的数据,通过函数映射为一个唯一(或基本唯一)的数字值(叫做哈希值),然后将这些数字值通过某种函数与一个数组的索引一一对应,从而实现将数组索引与原始数据一一对应。 这样就可以利用数组索引查询的快速性,达到迅速查找一个数据的功能。 当两个不同元素哈希后的数组索引冲突时,可以采用链地址法(在当前数组位置创建链表,用来储存冲突的数据)。 3.2 哈希函数的实现方法 哈希函数将原始数据计算为一个唯一(或基本唯一)的数字哈希值,然后将其压缩到哈希表长度范围内。 // Hash a string to array index. -- Marswiz function M_hashStringToArrayIndex(str, size) { // `str` for original string data // `size` for aim array length range // initial hashCode is set to zero. let hashCode = 0; for (let i = 0; i < str.length; i++) { // choose 37 here for cal the hash code. Any prime number can be chosen. hashCode = hashCode * 37 + str.charCodeAt(i); } // compress into aim array length range. return hashCode % size; } 3.3 哈希表的优缺点 优点: 快速查找、插入; 缺点: ① 空间利用率低,中间存在空元素; ② 无序,无法通过固定顺序遍历,也不能快速找到最大最小值; ③ 一旦需要扩容,代价很大。 3.4 哈希表各操作时间复杂度 对于一个哈希表HashMap: 插入键值对:平均O(1),最坏O(n); 删除键值对:平均O(1),最坏O(n); 查找键值对:平均O(1),最坏O(n); 最坏情况: 哈希表中所有元素都冲突了,在一个键上形成了很长的链表。这时哈希表相当于链表。 4. 二叉树 二叉树一个节点最多可以有两个子节点,拥有的子节点数目叫做这个节点的度。 二叉树的最深层数,叫做二叉树的深度。 完全二叉树(Complete Binary Tree): 除最后一层外,每层结点都完全填满。最后一层上允许不填满,但是结点必须靠左排列。 完美二叉树(Perfect Binary Tree):所有非叶子节点都具有2个子节点,而且所有叶子节点的深度都相同。(每一层都被填满的二叉树) 完满二叉树(Full Binary Tree):所有非叶子节点都有2个子节点。 平衡二叉树(Balanced Binary Tree): 所有结点的左子树和右子树深度差不超过1; 扩充二叉树:将二叉树所有的空子树位置,都用一个特殊的空树叶填充,形成的二叉树。 4.1 二叉搜索树 二叉搜索树是有序的树。它有以下特点: 二叉搜索树必须是完全二叉树; 二叉搜索树的所有节点的值,大于其左子节点,小于其右子节点; 二叉搜索树的任意子树也是二叉搜索树。 如果一个二叉搜索树左右两个子树深度差≤1,则叫做平衡二叉搜索树。 4.2 二叉树的存储方式 二叉树有两种存储方式:1. 指针 2. 数组 指针形式是通过节点设置left、right指针,将节点连接成二叉树。 数组形式是将二叉树按从上到下,从左到右的顺序储存在一个数组里。任一索引位置i的左子节点为2i+1,右子节点为2i+2 4.3 二叉树遍历方式 二叉树主要有两种遍历方式: 深度优先遍历:先往深走,遇到叶子节点再往回走。 前序遍历 中序遍历 后序遍历 广度优先遍历:一层一层的去遍历。 层序遍历 5. 图 图的组成 图由顶点和边组成。 图的术语 相邻顶点:一条边连接的两个顶点; 度:依附于一个顶点的边的总数; 子图:一幅图中所有边的一个子集; 路径:由边顺序依次连接的一系列顶点; 路径长度:路径中包含的边的个数; 简单路径:没有重复顶点的路径; 简单环:除了起点和终点相同外,其余没有重复顶点的路径; 连通:两个顶点间存在路径; 连通图:所有顶点都相互连通的图; 无环图:不包含环的图; 密度:已经连接的顶点对占全部可连接的顶点对的比例; 有向图:图中的边是有方向的,只能单向通过; 无向图:图中的边是无方向的,两边都可以走通; 二分图:能够将顶点分为两部分,每个边连接的两个顶点都分别属于不同的部分; 强连通:有向图中的两个顶点,如果互相可达,则它们是强连通的;(一个有向环的所有顶点,都是强连通的) 反向图:有向图中的边全部反转,所形成的图;(图G的反向图,用GR表示) 图的一些特殊结构 自环:一条边连接的两个顶点,是同一个顶点; 平行边:无向图中,连接同一对顶点的两条不同边; 无向图的基本API 标准图的输入、输出结构表示: V E edge1 edge2 edge3 … 其中第一行中的V和E表示图中的总顶点数和边数,后续的每一行为一对用空格隔开的顶点编号,表示这两个顶点间存在一条边。 构造:Graph(in),从标准输入读取一个图; 顶点数: getVNum(),获取图的全部顶点数; 边数:getENum(),获取图的边数; 添加边:addEdge(v1, v2),在v1和v2之间添加一个边; 获取相邻顶点:getAdjV(v),获取顶点v的全部相邻顶点; 转化为标准图输出:toString(),将当前图转化为标准图输出结构; 图的搜索 深度优先; 广度优先; 连通分量个数、连通性查询;

Web Socket协议

2021.11.01

Network

Web Socket协议 Web Socket 协议诞生的原因(相比于http的优势) WS支持服务端推送: http协议中,请求只能由客户端发起,无法进行服务端推送(获取实时信息只能轮询,浪费网络资源); WS在建立连接之后,数据包头部字段较轻: http协议每次都要携带完整头部,字节数较大。 WS支持二进制流式传输; WS可扩展,用于实现自定义的子协议。 Web Socket的应用场景 即时通讯应用,即时音视频 Web Socket连接的建立 WebSocket复用HTTP的握手通道。客户端通过HTTP请求与WebSocket服务端协商升级协议。 协议升级完成后,后续的数据交换则遵照WebSocket的协议。 步骤如下: 1. 客户端申请协议升级 客户端向服务器发送http请求,头部如下: GET / HTTP/1.1 Host: localhost:8080 Origin: http://127.0.0.1:3000 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw== 其中有四个关键头部: Connection: Upgrade:表示要升级协议 Upgrade: websocket:表示要升级到websocket协议。 Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。 Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如防止恶意的连接,或者无意的连接。 2. 服务端响应协议升级请求 HTTP/1.1 101 Switching Protocols Connection:Upgrade Upgrade: websocket Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU= 其中的Sec-WebSocket-Accept字段,是根据请求的Sec-WebSocket-Key首部计算出来的,计算的方法如下: 将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11字符串拼接。 通过SHA1计算出摘要,并转成base64字符串。 Web Socket 的数据帧格式 WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。 发送端:将消息切割成多个帧,并发送给服务端; 接收端:接收消息帧,并将关联的帧重新组装成完整的消息; 参考资料 WebSocket:5分钟从入门到精通

重绘和重排

2021.10.10

Performance

性能优化: 重绘和重排 什么是重绘和重排 重排:当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为重排。 重绘:当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并仅重新绘制它本身,这个过程称为重绘。 重排必将引起重绘,重绘不一定会引起重排。 重排 (Reflow, 也叫回流),为什么不好 重排比重绘的代价要更高。 造成回流的常见操作: 页面首次渲染 浏览器窗口大小发生改变 元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等) 元素字体大小变化 添加或者删除可见的 DOM 元素 激活 CSS 伪类(例如::hover) 查询某些属性或调用某些方法: clientWidth、clientHeight、clientTop、clientLeft offsetWidth、offsetHeight、offsetTop、offsetLeft scrollWidth、scrollHeight、scrollTop、scrollLeft、scrollIntoView()、scrollIntoViewIfNeeded() width、height getComputedStyle()、getBoundingClientRect()、scrollTo() 现代浏览器会对频繁的回流或重绘操作进行优化: 浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。 当在JS中,查询上方最后一条包含的那些属性时,浏览器会立即清空队列,执行回流,以确保JS可以拿到准确的数据。 如何避免频繁重排 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

数字在JS中的表示法

2021.10.10

JavaScript

IEEE 754 双精度数字表示法 数字在JavaScript中按IEEE 754标准中双精度浮点数(64bit)来表示。一个数占用64位,其格式如下: 用这种形式表示出来的数字,叫做这个数值的原码。 其中的阶码部分: 阶码有11位,可表示的数范围是0 ~ 2047,这样对应的指数计算结果是2^0 ~ 2^2047,无法表示很小的小数; 因为既需要表示很小的小数,又需要表示很大的数,所以标准在这里规定,取-1023作为移位值,也就是在阶码原有的基础上需要减去1023才是实际的指数值; 这样阶码表示的指数计算结果范围:2^-1023 ~ 2^1024; 这样既可以表示非常小的小数,也可以表示较大的数。 尾码部分: 因为阶码的存在,由阶码控制小数点的位置; 阶码的覆盖范围很广(2^-1023 ~ 2^1024),因此小数点可以前移1023位,后移1024位,远远超出了尾码的位数; 因此对于一个小数,如果它的整数位不为1,总可以通过向后移动小数点的方式,将它转化一个整数位为1的数。例如0.0001001,可以表示为1.001 * 2^-4; 这样设计,可以省出一位尾码,多精确表示一些数字。 特殊的规定 当指数部分为0(全0),且有效位为0(全0),表示数字0; 当指数部分为255(全1),且有效位为0(全0),表示无穷大或无穷小(取决于符号位); 当指数部分为255(全1),且有效位不为0(非全0),表示不是一个数,也就是NaN; 负数的补码表示 为了计算方便,实际保存时: 正数表示为原码本身; 负数表示为原码的补码。 补码的含义: 原码符号位不变,其余位置按位取反,然后整体+1。 例如,假设我们用8bit表示一个负数,左起第一位表示符号位,则对于-4: 原码表示为: 10000100; 符号位不变,其余按位取反:11111011; 整体+1,得到补码:11111100。 实际上对于正数a和b,计算机计算a-b时,是a的原码与-b原码的补码相加。例如按上述8bit表示法,计算2-4: 2的原码:00000010; -4的补码:11111100; 计算2-4,实际上是2+(-4): 结果是11111110,第一位为1,表示结果负数的补码; 把它还原成原码:先-1,再除符号位按位取反; 得到结果:10000010,为正确结果-2。 可准确表示的整数范围 因为尾数部分只有52位,因此只要整数数值转为2进制后,占位不大于52位,即可准确表示。 52位空间可以表示的整数范围是: 0 ~ 253-1 因此,IEEE754 双精度浮点数,可以精确表示-(2^53 - 1) 到 2^53 - 1范围的整数。 计算结果精度不够时的处理方式 标准规定,当一个计算结果数的二进制表示,出现尾数位不够时,需要整体向右移动,然后增大阶码。 这个过程中,执行0舍1入的原则: 如果向右移动后移除的部分是1,则移动完成后需要+1; 如果移除的是0,则不需要再进行任何操作。 例如: 0 01111111100 0.1100110011001100110011001100110011001100110011001101 + 0 01111111100 1.1001100110011001100110011001100110011001100110011010 = 0 01111111100 10.0110011001100110011001100110011001100110011001100111 向右移动后,移除的位为1,则需要补1: 0 01111111101 0011001100110011001100110011001100110011001100110011 1(1 多出,需要舍弃) 最终结果为: 0 01111111101 0011001100110011001100110011001100110011001100110100 (补 1) 执行位运算时,数字不视作IEEE754表示 JS在执行位运算操作时,将数字视为32位二进制串进行操作(同样首位代表正负)。而非上述754表示法。 >>是带符号右移,表示移动过程中左侧空出来位置用符号位的值来填充(正数补0,负数补1); >>>是无符号右移,表示移动过程中左侧空出来位置,始终用0来填充; 当>>或>>>移动的位数n >= 32时,先对32取余,再进行移动。,因此 a >> 32 与 a 始终相等。

前端工程化

2021.10.10

工程化

前端工程化 前端工程化包括什么 前端工程化的含义 前端工程化一般包括四个方面:模块化、组件化、规范化和自动化。 模块化、组件化 模块化就是将一个大文件拆分成相互依赖的小文件,再进行统一的拼装和加载。比如:CommonJS、ES6模块等。模块化是在文件层面上,对代码或资源的拆分。 组件化是在设计层面上,对用户界面的拆分。一个组件可能在页面上占据独立的一块,并且可以独立实现某项功能。 规范化 一般来说,前端规范化大体上可以分类为编码规范、开发流程规范和文档规范等,每个大类中又有一些子类,如编码规范中包含有目录规范、文件命名规范、js/css代码规范等。 自动化 工程自动化基本包含以下几方面内容: 图标合并 持续集成 自动化构建 自动化部署 自动化测试 前端工程化实操 带你入门前端工程 技术选型 可控性 稳定性 适用性 易用性 规范化 JS、CSS规范 制定JS、CSS规范 找一份知名的代码规范(Js和CSS),根据团队实际进行个性化修改。 知名Js规范: airbnb 规范; standard 规范; 百度规范; 知名CSS规范: styleguide; spec; 检查规范 Eslint + stylelint + VSCode。 ESlint + Stylelint + VSCode自动格式化代码 Git规范 分支管理规范 一般项目分为master分支 + 其他分支。 开发新功能、修改Bug,都需要从master分支新开一个分支,命名后在新分支上修改,然后再合并到master分支。 commit规范 commit提交的信息格式如下: <type>(<scope>): <subject> <BLANK LINE> <body> <BLANK LINE> <footer> 标题行 type: 描述主要修改类型和内容 (必填项); 主题内容 body: 描述为什么修改, 做了什么样的修改, 以及开发的思路等等;(选填项) 页脚注释 footer: 可以写注释,BUG 号链接。(选填项) type feat: 新功能、新特性 fix: 修改 bug perf: 更改代码,以提高性能 refactor: 代码重构(重构,在不影响代码内部行为、功能下的代码修改) docs: 文档修改 style: 代码格式修改, 注意不是 css 修改(例如分号修改) test: 测试用例新增、修改 build: 影响项目构建或依赖项修改 revert: 恢复上一次提交 ci: 持续集成相关文件修改 chore: 其他修改(不在上述类型中的修改) release: 发布新版本 workflow: 工作流相关文件修改 其他 scope: commit 影响的范围, 比如: route, component, utils, build… subject: commit 的概述 body: commit 具体修改内容, 可以分为多行. footer: 一些备注, 通常是 BREAKING CHANGE 或修复的 bug 的链接. 示例 // git commit message. feat(global): 添加网站主页静态页面 这是一个示例,假设对点检任务静态页面进行了一些描述。 这里是备注,可以是放BUG链接或者一些重要性的东西。 自动检查工具 使用husky工具进行检查。 Husky husky基于git hooks,实现对commit、push前后对代码、commit message等进行检查。 项目规范 包括: 项目的目录结构和文件的命名方式。 项目目录结构一般包括: public: 公共资源,不会被打包; src:源码; test: 测试代码。 src代码中典型目录结构: ├─api (接口) ├─assets (静态资源) ├─components (公共组件) ├─styles (公共样式) ├─router (路由) ├─store (vuex 全局数据) ├─utils (工具函数) └─views (页面) UI 规范 前端、UI、产品沟通,互相商量,最后制定下来,建议使用统一的 UI 组件库。 自动化 测试 前端测试 部署 部署项目,应该按如下步骤进行: 执行测试npm run test; 构建项目npm run build; 将打包好的文件,放置到服务器。 自动部署(又叫持续部署 Continuous Deployment,英文缩写 CD)一般通过监听webhook事件来实现。 前端项目自动化部署——超详细教程(Jenkins、Github Actions)

Promise A+ 规范与实现

2021.09.20

Promise

Promise A+规范 与 手动实现 一、Promis A+ 规范 截自:字节路白讲义 Promise A+ 官方原文 1. 术语 promise 是一个有then方法的对象或者是函数,行为遵循本规范; thenable 是一个有then方法的对象或者是函数; value 是promise状态成功时的值,也就是resolve的参数, 包括各种数据类型, 也包括undefined/thenable或者是 promise reason 是promise状态失败时的值, 也就是reject的参数, 表示拒绝的原因 exception 是一个使用throw抛出的异常值 2. 规范内容 2.1 Promise States promise只有三种状态: pending 1.1 初始的状态, 可改变. 1.2 一个promise在resolve或者reject前都处于这个状态。 1.3 可以通过 resolve -> fulfilled 状态; 1.4 可以通过 reject -> rejected 状态; fulfilled 2.1 最终态, 不可变 2.2 一个promise被resolve后会变成这个状态. 2.3 必须拥有一个value值 rejected 3.1 最终态, 不可变 3.2 一个promise被reject后会变成这个状态 3.3 必须拥有一个reason 总结一下, promise的状态流转规律是这样的: pending -> resolve(value) -> fulfilled pending -> reject(reason) -> rejected 2.2 then()方法 promise应该提供一个then()方法, 用来访问最终的结果, 无论是value还是reason. promise.then(onFulfilled, onRejected) 参数要求 1.1 onFulfilled 必须是函数类型, 如果不是函数, 应该被忽略. 1.2 onRejected 必须是函数类型, 如果不是函数, 应该被忽略. onFulfilled 特性 2.1 在promise变成 fulfilled 时,应该调用 onFulfilled, 参数是value 2.2 在promise变成 fulfilled 之前, 不应该被调用. 2.3 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数) onRejected 特性 3.1 在promise变成 rejected 时,应该调用 onRejected, 参数是reason 3.2 在promise变成 rejected 之前, 不应该被调用. 3.3 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数) onFulfilled 和 onRejected 应该是微任务 这里用queueMicrotask来实现微任务的调用. then方法可以被调用多次 5.1 promise状态变成 fulfilled 后,所有的 onFulfilled 回调都需要按照then的顺序执行, 也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个onFulfilled的回调) 5.2 promise状态变成 rejected 后,所有的 onRejected 回调都需要按照then的顺序执行, 也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多 返回值 then 应该返回一个promise promise2 = promise1.then(onFulfilled, onRejected); 6.1 onFulfilled 或 onRejected 执行的结果为x, 调用 resolvePromise( 这里大家可能难以理解, 可以先保留疑问, 下面详细讲一下resolvePromise是什么东西 ) 6.2 如果 onFulfilled 或者 onRejected 执行时抛出异常e, promise2需要被reject 6.3 如果 onFulfilled 不是一个函数, promise2 以promise1的value 触发fulfilled 6.4 如果 onRejected 不是一个函数, promise2 以promise1的reason 触发rejected resolvePromise过程 resolvePromise(promise2, x, resolve, reject) 7.1 如果 promise2 和 x 相等,那么 reject TypeError; 7.2 如果 x 是一个 promise; 如果x是pending态,那么promise必须要在pending,直到 x 变成 fulfilled or rejected. 如果 x 被 fulfilled, fulfill promise with the same value. 如果 x 被 rejected, reject promise with the same reason. 7.3 如果 x 是一个 object 或者 是一个 function:let then = x.then. 如果 x.then 这步出错,那么 reject promise with e as the reason. 如果 then 是一个函数,then.call(x, resolvePromiseFn, rejectPromise) resolvePromiseFn 的 入参是 y, 执行 resolvePromise(promise2, y, resolve, reject); rejectPromise 的 入参是 r, reject promise with r. 如果 resolvePromise 和 rejectPromise 都调用了,那么第一个调用优先,后面的调用忽略。 如果调用then抛出异常e 如果 resolvePromise 或 rejectPromise 已经被调用,那么忽略 则,reject promise with e as the reason 如果 then 不是一个function. fulfill promise with x. 二、手动实现 2.1 基本思路 通过为state设置setter,实现当状态state改变时(prepend -> fulfilled 或 prepend -> rejected),根据改变的目标值,按序依次执行.then()传入的方法; 多次调用.then(),用2个队列数组分别保存传入的onResolved和onRejected函数,这样可以按传入顺序依次执行; new Promise(fn)传入的函数fn应该是直接同步调用的; .then()传入的函数应该是微任务,使用queueMicrotask()方法实现; 2.2 实现代码 const PENDING = 'pending'; const REJECTED = 'rejected'; const FULFILLED = 'fulfilled'; class MPromise { _status = PENDING; // .then(onFulfilled: (res)=>{}, onRejected: (res)=>{}); // all callbacks are microtasks. RESOLVE_CALLBACKS = []; REJECT_CALLBACKS = []; constructor(fn) { this.value = null; this.reason = null; try { fn(this.resolve.bind(this), this.reject.bind(this)); } catch (error) { this.reject(error); } } get status() { return this._status; } set status(e) { this._status = e; if (e === FULFILLED) { for (let callback of this.RESOLVE_CALLBACKS) { callback(this.value); } } else if (e === REJECTED) { for (let callback of this.REJECT_CALLBACKS) { callback(this.reason); } } } resolve(value) { if (this.status === PENDING) { this.value = value; this.status = FULFILLED; } } reject(reason) { if (this.status === PENDING) { this.reason = reason; this.status = REJECTED; } } isFunc(e) { return typeof e === 'function'; } then(onFulfilled, onRejected) { const fulfillFn = this.isFunc(onFulfilled) ? onFulfilled : (value) => value; const rejectFn = this.isFunc(onRejected) ? onRejected : (reason) => { throw reason; }; const fulfillFnWithTryCatch = (resolve, reject, promise2) => { queueMicrotask(() => { try { if (!this.isFunc(onFulfilled)) { resolve(this.value); } else { let x = fulfillFn(this.value); this._resolvePromise(promise2, x, resolve, reject); } } catch (error) { reject(error); } }); } const rejectFnWithTryCatch = (resolve, reject, promise2) => { queueMicrotask(() => { try { if (!this.isFunc(onRejected)) { reject(this.reason); } else { let x = rejectFn(this.reason); this._resolvePromise(promise2, x, resolve, reject); } } catch (error) { reject(error); } }); } switch (this.status) { case FULFILLED: { const promise2 = new MPromise((resolve2, reject2) => { setTimeout(() => { fulfillFnWithTryCatch(resolve2, reject2, promise2); }, 0); }); return promise2; } case REJECTED: { const promise2 = new MPromise((resolve2, reject2) => { setTimeout(() => { rejectFnWithTryCatch(resolve2, reject2, promise2) }, 0); }); return promise2; } case PENDING: { const promise2 = new MPromise((resolve2, reject2) => { this.RESOLVE_CALLBACKS.push(() => { fulfillFnWithTryCatch(resolve2, reject2, promise2); }); this.REJECT_CALLBACKS.push(() => rejectFnWithTryCatch(resolve2, reject2, promise2)); }); return promise2; } } } _resolvePromise(newPromise, x, resolve, reject) { if (newPromise === x) { reject(new TypeError(`Try to resolve a promise with itself.`)); return; } if (x instanceof MPromise) { return x.then((value) => { this._resolvePromise(newPromise, value, resolve, reject); }, (reason) => { reject(reason); }); } if ((typeof x === 'object' && x !== null) || this.isFunc(x)) { let then; try { then = x.then; let called = false; if (this.isFunc(then)) { const resolvePromise = (y) => { if (called) return; called = true; this._resolvePromise(newPromise, y, resolve, reject); }; const rejectPromise = (r) => { if (called) return; called = true; reject(r); }; try { then.call(x, resolvePromise, rejectPromise); } catch (error) { if (called) return; reject(error); } } else { resolve(x); } } catch (error) { reject(error); } return; } resolve(x); } catch (onRejected) { return this.then(null, onRejected); } static resolve(value) { if (value instanceof MPromise) return value; return new MPromise((res, rej) => { res(value); }); } } Promise A+ 测试 All Pass,截图纪念。

浏览器的进程、线程与页面渲染流程

2021.09.20

Browser

浏览器渲染基本流程 进程和线程的区别 浏览器线程和进程 浏览器进程?线程?傻傻分不清楚! 进程 什么是进程 进程类似工厂,线程类似工人。 进程是cpu资源分配的最小单位(系统会给它分配内存)。操作系统会为每个进程分配私有、独立的一块内存资源,进程之间互不影响; 当进程被杀掉,分配的内存空间也被释放; 一个进程中可能有一个或多个线程在工作; 同一进程下的各个线程之间,共享系统为该进程分配的同一块内存空间(包括代码段、数据集、堆等); 进程间也可以互相通信,叫做IPC(Inter Process Communication),但是代价较大。 Chrome浏览器主要有哪些进程 主进程(Browser进程) 与其他进程一起协作,实现浏览器的功能; 负责浏览器界面显示,与用户交互。如前进,后退等; 负责各个页面的管理,创建和销毁其他进程; 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上; 网络资源的管理,下载等。 GPU进程:最多一个,用于合成图层、3D图形绘制等。 渲染进程(Renderer进程,也叫浏览器内核): 每个tab页会单独占用一个渲染进程; tab页内的<iframe>也会占用独立的渲染进程; 用于页面渲染、Js执行、事件循环; 渲染进程是多线程的,主要有: GUI渲染线程 JS引擎线程; 事件触发线程: 管理任务队列; 定时触发器线程; 异步http请求线程; 第三方插件进程 每个第三方插件可能对应一个进程; 实用程序进程: 储存进程、网络进程、音频进程等; 为什么浏览器被设置为多进程 多进程架构的优势 避免单个page crash影响整个浏览器; 避免第三方插件crash影响整个浏览器; 多进程充分利用多核优势; 方便使用沙盒模型隔离插件等进程,为不同进程提供不同的系统访问能力,提高浏览安全和稳定性。 多进程架构的缺陷 浏览器不同进程之间,不能共享同一个内存空间,因此某些基础模块会在不同进程中重复存在,占用额外内存空间。 比如: JS V8引擎在不同的标签页渲染进程的内存空间中都存在。 因此Chrome限制了最大进程数,当进程数达限,Chrome会将访问同一个网站的tab页都放在同一个进程里运行。 线程 线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元。 一个进程可以有多个线程。(进程是工厂,线程是工人) chromium 官方文档: 每个Chrome进程中含有: 1个主线程: 在浏览器主进程里:用于更新UI界面; 在渲染进程里:运行大部分Blink代码; 1个IO线程: 在所有的进程里:所有的IPC message到达此线程; 大部分异步io发生在此线程; 一些特殊用途线程; 一个通用线程池。 Chrome浏览器主进程中包含的线程 UI线程 : 绘制浏览器顶部按钮和导航栏输入框等组件; 网络线程:管理网络请求; 存储线程:控制文件读写; Chrome渲染进程中包含的线程 GUI渲染线程(主线程) 数目: 1个。 主要职能: 初始渲染:解析HTML,解析CSS,构建DOM树,CSSOM树,整合Render树,进行布局和绘制; 页面发生变化时,执行重绘和重排; GUI渲染线程与JS引擎线程是互斥的(不可同时运行)。当 JS 引擎执行时 GUI 线程就会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中,等到 JS 引擎线程空闲时,再立即取出执行。 JS引擎线程 数目: 1个。 主要职能: 运行JS引擎(V8):解析JS脚本,运行JS代码; JS引擎中有一个任务队列,它一直等待任务队列中任务的到来,一旦任务到来立即按序加以处理; 与GUI渲染线程互斥。(因此Js执行时间如果过长,会导致页面渲染卡顿) 事件触发线程 数目:1个。 主要职能是控制事件循环。 当JS执行遇到宏任务、微任务时,将其加入到事件触发线程的对应队列中; 事件触发线程会根据任务的执行时机(比如setTimeout宏任务约定的定时时间已到),将任务各自队列中取出,放入JS引擎任务队列中等待执行; JS引擎会在自己空闲的时候,从队列中依次取出任务执行; 定时触发器线程 主要职能: 用于setTimeout / setInterval等的计时; 时间到,则通知事件触发线程,将定时器对应的任务放入Js引擎的任务队列; HTML标准中要求,低于4ms的定时,时间间隔都算作4ms(也就是定时器最低时间间隔为4ms)。 异步http请求线程 主要职能: 处理异步http请求; 请求结果返回,通知事件触发线程,将回调任务放入JS引擎任务队列; 浏览器渲染基本流程 关键渲染路径:CRP 浏览器通过请求得到一个HTML文本; 渲染进程解析HTML文本,构建DOM树; 解析HTML的同时,如果遇到内联样式或者样式脚本,则下载并构建样式规则(stytle rules),若遇到JavaScript脚本,则会下载执行脚本; DOM树和样式规则构建完成之后,渲染进程将两者合并成渲染树(render tree); 渲染进程开始对渲染树进行布局,生成布局树(layout tree); 渲染进程对布局树进行绘制,生成绘制记录; 渲染进程的对布局树进行分层,分别栅格化每一层,并得到合成图层信息; 渲染进程将合成图层信息发送给GPU进程,GPU进程对各图层进行合成,然后显示页面。 关键渲染路径CRP(Critical Rendering Path),是浏览器将 HTML、CSS、JavaScript 转换为在屏幕上呈现的像素内容所经历的一系列步骤。也就是我们上面说的浏览器渲染流程。 其中有三个关键因素: 关键资源的数量: 可能阻止网页首次渲染的资源; 关键路径长度: 获取所有关键资源所需的往返次数或总时间; 关键字节: 实现网页首次渲染所需的总字节数,等同于所有关键资源传送文件大小的总和。 什么是渲染合成(Composite) ? 合成是一种将页面分成若干层,然后分别对每一层进行光栅化,最后在一个单独的线程 - 合成线程(compositor thread)里面合并成一个页面的技术。 当用户滚动页面时,由于页面各个层都已经被光栅化了,浏览器需要做的只是合成一个新的帧来展示滚动后的效果罢了。 页面的动画效果实现也是类似,将页面上的层进行移动并构建出一个新的帧,交由GPU进行显示即可。 参考资料 窥探现代浏览器架构:1 窥探现代浏览器架构:2

Web物理设备接口API

2021.09.20

Web

Web API

目前的 Web 设备接口API: Web USB API; (USB通信) Web Serial API; (串口通信) Web NFC API; (NFC通信) Web Bluetooth API; (蓝牙通信) Web HID API; (人机接口设备通信) 为保证安全,这些API: 只在htttps或wss协议下才可获取; 单例模式:它们的接口类不能通过new实例化,方法也不能直接调用,在浏览器中需要通过navigator下面的属性来获取对应接口实例。(navigator.usb等) 一、Web物理设备接口API的使用场景 Web物理设备接口API的前景非常广阔。 它的优势主要是:利用Web的跨平台性,通过物理设备接口API,可以让开发者充分利用用户现有的设备(无需重新购买),为用户开发使用体验更好的各类Web应用(无需安装且跨平台)。 Google在2020年底的开发者大会上,专题讲解了这类Chrome新特性,并提供了多种新的Web应用场景: Web Serial: 为每个用户配备一个微型物理设备(与用户主机通过串口连接),使用Web Serial API与其进行通信,对用户进行识别和认证,用户无需安装任何软件,只需要打开一个网页,就可以打开IDE进行代码编写。同时,软件更新只需要在网页服务器统一进行,无需考虑用户平台,也无需用户手动更新。 Web NFC: 艺术博物馆为每张画作配备一个NFC贴片,浏览者无需安装应用,打开网页即可贴近NFC查看具体画作信息。 Web HID: 键盘鼠标厂商通过Web网页直接控制、更新自家设备的固件,无需另配设备驱动; 二、Web各类物理设备接口API的特性及使用 2.1 Web USB API 2.1.1 USB 类 2.1.1.1 事件 2.1.1.1.1 USB.onconnect 每当连接到先前配对的USB设备时,调用此事件处理器。 2.1.1.1.2 USB.ondisconnect 每当配对USB设备断开连接时,调用此事件处理器。 2.1.1.2 方法 2.1.1.2.1 USB.getDevices() 返回Promise,Resolve为当前源下,先前配对过的USB设备组成的数组。 2.1.1.2.2 USB.requestDevice(<filters>) 请求获取USB设备。(弹出用户选择窗口,返回promise对象) 传入一个filters对象,其中有filters属性,值为一个数组,代表选择的USB设备类型过滤器。过滤器中属性取值范围: vendorId productId classCode subclassCode protocolCode serialNumber 2.2 Web Serial API Web Serial API: Web 串口通信 Web Serial API 提供了一种途径,让网页可以通过javascript读写串口设备。 这个API将通过允许网页文档与微型控制器、3D打印机等物理设备进行串口通信的方式,桥接web和物理现实世界。 2.2.1 Serial 类 Serial类,提供了Web串口通信的属性和方法。 Serial类无法手动实例化,其原型上的方法也不能手动调用,只能通过Chrome内置实例navigator.serial调用。 2.2.1.1 Serial 方法 2.2.1.1.1 navigator.serial.requestPort([options]) 这个方法必须由用户主动操作来调用(比如点击按钮)。 第一次访问网页,如果想要使用串口通信,必须先调用这个方法,来让用户选择暴露哪个串口给网页,之后才能获取到串口。 返回一个Promise: 当用户完成选择后,resolve为一个SerialPort对象,也就是用户选择的串口; 当用户取消选择,reject。 options为一个对象,可包含以下属性: filters: 过滤串口,值为一个数组,里面包含一系列过滤信息对象filterIObj,每个过滤信息对象filterObj可以包含: usbVendorId: 一个无符号整数,识别USB设备的制造商; usbProductId: 一个无符号整数,识别USB设备。 2.2.1.1.2 navigator.serial.getPorts() 返回一个Promise对象: resolve为一个数组arr; arr中包含了一系列SerialPort实例对象,表示当前源下已被允许访问的串口集合(通过之前的requestPort)。 2.2.1.2 Serial 事件 2.2.1.2.1 connect事件 当串口与设备连接时触发。 2.2.1.2.2 disconnect事件 当串口与设备断开连接时触发。 2.2.1.3 Serial 使用示例 navigator.serial.addEventListener('connect', (e) => { // Connect to `e.target` or add it to a list of available ports. }); navigator.serial.addEventListener('disconnect', (e) => { // Remove `e.target` from the list of available ports. }); navigator.serial.getPorts().then((ports) => { // Initialize the list of available ports with `ports` on page load. }); button.addEventListener('click', () => { const usbVendorId = ...; navigator.serial.requestPort({ filters: [{ usbVendorId }]}).then((port) => { // Connect to `port` or add it to the list of available ports. }).catch((e) => { // The user didn't select a port. }); }); 2.2.2 SerialPort 类 2.3 Web NFC API 仅在移动端浏览器可获取。 2.4 Web Bluetooth API 2.4.1 Bluetooth 类 2.4.1.1 Bluetooth.getAvailability() 返回Promise,resolve为一个布尔值。该值指示用户代理是否支持蓝牙。 2.4.1.2 Bluetooth.getDevices() 返回一个解析为BluetoothDevices数组的Promise,该数组的源端已经通过调用Bluetooth.requestDevice()获得了允许。 2.4.1.3 Bluetooth.requestDevice([options]) 通过传入一个指定的选项,请求蓝牙设备。当配对成功,返回resolve为配对的BluetoothDevice对象的Promise。 2.5 Web HID API 类似于USB API,通过navigator.hid获取。

Web组件: Web Component

2021.09.20

Web

Web Component

Web组件相关API: HTML模板) Shadow DOM 自定义组件 一、HTML模板 使用<template>标签包裹一系列DOM元素,叫做HTML模板。它有以下特性: 默认不会被浏览器渲染,内部内容包裹在一个DocumentFragment节点内; 内部内容不属于活动文档,因此document.querySelector()等方法,不会匹配到<template>内部元素(<template>本身可以被匹配); 匹配<template>元素后,访问.content属性可以取得内部DocumentFragment的引用; 在DocumentFragment引用上,使用查询方法,可以匹配内部的DOM元素; 获取的DocumentFragment可以动态挂载到活动DOM元素中。 注意: documentFragment里面的DOM树只能被挂载一次,再次挂载为空。 二、Shadow DOM 2.1 什么是Shadow DOM 将一个完整的DOM树,作为节点添加到父DOM树。 与HTML模板的区别: Shadow DOM会实际渲染到页面上,而HTML模板不会。 2.2 Shadow DOM的特点 完全隔离了子DOM树,内部DOM节点无法从外部选择获取,内部使用的CSS也限制在子DOM树中,不会干扰全局; 可以使用插<slot>,将宿主元素原本的内容插入到shadow DOM。 2.3 Shadow DOM的使用方式 使用attachShadow(<initObj>)方法创建并添加Shadow DOM到有效HTML元素(宿主节点)(只有部分HTML元素能添加Shadow DOM); 传入的initObj,叫做shadowRootInit对象,必须包含一个mode属性,取值为”open”或”closed”,表示ShadowDOM是否可以通过shadowRoot属性在HTML元素上获得(绝大多数情况都应该设为’open’); attachShadow()方法执行后,会立即替换原DOM元素内容。如果shadowDOM中有<slot>默认插槽,原DOM元素内容会作为content被添加到插槽中; 也可以用具名插槽<slot name="N1">,承接宿主元素对应的带有slot="N1"的内容; attachShadow()方法返回shadowDOM的根元素(叫做”影子根”),默认情况下shadowDOM内容为空; 2.4 浏览器默认添加的Shadow DOM 浏览器会在<video>、<input>等元素内部自动添加Shadow DOM,来显示诸如video控制按钮等某些内置元素。 三、自定义元素 3.1 定义自定义元素 customElements.define()方法可以声明创建自定义元素。 customElements.define()方法使用方式如下: customElements.define('x-foo', FooElement, {extends: 'div'}); // (tag, class, extends) 3.2 自定义元素如何封装组件 自定义元素通过内部挂载Shadow DOM封装组件。 ShadowDOM内容可以用<template>模板记载。 代码见红宝书P660。

Vue使用注意事项

2021.09.08

Vue

Vue使用的注意事项 一、v-for 与 v-if 不要同时使用 原因: 浪费性能。 v-for在Vue中比v-if优先级高,因此无论如何都会遍历所有列表中的子元素,才能确定哪些子元素被显示。(本意是只遍历+显示v-if为true的子元素集合) 解决方式: 使用computed等提前筛选出要显示的列表元素,然后用v-for遍历。 二、多个根节点的组件,需要显式指定Attribute继承的元素 原因: 具有多个根节点的组件不具有自动Attribute继承行为。如果未显式绑定 $attrs,将发出运行时警告。 解决方式: 使用v-bind="$attrs"显式绑定Attribute继承元素。 三、通过provide/inject机制传递的响应式变量,不要在inject一方修改 原因: 单向数据流。(在inject一方修改,会导致子组件修改父组件数据,导致数据流向混乱) 解决方式: 在provide的时候,对响应式对象进行readonly包装; 在provide的时候,同时提供修改响应式对象的方法,一并provide给inject方使用; 四、computed和watch的用途不同 computed解决的问题:【一依赖多】一个变量依赖多个响应式变量计算得出。 只有当computed属性依赖的响应式变量改变,computed才会重新计算; watch解决的问题:【一影响多】一个变量影响着多个其他变量。 对这个变量进行监听,当它本身改变,执行对应逻辑,修改它影响的多个其他变量。

前(后)端鉴权方式

2021.09.06

Safety

前端鉴权:掘金 一、cookie + session 浏览器登录发送账号密码,服务端查用户库,校验用户; 服务端把用户登录状态存为 session,生成一个 session_ID; 通过登录接口返回,把 sessionID set 到 cookie 上; 此后浏览器再请求业务接口,session_ID 随 cookie 带上; 服务端查 session_ID 校验 session; 成功后正常做业务处理,返回结果。 优点: 因为主要信息在服务器存储,客户端只需要存储一个用来标识唯一性的ID,减少了请求携带cookie的体积; 缺点: 服务端需要对每个session进行储存,压力大; 服务端如果是集群,需要把session用额外独立的库集中储存。 二、token 用户登录,服务端校验账号密码,获得用户信息; 把用户信息、配置等编码成 token,通过 cookie set 到浏览器; 此后用户请求业务接口,通过 cookie 携带 token; 接口校验 token 有效性,进行正常业务接口处理。 token与session的区别? token携带了用户的完整信息,通过服务端密钥编码储存在客户端,服务端不进行储存;而session则主要由服务端保存会话数据; 狭义上,session指代“服务端保存会话信息,客户端用cookie储存会话id”的鉴权形式,而token更为灵活,指代“客户端可以存在任何位置,服务器不保存信息”的鉴权形式; 优点: 服务端不保存用户会话信息,避免查库带来的延迟; 服务端压力小,不需要设置session统一管理架构,降低成本。 缺点: token一般较长,请求携带cookie体积较大; 三、JWT:JSON Web Token 什么是JWT JWT是Token的一种实现。它和普通Token都是访问资源的令牌。它们的区别是: 普通Token在被服务器收到后,服务器可能需要去查询数据库才能确认用户的身份,而JWT本身就包含加密信息,服务器利用自己的密钥对JWT进行验证,就可以确定用户身份,不需要查询数据库。 校验JWT的网站:JWT校验 JWT组成 Header和Payload JWT由三部分组成:Header、Payload和Signature。 其中Header中的字段比较固定: **typ: ** 类型。值恒定为'JWT'; **alg: ** 签名的加密算法。一般选为'HS256'; Payload的字段可以自由设置。 Header和Payload的原始数据都是JSON格式,通过Base64编码后,放入JWT的前两个部分。 Signature 签名 签名由两部分计算而成: A: Header和Payload的Base64编码串,中间用','串联; B: 安全码Secret-Code。可以自由指定,用于对JWT的有效性进行校验。(长度不超过256bit) 加密方式一般选择HS256,即加密后的字符串为HS256(A, B)。它将放在JWT的第三部分。 JWT的校验 服务端收到用户的JWT后:通过指定的安全码Secret-Code,对JWT的前两部分进行一次HS256计算即可,结果与第三部分进行比对,如果一致可通过校验。 四、单点登录 1. 什么是单点登录? 假设一个公司拥有多个业务,部署在多个域名下,只要用户在一个业务下完成登录,访问其他业务页面可自动登录,叫做单点登录。 2. 如何实现? 同一域名下的不同子域名,可以使用cookie的domain关键字来实现。 不同域名下,单点登录需要使用独立的认证服务SSO来实现。

Vue3中的diff算法

2021.09.06

Vue

一、Vue3中的diff算法 Vue3中对于没有key的片段,采用的是直接数组比较方法; 对于有key的片段,采用的是先掐头去尾,然后执行最长递增子序列的方法。 1. 最长递增子序列算法 最长递增子序列算法: 贪心策略:为了找到最长的递增子序列,我们希望递增序列增长得慢一些。这样后面的元素就更容易与其形成更长的递增子序列。 因此,假设我们在遍历过程中当前找到的最长递增子序列是sub,此时sub末尾(最大)的元素是a,当位于后面的元素b比a更小,我们应该更新a为b,因为此时b比a更容易实现最长的递增子序列。 // By Mars 2021.09.06 // Get max asscending sequence of an pure number array. // getMaxSequence(arr); // eg. [2,3,6,1,7] -> [2,3,6,7] // Algorithum (greedy) : // Time: O(n*logn) // Steps: // 1. Maintain an accending order array:[result], and an array:[p] whose length is the same with given array. // 2. result[i] = n, means that at current status, we have found a max accending sequence of length i+1, and the minimum number at the tail of the sequence is [n]. // 3. p[i] is setted when we take an element form the original given array and refresh the result array with it. p[i] records the previous number of result array of where we put the current element array[i] into at the result array; // 4. result array is an accending array, we iterate the original array and pick current element (arr[i]), then find the first element which is larger than current element in result array (assume that position is [pos]), and replace it with current element value; // 5. then we get the previous element of result array (result[pos-1]), and set p[i] with it; // 6. after we iterate all the elements, the last element of result array must be the real number of the max sequence, other elements can be replaced during the precess above, so they may be not the real number of the result max sequence; // 7. Luckily, we record the real element when we refresh every element of result, which is in array p. // 8. everytime, we get the element of result array, and find the position of it in the original array [pos], the real previous element of max subsquence is p[pos]; // 9. repeat the step.8 until all the sequence is found. function getMaxSequence(arr) { let result = []; let p = arr.slice(); // same length of arr. function bs(tar) { // find the first num which [ >= tar ]. let l = 0, r = result.length - 1; while (l < r) { let m = Math.floor((l + r) / 2); if (arr[result[m]] >= tar) r = m; else l = m + 1; } return l; } for (let i = 0; i < arr.length; i++) { if (result.length === 0) { result.push(i); p[i] = null; } else { let pos = bs(arr[i]); if (arr[result[pos]] >= arr[i]) { if (pos === 0) { result[pos] = i; p[i] = null; } else { result[pos] = i; p[i] = result[pos - 1]; } } else { p[i] = result[result.length - 1]; result.push(i); } } } let cur = result.length; let prev = result[result.length - 1]; while (cur > 0) { cur -= 1; result[cur] = prev; prev = p[result[cur]]; } return result; } 2. 无key元素的diff算法 没有key的元素片段,patch的时候采用的是直接进行数组比较的方法。 // 基本思路: // 1. 对oldChildren, newChildren,选取二者中长度较小的作为公共长度; // 2. 从0位置开始,对公共长度部分一一对应直接patch; // 3. 如果oldChildren长度更长,则把多余的部分直接unmount; // 4. 如果newChildren长度更长,则直接把剩余的部分依次mount到最下方。 const patchUnkeyedChildren = (c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => { c1 = c1 || EMPTY_ARR; c2 = c2 || EMPTY_ARR; // Choose the common length of c1 and c2. const oldLength = c1.length; const newLength = c2.length; const commonLength = Math.min(oldLength, newLength); let i; // Patch the common area. for (i = 0; i < commonLength; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i]) : normalizeVNode(c2[i])); patch(c1[i], nextChild, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); } if (oldLength > newLength) { // remove old unmountChildren(c1, parentComponent, parentSuspense, true, false, commonLength); } else { // mount new mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, commonLength); } }; 3. 有key元素的diff算法 基本思路如下: 对于两个子VNode数组oldChildren,newChildren: 从头部i=0开始,一直向后匹配,直到二者vnode不相同(或到达尾部);(Vue3以key和Vnode.type都相同标记为二者相同) 从二者各自尾部e1=oldChildren.length-1和e2=newChildren.length-1开始,一直向前匹配,直到二者vnode不同(或遇到头部指针i); 此时,存在三种情况: ① newChildren 在 oldChildren的基础上,前或后增加了若干元素(e.g. oc = [1,2,3]; nc = [1,2,3,4,5]):i > e1 && i <= e2 ② newChildren 在 oldChildren的基础上,前或后删除了若干元素(e.g. oc = [1,2,3,4,5]; nc = [1,2,3]):i <= e1 && i > e2 ③ newChildren、oldChildren在执行了前后比对之后,二者中间都剩余了部分元素(e.g. oc = [1,2,3]; nc = [1,2,3,4,5]):i <= e1 && i <= e2 对于①、②两种情况,和没有key的数组比较相同,直接mount或unmount剩下的元素即可; 对于③情况,对二者剩余子数组oc、nc,执行最大递增子序列算法: ① 为nc创建一个哈希表Map,键名为子元素child的key,键值为child在children中的索引;(为了减少复杂度,方便步骤②的查询) ② 设置一个数组newIndexToOldIndexMap,初始化各元素为-1(Vue3中为0,oc中newIndex向后移动了1),记录nc各元素在oc中的位置:newIndexToOldIndexMap[i] = k代表nc中i位置元素,在oc中位置为k; ③ 从头到尾遍历oc,对于它的子元素oldChild,找到它在新数组nc中的位置newIndex: 如果有key,从Map中查找key对应的index为newIndex,找不到则为undefined; 如果没有key,遍历nc,比较元素是否相同,相同则记录下它的index作为newIndex,找不到则为undefined。 如果newIndex为undefined,说明新子元素nc中没有这个旧元素,直接删除(unmount)当前的旧元素; 如果newIndex不是undefined: 记录它当前在oc中的位置newIndexToOldIndexMap[newIndex] = i; 因为nc中元素为升序排列,如果相对位置在oc中没变,那么它们在newIndexToOldIndexMap也应该是升序,因此newIndex应该是递增的: 记录当前已遍历的newIndex的最大值maxNewIndex,如果newIndex >= maxNewIndex,则更新maxNewIndex; 如果newIndex < maxNewIndex,因为newIndex也不是undefined,则说明当前的元素移动了位置,记录下这个情况(moved = true,代表nc整体存在元素移动情况); patch新、旧这两个元素; ④ 寻找newIndexToOldIndexMap的最大递增子序列maxSequence,它内部含有的元素,都只需要内部更新,不需要移动; ⑤ 从后到前遍历nc: 对于nc最后一个元素,它mount到容器的下方。对于非最后一个元素,它的anchor是前一个元素[i+1]对应的DOM元素; 如果newIndexToOldIndexMap[i] === -1,则说明oc中没有这个新元素,将它按照anchor,进行mount; 否则,如果存在moved === true,而且i不在maxSequence中,则按照anchor,移动当前元素(当前元素已经在步骤③被patch过,现在只需要移动)。 const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => { let i = 0; const l2 = c2.length; let e1 = c1.length - 1; // prev ending index let e2 = l2 - 1; // next ending index // [Mars] : 1. Compare the head and the tail first. // 1. sync from start // (a b) c // (a b) d e while (i <= e1 && i <= e2) { const n1 = c1[i]; const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i]) : normalizeVNode(c2[i])); if (isSameVNodeType(n1, n2)) { // type and key are the same. patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); } else { break; } i++; } // 2. sync from end // a (b c) // d e (b c) while (i <= e1 && i <= e2) { const n1 = c1[e1]; const n2 = (c2[e2] = optimized ? cloneIfMounted(c2[e2]) : normalizeVNode(c2[e2])); if (isSameVNodeType(n1, n2)) { // type and key are the same. patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); } else { break; } e1--; e2--; } // [Mars] : 2. Compare the head and the tail first. // 3. common sequence + mount // (a b) // (a b) c // i = 2, e1 = 1, e2 = 2 // (a b) // c (a b) // i = 0, e1 = -1, e2 = 0 if (i > e1) { if (i <= e2) { const nextPos = e2 + 1; const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor; while (i <= e2) { // mount new added vnodes not contained in oldChilds. patch(null, (c2[i] = optimized ? cloneIfMounted(c2[i]) : normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); i++; } } } // 4. common sequence + unmount // (a b) c // (a b) // i = 2, e1 = 2, e2 = 1 // a (b c) // (b c) // i = 0, e1 = 0, e2 = -1 else if (i > e2) { while (i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true); i++; } } // 5. unknown sequence // [i ... e1 + 1]: a b [c d e] f g // [i ... e2 + 1]: a b [e d c h] f g // i = 2, e1 = 4, e2 = 5 else { const s1 = i; // prev starting index const s2 = i; // next starting index // 5.1 build key:index map for newChildren const keyToNewIndexMap = new Map(); // use hashMap, cause it's easy to search an index of a key. for (i = s2; i <= e2; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i]) : normalizeVNode(c2[i])); if (nextChild.key != null) { if (keyToNewIndexMap.has(nextChild.key)) { warn$1(`Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.`); } keyToNewIndexMap.set(nextChild.key, i); } } // 5.2 loop through old children left to be patched and try to patch // matching nodes & remove nodes that are no longer present let j; let patched = 0; const toBePatched = e2 - s2 + 1; let moved = false; // used to track whether any node has moved let maxNewIndexSoFar = 0; // works as Map<newIndex, oldIndex> // Note that oldIndex is offset by +1 // and oldIndex = 0 is a special value indicating the new node has // no corresponding old node. // used for determining longest stable subsequence const newIndexToOldIndexMap = new Array(toBePatched); for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0; for (i = s1; i <= e1; i++) { const prevChild = c1[i]; if (patched >= toBePatched) { // all new children have been patched so this can only be a removal unmount(prevChild, parentComponent, parentSuspense, true); continue; } let newIndex; if (prevChild.key != null) { newIndex = keyToNewIndexMap.get(prevChild.key); } else { // key-less node, try to locate a key-less node of the same type for (j = s2; j <= e2; j++) { if (newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j])) { // type and key are the same. newIndex = j; break; } } } if (newIndex === undefined) { unmount(prevChild, parentComponent, parentSuspense, true); } else { newIndexToOldIndexMap[newIndex - s2] = i + 1; // n:[h,c,d,e] o:[c,d,e,h] -> newIndexToOldIndexMap: [4,1,2,3] if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex; } else { moved = true; } patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); patched++; } } // 5.3 move and mount // generate longest stable subsequence only when nodes have moved const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR; j = increasingNewIndexSequence.length - 1; // looping backwards so that we can use last patched node as anchor !!!!!!★★★ for (i = toBePatched - 1; i >= 0; i--) { const nextIndex = s2 + i; const nextChild = c2[nextIndex]; const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor; if (newIndexToOldIndexMap[i] === 0) { // mount new patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); } else if (moved) { // move if: // There is no stable subsequence (e.g. a reverse) // OR current node is not among the stable sequence if (j < 0 || i !== increasingNewIndexSequence[j]) { move(nextChild, container, anchor, 2 /* REORDER */ ); } else { // position is matched, no movement. j--; } } } } };

Vuex笔记

2021.09.02

Vue

Vuex

Vuex: 中心型状态管理工具 核心概念和基本操作 安装使用 npm 安装: npm i vuex@next; 在main.js引入:import { createStore } from 'vuex'; 创建新store(vuex仓库实例): const store = createStore({ state(){ return { // states.... }; }, mutations: { // mutations... }, // others... }); 作为插件挂载到Vue APP实例上: app.use(store); 在组件内获取store实例的方式: 选项式API:this.$store; 组合式API: 从vuex引入useStore方法: import {useStore} from 'vuex'; 在setup函数中创建store引用:const store = useStore();。 核心概念 Store 每一个Vuex状态库实例,叫做一个Store。 通过vuex的createStore(<options>)方法生成。 State 被管理的响应式状态,也就是MVVM中的Model。 被所有用到这个store的组件共享,组件内获取的state也是响应式的,会随着store中state的改变自动更新。 Getter 相当于vuex store中的computed属性。 使用一些state计算得出的响应式属性。和computed选项式api定义方式一样,值为一个函数,不同的是: vuex中自动以state作为第一个参数传入; 第二个参数为getters对象,也就是说可以在一个getter中引用其他getter。 { //... getters: { getter1: function(state, getters){ //do something with state and return... // eg. return state.A + getters.getter2; } } } Mutation 注册的可用于更改state值的方法。用store.commit()方法触发。 提交mutation,是唯一的更改store中state的方法。 // in component: this.$store.commit('mutationName', payload); mutation同样以state做为第一个参数。 在提交mutation时,可以提供额外的参数(payload),从mutation方法的第二个参数开始传入。 mutation含义为突变,它设计的功能是:调用后立即改变state,因此理论上必须是同步函数。 mutation设为异步函数也不会报错,但是会导致状态更新顺序难以捉摸,增加调试难度。 Action action用于提交mutation,它可以包含任意异步操作(可以不直接立即更改状态)。 Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。 action用store的dispatch方法触发,第一个参数为context,第二次参数同样可以传入一个payload。 // in vuex store config obj.. actions: { action1 (context) { // action用于提交mutation. context.commit('mutation1') } } // in component this.$store.dispatch('action1'); Module 使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。 // 官方代码,简单明了 const moduleA = { state: () => ({ ... }), mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: () => ({ ... }), mutations: { ... }, actions: { ... } } const store = createStore({ modules: { a: moduleA, b: moduleB } }) store.state.a // -> moduleA 的状态 store.state.b // -> moduleB 的状态 对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象; 对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState; 对于模块内部的 getter,根节点状态会作为第三个参数暴露出来; 基本操作

Vue-Router笔记

2021.09.02

Vue

Vue-Router

Vuex-Router: 路由管理工具 路由链接、路由出口 这是两个vue-router内置组件: <router-link>: 用来存放路由链接的内置组件,点击即可进行路由跳转; <router-view>: 用来显示路由匹配结果的内置组件,路由匹配到的组件会显示在这里。 JS: 配置一个vue-router基本流程 // 官方代码示例: // 1. 引入路由需要的vue组件. // 也可以从其他文件导入 const Home = { template: '<div>Home</div>' } const About = { template: '<div>About</div>' } // 【路由配置】 // 2. 定义一些路由 // 每个路由都需要映射到一个组件。 // 我们后面再讨论嵌套路由。 const routes = [ { path: '/', component: Home }, { path: '/about', component: About }, ] // 【路由器配置】 // 3. 创建路由实例并传递 `routes` 配置 // 你可以在这里输入更多的配置,但我们在这里 // 暂时保持简单 const router = VueRouter.createRouter({ // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。 history: VueRouter.createWebHashHistory(), routes, // `routes: routes` 的缩写 }) // 5. 创建并挂载到vue根实例 const app = Vue.createApp({}) //确保 _use_ 路由实例使 //整个应用支持路由。 app.use(router) app.mount('#app') // OK,路由已经应用。 动态路由匹配 路由配置中: path可以定义部分动态字段,通过:dymPara形式定义; 当匹配到路由后,在路由对象route的params属性中可以获取到匹配到的dymPara实际字段。 为动态路由匹配设定规则 正则表达式 可以在路由动态字段后面加上一对圆括号,里面设定一个匹配的正则表达式。 // route配置 // id的匹配对象是:一个或多个数字。 // 不满足则无法匹配到这个路由。 { path: `/user-:id(\\d+)`; } 匹配多个动态部分 在动态字段结尾,使用*(零个或多个)或+(一个或多个)操作符,表示动态字段支持重复。 const routes = [ // /:chapters -> 匹配 /one, /one/two, /one/two/three, 等 { path: '/:chapters+' }, // /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等 { path: '/:chapters*' }, ] 可选动态部分 在动态字段结尾,添加?操作符,表示动态部分可选。 // 匹配 /user-mars 或 /user- { path: `/user-:id?`; } 嵌套路由 一个路由的组件中,如果还有<router-view>出口,希望通过这个路由的子路由进行匹配显示不同的组件,则需要使用嵌套路由实现。 一个路由的子路由,通过该路由配置的children选项配置。子路由匹配的组件内容,会通过上层路由组件中的<router-view>输出。 { path: '/user/', component: UserInfo, children: [{ // 当匹配'/user/Mars'时,UserInfo中的<router-view>显示MarsInfo组件。 path: 'Mars', component: MarsInfo, },{ // 当匹配'/user/Liu'时,UserInfo中的<router-view>显示LiuInfo组件。 path: 'Liu', component: LiuInfo, }], } 为路由命名 在路由配置中配置name属性,可以为该路由命名。 被命名的路由,在其他地方使用的时候,可以不用再通过url触发,通过传入配置对象,减少url硬编码的麻烦: const routes = [ { path: '/user/:username', name: 'user', component: User } ] // 使用 <router-link :to="{ name: 'user', params: { username: 'erina' }}"> User </router-link> // 或 router.push({ name: 'user', params: { username: 'erina' } }) 命名视图 同一个路由,匹配的组件内可以定义多个<router-view>出口,通过name属性命名。 路由通过配置项中的components选项,对每个<router-view>出口显示哪一组件进行定义。 用router上的方法跳转路由 基本和window.history一样: router.push(<route>): 添加新记录,并跳转到新路由; router.replace(<route>): 覆盖当前记录,并跳转到新路由; router.go(<num>): 向前、向后(负数)跳转num个路由; 将路由的匹配参数route.params设置为组件的props 配置路由的props选项为true,即可将路由匹配的参数route.params直接设置为组件的props,而不是在组件内通过this.$route.params获取参数。 这样可以解耦组件和路由,组件可以被应用到其他地方,而非与路由参数强绑定(必须要this.$route.params和组件内的使用方式一致才能正确显示组件)。 将 props 传递给路由组件 重定向 路由重定向在路由配置中的redirect选项配置。 配置了重定向的路由在触发时,会直接跳转到重定向的路由,也不会触发当前路由的导航守卫beforeEnter。 有两种方式设置重定向路由: 静态:直接传入一个路由配置对象; 动态:传入一个函数cur => {...target route},接收当前路由cur为参数,可以根据当前路由的参数,动态重定向到对应的路由。 // 动态重定向路由配置: { // /search/text 被动态重定向到 --> /search?q=text path: '/search/:searchText', redirect: cur => { // 方法接收当前路由作为参数**** // return 重定向的路由 return { path: '/search', query: { q: cur.params.searchText } } }, }, 别名 alias 为当前路由设置别名,与当前路由具有相同的效果。 可以是一个单独的路径字符串,也可以是一个由它们组成的数组(代表多个别名)。 别名会如实显示在url中。 别名与重定向的区别: 别名没有重定向过程,与原路由是一样的效果,相当于配置了与原路由一模一样的多个路由; 重定向是在进入原路由后,又执行了跳转到另一个路由。 历史记录模式 在router配置中的history选项设置。 const router = createRouter({ // hash mode. history: createWebHashHistory(), }) 1. hash模式(默认) 在router配置项中的history选项,传入createWebHashHistory()。 hash模式的特点: url中存在一个/#/,#之后的部分都是hash部分,不会被传送到服务器(hash路由完全发生在前端); 页面url的hash部分发生变化,不会引起页面的刷新; hash部分发生变化,会新增一条记录到history,因此可以进行前进、后退操作; 服务器无法获得用户的实际url,用户请求的始终是#之前的url部分,因此在路由跳转、刷新后服务器无需进行任何配置,因为请求的都是同一个url; 每次点击触发路由,hash部分改变,但是并不会发送新的http请求,仅仅是在前端js层面执行了代码(进行了组件替换),搜索引擎爬虫可能无法获取到替换后页面的内容; 2. history模式 在router配置项中的history选项,传入createWebHistory()。 history模式的特点: url中没有’#’,更为美观; 全部url都会被传给服务器,进行对应的资源请求。因此服务器必须把所有可能用到的url都重定向到SPA根页面的html,否则一旦用户刷新页面,可能报错404; 通过history的pushState()和replaceState()方法实现修改路由(修改history记录,不会造成刷新),通过监听popState事件进行路由跳转操作; 路由懒加载 通过动态引入组件cosnt comp = import(component),实现让路由只有在被访问到的时候,才去加载组件。 这可以提高页面的加载速度。 只有在第一次访问对应路由时,执行加载对应的组件,加载完成后组件会被缓存,供再次使用。 全部路由,理论上都应该使用动态引入组件的懒加载模式。 ★ 导航守卫(导航过程中的钩子) 所有的导航守卫,都可以接受两个参数(to, from),beforeEach和beforeRouteEnter可以接受第三个参数next。 1. 全局守卫 router.beforeEach: 每次导航被触发,在所有钩子最前面调用; router.beforeResolve:每次导航被触发,在异步组件被解析后,导航被确认之前调用; router.afterEach:每次导航被触发,在所有钩子最后面调用; 2. 路由专属守卫 在路由配置中,可以为某一路由单独配置自己的守卫。 beforeEnter: 在该路由进入前调用; 3. 为组件单独配置路由守卫 组件内,可以单独配置专属于这个组件的路由守卫,在组件被路由加载后调用。 beforeRouteEnter: 组件已确定被路由加载,在路由执行之前调用; beforeRouteUpdate: 当路由发生变动,但是组件被重复使用的时候调用; beforeRouteLeave: 当渲染该组件的路由,即将跳转到其他路由时调用。 4. 路由守卫调用时序 Navigation triggered. Call beforeRouteLeave guards in deactivated components. Call global beforeEach guards. Call beforeRouteUpdate guards in reused components. Call beforeEnter in route configs. Resolve async route components. Call beforeRouteEnter in activated components. Call global beforeResolve guards. Navigation is confirmed. Call global afterEach hooks. DOM updates triggered. Call callbacks passed to next in beforeRouteEnter guards with instantiated instances. 路由元数据 meta 配置路由的时候,可以传入一个meta属性,为路由添加一些额外的数据。 因此,可以在导航守卫执行的时候,访问to路由或from路由的元数据,来针对性执行对应的逻辑。 导航 & 异步获取数据 导航完成后获取数据和导航完成前获取数据都是可以的。 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据(比如beforeCreate或created)。在数据获取期间显示“加载中”之类的指示。 导航完成之前获取:导航完成前,在beforeRouteEnter钩子中获取数据,在数据获取成功后执行导航。 组合式API 组合式 API中使用Vue-Router,需要从vue-router引入方法: useRouter: 返回this.$router; useRoute: 返回this.$route; 路由后的滚动行为 router配置的scrollBehavior选项,可以配置路由跳转后的默认滚动行为(位置,滚动是否顺滑等)。 滚动行为 const router = createRouter({ scrollBehavior(to, from, savedPosition) { // 始终滚动到顶部 return { top: 0 } }, })

Webpack笔记汇总

2021.09.02

Webpack

一、Webpack基本概念 二、Webpack配置 三、常用 Webpack Plugins 开启Source Map 一、Webpack基本概念 二、Webpack 配置 通过外部webpack.config.js文件来配置。 1. 模式 Mode development: 开发模式。 会使用DefinePlugin把代码中的process.env.NODE_ENV替换为development; 默认开启Source Map; 不会使用UglifyPlugin压缩代码; 注释不会被忽略; production(默认): 生产模式。 会使用DefinePlugin把代码中的process.env.NODE_ENV替换为production; 默认无Source Map; 使用UglifyPlugin压缩代码; 注释被忽略; none: 不使用任何优化。 三、常用 Webpack Plugins html-webpack-plugin 根据配置与模板自动生成html,把打包后的css和js文件直接引用注入到生成的html中,不用每次手动修改。 当打包后的名称里有hash字段时,会非常方便。 Define-Plugin 定义一些常量,可以在代码中使用,打包时自动按照配置文件中的设置,替换为对应的值。 本质上是可以为代码设置一些开关,在配置中定义开闭,对应不同的打包。 应用场景: 根据模式设置,写不同的代码; 根据是否使用新特性,打包不同的代码; 其他根据不同开关量,进行不同选择的情形; progress-plugin 监控打包进度,可以为打包过程提供一个进度提示,有很多hooks可以设置。 provide-plugin 为特定的标识符,设置一个路径。在使用这个标识符的时候无需用import或require引入,在打包的时候自动添加。 比如为$添加jquery路径,为_添加lodash路径。 source-map-devtool-plugin 精细设置Source Map。 split-chunks-plugin 根据配置,抽取公共模块,防止重复打包。 开启Source Map 有两种方式设置Source Map: 通过配置项的devTool选项配置; 使用 SourceMapDevToolPlugin 进行配置;

Vue3中的虚拟DOM和Diff算法

2021.09.01

Vue

一、虚拟DOM是什么?基本实现流程是? 1. 什么是虚拟DOM 虚拟DOM是用JS对象,对真实DOM树进行的简化模拟。 基本的虚拟DOM元素,叫做VNode,它包含以下几个属性: tag: 对应的DOM元素标签名; props: 对应的DOM元素Attributes; children: 子元素。可以是纯文本子元素,也可以是VNode子元素。 2. 基本的虚拟DOM实现流程 虚拟DOM的实现流程: 通过createNode(也常写作h函数),生成VNode节点; 各VNode节点通过children属性,相互嵌套形成树结构,构成虚拟DOM树(与真实DOM一一映射); 每当数据有更新,同步更新虚拟DOM树 (对应操作:render); 将新、旧虚拟DOM树进行比较(对应操作:diff),找出差异之处; 将这些差异之处,增量更新到真实的DOM树中(对应操作:patch —— 打补丁); 虚拟DOM更新一次操作的时间复杂度: O(render V-DOM) + O(diff) + O(patch) 其中,render和diff是纯JS操作,patch操作复杂度与DOM的更改量δdom正相关; 二、为什么需要虚拟DOM而不是直接操作DOM? 使用JS模拟DOM(虚拟DOM)的原因: 操作DOM元素的操作比较昂贵,JS操作相对便宜。虚拟DOM的render+diff是纯JS操作,patch操作复杂度与DOM的更改量δdom正相关;因此,虚拟DOM可以在各种情况下都保证相对不错的性能: 在DOM树元素较多而修改的部分较少的情况下:执行虚拟DOM的【render + diff + patch】操作,比直接重新生成渲染整个DOM树消耗更低; 在全部元素都修改了的情况下:直接重新生成替换DOM树的效率更高,render+diff操作将成为额外代价; 使UI界面生成函数化; 虚拟DOM树是一个JS对象,可以作为一种通用格式,不局限于浏览器DOM,也可以在其他平台进行渲染; 三、虚拟DOM与diff算法的简易实现 // create a [vnode], with properties: // 1. tag: HTML NodeTag // 2. props: HTML element attributes // props contain 2 kinds: // - [eventListener] 'onEvent'; // - [normal attributes] 'attr'; // 3. children: child vnodes. // child nodes contain 2 kinds: // - [text] normal text string node; <div>123</div> // - [other Vnodes] <div><span></span></div> function h(tag, props, children) { return { tag, props, children } } // mount [vnode] to a [container]. function mount(vnode, container, anchor) { // Create an DOM element according to vnode's tag. const el = document.createElement(vnode.tag) // When a Vnode is mounted: // Set a property to Vnode to memorize the real DOM element where it's mounted. vnode.el = el // Set attributes. (props) if (vnode.props) { for (const key in vnode.props) { if (key.startsWith('on')) { el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key]) } else { el.setAttribute(key, vnode.props[key]) } } } // Dealing with Vnode's children node. if (vnode.children) { if (typeof vnode.children === 'string') { el.textContent = vnode.children } else { vnode.children.forEach((child) => { mount(child, el) }) } } // Anchor means: // In container element, where the new element to insert before. // anchor should be an real DOM element, and it should be the container element's child element. if (anchor) { container.insertBefore(el, anchor) } else { // No anchor, insert it at the tail. container.appendChild(el) } } // patch() function: // Compare n1 node (old) and n2 node (new), find their difference & modify them in the real DOM tree. function patch(n1, n2) { // 1. check if n1 and n2 are of the same type if (n1.tag !== n2.tag) { // 2. if not same type, replace directly const parent = n1.el.parentNode const anchor = n1.el.nextSibling parent.removeChild(n1.el) mount(n2, parent, anchor) return } // Reuse the original DOM element const el = (n2.el = n1.el) // 3. if yes // 3.1 diff props const oldProps = n1.props || {} const newProps = n2.props || {} for (const key in newProps) { const newValue = newProps[key] const oldValue = oldProps[key] if (newValue !== oldValue) { if (newValue !== null) { el.setAttribute(key, newValue) } else { el.removeAttribute(key) } } } for (const key in oldProps) { if (!(key in newProps)) { el.removeAttribute(key) } } // 3.2 diff children const oc = n1.children // old children const nc = n2.children // new children if (typeof nc === 'string') { // pure text children. if (nc !== oc) { el.textContent = nc } } else if (Array.isArray(nc)) { // vnode array children. if (Array.isArray(oc)) { // array diff const commonLength = Math.min(oc.length, nc.length) for (let i = 0; i < commonLength; i++) { patch(oc[i], nc[i]) } if (nc.length > oc.length) { nc.slice(oc.length).forEach((c) => mount(c, el)) } else if (oc.length > nc.length) { oc.slice(nc.length).forEach((c) => { el.removeChild(c.el) }) } } else { el.innerHTML = '' nc.forEach((c) => mount(c, el)) } } } const Component = { data() { return { count: 0 } }, render() { return h( 'div', { onClick: () => { this.count++ } }, String(this.count) ) } } function createApp(Component, container) { // implement this const state = reactive(Component.data()) let unMount = true let prevTree watchEffect(() => { const tree = Component.render.call(state) if (unMount) { console.log(tree); mount(tree, container) unMount = false } else { patch(prevTree, tree) } prevTree = tree }) } // calling this should actually mount the component. createApp(Component, document.getElementById('app')) 四、Vue3中真实的diff算法 Vue3中的diff算法

前端路由

2021.08.31

JavaScript

前端路由相关 一、页面路由的历史 1. 早期:路由由服务端控制 流程: 客户端发起http请求 -> 服务端根据请求url匹配不同的资源(不同html) -> 返回请求数据 -> 客户端渲染显示 好处: 直接生成html,seo友好; 首屏渲染快; 首屏时间(白屏时间): 从输入url按下回车到页面任意元素加载出来的时间。 缺点: 服务器压力大。每次请求都需要处理+响应;、 前后端深度耦合,开发协同流程混乱; 2. 现代:SPA单页面应用+前端路由 单页: 单个html文件。 一个html文件,通过前端路由+ajax等技术,实现无刷新路由(更新页面)。 好处: 无需刷新; 加载过的资源无需重复加载,因为都在一个页面内; 只需向服务器请求必要资源,服务器压力小; 前后端分离开发; 缺点: SEO不友好,搜索引擎爬虫无法获取页面信息; 没有提前生成html,需要现场发请求,初次加载首屏渲染时间长; 前端逻辑复杂度增加; 二、前端路由的实现原理 1. location.hash 1.1 hash是什么 hash就是页面url中#后面的部分。 1.2 hash路由的特点 如果发生http请求,hash部分不会发送给服务器; hash部分改变,不会导致页面刷新; hash的改变,会导致浏览器访问历史中添加一条记录,可以通过浏览器的返回、前进按钮来访问前一、后一页面; hash改变,会触发window上的hashChange事件。 1.3 更改页面hash方式 location.hash a标签的href = '#xxx'; 1.4 监听hash变化的方式 通过window对象的hashChange事件监听。每次hash变化都会触发hashChange事件。 2. history 2.1 history对象的方法和事件 history.forward(); 前进一步; history.back();后退一步; history.go(num); 正数前进num步,负数后退num步; history.pushState(state, title, path);在浏览记录最后添加一个历史记录; history.replaceState(state, title, path);替换当前历史记录; pushState/replaceState 的参数: state, 是一个对象,是一个与指定网址相关的对象,当 popstate 事件触发的时候,该对象会传入回调函数; title, 新页面的标题,浏览器支持不一,一般传入null; url, 页面的新地址。 pushState和replaceState的特点: 不会触发popState事件,也不会刷新页面; 会将当前的页面href替换为最新加入的url(域名+path); 需要手动触发popState事件,才能更新页面; 2.2 history监听的事件 history监听popState事件,以在页面url变化的时候监听变化,做出响应。 触发popState事件的五个操作: 点击浏览器后退按钮; 点击浏览器前进按钮; JS执行history.forward(); JS执行history.back(); JS执行history.go(); 3. hash路由和history路由之间的区别 hash 有#符号,不美观。而history没有; hash 的url中#部分内容不会给服务端, history的所有url内容都会给服务端; hash 通过 hashchange 监听变化,history 通过 popstate 监听变化(监听事件不同)。

Express基本操作

2021.08.31

JavaScript

Express框架基本操作 引入与创建express app实例 const express = require('express') const app = express() 基本路由 app.METHOD(PATH, HANDLER) METHOD: http方法; PATH: 请求路径,一旦匹配执行handler; HANDLER: 路径匹配后的执行函数。 // 收到post请求,且路径为'/'时,返回'Got a POST request'。 app.post('/', function (req, res) { res.send('Got a POST request') }) 请求与响应api Express的请求与响应对象,继承自Node的http.IncomingMessage和http.OutgoingMessage。 Express对它们进行了扩展。 请求对象request request对象有以下api: req.app req.baseUrl req.body req.cookies req.fresh req.hostname req.ip req.ips req.method req.originalUrl req.params req.path req.protocol req.query req.route req.secure req.signedCookies req.stale req.subdomains req.xhr 响应对象response 中间件 Middleware 中间件的含义 中间件: 在客户端请求到达Express,与Express处理这个请求的中间,对请求进行拦截,从而执行一些其他逻辑的组件。 在Express中,app.use(fn)这个api可以为Express应用实例设置中间件。 app.use(fn)的功能是:对任何一个到达的请求,执行一次fn(不区分请求的具体形式)。 Express中间件函数fn接受三个参数:fn(req, res, next): req: 请求对象; res: 响应对象; next: 交出控制权,去往下一个处理流程。(可以直接调用:next()); 中间件可以实现以下功能: 执行任意代码; 对请求对象req和响应对象res进行修改; 结束请求-响应循环; 交出执行权,给下一个中间件。 中间件的实际效果,与它的设置位置有关 因为所有请求到达Express,都是按从上到下的顺序依次处理的。中间件设置在哪里,请求就在哪里处理。 其实路由也是中间件? 是的,路由只是一种特殊的中间件。路由中间件的功能是:只对匹配到相同路径的请求进行处理,否则执行next()向下交付。 中间件是Express核心的设计 中间件是Express最为核心的一个设计,它的好处是:可以在不修改业务逻辑代码的情况下,任意添加我们想要的功能。 Express的功能就是由一个个中间件先后串联完成的。 中间件,可以理解为是面向切面编程AOP的一种实现。 Express中间件的分类 应用程序级中间件: 托管静态文件 托管静态文件,相当于公开了某个文件夹的内容。当用户请求对应目录的静态文件,则返回对应的文件。 使用express.static()中间件实现对某个路径下文件的静态托管。 express.static(rootPath, [options]); 设置方式: app.use(path, express.static('public')); 含义是:当用户匹配path时,映射到对应的public文件夹,获取对应的静态文件返回给用户。 如果不提供path,则默认是根目录。 // 用户访问`域名/static/静态文件名`,则返回public文件夹下对应文件给用户。 app.use('/static', express.static('public'));

原生API的手动实现

2021.08.23

JavaScript

部分原生API的手动实现 一、数组 reduce()方法 二、对象 深拷贝 一、数组Array 1. reduce()方法 function myReduce(arr, fn = (accu, item, index, array) => {}, init) { // 判断arr是否是数组; if (Object.prototype.toString.call(arr).slice(8,-1) !== 'Array') { throw new Error('Must be called to an array.'); } // 判断fn是否是函数; if (Object.prototype.toString.call(fn).slice(8,-1) !== 'Function') { throw new Error('No callback function.'); } // 当没有提供初始值,初始值设为arr[0],并且从1位置开始遍历; // 提供了初始值,从0位置开始遍历; let start = 0; if (init === undefined) { if (arr.length === 0) throw new Error('Call reduce with no init value to an empty array.'); init = arr[0]; start = 1; } // 执行函数,迭代修改init。 for (let i=start; i<arr.length; i++) { init = fn(init, arr[i], i, arr); } // 返回最终结果。 return init; } 2. map()方法 // for循环实现 function myMap(arr, fn = i => i) { if (!Array.isArray(arr)) throw new Error(`myMap must be called on Array instance.`); if (typeof fn !== 'function') throw new Error(`the second parameter must be function.`); let res = new Array(arr.length); for (let i=0; i<arr.length; i++) { res[i] = fn(arr[i], i, arr); } return res; } // reduce方法实现 function myMap2(arr, fn = i => i) { if (!Array.isArray(arr)) throw new Error(`myMap must be called on Array instance.`); if (typeof fn !== 'function') throw new Error(`the second parameter must be function.`); return arr.reduce((res, item, index, array) => { return [...res, fn(item, index, array)]; }, []); } 3. filter()方法 function myFilter(arr, fn) { if (Object.prototype.toString.call(arr).slice(8,-1) !== 'Array') { throw new Error('Must be called to an array.'); } if (Object.prototype.toString.call(fn).slice(8,-1) !== 'Function') { throw new Error('No callback function.'); } let res = []; for (let i=0; i<arr.length; i++) { let cur = fn(arr[i], i, arr); if (cur) res.push(arr[i]); } return res; } 二、对象 1. call、apply 和 bind 1.1 call 和 apply 基本思路: 如果传入上下文context,则把方法挂载到context,如果没有则挂载到window; 为临时挂载的方法设置一个独一无二的symbol键名; 通过context调用this指向的方法; 调用完成,删除context上临时挂载的方法。 // call.js Function.prototype.myCall = function(context, ...args) { let c = context || window; let sym = Symbol('callFn'); context[sym] = this; let res = context[sym](...args); delete context[sym]; return res; }; apply相似,把第二个参数设置为数组即可。 1.2 bind 基本思路: 传入一个上下文context,如果没有则为window; myBind内部this指向需要被绑定的原始函数fn; 返回一个函数binded,通过fn.call(context),手动指定其内部的this指向; 返回这个函数即可。 // bind.js Function.prototype.myBind = function(context) { let c = context || window; let fn = this; let binded = function(){ fn.call(c); } return binded; } 2. 深拷贝

计算机网络:TCP与HTTP协议

2021.08.23

Network

TCP

HTTP

HTTPs

计算机网络笔记: TCP和HTTP部分 一、TCP协议及其特点 TCP协议是TCP/IP协议栈里面,用于运输层的协议。 TCP协议的特点: 面向连接:必须先握手建立连接再通信,通信完协商断开连接; 面向连接和有连接的关系? 面向连接的连接状态信息,只在连接的端点保存。而有连接的情况,是除了端点之外,中点保障二者通信的网络节点也保存它们的连接信息。 可靠交付:保证接收方能可靠接收到发送的全部数据,且双方数据完全一致; 流量控制:发送方发送的数据流量,不会淹没接收方; 点对点:TCP连接是一对一的(两端均由套接字Socket决定: IP+端口号); 全双工:双方可以随时互相发送数据; TCP头部占用20字节; 同一个IP可以有多个TCP连接,同一个端口号也可以出现在多个TCP连接中。 浏览器:一个域名最多只能同时创建6-8个TCP连接。 这些特点与UDP相互形成对比,UDP的特点: 无连接:不需要提前建立连接; 面向报文:一次发送一整个报文。不进行任何分割,加上UDP首部后,直接传递给网络层; 既不保证可靠交付,也不保证按序到达; 不进行流量控制:即收即发,即使拥塞也照常发送数据; 支持广播:可以一对一、一对多、多对多; 头部字节较少:UDP的头部只有8字节; TCP和UDP的典型应用 TCP: Web、FTP、Telnet远程登录、SMTP(EMail)等; UDP: 流媒体、DNS、远程会议、网络电话(基本都是实时性要求高的应用); 1.1 TCP报文 TCP协议的首部由20字节的固定部分和4n字节的可变部分构成。其固定部分组成结构如下: 源端口、目标端口(各16位) 注明数据来源和目标的端口号。 序号(32位) 在TCP传输中,每一个字节都按顺序被标上序号。整个字节流的传输起始序号在TCP连接建立握手时指定。 TCP报文首部的序号,表示这个报文段第一个字节的序号。 为什么不一直用固定的起始字节序号,比如0,开始传输? 因为TCP连接的两个端口,未必只进行一次连接,且TCP报文在传输的时候可能会在网络中滞留。如果都从一个字节序号开始,假设我们先后在两个相同设备的相同端口,建立了两个TCP连接(建立 -> 关闭 -> 建立),那么前一个建立的连接中滞留的报文,有可能在第二次建立的连接中到达,而且因为序号没有区别,会被正常接收,这样就造成了错误。 TCP连接的起始序号是如何选择的? 可以取时钟信号的低32位。这样也虽然存在冲突的可能,但是概率极低。 确认号(32位) 是对方期望接收的下一个报文段第一个字节的序号。 确认号是接收方返回报文的重要内容。如果返回的确认号是N,则代表N-1之前的(包括N-1)序号的字节都已经被正确接收。 数据偏移(4位) 指出TCP报文中,数据起始位置距离报文起点有多远。(实际上就是指出了报文首部的长度。) 单位是4字节(32位)。也就是说,数据偏移如果是5,则代表数据第一个字节在距离起始点20字节位置处。 4位二进制数最大能表示的数字是15,因此 TCP报文首部最大长度为60字节,也就是说选项部分长度不能超过40字节。 保留(6位) 暂时无用。应该全部置0. 紧急标志URG(1位) 老字段,一般不用。 URG置1,则代表该报文段数据是紧急数据。 紧急数据会被TCP插入本报文段数据的最前面,而不是在后面排队。 确认标志ACK(1位) 确认标志ACK=1时,确认号字段才是有效的。 TCP规定,在连接建立之后,所有报文段都必须将ACK置1. 推送标志PSH(1位) 如果希望对方在收到报文后立即响应,可以将PSH置1. PSH=1时,表示推送信息,对方在收到后将不再等待TCP缓存填满才上传给应用层,而是直接上传立即更新信息。 复位RST(1位) RST=1时,表示TCP连接出现严重差错,必须释放然后重新建立连接。 RST=1还用来拒绝一个非法的报文段或拒绝打开一个连接。 同步标志SYN(1位) 用于在TCP连接建立时同步序号。SYN为1代表请求建立连接。 当SYN=1、ACK=0,表示这是一个请求连接的报文段。当SYN=1、ACK=1时表示这是一个同意建立连接的响应报文。 终止标志FIN(1位) 用来释放一个连接。FIN=1表示数据已经发送完毕,请求释放一个连接。 窗口(16位) 指的是发送本报文一端的接收窗口大小。 单位是1字节。 如果接收的窗口字段为80,则代表如下含义: 对方缓存中还有80字节的空位,可以向它再传80字节的数据。 窗口值是发送方设置发送窗口大小的重要依据。(不能大于接收方的接收窗口大小) 检验和(16位) 检验和检验的范围包含首部和数据两部分。 检验和的计算与UDP一样,需要加一个伪首部。 紧急指针(16位) 紧急指针只有在URG=1时才有意义。它指出了紧急数据末尾在报文段中的位置。 通过它可计算本报文数据中的紧急数据的长度。紧急数据之后就还是普通数据。 选项(位数可变) 最长为40字节。如果使用后没凑满4字节的整数倍,则必须用填充0凑够4字节。 最初只有一种功能: 配置最大报文段长度MSS。(MSS代表每个TCP报文段中的数据部分的最大长度。) 默认情况下,如果没有配置MSS值,则MSS=536字节。 因此,所有互联网上的主机都必须能接受536+20=556字节的TCP报文段。 后来又加入了窗口扩大、时间戳和选择确认选项。 1.2 TCP协议如何实现可靠传输 发送窗口 发送窗口是发送数据流中的一段,有一个前沿和一个后沿。前沿是能发送的最远的数据位置,后沿是发送且已确认的最大序号字节位置。前后沿之间的字节位置就是发送窗口。 发送窗口表示:在没有收到接收端确认的情况下,发送方可以将发送窗口中的全部数据都发送给对方。凡是没有收到对方确认的数据都必须暂时保留,以便在超时传送中重用。 随着接收端确认信息的收到和窗口字段数的改变,发送窗口动态改变。已确认发送成功的字节发送端可丢弃,同时发送窗口后沿向前移动。 发送窗口的后沿只能向前移动或不动,不能向后移动。(已确认发送成功的信息不能撤回) 发送窗口的前沿根据接收窗口的大小和确认号可以向前或不动,也可以向后移动(但是TCP协议一般非常不建议这样做。) 发送窗口并不时刻保持与接收窗口一样大。因为网络时延和拥塞情况,发送窗口一般小于接收窗口。 接收窗口 接收窗口位于接收端,也有前沿和后沿。 接收窗口后沿取决于已返回确认的最大字节序号。前沿取决于接收端TCP缓存的大小。 1.3 TCP连接的建立与释放 建立链接:三次握手 TCP连接建立的过程叫做握手。需要在客户和服务器之间进行三次握手才能建立TCP连接。 为什么两次握手是不行的? 半连接:发送方向接收方请求建立连接,接收方回复的确认连接信息可能丢失,造成接收方建立了虚假的半连接,并为虚假连接分配了资源,实际上发送方并未建立起真正的连接; 旧数据被新连接接收:因为具有超时重传机制,发送方请求建立连接的信息,如果没收到确认也会超时重传。如果两次握手,可能会造成发送方发送的旧数据,被超时重传后建立的新连接,当做新数据接收; ① 请求连接握手 请求方发出连接请求报文段。 置SYN=1,ACK=0,同时给出序号seq=x; ② 同意建立连接确认 连接被请求方对TCP连接请求给出确认。 置SYN=1,ACK=1,同时给出序号seq=y和确认号ack=x+1; ③ 请求方再次给出确认 请求方对被请求方给出的确认,再次进行确认。(防止“已失效的连接请求报文段”问题,见P239) 置SYN=0、ACK=1,同时seq=x+1,ack=y+1. TCP连接释放:四报文挥手 TCP连接释放过程需要进行四次握手。 ① 连接释放请求端发送请求释放连接握手 请求端请求释放连接。发出握手信号。 置FIN=1、ACK=0、seq=u(当前序号) ② 对①信号的确认 被请求端进行确认。 置ACK=1、seq=v(当前序号)、ack=u+1(当前确认号) ③ 等待被请求方数据传输完毕 ②过后,被请求方依然可以向对方发送数据。直到数据传送完毕。 ④ 被请求方再次发送释放连接确认 数据传输完毕后,被请求方向对方再次发送可关闭连接的确认信号。 置FIN=1、ACK=1、seq=w(当前序号)、ack=u+1(★!!这里必须再次重复②中第一次确认的确认号!) ⑤ 请求方最后一次给出确认 请求方收到对方第二次确认后,发送最后一次确认,进入时间等待状态(TIME-WAIT, 等待时长为2MSL,一个MSL TCP建议为两分钟,实际情况可适当缩短)。 置ACK=1、seq=u+1(当前序号)、ack=w+1(当前确认号) ⑥ 被请求方收到后不再给出确认,直接释放连接。 ⑦ 2MSL时间到后,请求方也释放连接。 为什么要设置2MSL等待时间这种挥手机制? 因为TCP连接释放过程,无法进行完美确认。因为最后一次发送的确认断开报文,对方是否能够收到是不可知的。(如果想知道,必须要再发送一个确认报文,让对方应答,这样对方反过来也面临一个同样的问题) 1.4 TCP的慢开始 TCP的慢开始是一种TCP拥塞控制方法。 在TCP发送方刚开始发送数据的时候,由于不知道网络的负载情况,先从一个小的发送窗口开始,每次收到确认,对发送窗口进行扩大。 这样有效防止了网络拥塞,但是相对于直接设置大窗口一次性发送大量数据,TCP的传输速率变低。 1.5 浏览器中的TCP连接 浏览器中,一个域名最多只能同时创建6个TCP连接。 因此,多个请求同时处理,会发生队头阻塞(HTTP队头阻塞)。 二、HTTP超文本传输协议 HTTP协议定义了浏览器(万维网客户)怎样向万维网请求文档,也定义了服务器怎样把文档传送给浏览器。 HTTP用TCP协议作为运输层协议。 HTTP协议本身是无连接的(不需要提前建立连接),但TCP协议是面向连接的。 HTTP协议是无状态的(Stateless)。也就是说,每次向服务器发出HTTP请求,服务器都同等对待,不会因为之前的请求而改变响应内容。(但是后来因为业务需要,使用Cookies这类技术可以实现对双方的识别。) 2.1 HTTP请求与响应的过程 主机发出HTTP请求时,HTTP协议首先要和服务器建立TCP连接。 建立TCP连接需要三报文握手。前两个报文需要一个RTT往返时间,在第三个报文(主机二次确认报文)时,HTTP就把请求报文放入TCP的数据部分,发送给服务器。 服务器收到后,返回请求的文档给主机(先传输)。因此发送一个HTTP请求并相应所需的时间为: HTTP文档传输的时间 + 2×RTT(往返时间) 2.2 HTTP的版本、各版本改进点 目前HTTP版本主要有,HTTP/1.0、HTTP/1.1和HTTP/2。 2.2.1 HTTP/1.0 发布于1996年。 默认不支持TCP长链接:每次HTTP通信完成,断开TCP连接; 缓存使用If-Modified-Since和Expires判断; 2.2.2 HTTP/1.1 改进点 HTTP/1.1相较于HTTP/1.0改进了以下几点: 长连接; 不每次断开TCP连接,只要C/S任意一方没有明确提出终止连接,则保持TCP连接; HTTP/1.1中,所有连接默认为持久连接; 长连接首部行:Connection: keep-alive / close。 管线化(Pipelining); 可以一次性并行发出多个请求,服务器按请求顺序进行响应。而不是每次发出一个请求,然后等待响应接收完成再发出下一个请求; 缺点: 仍存在队头阻塞问题。如果先发出的请求在服务器端响应慢,仍然阻塞后续响应; 服务器压力大,为了按序返回,需要缓存多个响应; 浏览器中途断连服务器,需要重新处理多个请求; 添加ETag、If-Match、If-None-Match等缓存相关首部行; 添加Host首部行,记录主机名(因为虚拟主机后,一个IP地址可以对应多台虚拟主机),且强制请求必须带有Host头部。 新增24个错误状态码; 2.2.3 HTTP/2.0 改进点 新的二进制格式: 以二进制帧为最小单位传输,原本的报文消息被划分为更小的数据帧; 解析不再基于文本,而是变成基于二进制,避免了文本解析的复杂性; 多路复用: 一个Http请求,被当做一个流Stream,每个流有自己对应的Stream ID; 每一个二进制数据帧,都包含有它从属于哪个流的Stream ID; 在一个TCP连接上,可以交替发送从属于任意流的数据帧,在接收时,拼接每个流的数据帧成为完整数据。 优点: 解决了应用层队头堵塞问题,响应慢不影响下一次请求的发送; 只需要一个TCP连接。减少了TCP连接数,避开了TCP慢启动问题; Header压缩: 使用HPACK算法,避免了重复传输header,压缩了首部的体积; 服务端推送: 客户端请求一个资源,与这个资源相关的后续资源服务器一并推送给客户端,由客户端缓存,减少了请求次数; 应用层重置连接: 直接通过特殊类型的帧,从应用层关闭某一流,无需关闭TCP连接; 请求优先级设置: 每个流都可以设置权重优先级,关键请求优先响应; 流量控制: HTTP2.0中,每一方都向对方公开自己的流量窗口,限制另一方发送数据的大小; HTTP2.0的缺点: 所有数据帧共用1个TCP连接,因为TCP是按序确认,可靠交付,一旦中途发生丢包,整体传输效率变差。 当丢包率大于2%时,效率不如Http1.1; 2.2.4 HTTP/3.0:QUIC协议(Quick UDP Internet Connections,快速UDP互联网连接) 基于UDP,实现TCP的流量控制、可靠传输功能; 集成了TLS加密功能; 多路复用,实现了多路数据流单独传输,避免队头阻塞; 快速握手,0RTT或1RTT时间建立连接; QUIC无法推广的原因: 不再基于TCP而是UDP,普遍系统对UDP的优化程度较低; 网络中间设备对UDP的优化程度远低于TCP,容易丢包; 服务器和浏览器支持程度差。 2.3 ★ HTTP报文结构 HTTP是面向文本的。所以,报文的每个字段都是ASCII码串,各个字段的长度都是不确定的。 2.3.1 请求报文 2.3.1.1 请求报文结构 请求报文分为:开始行、首部行 和 实体主体。 开始行:也叫做请求行。开始行的三个字段(方法、URI、版本)之间都以空格分隔开。最后是CR(回车)和LF(换行)。 首部行:用来说明浏览器、服务器或报文主体的一些信息。首部可以有很多行,也可以不使用。每一个首部行都有首部字段名和它的值,结尾为CRLF(回车换行)。 首部行的顺序没有硬性规定。但是一般建议控制数据作为第一行。(请求是host,响应是date) 实体主体:请求部分一般不使用这个字段,用于响应报文回复内容使用。 2.3.1.2 请求方法 Method 方法(操作) 意义 OPTION 请求一些选项的信息。 GET 请求读取由URL所标志的信息。 HEAD 请求读取由URL所标志的信息的首部。 POST 给服务器添加信息。 PUT 在指明的URL下储存一个文档 DELETE 删除URL所标志的资源 TRACE(已废弃) 用来进行环回测试。(已废弃。) CONNECT 用于代理服务器。 2.3.1.3 get/post请求方法的区别 语义上有区别,GET是请求数据,POST是上传数据; GET请求通过URI携带信息,浏览器和服务器对其长度有限制。POST请求通过请求主体request body携带信息,参数大小无限制; GET请求会保存在浏览器历史记录中,以供缓存,而POST不会; GET操作是幂等的,多次操作最终效果相同。而POST操作不是幂等的; 2.3.2 响应报文 响应状态码: 2.3.3 容易混淆的首部行:host/origin/referer host:(格式:域名+端口号) Http1.1规定请求时必须携带(没有或超过1个都会返回400BadRequest)。告诉服务器请求的资源所处的域名和端口号; 因为虚拟主机技术的出现,可以把一台物理服务器,分为多个互联网主机,运行多个网站服务。 所以,http1.1规定必须指明host首部行,让ip对应的物理服务器可以识别请求的资源位于它下方哪个域名+端口中。 origin: (格式:协议+域名+端口号) origin表示跨域请求或预检请求的来源站点(也就是同源策略等所提到的源)。 origin只包含请求来源页面的协议、域名、端口号,不包含任何路径信息。 只有以下两种情况,会携带origin首部: 跨域请求; 同域的POST请求。 referer:(默认格式:协议+域名+端口号+路径+参数) 当前请求的来源页面的地址。referer不包含url中的hash值! referer表示当前请求是通过哪个页面发起的。比如通过www.baidu.com上的链接点击进入另一个页面,或者发起的ajax请求,都会携带referer: https://www.baidu.com/ 这个首部行。 以下几种情况,referer不会被发送: 来源页面采用的协议,为表示本地文件的 “file” 或者 “data” URI; 当前请求页面采用的是非安全协议(HTTP),而来源页面采用的是安全协议(HTTPS); 直接输入网址或通过浏览器书签访问; 使用 html5 中 rel=noreferrer 属性。<a href="/test/index.php?abc" rel="noreferrer" target="_blank">noreferrer</a> Referrer Policy 的设置方法 是否随请求发送referer,通过一种叫做referrer policy的策略来控制,它有9种取值,对应不同的referrer策略。设置referrer policy的方式是: 通过服务端响应的报文首部行中的Content-Secure-Policy字段; 通过html的<meta name="referrer">标签; 通过<img>、<a>等的referrerpolicy属性,单独设置某个链接的referrer policy。 三、Https协议 Https协议

JS模块化

2021.08.01

JavaScript

学习内容:《现代JavaScript教程》 1. 为什么要模块化Module 关注点分离:一个的长代码不易维护和重用,我们希望将一个单独的功能分离出来(单一职责),使用时按需加载。这个分离的实现某个单一功能的代码就是模块。 避免全局污染: 一个模块可以包含用于特定目的的类或函数库。 15.1 历史上的JS模块 因为历史原因而存在的早期JS模块化系统,现在不应该再被使用。 AMD —— 最古老的模块系统之一,最初由 require.js 库实现。 CommonJS —— 为 Node.js 服务器创建的模块系统。 UMD —— 另外一个模块系统,建议作为通用的模块系统,它与 AMD 和 CommonJS 都兼容。 15.2 同步模块与非同步模块 由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。 但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式。 浏览器加载模块,异步方式更适用。 15.3 CommonJS模块系统 因为Node.js起初只能使用CommonJS模块,所以还是需要学习这个同步加载模块系统的使用方法。 阮一峰:模块 CommonJS的一些基本特性: 每个文件是一个模块,具有独立的作用域。在每个文件内定义的变量都是自己私有的,外部不可访问; 如果想暴露变量给其他文件(模块),必须定义为global对象的属性; 不推荐。暴露模块内变量为全局,违背模块化设计的初衷。 每个模块内部,module变量代表当前模块,相当于this; module的exports属性(即module.exports)是模块对外的接口。加载某个模块,其实是加载该模块的module.exports属性; 模块内使用require()来引入外部模块,它获取到的值是模块的module.exports,可以赋值给当前模块任意变量; 模块只会在第一次加载的时候运行一次,之后结果被缓存,不会再次运行。如果想要再次运行,必须清除缓存; 所有代码都运行在模块作用域,不会污染全局作用域; 模块加载的顺序,按照其在代码中出现的顺序; module是Module()构造函数的实例,具有以下属性: module.id 模块的识别符,通常是带有绝对路径的模块文件名。 module.filename 模块的文件名,带有绝对路径。 module.loaded 返回一个布尔值,表示模块是否已经完成加载。 module.parent 返回一个对象,表示调用该模块的模块。 module.children 返回一个数组,表示该模块要用到的其他模块。 module.exports 表示模块对外输出的值。 为了方便,Node为每个模块提供一个exports变量,指向module.exports。 这等同在每个模块头部,有一行这样的命令; let exports = module.exports; require()中的filename,如果没有扩展名,则默认为.js; 根据参数的不同格式,require命令去不同路径寻找模块文件: 如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require(‘/home/marco/foo.js’)将加载/home/marco/foo.js。 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require(‘./circle’)将加载当前脚本同一目录的circle.js。 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。 举例来说,脚本/home/user/projects/foo.js执行了require(‘bar.js’)命令,Node会依次搜索以下文件: /usr/local/lib/node/bar.js 【先寻找全局目录】 /home/user/projects/node_modules/bar.js /home/user/node_modules/bar.js /home/node_modules/bar.js /node_modules/bar.js 这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。 如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require(‘example-module/path/to/file’),则将先找到example-module的位置,然后再以它为参数,找到后续路径。 如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。 如果想得到require命令加载的确切文件名,使用require.resolve()方法。 require()中传入一个目录的时候,会按以下方式操作: 如果目录中存在package.json文件,则从中读取main字段用来确定入口文件; 如果目录中没有package.json或没配置main字段,就寻找index.js或index.node作为默认的入口文件。 所有缓存的模块保存在require.cache之中。如果想删除某一个加载过的模块的缓存,可以使用delete require.cache[moduleName]; CommonJS模块输出的内容,一旦被require()获取,就不再受到原模块内变量的影响; 15.4 ES6引入的JavaScript语言级模块(静态导入) ES6之后引入了语言级Module语法: 使用import…from…引入模块; 使用export…导出模块内容; 注意: 如果在HTML文档中使用,必须在<script>标签内加上type=’module’,告诉浏览器这里的代码应该被当做module处理。 import/export都只能使用在当前文件的顶级作用域上,在{}包裹的代码块内部无法使用import和export,会报错(比如if(){}这类)。 15.4.1 import import有两种用法: 一是从export的内容中摘取某几个变量,使用花括号括起来利用对象的解构赋值规则进行赋值: import {a,b} from ‘./something.js’; // 从something.js export的对象中找到键名为a,b的两个,然后赋值给前面对象,变量名还是a,b。 二是作为一个对象整体导入。使用import * as obj from…语法。 import * as something from ‘./something.js’ // something有something.js中全部的方法,使用时用something.fun1()这样的语法结构。 import导入的时候也可以修改导入后的变量名。使用as关键字。 import {a as fun1} from ‘./something.js’; //将something.js中的a方法导入为fun1使用。 15.4.2 export 可以在任何声明前加上export,表示对外部引用导出这个变量。 export let a = 1; export const b = 3.14; export function fun1(){}; export class User{} 亦可以先声明,然后再统一导出。这里需要在导出的时候将他们汇总成一个对象。 let a = 1; const PI = 3.14; function fun1(){}; export {a,PI,fun1}; //统一为一个对象导出 export也可以使用as关键字,为导出的变量另起一个名。 let a = 1; export {a as goodBoy}; // 外面在import的时候,使用goodBoy进行匹配; 15.4.3 默认的export和import 使用export导出的数据,需要命名,且import导入的时候也需要用花括号内部使用同样的命名。 还有一种export方式,可以使得在import的时候无需使用花括号,也无需强制使用原来的命名,就是带有default的export。 // default默认导出 export default class User{}; // 默认导入:无需花括号,也可以随便起名,User可以换成其他变量名。 import User from ‘User.js’; 本质上,export default是在导出的对象中创建了一个键名为default的属性,一旦检测到import未使用花括号,就默认把这个default的键值赋给外部。 默认导出这个方式存在争议,它给了导入的人员一定的命名权。 一般来说使用带命名的导出更不容易出错。 默认导出在使用export … from … 这个立即导入并导出语法的时候也存在问题。 15.4.4 导入并立即导出export … from … 当我们需要import一个文件,并立即将其导出export给其他文件使用时,可以简化写法:export … from … export {User} from ‘User.js’; // 表示从User.js导入User,并立即export {User}。 15.4.5 ★动态导入 import…from… 称作静态导入,因为它from后面的必须是完整字符串,而不能是一个函数调用。而且也不能在代码块(如函数)内部使用,必须在顶级作用域声明。 这是因为 import/export 旨在提供代码结构的主干。 这是非常好的事儿,因为这样便于分析代码结构,可以收集模块,可以使用特殊工具将收集的模块打包到一个文件中,可以删除未使用的导出(“tree-shaken”)。 这些只有在 import/export 结构简单且固定的情况下才能够实现。 可以在import后加一个括号(),来进行动态的导入。 import(‘./index.js’); // 加载并返回一个promise对象,它被resolve为包含index.js所有导出的模块对象。 尽管 import() 看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号(类似于 super())。因此,我们不能将 import 复制到一个变量中,或者对其使用 call/apply。因为它不是一个函数。 动态导入在常规脚本中工作时,它们不需要 <script type=”module”>.

CSS笔记

2021.07.28

CSS

CSS细节记录 二、主要内容 2.1 特指度、继承和层叠关系 2.1.1 CSS特指度 CSS特指度由4位构成比如(0,0,0,0),可以认为是个十百千四位。越靠左优先级越高。 每存在一个选择器,总的CSS特指度按选择器权重增大。各选择器权重如下: 行内声明(元素的style属性):(1,0,0,0); Id选择器: (0,1,0,0); 类、伪类:(0,0,1,0); 元素、伪元素:(0,0,0,1); 通用选择器(*)的特指度为0:(0,0,0,0); 连接符(如:>、+等)和继承的属性没有特指度。(区别于0,它们没有特指度,相当于比0还要小) 如果两个选择器选择了同一个元素,特指度更大的将胜出(被应用)。 末尾带有!important的声明单独进行比较,同样遵循特指度优先规则。 带有!important和不带的CSS规则进行比较,始终是带!important的规则胜出。 2.1.2 CSS属性继承 CSS属性一般只会父传后代,不会向上传播。(特例是body元素的背景属性,可以向上传播给html根元素。) 不继承的CSS属性: 边框属性; 盒模型相关属性; 继承的属性,没有特指度,连0也没有。(弱于通配选择器*) 2.1.3 CSS的层叠关系 CSS的层叠关系,也就是谁的优先级更高,相互之间的覆盖关系。 CSS的层叠关系有以下几个影响因素: 显式权重:有无!important标记;(带有!important的比没有的优先级更高) 来源: 内联样式 > 内部样式 > 外部样式 > 浏览器用户自定义样式 > 浏览器默认样式; 特指度: 同上节; 声明顺序: CSS语句的声明前后顺序。(后声明的覆盖前面声明的) 因为有层叠关系,超链接的CSS伪类推荐声明顺序: LVFHA (link -> visited -> focus -> hover -> active) 2.2 CSS函数 2.2.1 calc()函数 calc()用于进行简单的数学计算。它的使用有如下几点限制: +和-号两边必须是同一单位类型,或者都是数字; *号两边必须有一个是数字,不能都是带单位的值; /号右边必须是数字,左边可以是数字也可以是带单位的值; 任何情况下都不能除以0; +和-号两边必须有空格,因为要与正负数区分开。 2.2.2 attr()函数 attr()函数,用于获取CSS选择的元素上的某一个属性值。(比如id属性) attr()目前仅支持在伪元素的content属性里使用,用于获取元素的某一属性作为字符串显示在伪元素中。 2.3 CSS自定义变量 使用--开头可以自定义一个CSS变量,它的作用和特性如下: 在后面可以使用var()函数引用这个变量,作用仅是把变量值替换到当前位置; 变量名称一般根据作用定义,尽量使用带有短横线连接的变量名; 变量名大小写敏感; 自定义变量具有作用域。 2.4 CSS的@规则 2.4.1 CSS特性查询 使用@supports(<css>){<css>}语法可以对不同的CSS支持情况,编写不同的CSS。 /* 仅支持color:black;的情况,才添加如下CSS */ @supports (color:black) { body { color: black; } } 2.4.2 @font-face 用于使用自定义字体。可以指定一个服务器上的字体,然后用户在读取到这个声明的时候会下载这个字体文件,用于渲染页面。 @font-face { font-family: 'Good'; src: url("good.otf") format('opentype'); } 对于浏览器,无论是否使用声明的字体,都会进行下载。 @font-family中两个属性是必须的: font-family: 给要引入的字体起个名; src: 字体的位置。 src中指定的字体文件位置,必须是同源的。 也可以设置多个用逗号分隔的列表,用于提供备用字体文件下载。 使用@font-face,在字体没加载完成前,会使用默认字体显示,然后在加载完成后瞬间替换成自定义字体。如果二者字形大小相差过大,会导致页面重新布局,影响性能。 2.5 视觉格式化 2.5.1 容纳块 容纳块由离元素最近的生成块级框或列表项目的祖辈元素的边界构成。 —《CSS权威指南》 容纳块是元素布局的参考基础。比如计算width等的百分比。 body元素的容纳块是html元素; html元素对应的容纳块,叫初始容纳块。它由的宽度等于视口的宽度,高度初始为0。 内部框的左右外边界间距(content-box宽度+内边距+边框+外边距),永远等于容纳块的content-box宽度; 2.5.2 块级框模型 块级元素内部格式化基本原理: 横向: 核心条件:左右外边界间距(content-box宽度+内边距+边框+外边距),永远等于容纳块的content-box宽度; 只有margin和width可以设置为auto(自动计算),其他横向属性(padding、border)不能,只能设置为0或具体值; 只有外边距可以为负值,其他都不可以; 块级元素的width设为百分数,基准于容纳块的内容框content-box的宽度; 如果两边margin都设为auto,则两外边距平分,值相等; 横向外边距永远不发生折叠; 置换元素(比如img),设置width为auto时,会自动设置为内部元素(图片)的宽度的100%; 纵向: 当设定块高度小于实际内容的高度时,行为取决于overflow属性的设置; 相邻的纵向外边距会发生折叠; 正外边距折叠时,结果取二者之间较大值。 负外边距折叠时,结果取负的绝对值较大的那个,然后用正外边距与它相加得到结果。 内边距和边框,不与任何区域发生折叠; 当设定块height为自动计算(auto,默认): 如果没有为块设置边框、内边距(或者设置了宽度为0的内边距、边框宽度),块的高度由子元素撑开,默认为:子代块级元素中最上面的上边框外侧到最下面的下边框外侧(内部元素外边距与外部父元素外边距折叠); 如果父元素设置了非0宽度边框、内边距,则块的高度为:子代块级元素中最上面的上边距外边界到最下面的下边距外边界(有东西隔开,子元素外边距与父元素不发生折叠); 当height设为百分数时,基准于容纳块显式设置的高度。如果容纳块的高度是auto,则百分数没有效果,退化为auto。 2.5.3 box-sizing 块模型修改 box-sizing属性只能用于块级元素,作用是设定块级元素的盒模型类型。(标准盒模型、怪异盒模型) 本质上是设定了width和height属性,约束的是哪一个框。 content-box(默认):width和height都约束内容框(内边距内界以里)的尺寸; border-box: width和height都约束边框框(边框外界以里)的尺寸,内边距从里面扣除,扣除后部分才是内容框; 2.5.4 行内元素模型 行内元素内部格式化基本原理: 行内元素内部可以划分为:内容区、行内框; 非置换元素(比如span)的内容区,等于字体框的合集区域; 置换元素(比如img)的内容区,等于置换元素内容本身+内边距+边框+外边距的合集区域; 行距 = line-height - font-size; 非置换行内元素的行内框高度由line-height属性决定,非置换元素的行内框高度 = line-height = 内容区高度 + 行距;(因为行距可正可负,所以行内框可以大于内容框高度,也可以小于) 置换元素的行内框高度 = 内容区高度; 行内框在行中纵向如何对齐,用vertical-align属性设定,这也决定着行框的高度; 内边距、外边距和边框,都不影响行内非置换元素的行内框高度,因此也不对当前行框高度产生影响; 行的高度(行框高度),由一行内各元素(置换、非置换)的行内框最高点和最低点之差决定; 确定一行的行框高度步骤: 非置换元素:根据line-height与font-size之差计算行距,然后除以2,分别添加在字体框的上部和下部; 置换元素:将纵向的padding、border和margin与height相加即为内容区高度; 非置换元素的基线是内部字体的基线,置换元素的基线相当于是内容区的底边; 根据各元素的vertical-align设置,按各自方法进行纵向对齐; 找到最高点和最低点,二者就是行框的上界和下界,行框高度是二者之差; 2.5.5 行内块级元素 inline-block 行内块级元素(display:inline-block;),对外部其他元素与行内框相同,对其内部内容它本身视作块级元素。 2.6 内外边距、边框和轮廓 2.6.1 内外边距 ★★★内、外边距如果使用百分数定义,它相对的是父元素内容区的宽度; 内外边距不影响行内非置换元素的行内框高度,也不影响这个元素所在行的行高; 2.6.2 边框相关 默认情况下,背景延伸到边框的外边界;(通过background-clip可以设置) 边框颜色默认为当前元素文字的颜色; 边框的样式默认是none,也就是没有样式; 边框样式为none时,边框完全不存在,边框的宽度也会自动置为0(而不是显示一个透明的带宽度的边框); 边框样式 hidden 等价于 none; 边框宽度只能是thin / thick / medium / <length>,不能是百分数。 2.6.2.1 图像边框 待补充。 2.6.3 轮廓 轮廓绘制在边框的外侧,紧贴边框; 轮廓不占空间,对布局没有任何影响,只是视觉上的效果; 轮廓不能单独设置一侧,只能四周同时设置; 轮廓可以不是矩形,在行内元素换行之后,可能形成只沿着外围的独特形状; 2.7 背景 2.7.1 几个分开的属性和合并属性 分开的属性: background-color: 设置最底层背景的背景色; background-image: 设置背景图片url,或各种渐变; background-position: 取值:长度、百分数或关键词(left,right,top,bottom,center); 计算基准是:background-origin属性所设定边框的左上角; 可以定义一对值,先横坐标后纵坐标; 百分数表示:将背景图的当前坐标百分数位置线,与元素的对应坐标百分数位置对齐; 可以混用百分数和长度值。 background-size: 关键字:cover、contain; 长度值:一对长度值定义背景大小; 百分数:根据background-origin定义的元素区域计算; auto:根据背景宽高比自动计算,如果没有宽高比,则使用元素本身的尺寸值。 background-repeat: 定义背景是否可以重复; background-attachment: scroll(默认): 元素内容滚动时,背景图始终在元素可见区域的固定位置; local: 元素内容滚动时,背景图相对元素内容固定,因此背景图随着元素可见区域滚动而滚动; fixed: 元素相对于当前视口固定,设置这个值,background-position的定位基准也会被设置为视口; background-clip: 定义背景的绘制区域,只负责裁剪背景的显示区域;(默认是border-box,可以设为content-box、padding-box) background-origin: 定义背景的定位区域,也就是background-position参考的基准(默认是padding-box,可以设为content-box、border-box); background-origin的默认值是padding-box,而background-clip的默认值是border-box! 合并属性:background 同时设定background-position和background-size,必须用/号隔开,顺序是<position>/<size>; 同时设定background-origin和background-clip,顺序是先origin后clip。 其他属性顺序随意。 2.8 浮动 2.8.1 浮动元素的特点和排布规则 浮动元素脱离原来的文档流,原来占据的位置也消失; 浮动元素的外边距不折叠; ★★★任何元素浮动后,都会变成块级元素; 浮动元素的容纳块,与普通块级元素一样,是距离最近的块级祖先元素; 行内框与浮动元素重叠,行内框的一切都在浮动元素之上渲染; 块级框与浮动元素重叠,块级框的背景和边框在浮动元素之下渲染,内容在浮动元素之上渲染。 △浮动元素如果不是置换元素,应该显式地设置宽度,否则浮动后元素宽度可能趋近于0; 浮动元素的几个排布规则: 浮动元素左、右边界,都不会超过容纳块的左右内边界(内边距内界); 浮动元素之间不会相互遮盖、重叠; 浮动元素的顶边,不会超过元素浮动之前所在的行框的顶边; 浮动元素沿着浮动方向,尽量移动到更远的位置。 从左到右排布的行内元素,如果全部添加右浮动,顺序会反转。 2.8.2 清除浮动 clear属性可以设置为: left/right/both/none(默认)。 用来保证在当前元素的左(右)侧不出现浮动的元素,如果出现就把元素向下移动直到离开浮动元素的位置。 注意 清除浮动的本质,是在当前元素外边距之外添加间隙(clearance)。如果需要向下移动50px才能离开左(右)侧的浮动元素,而元素上外边距为0,那么元素上方就会添加50px的间隙来实现清除浮动的效果。 当元素本身存在上外边距,比如20px,那么浏览器只会添加30px的间隙,就足以使元素清除浮动。这样在完成清除浮动之后,元素设置的20px上外边距看起来好像消失了,元素的上边框外界紧贴着浮动元素。 2.8.3 △定义浮动形状 shape-outside:使用图像或者一个基本形状来定义元素的浮动形状,也就是外部文字围绕的形状,默认是浮动元素外边距外界的矩形; shape-image-threshold:浮动形状为带透明度的图像时,定义文字可穿过的透明度阈值; shape-margin: 为浮动形状添加外边距; 2.9 定位 2.9.1 各种定位形式的容纳块确定 容纳块是确定定位位置的基准元素。定位相对于容纳块的左上角点。 static、relative: 最接近的祖先块级(包括inline-block)元素的内容区(content-box); absolute: 容纳块是最接近的position属性不为static的祖先元素(块级或行内),如果没有这样的元素,那么容纳块是初始容纳块,也就是html元素; ★★★当找到的祖辈元素是块级元素,容纳块是它的内边距外界组成的框(padding-box); 当找到的祖辈元素是行内元素,容纳块是它的内容边界(顶边底边由行框上下界组成,左边为第一个元素左边,右边为最后一个元素的右边界); fixed: 容纳块为视口; sticky:定位基准是最近的可滚动父元素,top、bottom、left和right定义的是相对定位基准四个边的偏移量。 2.9.2 偏移属性:top、bottom、left和right left、right设置百分数相对于容纳块的宽度,top和bottom设置百分数相对于容纳块的高度; 对于绝对定位,如果四个偏移值都设定了值,无需设定width和height(auto),元素的宽高会自动按照约束的区域计算; 定位元素横向格式化: left + margin-left + border-left + padding-left + width + padding-right + border-right + margin-right + right = 容纳块宽度; 当横向过约束时,right的值会被忽略,替换为auto; 当纵向过约束时,bottom的值会被忽略,替换为autox; 2.9.3 定位是否保留原文档占位 absolute、fixed定位,元素从源文档中脱离,源文档中不再占有位置; 其他定位方式,元素虽然可能不显示在原位置,但是在源文档中仍占位。 2.9.4 z轴位置:z-index z-index属性只能用于position不为static的元素,或者是flex、grid布局中的元素。 z-index可正可负; 声明了z-index的元素,会创建自己的堆叠上下文,它的后代元素会在这个堆叠上下文中进行比较排列; 在同一个堆叠上下文中,z-index值越大,显示越靠前; 子元素永远显示在父元素前面; z-index默认为auto,相当于z-index=0。 2.9.5 粘滞定位 粘滞定位的定位基准最近的可滚动(有滚动条)的父元素; 粘滞定位的top、bottom、left和right设置的不再是元素的位置,而是容纳块四个边的位移(正数向里,负数向外); 设置了粘滞定位的元素,在滚动过程中,遇到四个位置属性不为auto的边,则粘附在这个边上; 2.10 弹性盒布局 Flex布局相对比较熟悉,记录一些特殊知识点: Flex布局的目的是实现一维布局,最适合沿一个方向布局内容; 使用writting-mode、direction或text-orientation改变文字书写模式,Flex布局的主轴会随之自动改变; 弹性容器的直接子代自动变成弹性元素,不论是子元素还是文本节点; 弹性容器的外边距与它内部弹性元素的外边距不折叠; 弹性元素的特点: 弹性元素彼此之间,外边距不折叠; float和clear属性对弹性元素无效; vertical-align对弹性元素没有影响,只用于设定弹性元素中文本的对齐方式; 弹性元素中的绝对定位元素: 绝对定位弹性元素依然会被从文档流移除; 容纳块是弹性容器; 绝对定位弹性元素不参与弹性布局,因为他们已经不在文档流中; 仍然受弹性容器上CSS属性的影响; flex是flex-grow、flex-shrink、flex-basis属性的简写属性(设置顺序必须是grow/shrink/basis); 弹性元素的属性强烈建议一直用flex缩写属性定义,不要分开来写,flex只对弹性元素有效; min-width这类尺寸限制属性对弹性元素有影响,弹性元素尺寸需要优先满足它们约定的条件; 弹性元素的尺寸由flex属性中的弹性基准flex-basis决定 ★ 如果弹性元素上既没设置flex,也没设置flex-basis,那么它的弹性基准值默认为auto; ★ 如果弹性元素上设置了flex但没显式设置第三个值flex-basis,弹性基准这时默认为0; 如果flex中设定弹性基准flex-basis为一个特定的长度或百分比(基准为弹性容器主轴长度),那么width(纵向则是height)会被忽略,以弹性基准为主; 如果flex中设定弹性基准为auto,则以设定的非auto的width值为尺寸; 如果基准和width都为auto,则基准值等同于content关键字,意味着是容纳弹性元素内容的最小尺寸。 flex-grow: 弹性容器中多余的空间,按各元素的flex-grow比例分配给各个弹性元素;(默认为0,必须是正数) 如果flex中设定的(显式或默认)弹性基准为0,而且各元素的flex-grow也为0的话,各元素的宽度是自身可正好容纳内容的最小宽度(而不是0宽度); flex-shrink: 超过弹性容器长度,按照设定的缩小因子缩放各弹性元素,具体公式如下:(默认为1,必须是正数) flex属性有几个关键字可以设置: initial: 默认值,元素只能缩小不能放大。相当于flex: 0 1 auto; auto: 元素既可以缩小也可以放大,相当于flex: 1 1 auto; none: 完全没有弹性的弹性元素。相当于flex: 0 0 auto; <number>: 只提供一个单独的数字,默认为flex-grow值。此时相当于flex: 0 0; 这种情况相当于为每个弹性元素,设置了它在弹性容器中占容器长度的比例。因为元素弹性基准都为0,相当于总尺寸也为0,容器全部是空余长度,按设置的flex-grow分配给各弹性元素。 order属性:设置弹性元素在弹性容器轴上显示的顺序: 小的在前,大的在后,可以为负值; 同样order值的元素,按在源代码中出现的先后顺序排列; order纯粹是视觉上的排列效果变化,Tab键的索引和屏幕阅读器依然按照源代码顺序检索。 2.11 栅格布局 栅格布局用于定义二维平面网格,并方便把元素边界与网格线对齐。 栅格布局由声明display:grid开启,这样声明的元素是一个栅格容器; 栅格容器为它的内容定义一个栅格格式化上下文,栅格容器的子元素是栅格元素; 栅格容器外边距与它内部栅格元素的外边距不折叠; 栅格元素上float和clear属性无效; vertical-align对栅格元素没有影响,只用于设定弹性元素中文本的对齐方式; 栅格容器和其内部定义的栅格线外界不一定重合,栅格容器只是为内部栅格定义提供一个尺寸基准,以及为文档流中其他外部元素提供栅格容器的尺寸; 栅格元素按照栅格线划分的栅格单元排布,所以可能会超出栅格容器显示,超出部分不会对文档中的其他元素排布产生影响,只是覆盖; 栅格元素全部包含在栅格区域中,栅格区域包含的边界是元素的外边距外界; grid-template-rows、grid-template-columns定义栅格轨道宽度,也用于放置栅格线: 轨道宽度之间用空格分开,轨道宽度两侧可以定义栅格线名称,用方括号括起来; [col1] 1em [col2] 1em [col3] 轨道宽度可以有几种表示方式: 百分数: 按行和列区分,分别相对于栅格容器的height和width; 长度: 固定的轨道宽度; 最小最大值范围: minmax(min, max), 如果最大值比最小值小,则设定最小值的固定宽度轨道。最小值部分不能使用fr单位,会导致整个声明失效; 分配余下空间: fr为单位,1fr为平均分配1份; 关键字: min-content: 根据内容设置轨道宽度,尽量缩短每个内容的宽度,然后轨道宽度设为整个轨道中最窄的内容宽度; max-content: 尽量放宽每个内容的宽度,然后轨道宽度设为整个轨道中最宽的内容宽度。 fit-content(arg): 适应内容宽度。 当 min < arg < max: 宽度为arg; 当 arg < min < max: 宽度为min; 当 mim < max < arg: 宽度为max。 重复定义: repeat(times / autofill / autofit, pattern); 重复函数中不能再嵌套重复函数; 重复函数中的patten如果有栅格线,首尾连接的栅格线将合二为一,这个栅格线具有两个名称; 自动填充autofill:repeat(autofill, 1em)代表使用1em宽度,自动填充剩余的空间直到无法填充为止,不论是否有栅格空间中是否有元素; 自动填充autofit:repeat(autofit, 1em)代表使用1em宽度自动填充,如果没有元素填满栅格空间,就去掉这个自动填充的空间。 不同单位可以混用。 grid-template-areas: 定义栅格区域 按照栅格顺序,每一行用一个字符串表示; 每一列之间用空格隔开,用一个字母序列代表这个栅格单元所属的区域; 空单元用一个或多个 . 表示; 相同的字符串指向的区域,合并成为一个栅格区域,栅格区域必须是矩形; 例如:grid-areas: “a b b c” “. b b .”; 命名的栅格区域,它的行、列方向起止栅格线都隐式添加了名称,分别为name-start和name-end,行和列栅格线命名空间不冲突。 grid-row、grid-column:用于栅格元素。为栅格元素指定栅格区域(绑定元素边界到某一条栅格线) grid-row-start、grid-row-end、grid-column-start、grid-column-end用来指定四个边界的栅格线; 绑定栅格线,可以使用栅格线序号(从1开始),也可以使用栅格线名称,或者是栅格区域名称; 使用栅格线序号,如果起始序号大于终止序号(在终止线之后),则系统自动默认对调二者; 使用span+number,可以指定栅格元素跨越的栅格单元数目。如果span后面没有数字,则默认为1; span+number形式定义跨越单元数目,可以用在起始线,也可以用在终止线,方向都是向确定的边界的相反方向; span+number中number可以是负值,表示相反方向跨越计数; 如果使用栅格线名称,同时有多条线用了同一个名称,则需要在名称后面加上空格隔开的序号n,表示是第几个同名栅格线; span number <col-name> 也是有效的语法,表示跨越number个col-name栅格线; 如果是栅格区域名称,则根据起始还是终止线,自动识别区域的-start和-end线; grid-row、grid-column是简写属性,start和end栅格线定义用/号隔开,前面是起始线定义,后面是终止线定义; 如果设置的栅格线超出定义范围,则生成隐式栅格: 如果设置的栅格线名称不存在,则在该方向末尾添加一个这个名称的栅格线,然后把边界绑定到它; 如果设置的栅格线序号超出定义范围,则根据序号大小自动生成若干栅格线,直到序号被覆盖; 隐式增加栅格线的轨道宽度,受grid-auto-rows或grid-auto-columns影响; 隐式栅格是一种回落机制,一般最好不要使用。 grid-area:直接指定栅格元素的栅格区域 直接使用栅格区域名称,则元素四个边界自动绑定到这个区域的对应边界; 也可以用row-start/col-start/row-end/col-end这种形式显式定义四个边界的具体栅格线,方向比较奇怪,是从上边界开始的顺时针方向; 栅格流: 如果不指定栅格元素的位置(绑定边界),栅格元素按照默认的栅格流顺序,依次放入栅格单元中; 如果放不下,则自动创建轨道,用于放置多出来的栅格元素; grid-auto-flow设置默认的放置顺序: row(默认): 按行放置; column:按列放置; 可以附加dense关键字,表示紧凑布局,也就是无视元素原来的先后顺序,尽量铺满整个栅格。 grid-auto-rows、grid-auto-columns: 自动添加的轨道宽度 设置自动创建的轨道宽度,可以是固定值,或者fr值,或者minmax(); grid:定义栅格模板的缩写形式 基本形式是: <row> / <column> (具体语法复杂,查书); grid属性会把其他所有未设置的值,重置为auto,因此需要把grid属性写在所有其他属性的最前面; gap: 设置栏距 栏距基本上可以看做把栅格线加宽,让栅格单元之间留有距离; 栏距不占栅格单元的空间(除非是以fr定义的宽度,栏距计入在栅格总宽度里,剩余的宽度才会分配给fr定义的区域); 对齐方式: justify-items、align-items: 元素在栅格区域内的行、列方向对齐方式; justify-content、align-content: 整个栅格在栅格容器中的行、列方向对齐方式; justify-self、align-self:用于栅格元素。表示栅格元素单独在栅格区域内的对齐方式。 分层、排序: 默认按文档源码中的先后顺序分层:后者在上,前者在下; 使用z-index也可以显式修改分层顺序; 使用order可以设置栅格元素的排列顺序(尽量不用)。 2.12 BFC块级格式化上下文 BFC是什么 BFC(块级格式化上下文),有如下特性: BFC 会创建一个独立的空间,内部元素永远不会超出它的范围,即使是float元素(因此浮动元素也会撑开触发了BFC的盒子); 不同BFC之间,不会发生外边距折叠; BFC区域不会与任何float元素重叠(相当于左右都清除浮动); BFC的作用是? 防止浮动元素跑出父容器; 防止外边距折叠; 创建左右浮动,中间自适应的双飞翼布局(使用flex更佳); 如何触发BFC 以下元素会创建 BFC: 根元素(<html>) 浮动元素(元素的 float 不是 none) 绝对定位元素(元素的 position 为 absolute 或 fixed) overflow 计算值(Computed)不为 visible 的块元素 ★(没有副作用)display 值为 flow-root 的元素 弹性元素(display 为 flex 或 inline-flex 元素的直接子元素) 网格元素(display 为 grid 或 inline-grid 元素的直接子元素) 行内块元素(元素的 display 为 inline-block) 表格单元格(元素的 display 为 table-cell,HTML表格单元格默认为该值) 表格标题(元素的 display 为 table-caption,HTML表格标题默认为该值) 匿名表格单元格元素(元素的 display 为 table、table-row、 table-row-group、table-header-group、table-footer-group(分别是HTML table、row、tbody、thead、tfoot 的默认属性)或 inline-table) contain 值为 layout、content 或 paint 的元素 多列容器(元素的 column-count 或 column-width (en-US) 不为 auto,包括 column-count 为 1) 2.13 滤镜、裁剪、混合、遮罩 clip-path: 裁剪 形状函数: inset(): 上右下左四个向内收缩值; circle(): 只接受一个圆的半径(长度、百分比),然后可以加上一个at,后面接圆心的位置(可以是关键字,如center); ellipse(): 接受一对值,定义纵轴和横轴半径。同样用at接圆心位置; polygon(): 多边形,用多个逗号分隔的点(一对由空格隔开的长度或百分数值)定义。 mask 遮罩 mask-image: 用于蒙版的图片(与background-image类似); mask-mode: 定义蒙版的实现模式。 alpha: 通过透明度通道实现(全透明为不可见); luminance: 通过亮度实现(全黑为不可见); mask-size、mask-position、mask-repeat、mask-origin、mask-clip:都与对应的background属性类似; mask-composite:定义多个蒙板的合并方式(相交、减除等)。 object 置换元素相关 object-fit:置换元素在框内的填充方式: fill(默认): 元素被拉伸到容器尺寸,不保留宽高比; cover: 保留宽高比,拉伸元素直到覆盖整个容器(多余部分被切除); contain: 保留宽高比,拉伸元素直到正好被容器容纳; none: 不拉伸元素,保留元素初始尺寸; object-position:置换元素在框内的位置。与background-position类似,只不过是设置置换元素的位置。 响应式布局 响应式布局的常用解决方案对比(媒体查询、百分比、rem和vw/vh) 逻辑像素、物理像素、像素密度 物理像素: 设备屏幕上的一个物理像素点; 逻辑像素: CSS中设置的像素单位(1px),一个逻辑像素可以对应多个物理像素(多倍屏); 像素密度: 一个逻辑像素对应多少个物理像素。例如:CSS中设置1px在iphone等设备上可能显示为3个像素点,那么这台iphone此时的像素密度为pixel ratio = 3; BEM规范 BEM规范 BEM规范解决的问题 通过CSS选择器名称,就可以知道它定位的元素,以及与父元素的嵌套层级关系; BEM规范内容 BEM 是块(block)、元素(element)、修饰符(modifier)的简写,由 Yandex 团队提出的一种前端 CSS 命名方法论。 块是一个功能区域的根元素,元素是其内部实现的子元素,而修饰符代表元素的某一状态。 它定义CSS选择器的命名如下: Block__Element--Modifier 其中: 块与其内部元素之间用两个下划线(__)隔开; 元素与其修饰符之间用两个短横线(--)隔开; 如果块、元素、状态需要由多个单词组成,单词之间用-隔开; 例如: container__button--hover; video-box__show-btn--active; BEM规范的实操 只在需要处理父子嵌套关系的时候,使用BEM规范命名,单一状态等不需要; 手写麻烦,一般使用SCSS等预处理器的嵌套功能编写; 层级最好不要超过4级; 也可使用预处理器的mixin等功能,实现BEM的自动命名; 一些常用CSS技巧 单行文字溢出显示三点省略号 overflow: hidden; // 溢出隐藏 text-overflow: ellipsis; // 溢出用省略号显示 white-space: nowrap; // 规定段落中的文本不进行换行 多行文字溢出显示三点省略号 overflow: hidden; // 溢出隐藏 text-overflow: ellipsis; // 溢出用省略号显示 display: -webkit-box; // 作为弹性伸缩盒子模型显示。 -webkit-box-orient: vertical; // 设置伸缩盒子的子元素排列方式:从上到下垂直排列 -webkit-line-clamp: 3; // 显示的行数

使用数组构建堆结构的过程和堆排序原理

2021.06.17

Data Structure

使用数组构建堆结构的过程和原理: heapify()函数的含义和代码,构建起始位置和过程等。 1. 堆结构的特点 一个完全二叉树,如果每一个父节点值都大于等于(或小于等于)它子节点的值,则这个二叉树称为一个堆。 大顶堆:任一父节点值大于子节点值(最大值在根节点) 小顶堆:任一父节点值小于子节点值(最小值在根节点) 2. 数组构建堆结构过程 对于一个任一数组arr,可以使用下面的方法将其转化为一个堆。 从任一叶子节点向前遍历,对每一个节点使用heapify(arr,index)方法,直到index === 0(根节点)为止。 2.1 heapify()函数的作用 heapify(arr,index)函数的作用是: 将arr中index位置的元素,放在它子树中正确的位置。 因此,从叶子节点开始,向上遍历执行heapify()直到根节点,可以保证整棵树的元素都按堆的原则,放在了正确的位置上。(堆构建完成) // heapify函数的实现:i从0开始 // rightEdge是堆排序的右边界,为了之后的堆排序使用。从原数组右侧去掉若干元素,剩下的数组部分仍然是一个堆。 function heapify(arr, i, rightEdge){ let left = 2 * i + 1, right = 2 * i + 2 let largest = i if (left <= rightEdge && arr[left] > arr[largest]) largest = left if (right <= rightEdge && arr[right] > arr[largest]) largest = right if (largest !== i){ [arr[largest],arr[i]] = [arr[i],arr[largest]] heapify(arr, largest, rightEdge) } } 2.2 数组构建堆的起始位置,可以不是最后一个节点 可以使用Math.floor(arr.length/2)或Math.floor(arr.length/2)-1来作为第一个节点。 证明如下图: Math.floor(arr.length/2)-1返回的总是最后一个叶子节点的父节点。 3. 堆排序 完成了堆的建立,堆顶的元素就是堆中最大(或最小)元素。堆排序过程如下: 交换堆顶元素(数组元素arr[0])和堆最后一个元素(数组元素arr[arr.length-1]); 当前最后一个元素排序完成,堆的尺寸-1(也就是数组右边界-1)。然后对此时刚交换完成的堆顶新元素,进行堆化处理,将其放置在当前堆中正确的位置,保证当前堆的结构; 重复执行1步骤,直到堆尺寸为1(只剩顶部一个元素),排序完成。 注意: 对于大顶堆,排序后的结果为升序; 对于小顶堆,排序后的结果为降序。 堆的使用技巧 动态选取集合元素的最小值:建立小顶堆,每次取堆顶元素; 动态选取集合元素的最大值:建立大顶堆,每次取堆顶元素; 动态选取集合元素中的n个最大值:建立小顶堆,当堆中的元素个数=== n时,再加入的元素与堆顶元素(当前最小元素)进行比较,如果大于其值,则将堆顶元素弹出,将当前元素加入堆,同时更新选取的结果; 动态选取集合元素中的n个最小值:建立大顶堆,当堆中的元素个数=== n时,再加入的元素与堆顶元素(当前最大元素)进行比较,如果小于其值,则将堆顶元素弹出,将当前元素加入堆,同时更新选取的结果; 动态选取集合元素中第k大的值: 建立对顶堆。上面为小顶堆,下面为大顶堆; 上面小顶堆的最大体积为k; 加入元素时,如果上面小顶堆的体积< k,则直接加入上面的小顶堆; 如果体积=== k,则新加入的元素e与小顶堆堆顶元素进行比较: 如果新元素更大,则弹出小顶堆堆顶元素,将其加入下方大顶堆,同时将当前元素e加入上方小顶堆; 否则, 直接将当前元素e加入下方大顶堆。 当小顶堆体积=== k时,当前第k大的元素就是小顶堆的堆顶元素。

window.requestAnimationFrame定时器与浏览器重绘

2021.06.01

JavaScript

window.requestAnimationFrame API的功能:精确控制函数在重绘前时间节点执行。 1. 有关浏览器重绘机制的几个事实 大多数浏览器会限制重绘频率,一般不大于屏幕刷新率60Hz; 浏览器发生重绘的时机不确定,但是两次重绘时间间隔一定不小于(1/60)s,也就是大约16ms; requestAnimationFrame API传入一个函数,控制这个函数精确在紧邻浏览器下次重绘前被调用。 2. requestAnimationFrame的功能和机制 requestAnimationFrame 是浏览器提供的一个按帧对网页进行重绘的 API,是为了创建动画而设计的; 传统的setTimeout和setInterval定时器,确定的只是回调函数加入到任务队列中的时间,而无法确定函数实际执行的时间(前面可能还有别的任务未执行完); requestAnimationFrame采用系统时间来确定间隔,可以保证传入的函数fn,精确地在下一次屏幕刷新前时间点执行; requestAnimationFrame在页面非可见状态下,不会执行传入的函数,而是将他们保存在一个执行队列中,然后在页面恢复可见后立即依次执行; requestAnimationFrame(fn1)中,如果fn1内部再次调用requestAnimationFrame(fn2),则传入的函数fn2会被放入下下次重绘前执行。 3. 取消requestAnimationFrame 和setTimeout一样,通过返回的id进行取消。 let a = requestAnimationFrame() // 手动取消 cancelAnimationFrame(a) 取消后,requestAnimationFrame(fn)传入函数fn被从重绘前执行的回调队列中清除。 4. 典型应用:过渡的计数器 效果: // Marswiz @2021 let cur = 0; let counting = false; function count(target = 600, step = 10, anchor = '#counter') { counting = true; let container = document.querySelector(anchor); let i = cur; const show = function() { // 调用 requestAnimationFrame requestAnimationFrame(() => { container.innerText = i; i += step; cur += step; // 在 requestAnimationFrame 内部实现递归调用 if (i <= target) show(); else { // 递归出口 container.innerText = target; cur = target; counting = false; } }); } show(); } document.querySelector('#btn-100').addEventListener('click', () => { if (!counting) count(cur + 100, Math.floor(100/33)); }); document.querySelector('#btn-500').addEventListener('click', () => { if (!counting) count(cur + 500, Math.floor(500/33)); }); 参考资料 requestAnimationFrame 执行机制探索

JavaScript一些内置API

2021.06.01

JavaScript

JavaScript一些内置API: 跨文档通信API、FIle API、媒体元素API、拖放API、Page Visibility API、Performance API、Web组件 API 、Observer API 1. 跨文档通信API 主要是全局对象的postMessage()方法。 用于窗口间通信,或工作线程之间的通信。 1.1 postMessage()方法的使用 postMessage()方法接受三个参数:①消息本体; ②一个表示消息接受目标页面【源】的字符串; ③(与web worker相关)可传输对象数组; 其中第二个参数非常重要,发动消息的目标页面只有在与这个参数同源的情况下才能正常收到message。这是一种保护策略。 1.2 目标页面的响应事件 目标页面在成功接收postMessage发来的消息后,在自身window对象上会触发message事件(异步,因为传输可能会有时延)。 message的事件对象event具有下列信息: data: 传输的消息本体; origin:发送消息的文档的源; source:发送消息的文档的window的代理对象,主要用来向消息来源页面回复消息source.postMessage()。 1.3 跨文档通信的注意事项 postMessage的第二个参数可以保证接收页面的源,接收页面在收到消息后也应该对消息的来源event.origin进行检查,确保来自可信的地方。 2. File API File API用于访问并处理用户本地的文件。 2.1 获取本地文件的方式 获取文件的方式主要有两种: 文件类型input元素: 文件拖放 2.1.1 文件类型input元素 <input type="file"> 文件类型的input元素,本身具有files属性,里面包含了用户选择的文件集合(FileList类实例)。 其中的每个file可以通过索引获取,每个file对象都包含如下基本信息: name: 本地系统中的文件名; size: 文件体积(字节); type: MIME类型(字符串); lastModifiedDate:文件最后修改的时间(字符串)。 获取文件的详细内容,必须使用下面的文件读取器FileReader。 2.1.2 文件拖放 拖放本地文件到页面,在drop的事件对象event.dataTransfer.files中获取文件列表。 2.2 FileReader 文件读取器 FileReader用于异步读取本地文件内容,可以选择多种读取类型。 FileReader为全局构造函数,使用前需要先实例化。 const reader = new FileReader() FileReader实例具有下列方法: readAsText(file,encoding): 读取文件为文本; readAsDataURL(file):读取文件为内容的URL; readAsBinaryString(file):读取文件为二进制数据; readAsArrayBuffer(file):读取文件为ArrayBuffer。 FileReader实例读取文件过程中会有如下几个事件: progress: 每50ms触发一次,与XHR对象的progress事件相同,用来反馈文件读取的进度; error: 读取文件发生错误时触发; load: 读取文件完成后触发。 文件读取结果在reader.result中获取。因为文件读取是异步操作,需要在reader的load事件回调中获取reader.result。 reader.addEventListener('load',e=>{ console.log(reader.result) }) 2.3 对象URL 访问本地文件时,可以不读取文件内容到JavaScript,而是通过内存地址直接访问内存中的文件。这就是文件的对象URL(也叫Blob URL) // 为文件创建对象URL:返回指向文件内存地址的URL window.URL.createObjectURL(file) 使用这个URL,浏览器可以直接从本地相应的内存位置获取文件,并读取到页面上,不用像FileReader那样预先读取到JS中。 3. 媒体元素API 4. H5原生拖放API 元素被拖动时,依次触发: dragstart drag dragend 元素拖动到一个有效目标上时,依次触发: dragenter; dragover; dragleave / drop; 让一个元素变成可放置区域的方法: 通过e.preventDefault()阻止它的dragenter和dragover默认事件。(之后光标由阻止变为可放置) 5. Page Visibility API 提供页面是否被用户可见的信息。(比如页面被最小化等) document.visibilityState: visible: 页面可见(标签被打开,或者通过预览形式); hidden: 页面用户不可见。 visibilityChange事件: 当页面可见性变化时触发; 6. Performance API 接口暴露在window.performance对象上。 performance.now(): 返回一个更精确的时间戳(微秒精度)。每次页面打开或工作者线程创建,performance.now()从0开始计时。 performance.getEntries() 返回performance性能时间线,内含度量性能的多个对象。 7. Web组件 API 见web组件blog。 8. Observer API Observer API系列,一共有 4 个: Intersection Observer API:观察可见性 Resize Observer API:观察大小变化 Mutation Observer API:提供了监视 DOM 树的变化的能力 Performance Observer API:用于观察性能

JS的继承方式

2021.05.22

JavaScript

几种JS继承方式和优缺点 1、2、3是有构造函数参与的继承,4、5、6是无构造函数参与的单纯对象间的继承。 1. 原型链继承 SubType.prototype = new SuperType(); 核心: 子类构造函数的prototype属性,设置为父类的实例。 缺点: ① 所有子类实例的原型,都引用着同一个父类实例A。一旦这个父类实例A上有引用类型属性值obj,一个子类实例修改了这个A.obj,全部子类实例都受到影响。 ② 子类实例在创建时,无法给父类构造函数传参。 2. 盗用构造函数 function SubType(){ SuperType.call(this) } 核心: 子类构造函数中,用自身的this调用父类构造函数。 缺点: ① 没有实现函数方法的重用,相当于每个子类实例都拥有自己独立的属性和方法; ② 父类原型上的属性和方法,没有被继承。 3. 组合继承 // 借用构造函数,继承父类实例属性 function SubType(){ SuperType.call(this) } // 原型链继承,继承父类原型中的方法 SubType.prototype = new SuperType(); 核心: 通过盗用构造函数继承父类实例属性与方法,通过原型链继承父类原型中的方法。 缺点: 调用了两次父类构造函数,效率较差。 4. 原型式继承 // 1. Object.create(); 规范接口 Object.create(superTypeInstance) // 2. 手动实现方法 function create(prototypeObj) { function Res(){} Res.prototype = prototypeObj; return new Res(); } // 或者 function create(prototypeObj) { let res = {}; Object.setPrototypeOf(res, prototypeObj); return res; } 核心:不通过构造函数,实现两个对象间的继承。 本质上是创建了一个新的对象,它的原型是传入的superTypeInstance,然后返回了这个对象。 与原型链继承是一样的,区别只是是否有构造函数参与。 缺点: 与原型链继承一样,所有子类实例共享同一个原型,互相影响。 5. 寄生式继承 function createObj(superTypeInstance){ // 使用原型式继承对一个父类对象进行继承(创建一个新对象,原型是传入的父类实例) let res = Object.create(superTypeInstance) // 给这个新对象添油加醋,添加自己的属性 res.say = ()=>{console.log('hi!')} // 返回新创建的对象 return res } 基本还是相当于原型链继承,只不过是在新创建的对象上添加了些自己的属性或方法。 缺点: ① 子类实例共享同一个原型,互相影响; ② 子类中定义的方法,和盗用构造函数一样,没有实现重用。 6. 组合寄生式继承 // 借用构造函数,继承父类实例属性 function SubType(){ SuperType.call(this) } // 寄生式继承,继承父类原型 let prototype = Object.create(SuperType.prototype) prototype.constructor = SubType SubType.prototype = prototype 将组合式继承中的父类原型继承方式,从原型链继承(直接将父类实例赋值给子类构造函数prototype属性),更改为寄生式继承(使用父类原型生成新对象,然后添加constructor属性为subtype构造函数) 这样可以少调用一次父类的构造函数。 7. 类继承 使用extends关键字实现,背后仍然是原型链机制(组合寄生式继承)。 Class A extends Class B 的基本原理是这样的,这里可以看出为什么它本质上还是组合寄生式继承: ① A.prototype.[[prototype]] 被设置为 B.prototype; ② A.prototype.constructor 设置为 A本身; ③ A 的 constructor中强制需要调用父类构造函数super(),然后才能正常使用this定义内部属性。 Class本质上还是构造函数。 这里①②两步,对应组合寄生式继承中继承父类原型的操作。③步骤对应在子类中盗用构造函数继承父类实例属性的操作。

DNS解析流程

2021.05.21

Network

DNS的解析流程 DNS(Domain Name System)域名系统,就是把人们好记的计算机域名与对应的IP地址相互转换的系统。 应用很少直接使用DNS,一般都是间接使用。但是DNS是应用层的一个核心服务。 互联网的域名结构 任何一个连接到互联网的主机或路由器,都有一个唯一的层级结构的名字,称为域名。 域(domain)是一个命名空间中可被管理的划分。从级别上可以划分为:顶级域、二级域、三级域等等。 域名系统规定: ① 各级域名的标号都由数字和字母构成; ② 每一个标号不超过63个字符,也不区分大小写字母; ③ 标点符号只能使用连字符 - ; ④ 级别低的域名级别写在左边,级别高的写在右边; ⑤ 多个标号组成的完整域名,长度不超过255字符; 顶级域名 顶级域名有国家顶级域名(.cn等)、通用顶级域名(.com等)和基础结构域名(.arpa)三种。 二级域名 各国家顶级域名下的二级域名划分,由各国自行决定。我国划分为类别域名和行政区域名两个大类。 类别域名:.ac .com .edu .gov .mil .net .org 行政区域名: .bj .js 等 三级域名 如mail、www等,可以由各单位自己划分。 域名服务器 根域名服务器 最顶级的域名服务器。互联网一共有13个根域名服务器,也就是说根域名服务器只有13个ip地址。 但是并不是只有13个服务器主机,而是每一个服务器由分布在世界上各个位置的若干主机的一套设备构成,这些设备通用一个ip地址,彼此互为镜像。 根域名服务器由字母A-M命名。 顶级域名服务器 负责管理一个顶级域名下的所有二级域名。如.com服务器。 权限域名服务器 负责一个区的域名服务器。如a.abc.com这个区的服务器。 本地域名服务器 位于主机本地的服务器,与主机距离很近,一般也称默认服务器。 一个大学、大学的一个系都可以拥有自己的本地服务器。 当主机发出DNS请求时,首先向本地DNS服务器发送。其内部缓存了所有查询过的域名列表,如果查询不到,则作为客户代表主机向下一级DNS服务器发送查询请求。 DNS解析流程 DNS解析使用UDP协议进行。流程如下: 主机提出需要解析的域名(www.baidu.com),请求本地域名服务器解析。先查找缓存,顺序为:浏览器DNS缓存->操作系统hosts文件->本地DNS服务器缓存。如果缓存中存在这个域名和对应的ip地址,则直接返回ip给主机。 如果以上缓存中没有,则本地域名服务器代替主机与根域名服务器进行请求。(主机与本地域名服务器之间为递归查询) 递归查询:每次由下一级服务器代表上一级服务器进行查询,最终结果层层返回。 本地域名服务器向根域名服务器进行请求,根域名服务器返回下一级域名服务器(.com顶级域名服务器)的ip地址给本地域名服务器; 本地域名服务器向.com顶级域名服务器发起请求,.com顶级域名服务器返回下一级域名服务器(baidu.com二级域名服务器)ip地址给本地域名服务器; 本地域名服务器向baidu.com二级域名服务器发起请求,baidu.com二级域名服务器内保存了www.baidu.com域名的ip地址,所以返回这个ip地址给本地域名服务器; 本地域名服务器与其他域名服务器的查询,为迭代查询。 迭代查询:每次查询下一级服务器返回结果给发起查询的服务器,由发起查询的服务器自己进行发起下一次的查询。 本地域名服务器将查询到的结果返回给主机,并把域名www.baidu.com和对应的IP地址保存到本地DNS高速缓存中,以备主机下一次查询。 以上各级DNS服务器也具有自身的缓存,如果缓存中存在,则直接返回给代替客户查找的本地DNS服务器,不再进行后面的查找工作。

细碎知识点合集

2021.05.08

Drops

前端杂项记录 1. position: absolute相对谁定位 如果被position为非static的父元素包裹,则相对于最近的这类父元素。如果没有,则相对于html根元素。 2. 如何在高分辨率的屏幕画1px的Border 4. 浏览器渲染页面的流程 6. 函数柯里化如何实现 7. 实现响应式布局的方法 8. CSS的层级关系,Z-index 9. CSS background参数都有哪些 10. Js有几种继承方式? 10.1 原型链继承 子构造函数的prototype属性,值为父构造函数的实例。 function Father(){ this.name = 'father' this.sex = 'male' } function Son(){} Son.prototype = new Father() 11. 浏览器如何进行DNS解析 DNS解析流程 12. 手写深拷贝 12.1 JSON简单方法 function deepCopy(obj){ return JSON.parse(JSON.stringify(obj)) } 这种方式有如下几个缺点: BigInt类型无法解析; Symbol类型会消失(连同整个键值对在输出中都不存在); Date会变成字符串; Function、RegExp、Promise、Map、Set等类型都会变成空对象; 循环引用对象直接报错。 12.2 递归深拷贝 function deepCopy(target){ function getType(v){ return Object.prototype.toString.call(v).slice(8,-1) } let copiedObjMap = new Map() function _deepCopy(arg){ if ( typeof arg !== 'object'){ if ( getType(arg) === 'Symbol' ){ return Symbol(Symbol.keyFor(arg)) } else { return arg } } else { if ( !arg ){ return arg } if ( getType(arg) === 'Object' || getType(arg) === 'Array'){ // 这里是防止循环引用 if (copiedObjMap.get(arg)) return copiedObjMap.get(arg) let res = Array.isArray(arg) ? [] : {} // 注意一定要在这里设置set拷贝过的对象,因为如果进入到下面的递归,就会执行循环拷贝了,导致栈溢出。 copiedObjMap.set(arg, res) for (let i of Object.keys(arg)){ res[i] = _deepCopy(arg[i]) } return res } if ( getType(arg) === 'Date' || getType(arg) === 'RegExp'){ return new arg.constructor(arg) } if ( getType(arg) === 'Set'){ let res = new Set() for (let i of arg){ res.add( _deepCopy(i) ) } return res } if ( getType(arg) === 'Map' ){ let res = new Map() for (let i of arg.keys()){ res.set( _deepCopy(i), _deepCopy(arg.get(i)) ) } return res } } } return _deepCopy(target) } 13. 手写promise unknown.. 14. 手写快速排序 略 15. filter和transform的区别 16. set、map和 weakSet、weakMap的区别 Set为集合数据类型,内部只有值没有键。它可以保证内部元素不重复。 Map为升级版的对象,区别是可以使用任何类型作为键和值。 17. 没有defer和async: 浏览器加载到script标签时,停止HTML解析,立即加载并执行script脚本,然后再恢复HTML的加载渲染; defer: 浏览器加载到script标签时,立即异步加载JS脚本(解析完不立即执行),同时不停止HTML解析。等到页面所有元素解析完成后,再按顺序依次执行JS脚本。 async: 浏览器加载到script标签时,立即异步加载JS脚本,解析完就立即执行,不论HTML解析过程如何。 18. onevent和addEventListener有什么区别 DOM0 事件处理程序对一个事件只能绑定一个函数,后面的覆盖前面的。 DOM2 事件绑定addEventListener可以为同一个事件绑定多个处理函数,按绑定的先后顺序执行。

HTTP缓存策略:强缓存与协商缓存

2021.05.05

Network

HTTP

HTTP缓存: 强缓存与协商缓存 一、HTTP缓存 HTTP缓存针对HTTP响应报文,一般只对GET和HEAD方法响应报文有效。(POST响应在罕见特殊配置下也可以缓存,具体见MDN) HTTP缓存可以存在于浏览器本地,也可以存在于代理服务器。 1. 缓存相关的首部行 1.1 强缓存 优先级从高到低: Pragma -> Cache-Control -> Expires Pragma和Expires都是HTTP1.0的首部。 Pragma设置为no-cache(唯一有效值),则意味着每次请求都无法执行强缓存,只能进行协商缓存。 Expires设置的是一个GMT格式的绝对日期,意味着不超过这个时间节点,都会触发强缓存。(浏览器会把它和客户端系统时间比对,如果服务器和本地有时间差,缓存判断容易产生误差。) Expires: Thu, 01 Dec 1994 16:00:00 GMT (必须是GMT格式) Cache-Control可以设置如下参数,一次可以设置多个,中间用逗号隔开: 客户端请求中的Cache-Control: no-cache: 与Pragma一样,每次请求都无法执行强缓存,只能进行协商缓存,但是比Pragma优先级要低; no-store: 不进行缓存。强缓存和协商缓存都不会触发; max-age: 设置缓存相对过期时长(单位是秒),相对于请求的时间; max-stale: 表示客户端愿意接受一个过期的响应,最大过期时长不能超过max-stale设置的值(秒); min-fresh: 表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。 服务器响应中的Cache-Control: public:表示响应可以被任何对象缓存,包括客户端和中间代理服务器等; private: 表示响应只能被单个用户缓存,不能被中间代理服务器缓存; no-cache: 与Pragma一样,每次请求都无法执行强缓存,只能进行协商缓存,但是比Pragma优先级要低; no-store: 不进行缓存。强缓存和协商缓存都不会触发; max-age: 设置缓存相对过期时长(单位是秒),相对于请求的时间; must-revalidate:一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求; immutable: 资源不会发生变化。告诉浏览器不要发送协商缓存验证请求头(比如If-modified-since),即使用户手动刷新页面。 1.2 协商缓存 客户端请求: If-Modified-Since: 如果响应头中有Last-Modified,则协商缓存请求头中携带这个字段,值为Last-Modified的值,表示询问服务器资源在这个时间节点之前是否修改过; If-None-Match: 如果响应头中有ETag,则协商缓存请求头中携带这个字段,表示询问服务器资源的ETag是否有变化; 服务端响应: Last-Modified: 资源上次更新的时间; ETag: 资源通过摘要算法计算的Tag值,资源一旦有任何修改,这个值会变化。 2. 强缓存与协商缓存触发策略 2.1. 强缓存 浏览器执行请求时,如果发现本地有之前请求的缓存,先查看请求缓存中的首部行,判断是否命中强缓存: 如果Cache-Control(优先)存在且设置了max-age值,则计算此次请求的age值ageValue,并与max-age进行比较。如果ageValue>max-age则强缓存触发失败,反之则触发强缓存。 HTTP中缓存的使用期计算(Age Calculation) HTTP1.1协议要求,当一个响应报文是从缓存里获取的时候,HTTP/1.1协议要求在响应报文中必须添加一个Age首部行。它的值表示的是,从这个响应报文在源服务器中产生或者过期验证的那一刻起,到现在为止所经过时间的一个估计值(从名字上其实就看的出来,它表示的是缓存的年龄)。经常和max-age一起来验证缓存是否过期,即如果这个字段的值比max-age的值还大,那这份缓存就已经过期了。 这个ageValue值的计算,是缓存到达本地时带有的age值initAge,加上这次请求时间点为止,缓存在本地经过的时长agePassed。 age = initAge + agePassed 如果Cache-Control不存在,但设置了Expires,判断请求的Date是否超过Expires设置的时间,未过期则直接命中强缓存。 Expires: 是GMT格式字符串(绝对值),意味着何时过期,它来自于服务器时间。浏览器在请求进行比较的时候,使用的是系统时间,系统时间可以修改所以相对不可靠。 这时,直接从缓存中读取响应(包含响应头),不与服务器通信。(状态码200) 2.2 协商缓存 如果发现 Cache-Control 或 Expires 二者之一有过期,则发送请求到服务器: 如果缓存首部存在Etag,则发送带If-None-Match的请求;(优先级更高) 如果缓存首部存在Last-Modified,则发送带If-Modified-Since的请求。 ETag 与 Last-Modified 的区别? ETag是服务器根据资源内容,自动生成的唯一的ID。它更能体现资源是否已修改。 Last-Modified主要是有以下三点问题: ① 最短修改时间只能精确到秒; ② 有些文件在服务器周期性保存,内容并未修改,这时造成本地缓存的浪费; ③ 某些服务器系统不能得到精确的修改时间。 由服务器根据这两个字段判断,缓存是否还可以使用。 如果可以,则意味着协商缓存命中,服务器返回新的响应header信息,但是不带有响应主体。(意味着服务器仍可从缓存中读取响应)(校验码304) 如果校验失败,服务器返回带响应主体的响应报文。(校验码200) 2.3 用户行为对缓存的影响 按F5会忽略强缓存,保留协商缓存。 按Ctrl+F5会忽视全部缓存。 2.4 如何保证每次资源更新浏览器都会及时更新,防止从缓存读取? 设置Cache-Control为no-store; 为每一个更新的资源,配置一个独有的资源名。常用的是在资源后面加上query ID后缀。

Object与Array原生方法和Object几种属性遍历方式的区别

2021.05.03

JavaScript

总是搞混,这里记录一下。 Object.prototype 中的方法 hasOwnProperty(key): 检测字符串属性key,是否是对象自身的内部属性(纯自身内部,不包括从原型继承的属性); key in obj 这种形式,如果key在obj的原型中,也会返回True。 isPrototypeOf(obj): 检测对象是否在obj的原型链中; propertyIsEnumerable(key): 检测key是否是可枚举的; toLocaleString: 本地化字符串; toString: 转换字符串; valueOf: 返回对象本身; Object 函数本身挂载的方法 Object.assign(target, …objs): 将多个其他对象,合并整合到目标对象target,然后返回整合后的target; Object.create(prototype, [properties]): 用已知原型对象创建新对象; Object.defineProperty(object ,property ,descriptor): 为对象object添加属性,可以配置描述符; Object.defineProperties(obj, properties): 给对象添加多个属性并分别指定它们的配置。传入两个参数,第一个为添加属性的目标对象,第二个为以属性名为键,以属性描述符为值的配置对象。 Object.entries(): 返回给定对象以自身键值组成的数组[key, value]为元素,组成的数组。 Object.freeze() 冻结对象:其他代码不能删除或更改任何属性。 Object.getOwnPropertyDescriptor() 返回对象指定的属性配置。 Object.getOwnPropertyNames() 返回一个数组,它包含了指定对象所有的可枚举或不可枚举的属性名。 Object.getOwnPropertySymbols() 返回一个数组,它包含了指定对象自身所有的符号属性。 Object.getPrototypeOf() 返回指定对象的原型对象; Object.is(value1, value2) 比较两个值是否相同。(所有 NaN 值都相等,这与==和===不同); Object.isExtensible() 判断对象是否可扩展; Object.isFrozen() 判断对象是否已经冻结; Object.isSealed() 判断对象是否已经密封; Object.keys() 返回一个包含所有给定对象自身可枚举属性名称的数组; Object.preventExtensions() 防止对象的任何扩展。(使对象无法再添加新的属性。) Object.seal() 防止其他代码删除对象的属性; Object.setPrototypeOf() 设置对象的原型(即内部 [[Prototype]] 属性); Object.values() 返回给定对象自身可枚举值的数组。 对象obj的几种属性遍历方式及其区别 1. for (let key in obj) for…in会遍历出对象内所有非Symbol的可枚举enumerable属性,包括从原型继承来的属性。 只有这种方式返回的结果中带有原型的属性。 2. Object.keys() / values() 返回所有非Symbol的可枚举属性。 3. Object.getOwnPropertyNames(obj) 返回所有非Symbol键名,无论是否可枚举enumerable。 4. Object.getOwnPropertySymbols(obj) 返回所有Symbol类型的键名,无论是否可枚举enumerable。 Array或Array.prototype中的方法 构造Array Array.from(): 将类数组对象或可迭代对象,转化为数组; Array.of(): 将传入的一组参数转化为数组; 数组检测 Array.isArray(): 检测传入的变量是否为数组类型; 迭代器方法 返回的是一个迭代器。 Array.keys(): 返回数组键名的迭代器; Array.values(): 返回数组值的迭代器; Array.entries(): 返回数组键值对组成的数组的迭代器。 复制与填充 直接修改原Array。 Array.prototype.copyWithin(): 复制自身的一部分,并插入到自身指定的位置; Array.prototype.fill(): 将数组的一部分,用传入的变量重新填充。 字符串转换 Array.prototype.toString(): 调用每个元素的toString方法,然后用逗号串联; Array.prototype.toLocaleString(): 调用每个元素的toLocaleString方法,然后用逗号串联; Array.prototype.valueOf(): 返回数组本身。 栈和队列方法 直接修改原Array。 Array.prototype.push(): 入栈——将元素推入末尾; Array.prototype.pop(): 出栈——将元素弹出末尾; Array.prototype.unshift(): 入队列——将元素加入起始位置; Array.prototype.shift(): 出队列——将元素从起始位置移除; 排序方法 直接修改原Array。 Array.prototype.sort(): 不传入比较函数,默认将数组按照字符串顺序排序。传入比较函数,按比较结果排序; Array.prototype.reverse(): 反转数组。 操作数组 Array.prototype.concat(): (不修改原Array,返回新数组。) 在原数组后,添加其他参数到末尾,然后返回新数组; Array.prototype.slice(): (不修改原Array,返回新数组。) 复制原数组的一小段,并返回; Array.prototype.splice(): (直接修改原数组) 从原数组中删除部分元素,然后在删除的位置添加部分元素; 搜索方法 Array.prototype.indexOf(): 执行严格相等的比较,返回第一个结果索引; Array.prototype.lastIndexOf(): 执行严格相等的比较,返回最后一个结果索引; Array.prototype.includes() Array.prototype.find() Array.prototype.findIndex() 迭代方法 为数组每一项运行传入的函数。(参数为item ,index ,array) 修改item参数,不会对原数组造成影响。 通过array[index]进行修改,直接对原数组造成影响。 Array.prototype.every(): 每一项都返回true,则返回true; Array.prototype.filter(): 返回返回值为true的项目组成的数组; Array.prototype.some(): 如果有一项返回true,则方法整体返回true; Array.prototype.map(): 对每一个数组项运行函数,返回结果组成的数组; Array.prototype.forEach():对每一个数组项运行函数,不返回值。 归并方法 可接受两个参数: ① (accumulator,item,index,array)=>{…} 归并函数 ② accumulator的初始值。 Array.prototype.reduce(): 从左到右归并; Array.prototype.reduceRight(): 从右到左归并。

Cookie、Session和Web Storage

2021.05.02

cookie

localStorage

sessionStorage

indexedDB

浏览器端、服务器端储存数据的几种方式:cookie,session,localStorage,sessionStorage,indexedDB 全面整理,包含以前的笔记。 一、Cookie 1. Cookie的作用 HTTP是无状态协议。Cookie的存在,是为了让服务器能识别连接过它的用户,从而在响应上针对性地采取一些友好措施。 比如一个购物网站,如果服务器无法识别客户是谁,是否曾经连接过服务器并进行某些操作。那么每次重新访问网站服务器,都需要重新登录,购物车里的东西也会被清零,因为服务器无法识别每次连接的客户。 Cookie是直接保存在浏览器中的一小段数据,在每次请求相同的域的时候(根据cookie的domain/path/samesite设置),会自动携带cookie首部,供服务器进行识别。 cookie与域直接关联。所在页面与cookie绑定的域相同,则称为第一方cookie。否则称为第三方cookie。 2. Cookie的典型应用场景 Refer: 浅谈session,cookie,sessionStorage,localStorage的区别及应用场景 判断用户是否登陆过网站,以便下次登录时能够实现自动登录(或者记住密码); 保存上次登录的时间等信息; 保存上次查看的页面; 浏览计数。 3. Cookie的工作方式 当用户浏览某一个使用了Cookie的网站,则: ① 网站的服务器就生成一个该用户的识别码(唯一); ② 网站以此识别码为索引在服务器后端建立一个数据库项目; ③ 然后在给用户回复的HTTP响应报文中添加一个首部行: Set-cookie: ④ 当客户收到响应报文,浏览器就在管理Cookie的文件中添加一行,其中包括这个服务器的主机名和它响应的识别码。 ⑤ 当客户再次向这个服务器发送请求报文,浏览器就会自动地从管理Cookie的文件中抽出这个Cookie,并添加在请求报文的首部行中。 Cookie: 4. Cookie的结构、JS设置和体积限制 在打开页面的控制台输入document.cookie,可以查看某一个网站的cookie。结构如下: 某网站的Cookie: “_ga=GA1.2.1788237449.1610895464; _ym_d=1610895465; _ym_uid=1610895465705583085; _gid=GA1.2.529167652.1619787501; _ym_visorc=w; _ym_isad=2; pixelRatio=1.75” document.cookie 的值由 name=value 对组成,以 ; 分隔。每一个都是独立的 cookie。 通过document.cookie也可以设置当前域的cookie,Cookie的修改具有独立性: document.cookie = ‘cookieProperty=cookieContent;’ 这一设置,只会在cookie中添加(或修改已存在的)一个cookieProperty的值,不会覆盖全部的cookie。(document.cookie接口是一对getter/setter,因此get和set的操作方式不同) 通过document.cookie设置的cookie,都只能作用于当前域。 Cookie的体积大小不能超过4kB,条目数一般不能超过20条,具体与浏览器实现相关。 5. Cookie中的特殊字段 5.1 path 设置浏览器发送Cookie的url路径前缀。只有这个路径下的页面可以访问到这个Cookie。默认是’/’。 一般设置 path=/; 这样这个域名下所有网页都可以访问到这个Cookie。 如果设置 path=/main 则只有main子目录的页面可以拿到Cookie 5.2 domain 设置可访问Cookie的域名。 默认情况下domain等于当前域名。也就是说,www.marswiz.com下设置的 cookie,只能在这个域访问,即使是它的父级域marswiz.com也不行。 设置了 domain = site.com; 则 *.site.com 的所有域名都能拿到这个cookie(父域和子域)。 5.3 expires, max-age expires是Cookie的到期日期,到期后Cookie被浏览器自动删除。 expires 需采用 GMT 时区格式,可以用 date.toUTCString 来获取它。 max-age是Cookie的存活时长,为一个数字,单位是秒。 expires和max-age二者设置其一就可以。 5.4 secure 规定这个Cookie只能被Https协议传输。 默认情况下,Cookie不区分协议,只区分域。 5.5 samesite samesite关键字有三种取值: strict: 只有同站 lax: none: cookie如何判断是同站? cookie判断同站,是根据eTLD+1方法。 也就是:有效的顶级域名+二级域名相同,则判断同站,否则是非同站。 www.taobao.com 和 www.baidu.com 是跨站,www.a.taobao.com 和 www.b.taobao.com 是同站,a.github.io 和 b.github.io 是跨站(注意是跨站,因为github.io是注册的顶级域名)。 samesite关键字防止了一种叫做CSRF(Cross-Site Request Forgery 跨网站请求伪造)的攻击: 当用户已经在某一网站(site.com)完成登录认证,服务器返回了用户的Cookie并保存在浏览器中。默认情况下,只要从浏览器向site.com域发送请求,都会携带这个Cookie,服务器就会识别为认证的用户。 这时,如果用户访问了一个带有恶意请求代码(比如)的网站(evil.com),请求会带着Cookie发送到服务器,从而代表用户执行了恶意操作。 5.6 httpOnly(服务器端设置) 这个关键字在浏览器本地无法设置,只能在服务器端set-cookie的时候设置。 设置了httpOnly的Cookie,在浏览器中无法用JavaScript访问,也就是document.cookie看不到。(防止黑客获取到Cookie) 6. 什么时候会携带cookie? 根据cookie的几个关键字设置,浏览器自动判断发出请求时是否携带cookie。 domain: cookie的域名; path: cookie的路径; samesite: 是否同站; 二、session session也叫作”会话控制”,是用户第一次向服务器请求连接时,由服务器生成的一个唯一标识,用于区别用户,并对用户进行权限隔离。 session 认证流程: 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到Session 证明用户已经登录可执行后面操作。 session在客户端的储存位置: cookie中(最佳); url中(需重写URL); session与cookie的主要区别:关闭浏览器后(或者长时间没有任何往来,超过了服务器设定的会话有效期),Session自动失效。 session将重要信息保存在服务器,仅将session_id等简要信息保存在客户端,相对cookie更安全。 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token。 三、localStorage 与 sessionStorage 1. 概述 localStorage 与 sessionStorage 都用来在浏览器中保存键值对。(HTML5中提供) localStorage 与 sessionStorage的键和值都必须是字符串,传入其他类型也会自动转换为字符串。 它们比Cookie的好处(区别)在哪? 不会像Cookie一样,随着每次请求一块发送到服务器; 保存的数据量更大,一般至少有2MB; 只能通过纯JavaScript操作,无法通过http等协议修改; Cookie是绑定到域的(与协议和端口无关),而 localStorage 是绑定到源的,也就是必须同一“协议+域名+端口”,才能访问同一 localStorage; sessionStorage只在当前标签页下有效,数据也只存在于当前浏览器标签页。 2. 应用场景 保存用户在页面的输入内容,比如表单、文本等,防止刷新或重新访问后消失; 快速响应:静态文件第一次请求后储存在localStorage,后续从localStorage直接读取,加快响应速度; sessionStorage 的使用情况非常少。 3. localStorage 与 sessionStorage的通用方法 setItem(key, value) —— 存储键/值对。 getItem(key) —— 按照键获取值。 removeItem(key) —— 删除键及其对应的值。 clear() —— 删除所有数据。 key(index) —— 获取该索引下的键名。 length —— 存储的内容的长度。 4. localStorage 和 sessionStorage 的区别 localStorage用于本地长期储存,它的生命周期是永久的,除非用户手动删除或清空; sessionStorage只在本次会话窗口(标签页)有效,窗口关闭就清空; localStorage是绑定源的,“协议+域名+端口”相同的URI就可以访问,本地多个同源标签页共享同一localStorage; sessionStorage是绑定标签页的,不同标签页即使同源也不共享sessionStorage,但是一个标签页内的iframe可以获取到该页面的sessionStorage。 5. 有关localStorage和sessionStorage的事件 每次更新localStorage和sessionStorage中的数据,storage事件就会发生,它的触发对象是所有能获取到该localStorage和sessionStorage的全局对象window。 window.addEventListener('storage',(e)=>{ console.log('storage changed.') }) 这个事件对象e具有如下属性: key —— 发生更改的数据的 key(如果调用的是 .clear() 方法,则为 null)。 oldValue —— 旧值(如果是新增数据,则为 null)。 newValue —— 新值(如果是删除数据,则为 null)。 url —— 发生数据更新的文档的 url。 storageArea —— 发生数据更新的 localStorage 或 sessionStorage 对象。 所有能获取到更新的localStorage的window,都会被触发storage事件,这提供了一种跨标签页传递数据的方式: 比如:同时打开了两个同源的标签页,页面上同时监听了storage事件,回调函数是打印一个数据。 那么在一个标签页修改localStorage数据之后,另一个标签页会响应这个storage事件,同时也会打印这个数据。 四、indexedDB 1. 什么是indexedDB? indexedDB是浏览器内置的一个数据库,它比localStorage更强大: 支持多种类型的键,储存多种JS类型的值; 更大的储存容量(一般至少250MB,甚至没有上限); 支撑事务(事务是一组同时成功或失败的操作),保证数据操作的可靠性; 支持键的范围查询(min,max等)、索引。 2. indexedDB 应用场景 在传统的浏览器-服务器页面应用有限,indexedDB主要应用于web离线应用。 3. indexedDB的使用方式 indexedDB也是绑定到源的,不同源的页面无法访问同一个indexedDB. ① 要使用一个indexedDB数据库,需要先打开它; let openRequest = indexedDB.open(name, version); name —— 字符串,即数据库名称。 version —— 一个正整数版本,默认为 1。 ② indexedDB数据库中可创建对象库用来存储特定数据,通过事务进行数据操作; ③ 通过键、范围或索引可以查询数据; indexedDB使用的较少,具体使用方式参考: IndexedDB 现代JS教程

前端异步请求相关问题

2021.05.02

异步请求

跨域

前端发送http请求,跨域、跨页面通信的几种方式梳理 Refer: 阮一峰CORS 一、发送异步Http请求几种方式; 二、跨域问题; 一、发送异步Http请求的几种方式 1. 原生Ajax 实例化XMLHttpRequest对象; 绑定xhr对象状态变化监听函数; 打开xhr对象; 发送http请求。 let xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { if(xhr.readyState === 4){ if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304){ // do something } else { // fault. do something. } } }; xhr.open('get', 'url', true); // true异步,false同步。 xhr.send(null); // 向服务器发送信息 1.1 原生Ajax的一些细节 如果传入相对URL,则是相对当前代码所在页面; 调用open()不会发送请求,只是为send()做好准备,send()执行后才发出请求; XHR对象有一个readyState属性,整个请求过程可能有五个值: 0:未初始化。尚未调用open(); 1:已打开。已经调用open(),尚未调用send(); 2:已发送。已经调用send(),尚未收到响应; 3:接收中。已经接收到部分响应; 4:完成。已经收到所有响应,可以使用。 收到响应前,可以调用xhr.abort()方法取消异步请求; 除了默认的http头部之外,如果想添加其他请求头,可以用xhr.setRequestHeader()方法,在发送请求send之前添加; 如果有查询字符串,需要用encodeURIComponent()函数编码后,附加在URL的后面,再发送GET请求; 可以为xhr对象设置timeout属性,用来表示它的最大响应时长。如果超时仍未响应,则中断请求: 超时会触发ontimeout事件; 超时后,readyState值仍为4。 请求过程中会触发一系列事件,从前到后分别是: loadstart: 接收响应的第一个字节时触发; progress: 接收响应期间,反复触发; progress事件对象中包含:lengthComputable、position、totalSize三个属性; lengthComputable:布尔值。代表接收的数据长度是否是可计算的; position:接收到的字节数; totalSize:总字节数。 error、abort或load: load在接收响应完成时触发,error在出错条件下触发,abort在调用abort()终止请求时触发; loadend:代表通信完成,在上面三个事件之后触发。 通过CORS跨域发送ajax请求: 默认不会携带凭据信息,包括cookie信息、Http认证和客户端SSL证书; 如果想带有凭据信息,需要将xhr.withCredentials属性设为ture; 服务器响应需要带有头部:Access-Control-Allow-Credentials: true, 如果没有,则浏览器不会把响应交付给JS请求对象。 同步Ajax请求会阻塞页面,影响用户体验。一般除非是在页面生命周期末尾unload事件上发送请求,不使用同步Ajax请求(unload上执行的异步请求会被取消,因为页面即将销毁,浏览器认为没有必要再进行异步请求); 2. Fetch API Fetch API是JavaScript原生的,基于Promise的标准请求API。(无需加载额外资源) 3. axios 基于Ajax和Promise封装的请求库。 二、跨域问题 1. 什么是同源策略?什么时候出现跨域问题? 在HTML标签(比如img,script等)和CSS中(url函数)使用url进行资源请求,浏览器默认不设任何限制,可以对任何资源url进行请求。 一般情况下,下列三种情况只能在同源条件下实现,非同源默认会报错(跨域错误): 读取Cookie、LocalStorage 和 IndexDB 获取DOM 元素 Js发送AJAX请求 这是浏览器的一种基本的保护策略,叫做同源策略。 同源策略 协议、域名和端口号都相同的请求,叫做同源请求。JS代码内只能发出同源请求。 即便两个不同的域名指向同一个 ip 地址,也非同源。 2. 跨域AJAX请求的几种常见解决方式 2.1 跨域资源共享(CORS:Cross Origin Resource Sharing) CORS是W3C标准,是跨域请求的根本解决方式。 实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。 浏览器将请求分为两种:简单请求和非简单请求。 简单请求 (1) 请求方法是以下三种方法之一: HEAD GET POST (2)HTTP的头信息不超出以下几种字段: Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain 简单请求和非简单请求,浏览器的发送流程如下图所示: 2.2 JSONP JSONP是一种古老的广泛使用的跨域请求方式,好处是可以兼容很多老式的浏览器。 JSONP主要是利用了script标签内src属性请求不受跨域限制的特点。 JSONP的主要流程: 在页面引入或写好要执行的函数; 创建一个<script>标签或动态创建一个script元素,src属性为跨域请求的地址,地址的最后用?callback=funcName,向服务器标记函数方法名; 挂载这个script元素,发出请求; 服务器收到这个请求后,解析callback后的函数名funcName,然后返回一个funcName函数执行的代码字符串: funcName(<data>),里面包含参数<data>,就是服务器想要浏览器执行的操作; 浏览器接收响应,作为代码执行。 let id = 0; // 让不同的jsonp请求回调函数名不同。 const jsonp = function(url, params) { return new Promise((res, rej) => { id += 1; let callbackName = 'callback' + id; let src = url + '?'; for (let k of Object.keys(params)) { src += `${k}=${params[k]}&`; } src += `callback=${callbackName}` let script = document.createElement('script'); script.src = src; window[callbackName] = (data) => { res(data); document.documentElement.removeChild(script); }; document.documentElement.append(script); }); } JSONP的局限性: 需要前后端协调配合; JSONP只能发GET请求。 2.3 WebSocket WebSocket是一种协议,它不受同源策略限制。 只要服务器支持,使用WebSocket协议(ws:// 或 wss://)进行请求即可。 2.4 代理服务器 2.4.1 本地代理服务器 服务器间通信不受同源策略限制。 因此,可以在本地用node开启一个设置了CORS的服务器,用这个服务器转发请求到目标服务器。 2.4.2 Nginx代理 通过Nginx开启一个正向代理服务器。 二、 跨窗口通信 同源策略规定: 如果我们有对另外一个窗口(例如,一个使用 window.open 创建的弹窗,或者一个窗口中的 iframe)的引用,并且该窗口是同源的,那么我们就具有对该窗口的全部访问权限。 否则,如果该窗口不是同源的,那么我们就无法访问该窗口中的内容:变量,文档,任何东西。唯一的例外是 location:我们可以修改它(进而重定向用户)。但是我们无法读取 location(因此,我们无法看到用户当前所处的位置,也就不会泄漏任何信息)。 非同源的窗口,还可以通过postMessage向其发送一条消息。这是对于非同源页面引用唯二的两个操作。 — 现代JS教程 对于与当前页不同源的页面引用,只能进行下面两个操作之一: 修改它的location(重定向); 给它发送一个信息postMessage(<message>)。 要想让同一个二级域下的所有子域都被视作同源,需要在每个页面上添加以下代码: // 为当前页面设置域(默认为当前页面URL的域名) document.domain = 'site.com'; 1. 同源跨窗口通信 1.1 Broadcast Channel API (广播频道) Broadcast Channel API 可以实现同源下浏览器不同窗口,Tab页,frame或者 iframe 下的 浏览器上下文 (通常是同一个网站下不同的页面)之间的简单通讯。 // 1.连接到test_channel广播频道,如果还没有这个频道,这代表创建一个名叫test_channel的广播频道 const bc = new BroadcastChannel('test_channel'); // 2.向广播信道发送消息 bc.postMessage('This is a test message.'); // 3.其他所有链接到这个广播频道的页面,可以接收到一个message事件 bc.onmessage = (e)=>{ console.log(e) } // 4.断开频道连接 bc.close() 1.2 利用localStorage 同源的页面,可以访问同一个localStorage。 通过对localStorage的修改进行监听(storage事件),可以实现跨页面通信。 2. 非同源跨窗口通信 2.1 可以获取到目标窗口的引用时: postMessage 这种情况包括嵌入iframe,使用window.open打开这类情况。 跨窗口通信有以下三步: 获取到目标窗口的引用win,然后调用win.postMessage(message, targetOrigin)方法 这里: data是要发送的数据,targetOrigin是目标窗口的源。 也就是说,只有目标窗口在指定的源下(协议、域和端口),才能正常接收到消息。 这保护了目标页面的安全,因为发送方此时与目标页面不同源,因此不能知道目标页面现在实际的源,用户可以随时更改到任何源。这样设定保证了数据不会被发送到非目标源的页面。 在目标页面上,写入好window.onmessage事件,用于监听收取来自外部页面的message。 window.onmessage = (e) => { // do someting with the message. // 此时的e具有三个内部属性: ①data:数据本身;②origin:发送方的源; ③source:发送方窗口的引用(可以随时使用e.source.postMessage反向发送消息。); } 2.2 a.xxx.com 与 b.xxx.com 通信:修改document.domain 两个页面位于同一个上级域名的不同子域名下,原本为非同源(因为域不同),通过分别设置document.domain = 'xxx.com',可让二者为同源。 这样通过a.xxx.com的<iframe>就可以获取到b.xxx.com的全部内容了。

HTTPs协议

2021.05.02

Network

HTTPS协议内容、TLS握手过程 HTTP协议的缺点 HTTPs协议 2.1 加密方式:对称与非对称加密 2.2 HTTPs的加密方式 2.3 数字证书: 公钥的真实性问题 2.4 HTTPs的通信过程 2.5 HTTPs的缺点? 为什么不一直使用 2.6 HTTPs的会话复用 一、HTTPs协议 1. HTTP协议的缺点 通信不加密,使用明文传输; 不验证通信双方的身份,有可能遭遇伪装; 无法验证报文的完整性,可能遭到篡改。 2. HTTPs协议 HTTPs协议在HTTP协议基础上,在运输层(TCP)与应用层(HTTP)之间,添加一层安全层,使用SSL/TLS协议对http报文进行加密处理,实现双方安全通信。 所以HTTPs也叫HTTP-over-ssl/tls。 HTTPs = HTTP + 加密 + 身份认证 + 完整性保护 2.1 加密方式:对称与非对称加密 现代加密方式中,算法是公开的,而解密需要的密钥是保密的。 对称加密:客户端与服务器都使用同一个密钥进行加密、解密。(双方共同依赖密钥的相互通信是个问题,也就是密钥本身该如何加密传输) 非对称加密:密钥分为公钥和私钥,公钥是公开的密钥,私钥是保密的密钥。一段用公钥加密的密文只能用私钥解密,而用私钥加密的密文只能用公钥解密。(解决了对称加密密钥无法相互传输的问题,一方用公钥加密,另一方接收后用本地私钥解密即可,无需传递私钥。缺点是效率较低) 2.2 HTTPs的加密方式 混合加密: 先使用非对称加密传递密钥,然后利用对称加密传输信息。 HTTPs中使用的TLS协议,一共涉及三种加密(算法)技术:① 用于传递会话对称密钥的非对称加密算法(DH算法、RSA算法等);② 用于会话的对称加密算法(DES、3DES、AES); ③ 用于校验数据完整性的摘要算法(MD5、SHA1、SHA256等)。 2.3 数字证书:公钥的真实性问题 通信对象的公钥如何确认其真实性,而不是中间者伪造? 解决方式是:选取值得信任的第三方认证机构,对公钥进行认证,并提供数字签名用于校验公钥的真实性。 内部机制如下: 服务器方向第三方认证机构CA(Certificate Authority)(比如VeriSign威立信)提交认证申请,并提供公钥; VeriSign对服务器方进行审查,确认真实性后发放证书,其中包含公钥和VeriSign使用自己内部私钥生成的数字签名; 数字签名是怎么生成的? CA对服务器方进行审查后,生成一份证书明文数据initData,包括服务器公钥、Hash算法、证书过期时间、持有者信息等。 使用Hash算法对initData进行Hash处理,得到一份数据摘要initAbstract; 使用CA私钥对initAbstract进行加密,得到数字签名Sign; 组合证书明文数据initData和数字签名Sign,形成证书。 服务器将这个证书发送给客户端; VeriSign的公钥(和其他一些常见认证机构的公钥)已经内置在客户端浏览器中,客户端可以利用这个公钥,对证书的数字签名进行解密,确认服务器公开密钥的真实性(身份确认); 如何确认证书内容没有遭到篡改? 客户端使用Hash算法对证书明文数据initData进行Hash化处理,得到数据摘要abstract; 客户端使用浏览器内置CA公钥,对数字签名进行解密,得到原始数据摘要initAbstract; 按理说,这两份摘要(initAbstract和abstract)应该是相同的,如果不同则证明中间有人修改过证书,证书不值得信任。(中间人不可能通过同时修改数字签名达到二者一致,因为中间人没有CA私钥) 客户端此时就可以信任地使用服务器公钥加密报文,进行信息传递了。 2.4 HTTPs的通信过程 HTTPs先进行TCP握手,然后进行TLS握手,然后进行加密的HTTP通信。 TLS通信整体分为三个步骤: ① 验证服务器端身份,获取服务器端公钥; ② 双方协商生成对话密钥; ③ 使用对话密钥进行对称加密通信。 其中前两步是TLS握手阶段,这个阶段的所有报文都是明文。TLS1.2握手的流程如下: TLS1.3的握手有少许差别,但是原理是一样的。 客户端发出ClientHello请求,内容包含:一个客户端生成的随机数rand1、支持的TLS协议版本、支持的加密算法、支持的压缩方法; 服务器端回应ServerHello报文,内容包含:一个服务器端生成的随机数rand2、确认的TLS协议版本、加密算法和压缩方法; 服务端发送服务器证书(公钥和第三方机构数字签名); 客户端收到服务器证书后,对签名进行验证确认服务器身份,如果身份可疑,发出警告提示并由用户确定是否继续通信; 客户端确认服务器身份OK后,取出服务器公钥,然后生成一个随机数pre-master key,并使用服务器公钥对rand1、rand2和pre-master key三者的组合数进行加密,生成双方对话密钥; 客户端向服务器发送报文,内容包含:生成的对话密钥、编码改变通知、客户端握手结束通知; 第一次使用对话密钥的加密通信:客户端发送前面已发送全部内容的一个Hash值,用来供服务器校验(通信数据完整性); 服务器收到后,用私钥解密出对话密 钥,向客户端发送确认报文:session ticket(如果支持),编码改变通知、服务器握手结束通知; 服务器发送前面全部内容的Hash值,用来供客户端校验(通信数据完整性); 从此之后,双方使用同一个对话密钥,加密报文内容,进行常规的Http加密通信。 Ping douban.com 握手抓包WireShark 2.5 HTTPs的缺点?为什么不一直使用? HTTPs的加解密过程繁琐,消耗了两端CPU和内存资源,速度更慢(相比于HTTP慢2~100倍),一般只用在敏感数据的传输上; HTTPs通信中需要用到数字签名证书,服务器端必须向权威机构购买,是一笔额外的开销。 2.6 Https的会话复用 Https的非对称加密握手过程,耗时长,计算量大,非常消耗双方资源; 会话复用的实现,解决了这个问题; Https会话复用的两种机制: session ID: 服务器存储主要会话信息; session ID由客户端发起,在client hello报文中发送; 服务端根据session ID进行匹配,找到之前的某次会话信息,如果成功匹配会在server hello报文中携带这个session id; 双方绕过了密钥协商过程,直接复用之前的会话密钥; session ticket:客户端存储主要会话信息。 session ticket由服务端生成,它只有用服务端自己的私钥才能解密; session ticket在双方第一次加密握手结束之后,发送给客户端,由客户端保存; 客户端下次再和服务端进行连接的时候,client hello报文携带这个session ticket,服务端可以对它进行解密,拿出里面的会话密钥直接复用,双方因此绕过了复杂的加密流程。 在session ID机制和session ticket机制同时存在的情况下,session ticket生效,优先级更高; session ID模式和session ticket模式的区别是: session ID: session ID由客户端生成,服务端需要保存id和对应会话状态的一个字典,增大了服务端压力; session ID在服务器集群中使用,因为两次访问可能对应不同的响应服务器,因此sesision ID复用率下降,需要用公共储存介质来实现复用; session ticket: session ticket由服务端用自己的密钥ticket-key生成,里面携带了会话的密钥等信息,且只有服务端才能解密; session ticket由客户端保存,服务端不用保存,减小了服务端压力; 对于服务器集群,所有服务器都必须使用同一个密钥ticket-key,时间久了会不安全,一般需要定时轮换;

Vuex基操与原理

2021.04.29

Vue

Vuex 用于统一管理Vue app中各组件的状态,好处有二: ① 避免了属性父传子,子改父的麻烦操作。尤其是组件数目多、嵌套深的情况。 ② 可以统一拦截修改状态的操作,从而进行一些记录,做到修改状态有迹可循,方便查错维护。 Store 仓库 vuex通过创建Store,并作为插件应用在Vue app实例上,来达到统一管理app实例中组件状态的目的。 import { createStore } from 'vuex' import { createApp } from 'vue' import App from 'App.vue' const app = createApp(App) const store = createStore({ state(){ // define states here. }, mutations: { // define mutations here. } }) app.use() vuex的方法全部类似vue, createStore()传入的对象也与根组件配置对象形式相同,就像只是换了属性名字。data换为state,methods换为mutations,computed换为getter。 在app.use(store)执行后,app根组件实例上会被挂载一个$store全局对象,这样全部组件都可以在内部通过this.$store引用这个store内的数据。 在Vue3 组合式API的setup()函数中,this不指向组件实例,也就不能用this.$store访问。 此时需要从vuex引入useStore 方法,并在setup中声明 const store = useStore(); 这样,通过这个变量store就可以访问vuex store了。 State 状态 state是vuex中保存的状态,不能直接修改,必须通过mutations中定义的方法进行修改。 store中的state都是响应式的,在组件中可以通过computed()计算属性来引入,并保持响应式。 mapState()方法可以从vuex中引入,用来方便一次性获取多个store中的state,并返回给computed。 Mutations 变更(突变) vuex store 配置的mutations属性中声明对state的各种操作方式。 它们应该是修改state的唯一方式,虽然通过store.state.xx直接修改也是可以的,不会报错,但是这样就不能对组件修改操作进行拦截,也有悖设计原则,无法确定哪个组件修改了state,对debug产生困扰。 // store config // mutation函数在被调用的时候,State永远作为第一个参数传入,之后的参数可以自定义 mutations: { increment (state, n) { state.count += n } } // components { store.commit('increment', 2) } 官方说明: mutation应该必须为同步函数。 就像mutation的含义一样,它是直接变更state的。如果产生异步,则会出现违背设计的未知问题。 mapMutations 用来在子组件的methods中,一次性commit多个mutation。 getters 计算状态 getters相当于vue组件中的computed计算属性。只不过它是在store中定义的,全局组件都可以使用。 getters的设置目的是定义全局计算属性,免去每次在组件中引入并声明的繁琐。 { getters: { // 对state中的arr.good 为 true的子数组进行返回 goodArr: state.arr.filter((arr)=>{return arr.good === true})_ } } 同样,mapGetters方法用于一次性获取多个getter属性到组件。 Actions 动作 Actions 和 Mutations 差不多,官方提出他们的两点区别如下: Mutations直接修改status,而Actions是commit mutations,相当于间接通过mutations修改state; Actions可以是异步函数。 actions在触发的时候,会传入一个参数context,相当于整个store的上下文环境引用。 可以通过这个context,获取store内的任意属性。比如:context.state, context.mutation。 因此也可以使用解构语法,在传入context的时候直接获取想要的方法: actions: { increment ({ commit }) { // 解构获取了store.commit commit('increment') } } 不同于mutation的commit,actions的触发方式是dispatch。 // trigger an action inside a component. this.store.dispatch('action1') actions可以是异步的,因为它们是通过一个个的mutation来修改state的。不论什么时候触发mutation,只要最终触发了的不同mutation互不影响,store.state的最终状态就一样。 但是,触发修改同一state的多个mutation,触发的先后顺序可能造成state最终的结果不同。比如一个state为价格10,先+3再×2,和先×2再+3,结果不一致。 这时,就需要对actions进行设计,让它触发mutation的先后顺序可控。具体见官网下例: // assuming `getData()` and `getOtherData()` return Promises actions: { async actionA ({ commit }) { commit('gotData', await getData()) }, async actionB ({ dispatch, commit }) { await dispatch('actionA') // wait for `actionA` to finish commit('gotOtherData', await getOtherData()) } }

Vue工作流程及各类型对象API关系

2021.04.27

Vue

创建一个新的Vue app的整个流程,及各类型对象的API,和他们之间的交联关系梳理。 创建一个Vue app整体流程梳理 Vue模块中暴露的API有哪些? 这些API都可以从vue中直接导入获取。 import { <api> } from 'vue' 渲染 createApp:传入根组件配置对象,创建应用实例,并返回创建的应用实例。 createSSRApp:传入根组件,创建SSR应用实例。 render:手动渲染VNode的函数。 hydrate:用于SSR,将服务端渲染好的html中,用传入本地的剩余部分js为页面注水渲染。 SFC中CSS工具 应该很少使用到。 useCssModule: 选择使用哪个CSS Module,传入一个字符串指定。默认是’$style‘ useCssVars: 原生DOM组件 Transition TransitionProps (TS接口) TransitionGroup TransitionGroupProps (TS接口) 版本号 version: Vue版本号,目前是3.0.11 响应式API @vue/reactivity 核心 Core reactive: 创建响应式对象 (__v_isReactive === true) ref: 创建响应式变量 readonly: 创建只读对象 (__v_isReadonly === true) 小工具 Utilities unref: 把ref类型对象变回原始值 (返回ref.value),不是ref类型的原样返回 proxyRefs: isRef:判断是否是ref类型 toRef: 接受(object,key),将某一对象obj的某一key值变成Ref toRefs: 接受一个reactive对象,将其内部所有属性变成ref类型,从而可以对外解构赋值 isReactive: 判断是否是reactive对象。如果传入一个只读对象,获取它的Raw原始值,继续判断是否是响应式对象。 isReadonly: 判断是否是readonly对象 (ReactiveFlags.IS_READONLY === true) isProxy: 判断是否是经vue转换后的proxy。(是isReactive和isReadonly的或值) 高级 Advanced customRef: 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。 triggerRef:与shallowRef配合使用。因为shallowRef的内部值不是响应式的,这会导致包含它们的watch和computed无法响应它们的更新,这时可以通过trigger(shallowRef)进行手动触发。(一般也很少用吧。。- -||) shallowRef:创建浅ref。这种Ref只跟踪它内部这个value的变化,而这个value本身不会变成响应式的。例如: let a = {} let b = shallowRef(a) a = {name: 'Mars'} b.value // {name: 'Mars'} isReactive(a) //false 高级 Advanced shallowReactive:与shallowRef同理,创建一个浅响应式对象,只响应这个对象本身的更新(单层转化),而对象自身内部不会变成响应式。 shallowReadonly:创建一个既shallow又readonly的对象。 markRaw:置一个reactive对象的__v_skip属性为true,也就是说它将永远不会变成响应式对象。 toRaw:获取一个reactive对象的原始值。如果reactive是多层嵌套的,一直向内查询__v_raw属性,直到最内层非响应式的原始值。 (不是reactive对象类型,原样返回,包括ref类型) 计算和监听 computed: 创建计算属性。传入一个函数(getter)或两个函数(getter和setter),函数内包含的所有reactive和ref响应式变量改变时,计算属性自动重新计算。 watchEffect: 传入一个函数,立即运行,无需指定侦听的具体参数,然后自动跟踪其内部所有的响应式变量 —— 当任何一个响应式变量改变时立即重新运行函数。 watch: 侦听器函数。传入两个参数,第一个是侦听的参数,第二个是回调函数(当侦听的参数变化时执行。) 生命周期钩子 这些都是在Setup中使用的钩子函数。 不多赘述。不熟悉的引用官网说明。 onBeforeMount onMounted onBeforeUpdate onUpdated onBeforeUnmount onUnmounted onActivated: 被 keep-alive 缓存的组件激活时调用。 onDeactivated: 被 keep-alive 缓存的组件停用时调用。 onRenderTracked: 跟踪虚拟 DOM 重新渲染时调用。钩子接收 debugger event 作为参数。此事件告诉你哪个操作跟踪了组件以及该操作的目标对象和键。 onRenderTriggered: 当虚拟 DOM 重新渲染被触发时调用。和 renderTracked 类似,接收 debugger event 作为参数。此事件告诉你是什么操作触发了重新渲染,以及该操作的目标对象和键。 onErrorCaptured: 当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。 provide / inject 依赖注入,只能在Setup中使用。 provide:传入两个参数,第一个是provide变量的名称,第二个是具体的值。 provide的值可以是ref或reactive类型,在inject后依然保持响应性。 一般建议provide/inject的响应式值,只从provide的一方修改,inject一方只读取值不修改。(可以在provide的时候将变量变为readonly) inject: 可传入两个参数。第一个是接收的provide变量名称,第二个是可选参数,表示默认值。 nextTick nextTick: 传入一个回调函数,在下一个DOM更新周期执行。(用来防止修改响应式数据,DOM还没有更新,这时访问DOM中数据是旧数据的现象。) 组件、props、emit defineComponent: defineAsyncComponent: defineProps: defineEmit: useContext: 渲染、VNode相关 h: 传入三个参数:type、props、children,返回创建的VNode对象。 createVNode cloneVNode mergeProps isVNode VNode的四种类型(均为Symbol类型变量): Fragment Text Comment Static 原生组件 Teleport TeleportProps (TS interface) Suspense SuspenseProps (TS interface) KeepAlive KeepAliveProps (TS interface) BaseTransition BaseTransitionProps (TS interface) 指令相关 withDirectives 服务端渲染相关 useSSRContext ssrContextKey 自定义渲染器 API createRenderer createHydrationRenderer queuePostFlushCb warn handleError callWithErrorHandling callWithAsyncErrorHandling ErrorCodes resolveComponent resolveDirective resolveDynamicComponent registerRuntimeCompiler isRuntimeOnly useTransitionState resolveTransitionHooks setTransitionHooks getTransitionRawChildren initCustomFormatter devTools相关 devtools setDevtoolsHook 其他 getCurrentInstance: 只能在setup中使用,用于获取当前组件实例。 Vue定义的TS类型 Vue中直接导出了所有重要的TS类型,供开发者使用。 响应式相关 ReactiveEffect ReactiveEffectOptions DebuggerEvent TrackOpTypes TriggerOpTypes Ref ComputedRef WritableComputedRef UnwrapRef ShallowUnwrapRef WritableComputedOptions ToRefs DeepReadonly 侦听器相关 WatchEffect WatchOptions WatchOptionsBase WatchCallback WatchSource WatchStopHandle 应用实例相关 App AppConfig AppContext Plugin CreateAppFunction OptionMergeFunction VNode相关 VNode VNodeChild VNodeTypes VNodeProps VNodeArrayChildren VNodeNormalizedChildren 组件相关 Component ConcreteComponent FunctionalComponent ComponentInternalInstance SetupContext ComponentCustomProps AllowedComponentProps DefineComponent ComponentPublicInstance ComponentCustomProperties 组件配置项 ComponentOptions ComponentOptionsMixin ComponentOptionsWithoutProps ComponentOptionsWithObjectProps ComponentOptionsWithArrayProps ComponentCustomOptions ComponentOptionsBase RenderFunction MethodOptions ComputedOptions AsyncComponentOptions AsyncComponentLoader emit相关 EmitsOptions ObjectEmitsOptions 渲染器相关 Renderer RendererNode RendererElement HydrationRenderer RendererOptions RootRenderFunction slot相关 Slot Slots 指令相关 Directive DirectiveBinding DirectiveHook ObjectDirective FunctionDirective DirectiveArguments InjectionKey HMRRuntime SuspenseBoundary TransitionState TransitionHooks

RESTful规范及RESTful API设计

2021.04.25

规范

什么是RESTful规范?如何设计使用RESTful API? 参考: 阮一峰网络日志 RESTful架构是什么? RESTful架构,是目前最流行的一种互联网软件架构。(REST:Representational State Transfer 表现状态转化) 通俗理解: ① 资源在HTTP请求URI中定义 URI代表资源,资源是原始的数据,不带有表现形式。 ② 表现层在HTTP头部Accept和Content-Type字段指定 在网络中,页面可以用.html表现,图片可以用.png等表现,文字可以用.txt表现,加上这些表现形式,构成了资源的表现层。 ③ 表现层状态转化:使用GET/POST/PUT/DELETE等HTTP方法,操作服务器中的资源 RESTful API 设计 在网络中其实就是指前后端接口的设计(前后端整体遵循RESTful架构)。 协议、API域名、API版本号 应该总是使用HTTPs协议; 应该尽量把API放在专属域名下,并带有API的版本号: https://api.somesite.com/v2/。 路径 通过API请求的是资源,因此API中不应该有动词,而应该只有名词。 动词代表一种操作,应该是通过HTTP请求方法(GET、POST等)或对事件资源进行请求来实现。 获取动物园资源:https://api.website.com/zoos 获取动物资源:https://api.website.com/animals 资源操作 通过HTTP请求方法,对应数据库的增删改查方式,实现对资源的操作。(GET/POST/DELETE/PUT等) GET: 读取; POST: 创建; PUT: 整体更新(body中携带了完整的资源,服务端整体替换); PATCH: 部分更新(body中携带了需要更新的部分信息,服务端接收后执行部分更新); DELETE: 删除; 信息过滤Filtering: 查询字符串 API应该提供参数,返回过滤后的结果。 例如可以使用Axios发送的请求: // 请求目标为: http://marswiz.com?name=Cool axios.get({ url: 'http://marswiz.com', params: { name: 'Cool', }, }); 常见的过滤参数: limit: 返回的记录的数量; offset: 返回记录的开始位置; page: 页码; perPage: 每页的记录数; sortBy: 排序方式; type: 按类型筛选记录。 返回状态码与错误处理 每次请求,客户端都必须做出回应。 利用HTTP返回的状态码,告知客户端请求的结果状态。(2xx,3xx,4xx,5xx) 一种错误的做法,是任何请求都返回200状态码,然后在响应体中记录错误的具体细节。 这样只有在客户端解析了数据体后,才能得知响应失败的信息。 如果返回资源状态码为4xx或5xx,则需要在响应体里面附带错误的具体信息,便于客户端查看。 各种操作返回的资源类型 GET /collection:返回资源对象的列表(数组) GET /collection/resource:返回单个资源对象 POST /collection:返回新生成的资源对象 PUT /collection/resource:返回完整的资源对象 PATCH /collection/resource:返回完整的资源对象 DELETE /collection/resource:返回一个空文档 Hypermedia API Hypermedia API就是在访问API根目录的时候,应该返回根目录下所有可用API组成的JSON文件,提供了各种API的URI,让用户无需查文档也能获取到想要的API接口地址。 身份认证与CORS 使用Authorization首字段,传递身份信息。 可以设置CORS允许客户端进行跨域请求。 其他 服务器返回的数据格式,应该尽量使用JSON,避免使用XML。

Vue组件渲染函数及VNode生成函数h()、createVNode()

2021.04.21

Vue

源文件目录: packages/runtime-core/src/vnode.ts packages/runtime-core/src/h.ts Vue组件实现模板解析渲染的几种方式 单文件组件(SFC)中,Vue组件可以使用定义渲染模板; 任何组件都可以通过配置中tempalte property传入模板字符串定义渲染模板; 定义render()方法,作为组件的渲染函数进行纯JS渲染。 VNode的TS类型定义源码 VNode中常见的重要属性如下: type: VNode的类型。(代表类型的字符串,VNode对象,Component对象,Text类型Symbol,Static类型Symbol,Comment类型Symbol,Fragment类型Symbol等) props: VNode本身的属性。官方这里可选定义了:key,ref,和VNode的一些生命周期钩子:onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted等。Props可以在h()函数的第二个参数传入。 children: 子VNode; key: VNode的唯一标志符; el: VNode在DOM中挂载的元素对象 // packages/runtime-core/src/vnode.ts export interface VNode< HostNode = RendererNode, HostElement = RendererElement, ExtraProps = { [key: string]: any } > { /** * @internal */ // 判断是否是VNode的内部标志:__v_isVNode __v_isVNode: true /** * @internal */ // VNode设置为永远不是响应式的 [ReactiveFlags.SKIP]: true // VNode 的类型: // export type VNodeTypes = // | string // | VNode // | Component // | typeof Text // | typeof Static // | typeof Comment // | typeof Fragment (Fragment是Vue3引入的新节点类型,用来包裹多个根元素组件,让用户可以不用强制创建单一根元素组件。) // | typeof TeleportImpl // | typeof SuspenseImpl type: VNodeTypes props: (VNodeProps & ExtraProps) | null key: string | number | null ref: VNodeNormalizedRef | null /** * SFC only. This is assigned on vnode creation using currentScopeId * which is set alongside currentRenderingInstance. */ scopeId: string | null /** * SFC only. This is assigned to: * - Slot fragment vnodes with :slotted SFC styles. * - Component vnodes (during patchsuspense` ydration) so that its root node can * inherit the component's slotScopeIds */ slotScopeIds: string[] | null children: VNodeNormalizedChildren component: ComponentInternalInstance | null dirs: DirectiveBinding[] | null transition: TransitionHooks<HostElement> | null // DOM el: HostNode | null anchor: HostNode | null // fragment anchor target: HostElement | null // teleport target targetAnchor: HostNode | null // teleport target anchor staticCount: number // number of elements contained in a static vnode // suspense suspense: SuspenseBoundary | null ssContent: VNode | null ssFallback: VNode | null // optimization only shapeFlag: number patchFlag: number dynamicProps: string[] | null dynamicChildren: VNode[] | null // application root node only appContext: AppContext | null } h()函数 从源码上看,h()函数主要是对不同的传参形式进行了重载。在内部,还是调用createNode()函数。 // 这里定义了一系列h()函数的重载形式,用来让用户更容易使用。 // h()函数用来生成VNode,用户用来在组件中手动编写render()函数方法,用来代替template。 // !!————————从这里开始都是重载编写部分——————————————!! // 传入字符串类型,生成DOM元素 // 可以传入两个参数,也可以传入三个参数。对应下面第一个和第二个重载形式。 export function h(type: string, children?: RawChildren): VNode export function h( type: string, props?: RawProps | null, children?: RawChildren | RawSlots ): VNode // 传入文本类型符号或注释类型符号,生成对应VNode: // export const Text = Symbol(__DEV__ ? 'Text' : undefined) // export const Comment = Symbol(__DEV__ ? 'Comment' : undefined) // export const Static = Symbol(__DEV__ ? 'Static' : undefined) export function h( type: typeof Text | typeof Comment, children?: string | number | boolean ): VNode export function h( type: typeof Text | typeof Comment, props?: null, children?: string | number | boolean ): VNode // 下面是特殊类型节点的生成,不重点了解。 // 生成fragment类型VNode export function h(type: typeof Fragment, children?: VNodeArrayChildren): VNode export function h( type: typeof Fragment, props?: RawProps | null, children?: VNodeArrayChildren ): VNode // teleport (target prop is required) export function h( type: typeof Teleport, props: RawProps & TeleportProps, children: RawChildren ): VNode // suspense 组件类型节点 export function h(type: typeof Suspense, children?: RawChildren): VNode export function h( type: typeof Suspense, props?: (RawProps & SuspenseProps) | null, children?: RawChildren | RawSlots ): VNode // functional component export function h<P, E extends EmitsOptions = {}>( type: FunctionalComponent<P, E>, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren | RawSlots ): VNode // catch-all for generic component types export function h(type: Component, children?: RawChildren): VNode // concrete component export function h<P>( type: ConcreteComponent | string, children?: RawChildren ): VNode export function h<P>( type: ConcreteComponent<P> | string, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren ): VNode // 没有属性props的组件 export function h( type: Component, props: null, children?: RawChildren | RawSlots ): VNode // exclude `defineComponent` constructors export function h<P>( type: ComponentOptions<P>, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren | RawSlots ): VNode // fake constructor type returned by `defineComponent` or class component export function h(type: Constructor, children?: RawChildren): VNode export function h<P>( type: Constructor<P>, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren | RawSlots ): VNode // fake constructor type returned by `defineComponent` export function h(type: DefineComponent, children?: RawChildren): VNode export function h<P>( type: DefineComponent<P>, props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren | RawSlots ): VNode // !!————————重载编写部分结束——————————————!! // 这里是h()函数的具体实现:可以看到就是根据不同的形式调用createVNode() export function h(type: any, propsOrChildren?: any, children?: any): VNode { const l = arguments.length if (l === 2) { if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { // single vnode without props if (isVNode(propsOrChildren)) { return createVNode(type, null, [propsOrChildren]) } // props without children return createVNode(type, propsOrChildren) } else { // omit props return createVNode(type, null, propsOrChildren) } } else { if (l > 3) { children = Array.prototype.slice.call(arguments, 2) } else if (l === 3 && isVNode(children)) { children = [children] } return createVNode(type, propsOrChildren, children) } } createVNode函数: h()函数调用的原始函数 // 根据是否是开发环境选择不同的方法,核心都是_createVNode函数。 export const createVNode = (__DEV__ ? createVNodeWithArgsTransform : _createVNode) as typeof _createVNode function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag: number = 0, dynamicProps: string[] | null = null, isBlockNode = false ): VNode { // 判断传入的type是否合法(为falsy),不合法报错。 // 这时type会被设定为注释类型。 if (!type || type === NULL_DYNAMIC_COMPONENT) { if (__DEV__ && !type) { warn(`Invalid vnode type when creating vnode: ${type}.`) } type = Comment } // 判断传入的类型如果本身是VNode,就复制一个新的VNode返回。 if (isVNode(type)) { // createVNode receiving an existing vnode. This happens in cases like // <component :is="vnode"/> (动态组件!) // #2078 make sure to merge refs during the clone instead of overwriting it const cloned = cloneVNode(type, props, true /* mergeRef: true */) if (children) { normalizeChildren(cloned, children) } return cloned } // class component normalization. if (isClassComponent(type)) { type = type.__vccOpts } // class & style normalization. if (props) { // for reactive or proxy objects, we need to clone it to enable mutation. if (isProxy(props) || InternalObjectKey in props) { props = extend({}, props) } let { class: klass, style } = props if (klass && !isString(klass)) { props.class = normalizeClass(klass) } if (isObject(style)) { // reactive state objects need to be cloned since they are likely to be // mutated if (isProxy(style) && !isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } } // encode the vnode type information into a bitmap const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : __FEATURE_SUSPENSE__ && isSuspense(type) ? ShapeFlags.SUSPENSE : isTeleport(type) ? ShapeFlags.TELEPORT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : isFunction(type) ? ShapeFlags.FUNCTIONAL_COMPONENT : 0 if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) { type = toRaw(type) warn( `Vue received a Component which was made a reactive object. This can ` + `lead to unnecessary performance overhead, and should be avoided by ` + `marking the component with \`markRaw\` or using \`shallowRef\` ` + `instead of \`ref\`.`, `\nComponent that was made reactive: `, type ) } // VNode的初始值定义。包含VNode的所有属性参数。 const vnode: VNode = { __v_isVNode: true, [ReactiveFlags.SKIP]: true, type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, slotScopeIds: null, children: null, component: null, suspense: null, ssContent: null, ssFallback: null, dirs: null, transition: null, el: null, anchor: null, target: null, targetAnchor: null, staticCount: 0, shapeFlag, patchFlag, dynamicProps, dynamicChildren: null, appContext: null } // validate key if (__DEV__ && vnode.key !== vnode.key) { warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type) } normalizeChildren(vnode, children) // normalize suspense children if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { const { content, fallback } = normalizeSuspenseChildren(vnode) vnode.ssContent = content vnode.ssFallback = fallback } if ( shouldTrack > 0 && // avoid a block node from tracking itself !isBlockNode && // has current parent block currentBlock && // presence of a patch flag indicates this node needs patching on updates. // component nodes also should always be patched, because even if the // component doesn't need to update, it needs to persist the instance on to // the next vnode so that it can be properly unmounted later. (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) && // the EVENTS flag is only for hydration and if it is the only flag, the // vnode should not be considered dynamic due to handler caching. patchFlag !== PatchFlags.HYDRATE_EVENTS ) { currentBlock.push(vnode) } return vnode }

Vue3源码学习:从createApp开始发生了什么

2021.04.21

Vue

Vue3源码学习: runtime-core 从createApp开始,应用实例的创建过程。 源码目录:packages/runtime-core/src/apiCreateApp.ts Vue应用实例相关的TS类型声明 App应用实例 app应用实例所暴露的内部属性,都在这里定义了。 export interface App<HostElement = any> { // 版本号 version: string // app配置对象 config: AppConfig // use()方法 use(plugin: Plugin, ...options: any[]): this // 使用mixin mixin(mixin: ComponentOptions): this // 全局注册组件方法:可以只传入一个字符串name 或 先后传入字符串name和组件component本身。 // 两个都传入的时候,返回app本身 // 只传入一个字符串name,返回已注册的component。 component(name: string): Component | undefined component(name: string, component: Component): this // 全局注册自定义指令方法,同上。 directive(name: string): Directive | undefined directive(name: string, directive: Directive): this // 挂载方法mount // 三个参数:①rootContainer: 根容器,类型是【宿主环境的元素】或【字符串】。(浏览器中对应【DOM元素】和【query字符串选择符】) // ② isHydrate:设置是否是hydrate操作。(hydrate【注水】:含义是将服务器渲染出的字符串数据,在前端浏览器中进行再次渲染,将字符串数据整合成页面,类似将干巴巴的原始数据注水盘活成网页。) // ③ isSVG:设置挂载目标是否是SVG元素,用在render()函数中。 mount( rootContainer: HostElement | string, isHydrate?: boolean, isSVG?: boolean ): ComponentPublicInstance unmount(): void provide<T>(key: InjectionKey<T> | string, value: T): this // internal, but we need to expose these for the server-renderer and devtools _uid: number _component: ConcreteComponent _props: Data | null _container: HostElement | null _context: AppContext } 应用实例配置对象:AppConfig app的应用配置,使用应用实例对象的app.config获取。 export interface AppConfig { // @private readonly isNativeTag?: (tag: string) => boolean // app配置对象 // 官方解释:设置为 true 以在浏览器开发工具的 performance/timeline 面板中启用对组件初始化、编译、渲染和更新的性能追踪。只适用于开发模式和支持 performance.mark API 的浏览器。 performance: boolean // 自定义选项合并策略:父实例和子实例上同名选项的合并方式。用得少,具体看文档。 optionMergeStrategies: Record<string, OptionMergeFunction> // 组件的全局属性。 globalProperties: Record<string, any> // 官方解释:指定一个方法,用来识别在 Vue 之外定义的自定义元素。 isCustomElement: (tag: string) => boolean // 指定一个处理函数,来处理组件渲染方法执行期间以及侦听器抛出的未捕获错误。这个处理函数被调用时,可获取错误信息和应用实例。 errorHandler?: ( err: unknown, instance: ComponentPublicInstance | null, info: string ) => void // 为 Vue 的运行时警告指定一个自定义处理函数。只在开发环境下生效,在生产环境下它会被忽略。 warnHandler?: ( msg: string, instance: ComponentPublicInstance | null, trace: string ) => void } 应用环境:AppContext 应用环境类型,定义了应用实例内部配置、mixin、组件、自定义指令和provide信息。 export interface AppContext { app: App // for devtools config: AppConfig mixins: ComponentOptions[] components: Record<string, Component> directives: Record<string, Directive> provides: Record<string | symbol, any> /** * Flag for de-optimizing props normalization * @internal */ deopt?: boolean /** * HMR only * @internal */ reload?: () => void } 应用实例app的:createApp方法源码 export function createAppAPI<HostElement>( render: RootRenderFunction, hydrate?: RootHydrateFunction ): CreateAppFunction<HostElement> { return function createApp(rootComponent, rootProps = null) { if (rootProps != null && !isObject(rootProps)) { __DEV__ && warn(`root props passed to app.mount() must be an object.`) rootProps = null } const context = createAppContext() const installedPlugins = new Set() let isMounted = false // 这里创建了app实例对象: const app: App = (context.app = { // 内部属性定义 _uid: uid++, _component: rootComponent as ConcreteComponent, _props: rootProps, _container: null, _context: context, version, // 实例对象的config配置属性,是一对getter/setter,不允许通过app实例对象修改内部配置app.config。 get config() { return context.config }, set config(v) { if (__DEV__) { warn( `app.config cannot be replaced. Modify individual options instead.` ) } }, use(plugin: Plugin, ...options: any[]) { if (installedPlugins.has(plugin)) { __DEV__ && warn(`Plugin has already been applied to target app.`) } else if (plugin && isFunction(plugin.install)) { installedPlugins.add(plugin) plugin.install(app, ...options) } else if (isFunction(plugin)) { installedPlugins.add(plugin) plugin(app, ...options) } else if (__DEV__) { warn( `A plugin must either be a function or an object with an "install" ` + `function.` ) } return app }, mixin(mixin: ComponentOptions) { if (__FEATURE_OPTIONS_API__) { if (!context.mixins.includes(mixin)) { context.mixins.push(mixin) // global mixin with props/emits de-optimizes props/emits // normalization caching. if (mixin.props || mixin.emits) { context.deopt = true } } else if (__DEV__) { warn( 'Mixin has already been applied to target app' + (mixin.name ? `: ${mixin.name}` : '') ) } } else if (__DEV__) { warn('Mixins are only available in builds supporting Options API') } return app }, component(name: string, component?: Component): any { if (__DEV__) { validateComponentName(name, context.config) } if (!component) { return context.components[name] } if (__DEV__ && context.components[name]) { warn(`Component "${name}" has already been registered in target app.`) } context.components[name] = component return app }, directive(name: string, directive?: Directive) { if (__DEV__) { validateDirectiveName(name) } if (!directive) { return context.directives[name] as any } if (__DEV__ && context.directives[name]) { warn(`Directive "${name}" has already been registered in target app.`) } context.directives[name] = directive return app }, mount( rootContainer: HostElement, isHydrate?: boolean, isSVG?: boolean ): any { if (!isMounted) { const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) // store app context on the root VNode. // this will be set on the root instance on initial mount. vnode.appContext = context // HMR root reload if (__DEV__) { context.reload = () => { render(cloneVNode(vnode), rootContainer, isSVG) } } if (isHydrate && hydrate) { hydrate(vnode as VNode<Node, Element>, rootContainer as any) } else { render(vnode, rootContainer, isSVG) } isMounted = true app._container = rootContainer // for devtools and telemetry ;(rootContainer as any).__vue_app__ = app if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsInitApp(app, version) } return vnode.component!.proxy } else if (__DEV__) { warn( `App has already been mounted.\n` + `If you want to remount the same app, move your app creation logic ` + `into a factory function and create fresh app instances for each ` + `mount - e.g. \`const createMyApp = () => createApp(App)\`` ) } }, unmount() { if (isMounted) { render(null, app._container) if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsUnmountApp(app) } delete app._container.__vue_app__ } else if (__DEV__) { warn(`Cannot unmount an app that is not mounted.`) } }, provide(key, value) { if (__DEV__ && (key as string | symbol) in context.provides) { warn( `App already provides property with key "${String(key)}". ` + `It will be overwritten with the new value.` ) } // TypeScript doesn't allow symbols as index type // https://github.com/Microsoft/TypeScript/issues/24587 context.provides[key as string] = value return app } }) return app } }

有关Vue的一些基本原理和常识

2021.04.20

Vue

Vue的各部分是如何在一起工作的?各个API和配置项都代表什么含义?实际内部是怎样运行的? 有关这些基本原理和常识的问题,有结论都记在这里。 Vue中各种变量名形式的含义 DEV: 这种前后有两个下划线的大写变量名,表示环境标志。在一个环境中如果这个环境标志为真(truthy),则代表这里是相应的环境。(__DEV__代表开发环境) __v_isReadonly: 这种前面为__v_开头的属性,代表Vue的内部标志或属性 Vue组件 一个Vue组件就是一个JS对象,就像.vue文件中 组件对象(或组合式setup()函数)可以被传入component() API用来在各个组件内部注册(或全局注册); 一个.vue文件叫做单文件组件,包含<template>、<style>、<script>三部分,它就定义了一个Vue组件。这里<template>和<style>都是为了书写方便才这样显示,最后在使用import引入的时候,这两个标签内内容都会被添加到组件对象中(template和style属性),整个组件解析为一个纯JS的对象;` 一个手写JS Vue组件对象基本结构类似如下: { name: '', props: [], data(){}, methods: {}, components: {}, template: ``, style: '', emits: [], } Vue应用的本质就是层层嵌套的组件树。使用createApp()创建的app应用实例,使用的App.vue就是一个Vue组件,它叫做根组件。其他组件都是注册在它内部或者在app实例上全局注册的,在根组件内部使用。

Vue3插件探索:功能和原理

2021.04.20

Vue

Vue插件的功能和原理,如何编写一个Vue插件。 1. Vue插件的基本结构 Vue插件可以在createApp()创建的应用实例挂载前,使用app.use()方法应用在app上。 import somePlugin from 'somePlugin' const app = createApp(App); // 应用插件 app.use(somePlugin); app.mount(#app); Vue插件的export需要包含install方法,或者直接export一个函数作为install方法。Vue应用实例在use()这个插件的时候,会自动调用这个install方法。 插件的install()方法,在Vue应用实例调用的时候,会传入两个参数:实例对象app本身和用户自定义的配置对象option。 // vue-next\packages\runtime-core\src\apiCreateApp.ts : line90 // 插件安装函数的TS类型:传入App和options的函数 type PluginInstallFunction = (app: App, ...options: any[]) => any // 插件:直接是PluginInstallFunction函数,或者是带有install方法为PluginInstallFunction函数的对象。 export type Plugin = | PluginInstallFunction & { install?: PluginInstallFunction } | { install: PluginInstallFunction } // packages/runtime-core/src/apiCreateApp.ts : line160 // app.use 实际运行函数源码 use(plugin: Plugin, ...options: any[]) { if (installedPlugins.has(plugin)) { __DEV__ && warn(`Plugin has already been applied to target app.`) } else if (plugin && isFunction(plugin.install)) { installedPlugins.add(plugin) plugin.install(app, ...options) } else if (isFunction(plugin)) { installedPlugins.add(plugin) plugin(app, ...options) } else if (__DEV__) { warn( `A plugin must either be a function or an object with an "install" ` + `function.` ) } return app } 调用形式: install(app, option); 2.Vue插件的功能 官方解释:Vue插件是自包含的代码,通常向 Vue 添加全局级功能。它可以是公开 install() 方法的 object,也可以是 function. 插件在被use()加载的时候,运行了内部的install()方法,传入了应用实例app本身作为第一个参数。 因此,插件install方法内可以访问应用实例本身的任何属性,包括: app.component注册全局组件; app.config进行应用全局配置: app.config.globalProperties注册全局属性; app.directive注册全局指令; app.provide为全局子组件注入值;

Vue3源码阅读笔记——响应式reactivity部分II:reactive,ref等

2021.04.19

Vue

尝试探索Vue3源码: 响应式reactivity部分: reactive,ref等。 原项目目录: /packages/reactivity 1. reactive()函数 /* * Creates a reactive copy of the original object. * * The reactive conversion is "deep"—it affects all nested properties. In the * ES2015 Proxy based implementation, the returned proxy is **not** equal to the * original object. It is recommended to work exclusively with the reactive * proxy and avoid relying on the original object. * * A reactive object also automatically unwraps refs contained in it, so you * don't need to use `.value` when accessing and mutating their value: * * ```js * const count = ref(0) * const obj = reactive({ * count * }) * * obj.count++ * obj.count // -> 1 * count.value // -> 1 * */ // 对不同的响应式对象,分别在内部声明了不同的weakMap用来储存。 export const reactiveMap = new WeakMap<Target, any>() export const shallowReactiveMap = new WeakMap<Target, any>() export const readonlyMap = new WeakMap<Target, any>() export const shallowReadonlyMap = new WeakMap<Target, any>() // 常规reactive()函数定义如下,返回的是非shallow非readonly的响应式对象。 // 类似地,还有shallowReactive()、readonly()、shallowReadonly(),用来创建不同类型的响应式对象。 export function reactive<T extends object>(target: T): UnwrapNestedRefs<T> export function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target } // 这里返回了创建的响应式对象。 return createReactiveObject( target, false, // 传入对应的handler,对于普通reactive对象就是mutableHandler,这些handler决定了对target执行各种操作时发生的行为。 mutableHandlers, mutableCollectionHandlers, reactiveMap ) } // 这里传入的target是Target类型: // 它的定义:也可以是原始object,但可选具有以下四个内部属性:__v_skip,__v_isReactive,__v_isReadonly,__v_raw。 export const enum ReactiveFlags { // SKIP为ture,代表这个对象永远不会成为reactive对象 SKIP = '__v_skip', // IS_REACTIVE为true,代表为reactive对象 IS_REACTIVE = '__v_isReactive', // IS_READONLY为true,代表这是只读reactive对象 IS_READONLY = '__v_isReadonly', // RAW中记录了reactive proxy对象代理的原始值 RAW = '__v_raw' } export interface Target { [ReactiveFlags.SKIP]?: boolean [ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.RAW]?: any } function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any> ) { // 非对象的原始值不能用reactive()转化 if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } // 如果target本身就是一个reactive proxy,则直接返回。 // exception: calling readonly() on a reactive object if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target } // 已经注册过的target proxy,直接获取并返回。 const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } // 根据不同的target类型,创建设置不同的proxy handler,返回对应proxy。 // 这些handler内部,定义了对target各种操作的拦截器,在各个调用时机执行trace或trigger方法,用来实现target的响应式。 // target就是传入用来创建响应式的原始变量。 // // Vue定义的target类型有三种:COMMON/COLLECTION/INVALID // COMMON: object和array // COLLECTION: map,set,weakmap,weakset // 其他都是INVALID:直接返回原target。 // const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { return target } // 这里使用了不同的handler,创建proxy,然后返回。 const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ) // 在对应的map记录新生成的proxy。 然后返回proxy proxyMap.set(target, proxy) return proxy } 2. 用于创建响应式对象proxy的拦截器:baseHandlers和collectionHandlers // 普通reactive()函数的proxy handler // 只捕获五种操作:Get,set,deleteProperty,has和ownkeys export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys } // 下面拿一个最常见的get捕获器,进行说明。 // 普通reactive()函数proxy get拦截器的定义。 const get = createGetter() function createGetter(isReadonly = false, shallow = false) { // 这里设置了读取(GET)一个Target的某一个key时所进行的操作。 return function get(target: Target, key: string | symbol, receiver: object) { // get的target属性是内部属性:ReactiveFlags.IS_REACTIVE、ReactiveFlags.IS_READONLY、ReactiveFlags.RAW时的返回值。 // ReactiveFlags.RAW返回原始对象target, 其他按情况返回对应布尔值。 if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if ( // Marswiz: receiver是捕获器中,传入的调用这个捕获器的原始对象。 // 这里的含义是:只有从各个Map中注册过的proxy get这个raw值(也就是通过createReactive、createReadonly这类工厂函数创建的proxy). // 才返回原始target,其他proxy对象即使有ReactiveFlags.RAW属性,也用这个get捕获器,也不会返回target,因为vue不认这个对象,它不是从Vue内部创建的。 key === ReactiveFlags.RAW && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target } // !!注意:如果target是数组对象,有特殊规则限制! const targetIsArray = isArray(target) // 对于数组的target,如果get的key是arrayInstrumentations中注册的,则执行arrayInstrumentations内对应的方法。 // arrayInstrumentations主要拦截数组的三种操作: 'includes', 'indexOf', 'lastIndexOf' // 这三种操作因为对数组元素的身份敏感,如果一个数组内还有嵌套的reactive元素,那么在执行这些操作的时候有可能出现错误结果。 // arrayInstrumentations 保证了无论是直接按照内部reactive()对象执行这些方法还是通过原始值RawValue,都能获取到正确的结果。 // 同时,在执行这些方法的时候,如果数组target本身没有被track,则按数组的索引字符串为key进行track。 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } // 获取Target对应key的目标值res。 const res = Reflect.get(target, key, receiver) // 内置symbol作为key,或者没有被track的key,都直接返回目标值。 if ( isSymbol(key) ? builtInSymbols.has(key as symbol) : isNonTrackableKeys(key) ) { return res } // ★★★★★★★ 关键操作 ★★★★★★★ // 非只读target,对target.key进行track。 if (!isReadonly) { track(target, TrackOpTypes.GET, key) } // 浅响应式对象,直接返回目标结果。 if (shallow) { return res } // 如果目标值本身直接是ref对象,直接返回Ref.value // !!!注意这里有一个例外:target是Array数组类型,且key是整数索引时,默认直接返回ref而不是ref.value; // 也就是说,在响应式数组中,保存的ref,在执行get操作的时候,返回值仍然是ref,需要进行.value操作才能获取到结果。 if (isRef(res)) { // ref unwrapping - does not apply for Array + integer key. const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return shouldUnwrap ? res.value : res } // 如果目标值是对象类型,按是否只读,转化为 只读响应式对象 or 普通响应式对象 返回。 if (isObject(res)) { // Convert returned value into a proxy as well. we do the isObject check // here to avoid invalid value warning. Also need to lazy access readonly // and reactive here to avoid circular dependency. return isReadonly ? readonly(res) : reactive(res) } // 其他默认情况,都直接返回res目标值。 return res } } readonly proxy的拦截器: // Marswiz: 只读对象的proxy捕获器 // 只拦截get/set/deleteProperty三个操作。 export const readonlyHandlers: ProxyHandler<object> = { get: readonlyGet, // 可以看到,这里Set和deleteProperty都不进行任何操作,且开发环境会报错。 set(target, key) { if (__DEV__) { console.warn( `Set operation on key "${String(key)}" failed: target is readonly.`, target ) } return true }, deleteProperty(target, key) { if (__DEV__) { console.warn( `Delete operation on key "${String(key)}" failed: target is readonly.`, target ) } return true } } 3. ref()函数 // ref的几个重载: export function ref<T extends object>(value: T): ToRef<T> export function ref<T>(value: T): Ref<UnwrapRef<T>> export function ref<T = any>(): Ref<T | undefined> export function ref(value?: unknown) { return createRef(value) } // createRef(): 可以看到这里是返回了一个RefImpl类的实例。 function createRef(rawValue: unknown, shallow = false) { if (isRef(rawValue)) { return rawValue } return new RefImpl(rawValue, shallow) } // RefImpl Class类 class RefImpl<T> { private _value: T public readonly __v_isRef = true constructor(private _rawValue: T, public readonly _shallow = false) { // 根据是否shallow(浅响应式),来确定内部_value的值:浅的话就直接传入原始值,非浅就把原始值深度转化为响应式再传入。 this._value = _shallow ? _rawValue : convert(_rawValue) } // 这里是ref响应式的核心内容: // ① 在get这个ref.value的时候,对这个响应式对象的原始对象value属性进行GET形式的Track; // ② 在set这个ref.value的时候,对新的值newVal和旧的值value进行比较,如果有改动,就将内部的_rawValue修改为newVal,然后将_value修改为响应式的newVal,然后用newVal对原始对象的value属性进行SET trigger。 // // ★★★ // toRaw的函数定义:这里使用了逻辑表达式进行递归,很难理解。 // 结果就是通过传入对象observed,层层递归找到最内层的[ReactiveFlags.RAW]值返回。如果对象observed本身就不带有[ReactiveFlags.RAW]值,说明本身就是原始对象,就返回对象本身。 // export function toRaw<T>(observed: T): T { // return ( // (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed // ) // } get value() { track(toRaw(this), TrackOpTypes.GET, 'value') return this._value } set value(newVal) { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : convert(newVal) trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal) } } }

Vue3源码阅读笔记——响应式reactivity部分I:effect/track/trigger

2021.04.18

Vue

尝试探索Vue3源码: 响应式reactivity部分。 原项目目录: /packages/reactivity 1. effect.ts 1.1 定义dep、keyToDepMap和targetMap的接口类型 可以看到,这里: dep被定义为由ReactiveEffect组成的Set集合; KeyToDepMap被定义为<any,Dep>类型构成的Map; targetMap为一WeakMap实例,其内部元素类型为<any, KeyToDepMap>。 执行顺序是: targetMap -> keyToDepMap -> dep type Dep = Set<ReactiveEffect> type KeyToDepMap = Map<any, Dep> const targetMap = new WeakMap<any, KeyToDepMap>() 1.2 reactiveEffect接口定义 export interface ReactiveEffect<T = any> { (): T //直接执行的定义 _isEffect: true // 判断是否为effect的标志,reactiveEffect永远为true. id: number // reactiveEffect 的 ID active: boolean // reactiveEffect是否为活动状态 raw: () => T // reactiveEffect内effect的原始函数 deps: Array<Dep> // 保存reactiveEffect在那些Dep中注册的信息,用<dep>类型的Array存放 options: ReactiveEffectOptions // reactiveEffect的选项配置 allowRecurse: boolean // 是否允许递归reactiveEffect? } export interface ReactiveEffectOptions { lazy?: boolean // reactiveEffect是否是懒执行的 scheduler?: (job: ReactiveEffect) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void allowRecurse?: boolean } 1.3 createReactiveEffect函数 // effectStack为当前所有ReactiveEffect组成的数组,默认初始值为空; const effectStack: ReactiveEffect[] = [] // activeEffect声明,取值类型为ReactiveEffect或undefined,没设置初始值(所以是undefined); let activeEffect: ReactiveEffect | undefined function createReactiveEffect<T = any>( fn: () => T, // Effect记录的原始操作fn options: ReactiveEffectOptions // 配置选项 ): ReactiveEffect<T> { // 返回的effect:是一个函数,可以被执行。 const effect = function reactiveEffect(): unknown { // 这里定义了返回的effect这个函数,在后续被执行时候,发生的事情。 // 当这个effect不是active激活状态,按options.scheduler是否为true,决定返回effect为undefined或执行fn(); if (!effect.active) { return options.scheduler ? undefined : fn() } // 当执行时的effectStack栈中,还没有包含这个Effect时:(已包含不进行任何操作) // 1. 先在所有Deps中彻底清除这个effect,以防重复注册; // 2. 尝试重新track这个effect,并且把它重新push到effectStack,并且设置它为当前的activeEffect,然后执行effect原始方法fn(); if (!effectStack.includes(effect)) { cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn() } finally { effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect // effect函数接口类型为ReactiveEffect,也就是effect函数上具有ReactiiveEffect接口定义的各种属性(函数属性)。 // 下面设置了这个返回的effect函数的函数属性。 effect.id = uid++ // 每次uid+1,effect从0开始每个有自己的id。 effect.allowRecurse = !!options.allowRecurse // 按配置决定effect是否可递归。 effect._isEffect = true effect.active = true effect.raw = fn // effect记录的原始方法fn effect.deps = [] // 初始Effect不注册在任何dep中 effect.options = options // effect也记录了创建时的配置对象信息 return effect } 1.4 effect相关函数定义 export function effect<T = any>( // 传入两个参数: 一个是fn函数,是effect需要运行的主体函数; // 另一个是options,默认值为空对象,是ReactiveEffectOptions类型的配置对象,为这个effect进行配置。 fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { //返回值是reactiveEffect接口类型的 // fn可能本身就是effect类型,因为这里需要原始fn函数,所以读取它的原始函数 if (isEffect(fn)) { //如果fn是一个Effect,则读取它的原始函数 fn = fn.raw } const effect = createReactiveEffect(fn, options) //使用fn和options配置调用createReactiveEffect,创建effect(一个带有ReactiveEffect接口类型的函数)用于返回值。 if (!options.lazy) { //非懒执行的effect,创建后立即执行一次 effect() } return effect } 停止一个effect函数: export function stop(effect: ReactiveEffect) { // 当effect为Active状态时: if (effect.active) { // 先彻底清除掉这个effect cleanup(effect) // 如果Effect有手动停止函数onStop,就运行一下。 if (effect.options.onStop) { effect.options.onStop() } // 让这个effect的Active属性为false effect.active = false } } // cleanup function: 用来彻底清除一个effect function cleanup(effect: ReactiveEffect) { // 1. 先提取包含这个effect的所有dep const { deps } = effect // 2. 遍历清除所有dep内的这个effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } // 3. 手动销毁deps对象,腾出内存; deps.length = 0 } } 1.5 track相关函数 // 设置当前是否可以进行Track操作的总开关:shouldTrack let shouldTrack = true // 记录track的历史 const trackStack: boolean[] = [] // 终止Track方法 export function pauseTracking() { trackStack.push(shouldTrack) shouldTrack = false } // 开启track方法 export function enableTracking() { trackStack.push(shouldTrack) shouldTrack = true } // 返回上一个track操作状态 export function resetTracking() { const last = trackStack.pop() shouldTrack = last === undefined ? true : last } // 定义Track函数: 用于track一个effect。 // 传入的参数有: 1. target: track的对象 2. type: track的操作类型,定义如下 3. key: track的target的key // export const enum TrackOpTypes { // GET = 'get', // HAS = 'has', // ITERATE = 'iterate' // } export function track(target: object, type: TrackOpTypes, key: unknown) { // 这里先进行如下两个判断: // ① !shouldTrack 为判定当前不允许track操作 // ② activeEffect === undefined 为判定当前没有活跃状态的effect,当然这时就无法进行track,因为无effect可track. if (!shouldTrack || activeEffect === undefined) { return } // 获取需要track的target对象对应的depsMap,没有则创建新map。 let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } // 在depsMap中通过key获取到对应的dep,没有则创建 let dep = depsMap.get(key) if (!dep) { depsMap.set(key, (dep = new Set())) } // 当dep中还没注册(track)当前活跃的effect(activeEffect时),执行track操作。具体看下面每一步。 if (!dep.has(activeEffect)) { // 1. 在Dep中添加这个effect, dep.add(activeEffect) // 2. 在当前track的effect中,添加注册dep的信息 activeEffect.deps.push(dep) // △3. 在开发环境:当当前的effect配置中具有onTrack方法时,表明开发者需要在此时执行这个onTrack操作。 // 此时传入相关的对象,执行onTrack方法。 if (__DEV__ && activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type, key }) } } } 1.6 trigger函数 export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { // 获取target对应的depsMap const depsMap = targetMap.get(target) if (!depsMap) { // never been tracked: 从未被track的effect,不需做任何操作直接返回。 return } // 声明需要被触发的所有Effect集合:effects const effects = new Set<ReactiveEffect>() // 添加effect到effects的函数,传入一个dep Set。 const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { if (effect !== activeEffect || effect.allowRecurse) { effects.add(effect) } }) } } // 当触发操作类型是CLEAR时,重新触发Target全部的effect。 if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target depsMap.forEach(add) } else if (key === 'length' && isArray(target)) { //这里是修改数组长度对应的触发操作 depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { add(dep) } }) } else { // schedule runs for SET | ADD | DELETE // 其他操作 if (key !== void 0) { add(depsMap.get(key)) } // also run for iteration key on ADD | DELETE | Map.SET switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) if (isMap(target)) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // new index added to array -> length changes add(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) if (isMap(target)) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { add(depsMap.get(ITERATE_KEY)) } break } } const run = (effect: ReactiveEffect) => { if (__DEV__ && effect.options.onTrigger) { effect.options.onTrigger({ effect, target, key, type, newValue, oldValue, oldTarget }) } if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() } } effects.forEach(run) }

axios基本操作

2021.04.12

Axios

Ajax

基于Promise和ajax的前端异步请求库 axios简介 Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。 特点: 在浏览器环境中,创建 XMLHttpRequests 在 node.js 环境中,创建内置 http 请求 支持 Promise API 可拦截请求和响应 转换请求数据和响应数据 可取消请求 自动转换 JSON 数据 客户端支持防御 XSRF 发送HTTP请求的方式:直接发送和创建实例 axios可以直接传入配置对象,从而发送请求,返回带结果的promise对象。 axios也可以通过其自身的create()方式,创建一个axios实例,来发送请求,二者基本上是一样的。 axios实例可以创建多个,但是axios只能每次请求独立配置,为每一类请求创建独立的实例是更好的选择。 import axios from 'axios'; axios({ method: 'get', url: 'http://localhost:3000', }); // 等同于 const get = axios.create({ method: 'get', url: 'http://localhost:3000', }); 默认配置的设定 全局axios和axios实例均可以配置默认项,使用对应的.defaults.[prop]属性配置。 例如,想为默认请求配置baseURL为’http://localhost:3000’, 则需要: // 全局axios的默认BaseURL配置 axios.defaults.baseURL = 'http://localhost:3000'; // axios实例abc的默认BaseURL配置 const abc = axios.create({}); abc.defaults.baseURL = 'http://localhost:3000'; 默认配置会按优先级进行相互覆盖。其优先级从大到小的顺序是: 请求的 config 参数; 实例、全局的 defaults 属性; 在 lib/defaults.js 找到的库的默认值; 配置项的具体内容 配置项中,只有url是必须的,其他都是可选配置。 { // `url` 是用于请求的服务器 URL url: '/user', // `method` 是创建请求时使用的方法 method: 'get', // default // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。 // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL baseURL: 'https://some-domain.com/api/', // `transformRequest` 允许在向服务器发送前,修改请求数据 // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法 // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream transformRequest: [function (data, headers) { // 对 data 进行任意转换处理 return data; }], // `transformResponse` 在传递给 then/catch 前,允许修改响应数据 transformResponse: [function (data) { // 对 data 进行任意转换处理 return data; }], // `headers` 是即将被发送的自定义请求头 headers: {'X-Requested-With': 'XMLHttpRequest'}, // `params` 是即将与请求一起发送的 URL 参数 // 必须是一个无格式对象(plain object)或 URLSearchParams 对象 params: { ID: 12345 }, // `paramsSerializer` 是一个负责 `params` 序列化的函数 // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/) paramsSerializer: function(params) { return Qs.stringify(params, {arrayFormat: 'brackets'}) }, // `data` 是作为请求主体被发送的数据 // 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH' // 在没有设置 `transformRequest` 时,必须是以下类型之一: // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams // - 浏览器专属:FormData, File, Blob // - Node 专属: Stream data: { firstName: 'Fred' }, // `timeout` 指定请求超时的毫秒数(0 表示无超时时间) // 如果请求话费了超过 `timeout` 的时间,请求将被中断 timeout: 1000, // `withCredentials` 表示跨域请求时是否需要使用凭证 withCredentials: false, // default // `adapter` 允许自定义处理请求,以使测试更轻松 // 返回一个 promise 并应用一个有效的响应 (查阅 [response docs](#response-api)). adapter: function (config) { /* ... */ }, // `auth` 表示应该使用 HTTP 基础验证,并提供凭据 // 这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头 auth: { username: 'janedoe', password: 's00pers3cret' }, // `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream' responseType: 'json', // default // `responseEncoding` indicates encoding to use for decoding responses // Note: Ignored for `responseType` of 'stream' or client-side requests responseEncoding: 'utf8', // default // `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称 xsrfCookieName: 'XSRF-TOKEN', // default // `xsrfHeaderName` is the name of the http header that carries the xsrf token value xsrfHeaderName: 'X-XSRF-TOKEN', // default // `onUploadProgress` 允许为上传处理进度事件 onUploadProgress: function (progressEvent) { // Do whatever you want with the native progress event }, // `onDownloadProgress` 允许为下载处理进度事件 onDownloadProgress: function (progressEvent) { // 对原生进度事件的处理 }, // `maxContentLength` 定义允许的响应内容的最大尺寸 maxContentLength: 2000, // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte validateStatus: function (status) { return status >= 200 && status < 300; // default }, // `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目 // 如果设置为0,将不会 follow 任何重定向 maxRedirects: 5, // default // `socketPath` defines a UNIX Socket to be used in node.js. // e.g. '/var/run/docker.sock' to send requests to the docker daemon. // Only either `socketPath` or `proxy` can be specified. // If both are specified, `socketPath` is used. socketPath: null, // default // `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项: // `keepAlive` 默认没有启用 httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }), // 'proxy' 定义代理服务器的主机名称和端口 // `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据 // 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。 proxy: { host: '127.0.0.1', port: 9000, auth: { username: 'mikeymike', password: 'rapunz3l' } }, // `cancelToken` 指定用于取消请求的 cancel token // (查看后面的 Cancellation 这节了解更多) cancelToken: new CancelToken(function (cancel) { }) } 用别名创建请求 这些别名方法也可以创建请求,相当于直接配置了method选项。 各个axios实例也有自己的别名函数。 axios.request(config) axios.get(url[, config]) axios.delete(url[, config]) axios.head(url[, config]) axios.options(url[, config]) axios.post(url[, data[, config]]) axios.put(url[, data[, config]]) axios.patch(url[, data[, config]]) 响应对象的格式 { // `data` 由服务器提供的响应 data: {}, // `status` 来自服务器响应的 HTTP 状态码 status: 200, // `statusText` 来自服务器响应的 HTTP 状态信息 statusText: 'OK', // `headers` 服务器响应的头 headers: {}, // `config` 是为请求提供的配置信息 config: {}, // 'request' // `request` is the request that generated this response // It is the last ClientRequest instance in node.js (in redirects) // and an XMLHttpRequest instance the browser request: {} } 拦截器、取消请求 axios也可以对发送请求前、收到响应后的节点实施拦截,运行自定义程序。也可以在响应完成前随时取消请求。 具体方法看文档就行。

Vue3响应式原理基本概念及手动实现

2021.04.12

Vue

Vue3的响应式原理 一、 Vue3响应式基本实现原理 假设reacObj是Vue中的一个响应式对象,它具有属性a和b; Vue3使用reactive(obj)函数,创建一个响应式的对象,返回一个obj的代理对象Proxy。 reactive内部使用track、effect和trigger三个关键方法来描述响应式过程: track(obj, property):对obj中的property进行响应式追踪; effect: 依赖某一个响应式property的变量的计算方法函数; trigger(obj, property):对依赖obj.property的所有变量计算方法进行重新调用。 基本流程是: reacitive函数使用Proxy的捕获器,对响应式对象property的get和set操作进行捕获。 当get的时候,对属性进行track操作; 当set的时候,对属性进行trigger操作。 解释: 当get操作被捕获,说明有变量计算需要访问这个响应式属性,也就是依赖这个属性,所以要对这次get操作的effect进行追踪,添加到dep中以便后续进行响应式更新。 当set操作被捕获,说明有变量被更改,那么需要找到这个变量的dep依赖集,并运行内部的全部方法,来更新依赖它的全部变量。 1.1 对于响应式对象内部属性pbj.prop 对于属性a,如果我们用一个集合Set记录了依赖a的所有变量和它们的计算方法函数(这些计算方法函数叫做effect),在更新a的时候,将这些方法一一重新调用一遍,就可以实现a更改之后依赖它的变量的实时更新,也就是a属性成为了响应式的。 Vue中,为每个响应式对象内部属性所建立的Set对象称为一个dep(PS:dependency,依赖集),只要在修改任何响应式变量后,对应的依赖集dep内方法全部运行一次,就能实现其他变量的更新。 使用Set对象的理由是:它可以保证依赖集内部没有重复。 1.2 对于响应式对象本身obj 一个响应式对象本身可能具有多个属性,需要为它们每个单独建立一个dep依赖集,然后用另一个映射Map将对象属性和dep一一对应。 当track另一个对象属性property2的时候,就把这个property2也添加到映射Map里,然后把所有依赖property2的变量计算方法添加到新的对应dep中,并在Map中与property2对应起来。 这样,这个Map就记录了这个对象obj的全部属性的依赖信息,并可以根据属性更新其对应的那一部分依赖变量。这个Map变量名为depsMap。 1.3 对于整个Vue应用实例中所有响应式对象 一个Vue应用实例,可能声明有多个响应式对象,每一个响应式对象及其内部属性都应该被记录并追踪(track)。 整个应用实例使用一个WeakMap,记录应用实例中的每个响应式对象。这个WeakMap为targetMap。 1.4 effect(fn)的实现 假设一个fn在执行的过程中,引用了某一响应式对象obj的属性prop; 这个fn就是一个依赖(effect),需要在obj.prop被重新赋值的时候,再次调用来保证更新; 因为需要在获取响应式对象的属性过程中,收集这个依赖fn,因此必须获取到当前执行函数fn的引用; 解决方法是:把函数作为effect()的参数包装起来执行,然后在包装器里面获取到函数的引用,进行依赖收集。 1.5 track(obj, property)的实现 track(obj, property)运行后: 从targetMap中查找obj,如果没有就创建一个新的Map并赋值给obj对应的值,并作为depsMap返回。如果已经存在,则找到对应的depsMap; 在找到的depsMap中,查找property。如果没有则创建一个新的Set,赋值给property对应的值作为dep,并返回这个dep。如果已经存在,则返回找到的Dep; 在dep中加入全部effect。 1.6 trigger(obj, property)的实现 trigger(obj, property)运行后: 先从targetMap中查找obj,然后找到对应的depsMap; 从depsMap中查找property,找出对应的dep; 对dep中每一个effect进行执行。 弱映射WeakMap:只能储存对象,且当对象在其他地方没有引用的时候,WeakSet内的对象会被垃圾回收机制识别并回收。 1.7 reactive()函数:将对象变成响应式 Vue使用reactive(obj)函数,将一个对象变成响应式对象。(设置捕获器,返回Proxy对象即可) 官方教程代码如下:(示例,并非源码) 二、为什么要使用Proxy,相比Object.defineProperty有什么好处? Object.defineProperty只能劫持对象中已经存在的属性,动态添加的新属性无法劫持(Proxy可以); Object.defineProperty不能监听数组变化(Proxy可以); Proxy存在兼容性问题,且无法完全polyfill. 三、手动实现Vue响应式代码 // Mars 2021.08 class Dep { constructor() { this.dep = new Set(); } trigger() { for (let e of this.dep) { e(); } } add(e) { this.dep.add(e); } } let activeEffect = null; const objsMap = new WeakMap(); function effect(fn) { activeEffect = fn; fn(); activeEffect = null; } function track(obj, prop) { if (activeEffect !== null) { if (!objsMap.has(obj)) objsMap.set(obj, new Map()); let depsMap = objsMap.get(obj); if (!depsMap.has(prop)) depsMap.set(prop, new Dep()); let dep = depsMap.get(prop); dep.add(activeEffect); } } function trigger(obj, prop) { if (objsMap.has(obj)) { if (objsMap.get(obj).has(prop)) { objsMap.get(obj).get(prop).trigger(); } } } function reactive(obj) { const handler = { get(obj, prop) { track(obj, prop); return Reflect.get(obj, prop); }, set(obj, prop, value) { let oldValue = Reflect.get(obj, prop); if (oldValue !== value) { Reflect.set(obj, prop, value); trigger(obj, prop); } return true; } }; return new Proxy(obj, handler); } // test let cal = reactive({ a: 12, b: 13 }); effect(() => { console.log(cal.a + cal.b); });

Webpack和Webpack-dev-server配置中publicPath、path、contentBase等的区别理解

2021.04.11

Webpack

容易混还记不住。 output中的 path 打包后文件的输出目录,规定必须是绝对路径。 常用node.js的path模块解析成绝对路径传入。 output: { path: path.resolve(__dirname, './dist/'); } // 输出文件全部在本项目根目录的/dist文件夹内。 output中的 publicPath 这里的publicPath的含义是: 资源请求的公共路径。 output.publicPath的定义对资源请求非常重要,它代表了资源请求时相对的基础路径。 Webpack5 之后,publicPath必须显式定义,没有默认值。 这里的资源请求主要有三个方面: html中标签内通过href、src等路径请求的资源; css中通过url()请求的资源; js中通过URI请求的外部资源; 这些资源在webpack打包后的基础路径,都是这个publicPath路径。比如: 如果设置output.publicPath为’./’,则webpack前: html文档中<img src='assets/img/cat.png'>:请求的实际路径是./assets/img/cat.png; 使用webpack-dev-server配合html-webpack-plugin的时候,自动引入的文件会加上publicPath前缀 CSS中的背景图片:background-image: url(‘assets/spinner.gif’);实际上是./assets/spinner.gif; js中,使用import请求的资源和js文件自身有关,不受publicPath影响(仍需使用相对js文件自身的路径); devServer中的 publicPath 这里的publicPath的含义是: devSever服务对外开放的路径。 devServer服务在开启后并不向磁盘中输出生成文件,而是将结果保存在内存中。这里devServer的 publicPath代表的是通过哪个目录,可以访问devServer服务,也就是提供生成在内存中的结果文件。 注: devServe.publicPath 如果未手动设置,默认自动采用output.publicPath; devServe.publicPath 需要前后都以/结尾。 比如:设置了port为3000,open为true,devServer.publicPath为 ‘/’,则: 服务部署在http://localhost:3000/目录,一打开(自动打开)这个目录就可以看到serve结果。 如果设置devServer的publicPath为’/dist/’,则: 服务开启在http://localhost:3000/dist/目录,自动打开的为http://localhost:3000,需要手动在地址栏输入/dist/进入目录才能看到服务结果。 devServer中的 contentBase 官方说明: 告诉服务器内容的来源。仅在需要提供静态文件时才进行配置,默认情况下,它将使用当前的工作目录(根目录)来提供内容。 建议使用绝对路径。 设置不受webpack控制(不由 webpack 打包生成的)的静态资源文件的来源地址,默认是项目根目录。

Webpack基本配置与功能用途注释

2021.04.09

Webpack

Webpack基本功能配置。 生产环境基本配置 loader CSS、SCSS资源 MiniCssExtractPlugin.loader: 将css文件单独输出,然后html-webpack-plugin自动引入到输出html。 css-loader: 载入CSS代码; postcss-loader: 对CSS进行兼容性处理,根据package.json里的browserlist约定,自动添加CSS前缀; sass-loader:载入识别SASS、SCSS代码; 图片资源 Webpack 4之前: url-loader (依赖file-loader): 识别除了html文档里直接引用的url资源,按设定的限制大小,自动选择打包成base64形式还是独立文件形式。 Webpack5 已经不建议使用。 应该使用新特性:asset modules html内媒体资源 html-loader: 其他资源(如字体) file-loader 插件 const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin'); const ESLintWebpackPlugin = require('eslint-webpack-plugin'); module.exports = { mode: 'production', entry: './index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, './dist/'), }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', ], }, { test: /\.s[ca]ss$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader', ], }, { test: /\.(png|jpe?g|gif)$/, use: [ { loader: 'url-loader', options: { limit: 1024 * 8, }, }, ], }, // 为HTML文档中的媒体资源:比如img、video、audio等引用的资源进行加载并打包。 { test: /\.html$/, use: { loader: 'html-loader', options: { esModule: false, }, }, }, // 为js代码执行兼容性处理,使用babel-loader @babel/core @babel/preset-env core-js // 它们根据package.json中的browserlist约定,对代码进行兼容性处理。 { test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env', { targets: { ie: 10, chrome: 56, }, // 设定从corejs中按需加载polyfill,而不是一次性加载全部。 useBuiltIns: 'usage', // 指定core-js版本 corejs: { version: 3, }, }], ], }, }, ], }, ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, './index.html'), filename: 'index.html', }), new MiniCssExtractPlugin({ filename: 'bundle.css', }), new OptimizeCssAssetsWebpackPlugin(), new ESLintWebpackPlugin({ fix: true, }), ], // webpack-dev-server 配置,打包后根文件目录和端口号。 devServer: { contentBase: path.resolve(__dirname, './dist'), port: 3000, // 完成后打开页面 open: true, // 开启压缩 compress: true, // 开启HMR功能 hot: true, }, };

前端性能优化

2021.04.09

Performance

记录前端性能优化的方向、方式方法。 前端性能评价指标 白屏时间 白屏时间(FP:First Paint):从浏览器响应用户输入的url,到页面开始显示内容的时间(解析完HTML文档的<head>部分,开始渲染<body>); 白屏时间的计算: <head> <!-- other content ... --> <script> // 在head标签的最下方,获取白屏时间,因为接下来即将解析body元素。 // performance.timing.navigationStart是浏览器响应用户url输入的时间节点。 const FP = Date.now() - performance.timing.navigationStart; </script> </head> 首屏时间 首屏时间(FCP:First Contentful Paint):从浏览器响应用户输入的url,到首屏渲染完成的时间(完全渲染完成,包括内部图片等置换元素的请求、解析)。 首屏时间计算方法: 估计首屏截止位置,在对应位置设置<script>标签获取; 最慢图片加载法:为首屏的图片添加load事件,获取最慢加载完成时间,减去起始时间得到首屏时间; 异步请求法:在首屏内容的异步请求回调中,获取时间节点计算首屏时间。 如何选择 首屏时间相对更为准确地反映了用户体验。 大型复杂页面优先选择首屏时间,简单页面二者差别不大,可随意选择。 前端性能检测 优化的起点,是对当前的页面进行性能检测,查看有哪些优化点,是否需要优化。 性能分为加载性能和运行时性能。 加载性能通过首屏、白屏时间来度量。 运行时性能,指网页在用户正常交互过程中,是否出现卡顿、掉帧等情况。 运行时性能可以通过chrome的performance面板进行监控&录制获得。 二、前端性能优化主要方向 静态资源优化:html、CSS、JS、图片、字体文件等,包括各种打包压缩策略; 页面渲染策略; 原生APP的优化; 网络性能优化; 研发开发流程优化; 性能监控及评价; 三、常用方法 减少 HTTP 请求 使用 HTTP2 考虑使用服务端渲染 SSR 静态资源使用 CDN 将 CSS 放在文件头部,JavaScript 文件放在底部 使用字体图标 iconfont 代替图片图标 善用缓存,不重复加载相同的资源 压缩文件 图片优化 通过 webpack 按需加载代码,提取第三库代码,减少 ES6 转为 ES5 的冗余代码 减少重绘重排 使用事件委托 注意程序的局部性 if-else 改为 switch 查找表 修改JS,避免页面卡顿 使用 requestAnimationFrame 来实现视觉变化 使用 Web Workers 使用位操作不要覆盖原生方法 降低 CSS 选择器的复杂性 使用 flexbox 而不是较早的布局模型 使用 transform 和 opacity 属性更改来实现动画 合理使用规则,避免过度优化 静态资源优化 一、图片优化 1.正确选择图片格式 jpg、jpeg: 有损储存,适合颜色丰富的照片、彩色图; png: 无损储存,体积大,支持透明度,适合透明、线条、图标,边缘清晰,大块同色区域,颜色数少但是需要半通明的场景; gif: 8位色,不适合高保真彩色照片,适合动画、图标; webp: 现代化格式,可提供有损或无损压缩,最多256色,不适合彩色图片,适合图形和半透明图像; 2. 压缩图片体积 压缩png图片: node-pngquant-native工具,可在npm安装; 压缩jpg图片: jpegtran工具; 压缩gif图片: gifsicle工具; 3. 根据设备选用不同分辨率的图片 在移动端加载小图片。 4. 先采用图片占位符,后续再慢慢加载图片 低质量图片占位符LQIP:npm install lqip; SVG图像占位符SQIP:npm install sqip; 5. 用其他方式代替图片 Data URI: 比如Base64形式; Web Font; 雪碧图; 6. 服务端图片自动优化 按请求自动裁剪、调节分辨率、AI抠图、质量控制等。 二、HTML优化 1. 精简HTML代码 减少HTML嵌套; 减少DOM节点数; 减少无语义的代码; 压缩:删除注释、多余空格和缩进等; 使用相对路径的url; 文件放在合适的位置 CSS引入在头部进行; Js引入在末尾进行; 增强用户体验 始终设置favicon图标; 增加首屏必要的CSS和JS:为需要加载的资源设置一个背景色,然后用js控制资源加载完成后去掉,可以减少用于白屏等待的时间。 三、CSS优化 1. 提升CSS渲染性能 谨慎使用耗费计算资源的属性。比如:nth-child,position:fixed定位; 减少样式嵌套层数; 避免占用CPU过多的属性值:比如left: -99999px; 避免复杂动画、透明度转换等; 2. CSS文件优化 尽量使用外联CSS文件,充分利用浏览器缓存; 避免使用@import: 因为CSS中引入@import相当于整合所有CSS文件为一个,必须所有import的CSS文件都加载完成,才能开始渲染,相比于独立文件加载渲染更慢。 CSS代码压缩; 3. 中文字体优化、合理使用Web Fonts 中文字体体积太大,使用fontmin对常用3500汉字进行字体裁剪,然后再转换为woff格式,可以大幅缩减字体体积; 字体保存在CDN上; 字体以Base64形式保存在CSS,并且通过localStorage缓存; Google相关服务选择国内托管;

前端防抖(debounce)和节流(throttle)

2021.03.31

Frontend

防抖和节流,在本质上都是为了防止函数被多次频繁触发,采取的保护措施。 防抖和节流的区别:在约定的时间间隔内重复执行函数,是否重新计时。节流不会重新计时,而防抖会。 一、防抖 (Debounce) 防抖(Debounce):在函数被执行后的规定间隔时间内,无法再次执行函数。如果间隔时间内再次执行了函数,则重新计算时间间隔。 防抖可以保证函数不会被连续触发,规定时间间隔内最多只能触发一次。连续的触发事件如果间隔均小于防抖规定的时间间隔,则只会在最后一次事件结束之后才会执行。 下面是一段实现函数防抖的代码: // 防抖函数 function debounce(fn, delay) { let timer = null; return function (...args) { if(timer !== null){ clearTimeout(timer); } timer = setTimeout(() => { fn.call(this, ...args); // 防止this指向错误 }, delay); } } function say(e){ console.log(e.target.value); } let debouncedSay = _debounce(say, 1000); document.getElementById('debounceElement').addEventListener('keyup', debouncedSay); //输入结束1S后才会打印到控制台。 应用: input元素在输入的时候,输入完毕再获取输入值,而不是在输入的时候一直触发; resize事件,等到窗口调整完成后再触发事件。 二、节流(Throttle) 节流(Throttle):函数在规定的时间间隔内最多执行1次,时间间隔内多次触发无任何效果,不会重新计算时间间隔。 节流模式分为: 首节流:第一次触发立即响应,事件结束之后不再响应; (时间戳实现法) 尾节流:第一次触发不立即响应,需要等到第一次时间间隔到了才响应,事件结束之后也会在最后一个间隔周期到了的时候保留一次响应;(setTimeout实现法) 首尾节流:第一次触发立即响应,事件结束后的下一个间隔周期也保留一次响应;(前二者结合) 下面是一段实现函数节流的代码: // 首节流 function throttleFront(fn, delay) { let prevTime = 0; return function (...args) { let curTime = Date.now(); if (curTime - prevTime >= delay) { prevTime = curTime; fn.call(this, ...args); } } } // 尾节流 function throttleEnd(fn, delay) { let timer = null; return function (...args) { if (timer === null) { timer = setTimeout(() => { fn(...args); timer = null; }, delay); } } } // 首尾节流 function throttleBoth(fn, delay) { let prevTime = 0; let timer = null; return function (...args) { let curTime = Date.now(); // 第一次触发和中间过程的触发,都是使用时间戳实现; if (curTime - prevTime >= delay) { fn(...args); prevTime = curTime; } else { // 这里相当于是防抖,只有事件结束后的最后一次触发,才使用setTimeout进行。 if (timer !== null) { clearTimeout(timer); } timer = setTimeout(() => { fn(...args); }, delay); } } } 应用: scroll,resize等事件,防止高频触发; 鼠标点击事件,防止高频触发;

FE待做事项清单

2021.03.30

Frontend

新内容或没完全掌握的模糊知识内容,或者用来提醒待做的前端任务,防止忘记。 FrontEnd内容待学习、复习 现代工程化前端项目如何进行打包与上线,及其自动化操作; 前端的HTTP请求相关:如何手动发送与接收,XMLRequest,fetch和axios.js等; TCP协议、HTTP协议内容回顾; HTTPs协议的内容、特点 微前端:概念、特点等; 各种前端框架软件架构设计模式:MVC、MVP、MVVM的概念、区别和优劣; 节流、防抖的原理和实现; 虚拟DOM的概念原理; WebGL、Web Assembly和Web Worker的概念与应用; TypeScript学习; 前端性能优化,包含哪些方面,成熟的技术有哪些; 常用Git工作流,适用的情况; 如何进行代码测试?如何编写前端单元测试? Vue内部原理,核心源码理解; 响应式; 运行时; FrontEnd工作待做 CookWiz: 添加登录页面与功能,为每一个登录的账号添加一个class; 修复首页部位main-page的bug; 美化add-recipe页面; 给函数添加节流功能,防止多次触发; 添加手机号和邮箱验证功能,可通过二者找回或修改密码; 设置个人每日营养素摄入值,在菜谱内显示三大营养素占每人一天总量的百分比; 视个人需求选择公开分享与否; 查看他人分享的私房菜谱,并收藏到个人菜单; 修改用户名和密码功能; 适配平板与PC端; 对菜品制作进行反馈,上传点评,图片,评分,多人评分会综合计算并反映在菜谱评分组件内; 维护一个食材库,对食材库中没有的食物,在添加时自动提示填写并上传; 根据私人菜单,从各类别菜品中随机组合生成套餐; Blog编写、添加个人简历页面; Webpack安装、配置方法回顾并熟悉; 面试重点问题 简历项目细节 手写防抖和节流 手写Vue Reactive函数 项目部署 http缓存、http状态码、http与https、http2 git的开发流程,git fetch和pull的区别、rebase和merge的区别 Vue数组的特殊处理? 图片懒加载实现

Drops of Frontend

2021.03.30

Frontend

杂记。 如何为用于提交的数据做验证 前端验证:必填项目是否确实、(邮箱、电话号、地址等)格式匹配、密码强度检测、验证码(简单的 图灵测试 ); 后端验证:唯一性验证、验证码、敏感词; 前端验证的主要目的是对不影响安全性的验证进行预校验,减少后端负担,增加用户体验。(比如后端已经提供库存为零信息,购物车中商品在提交给后端前就应该校验是否有库存,否则提交给后端再返回无库存太降低用户体验。) 后端验证是必须的,因为所有关乎安全的验证都必须在后端进行。 前端如何进行seo优化 3.1 搜索引擎的运行原理 搜索引擎的后台,是一个巨大的数据库,里面存放了大量关键词 -> url映射; 搜索引擎收集数据,途径是通过网络爬虫; 爬虫访问互联网上的连接,下载对应的内容进行关键词分析,如果判断有存储价值则进行存储; 当用户利用搜索引擎搜索关键词,服务器就从数据库中抽取关键词对应的url集合,返回给用户。 3.2 SEO:Search Engine Optimization 搜索引擎爬虫抓取网页内容,在提炼关键词的过程中,存在一个问题:它能否看懂网页内容; js等内容爬虫是看不懂的; 如果网页内容搜索引擎爬虫可以看懂,搜索引擎会提高该网站的权重,让其位于用于搜索关键词结果的较前面; 针对搜索引擎爬虫的爬取过程,对网页进行调整,让爬虫更容易理解网页的内容,提高在搜索引擎中的排名,就是SEO。 3.3 基本的SEO优化操作 合理的title、description、keywords搜索对这三项的权重逐个减小,title值强调重点即可;description把页面内容高度概括,不可过分堆砌关键词;keywords列举出重要关键词。 在网站上合理设置robots.txt文件; 生成针对搜索引擎友好的网站地图; 增加外部链接,到各个网站上宣传; 语义化的HTML代码:,符合W3C规范:语义化代码让搜索引擎容易理解网页 重要内容HTML代码放在最前搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取 重要内容不要用js输出,一般的爬虫不会执行js获取内容 少用iframe搜索引擎不会抓取iframe中的内容 非装饰性图片必须加alt属性 提高网站速度网站速度是搜索引擎排序的一个重要指标 对于SPA应用,使用history模式进行路由 4. 机器码、字节码分别都是什么 4.1 机器码 机器码(machine code),学名机器语言指令,有时也被称为原生码(Native Code),是电脑的CPU可直接解读的数据(计算机只认识0和1)。 机器码就是计算机可以直接执行,并且执行速度最快的代码。 4.2 字节码 字节码(byte code)是一种包含执行程序、由一序列OP代码(操作码)/数据对组成的二进制文件。 字节码是一种中间码,它比机器码更抽象,需要直译器转译后才能成为机器码的中间代码。 字节码主要为了实现特定软件运行的软件环境,让软件执行与硬件环境无关。(跨平台)字节码的实现方式是通过编译器和虚拟机器。 编译器将高级语言源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以直接执行的指令。(源码 -> 字节码 -> 机器码) 字节码的设计动机 跨平台 字节码解析速度更快 字节码size压缩的更小 字节码格式版本更稳定,语法改变大部分情况下不影响字节码格式 字节码可以在编译时提前做编译优化,节省运行时编译优化时间 字节码能保护源码,增加反编译成本 字节码能支持多语言,在其平台上开发新语言更容易 编译器前端和虚拟机可以独立开发,互不影响 有中间格式也更容易debug 5. “关注点分离”原则 7. 什么是同构应用? 同构,是指同一个程序可以跑在不同的平台上。 前端同构应用,是指同一份项目x代码,既可以客户端渲染,也可以服务端渲染。 同构,主要针对的是路由、模板和数据; 8. 框架和库的区别是? 整体上,框架是一类软件的抽象,而库是某一或一系列功能的集合。 控制方面 库是一系列函数(功能)的集合,由程序编写者决定使用哪些功能,以及功能的执行顺序。 框架的宏观控制流程是固定的,程序编写者只能编写其中的可扩展部分。 可扩展性 库的功能一般是固定的,是不可扩展的。 框架仅规定了基本的运作流程,具体的实现还是依赖实现者本身,因此具有可扩展性。 9. 请求中的端口号是什么含义? 端口号对应的是TCP报文段中的端口号(0 ~ 65535),它的主要作用是表示一台计算机中的特定进程所提供的服务。 网络中的计算机是通过IP地址来代表其身份的,它只能表示某台特定的计算机,但是一台计算机上可以同时提供很多个服务,如数据库服务、FTP服务、Web服务等,我们就通过端口号来区别相同计算机所提供的这些不同的服务,如常见的端口号21表示的是FTP服务,端口号23表示的是Telnet服务端口号25指的是SMTP服务等。在同一台计算机上端口号不能重复,否则,就会产生端口号冲突这样的例外。

排序算法基本思想与JS实现

2021.03.26

Algorithm

各种排序算法基本思想描述与JS实现:冒泡、选择、插值(待补充)、希尔(待补充)、快速排序。 0. 总览 盗图一张。排序 1. 冒泡排序 从头开始,选出元素与其他元素逐一比较,并当场换位。 一次比较后,当前轮的极值一定会被放在边缘,然后排除这个极值开启下一轮比较,直到所有完成排序。 复杂度: 比较: O(n2) 交换: O(n2) // 冒泡排序 let arr = [40,29,8,4,5,6,1,0,-1]; function bubble(arr){ let round = arr.length; while(round >= 2){ for (let i = 0; i<round-1; i++){ if (arr[i]>arr[i+1]){ let temp = arr[i+1]; arr[i+1] = arr[i]; arr[i] = temp; } } round--; } } bubble(arr); // [-1, 0, 1, 4, 5, 6, 8, 29, 40] 2. 选择排序 逐一比较选出极值,依次放在一侧。直到所有的元素排序完毕。 复杂度: 比较: O(n2) 交换: O(n) 它与冒泡排序比较次数相同,但是不用每次都交换数据,相当于把冒泡法的多次交换汇总成一个,因此剩下了部分交换产生的时间复杂度。 let arr = [40,29,8,4,5,6,1,0,-1]; // 交换Arr中两个元素的函数 function _exchange(arr,a,b){ let temp = arr[b]; arr[b] = arr[a]; arr[a] = temp; } // 选择排序 function selectionSort(arr){ for (let j = 0; j < arr.length - 1; j++){ let min = j; for (let i = j; i < arr.length; i++) { if (arr[i] < arr[min]) { min = i; } }; console.log(min); _exchange(arr, j, min); }; } selectionSort(arr); console.log(arr); //[-1, 0, 1, 4, 5, 6, 8, 29, 40] 3. 插入排序 4. 堆排序 堆排序 5. 归并排序 先将数组二分,直到分成不可分割的单个元素数组,然后通过逐层向上合并成有序数组的方式,对整个数组排序。 特点: 稳定排序。 复杂度分析: 每一次拆分数组,数组的元素总数没有变化。一次拆分就对应着一次合并,每次合并都需要遍历拆分后数组的所有元素,因此单次合并时间复杂度为O(N)。 N个元素拆分到最小单元,拆分次数为logN,也就是对应着logN次合并,因此总时间复杂度为O(NlogN)。 Time: O(NlogN) Space: O(N) // 这里每次递归都创建了新数组,因此空间复杂度是O(NlogN) function mergeSort(array) { if(array.length === 1) return array; function merge(arr1, arr2){ let p1 = 0, p2 = 0; let res = []; while(p1 < arr1.length || p2 < arr2.length){ if(p1 === arr1.length){ res.push(arr2[p2]); p2++; } else if (p2 === arr2.length){ res.push(arr1[p1]); p1++; } else { if(arr1[p1] < arr2[p2]){ res.push(arr1[p1]); p1++; } else { res.push(arr2[p2]); p2++; } } } return res; } let mid = Math.floor(array.length/2); let left = array.slice(0, mid); let right = array.slice(mid); let sortedLeft = mergeSort(left); let sortedRight = mergeSort(right); return merge(sortedLeft, sortedRight); } 6. 快速排序 分而治之思想。就像二叉搜索树,一次选一个元素,把序列分为两半,左边一半都比元素小,右边一半都比元素大。 这样中间的元素位置就确定了。然后再用同样的算法,递归中间元素两边的序列,直到全部完成排序。 复杂度: 平均复杂度: O(n·log(n)) // 快速排序: 双指针法 // // 首先是查找中间值(枢纽)并移动的函数: // 1. 查找left到right的中点,然后对三者进行排序移动; // 2. 把找到的中间点移动到倒数第二位置(right-1),因为已知最后的一个元素肯定大于它。这样方便后续的操作。 // 快速排序核心算法: // 1. 对一个数组取中心点(枢纽),并使用findMedian函数排序并移动(移动后,最小值在left位置,最大值在right位置,枢纽也就是中值在right-1位置); // 2. 另设置两个指针,i从left向右,j从right-1位置向左依次查找。一旦发现i指向的元素大于枢纽值mid就停下来,j指向的元素小于枢纽值mid也停下来。 // 3. 交换i和j的值,然后继续2步骤; // 4. 直到i<j,也就是二者交叉,停止搜索; // 5. 把枢纽值(此时在right-1位置存放)与i元素进行对换,就实现了两侧分别小于和大于中间值的排序。 // 6. 此后,i-1作为左序列的right值,i+1作为右序列的left值,继续递归运算,left和right相交时,终止迭代,完成排序。 function quickSort(arr,left=0,right=arr.length-1){ function swap(i,j){ [arr[i], arr[j]] = [arr[j], arr[i]]; } function findMid(left,right){ if(left > right) return ; let mid = Math.floor((left+right)/2); if(arr[mid] < arr[left]) swap(mid,left); if(arr[right] < arr[mid]) swap(mid,right); if(arr[mid] < arr[left]) swap(mid,left); swap(mid,right-1); return arr[right-1]; } if(right - left <= 0) return; if(right - left === 1 || right - left === 2){ findMid(left, right); return; } let midVal = findMid(left,right); let leftP = left, rightP = right-2; while(leftP < rightP){ while(arr[leftP] < midVal) leftP++; while(arr[rightP] >= midVal) rightP--; if(leftP < rightP) swap(leftP, rightP); } swap(leftP,right-1); quickSort(arr,left,leftP-1); quickSort(arr,leftP+1,right); } quickSort(arr); // [1, 4, 3, 0, 2, 6, 20, 10, 7, 70, 9] // [1, 0, 2, 4, 3, 6, 20, 10, 7, 70, 9] // [0, 1, 2, 4, 3, 6, 20, 10, 7, 70, 9] // [0, 1, 2, 3, 4, 6, 20, 10, 7, 70, 9] // [0, 1, 2, 3, 4, 6, 7, 9, 70, 10, 20] // [0, 1, 2, 3, 4, 6, 7, 9, 10, 20, 70] 整个流程如下图所示。 7. 桶排序(Bin Sort) 7.1 桶排序原理 首先根据待排序元素的上下限,从左到右设置一系列桶容器。 将待排序元素按一定规则,放入多个桶容器中。每个桶容器单独排序,桶内可采用任何排序算法。 按桶的顺序依次取出元素,排好序即可。 7.2 复杂度分析 假设原始数组元素数目为n,桶数目为m。 那么,平均每个桶里面有n/m个元素。 如果使用快排,则每个桶的时间复杂度为O(n/m * log(n/m)),全部桶排好序的总复杂度为m * O(n/m * log(n/m)) = O(n * log(n/m))。 遍历整个数组的复杂度为O(n),所以整体桶排序的复杂度为: O(n + n * log(n/m)) 最好情况:m=n,桶的数目与元素数目相同。此时为计数排序,复杂度为O(n); 最坏情况:m=1,只有一个桶,此时退化为直接为原数组快速排序,复杂度为O(n*logn)。 8. 计数排序 在数组中的元素大小差别不大的时候可以使用,时间复杂度为线性。 先遍历数组,取元素最大值max和最小值min。 新建一个数组,长度为max-min+1。直接将每一个元素的值N减去min作为数组的索引(N-min),放在这个数组中(重复则计数),然后从左到右遍历这个数组,依次按数目打印其中的元素即可。 Time: O(N+k) Space: O(k) n为原数组长度,k为数组中元素最大值与最小值的差。

JS实现双向链表的一小段代码

2021.03.20

JavaScript

用JS类实现双向链表。 // Node class Node{ constructor(data, pre = null, next = null) { this.data = data; this.pre = pre; this.next = next; } } // LinkedList class LinkedList{ constructor() { this.head = null; this.tail = null; this.length = 0; } // Check if the linkedlist is empty. isEmpty(){ if (this.length == 0) return true; } // Translate to String. toString(){ let current = this.head; let res = ''; while(current){ res += '-' + current.data; current = current.next; } return res.slice(1); } // add node to linkedlist. addNode(data, pos = this.length + 1){ if (this.length == 0) { let node = new Node(data); this.head = node; this.tail = node; this.length++; } else if (pos >= this.length+1){ let node = new Node(data); this.tail.next = node; node.pre = this.tail; this.tail = node; this.length++; } else if(pos== 1) { let node = new Node(data); this.head.pre = node; node.next = this.head; this.head = node; this.length++; } else { let current = this.head; let i = 1; while (i < pos){ current = current.next; i++; } let node = new Node(data); node.pre = current.pre; node.next = current; current.pre = node; node.pre.next = node; this.length++; } return this.toString(); } // Find a node at the position of num from Head. findNode(num){ let current = this.head; let i = 1; while(i < num){ current = current.next; i++; } return current; } // return node data of NO.x checkNode(num){ if (num > this.length || num < 1){ return null; } return this.findNode(num).data; } // delete node from list deleteNode(num = this.length){ if (this.length == 0 || num > this.length || num < 1){ return; } if (num > 1 && num < this.length) { let node = this.findNode(num); node.pre.next = node.next; node.next.pre = node.pre; this.length--; } else if (num == 1){ this.head = this.head.next; if (this.head){ this.head.pre = null; } this.length--; } else if (num == this.length){ this.tail = this.tail.pre; this.tail.next = null; this.length--; } return this.toString() + this.length; } // modify the data of node No.x modifyNode(num, data){ if (this.length == 0 || num > this.length || num < 1){ console.error('invalid node number.'); return; } let node = this.findNode(num); node.data = data; } } let linkList = new LinkedList(); linkList.addNode('Mars'); linkList.addNode('Liu'); linkList.addNode('Page'); linkList.addNode('Page2'); linkList.addNode('Page3');

JavaScript中按值传递和按引用传递的原理

2021.03.20

JavaScript

函数内部对按值传入的参数进行修改,不会对外部参数有影响; 对按引用传入的参数修改(如传入的对象),会影响外部参数本身. 具体原因是: 外部变量在内存中对应一个地址,这个地址指向的一段内存空间记载着这个外部变量的具体值。 函数的参数位于内存中的另一个不同的地址,在函数调用时,函数接收传来的外部变量,实际上是沿着外部变量的内存地址,找到内存中储存的值,然后复制一份到函数参数地址所指向的内存空间。这样,之后沿着函数参数在内存中的地址,去内存中寻找,也会找到与外部变量相同的值。 但是这二者是有区别的,函数内部始终访问、修改的是函数参数所在的内存中的值,而与外部变量无关。 那么,为什么按引用传入的参数(比如给函数传入对象类型的参数),在函数内部的修改会直接反应到函数外呢? 因为按引用传入的参数在赋值的时候,并不如上述复制内存中的实际值。而是直接把内存地址本身作为值储存起来。比如,a是一个对象,当执行let b = a;语句时,b的内存空间储存的是a的内存地址,而不是a的复制。 所以,传入对象作为函数参数,在函数内部使用.或[]访问具体的属性时,函数内外都引用的是一个内存空间中的同一对象,当然会直接反应在函数外部啦。

现代JS学习笔记:可迭代对象(Iterable Object)

2021.03.16

JavaScript

学习内容:《现代JavaScript教程》 可迭代对象 Iterable Object 1 同步可迭代对象 是数组的泛化。这个概念是说任何对象都可以被定制为可在 for..of 循环中使用的对象。 为了让 range 对象可迭代(也就让 for..of 可以运行)我们需要为对象添加一个名为 Symbol.iterator 的方法(一个专门用于使对象可迭代的内置 symbol)。 当 for..of 循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有 next 方法的对象; 从此开始,for..of 仅适用于这个被返回的对象; 当 for..of 循环希望取得下一个数值,它就调用这个对象的 next() 方法; next() 方法返回的结果的格式必须是 {done: Boolean, value: any},当 done=true 时,表示迭代结束,否则 value 是下一个值。 function Queue(num){ this.num = num; this[Symbol.iterator] = function (){ //★这里开始定义了迭代器函数 return { current: 0, total: this.num, next(){ if (this.current < this.total){ this.current++; return { done: false, value: '人' + this.current } } else { return { done: true } } } } } } let a = new Queue(5); for (let item of a){ console.log(item); } //人1人2人3人4人5 !注意:为什么使用for…of遍历一个数组,无法修改原数组的内容? 数组作为可迭代对象,本质上使用的是next()函数返回的新对象里的value属性,所以使用for…of遍历数组获取的是另一个对象的属性,直接修改遍历结果,无法影响到原对象。 相当于使用可迭代对象方法进行迭代操作是安全的,永远不会修改原对象内容。 2 ★可迭代(遍历)与类数组对象的区别 可迭代对象,指的是部署了Symbol.iterator函数,能用for…of进行遍历的对象。 内置可迭代对象:String、Array、Map、Set 类数组对象,指的是具有length属性,并且具有数字索引的对象。 可迭代对象和类数组对象,都可以用Array.from(iterable),显式转化为‘真’数组。 3 异步可迭代对象 为对象设置[Symbol.iterator]方法,可以使用for…of循环对对象进行迭代。前提是迭代过程是同步的,而不是异步的。 如果每次迭代存在异步过程,比如每次迭代都要向服务器请求信息,则需要使用另外的迭代方法。 要使对象异步迭代: 使用 Symbol.asyncIterator 取代 Symbol.iterator。 next() 方法应该返回一个 promise(带有下一个值,并且状态为 fulfilled)。关键字 async 可以实现这一点,我们可以简单地使用 async next()。 我们应该使用 for await (let item of iterable) 循环来迭代这样的对象。(注意关键字 await。) 注意: 异步的迭代对象,无法使用…spread语法展开。因为默认使用[Symbol.iterator]而不是[Symbol.asyncIterator],如果不存在会报错。

现代JS学习笔记:理解浏览器事件循环——宏任务和微任务

2021.03.11

JavaScript

学习内容:《现代JavaScript教程》 1.★事件循环 当浏览器没有任务执行时,处于休眠状态。 当任务出现,JS脚本默认情况下是单线程同步执行的,也就是按照出现的先后顺序执行任务,先进入的任务先执行。(任务队列) 事件循环:单线程脚本语言Javascript处理任务的一种执行机制,通过循环来执行任务队列里的任务。一个宏任务执行开始到下一个宏任务执行开始,叫做一次事件循环(一个tick)。 浏览器中的任务分为宏任务和微任务。 1.1 宏任务 以下内容被称为宏任务,这些任务按照出现的顺序在浏览器内部组成一个队列,按照进入的先后顺序执行,先进先出。 下列任务都属于宏任务: 当外部脚本 <script src="..."> 加载完成后,执行这个脚本的任务过程,是宏任务; 用户事件:当用户移动鼠标时,任务就是派生出 mousemove 事件和执行处理程序,这个过程也是宏任务; setTimeout/setInterval这类事件:当安排的(scheduled)setTimeout 时间到达时,会产生宏任务,任务就是执行其回调; postMessage。 宏任务执行的间隙,如果有微任务,则浏览器先执行微任务,然后执行DOM渲染。 在一个宏任务的执行过程中不进行任何DOM渲染,只有完成后才进行。 1.2 微任务 微任务仅来自于我们的代码。有如下几种形式: 由 promise 创建的:在主线程执行过程中,如果遇到promise对象resolve/reject之后,调用了then/catch/finally方法,会立即将这些方法内部的函数作为微任务,添加到微任务队列中(注意必须先resolve/reject); async/await函数也会创建微任务:await之后的代码(不包括await所在行)都会作为微任务异步执行(await 当前行的代码会立即执行,它后面的代码作为微任务异步执行,相当于应用了.then方法); Generator函数; DOM中的MutationObserver触发后的回调函数; queueMicrotask(func),它手动添加 func 到微任务队列。 每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后渲染(如果有更新),然后再执行其他的宏任务。 1.3 async/await函数中的宏任务和微任务 async异步函数中的await的含义是: 在此处等待一个异步结果,并阻塞所有后面的函数执行,直到这个结果被获取,再继续执行后面的函数程序。(await后如果是一个函数,也会立即同步执行。) 包括await本身这一部分及其之前的所有函数代码,都是立即同步执行的。但是await之后的代码并不是,它后面的所有代码会变成微任务,先暂时性转移到另一个队列(微任务队列)中,等待后续再异步执行。 也就是说,浏览器不会等待await返回结果,而是会继续执行async/await函数体外后面的宏任务代码,完毕后再按顺序执行微任务队列中的任务。再之后进行渲染,然后再执行下一个宏任务。 所以结果是一个await把async异步函数内的代码分隔成了两部分: 它本身和它前面的部分都是同步执行的; 它的回调函数和它后面的async体内代码都是微任务,会按微任务入队顺序选取合适时机依次执行。 1.4 一个经典的案例来理解宏任务和微任务 下面这个经典的代码题,可以供分析参考理解。 // 这段函数会先后输出什么字符串呢? async function async1() { console.log('async1 start'); console.log(await async2()); console.log('async1 end'); } async function async2() { console.log('async2'); return 'async2 return'; } console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); async1(); new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); console.log('script end'); 需要注意以下几个事实: 整个这段代码段在加载后,会被当做一个宏任务来执行; 其中的async1是带有await的异步函数; 其中的async2是不带await的异步函数; setTimeout的回调函数另一个宏任务,会被推送到宏任务队列中; Promise的声明本体是同步的,then()函数内的部分是异步的,会被推送到微任务队列。 ★★★ 这里的过程如下: (Mars 2021.03.11) 整个代码作为宏任务,从上到下执行; async1、async2两个函数声明本身没有被执行,无输出; console.log(‘script start’)输出第一个字符串script start; setTimeout执行,将一个回调函数function(){console.log(‘settimeout’)}作为下一个宏任务,推入宏任务队列中等待执行;(此处未执行) 执行async1函数:await前都是同步的,可以正常立即执行,所以输出第二个字符串async1 start。 然后,遇见了await关键字。它会立即执行后面的async2函数,然后等待返回结果用于给console.log作参数输出。因此,它在操作执行async2后打断,把后面的所有流程都变成了微任务,推送到微任务队列中等待执行。此处执行了async2函数; async2函数内没有await和其他异步行为,直接执行输出第三个字符串async2,然后返回async2 return供async1的await在后续的微任务中使用; 然后是一个Promise对象声明,声明过程本身是同步执行的,所以then之前的代码都立即执行,输出第四个字符串promise1; then()包含的函数代码是需要等待promise被resolve然后异步执行的,属于微任务,被推送到微任务队列等待执行; 宏任务代码的最后一行console.log(‘script end’),直接正常执行,输出第五个字符串script end。 至此,这段脚本作为第一个宏任务执行完毕,在下一个宏任务(第4步setTimeout的回调)执行前,先处理微任务队列。因此此时按先进先执行的原则,执行async1中的剩余部分:输出async2的返回值async2 return,然后输出async1 end; 执行下一个微任务:promise声明后的then()。因此此时输出第八个字符串promise2; 至此所有微任务执行完毕。可以开始执行下一个宏任务:setTimeout的回调。因此此时输出了第九个字符串settimeout。 至此全部任务执行完毕,浏览器进入等待状态。

现代JS学习笔记:网络请求Fetch

2021.03.10

JavaScript

学习内容:《现代JavaScript教程》 网络请求fetch() 可以使用fetch()方法请求任何URL的网络资源。语法如下: let promise = fetch(url, [options]) url —— 要访问的 URL。 options —— 可选参数:method,header 等。 使用Fetch请求资源,一共分为两步: 【第一步】服务器返回响应头部header,fetch使用内建的Response Class来对响应头部进行解析,返回的是一个promise对象。(如果fetch过程没有发生错误) 此时还没有获取到响应主体,只是获取到了Header。这样做是为了提前发现请求中的错误,防止资源浪费。Response对象有几个属性: header —— 返回的头部信息; ok —— 是否成功响应的布尔值,如果 HTTP 状态码为 200-299,则为 true; status —— HTTP 状态码,例如 200。 【第二步】 得到了resolve的Response对象,可以继续利用这个对象内部的方法,进一步获取响应主体。方法如下: response.text() —— 读取 response,并以文本形式返回 response, response.json() —— 将 response 解析为 JSON, response.formData() —— 以 FormData 对象的形式返回 response, response.blob() —— 以 Blob(具有类型的二进制数据)形式返回 response, response.arrayBuffer() —— 以 ArrayBuffer(低级别的二进制数据)形式返回 response, 另外,response.body 是 ReadableStream 对象,它允许你逐块读取 body。

现代JS学习笔记:函数进阶

2021.03.10

JavaScript

学习内容:《现代JavaScript教程》 11 函数进阶 11.1 递归recursion 函数内部调用自身,就是函数递归。 递归深度: 最大的嵌套调用次数(包括首次)被称为 递归深度。 最大递归深度受限于 JavaScript 引擎。对我们来说,引擎在最大迭代深度为 10000 及以下时是可靠的,有些引擎可能允许更大的最大深度,但是对于大多数引擎来说,100000 可能就超出限制了。 11.2 ★执行上下文context 函数运行执行过程的相关信息,存储在【执行上下文context】中。 执行上下文中储存着函数运行的: this值指向; 当前控制流所在位置(函数执行到第几行了?); 当前的变量; 其他内部细节; 一个函数调用的过程中,只有一个与其对应的执行上下文。 执行上下文不是对象,是一种特殊的内部数据结构。 当递归调用函数的时候,存在一个执行上下文堆栈。递归调用时,前一函数运行的执行上下文被固定并推入上下文堆栈中,新的函数调用完毕返回值后,从上下文堆栈中读取函数执行上下文,继续运行。 11.3 Rest参数 11.3.1 Rest参数 Rest 参数可以使函数传入任意数量的参数。 通过使用三个点 … 并在后面跟着包含剩余参数的数组名称,来将它们包含在函数定义中。这些点的字面意思是“将剩余参数收集到一个数组中”。 function fun1(…args){} // 传入的参数都被收纳在args这个数组中,可以在函数内调用。 Rest参数必须放在函数参数列表的最末尾,放在中间会报错。 rest参数与arguments对象的区别:rest是真正的数组,而Arguments是类数组的对象,无法使用数组的自带函数。(arguments是历史遗留问题,新代码尽量不用。) function sum(...arg){ //Rest参数 return arg.reduce((accu,item)=>{accu += item; return accu;}); } let arr = [1,2,3,4,5]; console.log( sum(...arr) ); //15 11.3.2 Spread参数 与Rest参数相反,Spread参数可以把数组展开为独立的参数列表。 func(a1,a2,a3); let arr = [1,2,3]; func( …arr ); //这里可以正常传入1,2,3,数组被Spread打散。 11.4 变量作用域与闭包 11.4.1 变量作用域 11.4.1.1 代码块 代码块是用{}括起来的一段代码,使用let/const在代码块中声明的变量,只能在该代码块中使用,代码块外无法访问。 for(let xxx;;){}循环中,在圆括号内声明的变量也被视为代码块的一部分。 不同代码块,可以声明相同名称的变量,互不影响。 11.4.1.2 嵌套函数 嵌套函数有两种情况: 在一个函数内,声明另一个函数,则内部函数可以获取外部函数作用域内变量。 函数的返回值也可以是一个函数,这个函数在任何部位调用,都可以访问函数内部的变量。 function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter() ); // 2 11.4.2 词法环境 在 JavaScript 中,每个运行的函数,代码块 {…} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment)的内部(隐藏)的关联对象。 词法环境由两部分组成:①当前环境记录:一个对象,储存当前环境所有的局部变量及其他信息(如this的值);②外部词法环境:对外部词法环境记录的引用; 词法环境对象,不是一般的JS对象,而是一个规范对象(specification object):它仅仅是存在于编程语言规范中的“理论上”存在的,用于描述事物如何运作的对象。我们无法在代码中获取该对象并直接对其进行操作。 11.4.2.1 在全局环境中声明一个变量并赋值,词法环境的变化情况 没用用let声明之前,词法环境里就有了phrase这个属性,也就是浏览器已经知道phrase的存在,但是状态是unintiallized未初始化,不能引用(报错); let phrase 声明之后,phrase的值在词法环境里变成了undefined,可以使用; 赋值后,相应的词法环境中属性值也会改变。 11.4.2.2 声明一个函数情况 创建一个执行环境的时候,所有环境内(如代码块内)的函数声明都立即被创建为可执行函数,而不是等到读取到函数声明才创建。这就解释了为什么函数可以在声明之前使用。 创建(声明)函数的时候,函数内部的属性[[Environment]]会记录下它创建时的外部词法环境,但只有当函数运行的时候,它本身的词法环境才被创建,这时候会使用声明时记录下的[[Environment]]属性值作为自己词法环境的outer外部引用参数。 11.4.2.3 内部和外部词法环境 比如在一个函数内部创建另一个函数,内部函数的词法环境就包含当前环境记录,记录了内部函数的参数和局部变量;此外还包含外部词法环境的引用,直到全局环境。 这个内部函数内查询变量,是从内向外的。局部词法环境查询不到就去上级环境里查询,使用可查询到的第一个变量。 所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]]的隐藏属性,该属性保存了对创建该函数的词法环境的引用。 11.4.3 闭包 闭包 是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命终结)了之后。 11.4.4 函数对象 JS中,函数是对象。(可以把函数想象成可被调用的“行为对象(action object)”。) 函数对象包含的属性有: name 函数名; length 参数的个数;(rest参数不算在内) 自定义属性: 自己可以给函数指定属性; 11.4.5 命名函数表达式NFE 带有名字的函数表达式。 常规函数表达式: let a = function(){}; 命名函数表达式: let a = function fun1(){}; 关于名字 func 有两个特殊的地方,这就是添加它的原因: 它允许函数在内部引用自己。(比引用函数表达式的变量名好,因为这个名字是函数本身的,不怕变量名修改。) 它的命名在函数外是不可见的。 11.5 new Function()創建函數 new Function('a', 'b', 'return a + b'); // 基础语法 new Function('a,b', 'return a + b'); // 逗号分隔 new Function('a , b', 'return a + b'); // 逗号和空格分隔 一般情况下不需要使用new Function()这种特殊的形式创建函数。但是它有特殊的用途。 new Function()的特殊之处在于:它创建的函数词法环境为全局环境,无法访问当前声明环境中的局部变量。 11.6 函数柯里化Currying 函数的柯里化,指的是把一次性传入全部参数的函数,转化为可连续依次传入参数的函数类型。 例如,原函数为f(a,b,c),柯里化后使用方式为f(a)(b)(c)。 JS中,一般Currying柯里化高级一点的实现,可以保证函数既可以像原来一样同时传入多个参数调用,也可以一个一个传入参数调用。 柯里化的好处是:如果传入参数不足原参数数量,则返回保存了已传入参数的剩余函数,这样在之后只需要传入剩下的参数就可以获取结果。这句话难以理解,看下面的例子: 一个函数柯里化后,例如f1(a,b,c)被柯里化为f2(a)(b)(c),如果调用一次f2(a),则返回的是可继续调用两次的函数f3(b)(c),这时a可以看做是被传入了默认值,之后使用f3只需要依次再传入两个参数b,c即可。 这个f3函数叫做原f1函数的偏函数。 函数柯里化的好处,就是随时随地可以轻松创建偏函数。

现代JS学习笔记:代码质量控制

2021.03.10

JavaScript

学习内容:《现代JavaScript教程》 21 代码质量 21.1 断点调试 Chrome打开控制台,在脚本的行号上单击,就可以设置断点。右击可以设置条件断点。 在代码中直接写入命令: debugger。就可以在当前位置停下并打开调试窗; 21.2 注释 应该注释这些内容: 整体架构,高层次的观点。 函数的用法。 重要的解决方案,特别是在不是很明显时。 避免注释这些内容: 描述“代码如何工作”和“代码做了什么”。 避免在代码已经足够简单或代码有很好的自描述性而不需要注释的情况下,还写些没必要的注释。 21.3 代码风格 官方建议的代码风格: 建议风格列表如下(非必须): 左花括号与相应的关键词在同一行上 — 而不是新起一行。左括号前还应该有一个空格; 过长的代码应该分隔成多行,一行代码的最大长度应该在团队层面上达成一致。通常是 80 或 120 个字符; 水平方向上,每次缩进2个空格; 垂直方向上,每个逻辑代码块前后各空一行。 永远不应该出现9行没有空行的情况; 每一个语句后面都应该有一个分号。即使它可以被跳过; 尽量避免代码嵌套层级过深; 主代码辅助函数的声明,放在代码最后写,先写调用代码。 21.4 避免“忍者代码”(忍者:垃圾代码的测试人员) 正确命名变量,体现变量的含义,不要使用没意义甚至一个字母的变量; 不要为了体现个人智慧或可以简短而丧失代码可读性; 不要使用缩写,否则很难识别; 不要使用抽象的变量名,例如:data/value/num/str等; 不要使用看起来相似的变量名:比如data和date; 同一个函数命名前缀应该达成同一种功能,show\display\paint这些如果都作为显示使用,则应该统一不应该随机出现; 不要复制相同的名字变量另作他用; 不要在变量名头部随意使用下划线; 不要使用带有个人感情的词汇命名变量,比如:theBestThing; 不要在不同的代码块内使用同名变量; 函数不应该有除自身功能之外的副作用,并且返回期待类型的结果; 函数的命名与函数的功能应该统一,不要添油加醋的功能。

现代JS学习笔记:Proxy和Reflect

2021.03.10

JavaScript

学习内容:《现代JavaScript教程》 Proxy和Reflect Proxy代理器 Proxy是什么 Proxy顾名思义,是一个对象的代理对象。 Proxy可以将一个对象包装成另一个对象,可以简单理解为被包装对象的“经纪人”。它监控对这个被包装对象的一切操作,并针对不同的操作,代替这个对象执行一些设定好的操作。Proxy是ES6之后才添加的新对象。 对象是明星,Proxy是这个明星的经纪人,要找明星办事先要联系经纪人. 经纪人可以拒绝你的任何要求,或者可以告诉你一些明星的信息,也可以帮你传信给明星本人。 Proxy基本语法 Proxy的基本语法是这样的: let proxy = new Proxy(target, handler); 其中,target是被包装的对象,handler是代理配置(也叫“捕获器”(trap),就是拦截各种操作的方法)。 一般可以将handler对象在外部单独声明,然后在声明Proxy的时候直接使用变量名。 对Proxy进行操作,如果Proxy存在针对这种操作的捕获器trap,则将执行捕获器定义的操作。如果不存在,就直接把这种操作传入给被包装的对象。 handler捕获器有13种,见下表。 内部方法 Handler方法 何时触发 [[Get]] get 读取属性 [[Set]] set 写入属性 [[HasProperty]] has in 操作符 [[Delete]] deleteProperty delete 操作符 [[Call]] apply 函数调用 [[Construct]] construct new 操作符 [[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf [[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf [[IsExtensible]] isExtensible Object.isExtensible [[PreventExtensions]] preventExtensions Object.preventExtensions [[DefineOwnProperty]] defineProperty Object.defineProperty, Object.defineProperties [[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries [[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object/keys/values/entries 这里的内部方法,是Js语言中操作对象最底层的方法,这些方法我们不能直接使用,仅仅能在规范中使用。 但是我们可以通过Proxy检测并拦截这些底层方法。 常见的捕获器设置 new Proxy(target, handler)创建的时候,第二个参数捕获器handler是一个对象,里面的各个方法设定了捕获到各种操作后,进行相应的处理方式。 最简单常见的是get和set捕获器,它们分别在target被获取和被修改的时候触发。 get捕获器 要拦截读取操作,handler 应该有 get(target, property, receiver) 方法。 读取属性时触发该方法,参数如下: target —— 是目标对象,该对象被作为第一个参数传递给 new Proxy; property —— 目标属性名; receiver —— 与Getter访问器属性有关,暂时不考虑。 set捕获器 当写入属性时 set 捕捉器被触发。 set(target, property, value, receiver): target —— 是目标对象,该对象被作为第一个参数传递给 new Proxy, property —— 目标属性名称, value —— 目标属性的值, receiver —— 与 get 捕捉器类似,仅与 setter 访问器属性相关。 set捕获器在属性值成功设定后,必须返回true(return true)。否则会报错。 ownKeys捕获器 Object.keys,for..in 循环和大多数其他遍历对象属性的方法都使用内部方法 [[OwnPropertyKeys]](由 ownKeys 捕捉器拦截) 来获取属性列表。 ownKeys捕获器可以捕获这些行为。 ★ 几种不同遍历对象属性方法之间的区别: Object.getOwnPropertyNames(obj) 返回所有非 Symbol 键,无论是否enumerable; Object.getOwnPropertySymbols(obj) 只返回 Symbol 键; Object.keys/values() 返回带有 enumerable 标志的非 Symbol 键/值; for..in 循环遍历所有带有 enumerable 标志的非 Symbol 键,还会返回原型对象的键。 ownKeys(target)传入一个目标对象,它应该Return 一个数组对象,里面元素是想要返回的遍历结果。 function unModified(obj){ return new Proxy(obj,{ ownKeys(target) { return ['a','b','c']; //这里设定在遍历的时候返回a,b,c,但是每个属性都相当于没有配置属性标志,enumerable默认为false。所以只能用Object.getOwnPropertyName()遍历才能显示出来。其他方法返回都是空的。 } }); } has捕获器 has捕获器在对包装对象使用in操作符的时候触发。 has(target,prop){} // target是包装的对象本身,prop表示in前面的,也就是传入的属性参数。 apply捕获器 apply(target, thisArg, args) 捕捉器能使代理以函数的方式被调用: target 是目标对象(在 JavaScript 中,函数就是一个对象); thisArg 是 this 的值; args 是参数列表。 具体见:proxy-apply Proxy的弊端 Proxy != target Proxy虽然可以把外部的操作透明地传给内部,但是毕竟代理后的Proxy对象和原target不是一个对象,因此他们之间不相等。 使用”内部插槽”的对象无法使用 有些特殊类型对象的数据,使用“内部插槽”的形式保存,并使用内部方法修改(比如Map.set()),并不通过常规[[get]]/[[set]]这类内建方法,因此代理后无法实现捕获和修改等操作。 这类对象类型包括:Map/Set/Date/Promise/class的#开头私有属性。 这些对象需要通过以下方式,将原本的内部方法绑定到自身后返回使用: let map = new Map(); let proxy = new Proxy(map, { get(target, prop, receiver) { let value = Reflect.get(...arguments); return typeof value == 'function' ? value.bind(target) : value; } }); proxy.set('test', 1); alert(proxy.get('test')); // 1(工作了!) Reflect对象 Reflect对象引入的目的 Reflect对象是ES6新加入的一个内置对象,它的目的如下: 将原来Object中本来属于语言内部的一些方法,转移到Reflect对象上,未来的新的语言内部方法都只部署在Reflect对象上; 修改如defineProperty的某些Object方法的返回结果,使更合理; 把原来的delete obj, key in obj这类命令式操作,都转化为函数操作Reflect.deleteProperty()、Reflect.has(); ★Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。 也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取原始的默认行为。 有了Reflect对象以后,很多操作会更易读。 Reflect静态方法 Reflect对象一共有 13 个静态方法。 Reflect.apply(target, thisArg, args) Reflect.construct(target, args) Reflect.get(target, name, receiver) Reflect.set(target, name, value, receiver) Reflect.defineProperty(target, name, desc) Reflect.deleteProperty(target, name) Reflect.has(target, name) Reflect.ownKeys(target) Reflect.isExtensible(target) Reflect.preventExtensions(target) Reflect.getOwnPropertyDescriptor(target, name) Reflect.getPrototypeOf(target) Reflect.setPrototypeOf(target, prototype) 上面这些方法的作用,大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的。

现代JS学习笔记:Promise

2021.03.10

JavaScript

学习内容:《现代JavaScript教程》 13 Promise 13.1 基于回调的异步编程 比如想要在一个请求完成后,立即执行一个函数。这个形式叫做异步编程,可以使用回调函数的形式编写。如下: function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); //这里编写了script标签载入后的回调函数。 document.head.append(script); } loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { alert(`Cool, the script ${script.src} is loaded`); alert( _ ); // 所加载的脚本中声明的函数 }); 这种回调函数在少量的嵌套时没问题,在多层嵌套时,比如回调里面还有异步的回调,多层嵌套会导致代码难以维护。 13.2 Promise基本知识 Promise含义是“承诺”,顾名思义,一个Promise对象是一个承诺,它承诺了在一段时间后会给出某种解决方案:抛出一个结果或抛出错误。 Promise对象的存在是为了解决异步编程中的回调地狱问题。Promise在“生产者”和“消费者”之间达成契约,作为中间商,当生产者一旦产出结果,立即通报给消费者。 Promise()是JavaScript内置构造函数。语法如下: let promise = new Promise(function(resolve, reject) { //参数为一个函数,函数参数列表第一个是resolve函数,第二个是reject函数,这两个函数都是JS内置的,无需自己定义。 // executor(生产者代码) }); Promise构造时传入的函数被称为executor,它最终一定应该调用resolve或reject函数中的一个,代表执行成功返回结果 或 出现Error。 13.3 Promise中的resolve和reject函数 Promise对象在构造的时候,传入的executor函数带有两个参数,resolve和reject函数。 这两个函数都是可以直接执行的(Js内部定义好的),可以传入任何类型的值。 resolve(value)传入的value会作为promise最终输出的Value保存在promise的result属性中。reject(error)传入的error会作为promise错误输出值,保存在promise的内部result属性中。 使用reject函数作为报错输出时,一般建议使用Error对象作为参数,因为这样可以明确看出是出现了错误,后面有很多好处。 promise中resolve和reject调用时间不受限制,不一定非要异步操作,也可以直接调用二者其一,把promise立即解决。 13.4 Promise对象的内部属性 Promise对象具有两个内部属性:state 和 result。 state: 初始值为’pending’,当resolve函数运行后,变更为’fulfilled’。在reject函数运行后,变更为’rejected’; result: 初始值为undefined。在resolved函数运行后变更为返回的结果value,在Reject函数运行后变更为错误对象error; 所以, promise 最终必将变为以下状态之一: 无论promise是被解决resolved还是被拒绝rejected,都称作settled。 注意: state和result这两个属性,和原型对象[[prototype]]一样,都是内部属性,不能直接访问。只能通过.then()这类方法访问。 13.5 then()、catch()和finally() 13.5.1 then() then()方法是promise对象内置的方法,可以调用,目的是在promise对象被resolved或rejected之后,做出相应操作。 then传入两个函数作为参数,第一个函数代表了resolve之后执行的操作,第二个函数代表了reject之后执行的操作。 promise 被 settle 后,始终都会传入一个结果给.then()内部的函数:当promise被resolve,传入结果给第一个函数参数;当promise被reject,传入结果给第二个函数参数。 promise.then( function(result) { /* handle a successful result */ }, function(error) { /* handle an error */ } ); 如果只对promise运行成功之后的结果感兴趣,可以不传入第二个参数,只传入第一个函数参数。 只要promise是settled状态,调用其.then函数就会立即执行传入的回调函数。 then函数可以在一个promise上调用多次,相当于多次处理同一个promise产生的结果。 then()方法传入的如果不是函数,传入的参数会被忽略,变成默认函数val => val (透传,参数传递给下一个.then)。 13.5.2 catch() catch(func)只在promise被reject的时候才起作用。它相当于then(null, func)。 13.5.3 finally() finally(func)中的func会在promise被Settled之后运行。 基本相似于then(func, func),表示无论如何,只要promise产生了结果,就一定要运行的函数。(比如执行某些清理操作。) finally()不处理promise运行的结果,只是执行某些一定要进行的操作。finally运行之后,带有结果的promise对象依然被传出,可以继续直接调用.then或.catch,对结果进行处理。 13.6 Promise链 一个Promise在使用了.then()之后,整个.then()返回的仍是一个promise(但不是原来的promise.),它也有.then()方法,可以继续调用。 如果.then内部的两个函数返回了值,那么它就是整个.then()返回promise的result;如果没有返回值,.then()返回promise result是undefined。 上一个.then()调用后的结果,会作为其返回promise对象的内部result属性值,下一个.then()会获取该值进行相应处理。 比如: new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); }).then(function(result) { alert(result); // 1 return result * 2; }).then(function(result) { alert(result); // 2 return result * 2; }).then(function(result) { alert(result); // 4 return result * 2; }); then方法内部也可以手动return一个promise对象,那么下一个.then将等待这个返回的promise对象Settle之后才会运行。这使得我们可以利用promise链进行一连串的异步行为。 13.7 Promise构造函数自带的方法 13.7.1 Promise.all() Promise.all()方法接收一个数组作为参数,当该数组中含有的所有Promise对象都被resolve,返回一个新promise对象,它的Result是原数组中所有result组成的数组(原数组中不是promise对象的,就原样返回)。 当传入数组中有一个promise被reject,则Promise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略。 let promise1 = new Promise((resolve, reject)=>{ setTimeout(()=>resolve('first promise resolved!'),1000); }); let promise2 = new Promise((resolve, reject)=>{ setTimeout(()=>resolve('second promise resolved!'),3000); }); let controler = Promise.all( [promise1, promise2, 1234] ); controler.then((result)=>{ console.log(result); // ["first promise resolved!", "second promise resolved!", 1234] }) 13.7.2 Promise.allSettled() Promise.allSettled()方法接收一个数组作为参数,当该数组中含有的所有Promise对象都被settle,也就是被resolve或reject,就立即返回一个新promise对象,它的Result是由对象构成的数组,对象的结构视resolve或reject而不同,具体如下: {status:"fulfilled", value:result} // 对于成功的响应, {status:"rejected", reason:error} //对于 error。 13.7.3 Promise.race() Promise.race()方法接收一个数组作为参数,返回该数组中第一个被settle的Promise结果(result或error)。 Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1 13.8 Promisefication (Promise化) “Promisification” 是用于一个简单转换的一个长单词。它指将一个接受回调的函数转换为一个返回 promise 的函数。 具体的promisefy方法见:https://zh.javascript.info/promisify 13.9 微任务microtask Promise始终是异步的,它一旦被settle,它的then/catch/finally这些handler会被放入一个叫做微任务队列(microtask queue)的队列中等待被执行。 当JS代码执行完毕后,JS引擎才会从微任务队列里按先后顺序执行这些任务。 注: 队列是一种数据结构,它的特点是先进先出。 因此,即使是创建后立即就被resolve或reject的promise对象,它的then方法也会在全部JS代码执行完毕后才会执行。 13.10 async/await 13.10.1 async async关键字可以用在函数的前部,它的含义是: 让这个函数总是返回一个 promise; 允许在该函数内使用 await。 async关键字修饰的函数,即使return了一个非promise对象的变量,也会被作为Result包装成promise对象返回。 13.10.2 await await关键字用在async修饰的函数体内的promise对象前,它的含义是: 让JS引擎暂停,等待这个promise对象被settle,然后返回结果result或错误error。(可以避免使用.then方法,让代码更容易阅读) 注意: await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为只是在本函数内有效,JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。 await也可以Promise.all()一起使用,把许多promise对象包装在一起等待settle。 13.10.3 async/await中的Error处理 如果await后面的promise对象被reject了,那么这一行相当于抛出了这个Error。和throw error是一样的。

现代JS学习笔记:JS中各种变量值之间的比较方式

2021.03.10

JavaScript

学习内容:《现代JavaScript教程》 JS中各种变量值的比较方式 数学比较 包括>、<、>=、<=、==、!=等。 数字比较返回值为Boolean类型。 字符串比较 在比较字符串的大小时,JavaScript 会使用“字典(dictionary)”或“词典(lexicographical)”顺序进行判定(实际是 Unicode 编码顺序)。换言之,字符串是按字符(母)逐个进行比较的。 !注意: 即使两个字符串都看起来像是数字,也遵循字符串的字典比较原则。 ‘2’>’1999’ //true,因为首字符2的Unicode编码大于1 !字符串的比较算法: 首先比较两个字符串的首位字符大小。(非真正的字典顺序,而是 Unicode 编码顺序) 如果一方字符较大(或较小),则该字符串大于(或小于)另一个字符串。算法结束。 否则,如果两个字符串的首位字符相等,则继续取出两个字符串各自的后一位字符进行比较。 重复上述步骤进行比较,直到比较完成某字符串的所有字符为止。 如果两个字符串的字符同时用完,那么则判定它们相等,否则未结束(还有未比较的字符)的字符串更大。 !其他语言字符串的比较: 所有现代浏览器(IE10- 需要额外的库 Intl.JS) 都支持国际化标准 ECMA-402。 它提供了一种特殊的方法来比较不同语言的字符串,遵循它们的规则。 调用 str.localeCompare(str2) 会根据语言规则返回一个整数,这个整数能表明 str 是否在 str2 前,后或者等于它: 如果 str 小于 str2 则返回负数。 如果 str 大于 str2 则返回正数。 如果它们相等则返回 0。 不同原始类型之间的比较 首先转换为数字类型,然后再比较大小。 对于布尔类型值,true 会被转化为 1、false 转化为 0。 严格比较 严格相等比较时,不进行任何类型转换。二者必须完全一致才返回True. === 严格相等 !== 严格不相等 ★ null、NaN 与 undefined 参与的比较 ★ NaN与任何值进行比较都返回false; null === undefined // false,二者属于不同的类型; null == undefined //true; 这是JavaScript强制规定。null只与undefined相等 ,各自不与任何其他变量值相等; 当使用数学式或其他比较方法 < > <= >= 时: null/undefined 会被转化为数字:null 被转化为 0, undefined 被转化为 NaN。(NaN与任何值进行比较都会返回false。所以尽量永远不让undefined参与比较。) !注意: ① 除了严格相等 === 外,其他但凡是有 undefined/null 参与的比较,我们都需要格外小心。 ② 除非你非常清楚自己在做什么,否则永远不要使用 >=、>、<、<=去比较一个可能为 null/undefined 的变量。对于取值可能是 null/undefined 的变量,请按需要分别检查它的取值情况。

现代JS学习笔记:Generator(生成器)

2021.03.10

JavaScript

学习内容:《现代JavaScript教程》 14 Generator (生成器) 14.1 Generator基本知识 Generator生成器函数,顾名思义,就是一个能按照既定的规则,连续产生多个数据的函数。它区别于常规的函数,因为函数只能返回一个值。它的创建方法是: 创建一个Generator函数声明,需要在function关键字后面加一个星号: function* <functionName>(){}; 在对象内部定义Generator函数方法,只需在方法名前加一个星号*: { *fun1(){…}, } 在Generator函数中可以使用yield关键字(yield的本意是生产、产出),定义Generator对象每次产出的值。 调用Generator函数会返回一个Generator对象,可以将其赋值给变量。每一个Generator对象都有next()方法,可以根据Generator内部yield的value依次返回以下格式的对象,直到最后的retrun。 ` {value: ,done: <true/false>} ` 例如: function* generator1(){ yield 1; yield 2; yield 3; return 4; } let a = generator1(); //调用生成Generator对象赋值给变量a let i = true; while(i){ let temp = a.next(); console.log(temp); i = !temp.done; } // 结果如下: 14.2 Generator对象是可迭代的Iteratable 一个Generator对象,是一个可迭代(Iteratable)对象。也就是说,它可以用for…of迭代,也可以使用spread符号(…)展开为独立的参数列表。 例如,针对上小节的a对象,可以这样迭代使用: for (let value of a){ console.log(value); } // 1,2,3 console.log(…a); // 1 2 3 注意: Generator对象中只有使用yield关键字声明的value才会被迭代返回,而return的参数不会。 Generator函数的产生使得对象自定义Symbol.iterator方法的编写更加明晰和方便。 let range = { from: 1, to: 5, *[Symbol.iterator](){ // [Symbol.iterator]: function*() 的简写形式,返回Generator。 for(let value = this.from; value <= this.to; value++) { yield value; } } }; alert( [...range] ); // 1,2,3,4,5 14.3 Generator合并 如果想将多个Generator对象yield的值传入另一个Generator函数中,可以使用yield* 这个语句。 14.4 yield是一条双向路:既可以传出,也可以传入 单独使用yield value表示传出value到Generator外。 将yield value赋值给一个变量a,则代表让Generator函数在此停止运行,等待在下次next()方法被调用的时候,传入一个参数value,也就是下次调用是next(value)这个形式。这个传入的value会自动传入Generator对象内部并赋值给变量a。 function* gene1(){ yield 1; let a = yield 'what is your lucky number?'; console.log('your lucky number is: '+a); yield 3; } let g = gene1(); 14.5 generator.throw() 也可以在yield那里发起(抛出)一个 error,因为 error 本身也是一种结果,可以传入yield那一行赋值的变量a。要向 yield 传递一个 error,我们应该调用 generator.throw(err)。在这种情况下,err 将被抛到对应的 yield 所在的那一行。 可以在Generator函数内部使用try…catch捕获这个传入的Error。 14.6 使用Generator进行异步迭代 在generator声明前加上async关键字,可以使generator声明为异步Generator。同时,其内部可以使用await关键字进行promise等待与解析。 这样异步generator,常规使用for…of进行迭代也是不行的,会报错(not iteratble)。必须使用for await…of这样的异步迭代方式进行迭代(相当于是异步可迭代对象,而非普通可迭代对象)。例子如下:

现代JS学习笔记:DOM及其操作

2021.03.10

JavaScript

学习内容:《现代JavaScript教程》 17 DOM(Document Object Model) 17.1 DOM DOM将一个页面的所有内容转化为可以被JS获取、修改的对象树,包括页面的根(document)、元素、文本、注释等。 一个HTML页面的DOM树大概是这种结构: 17.2 DOM节点类 每一个DOM节点根据自身类型的不同,可能具有自身不同的属性。但是他们都遵循一定的继承规律,因而才能共享一些方法属性。 DOM节点的继承关系如下: EventTarget — 是根的“抽象(abstract)”类。该类的对象从未被创建。它作为一个基础,以便让所有 DOM 节点都支持所谓的“事件(event)”,我们会在之后学习它。 Node — 也是一个“抽象”类,充当 DOM 节点的基础。它提供了树的核心功能:parentNode,nextSibling,childNodes 等(它们都是 getter)。Node 类的对象从未被创建。但是有一些继承自它的具体的节点类,例如:文本节点的 Text,元素节点的 Element,以及更多异域(exotic)类,例如注释节点的 Comment。 Element — 是 DOM 元素的基本类。它提供了元素级的导航(navigation),例如 nextElementSibling,children,以及像 getElementsByTagName 和 querySelector 这样的搜索方法。浏览器中不仅有 HTML,还会有 XML 和 SVG。Element 类充当更多特定类的基本类:SVGElement,XMLElement 和 HTMLElement。 HTMLElement — 最终是所有 HTML 元素的基本类。各种 HTML 元素均继承自它: HTMLInputElement — <input> 元素的类, HTMLBodyElement — <body> 元素的类, HTMLAnchorElement — <a> 元素的类, ……等,每个标签都有自己的类,这些类可以提供特定的属性和方法。 因此,给定节点的全部属性和方法都是继承的结果。 17.3 DOM节点的属性 17.3.1 nodeType 过时的获取DOM节点类型的方法:每一个DOM类型有自己的nodeType值,是一个数字,对于元素节点 elem.nodeType == 1,对于文本节点 elem.nodeType == 3等。 现在可以使用 instanceof 操作符查看节点类型。 17.3.2 nodeName、tagName 获取节点的名称。nodeName支持所有DOM节点,tagName仅支持元素节点。 一般使用nodeName即可。tagName不如nodeName强大。 17.3.3 innerHTML、outerHTML innerHTML 属性允许将元素中的 HTML 获取为字符串形式,可直接修改。 outerHTML 属性包含了元素的完整 HTML,就像 innerHTML 加上元素本身一样。也可以直接修改。 !注意: 直接修改innerHTML和outerHTML都会造成修改部分的刷新。 即使是使用+=这种部分修改的方法,DOM引擎也会先删除所有原有内容,然后加入修改后的内容。 17.3.4 textContent textContent 提供了对元素内的文本的访问权限:仅文本,去掉所有 <tags>。 修改textContent的内容,如果里面包含类似HTML标签的文本,也会按照文本对待,不会真正地变成HTML。 17.3.5 HTML特性:写在tag标签内可识别的属性 HTML标准规定了各类型元素的一些标准特性(Attribute),在HTML中可以在tag标签内书写,并直接可以通过DOM节点访问(被自动识别)。比如<input>的value特性,<a>的href特性,各元素的id特性等。 HTML特性是大小写不敏感的,而且总是字符串类型的。 注意: 属性Property和特性Attribute的区别:属性Property指的是JS对象内部储存的键值对,无论是标准的还是自定义非标准的。在DOM对象中既可以是HTML标准特性、原生属性也可以是自定义属性,都叫做Property。而特性Attribute指的是HTML元素中标准规定的一些属性,也就是常用于写在Tag内的那些标准属性。 ★ HTML标准里规定的特性,都可以被浏览器自动识别,可直接通过DOM元素的同名属性访问。(注意必须是HTML规定的标准特性) ★ 非标准属性,比如自定义属性,不能直接访问,需要通过专用API: DOMelement.hasAttribute(name) — 检查特性是否存在。 DOMelement.getAttribute(name) — 获取这个特性值。 DOMelement.setAttribute(name, value) — 设置这个特性值。 DOMelement.removeAttribute(name) — 移除这个特性。 使用这些方法后,对应DOM对象的属性也会更新。 DOMelement.setAttribute(‘class’, ‘value’); DOMelement.class; //value 17.3.5.1 HTML特性和DOM属性有时存在差异 比如:<input>元素内的checked特性,在使用.getAttribute()方法返回的是空字符串,而使用DOMelement.checked获取的是true布尔值。 <a>标签的href=’#here’特性,使用getAttribute()获取到的是字符串’#here’,而DomElement.href获取的是完整的URL。 也就是说,getAttribute()方法会照着HTML中书写的原样返回字符串。而DOM属性会返回JS想要的结果。 17.3.6 非标准的HTML特性:dataset 可以在HTML标签中使用非标准的自定义特性,然后在JS中用getAttribute/setAttribute方法获取与设置。 但是为了防止冲突,预留了以data-开头的特性用于自定义使用。以data-开头的特性,可以在DOM对象中使用DOMelement.dataset中对应属性找到。比如: <div id=’obj’ data-pool=’swim’></div> 中的data-pool属性,可以使用obj.dataset.pool获取。 data-time-counter这类使用短横线连接的自定义特性名,属性中使用驼峰法获取:dataset.timeCounter。 17.3.7 自定义属性 也可以随意为DOM元素添加属性,使用自定义的命名。但需要注意他们是大小写敏感的。比如:可以设置document.body.say = function(){ alert(‘Hey!’) } 17.4 遍历DOM节点 17.4.1 顶层节点<html>、<body>、<head> document.documentElement 对应 <html>节点; document.body 对应 <body>节点; document.head 对应 <head>节点。 17.4.2 DOM节点的遍历属性 如上图所示,每一个DOM节点都有遍历其他节点的方法。这些遍历是在纯DOM节点层面的,里面包含了html的各种类型节点,包括可能不关注的文本节点等。 如果想在纯元素节点之间遍历,使用下面的这些属性。 17.5 DOM对象的创建、修改与插入 17.5.1 创建DOM节点 document.createElement(tag): 创建元素节点; document.createTextNode(text): 创建文本节点。 17.5.2 修改DOM节点 使用DOM对象的属性,直接修改新创建DOM对象。 class特性,需要使用domElement.className修改,因为class是JS中保留关键字。 17.5.3 插入DOM节点 以下方法用于插入DOM节点: node.append(…nodes or strings) —— 在 node 末尾 插入节点或字符串, node.prepend(…nodes or strings) —— 在 node 开头 插入节点或字符串, node.before(…nodes or strings) —— 在 node 前面 插入节点或字符串, node.after(…nodes or strings) —— 在 node 后面 插入节点或字符串, node.replaceWith(…nodes or strings) —— 将 node 替换为给定的节点或字符串。 这些方法在参数为字符串时,插入到HTML后不会被识别为HTML代码,而是各种符号都被转义的字符串。如果想要使用字符串形式,为HTML文档动态添加可识别的HTML代码,需要使用DOM对象的.insertAdjacentHTML()方法: elem.insertAdjacentHTML(where, html)。该方法的第一个参数是代码字(code word),指定相对于 elem 的插入位置。必须为以下之一: “beforebegin” — 将 html 插入到 elem 前插入, “afterbegin” — 将 html 插入到 elem 开头, “beforeend” — 将 html 插入到 elem 末尾, “afterend” — 将 html 插入到 elem 后。 17.5.4 替换DOM节点 DOMelement.replaceWith()可以用于原地替换一个DOM节点。 17.5.5 删除DOM节点 使用DOMelement.remove()方法移除节点。 17.5.6 移动DOM节点 获取并移动一个DOM节点到另一个位置,引擎会自动将原HTML元素删除,然后在新的位置插入。(无需手动删除原节点)。 17.5.7 克隆DOM节点 使用DOMelement.cloneNode( )来克隆一个DOM节点。 当传入参数true时,为深克隆(全部子元素都克隆); 当传入参数False,为浅克隆,克隆将不包含子元素。 17.6 使用DOM修改、获取CSS样式 17.6.1 直接修改style属性(元素样式) DOM元素自身具有Style属性,可以直接使用JS访问修改。它对应的是HTML中这个元素的Style特性,也只对应HTML中的style特性(无法访问其他形式加入的CSS内容,比如外部样式表)。 elem.style.width="100px" 的效果等价于我们在 style 特性中有一个 width:100px 字符串。 多词属性,需要进行驼峰化处理才能访问。 这种修改方式的特点如下: 它比通过CSS Class修改的优先级要高(内联),默认会覆盖CSS Class中相同的内容; style.css只能逐一修改CSS属性(可能造成多次回流、重绘); 如果需要像定义CSS一样,一次传入一个字符串作为Style,需要使用style.cssText = ;(这样操作会删除所有现有已定义的style特性) 只有需要复杂计算才能获取的CSS属性,需要通过这样的方式修改。其他情况一般默认应采用CSS Class修改。 17.6.2 创建、修改CSS Class 17.6.2.1 className 早期的JS不允许设置‘class’为对象属性名,因为是保留关键字。所以早期使用className作为获取DOM元素CSS Class的属性名。 className设置的是DOM元素完整的字符串,一旦修改整个class都会被替换。 17.6.2.2 classList DOM元素还有一个classList属性,它是一个可迭代对象,里面记录着这个DOM元素上绑定的所有CSS Class。它自带四个方法: elem.classList.add/remove(class) — 添加/移除类。 elem.classList.toggle(class) — 如果类不存在就添加类,存在就移除它。 elem.classList.contains(class) — 检查给定类,返回 true/false。 17.6.3 获取最终CSS样式 使用.style只能获取定义在style特性中的CSS样式,不能获取最终的元素CSS。 使用全局环境下的getComputedStyle(DOMelement,[pseudo])方法,可以返回计算后的(Computed)CSS结果对象,然后按需获取想要的结果。 17.7 DOM元素的各种几何属性 DOM元素自身具有一些反应自身几何性质的属性。这些属性所反应的信息如下图所示。 17.7.1 offset参数 带有offset的参数反应的是元素的外边界相关的参数。(元素的外边界,指的是元素border外的的最外层边界) offsetParent: DOM元素的offset基准元素。offsetParent 是最接近的祖先(ancestor),在浏览器渲染期间,它被用于计算坐标。 它是下列元素之一:CSS 定位的(position 为 absolute,relative 或 fixed),或 <td>,<th>,<table>,或 <body>。 offsetLeft、offsetTop:元素左上角点到offsetParent左上角点的横向、纵向距离; offsetWidth、offsetHeight: 元素外边界的宽度和高度; 17.7.2 client参数 带有client的参数反应的是元素的内边界相关的参数。(元素的内边界,指的是元素border内的不包含滚动条的边界) clientTop、clientLeft: 内边界左上角点到外边界左上角点的水平、垂直距离;(在一般条件的盒子模型中就是边界厚度) clientWidth、clientHeight: 内边界的宽度和高度; 17.7.3 scroll相关参数 是可滚动部分的相关参数。 scrollTop、scrollLeft:可滚动内容的上边界到现在内边界上边缘的距离; (已滚动的距离) scrollWidth、scrollHeight: 可滚动部分的总宽度、高度; 17.7.4 为什么不建议从CSS获取元素几何参数? ① CSS中元素的几何参数,还取决于CSS box-sizing属性,因此不准; ② CSS中获取到的几何参数,有可能是’auto’; ③ 因为滚动条的存在,不同浏览器返回的CSS属性有差异。 ★ clientWidth与getComputedStyle(elem).width的区别? clientWidth 值是数值,而 getComputedStyle(elem).width 返回一个以 px 作为后缀的字符串。 getComputedStyle 可能会返回非数值的 width,例如内联(inline)元素的 “auto”。 clientWidth 是元素的内部内容区域加上 padding,而 CSS width(具有标准的 box-sizing)是内部内容区域,不包括 padding。 如果有滚动条,并且浏览器为其保留了空间,那么某些浏览器会从 CSS width 中减去该空间(因为它不再可用于内容),而有些则不会这样做。clientWidth 属性总是相同的:如果为滚动条保留了空间,那么将减去滚动条的大小。 17.7.5 获取窗口的width/height(布局视口Layout Viewport) 获取整个窗口(可视部分)的width或者height,需要使用document.documentElement 的 clientWidth/clientHeight: window.innerHeight 和 window.innerWidth 都是包含了滚动条的。 document.documentElement.clientWidth是不包含滚动条的。 17.7.6 获取整个文档的width/height 理论上使用document.documentElement.scrollWidth/scrollHeight. 但是,由于种种历史原因,document.documentElement.scrollHeight经常不返回整个文档的高度,而是别的数据。因此必须使用以下方法: let scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight ); 17.8 DOM元素的坐标 17.8.1 坐标系 大多数 JavaScript 方法处理的是以下两种坐标系中的一个: 相对于视口 — 类似于 position:fixed,从视口的顶部/左侧边缘计算得出。 我们将这些坐标表示为 clientX/clientY,当我们研究事件属性时,就会明白为什么使用这种名称来表示坐标。 相对于文档 — 与文档根(document root)中的 position:absolute 类似,从文档的顶部/左侧边缘计算得出。 我们将它们表示为 pageX/pageY。 17.8.2 获取DOM元素的视口坐标 每个DOM元素都有一个getBoundingClientRect()方法,它返回一个对象(DOMRect对象),里面包含了各种这个元素相对于窗口的坐标属性。 17.8.3 获取视口中某一坐标的元素 document.elementFromPoint(x,y)可以获取当前窗口的x,y坐标点,嵌套最多的DOM元素。(指哪打哪!) 17.8.4 获取DOM元素的文档坐标 思路是:先获取DOM元素的窗口坐标,然后获取整个窗口的滚动参数(window.pageXOffset、window.pageYOffset),然后将两者相加。 17.9 页面滚动 17.9.1 获取当前窗口的滚动参数 window.pageXOffset和window.pageYOffset可以获取当前窗口的滚动位置参数。但是这两个参数都是只读的。 17.9.2 直接使用JS代码操作页面滚动 有三种方式可以使用JS操作页面滚动: 可以通过直接赋值给document.documentElement.scrollTop/scrollLeft来实现页面的滚动; window.scrollTo(x,y)和window.scrollBy(delta_x,delta_y)可以实现直接滚动到xy和相对当前位置步进滚动delta_x,delta_y的功能。 ★ DOMElement.scrollIntoView(<Boolean>);这个方法可以使任何DOM节点立即跳转到窗口内。(当传入的Boolean为true,节点位于窗口最上方。当为false,在窗口最下方。) 17.9.3 禁止页面滚动 要使文档不可滚动,只需要设置 document.body.style.overflow = "hidden"。该页面将“冻结”在其当前滚动位置上。 document.body.style.overflow = “” 可以恢复滚动。 这个方法滚动条会消失,所以页面会有点变化。 17.10 ★DOM事件 17.10.1 绑定DOM事件的方法 17.10.1.1 HTML标签特性 直接在HTML标签内写onclick、onload这类特性。 这种显式传入的方式,函数尾部应该带有括号。 17.10.1.2 JS获取DOM元素并绑定 用JS获取DOM元素,然后绑定onload、onclick这类方法。(传入函数体,而不是调用的函数) 上面这两种方法会互相覆盖。 DOM事件中This就是发生事件的那个元素本身。 17.10.1.3 addEventListener() 使用addEventListener()可以为DOM元素添加多个事件。 element.addEventListener(event, handler[, options]); event:事件名,例如:”click”; handler:处理程序; options:具有以下属性的附加可选对象: once:如果为 true,那么会在被触发后自动删除监听器。 capture:<true/false>元素在捕获还是冒泡阶段发生(true: 捕获, false:冒泡)。由于历史原因,options 也可以是 false/true,它与 {capture: false/true} 相同。 passive:如果为 true,那么处理程序将不会调用 preventDefault(). 使用removeEventListener()去除DOM元素上绑定的事件。 17.10.2 事件对象 DOM事件发生的时候,会生成一个事件对象event,里面记载了这次事件的一些信息。这个event对象可以通过事件的handler参数进行引用。 17.10.3 使用对象作为处理程序handleEvent addEventListener()第二个参数不仅可以传入函数作为事件,还可以传入对象obj。 这种情况下,将默认使用obj.handleEvent()作为事件。 为什么要使用对象呢? 因为一个对象内可以定义多个方法,这样每一个单独的功能就能独立出来,单独起一个函数名,这样的代码更直观容易维护、修改。 17.10.4 事件的冒泡和捕获 17.10.4.1 事件冒泡 默认情况下,几乎所有的事件都以冒泡的形式发生。(focus事件不冒泡) 17.10.4.1.1 什么是事件冒泡? 一个元素,它被包裹在它的父级元素中,然后还有更高层的元素包裹。这些元素上可能都被挂载了自己的事件。(比如click) 冒泡的意思,就是当你在一个内部元素上触发事件(click),它先在最内层的元素上发生,然后是上一层父级元素的事件,逐级直到最顶层元素事件完毕。像一个气泡一样,从最下面一直传递到最上面。 17.10.5 阻止浏览器默认行为 浏览器的默认行为,比如点击链接会跳转等,可以被手动取消。总共有两种方法: event.preventDefault()方法; 使用on<event>分配事件时,可以返回false表示阻止浏览器默认行为。 17.11 页面生命周期 以下事件按页面生命流程发生。 17.11.1 DOMContentLoaded 此时浏览器已完全加载 HTML,并构建了 DOM 树,但此时像 <img> 和样式表之类的外部资源可能尚未加载完成。 这个事件的使用方法是: document.addEventListener(‘DOMContentLoaded’, func); 17.11.2 load 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。 这个事件在window对象上挂载。 window.onload = func; 17.11.3 beforeunload 当用户点击离开,或者试图关闭页面,会触发这个事件函数。 window.onbeforeunload = func; 这里注意:func的返回值有重要意义,func返回False或者一个字符串的时候,浏览器会触发弹出离开前的确认信息。 因为这个功能经常被滥用,现在返回字符串也不会被显示出来,只是当做返回了false。 17.11.4 unload 当访问者离开页面时,window 对象上的 unload 事件就会被触发。我们可以在那里做一些不涉及延迟的操作,例如关闭相关的弹出窗口。 17.11.5 监测页面生命周期变化的方法 document.readyState 记载了页面的加载状态。它有四种取值: uninitialized - 还未开始载入 loading - 载入中 interactive - 已加载,文档与用户可以开始交互 complete - 载入完成 可以在document上加载一个readystatechange监听器,当页面生命周期发生变化,会执行相应函数。 17.12 <script>的同步加载与异步加载 17.12.1 同步加载 <script>默认情况下在页面中是同步加载的,也就是说当浏览器对HTML解析到script标签时,会立即下载+执行里面的代码,整个页面停止加载,进行等待,直到JS下载+执行完毕。 17.12.2 异步加载 17.12.2.1 defer 在Script标签中加入defer 特性,就是告诉浏览器不要等待这个脚本下载。浏览器将继续加载后面的 HTML,构建 DOM。JS脚本会异步下载(与页面加载并发),然后等 DOM 解析完成后,脚本才会执行。 defer标记的脚本,会在DOM解析完成后(解析到</html>),但DOMContentLoaded事件触发前,按照声明的先后顺序执行。 排在后面的defer脚本,即使先加载完成,也等待前面的加载执行完毕后再执行。 17.12.2.2 async 在下载方面,async 与 defer 相同,都是让脚本不阻塞页面加载,异步下载。 但是async标记的script不会等待任何其他DOM元素或Script的加载或执行,它是完全独立的,独立加载+执行,下载完了立即执行,可能发生在页面周期的任何时候。 17.12.3 动态脚本加载 在JS中创建script对象,然后添加到页面中的脚本。叫做动态脚本。 动态脚本默认和带有async关键字是一样的:立即加载,按加载完成先后顺序执行,与代码内挂载的顺序无关。 但如果手动设置了async = false,则以挂载先后顺序执行,就是defer。 17.13 其他资源的加载 onload和onerror几乎可以用在带有src特性的任何DOM元素上。 图片 <img>,外部样式,脚本和其他资源都提供了 load 和 error 事件以跟踪它们的加载: load 在成功加载时被触发。 error 在加载失败时被触发。 唯一的例外是 <iframe>:出于历史原因,不管加载成功还是失败,即使页面没有被找到,它都会触发 load 事件。 17.14 DOM变动观察器 MutationObserver 是一个内建对象,它观察 DOM 元素,并在检测到更改时触发回调。具体见:Mutation Observer 17.15 事件循环 当浏览器没有任务执行时,处于休眠状态。当任务出现,则按照出现的先后顺序执行任务,先进入的任务先执行。 17.15.1 宏任务 以下内容被称为宏任务,这些任务按照出现的顺序在浏览器内部组成一个序列,按照进入的先后顺序执行,先进先出。 Js脚本:当外部脚本 <script src="..."> 加载完成时,任务就是执行它。 事件回调:例如当用户移动鼠标时,任务就是派生出 mousemove 事件和执行处理程序。 定时器:当安排的(scheduled)setTimeout 时间到达时,任务就是执行其回调。 宏任务执行的间隙,如果有微任务,则浏览器先执行微任务,然后执行DOM渲染。在一个宏任务的执行过程中不进行DOM渲染,完成后才进行。 17.15.2 微任务 微任务仅来自于我们的代码。它们通常是由 promise 创建的:对 .then/catch/finally 处理程序的执行会成为微任务。微任务也被用于 await 的“幕后”,因为它是 promise 处理的另一种形式。 还有一个特殊的函数 queueMicrotask(func),它手动添加func到微任务队列,以在下次执行时机执行。 每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。

现代JS学习笔记:原型与继承

2021.03.09

JavaScript

学习内容:《现代JavaScript教程》 1. 原型与继承 1.1 什么是原型 原型,通俗的理解就是一个东西最普通,最广泛,最标准,不带有任何特色的原始模型。 比如说橘子,可能它细分有很多品种:有八瓣的有十瓣的,有剥皮吃的有不剥皮吃的,有酸的有甜的。但是,所有的橘子通常讲都应该是分瓣的,都应该有皮,都应该是是树上结出来的,这就是橘子的原型具有的性质。橘子的原型就是一个最普通、最大众的橘子,所有的具体的橘子品种都应该含有橘子原型的特点,然后才是各个橘子自身的特点。 一个对象的原型理论上是这个对象高一层级类别的对象,所具有的属性相对这个对象应该更抽象化。这个对象是原型对象的一种特例。 1.2 原型继承 所有的对象,都有一个内置的,隐藏的[[prototype]]属性,它的值要么是一个对象,要么是null。 [[prototype]]属性是不直接访问的,需要通过对象的__proto__属性访问。 __proto__属性不是直接访问[[prototype]],而是一对getter/setter,用于读取或修改[[prototype]]。 对象obj的[[prototype]]属性值对象,就是该对象obj的原型对象。 ★ 写入和删除从原型对象继承来的属性,不会对原型对象本身的属性产生影响,而是操作在继承的对象中。(当写入的属性在对象本身内部不存在,就会在本地新建一个属性,无论原型链中是否有这个属性。可以理解为这些继承来的属性都是只读的,写入还是在本地。) for..in 循环会遍历到本身的属性和继承的属性。所有其他的键/值获取方法仅对对象本身起作用,不返回继承的属性。 对象在查询属性时,如果查询的不是对象自身的属性,对象就会自动沿着原型链向上寻找,直到找到最近的并返回。如果到了原型链顶端仍未找到,就返回undefined。 1.3 函数(构造函数)的prototype属性 任何一个函数都自带一个键名为prototype的常规属性; 这个属性值在使用new操作符操作函数时候,会自动成为生成新对象的原型[[prototype]]对象; 一般用于构造函数,手动设置这个prototype属性可以变更构造实例对象的默认原型对象; 普通非构造函数也自动带有这个属性,只不过默认的 “prototype” 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。(默认情况下,所有函数都有 F.prototype = {constructor:F},所以我们可以通过访问它的 “constructor” 属性来获取一个对象的构造器。) 1.4 原生的原型 Object()、Array()、Date()这类构造函数是JavaScript语言内部自带的,我们无需自己定义就能使用。 这些原生的构造函数也存在自己的prototype属性,内部已经写好了各种有用的方法,比如toString方法。 这样我们在创建一个对象或数组实例的时候,就自动继承了Object.prototype或Array.prototype,它们里面的方法就可以在我们创建的对象里使用了。 规范规定:所有原生原型顶端都是 Object.prototype。也就是说,Object.prototype上端再没有其他原型对象,Object.prototype.__proto__会返回null; 其他原生原型的原型对象,都是Object.prototype,除了Object自身之外。例如: Array.prototype.__proto__== Object.prototype; 基本数据类型,比如String、Number和Boolean,也有自己的原生构造函数。虽然基本数据类型变量本身没有属性,但是当试图访问一个基本类型变量的属性时,为了使这些基本类型也能使用一些方法,一个临时的包装器对象会被创建,使用的正是这些原生的构造函数。他们也有自己的prototype。比如Number.prototype对象里面就含有toFixed()方法。 !永远尽量不要更改原生原型。 例如Number.prototype.someFunction = ….这样的修改会直接传递到所有数字包装器对象上,所有人在这个对象上修改会互相影响,很容易出现错误。 1.5 直接访问原型的几种现代方法 Object.create(proto, [descriptors]) —— 利用给定的 proto 作为 [[Prototype]](可以是 null)和可选的属性描述来创建一个空对象。 Object.getPrototypeOf(obj) —— 返回对象 obj 的 [[Prototype]](与 proto 的 getter 相同)。 Object.setPrototypeOf(obj, proto) —— 将对象 obj 的 [[Prototype]] 设置为 proto(与 proto 的 setter 相同)。 Object 与 Function 的原型关系

现代JS学习笔记:Set集合

2021.03.09

JavaScript

学习内容:《现代JavaScript教程》 9 Set:集合 9.1 什么是Set Set 是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次。 9.2 Set的特点 每个元素只可能在同一个Set里出现一次。 使用Set比每次调用arr.find()查找对比更迅速。Set 内部对唯一性检查进行了更好的优化。 重复使用同一个值调用 set.add(value) 并不会发生什么改变。这就是 Set 里面的每一个值只出现一次的原因。 9.3 Set的方法 new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。 比如a是一个数组,则可以直接使用new Set(a)创建Set。 set.add(value) —— 添加一个值,返回 set 本身 set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false。 set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false。 set.clear() —— 清空 set。 set.size —— 返回元素个数。 9.4 Set的遍历 可以使用for…of遍历Set数据。 与Map一样,Map 中用于迭代的方法在 Set 中也同样支持: set.keys() —— 遍历并返回所有的值(returns an iterable object for values), set.values() —— 与 set.keys() 作用相同,这是为了兼容 Map, set.entries() —— 遍历并返回所有的实体(returns an iterable object for entries)[value, value],它的存在也是为了兼容 Map。 在 Map 和 Set 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。 也可以使用如同数组的forEach方法,传入一个函数,对Set对象进行遍历。 9.5 WeakSet WeakSet 的表现类似 WeakMap: 与 Set 类似,可以保证weakSet内部没有重复的元素,但是我们只能向 WeakSet 添加对象(而不能是其他原始类型值); 对象只有在其它某个(些)地方能被访问的时候,才能留在 set 中; 跟 Set 一样,WeakSet 支持 add,has 和 delete 方法,但不支持 size 和 keys(),并且不可迭代。

现代JS学习笔记:Map映射

2021.03.09

JavaScript

学习内容:《现代JavaScript教程》 Map:映射 什么是Map Map是一种带键的数据项的集合,是一种特殊的Object。 Map的特点 Map与普通对象Object数据结构的最大不同是: Map允许键名是任何类型(包含null,undefined和NaN),而Object只有字符串类型的键名(即使传入其他类型也会自动转换为String类型)。 Map的方法和属性 new Map() —— 创建 map。 map.set(key, value) —— 根据键存储值。(返回Map本身,可以链式调用) map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined。 map.has(key) —— 如果 key 存在则返回 true,否则返回 false。 map.delete(key) —— 删除指定键的值。 map.clear() —— 清空 map。 map.size —— 返回当前元素个数。 Map的遍历 实现Map数据结构的遍历,有三种方法: map.keys() —— 遍历并返回所有的键(returns an iterable for keys), map.values() —— 遍历并返回所有的值(returns an iterable for values), map.entries() —— 遍历并返回所有的实体(returns an iterable for entries)[key, value],for..of 在默认情况下使用的就是这个。 在 Map 和 Set 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。 Map与Object的互相转换 从普通Object生成Map 使用Object.entries()。 这个函数可以把普通对象转换为键值对数组组成的数组。 然后使用这个数组传入new Map()即可生成对应Map。 let mapObj = { good: 'boy', say(){ console.log(this.good); } }; let map1 = new Map(Object.entries(mapObj)); console.log(map1.get('good')); //’boy’ 从Map生成普通Object Map()构造函数可以接受一个由键值对组成的数组[key, value]为元素,构成的数组作为参数,使用每一个元素的键值对自动生成Map。 let a = new Map( [[key1, value1],[key2, value2]] ); // 自动生成Map: key1对应value1,key2对应value2. Map.entries()可以返回一个可迭代的键/值对,可供Object.fromEntries()使用生成对应普通Object。 所以使用Object.fromEntries(map.entries())就可以把Map转为Object。 还可以省略,直接传入Map。Object.fromEntries(map) WeakMap 弱映射 WeakMap弱映射,它有两个特殊性: 只能接受对象作为键名,其他类型无效; 键名引用的对象,外部全部失去引用后,即使在Map内存在,也会被垃圾回收机制识别并回收。 常规的Map有一个垃圾回收机制的问题。 当Map的键是一个对象时,即使对象在外部被设置为null,Map也依然在引用着该对象,依然存储在内存中,不会被当做垃圾清除。 WeakMap可以解决这个问题。当一个对象仅仅是作为 WeakMap 的键而存在 —— 它将会被从 map和内存中自动删除。 // 创建方式: let a = new WeakMap(); WeakMap的方法 WeakMap 不支持迭代以及 keys(),values() 和 entries() 方法。所以没有办法获取 WeakMap 的所有键或值。 WeakMap 只有以下的方法: weakMap.get(key) weakMap.set(key, value) weakMap.delete(key) weakMap.has(key) 因为不能确定浏览器的垃圾回收时机(即使外部对象被解除引用,weakmap里面的元素也可能不会瞬间立即被删除,而是要等待垃圾回收的时机。),所以WeakMap里面的元素数量是不能确定的,因此没设有keys这一类方法。 WeakMap应用场景 计算结果缓存 缓存函数计算结果,如果入参obj是引用类型,使用WeakMap可以在入参obj被销毁的时候,同步自动消除缓存的结果数据,不需手动消除。 let cache = new WeakMap(); function A(obj) { if (cache.has(obj)) return cache.get(obj); // ... some calculate... cache.set(obj, res); return res; } obj = null; // obj in cache is destoryed automatically. (no more need) 储存外部引入的数据 从外部引入的数据,想要临时保存在本地,但是又希望不因为本地的引用而影响外部变量本身的垃圾回收(与外部变量共存亡),可以使用WeakMap。

Vue.js 学习笔记

2020.04.14

Vue

学习内容:《Vue.js 官方教程》 1. 声明式渲染 通过new Vue()可声明一个 Vue应用,其接受一个对象参数,在这个参数中用el属性标明Vue对象挂载的HTML元素,用data属性记载对象的数据值。 <div id="app"> {{ message }} </div> <script> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }) </script> 2. 指令 Vue指令-思维导图 在Vue应用所绑定的HTML元素上,添加的带有v-前缀的属性叫做指令。 2.1 v-bind 指令 缩写: : v-bind指令用于将HTML上元素的某一属性与Vue应用内的某一属性绑定在一起(共同变化)。 <div id="app-2"> <span v-bind:style="cssContent"> 你看到的我是蓝色的。 </span> </div> <script> var app2 = new Vue({ el: '#app-2', data: { cssContent: 'color: blue;' } }) </script> 2.2 v-if 指令 (条件渲染) 元素设置了v-if指令时,只有在v-if指令表达式返回true值时,元素才会被渲染,否则将不被渲染。 <div id="if" v-if='show'>看得到吗?</div> <script> let iiff = new Vue({ el: '#if', data: { show: false } }); // 此時看不到#if這個元素。 </script> 2.3 v-else 指令 (条件渲染之后,如果为假则渲染) v-else指令表示条件渲染之后,如果为假则渲染此元素。 (v-else元素必须紧跟在带v-if或者v-else-if的元素的后面,否则它将不会被识别。) <div v-if="Math.random() > 0.5"> Now you see me </div> <div v-else> Now you don't </div> 2.4 v-else-if 指令 (条件渲染之后,继续添加条件渲染块) 表示 v-if 的“else if 块”。可以链式调用。 (v-else-if元素必须紧跟在带v-if或者v-else-if的元素的后面,否则它将不会被识别。) <div v-if="type === 'A'"> A </div> <div v-else-if="type === 'B'"> B </div> <div v-else-if="type === 'C'"> C </div> <div v-else> Not A/B/C </div> 2.5 v-show 指令 (条件显示) 根据v-show表达式之真假值,切换元素的 display CSS 属性。 2.6 v-for 指令 (遍历渲染) v-for指令表示利用目标元素的可遍历性,遍历多次渲染其每一内部元素。 【接受数据类型】 Array | Object | number | string | Iterable (2.6 新增) 2.6.1 v-for 指令通常用法 使用语法alias in expression遍历数组内每一元素。 (不能使用在Vue应用绑定的根元素上,必须是内部的子元素) <div id='app'> <div v-for='item in list'> {{ item.content }} </div> </div> <script> let app = new Vue({ el: '#app', data:{ list: [{content:1},{content:2},{content:3}] } }); </script> 2.6.2 v-for 指令同时遍历数组的内容和索引 可以用一个圆括号内的两个变量对目标变量进行遍历,其遍历结果第一个为遍历结果本身,第二个为该遍历结果在原元素内的索引值。 <div id='app'> <div v-for='(a,b) in list'> {{ a }} </div> </div> <script> let app = new Vue({ el: '#app', data:{ list: [{content:1},{content:2},{content:3}] } }); </script> 显示结果: { “content”: “1” }0 { “content”: “2” }1 { “content”: “3” }2 2.6.3 v-for 指令遍历对象属性 也可以用 v-for 来遍历一个对象的属性。 可以使用单独的变量来遍历,也可以使用圆括号括着的两个或三个变量来遍历。 当使用一个变量时,遍历结果为每一属性的值; 当使用两个变量时,遍历结果第一个为每一属性的值,第二个为属性的键名; 使用三个变量时,遍历结果第一个为每一属性的值,第二个为属性的键名,第三个为每一属性的索引。 <ul id="v-for-object" class="demo"> <li v-for="(a,b,c) in object"> {{ a }}/{{ b }}/{{ c }} </li> </ul> new Vue({ el: '#v-for-object', data: { object: { title: 'How to do lists in Vue', author: 'Jane Doe', publishedAt: '2016-04-10' } } }) 显示结果: How to do lists in Vue / title / 0 Jane Doe / author / 1 2016-04-10 / publishedAt / 2 2.6.3 v-for 指令渲染结果的更新机制及key属性的必要性 Vue更新使用v-for渲染的元素列表时,采取“就地更新”的策略。如果数据项的顺序被改变,Vue不会移动DOM来匹配顺序,而是就地更新每个元素,并确保它们在每个索引位置正确地渲染。 如果想让Vue跟踪每个节点的身份,当原始数据项更新,想让Vue对现有元素进行重新排序,则需要为每项绑定一个key属性。 Vue2.0 中 v-for里面的 “就地复用” 策略 是什么? - 霸都丶傲天的回答 - 知乎 2.7 v-on 指令 缩写: @ 接受数据类型: Function | Inline Statement(内联语句) | Object 传入参数: 原生DOM事件event 什么叫内联语句? 内联语句,就是写在HTML部分的JavaScript语句。 什么叫传入参数?传入参数,也就是跟在指令+冒号(如v-on)后面的语句,与‘指令:’一同组成元素的属性。 修饰符: .stop - 调用 event.stopPropagation()。 .prevent - 调用 event.preventDefault()。 .capture - 添加事件侦听器时使用 capture 模式。 .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。 .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。 .native - 监听组件根元素的原生事件。 .once - 只触发一次回调。 .left - (2.2.0) 只当点击鼠标左键时触发。 .right - (2.2.0) 只当点击鼠标右键时触发。 .middle - (2.2.0) 只当点击鼠标中键时触发。 .passive - (2.3.0) 以 { passive: true } 模式添加侦听器 用法说明: v-on指令用于绑定事件监听器。 v-on用在普通元素上时,只能监听原生 DOM 事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件。 在监听原生 DOM 事件时,方法以event为唯一的参数。如果使用内联语句,语句可以访问一个 $event property。 如:v-on:click='show('love you',$event)' v-on的修饰符如没有可以省略。 从 Vue 2.4.0 开始,v-on 支持不带参数绑定一个事件/监听器键值对的对象。注意当使用对象语法时,是不支持任何修饰器的。 2.8 v-model 指令 功能:在表单元素或者Vue组件上,创建数据双向绑定。 使用元素限制: <input>、<select>、<textarea>、components 修饰符: .lazy - 不监听input,改为监听change(在change事件之后才进行同步,也就是输入完毕input失去焦点后)。 .number - 把输入的字符串转为有效的数字。 .trim - 首尾空格去除。 具体使用情况说明: v-model绑定参数的类型,应依据 <input>标签的type属性或不同标签情况进行选择: type=’radio’时,应绑定单个字符串或布尔值,该值绑定为所选择元素的value属性(如有),如果没有value属性则绑定选中与否的Truthy值; type=’checkbox’,且<input>个数为一个时,应绑定一个布尔值,该值绑定为所选择元素的选中情况(Truthy); type=’checkbox’,且<input>个数为多个时,应绑定一个数组,该数组绑定为所选择元素的value属性组成的字符串数组,排列顺序按点击顺序; type=’range’,应绑定一个字符串,该值绑定为Range中选中的值; type=’color’,应绑定一个格式为’#XXXXXX’的字符串,代表初始颜色Rgb值,该值绑定为Color中选中的值; 绑定到<select>元素且单选时(multiple= ‘false’),应绑定到单个字符串,如果存在value属性,则该值绑定为value属性值,否则该值绑定为所选择<option>选项的textContent; 绑定到<select>元素且多选时(multiple= ‘true’),应绑定到一个数组,如果存在value属性,则该值绑定为value属性值,该值绑定为所选择<option>选项的textContent组成的字符串数组,排列顺序按option排列先后顺序; 2.9 v-slot 指令 待补充 2.10 v-pre 指令 功能:跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的HTML节点可以加快编译。 2.11 v-cloak 指令 功能:用于控制元素在编译完成之前的显示效果。在编译完毕之前,v-cloak指令一直保持在元素上。一旦编译完毕,这个指令就消失了。 2.12 v-once 指令 功能:配置了v-once指令的元素,只能被渲染一次。之后的渲染,这一元素或组件被视为静态内容并跳过。 2.13 v-text 指令 功能:修改绑定元素的textContent(元素内的文本内容)。 2.14 v-html 指令 功能:修改元素的innerHTML。可以真正地插入HTML内容。Mustache语法内的HTML内容只会被识别为文本而不会被浏览器编译。

减脂学习笔记

2019.08.22

Life

学习参考内容:《不交智商税系列》 —— 可爱小韬韬 1. 减重速度 体脂含量为25~30%的男性,周减重值建议为0.9kg。 2. 三大营养素摄入 2.1 蛋白质 2.1.1 优质蛋白质的食物来源 鸡肉、牛肉、深海鱼类、鸡蛋、蛋白粉。 2.1.2 什么时候补充蛋白质? 早上起床后; 早饭后和晚饭前加餐补充; 训练后30~60min补充; 2.2 脂肪 待补充。 2.3 碳水化合物 待补充。 3. 平台期 3.1 如何判断平台期? 连续测量三周平均体重,基本无变化。 3.2 平台期如何处理? 减少卡路里摄入(碳水减15-25g)[ Caution 1 ]; 加入有氧运动[ Caution 2 ]; 加入无氧运动; 【 Caution 】 在第一个平台期,选择上述一个措施即可,不要全部实施。如果选择有氧或无氧运动,一周一两次即可,需要为减脂留有余地。在第二个平台期可以加入另一种措施,第三个加入剩下的一种措施。 有氧运动:心率为最大心率的55%~85%,且坚持20min以上的运动。简易最大心率计算公式:220 - 年龄。 4. Refeed Days: 补碳日 4.1 什么时候才需要补碳日? 体重掉的太快; 有超过两周时间没有发生任何体重变化; 每次训练都没劲,头晕眼花低血糖。 如何称体重? 保证每天进食和结束进食的时间基本固定; 测量选取在每天的同一时刻; 每天记录一次自己的体重,一周求一次平均值; 只有周与周的平均值之间才能互相比较,每天的体重波动不具意义。 4.2 如何规划自己的补碳日? 必须满足条件:减脂已经进行到8~24周时间区间内; 听从自己身体感受; 初期每10天一个补碳日; 当减脂进行到3个月的时间,可以调整到每5~7天一个补碳日。 4.3 如何计算补碳日的三大营养素摄入值? 碳水(克g):体重(磅lbs)× (1.5~2.8)[ Notice 1 ]; 脂肪(克g):体重(磅lbs)× 0.28; 蛋白质(克g):体重(磅lbs)× 0.85; 【 Notice 1 】 碳水需要根据实际情况进行调整,方法为:检查补碳日间隔一天后,第三天的体重值。如果仍未恢复至补碳前体重,说明碳水摄入过多,则应适量降低碳水系数(1.5~2.8)。

ECMAScript6 学习笔记

2019.08.21

JavaScript

学习内容:《ECMAScript 6 入门》 —— 阮一峰 Ps:仅用于个人学习笔记使用,大量内容和实例可能直接复制原文。 1. 块级作用域、let 与 const 声明变量 1.1 let 命令 只在其所在的代码块(花括号)内起作用; 可以用于声明for循环的计数器i,使得i只作用于for的代码块里,每一轮都生成新的i而不是将i泄露于全局; 不存在变量提升现象; 一旦let在块级作用域中声明,就与这个代码块绑定,外部同名变量不能在这个代码块内使用; 块级作用域在let声明前,对于同名变量是"暂时性死区",不可使用,否则报错; let不允许在相同块级作用域内,重复声明同名变量; 1.2 块级作用域 ES6引入了块级作用域,明确允许在块级作用域之中声明函数; ES6规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用; 应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。 1.3 const 命令 const声明一个只读的常量。一旦声明,常量的值就不能改变; const一旦声明变量,就必须立即初始化,不能留到以后赋值; 与let具有相同的属性; const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。当const保存的是基本类型变量,可以保证其值不被修改。但如果保存的是对象等引用类型变量,因为本质上变量只是一个指向实际对象堆内存地址的指针,只能保证const变量所指向的内存地址固定,const声明不能约束指向的对象本身是否可改变(给const声明的对象修改属性是可以的); 2. 解构赋值 2.1 数组解构赋值 let [a,b,c] = [1,2,3]; 按照先后位置给数组内变量赋予相应的值。 let [x, y = 'b'] = ['a']; // x='a', y='b' let [x, y = 'b'] = ['a', undefined]; // x='a', y='b' 可以给数组内元素设定默认值。当给变量赋予的值严格等于undefined时,默认值生效。 默认值也可以是函数,此时函数是惰性求值的,也就是说只有真的模式匹配失败变量被赋予默认值时才会执行。 2.2 对象解构赋值 let { foo, bar } = { foo: 'aaa', bar: 'bbb' }; foo // "aaa" bar // "bbb" 对象解构赋值的依据是对象的属性名,必须属性同名的变量才能发生赋值。 如果因找不到相同属性名而不能解构,则变量会被赋予undefined。 变量名也可以与属性名不一致,此时变量名必须作为属性的值在左边对象中标明。 let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; baz // "aaa" let obj = { first: 'hello', last: 'world' }; let { first: f, last: l } = obj; f // 'hello' l // 'world' 对象的解构赋值也可以取到对象继承的属性。 2.3 字符串解构赋值 字符串可以像数组一样进行解构赋值,赋值规则是按先后位置。 const [a, b, c, d, e] = 'hello'; a // "h" b // "e" c // "l" d // "l" e // "o" 2.4 数值、布尔值解构赋值 数值和布尔值,解构赋值会先转为对象。只能赋值其中的toString等属性。 undefined和null无法解构赋值,会报错。 2.5 函数参数的解构赋值 function move({x = 0, y = 0} = {}) { return [x, y]; } move({x: 3, y: 8}); // [3, 8] move({x: 3}); // [3, 0] move({}); // [0, 0] move(); // [0, 0] 解释:函数的参数是一个对象{x,y},x,y都有默认值0,函数参数对象的默认值是空对象{}。当函数未传入参数时,函数参数对象默认取值{},则此时进行解构赋值,x,y都找不到同名属性用来赋值,则均取默认值0。因此输出为[0,0]。 2.6 解构赋值注意事项 尽量不要用圆括号(); 只有赋值语句的非模式部分才允许使用圆括号()。 3. 模板字符串 3.1 模板字符串说明 模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量(用${}包裹)。 let text = '我勒个去~'; `我说: ${text}`; // 我说: 我勒个去~ 模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。 ${}中表示的变量可以进行运算,也可以引用对象的属性,或者调用函数。 let x = 1; let y = 2; `${x} + ${y} = ${x + y}` // "1 + 2 = 3" `${x} + ${y * 2} = ${x + y * 2}` // "1 + 4 = 5" let obj = {x: 1, y: 2}; `${obj.x + obj.y}` // "3" function fn() { return "Hello World"; } `foo ${fn()} bar` // foo Hello World bar 模板字符串中引用未声明的变量,会报错。 模板字符串中的变量如果内部是一个字符串,则会原样输出。 3.2 标签模板字符串 所谓标签模板字符串,就是模板字符串跟在一个函数名后面。 当标签模板字符串中没有变量时,相当于普通字符串作为参数传入了函数并执行。、 let a = 5; let b = 10; tag`Hello ${ a + b } world ${ a * b }`; // 等同于 tag(['Hello ', ' world ', ''], 15, 50); 当标签模板字符串中含有变量,情况会变复杂。模板字符串中没被变量替换的部分会组合成一个字符串数组作为第一项参数传入。然后是各变量值作为独立参数依次传入。 let total = 30; let msg = passthru`The total is ${total} (${total*1.05} with tax)`; function passthru(literals) { let result = ''; let i = 0; while (i < literals.length) { result += literals[i++]; if (i < arguments.length) { result += arguments[i]; } } return result; } msg // "The total is 30 (31.5 with tax)" 解释:passthru函数只接受一个参数literals,而最后一行msg调用后传入了模板字符串`The total is ${total} (${total*1.05} with tax)`,由上一个例子,实际上是传入了["The total is ", " (", " with tax)"],30,31.5,而literals接受第一个赋值,被赋值["The total is ", " (", " with tax)"]。arguments则包含全部接收参数,为["The total is ", " (", " with tax)"],30,31.5。第七行的i++正好导致错开arguments里第一位的字符串数组,从而可以正确插入变量。 4. 字符串新增方法 4.1 String.raw() String.raw()一般用于处理模板字符串,返回模板字符串的原始值,且其中的每一个斜杠都被转义,每一个变量都被替换完毕。 String.raw`Hi\n${2+3}!`; // 返回 "Hi\\n5!" String.raw`Hi\u000A!`; // 返回 "Hi\\u000A!" 4.2 String.fromCodePoint() 和 String.codePointAt() String.fromCodePoint()接受一个Unicode 码点,返回对应的实际字符,且可识别码点大于0xFFFF的字符。 如果接受的参数大于1个,则返回由参数码点实际字符组成的字符串。 String.fromCodePoint(0x20CC5) // "𠳅" String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y' // true String.codePointAt() 接受一个位置数值,返回该字符串位置的Unicode码点。 4.3 includes(), startsWith() 和 endsWith() 接受一个字符串,返回Boolean值。 includes():检查传入字符串是否为原字符串的子字符串。 startsWith():检查原字符串是否以传入字符串开头。 endWith():检查原字符串是否以传入字符串结尾。 4.4 repeat() repeat() 函数接受一个数值N,返回一个新字符串,表示将原字符串重复N次。 如果N为小数,则向下取整。如果N为负数,则报错。 4.5 padStart() 和 padEnd() 'x'.padStart(5, 'ab') // 'ababx' 'x'.padStart(4, 'ab') // 'abax' 'x'.padEnd(5, 'ab') // 'xabab' 'x'.padEnd(4, 'ab') // 'xaba' padStart() 和 padEnd()接受两个参数,第一个为数值N,第二个为字符串string。 分别表示将原字符串调整为N位,不足的位数以string补齐。 padStart() 表示在原字符串前面补齐, padEnd()表示在原字符串后补齐。 如果只传入一个数值,则默认以空格补齐。 'x'.padStart(4) // ' x' 'x'.padEnd(4) // 'x ' 如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。 'abc'.padStart(10, '0123456789') // '0123456abc' 4.6 trimStart() 和 trimEnd() 用法同trim(),区别在于仅去除字符串前面或后面的空格。 4.7 matchAll() matchAll()方法返回一个正则表达式在当前字符串的所有匹配。 5. 函数的扩展 5.1 允许为函数赋默认值 function log(x, y = 'World') { console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello 函数具有一个length属性,返回函数第一个赋予了默认值的参数前,未赋予默认值参数的个数(不包括rest 参数)。 (function abc(r,t,s,x){}).length // 4 (function abc(r,t,s,x = 1){}).length // 3 (function abc(r,t,s = 1,x){}).length // 2 函数参数的默认值拥有一个单独的作用域,等到函数初始化完毕,这个作用域消失。如果作用域内存在同名变量,则优先使用参数作用域变量进行赋值。 var x = 1; function f(x, y = x) { console.log(y); } f(2) // 2 函数内部作用域内的变量不能影响参数的初始化赋值,如果参数作用域内没有用于赋值的同名变量,则会向上级作用域寻找。 let x = 1; function f(y = x) { let x = 2; console.log(y); } f() // 1 5.2 rest 参数 形如...[变量名]被称作rest 参数,表示函数多余的参数。其中[变量名]为一个数组,保存了多余的参数。 rest 参数只能是最后一个参数,否则报错。 function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum; } add(2, 5, 3) // 10 5.3 函数体内严格模式的使用注意事项 当函数存在解构赋值、参数默认值、扩展运算符时,不能在函数体内显式声明严格模式,否则报错。 5.4 name 属性 函数的name属性返回函数的函数名,函数表达式声明的匿名函数也能返回函数名,匿名函数返回空字符串''。 5.5 箭头函数 箭头(“=>”)可用于定义函数。 var f = v => v; // 等同于 var f = function (v) { return v; }; var f = () => 5; // 等同于 var f = function () { return 5 }; var sum = (num1, num2) => num1 + num2; // 等同于 var sum = function(num1, num2) { return num1 + num2; }; 如果返回是一个对象,则返回对象外必须用圆括号括起来。 // 报错 let getTempItem = id => { id: id, name: "Temp" }; // 不报错 let getTempItem = id => ({ id: id, name: "Temp" }); 可以结合变量的解构赋值一起使用,例如: const full = ({ first, last }) => first + ' ' + last; // 等同于 function full(person) { return person.first + ' ' + person.last; } 箭头函数内部this的指向是固定的,就指向定义时候的那个对象。 实际上箭头函数没有自己的this,它的this来源于外部this的引用。 函数内部同样没有arguments,它也为外部arguments的引用。 function Timer() { this.s1 = 0; //line1 this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log('s1: ', timer.s1), 3100); setTimeout(() => console.log('s2: ', timer.s2), 3100); // s1: 3 箭头函数内的this.s1就指向Timer实例定义时的s1(line1). // s2: 0 没有箭头函数,则this.s2在Timer实例被创建后指向外部,而不是实例内部的s2. 不应该使用箭头函数的三种场合: 定义对象的方法时; 需要动态使用this时; 函数体很复杂,有很多行时。 6. 函数尾调用的优化 尾调用的优化 7. Symbol 7.1 Symbol特性 Symbol是一种新的原始数据类型,表示一种独一无二的值。(目前JavaScript中的七种数据类型:null、undefined、Number、String、Boolean、Object、Symbol) Symbol值通过Symbol函数生成。(注意:Symbol函数前不能使用new命令,否则会报错。) let s = Symbol(); typeof s // "symbol" Symbol可用于对象属性名,可完全保证不与任何其他属性名冲突。 Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。 let s1 = Symbol('foo'); let s2 = Symbol('bar'); s1 // Symbol(foo) s2 // Symbol(bar) s1.toString() // "Symbol(foo)" s2.toString() // "Symbol(bar)" 两个独立生成的Symbol彼此是不相等的,即使是相同参数生成的Symbol值也是一样。 // 没有参数的情况 let s1 = Symbol(); let s2 = Symbol(); s1 === s2 // false // 有参数的情况 let s1 = Symbol('foo'); let s2 = Symbol('foo'); s1 === s2 // false Symbol值不能与其他类型的值进行运算,会报错。但是Symbol 值可以显式转为字符串,也可以转换为Boolean值。 let sym = Symbol('My symbol'); "your symbol is " + sym // TypeError: can't convert symbol to string `your symbol is ${sym}` // TypeError: can't convert symbol to string String(sym) // 'Symbol(My symbol)' sym.toString() // 'Symbol(My symbol)' let sym = Symbol(); Boolean(sym) // true !sym // false if (sym) { // ... } Number(sym) // TypeError sym + 2 // TypeError 读取Symbol的描述值:Symbol.description const sym = Symbol('foo'); sym.description // "foo" 7.2 作为对象属性名的Symbol 将Symbol用于对象属性名的几种方式: let mySymbol = Symbol(); // 第一种写法 let a = {}; a[mySymbol] = 'Hello!'; // 第二种写法 let a = { [mySymbol]: 'Hello!' }; // 第三种写法 let a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); // 以上写法都得到同样结果 a[mySymbol] // "Hello!" 将Symbol用于对象属性名的几个注意事项: Symbol 值作为对象属性名时,不能用点运算符,因为这样会将点后面的Symbol名误认作字符串。 在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。 Symbol 作为属性名,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。 7.3 Symbol.for() Symbol.for()方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。 Symbol.for("bar") === Symbol.for("bar") // true Symbol("bar") === Symbol("bar") // false 7.4 内置的Symbol值 除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。具体见:ES6提供的11个内置的Symbol值 8. Set和Map数据结构 8.1 Set数据结构 ES6 提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

JavaScript BOM

2017.02.20

JavaScript

BOM

《JavaScript 高级程序设计第八章》:BOM ————2017.2.20 Mars 北航三馆314教研室 BOM 什么是BOM? BOM(Browser Object Document)即浏览器对象模型。 BOM提供了独立于内容 而与浏览器窗口进行交互的对象; 由于BOM主要用于管理窗口与窗口之间的通讯,因此其核心对象是window; BOM由一系列相关的对象构成,并且每个对象都提供了很多方法与属性; BOM缺乏标准,JavaScript语法的标准化组织是ECMA,DOM的标准化组织是W3C,BOM最初是Netscape浏览器标准的一部分。 1.窗口关系和框架 window:当前框架的Global对象 top:最高层框架的Global对象 parent:当前框架的直接上层框架 1.窗口位置 screenLeft、screenRight、screenX、screenY 2.窗口大小 innerWidth、outerWidth、innerHeight、OuterHeight、document.documentElement.clientWidth、document.documentElement.clientHeight、document.body.clientWidth、document.body.clientHeight 3.导航和打开窗口 window.open("url","_target"); 返回新打开标签页的指针。可以这样跟踪新标签页: let newTag = window.open("www.marswiz.com","_blank"); 关闭新标签页: newTag.close(); 默认由一个页面打开的另一个页面,新页面保存着opener属性以建立联系,彼此通信。 设置newTag.opener = null切断联系,让新页面独立运行。 4.间歇调用和超时调用 重点  超时调用:setTimeOut() 接受两个参数:第一个是包含要执行的JavaScript的字符串或者一个函数。第二个是要等待多长时间的毫秒数。 //不推荐 setTimeout("alert('Hello world!') ", 1000); //推荐 setTimeout(function() { alert("Hello world!"); }, 1000); 为啥时间到了也不一定执行? JavaScript是一个单线程的解释器,为控制要执行的代码,就有一个JavaScript任务队列,这些任务会按照他们添加到队列的顺序执行。setTimeOut()第二个参数告诉JavaScript再过多长时间把当前任务添加到队列。如果队列是空的,添加的代码会立即执行。如果队列不是空的,要等前面的代码都执行完毕才会执行。 间歇调用:setInterval() 用法与setTimeOut()相同,到时间间隔重复执行代码。 5.系统对话框 alert():显示一个提示框,只有一个OK按钮。 confirm():用于确认,有Ok和cancel两个按钮。会返回boolean值,ok-ture,cancel-false。 prompt():向用户提出一个问题并让用户输入结果,最后返回值。 6.location 对象 重点  location对象既是window对象的属性,也是document对象的属性。 保存着当前文档的信息,还将URL解析为独立的片段,让开发人员可以通过不同的属性访问这些片段。 每次修改location对象属性,页面会以新的URL重新加载。 location方法:replace(),传入一个url并且直接转到,且无法后退。 reload():以最有效的方式重新加载当前页面,如果传入参数true,则强制重新加载。 7.navigator对象 识别客户端浏览器的标准。常用来检测浏览器类型。 常用功能: 7.1 检测插件 利用navigator.plugins数组,每一项都包含name description filename length四个属性。 //plugin detection - doesn't work in IE function hasPlugin(name){ name = name.toLowerCase(); for (var i=0; i < navigator.mimeTypes.length; i++){ if (navigator.mimeTypes[i].name.toLowerCase().indexOf(name) > -1){ return true; } } return false; } //detect flash alert(hasPlugin("Flash")); //detect quicktime alert(hasPlugin("QuickTime")); //detect Java alert(hasPlugin("Java")); 8.screen 对象 保存浏览器窗口外部显示器的信息。 9.history 对象 保存着用户的上网记录,从window被打开的一刻算起。 常用方法: history.go()按历史记录跳转。接受一个数值,正数表示向前,负数表示向后。 history.back()向后 history.forward()向前

JavaScript 变量、作用域、和内存问题学习笔记

2017.02.08

JavaScript

《JavaScript 高级程序设计第四章》:变量值的基本类型和引用类型、类型检测、执行环境、作用域链、垃圾收集。 这应该是JavaScript最重要的部分,理解这些概念真的很重要。 ————2017.2.8 Mars 北航三馆314教研室 1. 变量 1.1 JavaScript 的松散变量类型 JavaScript的变量是松散类型的:变量名称只是用于在特定时间内保存特定值的一个名字,并非定义某一类型变量必须存放该类型的数据。因此同一变量名的变量值和类型都可以在生存周期内被改变。 1.2 变量值的基本类型与引用类型 基本类型值:简单的数据段。(undefined\null\boolean\number\string),按值访问,操作的是保存在变量中的实际的值。 引用类型值:保存在内存中的对象。JavaScript 不支持直接访问内存空间,操作对象访问的都是对象的引用。 什么是引用?引用可以理解为对象的另一个复制品,并且与对象捆绑,同时更改或变化。(就是对象的分身) 1.3 变量值的复制 1.3.1 基本类型值的复制 var mars1=5; var mars2=mars1; 创建mars2之后,内存为其分配了新空间,并把mars1的值5和类型number复制到新空间中,从此mars1和mars2这两个变量互不干扰。 1.3.2 引用类型值的复制 var mars1 = new Object(); var mars2 = mars1; // 复制mars1对象 mars1.name = "mars1"; alert(mars2.name); //mars1 mars2复制了mars1之后,实际上只是指向mars1所指向对象的一个指针被复制到mars2的新内存空间中,并不是mars1指向对象的实际值。 所以复制之后,mars1与mars2共同指向同一个堆内存中的对象,所以name属性被同时修改了。 1.4 变量值向函数参数的传递 ECMAScript 所有函数的参数都是按值传递的。 1.4.1 基本类型值的传递 基本类型值在传递过程和基本类型值复制过程一样。函数内部对参数的操作不影响函数外部变量本体。 1.4.2 引用类型值的传递 引用类型值在传递给函数参数的时候是按值传递的,这个值是原对象的内存地址,因此函数参数在函数中的操作可以直接影响到外部的变量本体。 function mars1(obj){ obj.name = "mars"; } var mars2 = new Object(); mars1(mars2); alert(mars2.name); // mars obj是函数mars1的参数(局部变量),mars2的内存地址作为值传给obj在mars1中运算,增添了name属性,实际上就是增添了堆内存中mars2所代表对象的name属性。 function mars1(obj){ obj.name = "mars"; obj = new Object(); obj.name = "whatthehell"; } var mars2 = new Object(); mars1(mars2); alert(mars2.name); //mars 上述例子表明,这种传递并不是按引用传递。 如果是按引用传递,那么mars2的指向对象会随着obj(mars2指针)的重新赋值而随之指向新的对象,实际上并不会。 1.5 类型检测 1.5.1 typeof() 操作符 typeof() 操作符可以用来检测基本类型值。 typeof()操作符是确定变量是string、number、boolean、undefined的最佳方法。用typeof()来检测null和object都会返回Object。 1.5.2 instanceof 操作符 用于检测引用类型值,instanceof 根据原型链来识别。 alert(person instanceof Object); // true alert(colors instanceof Array); // true alert(pattern instanceof RegExp); // true 所有引用类型的值都是Object 的实例。 2. 执行环境与作用域 2.1 执行环境 执行环境 定义了变量或函数有权访问的其他数据,决定了它们各自的行为。 每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。 全局环境是最外层的执行环境。Web浏览器中是Windows对象,全局环境中的变量只有在应用程序退出(关闭浏览器或者网页)才会销毁。 每执行一个函数,函数的执行环境就会被推入一个环境栈中,函数执行完毕,栈将其环境弹出,把控制权交给之前的执行环境。 2.2 作用域链 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问。 作用域链的前端是当前执行的代码所在环境的变量对象,最后端是全局环境的变量对象。 2.3 标识符解析 沿着作用域链一级一级搜索标识符的过程,从前端开始,一直到找到标识符为止。 2.4 if 和 for 语句中定义变量的注意事项 var x=true; if(x){ var y="mars"; } alert(y); //mars 这里if语句内定义的变量y被保存在当前的执行环境(全局)中,所以在{}外面也可以访问。 for(var i=0;i<10;i++){ ... } alert(i); 这里i在for循环内定义,结束后并不会释放,因为在全局环境中。 3. 垃圾收集与内存管理 垃圾收集的两种方式:标记清除和引用计数。 所有浏览器都是用标记清除式的垃圾收集策略。 内存管理:对所有的全局变量,在不再有用的时候应该设置为null以便使其脱离工作环境让垃圾回收器回收

Git操作指南

2017.01.21

Git

我在工作中是如何使用 git 的 Git基本概念 Workspace:工作区,就是平时进行开发改动的地方,是当前看到最新的内容,在开发的过程也就是对工作区的操作 Index:暂存区,当执行 git add 的命令后,工作区的文件就会被移入暂存区,暂存区标记了当前工作区中哪些内容是被 Git 管理的,当完成某个需求或者功能后需要提交代码,第一步就是通过 git add 先提交到暂存区。 Repository:本地仓库,位于自己的电脑上,通过 git commit 提交暂存区的内容,会进入本地仓库。 Remote:远程仓库,用来托管代码的服务器,远程仓库的内容能够被分布在多个地点的处于协作关系的本地仓库修改,本地仓库修改完代码后通过 git push 命令同步代码到远程仓库。 Git基本流程 在工作区开发,添加,修改文件。 将修改后的文件放入暂存区。 将暂存区的文件提交到本地仓库。 将本地仓库的修改推送到远程仓库。 Git基本操作 合并分支 变基操作 git rebase 与 合并操作 git merge Git连接Github初始化与SSH配置 1.Git 初始化配置 建立本地文件夹base,右击Git bash 运行 git init 2.设置Git 全局用户名和Email git config --global user.name "Mars" git config --global user.email "marswiz@yeah.net" 3.查看本地是否存在SSH文件 在本地C盘用户→本用户文件夹下显示隐藏文件,看是否.ssh文件。 4.利用Git生成本地SSH文件 ssh-keygen -t rsa -C "marswiz@yeah.net" 直接使用ssh-keygen命令也是可以的,默认生成的就是rsa SSH,(会提示设置放置SSH的目录和每次动用SSH需要输入的口令,私人电脑可以不用设置。) 5.设置Github上SSH pub key 在本地C盘用户→本用户文件夹下显示隐藏文件,.ssh文件夹内找到pub key,打开并复制添加到github SSH控制项内。 6.测试是否可以连通 ssh -T git@github.com 7.添加远程库 git remote add origin git@github.com:Marswiz/Marswiz.github.io.git 这里建议使用SSH协议的远程库形式,如果使用https协议那么SSH就无法作为凭证使用,每次都会提示输入用户名和密码才能进行git仓库的操作,很麻烦。 使用HTTPs协议,如果想不用每次都输入用户名和密码,需要配置“凭据缓存”,把凭据信息暂时保存在内存: git config –global credential.helper cache 8.Git pull git pull -f origin master

Mars Windows系统优化流程

2017.01.14

Windows

1. 优化视觉效果→关闭”视觉效果”中不需要的效果 右键单击”我的电脑”→点击”属性”→点击”高级”→在”性能”一栏中→点击” 设置”→点击”视觉效果”→在这里把所有特殊的外观设置都关闭掉。 2. 优化启动和故障恢复 右键单击”我的电脑”→“属性”→“高级”→“启动和故障修复”中点击”设置”→ 去掉”将事件写入系统日志”→“发送管理警报”→“自动重新启动”选项 将”写入 调试信息”设置为”无”,显示操作系统时间改为0S。 3. 禁用错误报告 单击”我的电脑”→“属性”→“高级”→“错误报告”→点选”禁用错误汇报”,勾选”但在发生严重错误时通知我”→确定。 4. 关闭系统还原 右键单击”我的电脑”→点击”属性”→会弹出来系统属性对话框→点击”系统 还原”→在”在所有驱动器上关闭系统还原”选项上打勾。 5. 关闭自动更新 右键单击”我的电脑”→“属性”→“自动更新”→在”通知设置”一栏选择”关闭 自动更新。选出”我将手动更新计算机”一项。 6. 关闭远程桌面 右键单击”我的电脑”→“属性”→“远程”→把”远程桌面”里的”允许用户远程 连接到这台计算机”勾去掉。 7. 禁用休眠功能 单击”开始”→“控制面板”→“电源管理”→“休眠”→将”启用休眠”前的勾去 掉。 8. 关闭”Internet时间同步”功能 依次单击”开始”→“控制面板”→“日期、时间、语言和区域选项”→然后单 击”Internet时间”→取消”自动与Internet时间服务同步”前的勾。 9. 设置虚拟内存 虚拟内存最小值物理内存1. 5-2倍→最大值为物理内存的2-3倍的固定值→ 并转移到系统盘以外的其他分区。虚拟内存设置方法是右击我的电脑-属性→ 高级→性能设置→高级→虚拟内存更改→在驱动器列表中选中系统盘符→自定义大小→在”初始大小”和”最大值”中设定数值→然后单击”设置”按钮→最后点击”确定”按钮退出。 10. 自动释放系统资源 在Windows中每运行一个程序→系统资源就会减少。有的程序会消耗大量 的系统资源→即使把程序关闭→在内存中还是有一些没用的DLL文件在运行→ 这样就使得系统的运行速度下降。不过我们可以通过修改注册表键值的方法→ 使关闭软件后自动清除内存中没用的DLL文件及时收回消耗的系统资源。打开 注册表编辑器→找到”HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\explore r”主键→在右边窗口单击右键→新建一个名为”AlwaysUnloadDll”的”字符串值”→ 然后将”AlwaysUnloadDll”的键值修改为”1”→退出注册表重新启动机器即可达 到目的。 11. 关闭家庭组,因为这功能会导致硬盘和CPU处于高负荷状态 用不到家庭组可以直接把家庭组服务也给关闭了:控制面板 – 管理工具 – 服务 – HomeGroup Listener 和 HomeGroup Provider 禁用。 12. 关闭Windows Defender 全盘扫描系统 然后可以排除信任的EXE程序,建议排除explorer.exe(资源管理器进程),如果你不用系统自带的杀毒软件,也可以直接关闭它。 Win+X – 控制面板 – Windows Defender – 设置 – 实施保护 -去掉勾 和 管理员 – 启用 Windows Defender – 去掉勾。 控制面板 – 管理工具 – 服务 - Windows Defender Service 禁用。 13. 用好索引选项,减少硬盘压力 控制面板 – 索引选项 – 选中索引项 – 修改 – 取消不想索引的位置。(索引服务对系统性能的影响)。 14. 关闭磁盘碎片整理计划 用好磁盘碎片整理可以提高磁盘性能,如果习惯手动整理,可以关闭整理计划,避免在你工作的时候自动整理,影响性能。 资源管理器,选中磁盘 - 属性 – 工具 – 对驱动器进行优化和碎片整理 – 优化 – 更改设置 – 取消选择按计划运行 菜鸟必看的10个Win10优化技巧 15. 设置好Superfetch服务 控制面板 – 管理工具 – 服务 – Superfetch - 启动类型 – 自动(延迟启动),可以避免刚启动好Win10对硬盘的频繁访问。 16. 如果覆盖或者升级安装Win10,需要清理产生的Windows.old文件夹,腾出C盘空间 C盘 – 右键 – 属性 - 磁盘清理 - 选中 以前的 Windows 安装 复选框 – 确定清理。 17. 设置Win10 自动登陆,省去输入密码步骤,开机更快 快捷键Win+R – 输入 netplwiz  - 取消使用计算机必须输入用户名和密码的选项 – 然后双击需要自动登录的账户 – 输入你的密码。 18. 开启Hybrid Boot win10启动飞快 。默认是启动的,如果没有启动,可以到控制面板 – 电源选项 – 选择电源按钮的功能 – 更改当前不可用的设置 – 关机设置 – 勾上启用快速启动。

DNF PK 系统有什么特点?

2016.12.30

Game

Zhihu

相关:DNF8年老玩家,曾获2011南京高校联赛冠军,四川四区 擅长职业:战斗法师 ID:⑨⑨归① 2016年F1天王赛刚刚结束,恭喜59 4:0 拿下OGC捍卫中国主场,同时也证明了在网络环境逐渐变好的情况下,中国DNF选手基本功扎实的优势愈发体现出来。作为8年老玩家(7年PK) ,对DNFPK系统有一定的理解,在此盘点一下DNFPKC的特点。PS:因为比赛服没有装备差异,相对更为公平,所以说的以比赛服为主。 一代版本一代神,版本与BUG共存 DNF老玩家应该都还记得最早的剑魂无限里鬼,战法碎霸一半血、伪连一套秒版本(怀念!),到后来的机械G3崛起,冰结破冰秒人的BUG,DNF的发展过程中,版本与 BUG绝对是一个重要的组成部分。不过这里的BUG不是游戏代码的错误,而一般指的是某版本,人为对某个技能过度的加强影响了职业间强弱公平性。技能BUG也影响了一 部分职业强弱的排名变化,比如冰结师的破冰飞刃高伤BUG,让当时的冰结师在国服几乎无人能敌,被大家屏蔽。第三赛季前的女气功在PK场一直很弱势,而第三赛季初为加强 女气功PK能力设置的手动引爆分身技能,使女气功一跃成为当时PK场霸主的存在(后来因为太BT而和谐)。 也因为这个原因,为了维护公平性,在BUG不能修复之前,这些不幸的职业在该版本一般会被禁止参加比赛。国服第一冰结师安心在破冰技能BUG版本期间两 次无缘格斗大赛,最终导致他放弃冰结而转玩魔皇。 现在的知名PK职业选手,早期刚出道的时候都或多或少地借了版本的东风。凤凰羽成名于机械G3高伤版本他创造的G3打法,逍遥尊的格斗天王大半 要感谢当时的高网络延迟,二海当年在高伤时代线上赛伪连一套秒等等。不过好在第三赛季以来(尤其是Neople回归之后),降低了各职业伤害(尤其是比赛服), 游戏公平性大大增强,网络改善,游戏流畅度提高,版本BUG也越来越少了。 比赛服与国服、线上与线下的巨大差异 游戏高昂的代理费用和运营成本导致网络代理商必然要从玩家身上吸取利润,国服刺激玩家消费的主要途径就是各种节日装扮和收费道具附带的增强属性,这些收费道具部分在PK场也同样有效, 是造成国服与比赛服差别的原因。与比赛服相比,国服提升属性的途径主要是:宠物装备、强化增幅锻造、宠物属性、武器装扮和称号。 顶级的PK装备带来的速度和伤害上的提升巨大,是装备一般的平民玩家不能抗衡的,因此国服想要玩到高分段装备绝对是决定因素之一。 角色属性对于连招、战术和跑位方面同样有影响。例如:力法的天击和落花掌滑行距离和发动速度受到人物攻击速度的影响, 攻速越快,天击的滑行距离越长,相应扫过的范围也就越大,因此也就更容易命中对方和逃跑。 格斗系的4X扫地接技能如果没有一定的攻速做支持是不能成立的(当然手速也很重要)。释放速度直接影响发动速度和收招速度,硬直影响人物被命中后僵直时间(黄金套玩家基本不能平推),因为保护系统的存在,高属性的角色伤害更高,因此保护出的更快,相应的连招过程更短,连招方式倾向于高爆发打法,是平民玩家做不到的。 而比赛服装备统一,伤害回归到较低的水平,比拼的是选手的战术策略、对职业的理解和临场心态与稳定程度。很多网络主播在国服风生水起,战斗力排名也非常高,而一到了比赛服就 成绩平平,甚至打不进区域赛,就是因为在国服打的太久适应了高属性的角色玩法,跑位意识以及连招方式已经不适用于比赛服环境。同样这种差异也影响职业的对战策略,很多职业在国服高属性条件下具有强大的优势,而比赛服就相对弱很多,比如现版本元素、机械、大枪等。在这方面,最典型职业的莫过于召唤师和红眼。国服高伤版本召唤基本都会选择加满献祭以提供高爆发的献祭流打法,红眼则一般会选择放弃一些基础技能(如邪光斩),加满崩山裂地斩等高爆发技能一套打残对手。这样的打法在比赛服是不合适的,因为在面对像散打蓝拳力法这样的高机动性职业时会有较大的弱势,CD长收招慢(献祭会导致召唤本体暴露),一旦打空是很大的损失。还有比赛服的速度条件下,如果鬼剑士利用扫地波最大范围将对手扫倒地,立刻跑过去接上挑是很难成立的,而这种起手方式在国服基本占主流。因此比赛选手一般会均衡加点,利用各种技能的优势,增强综合能力而不是依赖高伤技能打爆发。 还有一个不得不提的重要因素就是国服连发插件的普遍存在。按键连发导致普通攻击连招稳定性大大增加,很多玩家(尤其是红眼玩家)在国服PKC依赖普通攻击输出,对技能衔接节奏已经生疏。在比赛服, 这样的输出方式显然是不可取的。一是正规比赛连发的禁止导致普通攻击浮空连、扫地连都会出现一定程度的不稳定性;二是在比赛服装备环境下普通攻击的伤害太低,输出太慢,过度依赖普攻输出会导致命中保护出现的时候还没有打 出伤害保护,导致后面爆发技能不能命中,输出太慢,伤害不足。这也是很多打惯了国服环境玩家在比赛服不能适应的原因。 网络环境与硬件条件要求高 DNFPVP的网络格斗游戏属性决定了对网络延迟和硬盘读取速度的高要求。毫不夸张地说,一个100ms以上延迟和一个50ms以下延迟的DNF完全是两个游戏。 早期机械硬盘时代的DNF和SSD时代的DNF在游戏体验上完全不同(同时装有SSD和机械硬盘的同学可以亲自尝试一下)。 不同网络延迟和硬盘读取速度甚至 可以直接影响PK的连击效果,乃至意外成为很多玩家追求的操作技巧。 最典型的莫过于力法的伪连技术:在人物角色倒地即将起身的一刹那,DNF系统设定 默认存在一个时间极短的蹲伏动作,然后才是真正的站立姿态。伪连成功秘诀就在于:利用力法炫纹的释放节奏,在这个起身蹲伏的瞬间击中对手,造成 对方已经起身被攻击的判定,从而清零保护系统,可以继续打出下一套伤害。 伪连的难易程度取决于延迟高低。延迟非常低的时候(30以下),伪连非常难,需要极好的节奏感。延迟过高(大于150)的时候,伪连相对容易,但是破保护后的 连接容易出现问题。在有一定的Ping值但是又不至于使连招不成立的条件下,力法伪连的优势才能体现出来。可以看到早期月与海的比赛大量应用伪连破保护 连招打出极高伤害,而近期比赛由于网络环境的改善,伪连成功率下降,力法基本不会冒险选择伪连的激进打法。 很多玩家会发现国服在不同位置网吧、不同配置机器上连击节奏有一定差别,就是网络延迟与硬件延迟造成的。所以国服DNF想要玩得流畅不 吃延迟的亏,还需要一个配备高速SSD的硬盘和提高自己的上传速度才行。另外,这一差异体现基本只限于国服,由于比赛使用相同的机器以及 线下基本归零的延迟,网络环境和硬件的差别在比赛中基本可以忽略不计。 职业差异显著,存在相生相克,天花板高可开发性强 DNF的游戏角色设计非常出色。首先是各职业之间(这里不包括现在仍不能参赛的职业),无论是在操作难度、节奏速度、人物属性上还是从攻击方式、 类型、与范围上都差异显著,各具特色,这使得不同类型玩家的需求都可以得到满足。(这里不得不吐槽一下nexon接盘的那段时间出的几个职业,不 知道是不懂DNF还是纯粹的鹅厂为了更快的圈钱透支,真的不要太恶心。) 同时职业设计上可以看出早期的开发者的用心,在职业技能设计上存在明显的相生相克关系,这在某种程度上维持了一种游戏平衡,也让PVP更具有趣味性和挑战性。格斗 系的武神职业具有超高的爆发力和短时间的霸体buff,对于男女大枪这种近身防御技能少、 收招速度慢依赖远程输出的职业简直是噩梦,然而面对同门柔道的强力抓取技,武神的霸体就基本成为废柴。被武神克制的大枪在面对召唤、机械、驱魔师等职业的时候 ,利用其技能的穿透力和良好的远程输出能力,反而具有相当大的优势。这样的相生相克在DNF中比比皆是,职业特性和技能间的克制让DNF比赛偶然性增加(所谓抽签决定 成败),同时也意味着如果想取得好成绩,则必须在打法上攻克对自己克制的职业,更具挑战性。 虽然职业强弱随着版本起伏,也存在相生相克,但和所有竞技游戏一样,最终决定比赛成败的永远不是角色职业,是选手本身。今年的F1天王赛, 剑魂OGC刚刚结束自己的兵役期,回归比赛面对克制自己的近身物理克星、强势金身玩家凡神金度勋,依然以3:0横扫对手,足以证明OGC的强大实力。 高水平的操作可以让一个不被看好的三线职业变成超一流。近几年的比赛也可以看出,比赛结果越来越出人意料,高水平比赛上职业差距的影响越来越小——强的不是职业,而是选手本身。 竞技元素丰富,节奏快,充满偶然性与戏剧性 DNF比赛有丰富的竞技元素。首先是游戏本身的元素设计,角色姿态有站立、浮空、倒地三种,角色状态有霸体、抓取、无敌、僵直、折颈等,异常态有中毒、冰冻、灼烧、束缚 、睡眠、诅咒、失明等。每个技能释放和命中的过程都是这些元素的组合,这导致每个职业的每个状态与技能,都具有相应的应对方式,即没有绝对无解的绝对克制技能。 DNF比赛个人赛每场总时长最多3分钟,基本比赛会在两分三十秒之内结束,而这短短的150S时间内,两方会起手3-5次,也就是每回合交手,从跑位到起手至连击结束, 平均仅有三十秒左右时间。在这短短三十秒不仅需要跑位,同时要随时对对手施放技能时机与附带的效果进行准确的判断,估计连击保护与对手的HP值位置,并及时调整自己的应对策略,因此非常考验选手的反应速度 。这种快节奏也是所有格斗游戏的通用特点,DNF相比于传统的格斗游戏在技能操作上稍稍简单,但技能数目更多,竞技元素更多 ,比赛具有偶然性和戏剧性,充满各种亮眼的操作,可看性强。 总之,DNF作为独树一帜的格斗类游戏,在PVP系统制作方面还是比较优秀的。每年的赛事也给喜欢PK的玩家带来了无数印象深刻的精彩瞬间,希望有更多的选手参 与比赛,体验惊险刺激的PK乐趣。同时希望中国能保持这次F1天王赛的好成绩。

人际交往小原则

2016.11.22

Personal

1. 价值交换原则 人与人之间最稳固的是价值交换关系,互相能交换彼此的想要的价值,才能长久保持稳定的关系。这里的价值包括精神价值和物质价值。所以在你进行人际交往的时候最好心里打一个小算盘,我能从他这里得到些什么,我能给他什么,如果两者相差太大那么就要当心了,这段关系可能不会很稳定。 2. 双方相关原则 谈话交流和活动的内容需要是双方都相关的,不能涉及到别人的私人生活或者围绕着自己的私事交谈。 3. 变好原则 / 解决问题原则 双方遇到了问题,或者对方向你倾诉问题,要知道对于这种情况怎样会变得更好,然后交流向着变好/能解决问题的方向进行,不要进行无助于解决问题的交流。 4. 不甩包袱原则 当自己有事情需要求助于对方,而又暂时没有对方的需求可以交换。做到在自己这一边尽量多的解决部分问题,至少把前期资料准备充分,而不要把问题直接甩给对方。 5. 最高默认原则 谈及一个新领域,不知道对方在这一领域建树如何的时候,默认对方具有最高级别的认知,直到对方告知自己的实际水平再进行调节,这样不会因为低估别人而显得不礼貌。 6. 不挖苦原则 / 自黑别描原则 对方有痛楚,有不快,有坎坷被你知道或者向你倾诉,在关系不是无话不谈非常熟悉的情况下,最好不要把对方的痛处做谈资,或开对方的玩笑。同样,在对方对自己的缺点痛点进行自嘲的时候,千万不要随声附和同意他,这一描可能会让对方信以为真。 7. 留有选择原则 邀请对方做事情,或者让对方做出决策的时候,不要替对方做出选择,也不要把话说死,定好了本来应该双方商量决策的事情。该让对方选择的时候一定要给对方留有选择的余地。 8. 见微知著原则 人们通常不喜欢把观点说的太过绝对,但是并非没有看法。当对方在赞赏的同时稍稍表现出一点点反对时,其实他的内心是完全反对你的观点的。所以察觉这种微小的信号可以让你更容易掌握对方的立场。 9. 自嘲技巧 自嘲可以引起对方暂时的优越感,让你降低姿态博取对方一定的信任,也让对方对你产生临时的好感,因此在交往初期适时的自嘲很有利于人际关系的发展。 10. 观点后言原则 一般情况下,与人交流时不能确定真伪的主观观点要放在对方发表观点之后说,尤其是向别人讨教问题,或者对方在该话题有一定话语权的情况下。主观性很强的观点一般而言具有一定的锋利性,放在后面说就体现出是一种试探性、探究性语态,而不是让对方以为你对此坚信不疑,也是尊重对方的一种非暴力沟通方式。

木有找到Der~~