经典算法

2021.07.05

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

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

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(只剩顶部一个元素),排序完成。 注意: 对于大顶堆,排序后的结果为升序; 对于小顶堆,排序后的结果为降序。

JavaScript一些内置API

2021.06.01

JavaScript

JavaScript一些内置API: 跨文档通信API、FIle API、媒体元素API、拖放API、Page Visibility API、Performance API、Web组件 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. 拖放API 5. Page Visibility API 6. Performance API 7. Web组件 API

深扒JS函数中的this

2021.05.23

JavaScript

JS函数中的this到底指向啥? 哪里有this? 只有函数作用域和全局作用域中有this属性,块级作用域中没有。 非严格模式,浏览器全局作用域中this 指向 Window对象。(严格模式为undefined) 函数作用域中的this指向在函数调用的时候决定。 普通函数内部的this 使用函数声明或函数表达式声明的函数,内部的this: 指向调用这个函数的上下文对象,也就是函数被调用时点号前面的对象。 函数作为一种特殊的对象,存放于堆内存中,函数名保存的是函数对象的内存地址。无论函数被如何传递,函数中的this指向只有在函数执行的时候才决定,它永远指向调用它的那个对象。 如果函数被直接调用,而不是作为对象的方法(不使用’obj.func’形式,而是func()形式),那么浏览器下默认函数的this为window。 箭头函数内的this 箭头函数内部作用域的没有自己的this,它始终引用它定义所在位置上下文中的this。 翻译成人话: 全局作用域中定义的箭头函数,this值始终指向window,永远不会改变。 一个函数A中定义的箭头函数,箭头函数里的this就等同于函数A的this。只要A的this不变,箭头函数在任何地方以任何形式调用this都不变。 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

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. 原型式继承 Object.create(superTypeInstance) 核心:不通过构造函数,实现两个对象间的继承。 本质上是创建了一个新的对象,它的原型是传入的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本质上还是构造函数。 这里①②两步,对应组合寄生式继承中继承父类原型的操作。③步骤对应在子类中盗用构造函数继承父类实例属性的操作。

JS笔试:数据结构、算法和技巧

2021.05.21

JavaScript

