《重构》分享

## 概念 段落引用一言以蔽之,就是在不改变外部行为的前提下,有条不紊地改善代码。 《重构》一书的作者对重构的定义: > 段落引用重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。 ## 为什么要重构? 当接手前人开发的某个项目,代码混乱不堪,难以做新功能开发。如果没有重构,代码的设计会逐渐腐败变质。当人们只为短期目的,或是在完全理解整体设计之前,就贸然修改代码,代码将逐渐失去自己的结构,程序员越来越难通过阅读源码而理解原本设计。重构其实是在整理代码,所做的就是让所有东西回到应该的位置上。经常性的重构可以帮助代码维持自己该有的形态。 所谓写代码,代码是写给人看,写给人理解的,然后才是交给机器去执行,你编写代码告诉计算机做什么事,它的响应则是精确按照你的指示行动。但是除了计算机外,你的源码还有其他读者,几个月之后可能会有其他人尝试读懂你的代码并做一些修改,这才是最重要的。如果他理解你的代码,这个修改只需一小时。但是如果一个程序员需要花费一周时间才能理解你的代码,那才事关重大。所以把代码重构成易于理解的代码尤为重要。 重构是时刻保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。项目在演进,代码不停地在堆砌。如果没有人为代码的质量负责任,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。 ## 何时重构? > 事不过三准则:第一次去做某件事,尽管去做;第二次做类似的事情,就会开始反感,但无论如何还是得去做;第三次再接着第二次做类似的事情,你就应该进行重构了。 **添加功能时重构**:如果现有的代码设计无法帮我们轻松添加我们所需要的新功能时,但是如果使用另外一种新的设计可以使我更加快速地实现我所需要的功能,也是重构的时机。 **修补错误时重构**:debug过程中频繁出现错误是一个很明显需要进行重构的信号,如果你的程序出现了意想不到的bug,这正是你的代码需要重构的信号。修复错误的时候重构,可以帮助我们理解代码,从而更清晰地看到bug的所在。 **复审代码时重构**:可以在CR的过程中听取别人的意见,从而找出我们开发过程中没有发现的一些坏设计,从而在代码正式上线部署时应用合理的代码。 ## 重构代码 ### 有坏味道的代码 #### 1. 消除重复的代码 ```js function main( currPage ){ if ( currPage <= 0 ){ currPage = 0; jump( currPage ); }else if ( currPage >= totalPage ){ currPage = totalPage; jump( currPage ); }else{ jump( currPage ); } }; ------------------------------------------------------------------------------ function main( currPage ){ if ( currPage <= 0 ){ currPage = 0; }else if ( currPage >= totalPage ){ currPage = totalPage; } jump( currPage ); }; ``` #### 2. 过长的参数列表 ```js function setUserInfo (id, name, address, sex, mobile) { console.log("id" + id) console.log("name" + name) console.log("address" + address) console.log("sex" + sex) console.log("mobile" + mobile) } setUserInfo(12, "sven", "guangzhou", "mail", "137****") ------------------------------------------------------------------------------ function setUserInfo (userInfo) { console.log("id" + userInfo.id) console.log("name" + userInfo.name) console.log("address" + userInfo.address) console.log("sex" + userInfo.sex) console.log("mobile" + userInfo.mobile) } setUserInfo({ id: "12", name: "sven", address: "guangzhou", sex: "mail", mobile: "137***" }) ``` #### 3. 过多的注释 >注释之所以存在是因为代码很糟糕 。注释的最高境界——代码即注释。 当你感觉需要撰写注释时,请先尝试重构,试着让所有的注释都变得多余。 ```js // 循环foo数组 foo.forEach(function(a){ //年龄大于16 if(a.b > 16){ //创建一个新对象 var n = { person: a, friends: []; }; // 将新对象推入结果中 r.push(n); } }) ------------------------------------------------------------------------------ foo.forEach(function(person){ if(person.age > 16){ var newItem = { person: person, friends: []; }; result.push(newItem); } }) ``` ### 重新组织函数 #### 1. 提炼函数 ```js function getIdArr(arr) { //数组去重 let new=[],newArrIds=[]; for(let i=0;i<arr.length;i++){ if(newArrIds.indexOf(arr[i].id)===-1){ newArrIds.push(arr[i].id); newArr.push(arr[i]); } } //遍历替换 newArr.map(item=>{ for(let key in item){ if(item[key]===""){ item[key]="--"; } } }); return newArr; } ------------------------------------------------------------------------------ function getIdArr(arr) { let filterArr = filterRepeatById(arr) return replaceEachItem(filterArr) } ``` #### 2. 内联函数 ```js function getRating(){ return moreThanFiveLateDeliveries() ? 2 : 1 } function moreThanFiveLateDeliveries(){ return numberOfLateDeliveries > 5 } ------------------------------------------------------------------------------ function getRating(){ return numberOfLateDeliveries > 5 ? 2 : 1 } ``` #### 3. 内联临时变量 ```js let basePrice = project.basePrice return basePrice > 5 ------------------------------------------------------------------------------ return project.basePrice > 5 ``` #### 4. 引入注释性变量 ```js if ((platform.toUpperCase().indexOf("MAC") > -1) && (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize >0){ //do smothing } ------------------------------------------------------------------------------ const isMacOs = platform.toUpperCase().indexOf("MAC") > -1; const isIEBrowser = browser.toUpperCase().indexOf("IE") > -1; const wasResized = resize >0; if(isMacOs && isIEBrowser && wasInitialized() && wasResized){ //do smothing } ``` #### 5. 替换函数 ```js function foundPerson(people){ for(let i = 0;i < people.length; i++){ if(people[i] === "Don"){ return "Don"; } if(people[i] === "John"){ return "John"; } if(people[i] === "Kent"){ return "Kent"; } } return ""; } ------------------------------------------------------------------------------ function foundPerson(people){ const candidates = ["Don", "John", "Kent"]; for(let i = 0;i < people.length; i++){ if(candidates.includes(people[i])){ return people[i]; } } return ""; } ``` ### 重新组织数据 #### 1. 以对象取代数组 ```js let row = []; row[0] = "live"; row[1] = 5; ------------------------------------------------------------------------------ let row = {}; row.name = "live"; row.age = 5; ``` #### 2. 以字面常量取代魔法数 ```js const getWeight = (mass) => mass * 9.81 ------------------------------------------------------------------------------ const GRAVITATIONAL_CONSTANT = 9.81; const getWeight = (mass) => mass * GRAVITATIONAL_CONSTANT; ``` #### 3. 以类取代字段 ```js class Person { constructor(name, bloodGroup) { this.name = name; this.bloodGroup = bloodGroup; } } const person = new Person("joe", "a") ------------------------------------------------------------------------------ class BloodGroup { constructor(name) { this.bloodGroup = name; } } class Person { constructor(name, bloodGroup) { this.name = name; this.bloodGroup = bloodGroup; } } const bloodGroup = new BloodGroup("a"); const person = new Person("joe", bloodGroup) ``` ### 简化条件表达式 #### 1. 分解条件表达式 ```js if(date.before(SUMMER_START) || Date.after(SUMMER_END)){ charge = quantity * winterRate }else { charge = quantity * summerRate } ------------------------------------------------------------------------------ if(notSummer(date)){ charge = winterCharge(quantity) }else { charge = summerCharge(quantity) } ``` #### 2. 合并条件表达式 ```js function disabilityAmount (){ if(isPartTime) return 0; if(monthDisabled > 12) return 0; if(age > 18) return 0; } ------------------------------------------------------------------------------ function disabilityAmount (){ if(isNotDisabilityAmount()) return 0; } ``` #### 3. 取代嵌套条件表达式 ```js function getPayAmount(){ let result; if(isDead) result = deadAmount() else{ if(isSeparated) result = separatedAmount() else { if(isRetired) result = retiredAmount(); else result = normoalPayAmuont() } } return result } ------------------------------------------------------------------------------ function getPayAmount(){ if(isDead) return deadAmount(); if(isSeparated) return separatedAmount(); if(isRetired) return retiredAmount(); return normoalPayAmount(); } ``` ### 处理概括关系 #### 1. 继承 ```js class Manage { constructor(name, id, grade){ this.name = name, this.id = id, this.grade = grade } } ------------------------------------------------------------------------------ class Manager extends Employee { constructor(name, id, grade){ super(name, id) this.grade = grade } } ``` #### 2. 提取类 ```js class Person { constructor(name, phoneNumber) { this.name = name; this.phoneNumber = phoneNumber; } addAreaCode(areaCode) { return `${areaCode}-${this.phoneNumber}` } } ------------------------------------------------------------------------------ class PhoneNumber { constructor(phoneNumber) { this.phoneNumber = phoneNumber; } addAreaCode(areaCode) { return `${areaCode}-${this.phoneNumber}` } } class Person { constructor(name, phoneNumber) { this.name = name; this.phoneNumber = new PhoneNumber(phoneNumber); } } ``` 程序员是唯一负责编写高质量代码的人,我们都应该养成从第一行就写好代码的习惯。编写清晰易懂的代码这样做对我们和接手我们代码的人都有好处。 >任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。 ------------------------------------------------------------------------------- 糟糕的代码可以运作,但早晚会让我们付出代价。你有没有遇到过这样的问题:几周后,你无法理解自己的代码,于是不得不花上几个小时,甚至几天的时间来弄清楚到底发生了什么。是我们的代码写的太烂了吗,是的没错,所以我们就需要重构来将我们的代码变得尽可能清晰。 解决这个常见问题的方法是使代码尽可能清晰。如果做得更好的话,即使是非技术人员也应该能理解你的代码。 概念 重构的目的是让软件更易于理解和修改。但从外部来看,重构造成的修改对可观察的外部行为只造成很小的改变,甚至不造成改变。重构和性能优化一样,通常不会改变组件的行为,只是改变其内部结构,只是性能优化还会改变执行速度;但两者的出发点不同,性能优化有时候会让代码更加难以理解和修改,这也是为了提高性能不得不付出的代价 重构是确保功能无损前提下,不断优化结构,让代码易读,结构清晰,容易调试,易找bug 为什么 代码是写给计算机读的,也是写给开发者读的,你需要让计算机知道你所要达到的意图,同样也需要让其他开发者知道软件的意图。事实上,有时候我们常常不需要记忆自己写过的代码,因为当我们需要了解软件的时候,只要打开形成它的代码,就应该对“想要干什么”和“能干什么”了如指掌了。 软件应该是“自描述”的。 优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。我们无法 100% 遇见未来的需求,也没有足够的精力、时间、资源为遥远的未来买单,所以,随着系统的演进,重构代码也是不可避免的。 有什么用 软件产品最初制造出来,是经过精心的设计,具有良好架构的。但是随着时间的发展、需求的变化,必须不断的修改原有的功能、追加 新的功能,还免不了有一些缺陷需要修改。为了实现变更,不可避免的要违反最初的设计构架。经过一段时间以后,软件的架构就千疮百孔了。bug越来越多,越 来越难维护,新的需求越来越难实现,软件的构架对新的需求渐渐的失去支持能力,而是成为一种制约。最后新需求的开发成本会超过开发一个新的软件的成本,这 就是这个软件系统的生命走到尽头的时候。 重构就能够最大限度的避免这样一种现象。系统发展到一定阶段后,使用重构的方式,不改变系统的外部功能,只对内部的结构进行重新的整理。通过重构,不断的调整系统的结构,使系统对于需求的变更始终具有较强的适应能力 重构是时刻保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。项目在演进,代码不停地在堆砌。如果没有人为代码的质量负责任,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。 1.命名 整洁代码最重要的一环 就是好的名字,所以我们要深思熟虑如何给函数、模块、变量和类命名,使它们 能清晰地表明自己的功能和用法。如果对这个函数内所有难于理解的地方我们做了适当的重构,把每个细小的逻辑抽象成一个小函数并起一个容易理解的名字,当我们看代码时就有可能像看注释一样,不用再像以前一样通过看代码的实现来猜测这段代码到底是做什么的。 2.重复 如果你在一个以上的地点看到相同的代码,那么可以肯定:设法将它们合二为一,程序会变得更好 同一个函数中有相同的代码:提炼出重复的代码,然后让两个地方都调用被提炼出来的那一段代码; 形成原因:代码搬运,复制粘贴 造成后果:加大修改的难度,在发生故障时,需要修改多个位置的代码 解决办法:把这些代码封装独立为一个函数,类,或者接口 3. 提炼函数 看到一个过长的函数或者一段需要注释才能让人理解用途的代码,将这段代码放一个独立的函数中; 做法: 创造一个新函数,根据这个函数的意图来命名它; 只要新函数的名称能够以更好的方式昭示代码意图,你也应该提炼它。但如果想不到一个更有意义的名称就别动 将提炼的代码从原函数复制到新建的目标函数中; 将被提炼代码段中需要读取的局部变量,当作参数传递给目标函数; 在源函数中,将被提炼代码段替换为目标函数调用。 把处理某件事的流程和具体做事的实现方式分开。 - 把一个问题分解为一系列功能性步骤,并假定这些功能步骤已经实现 - 我们只需把把各个函数组织在一起即可解决这一问题 - 在组织好整个功能后,我们在分别实现各个方法函数 函数太长,指向下翻几页都看不到函数尽头的状态 形成原因:行数太长,无法总结函数功能 造成后果:影响阅读和理解 解决办法:把一个臃肿的函数分成不同的小函数 4.内联函数 重构本应以简短的函数表现动作意图,这样会使代码清晰易读,但有时候会遇到某些函数,其内部代码和函数名称同样清晰易读,如果是这样,你应该去掉这个函数,直接使用其中的代码。使用的太多的间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,造成在委托动作之间晕头转向。 内联临时变量 有一个临时变量,只被一个简单的表达是赋值一次,而它妨碍了其他重构手法。将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。 如果这个临时变量妨碍了其他的重构手法,比如提炼函数,你就应该将它内敛化 注释性变量 你有一个复杂的表达式。将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。 表达式有可能非常复杂难以理解。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。 在条件逻辑中,你可以用这项重构将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。另一种情况是:在较长的算法中,可以运用临时变量来解释每一步运算的意义。 替换算法 把某个算法替换为另一个更清晰的算法,将函数本体替换为另一个算法,重构可以把一些复杂东西分解为更简单的小块,但有时候必须将整个算法替换为较简单的算法。 以对象取代数组 数组是很常见的数据结构,他们只用于以某种顺序容纳一组相似对象,如果一个数组容纳了多种不同的对象,这会给我们带来麻烦,对象可以是用键值来表达这个信息。 字面常量 如果我们有很多重复的值且表示一样的含义,但没有明确地说明,那么我们应该将它们转换为常量,以便每个人都知道它们的含义,并且如果需要更改,我们只需更改一个地方就行了。 魔法数是指拥有特殊意义,却又不能明确表现出这种意义的数字。 现在我们知道9.81实际上意味着GRAVITATIONAL_CONSTANT,我们不必重复自己。 上面我们用常量 GRAVITATIONAL_CONSTANT 表示 9.81 ,这样别人一看就知道它表示是万有引力常数常量。 类型码 我们可以将字段替换为其自己的数据类,这样在记录数据中会有更强灵活性。如果我们想扩充 bloodGroup (血型)的种类,我们可以把 bloodGroup 重构成一个类。这样,我们就可以在bloodGroup字段中存储更多种类的数据。 5.分解条件表达式 在带有复杂条件逻辑的函数中,代码常常会然你弄不清楚为什么会发生这样的事,我们可以将它分解成多个独立函数,每个小块代码的用途,用心函数命名,这样可以突出条件逻辑,更清楚的表明每个分支的作用,并且突出每个分支的原因。有一复杂的条件语句。从if、then、else三个段落中分别提炼出独立函数。 嵌套表达式 函数中的条件逻辑使人难以看清正常的执行路径,如果每个分支都是正常行为,就应该单独检查该条件,并在该条件为真时立即从函数中返回,保持单一出口能使这个函数更清楚易读。 函数继承 避免行为重复是很重要的,在各个class中拥有一些构造函数,他们的本体几乎一致,在 类声明 或 类表达式 中用于创建一个类作为另一个类的一个子类。 提取类 如果我们的类很复杂并且有多个方法,那么我们可以将额外的方法移到新类中。 上面我们将Person类不太相关的方法addAreaCode 移动了自己该处理的类中。 通过这样做,两个类只做一件事,而不是让一个类做多件事。 // 下面的函数使用了全局变量。 // 如果有另一个函数在使用 name,现在可能会因为 name 变成了数组而不能正常运行。 6. 嵌套的条件语句对于代码维护者来说绝对是噩梦,对于阅读代码的人来说,嵌套的if、else语句相比平铺的if、else,在阅读和理解上更加困难,有时候一个外层if分支的左括号和右括号相距一屏才能看完的代码。用《重构》里的话说,嵌套的条件语句往往是由一些深信“每个函数只能有一个出口”程序员写出的。下面优化代码: 我们有时候完全忽略之前的代码,重新进行一遍实现。然而重新实现一遍代码真的不难,难点在于如何让之前的遗留代码还能正常工作。我们常常要手工去处理这些遗留代码,不仅非常花费时间,还很容易出错,如果之前的代码自动化测试不足,那基本上等于自己给自己挖了一个大坑。有时,我们还可能会让代码中存在两个版本的逻辑,给新的API一个版本号,这就更让人疑惑了。试想,如果我们在修改已有的代码,我们是否应该用新的API呢?新的API真的能和已有的老版本API兼容吗? 7. 有时候一个函数可能接收多个参数,而参数的数量越多,函数就难以理解、使用和测试。下面看一个函数:使用这个函数的时候得小心翼翼,如果搞反了某两个参数的位置,那么将得到不同的结果。这个时候可以把参数放在一个对象里传递:太长的参数列难以理解,太多的参数会造成前后不一致、不容易使用,而且一旦你需要更多数据,就不得不修改它。如果将对象传递给函数,大多数修改都将没有必要。 8.// 判断是什么浏览器// 循环判断,将对应关系抽象为配置,更加清晰明确 组件 两者的不同之处? - 没有了class关键字;使用了函数来代替 - 在函数式组件里没有this; 而是使用了函数作用域来调用 根据重构的规模可以大致分为大型重构和小型重构: 大型重构:对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入bug的风险也会相对比较大。 小型重构:对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名和注释、消除超大类或函数、提取重复代码等等。小型重构更多的是使用统一的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入bug的风险相对来说也会比较小。什么时候重构 新功能开发、修bug或者代码review中出现“代码坏味道”,我们就应该及时进行重构。持续在日常开发中进行小重构,能够降低重构和测试的成本 大部分开发者其实并不愿意把精力“浪费”在重构上,有可能出现很多问题并且带不来多少拿得出手的成绩;重构总是会在“不经意间”破坏原有功能,带来的麻烦很多,投入与收益完全不成比例。 重构是注重实践的技艺,仅仅了解其理念而忽视实践则有如抟沙作饭,白费心思;而企图把它当做“万金油”来解决所有问题也只会陷入不恰当重构的陷阱,最终得不偿失。只有在合适的场景下恰当的实践,才会实现其应有的价值。