记录笔试常用方法技巧 一、 索引 1. 栈、队列、哈希表、数组 数据结构汇总 一、栈 单调栈 二、队列 单调队列; 三、哈希表 构建哈希表方法的适用情况; 哈希表计数去重; 四、数组 差分数组; 2. 链表 链表反转; 链表寻环、查找环的入口点; 合并链表; 3. 二叉树、BST Binary Tree 翻转二叉树; 前、中、后序遍历(递归、迭代写法); 层序遍历; 最大路径和; 节点间距离; BST: BST插入、查询、删除(重难点)操作; 确认BST有效性; 寻找BST中最大(小)kth元素; 4. 图 深度优先搜索DFS; 广度优先搜索BFS; 5. 动态规划 背包问题; 零钱问题; 接雨水; 字符串操作距离; 股票买卖问题; 三个无重叠子数组最大和; 6. 贪心算法 7. 回溯算法 基本思想; 回溯算法:全排列问题; 8. 递归 递归复杂度分析; 典型递归问题; 9. 排序 排序算法汇总 插入排序、选择排序、冒泡排序; 快速排序; 归并排序; 堆排序:构建大(小)顶堆; 桶排序; 计数排序; 基数排序; 10. 搜索 二分查找; 滑动窗口; 双指针; 11. 知名算法 Kadane算法:查找最大连续子数组; KMP算法:查找子字符串subStr是否存在于父字符串str; ☆概念、常用操作 各种概念 数组操作 判断变量类型 链表的前序、后序遍历 辗转相除找最大公约数 找两数最小公倍数 ABC集合的合集元素数目 合并区间 二、主要内容记录 2.1 栈、队列和哈希表 2.1.1 栈 2.1.1.1 单调栈 单调栈:栈内的元素索引值单调递增,且元素值从底向顶也单调递增或单调递减。 单调栈同普通栈一样,元素只能在栈顶进出。 单调栈的目的是,随着原序列的遍历,维护一个局部最优的子序列。属于贪心算法的工具。 单调栈可以用来解决: 查找数组中每个元素下一个大于自身的值; 查找最小或最大子序列; 找出最具竞争力的子序列 移掉K位数字 2.1.2 队列 队列是”先进先出(FIFO)”的数据结构。 2.1.2.1 单调队列 单调队列:队列中元素的索引值单调递增,且队列中的元素值单调也递增(或递减)。 单调队列与普通队列不同,一般为双端队列。元素可以在两侧出队,而只能在队尾入队。 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对应的元素,如果是则将它从队首出队,如果不是则不用做任何操作。 每次窗口移动,队首的元素就是当前窗口内的最大值。 窗口中的最大值 绝对差不超过限制的最长连续子数组 2.1.3 哈希表 适用于:计数类问题。哈希表可以快速判断一个值是否出现在集合中,而避免了每次都要遍历查找。 缺点是空间复杂度高,是以空间换时间的方法。 哈希结构:数组、对象、Set、Map 2.1.3.1 哈希表计数去重 使用哈希表计数两数组合,如何能不重复? 给出一个数组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,他们之间两两存在先后顺序,也就是说如果存在任意组合需要被计入,只会在组合元素最后一次被添加的时候计数,也就是只会计数一次。 2.1.4 数组 2.1.4.1 差分数组 2.1.4.1.1 定义和适用情况 差分数组:对于一个源数组arr,差分数组diff[i]定义如下: diff[0] = 0; diff[i] = arr[i] - arr[i-1]; (i > 0) 差分数组主要用于对原数组子区间内元素,进行统一增减操作的情况,可以更方便地合并区间操作(降低时间复杂度),更直观地显示出操作结果。 2.1.4.1.2 差分数组的性质 差分数组是个啥 对于一个数组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之后的前缀和没有变化。 2.1.4.1.3 差分数组的好处 差分数组将一个区间内的操作,转化为在两个端点的操作,省去了遍历整个区间的过程,减少了时间复杂度。 差分数组应用实例: 1893.区间是否被全覆盖 2.5 动态规划(Dynamic Programming) 2.5.1 背包(零钱)问题 LeetCode问题:零钱问题2 给出零钱和目标值,求组成目标值的方法数。(比如由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,再更新这个新面额和之前所有面额组成的方法数。 对于情况①,计算出来的是所有的排列数。对于情况②,计算出来的是组合数。 本题不同排列同一组合,视为同一个方法,因此应该使用第二个遍历方式。 2.5.2 股票买卖 股票买卖 给你一个每日股票价格组成的数组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]; } 2.5.3 三个无重叠子数组最大和 三个无重叠子数组最大和 使用动态规划方法解题。步骤如下: 先遍历数组,用一个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); }; 2.7 回溯算法 2.7.1 算法基本思路 回溯算法是遍历决策树的过程。决策过程可以表示为当前路径、当前决策空间和结束条件。 当前路径:是由已经进行过的所有决策组成的。下一步决策依赖于当前走过的决策路径。(比如:有三张牌ABC,第一次选择B,第二次选择C,则当前第三次选择的路径为B-C); 当前决策空间:根据当前路径,本次决策剩余的选择空间。(ABC三张牌,前两次选择BC,则第三次当前选择空间只有一张牌A); 结束条件:决策空间中没有任何选项时,则代表决策树遍历到尽头,需要用结束条件处理这次决策的结果。(比如将当前决策路径B-C-A保存起来) 回溯算法一般是基于递归调用实现,递归主要负责实现决策树的遍历。回溯算法的主要过程如下: 先判断当前路径是否满足结束条件,满足则执行结束操作; 在当前决策空间B内进行决策,然后在当前路径A中添加当前决策(结果为A1),在当前决策空间B中去掉当前决策(结果为B1); 以A1为下一决策的路径,以B1为下一决策的决策空间,递归调用自身; 调用完成后,将决策路径B1恢复为B,路径A1也恢复为A。(这一步称为回溯过程) 回溯算法的应用:DFS深度优先搜索、全排列问题等。 2.7.2 回溯算法:全排列问题 2.7.2.1 元素互不相同的全排列 给定一个由字母组成的数组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)。 代码如下: 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.7.2.2 元素存在重复的全排列 元素存在重复的全排列 当给定数组中存在重复元素时,进行全排列会出现重复结果。 比如: [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; } 2.7.3 回溯算法:括号匹配 括号生成 回溯算法,解法等同于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(); }; 2.8 递归 2.8.1 递归的特点和基本思想 递归是一种解空间的遍历手段,它不同于for/while循环,递归遍历的空间可以是非线性的。(图、树等) 递归通过在函数内调用自身实现。递归在内存中是通过函数调用栈来实现的,每次调用函数在调用栈中压入函数,函数执行完毕后弹出,恢复上层函数的执行环境。 因为这个原因,递归在内存中需要占用空间,空间复杂度较高。(占用空间与函数的最大调用次数n正相关。) 递归的注意事项: 设置合理的退出条件; 递归深度影响空间复杂度,能用迭代实现尽量不用递归。 2.8.2 递归复杂度分析 递归问题的复杂度可以画递归树分析: 时间复杂度等于:每次函数调用的时间复杂度 × 递归调用次数。 空间复杂度等于:每次调用函数所需空间复杂度 * 最大递归深度。 2.8.3 典型递归问题 生成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; } 2.9 排序算法 各排序算法基本思想与JS实现 2.9.4 堆排序 2.9.4.1 构建堆(也叫优先级队列) 构建堆 适用于:获取一个集合中,某一属性的前N名元素集合。 例如: 获取一个数组中出现次数最高的前N个元素。 为集合元素构建堆(大顶堆、小顶堆),可以通过堆排序,方便获取某一属性优先的前N个元素。 2.10 搜索算法 各种搜索问题,本质上都是树的遍历问题。 2.10.1 二分查找 2.10.1.1 复杂度分析 时间复杂度: O(logN),N为原数组长度。 空间复杂度: O(1) 2.10.1.2 二分查找适用情况 应用二分查找的必要条件是: ①顺序存储:数组结构; ②通过任意一个下标获取子元素,可以缩小结果范围(一般数组需有序排列)。 一般适用于:按序排列,且无重复元素的数据搜索。 2.10.1.3 基本代码实现 二分查找实现的两种方式:1.迭代 2.递归。 // 递归实现数组nums中查找一个元素target。 var search = function(nums,target) { function find(left, right){ if(left <= right){ let mid = Math.floor((left+right)/2); if(nums[mid] > target) return find(left, mid-1); else if(nums[mid] < target) return find(mid+1, right); else return mid; } return -1; } return find(0,nums.length-1); }; // 迭代实现 var search = function(nums,target) { let left = 0, right = nums.length-1; while(left <= right){ let mid = Math.floor((left+right)/2); if(nums[mid] > target) right = mid-1; else if(nums[mid] < target) left = mid+1; else return mid; } return -1; }; 2.10.1.4 需注意事项 二分查找的边界条件是left<=right,也就是区间左右指针left和right在查找完毕后会交叉(left=right+1)。因此,如果需要在查找完成后使用left或right作为索引获取数组元素,需要考虑数组索引越界的情况: 当数组全部元素都大于目标值target,最终left=0, right=-1; 当数组全部元素都小于目标值target,最终left=arr.length-1, right=arr.length; right只可能在左边界越界,left只可能在右边界越界。 2.10.2 滑动窗口 适用于:寻找空间连续的特定组合。 基本思路: 通过左右边界维护一个窗口:left,right; 右边界负责向右扩展窗口,每次探索到一个符合条件的窗口值就停止扩展; 收缩左边界,优化当前窗口值,直到不符合要求; 继续向右扩展右边界,重复1-3步骤,直到右边界到达末尾。 2.10.3 双指针 待补充。 三、 常用操作 3.0 概念定义 子序列: 由原数组中部分或全部元素组成的新数组,子序列中元素的先后顺序必须与原数组相同,但是元素在原数组中不必相邻。 子数组: 由原数组中部分或全部元素组成的新数组,子数组中元素的先后顺序必须与原数组相同,而且元素在原数组中必须相邻。 3.1 数组操作 3.1.1 数组中随机取一个元素 let randPos = Math.floor( Math.random() * arr.length ) let res = arr[randPos] 3.1.2 数组中删除一个元素 arr.splice(position, 1) 3.1.3 数组中动态删除元素,考虑从右到左遍历 从右到左遍历,指针左侧的元素不会被动态修改,指针右侧的元素删除不影响指针的下一位置。 3.1.4 浅拷贝数组 Array.from(arr) 3.1.5 数组sort排序 // 字符串升序 arr.sort() // 字符串降序 arr.sort().reverse() // 数字升序 arr.sort( (a,b) => a-b ) // 数字降序 arr.sort( (a,b) => b-a ) 3.2 判断变量类型 // 精确返回变量类型,首字母大写 Object.prototype.toString.call(arg).slice(8,-1) 3.3. 链表操作 2.3.1 链表的前序、后序遍历 function iterate(nodeHead){ while (nodeHead.next){ // 这里写处理逻辑就是前序遍历:正序 iterate(nodeHead.next) // 这里写处理逻辑就是后序遍历:倒序 } } 3.4 数值操作 3.4.1 辗转相除找最大公约数 function findGCD(num1,num2){ let remainder,divider if (num1 >= num2){ remainder = num1 % num2 divider = num2 } else { remainder = num2 % num1 divider = num1 } while (remainder !== 0){ let temp = remainder remainder = divider % remainder divider = temp } return divider } 3.4.2 找两数最小公倍数 let LCM = num1 * num2 / findGCD(num1,num2) 3.4.3 ABC集合的合集元素数目 |A∪B∪C| = |A| + |B| + |C| - |A∩B| - |A∩C| - |B∩C| + |A∩B∩C| 3.4.4 合并区间 对于一系列闭区间组成的数组arr:[[l1,r1],[l2,r2],[l3,r3]...[ln,rn]],它们之间可能无序,也可能存在嵌套,合并它们的策略是: 2.1. 先按起始位置l1,l2,l2.3...对区间进行升序排序; 2.2. 声明一个数组edges = [l1]用来储存:合并后区间的左右边界位置(每个位置是此时是独立的,而不是两两成组); 2.3. 从左到右遍历这个区间数组(索引index从0到n-2),对每个区间[l1,r1]和它下一个区间[l2,r2],判断l2 <= r1: - 如果为false,则说明二者之间存在间隙,那么r1和l2一定都是合并后区间的边界,将他们push到edges数组; - 如果为true,则说明二者之间相连或相交,r1和l2一定不是合并后的区间边界,此时什么都不用做; 2.4. 直到遍历结束,此时index位于arr倒数第二个元素(i=arr.length-2),并且所有合并后内部区间边界已经被加入到edges数组,只剩最后一个区间的末尾边界rn没有被加入。此时将其加入。(edges.push(arr[i+1][1])) 2.5. 所有区间合并完毕,edges内元素一定为偶数,从前到后两两配对即可。(每对前一元素为合并后区间左边界,后一元素为右边界。) 图示如下:

DNS解析流程

2021.05.21

Computer_Network

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 3. 回流和重绘 4. 浏览器渲染页面的流程 5. Js高阶函数 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可以为同一个事件绑定多个处理函数,按绑定的先后顺序执行。

JS执行环境与作用域

2021.05.06

JavaScript

JS函数词法环境和作用域问题,还有var和let的区别问题。 字节跳动面试 一、 作用域 JS有三种作用域:块级作用域、函数作用域和全局作用域。 if{}、for(){}、while(){}和普通的{}都会创建块级作用域。 for{}和while{}循环,相当于多次执行了块级作用域创建,并在新的块作用域内修改变量。 函数内部是函数作用域,全局环境是全局作用域。 二、词法环境 块级作用域(代码块)、函数乃至全局作用域在创建的时候,都存在一个与之关联的词法环境(Lexical Environment)内部对象。 词法环境对象由三部分组成:1.当前环境的所有局部变量(变量名作为属性,值作为属性值);2.this的值 3.外部环境的引用。 修改词法环境下的变量,相当于修改词法环境对象上对应名称的属性值。 全局词法环境,外部环境的引用为null。其他词法环境通过层层引用,最外层会引用到全局词法环境。 每创建一个新的词法环境,就会自动记录当前环境的局部变量,this指向,以及对外层词法环境的引用。 在某一个词法环境下,访问一个变量的时候,先寻找内部词法环境,然后逐层向外寻找,直到找到全局环境。 函数在声明的时候,会通过内部的[[environment]]属性记录下它创建时候的词法环境,然后在执行的时候才会创建内部的词法环境,并把外部环境的引用设定为[[environment]]指定的词法环境对象。 三、for、while循环内声明函数 for{}和while{}循环,相当于多次创建了块级作用域,并在新的块作用域内修改变量。 因此每一个在for、while循环内声明的函数,都通过[[environment]]记录下当前的外部词法环境,也就是不同的块级作用域,因此当它们在执行的时候,创建的内部词法环境引用的也是不同的外部块词法环境。这也就解释了如下的代码: 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内部声明的箭头函数的[[environment]],记录了每次迭代时外部的块级作用域词法环境。 // 当后续这个箭头函数按序执行的时候,创建函数内部的词法环境,外部引用的是创建时[[environment]]记录的外部块级作用域的词法环境,因此log的是每个词法环境下的i,也就是0-9 三、var和let的区别 1. 是否有块级作用域 var只能存在函数作用域和全局作用域,没有块级作用域。在块级作用域内部声明的var,会穿透块级作用域泄漏到全局。 let可以有块级作用域,块级作用域内部用let声明的变量,只能在块内部访问,外部无法访问。 2. 变量提升 var声明的变量,声明会被提升到块级作用域的头部,而赋值还是在本地。 // other js code.. var a = 2; 其实是发生了下面的事情: var a; //此时a是undefined. // other js code.. a = 2; 3. var可以重复声明 var可以重复声明同名的变量,后面的覆盖前面的。而let不可以(报错)。

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