南浮宫魅影

2021-07-19 12:11:21 5次观看
面试

V8的sort实现

## 前言 相信大家对sort的这个方法并不陌生,这就是数组的排序方法,众所周知,排序可以用很多种方法实现,插入排序,冒泡排序,快速排序等等,那V8内部用的是哪一种排序方法呢? ## Array.prototype.sort sort() 方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的。 ::: hljs-right ————MDN ::: ```js const months = ["March", "Jan", "Feb", "Dec"]; months.sort(); console.log(months); // expected output: Array ["Dec", "Feb", "Jan", "March"] const array1 = [1, 30, 4, 21, 100000]; array1.sort(); console.log(array1); // expected output: Array [1, 100000, 21, 30, 4] ``` ## V8的sort ES规范没有指明sort具体使用哪一种排序算法,查资料显示V8在7.0版本之前,如果数组小于10,那么使用插入排序,否则使用快速排序 ::: hljs-center ![image.png](http://img.nanwayan.cn/image/png/image.png) ::: 测试后快速并没有插入快,原因可能是快速排序不是稳定的排序算法。 ps:稳定算法是指不会打乱原有顺序的排序算法,比如,排序后的0,1,2,3,8,8,8,这三个8有可能之前不是这么排列的,就好比两个商品价格相同,如歌按照价格排序,在不稳定的排序算法下就有可能将原有的顺序打乱。 于是V8采用了一个混合排序的算法:TimeSort ## What is TimSort? TimSort会利用数组本身的升序和降序特性来划分数组,比如对集合{7,4,2,1,1,3,5}进行排序。发现从第一个元素开始是一个降序。那么我把集合分割成下面两个字集合。{7,4,2,1}{1,3,5}。因为降序,第5个元素1,被划入后一个集合。就是分析待排序数据,根据其本身的特点,将排序好的(不管是顺序还是逆序)子序列的分为一个个run分区,之后将一个个run通过归并排序合并。 简易实现: ```js const arr = [ 3, 12, 4, 30, 8, 26, 27, 20, 29, 5, 11, 30, 21, 24, 7, 30, 5, 8, 4, 29 ]; function merge(left,right){ let result = [],iLeft = 0,iRight = 0 while(iLeft < left.length && iRight < right.length){ if(left[iLeft] < right[iRight]){ result.push(left[iLeft++]) }else{ result.push(right[iRight++]) } } while(iLeft < left.length){ result.push(left[iLeft++]) } while(iRight < right.length){ result.push(right[iRight++]) } return result } function timeSort(arr){ if(!arr || arr.length < 2)return arr let runs = [],sortedRuns = [],newRun = [],len=arr.length for(let i=1;i<len;i++){ if(arr[i]<arr[i-1]){ runs.push(newRun) newRun = [arr[i]] }else{ newRun.push(arr[i]) } if(len-1 == i){ runs.push(newRun) break } } for(let run of runs){ sortedRuns = merge(sortedRuns,run) } return sortedRuns } const timeSort2 = "timeSort" console.time(timeSort2) console.log(timeSort(arr)) console.timeEnd(timeSort2) ``` 只是简单实现,并没有提升很多。希望以后有机会可以真正理解并写出来timsort。 ## 参考资料 - [男科一梦(再续一集)-TimSort的实现](https://mp.weixin.qq.com/s?__biz=MzI2MTY0OTg2Nw==&mid=2247483816&idx=1&sn=079af3d70efcb68efa7400f09decb59c&chksm=ea56650cdd21ec1ace7c8fd168d62feb636e4b32f9a4d90329fe479489d8e7a70e612df8920b&token=2074049324&lang=zh_CN#rd) - [timsort是什么,如何用代码实现?](https://cloud.tencent.com/developer/article/1773970) - [讲下 V8 sort 的大概思路,并手写一个 sort 的实现](https://mp.weixin.qq.com/s/zrhwCosK4fi3uCA9Gms3Lg)