2021.05.05

http

HTTP缓存: 强缓存与协商缓存 一、HTTP缓存 HTTP缓存针对HTTP响应报文,一般只对GET和HEAD方法响应报文有效。(POST响应在罕见特殊配置下也可以缓存,具体见MDN) HTTP缓存可以存在于浏览器本地,也可以存在于代理服务器。 1. 缓存相关的首部行 1.1 强缓存 优先级从高到低: Pragma、Cache-Control、Expires Pragma和Expires都是HTTP1.0的首部。当Pragma设置为no-cache,则意味着每次请求都无法执行强缓存,只能进行协商缓存。 Cache-Control可以设置如下参数: no-cache: 与Pragma一样,每次请求都无法执行强缓存,只能进行协商缓存,但是比Pragma优先级要低; no-store: 不进行缓存。强缓存和协商缓存都不会触发; max-age: 设置缓存相对过期时长; 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. 协商缓存 如果发现 Cache-Control 或 Expires 二者之一有过期,则发送请求到服务器: 如果缓存首部存在Etag,则发送带If-None-Match的请求;(优先级更高) 如果缓存首部存在Last-Modified,则发送带If-Modified-Since的请求。 ETag 与 Last-Modified 的区别? ETag是服务器根据资源内容,自动生成的唯一的ID。它更能体现资源是否已修改。 Last-Modified主要是有以下三点问题: ① 最短修改时间只能精确到秒;② 有些文件在服务器周期性保存,内容并未修改,这时造成本地缓存的浪费;③ 某些服务器系统不能得到精确的修改时间。 由服务器根据这两个字段判断,缓存是否还可以使用。 如果可以,则意味着协商缓存命中,服务器返回新的响应header信息,但是不带有响应主体。(意味着服务器仍可从缓存中读取响应)(校验码304) 如果校验失败,服务器返回带响应主体的响应报文。(校验码200) 3. 用户行为对缓存的影响 按F5会忽略强缓存,保留协商缓存。 按Ctrl+F5会忽视全部缓存。 4. 如何保证每次资源更新浏览器都会及时更新,防止从缓存读取? 为每一个更新的资源,配置一个独有的资源名。 常用的是在资源后面加上query ID后缀。

数据结构 —— 常见与不常见

2021.05.04

Data Structure

数据结构原始笔记转Blog,然后边学边记。 数据结构学完了忘,别忘了及时回来看看啊。 一、基本常见数据结构 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 二叉树遍历方式 二叉树主要有两种遍历方式: 深度优先遍历:先往深走,遇到叶子节点再往回走。 前序遍历 中序遍历 后序遍历 广度优先遍历:一层一层的去遍历。 层序遍历 4.2 红黑树 5. 图

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(): 从右到左归并。

前端跨域请求、跨页面通信方式

2021.05.02

跨域

前端跨域请求、跨页面通信的几种方式梳理 Refer: 阮一峰CORS 一、跨域问题 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函数执行的代码字符串,里面包含了服务器想要浏览器执行的操作; 浏览器接收响应,作为代码执行。 <!--这里定义要执行的函数--> <script> function show(data){ alert(data) } </script> <!-- 在另一个script元素内发出跨域请求,后端收到show这个请求,然后返回带数据的show函数字符串--> <!-- 因为是script标签,返回的数据作为代码直接在浏览器执行。--> <script src="http://server?callback=show> JSONP的局限性:JSONP只能发GET请求。 2.3 WebSocket WebSocket是一种协议,它不受同源策略限制。 只要服务器支持,使用WebSocket协议(ws:// 或 wss://)进行请求即可。 2.4 代理服务器 服务器间通信不受同源策略限制。因此,可以在本地开启一个同源服务器,用同源服务器与目标服务器进行跨域请求通信,然后页面在本地与本地服务器进行同源请求即可。 二、 跨页面传递数据(跨窗口通信) 同源策略规定: 如果我们有对另外一个窗口(例如,一个使用 window.open 创建的弹窗,或者一个窗口中的 iframe)的引用,并且该窗口是同源的,那么我们就具有对该窗口的全部访问权限。 否则,如果该窗口不是同源的,那么我们就无法访问该窗口中的内容:变量,文档,任何东西。唯一的例外是 location:我们可以修改它(进而重定向用户)。但是我们无法读取 location(因此,我们无法看到用户当前所处的位置,也就不会泄漏任何信息)。 非同源的窗口,还可以通过postMessage向其发送一条消息。这是对于非同源页面引用唯二的两个操作。 — 现代JS教程 要想让同一个二级域下的所有子域都被视作同源,需要在每个页面上添加以下代码: // 为当前页面设置域(默认为当前页面URL的域名) document.domain = 'site.com'; 1. 同源跨窗口通信 1.1 Broadcast Channel API (广播频道) Broadcast Channel API 可以实现同源下浏览器不同窗口,Tab页,frame或者 iframe 下的 浏览器上下文 (通常是同一个网站下不同的页面)之间的简单通讯。 // 1.连接到test_channel广播频道,如果还没有这个频道,这代表创建一个名叫test_channel的广播频道 var 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反向发送消息。); }

在浏览器本地如何储存数据:Cookie和Web Storage

2021.05.02

cookie

localStorage

sessionStorage

indexedDB

浏览器端储存数据的几种方式:cookie,localStorage,sessionStorage,indexedDB 全面整理,包含以前的笔记。 一、Cookie 1. Cookie的作用 HTTP是无状态协议。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设置的cookie,都只能作用于当前域。 Cookie的体积大小不能超过4kB,条目数一般不能超过20条,具体与浏览器实现相关。 5. Cookie中的特殊字段 4.1 path 设置浏览器发送Cookie的url路径前缀。只有这个路径下的页面可以访问到这个Cookie。 一般设置 path=/; 这样这个域名下所有网页都可以访问到这个Cookie。 如果设置 path=/main 则只有main子目录的页面可以拿到Cookie 5.2 domain 设置可访问Cookie的域。 默认情况下,cookie是绑定到域的。也就是说,绑定到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 或 samesite 本身,含义是只在Cookie本身的域下向目标服务器发出请求,才会携带Cookie。、 其他域下,即使向相同的目标服务器发送请求,也不会携带Cookie。 这防止了一种叫做XSRF(Cross-Site Request Forgery 跨网站请求伪造)的攻击: 当用户已经在某一网站(site.com)完成登录认证,服务器返回了用户的Cookie并保存在浏览器中。默认情况下,只要从浏览器向site.com域发送请求,都会携带这个Cookie,服务器就会识别为认证的用户。 这时,如果用户访问了一个带有恶意请求代码(比如)的网站(evil.com),请求会带着Cookie发送到服务器,从而代表用户执行了恶意操作。 5.6 httpOnly(服务器端设置) 这个关键字在浏览器本地无法设置,只能在服务器端set-cookie的时候设置。 设置了httpOnly的Cookie,在浏览器中无法用JavaScript访问,也就是document.cookie看不到。(防止黑客获取到Cookie) 二、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教程

HTTPS协议与常见的网络攻击形式

2021.05.02

http

https