南浮宫魅影

2021-04-26 14:13:19 12次观看

一个页面卡顿的原因

# 一个页面卡顿其原因有哪些呢?有什么办法锁定原因并解决卡顿? 这个问题涉及到的东西很多,是一个非常广且有深度的问题,我还记得当初面试官问我这个问题我是这么回答的: 1. 因为现在的应用都是框架开发的,一般都是单页面,所以都会使用路由来决定展示哪些组件,我们可以找到发生卡顿问题的路由,再去查看当前路由涉及到的所有组件,将组件一一注释掉,直到页面不卡顿为止,就可以判断出页面卡顿是因为哪个组件而产生的了。 2. 或者查看一下网络是否请求过多,导致数据返回太慢,适当做一些缓存。 3. 也可能是打包后的某个文件过大,导致文件没有及时下载,渲染不及时,可以拆分文件。 4. 浏览器某一帧渲染的东西太多,导致卡顿,也有可能有太多的重绘回流。 其实还有最重要的一点就是长时间卡顿也有可能是内存泄漏导致的。 ## 1.那什么是内存泄漏呢 程序中动态分配的内存由于某种原因没有释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃的严重后果。用我自己的话来说就是,不再用到的变量没有及时回收,就有可能造成内存泄漏。 ## 2.js中的内存存储 js中内存分为栈内存和堆内存,基本数据类型的变量放在栈内存中,比如`string`,`number`,`boolean`,`null`,`undefined`,`symbol`,`bigint`。而引用类型的变量是放在堆内存中的,`object`。简单说一下因为基本数据类型一般声明他的大小就是固定的所以放在栈内存中。而引用数据类型他的大小是不固定的,比如一个对象可以新增删除属性,所以放在堆内存中。 ## 3.GC 一些变量不再使用那么他就是垃圾,如果他一直保存在内存中,最终就有可能导致内存占用过多的情况,最终就会导致程序运行速度减慢或者崩溃。所以这些垃圾应该被程序回收掉,js就引入了垃圾回收机制。 js中的垃圾回收机制是自动回收,我们不需要关心为变量分配多大的内存,也不需要关心何时去释放掉,因为js内部会自动完成这些操作。但是我们仍然需要关心内存的管理,因为如果不合理的使用内存就会导致内存泄漏。 # 4.浏览器开发者工具查看内存情况 我们系拿来看看如何使用谷歌浏览器来查看js运行时的内存情况。 打开开发者工具,找到`Performance`这一选项,有一些按钮分别是,开始录制,刷新页面,清空记录,记录可视化内存,手动触发垃圾回收等等。 还有一个`memory`选项,主要记录某段时间内页面堆内存的具体情况以及js堆内存加载时间的分配情况。 ## 5.内存泄漏的场景 首先来列举几种常见的几种场景: 1. 过度依赖闭包 2. 意外引起的全局变量 3. 遗忘的定时器 4. 没有及时移除监听的事件 接下来来介绍一下各种情况 ### 闭包 ### 全局变量 ### 定时器 ### 监听事件