网络攻击

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(比如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握手阶段,这个阶段的所有报文都是明文。TLS握手的流程如下: 客户端发出ClientHello请求,内容包含:一个客户端生成的随机数rand1、支持的TLS协议版本、支持的加密算法、支持的压缩方法; 服务器端回应ServerHello报文,内容包含:一个服务器端生成的随机数rand2、确认的TLS协议版本、加密算法和压缩方法以及服务器证书(公钥和第三方机构数字签名); 客户端收到服务器证书后,对签名进行验证确认服务器身份,如果身份可疑,发出警告提示并由用户确定是否继续通信; 客户端确认服务器身份OK后,取出服务器公钥,然后生成一个随机数pre-master key,并使用服务器公钥对rand1、rand2和pre-master key三者的组合数进行加密,生成双方对话密钥; 客户端向服务器发送报文,内容包含:生成的对话密钥、编码改变通知、客户端握手结束通知(其中包含前面发送的全部内容的Hash值,用来供服务器校验) 服务器收到后,用私钥解密出对话密钥,向客户端发送确认报文:编码改变通知、服务器握手结束通知(其中包含前面发送的全部内容的Hash值,用来供客户端校验) 之后,双方使用同一个对话密钥,加密报文内容,进行常规的Http加密通信。 2.5 HTTPs的缺点?为什么不一直使用? HTTPs的加解密过程繁琐,消耗了两端CPU和内存资源,速度更慢(相比于HTTP慢2~100倍),一般只用在敏感数据的传输上; HTTPs通信中,服务器端必须购买认证证书,是一笔额外的开销。

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架构?如何设计使用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请求方式或对事件资源进行请求来实现。 资源操作 通过HTTP请求方式,对应数据库的增删改查方式,实现对资源的操作。(GET/POST/DELETE/PUT等) 信息过滤Filtering API提供参数,返回过滤后的结果。 例如可以使用Axios发送的请求: // 请求目标为: http://marswiz.com?name=Cool axios.get({ url: 'http://marswiz.com', params: { name: 'Cool', }, }); 返回状态码与错误处理 HTTP返回状态码用来提示客户端资源的信息。(2xx,3xx,4xx,5xx) 如果返回资源状态码为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接口地址。 其他 服务器返回的数据格式,应该尽量使用JSON,避免使用XML。

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

2021.04.21

Vue

渲染函数和h()是最本质的组件渲染方式,应该掌握。 源文件目录: 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

前端发送Ajax请求的方式之一,配合Vue使用。 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响应式基本原理和自己实现的代码,这里记录一下。 1.Vue3响应式基本实现原理 假设reacObj是Vue中的一个响应式对象,它具有属性a和b; Vue3使用reactive(obj)函数,创建一个响应式的对象。 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依赖集,并运行内部的全部方法,来更新依赖它的全部变量。 2.响应式对象内部属性的响应原理 对于属性a,如果我们用一个集合Set记录了依赖a的所有变量和它们的计算方法函数(这些计算方法函数叫做effect),在更新a的时候,将这些方法一一重新调用一遍,就可以实现a更改之后依赖它的变量的实时更新,也就是a属性成为了响应式的。 Vue中,为每个响应式对象内部属性所建立的Set对象称为一个dep(PS:dependency,依赖集),只要在修改任何响应式变量后,对应的依赖集dep内方法全部运行一次,就能实现其他变量的更新。 使用Set对象的理由是:它可以保证依赖集内部没有重复。 3.响应式对象本身的响应原理 一个响应式对象本身可能具有多个属性,需要为它们每个单独建立一个dep依赖集,然后用另一个映射Map将对象属性和dep一一对应。 当track另一个对象属性property2的时候,就把这个property2也添加到映射Map里,然后把所有依赖property2的变量计算方法添加到新的对应dep中,并在Map中与property2对应起来。 这样,这个Map就记录了这个对象obj的全部属性的依赖信息,并可以根据属性更新其对应的那一部分依赖变量。这个Map变量名为depsMap。 整个Vue应用实例的响应式原理 一个Vue应用实例,可能声明有多个响应式对象,每一个响应式对象及其内部属性都应该被记录并追踪(track)。 整个应用实例使用一个WeakMap,记录应用实例中的每个响应式对象。这个WeakMap为targetMap。 track(obj, property)的实现流程 track(obj, property)运行后: 从targetMap中查找obj,如果没有就创建一个新的Map并赋值给obj对应的值,并作为depsMap返回。如果已经存在,则找到对应的depsMap; 在找到的depsMap中,查找property。如果没有则创建一个新的Set,赋值给property对应的值作为dep,并返回这个dep。如果已经存在,则返回找到的Dep; 在dep中加入全部effect。 trigger(obj, property)的实现流程 trigger(obj, property)运行后: 先从targetMap中查找obj,然后找到对应的depsMap; 从depsMap中查找property,找出对应的dep; 对dep中每一个effect进行执行。 弱映射WeakMap:只能储存对象,且当对象在其他地方没有引用的时候,WeakSet内的对象会被垃圾回收机制识别并回收。 reactive()函数:将对象变成响应式 Vue使用reactive(obj)函数,将一个对象变成响应式对象。 官方教程代码如下:(示例,并非源码) 因为在Vue3中,响应式是通过Proxy实现的,它不再单独为每个property设置get和set访问器,而是为每个property采取同样的操作方式。 因此,即使在声明的时候没有声明的property,后续在响应式对象中添加,也同样是响应式的。(与Vue2有区别) 手动实现Vue响应式代码 // targetMap to store depMaps let targetMap = new WeakMap(); // track and trigger function function track(target, key, effect){ // get depsMap from targetMap let depsMap, deps; if ( !targetMap.has(target) ){ depsMap = new Map(); targetMap.set(target, depsMap); } else { depsMap = targetMap.get(target); } // get deps from depsMap if ( !depsMap.has(key) ){ deps = new Set(); depsMap.set(key, deps); } else { deps = depsMap.get(key); } deps.add(effect); console.log(depsMap); } function trigger(target, key){ let depsMap,deps; //If no deps or no depsMap , return directly, nothing to refresh. if ( !targetMap.has(target) ){ return ; } else { depsMap = targetMap.get(target); } if ( !depsMap.has(key) ){ return ; } else { deps = depsMap.get(key); } // run all the effect in deps. (Refresh Data.) for (let effect of deps){ effect(); } console.log(depsMap); } let added; let effect = function (){ added = a.num1 + a.num2; }; function reactive(target){ const handler = { get(target, key, receiver){ track(target, key, effect); console.log(); return target[key]; }, set(target, key, value, receiver){ if ( target[key] != value ){ target[key] = value; } trigger(target, key); return value; }, }; // return a new proxy of target. return new Proxy(target, handler); } let a = reactive({ num1: 1, num2: 2, }); effect(); a.num1 = 4; console.log(added); //6 a.num2 = 40; console.log(added); //44

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文档中:请求的实际路径是./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

记录前端性能优化的方向、方式方法。 前端性能优化主要方向有: 静态资源优化:html、CSS、JS、图片、字体文件等,包括各种打包压缩策略; 页面渲染策略; 原生APP的优化; 网络性能优化; 研发开发流程优化; 性能监控及评价; 常用方法 减少 HTTP 请求 使用 HTTP2 使用服务端渲染 静态资源使用 CDN 将 CSS 放在文件头部,JavaScript 文件放在底部 使用字体图标 iconfont 代替图片图标 善用缓存,不重复加载相同的资源 压缩文件 图片优化 通过 webpack 按需加载代码,提取第三库代码,减少 ES6 转为 ES5 的冗余代码 减少重绘重排 使用事件委托 注意程序的局部性 if-else 对比 switch 查找表 避免页面卡顿 使用 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相关服务选择国内托管;

计算机网络:TCP与HTTP复习

2021.04.07

Network

TCP

HTTP

计算机网络笔记复习:TCP和HTTP,面试容易考。 1. TCP协议及其特点 TCP协议是TCP/IP协议栈里面,用于运输层的协议。 TCP协议的特点: 面向连接:必须先建立连接再通信,通信完协商断开连接; 可靠交付:保证接收方能可靠接收到发送的全部数据,且双方数据完全一致; 点对点:TCP连接是一对一的(两端均由套接字Socket决定: IP+端口号); 全双工:双方可以随时互相发送数据。 同一个IP可以有多个TCP连接,同一个端口号也可以出现在多个TCP连接中。 1.1 TCP报文 TCP协议的首部由20字节的固定部分和4n字节的可变部分构成。其固定部分组成结构如下: 源端口、目标端口(各16位) 注明数据来源和目标的端口号。 序号(32位) 在TCP传输中,每一个字节都按顺序被标上序号。整个字节流的传输起始序号在TCP连接建立握手时设置。 TCP报文首部的序号,表示这个报文段第一个字节的序号。 确认号(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代表请求建立连接。 当SYHN=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时间到后,请求方也释放连接。 2. 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/1.1和HTTP/2。 HTTP1.1使用了持续连接、流水线方式等改进措施。 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.2 响应报文 响应状态码:

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

2021.03.31

Frontend

防抖和节流,在本质上都是为了防止函数被多次频繁触发,采取的保护措施。 防抖和节流的区别:在约定的时间间隔内重复执行函数,是否重新计时。节流不会重新计时,而防抖会。 防抖 (Debounce) 防抖(Debounce):在函数被执行后的规定间隔时间内,无法再次执行函数。如果间隔时间内再次执行了函数,则重新计算时间间隔。 防抖可以保证函数不会被连续触发,规定时间间隔内最多只能触发一次。连续的触发事件如果间隔均小于防抖规定的时间间隔,则只会在最后一次事件结束之后才会执行。 下面是一段实现函数防抖的代码: // 防抖函数 function _debounce(func, delay){ return function (...args){ if (this.timeoutID){ clearTimeout(this.timeoutID); } let timeout = setTimeout(()=>{ func(...args); }, delay); this.timeoutID = timeout; } } function say(e){ console.log(e.target.value); } let debouncedSay = _debounce(say, 1000); document.getElementById('debounceElement').addEventListener('keyup', debouncedSay); //输入结束1S后才会打印到控制台。 节流(Throttle) 节流(Throttle):函数在规定的时间间隔内最多执行1次,时间间隔内多次触发无任何效果,不会重新计算时间间隔。 下面是一段实现函数节流的代码: // 节流函数 function _throttle(func, delay){ return function (...args){ if (this.timeoutID){ console.log('canceled'); return ; } let timeout = setTimeout(()=>{ func(...args); this.timeoutID = null; } ,delay); this.timeoutID = timeout; } } function say(e){ console.log(e.target.value); } let throttledSay = _throttle(say, 1000); document.getElementById('throttleElement').addEventListener('keyup', throttledSay);

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

怕忘。记录各种有价值的前端信息,后续研究。 前端工程化 https://mp.weixin.qq.com/s?__biz=MzI1NTMwNDg3MQ==&mid=2247485393&idx=1&sn=2e7db87ad4cb661201feb384104c537e&chksm=ea36b219dd413b0f949b6fd8dea30456c820a0b938908edc9cc839cdc4d3851e945e06dc95a7&scene=21#wechat_redirect 前端工程化一般包括四个方面:模块化、组件化、规范化和自动化。 1. 模块化 模块化就是将一个大文件拆分成相互依赖的小文件,再进行统一的拼装和加载。比如:CommonJS、ES6模块等。 2. 组件化 模块化是在文件层面上,对代码或资源的拆分。而组件化是在设计层面上,对用户界面的拆分。 一个组件可能在页面上占据独立的一块,并且可以独立实现某项功能。 3. 规范化 一般来说,前端规范化大体上可以分类为编码规范、开发流程规范和文档规范等,每个大类中又有一些子类,如编码规范中包含有目录规范、文件命名规范、js/css代码规范等。 4. 自动化 工程自动化基本包含以下几方面内容: 图标合并 持续集成 自动化构建 自动化部署 自动化测试 目前支持Vue3的Vue组件库 ElementUI Vant Vuetify 数据验证 前端验证:必填项目是否确实、(邮箱、电话号、地址等)格式匹配、密码强度检测、验证码(简单的 图灵测试 ); 后端验证:唯一性验证、验证码、敏感词; 前端验证的主要目的是对不影响安全性的验证进行预校验,减少后端负担,增加用户体验。(比如后端已经提供库存为零信息,购物车中商品在提交给后端前就应该校验是否有库存,否则提交给后端再返回无库存太降低用户体验。) 后端验证是必须的,因为所有关乎安全的验证都必须在后端进行。 前端如何进行seo优化 合理的title、description、keywords搜索对这三项的权重逐个减小,title值强调重点即可;description把页面内容高度概括,不可过分堆砌关键词;keywords列举出重要关键词。 语义化的HTML代码:,符合W3C规范:语义化代码让搜索引擎容易理解网页 重要内容HTML代码放在最前搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取 重要内容不要用js输出爬虫不会执行js获取内容 少用iframe搜索引擎不会抓取iframe中的内容 非装饰性图片必须加alt属性 提高网站速度网站速度是搜索引擎排序的一个重要指标

排序算法基本思想与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处理任务的一种执行机制,通过循环来执行任务队列里的任务。一个任务执行开始到下一个任务执行开始,叫做一次事件循环。 浏览器中的任务分为宏任务和微任务。 1.1 宏任务 以下内容被称为宏任务,这些任务按照出现的顺序在浏览器内部组成一个队列,按照进入的先后顺序执行,先进先出。 下列任务都属于宏任务: 当外部脚本 <script src="..."> 加载完成后,执行这个脚本的任务过程,是宏任务; 用户事件:当用户移动鼠标时,任务就是派生出 mousemove 事件和执行处理程序,这个过程也是宏任务; setTimeout/setInterval这类事件:当安排的(scheduled)setTimeout 时间到达时,会产生宏任务,任务就是执行其回调; 其他类似事件。 宏任务执行的间隙,如果有微任务,则浏览器先执行微任务,然后执行DOM渲染。在一个宏任务的执行过程中不进行任何DOM渲染,只有完成后才进行。 1.2 微任务 微任务仅来自于我们的代码。有如下几种形式: 由 promise 创建的: then/catch/finally 处理程序的执行会成为微任务。 async/await函数也会创建微任务:await之后的代码都会作为微任务异步执行; 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访问器属性有关,暂时不考虑。 let numbers = [0, 1, 2]; numbers = new Proxy(numbers, { get(target, prop) { if (prop in target) { return target[prop]; } else { return 0; // 默认值 } } }); alert( numbers[1] ); // 1 alert( numbers[123] ); // 0(没有这个数组项) 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产生的结果。 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一样,一次传入一个字符串作为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();这个方法可以使任何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>元素在捕获还是冒泡阶段发生。由于历史原因,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分配事件时,可以返回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>默认情况下在页面中是同步加载的,也就是说当页面加载到script标签时,会立即执行里面的代码,整个页面都会进行等待,即使花费时间很长或者DOM根本没加载完成。 17.12.2 异步加载 17.12.2.1 defer 在Script标签中加入defer 特性,就是告诉浏览器不要等待这个脚本。浏览器将继续处理后面的 HTML,构建 DOM。脚本会“在后台”下载,然后等 DOM 构建完成后,脚本才会执行。 defer标记的脚本,会在DOM加载完成后(但DOMContentLoaded触发前),按照先后顺序执行。排在后面的脚本,即使先加载完成,也等待顺序执行。 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 元素,并在检测到更改时触发回调。具体见:https://zh.javascript.info/mutation-observer 17.15 事件循环 当浏览器没有任务执行时,处于休眠状态。当任务出现,则按照出现的先后顺序执行任务,先进入的任务先执行。 17.15.1 宏任务 以下内容被称为宏任务,这些任务按照出现的顺序在浏览器内部组成一个序列,按照进入的先后顺序执行,先进先出。 当外部脚本 当用户移动鼠标时,任务就是派生出 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的原型对象。 ★ 写入和删除从原型对象继承来的属性,不会对原型对象本身的属性产生影响,而是操作在继承的对象中。(当写入的属性在对象本身内部不存在,就会在本地新建一个属性,无论原型链中是否有这个属性。可以理解为这些继承来的属性都是只读的,写入还是在本地。) 如果我们调用 obj.method(),而且 method 是从原型中获取的,this 仍然会引用 obj。因此,方法始终与当前对象一起使用,即使方法是继承的。 for..in 循环会遍历到本身的属性和继承的属性。所有其他的键/值获取方法仅对对象本身起作用,不返回继承的属性。 对象在查询属性时,如果查询的不是对象自身的属性,对象就会自动沿着原型链向上寻找,直到找到最近的并返回。如果到了原型链顶端仍未找到,就返回undefined。 1.3 函数(构造函数)的prototype属性 任何一个函数都自带一个键名为prototype的常规属性; 这个属性值在使用new操作符操作函数时候,会自动成为生成新对象的原型[[prototype]]对象; 一般用于构造函数,手动设置这个prototype属性可以变更构造实例对象的默认原型对象;z 普通非构造函数也自动带有这个属性,只不过默认的 “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 相同)。

现代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学习笔记:Module模块化

2021.03.09

JavaScript

学习内容:《现代JavaScript教程》 15 模块化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”>.

现代JS学习笔记:Map映射

2021.03.09

JavaScript

学习内容:《现代JavaScript教程》 9 Map:映射 9.1 什么是Map Map是一种带键的数据项的集合,是一种特殊的Object。 9.2 Map的特点 Map与普通对象Object数据结构的最大不同是: Map允许键名是任何类型(包含null,undefined和NaN),而Object只有字符串类型的键名(即使传入其他类型也会自动转换为String类型)。 9.3 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 —— 返回当前元素个数。 9.4 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 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。 9.5 Map与Object的互相转换 9.5.1 从普通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’ 9.5.2 从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) 9.3.6 WeakMap 弱映射 WeakMap弱映射,它有两个特殊性: 只能接受对象作为键名,其他类型无效; 键名引用的对象,外部全部失去引用后,即使在Map内存在,也会被垃圾回收机制识别并回收。 常规的Map有一个垃圾回收机制的问题。 当Map的键是一个对象时,即使对象在外部被设置为null,Map也依然在引用着该对象,依然存储在内存中,不会被当做垃圾清除。 WeakMap可以解决这个问题。当一个对象仅仅是作为 WeakMap 的键而存在 —— 它将会被从 map和内存中自动删除。 // 创建方式: let a = new WeakMap(); 9.6.1 WeakMap的方法 WeakMap 不支持迭代以及 keys(),values() 和 entries() 方法。所以没有办法获取 WeakMap 的所有键或值。 WeakMap 只有以下的方法: weakMap.get(key) weakMap.set(key, value) weakMap.delete(key) weakMap.has(key) 因为不能确定浏览器的垃圾回收时机(即使外部对象被解除引用,weakmap里面的元素也可能不会瞬间立即被删除,而是要等待垃圾回收的时机。),所以WeakMap里面的元素数量是不能确定的,因此没设有keys这一类方法。

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以便使其脱离工作环境让垃圾回收器回收

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

Growth

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

木有找到Der~~