南浮宫魅影

2021-04-24 17:12:37 5次观看

GC

V8 的垃圾回收策略是基于分代式垃圾回收机制。在所有垃圾回收的算法中,没有一种能胜任所有的场景。因为在我们的实际应用中,对象的生存周期长短不一,不同的算法只能针对特定的情况具有最好的效果。 因此目前的垃圾回收算法一般是按照对象的存活时间将内存进行分代,然后对不同的内存代采用不同的垃圾回收算法。 V8 的内存分代 在 V8 中主要将内存分为新生代和老生代,新生代中的对象存活时间较短,老生代中的对象存活时间较长或常驻内存,新生代中的对象有机会晋升到老生代。 新生代老生代表示 V8 堆整体的大小就是新生代的内存空间加上老生代的内存空间。在默认情况下,如果一直分配内存,在 64 位操作系统和 32 位操作系统下分别只能使用约 1.4 GB 和 0.7 GB 的大小。 对于新生代而言,在 64 位和 32 位操作系统下内存的最大值为 32MB 和 16MB;对于老生代而言,在 64 位和 32 位操作系统下内存的最大值为 1400MB 和 700MB V8 的主要垃圾回收算法 根据不同的分代,V8 在新生代中使用 Scavenge 算法进行垃圾回收,而在老生代中使用 Mark-Sweep 和 Mark-Compact 进行垃圾回收。 Scavenge 算法 Scavenge 算法是新生代中的对象进行垃圾回收的算法,其主要采用了 Cheney 算法,算法的核心思想是: 将堆一分为二,每一部分空间称为 semispace,然后采用复制的方式进行垃圾回收。在这两个 semispace 中,只有一个处于使用中,另一个处于闲置状态。处于使用中的空间称为 From 空间,处于闲置中的空间称为 To 空间。当我们在分配对象的时候,首先在 From 空间中进行分配。当进行垃圾回收的时候,检查 From 空间中的存活对象,将存活对象复制到 To 空间,而非存活对象的空间将会被释放。完成复制之后,From 空间变为 To 空间, To 空间变为 From 空间,即进行角色互换。 Scavenge 算法的优点是时间效率较高,缺点是只能利用一半的内存。由于该算法只复制存活的对象,因此对于生存周期较短的场景(新生代),存活的对象较少,非常适合应用该算法进行垃圾回收。 当一个对象在新生代中经过多次复制依然存活,它将被认为是生存周期较长的对象。这些生命周期较长的对象会被移动到老生代中,采用新的算法进行管理。对象从新生代移动到老生代称为晋升。 因此我们在将 From 空间的对象移动到 To 空间之前需要进行检查,在一定条件下需要将存活周期较长的对象移动到老生代中,也就是完成对象晋升。 对象晋升的主要条件有两个: 对象是否经历过 Scavenge 回收 在默认情况下, V8 的对象分配主要集中在 From 空间,对象从 From 复制到 To 空间的时候,会检查它的内存地址来判断该对象是否经历过一次 Scavenge 回收。如果经历过,会将该对象复制到老生代空间中;否则复制到 To 空间。 To 空间的内存占比超过 25% 当要从 From 空间复制一个对象到 To 空间的时候,如果 To 空间已经使用了超过 25%,则这个对象直接晋升到老生代空间中。 Mark-Sweep 和 Mark-Compact 对于老生代中的对象,由于存活对象占比较大,再采用 Scavenge 算法会造成两个问题: 存活对象较多,复制存活对象的效率将会很低 浪费一半的空间 因此 V8 在老生代中主要采用 Mark-Sweep 和 Mark-Compact 相结合的方法进行垃圾回收。 Mark-Sweep 实际上就是标记清除的意思,它分为标记和清除两个阶段。该算法会遍历堆中的所有对象,并标记存活的对象,在随后的清除过程中,清除未被标记的对象。可以看出 Scavenge 中只复制活着的对象,而 Mark-Sweep 中只清理死亡的对象。活对象在新生代中占较少一部分,死亡对象在老生代中占较少一部分,这是两种回收方式能高效处理的原因。 如图所示,黑色部分标记为死亡的对象。 标记清除 Mark-Sweep 算法最大的问题是,在进行一次垃圾回收之后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题。例如我们要给一个大对象分配内存的时候,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收机制,而这次回收是没有必要的。 因此,为了 Mark-Sweep 解决内存碎片的问题,Mark-Compact 算法被提出来了。Mark-Compact 是标记整理的意思,是在 Mark-Sweep 的基础上演变而来的。Mark-Compact 在标记对象为死亡之后,在整理的过程中,将活的对象往一端移动,移动完成之后直接清理掉边界外的内存。 由于 Mark-Compact 需要移动对象,因此它的执行效率不可能很快,所以在取舍上, V8 主要采用 Mark-Sweep 算法,在空间不足以给从新生代中晋升过来的对象分配空间的时候才使用 Mark-Compact。 Incremental Marking 为了避免出现 JS 应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的三种基本算法都需要将应用逻辑暂停下来,待执行完回收之后再恢复应用程序的执行,这被称为”全停顿“。 在 V8 的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置较小,且其中存活的对象较少,所以即使它是全停顿也影响不大。但是在老生代中,空间配置较大,存活对象较多,全堆垃圾回收的标记、清理、整理等操作所造成的停顿就会较大,需要设法改善。 为了降低全堆垃圾回收带来的停顿时间,V8 采用了增量标记,也就是将原本需要一口气完成标记的过程拆分为许多小步进行,每做完一小步就让 JS 应用逻辑执行一小会,标记与应用程序交替执行直到标记完成。

南浮宫魅影

2021-04-03 19:46:55 7次观看
面试
面试题

Vue 的奇淫技巧

当我们需要导入很多组件时是不是要这么写 ```js import lineChart from "@/widgets/lineChart"; import StackedAreaChart from "@/widgets/StackedAreaChart"; import DataTable from "@/widgets/DataTable"; import BarChart from "@/widgets/BarChart"; import XyBarChart from "@/widgets/XyBarChart"; import StackBarChart from "@/widgets/StackBarChart"; import HorizontalBar from "@/widgets/horizontalBar"; import PieChart from "@/widgets/PieChart"; import RoseChart from "@/widgets/RosePie"; import BasicRadarChart from "@/widgets/BasicRadarChart"; ``` 而现在有一种批量导入的方法,而且支持动态导入,岂不是很香 ```js const path = require("path"); const files = require.context("@/widgets", false, /.vue$/); // console.log("files: ", files.keys()); const modules = {}; files.keys().forEach((key) => { const name = path.basename(key, ".vue"); modules[name] = files(key).default || files(key); }); ``` 之后只需要在`compoments`加入即可 ```js components{ ...modules } ``` 如此一来这个文件夹下的所有vue文件都将被导入进来了

南浮宫魅影

2021-02-26 22:04:58 27次观看
javascript
vue
  • 1
  • 2
  • 3
  • 4
  • 9