为了提升代码质量我做了哪些努力?(关于重构、ES6-ES13、React篇,近2万字,建议收藏)

10,473 阅读31分钟

前言:

关于为什么要写这篇文章,灵感一方面是前几日看了一篇名为《你会用ES6,那你倒是用啊》的文章,文中说的大部分情况真的是我亲身经历过的,明明有些知识技能已经学会了,可工作中却总想不起来用,那学它干啥? 另一方面是最近自己辞职在家整理自己的简历和自己这两年做过的东西,所以我想就我的个人感受分享一下关于我对提升代码质量的看法和做法,主要是从方法和推荐使用角度入手,分析如何才能让我们的代码看起来更优雅,运行起来性能更优秀。文章比较长,建议收藏起来慢慢看。

注 :文中关于一些之前分享过的知识点,有给链接支持,可以复用的这里就不再做重复,因为不希望为了凑字数而写,这不是我的初衷。我把自己认为好的东西分享大家,也在这个过程巩固了自己对知识点的认知,希望这个良性循环一直保持吧

1636027085249.jpg

代码质量是个很广的话题,本篇先从三个方面做分享(后期会逐渐补充),分别是:

1. 代码重构 ;

2. ES6(7、8、9、10、11、12、13)- 会持续更新;

3. React相关优化 。

一、代码重构

关于重构可能是大家比较不愿意去做的一个事情,因为重构意味着我们要重新梳理代码逻辑功能需求,以及有可能延误工期。但我是感觉我们是需要有一种渐进式重构意识存在的,如果当前的代码已经远远落后于现有技术,或者可读性已经无法容忍,那么还等什么,最好的解决方案就是重构,重构可以大幅度的提升代码的可读性,提高代码的简洁性,也更方便后期维护。

1. 重构前的资源协调

我们想要重构,首先要考虑的就是让业务方意识到重构的必要性、咨询各部门资源的剩余量、说服各部门参与配合前端重构。 导致重构的原因: 需求的频繁变动,紧急需求倒排工时,没有长期规划方向同步开发,团队代码风格不统一,技术的更新迭代等。

重构的必要性: 强调重构带来的技术收益和业务收益,提供切实可行的重构计划方案(包括人力资源,排期,改动范围等),让业务方、产品、后端、测试看到开发中的痛点和技术上的瓶颈,如不重构可能带来的后果(更新迭代的局限性,开发进度的缓慢,排查问题困难)

重构的好处: bug数量减少、维护成本降低、排查问题变快、开发速度提升等

2. 重构的步骤

根据重构涉及的范围可分为以下4种场景:

A. 改头换面重构( vue -> react );

我第二家公司刚入职就参与了一场大重构,把原来老项目vue换成react。 我们首先是把框架搭建起来,然后把需要重构的各个页面的优先级做一个排序,设计、前端、后端、测试全部参与进来,接口大部分还是可以重用,但前端需要重新写,当时。这里的重点是要保证旧的项目是可运行的,新的功能尽量在新项目里开发和修改,排列好优先级,逐步迁移,一口吃不成个胖子。

B. 代码格式规范性重构( js -> ts );

这个重构的重点是:先挑选一个目标或者一个改动方向,有把握地进行,保证每次修改都可以跑通,毕竟静态校验不是修改本身功能逻辑,出错的机率较小,我们少量多次进行修改即可。

C. 公共组件重构;

关于公共组件的重构,要遵循优雅降级和渐进增强的原则,如果当前公共组件被依赖的较少,那一次性即可完成全部更新,但如果是使用频率较高的组件重构可能影响的范围较广,那就需要在兼容老的写法的同时做渐进式更新重构,后续再修改时逐步将旧的方法放弃。

D. 具体功能重构。

本来以为文字可以表达清楚,可当自己写下后发现还是画图表达更加清晰: 先检查是否有被其他组件引用,是否有依赖项,如有,保证此次重构不会对其产生影响,如都无,梳理当前业务,和测试及产品同步修改范围(一般情况下不涉及后端改动的,通知后端知晓,以防有问题需要咨询后端同学)->前端开发->协同测试完成回归->通知产品验收->通知业务方改动,建立企业微信问题反馈群,保证有问题随时沟通处理。

1636029133631.jpg

3. 代码修改规则

具体到代码的修改,推荐遵循以下几种规则:

  • 单一职责原则(SRP:Single responsibility principle)

又称单一功能原则,面向对象五个基本原则(SOLID)之一。它规定一个类应该只有一个发生变化的原因。

姓名 :单一职责原则

英文名 :Single Responsibility Principle

座右铭 :There should never be more than one reason for a class to change. 应当有且仅有一个原因引起类的变更。。。意思就是不管干啥,我都只干一件事,你叫我去买菜,我就只买菜,叫我顺便去倒垃圾就不干了,就这么拽

脾气 :一个字“拽”,两个字“特拽“

伴侣 :老子职责单一,哪来的伴侣?

个人介绍 :在这个人兼多责的社会里,我显得那么的特立独行,殊不知,现在社会上发生的很多事情都是因为没有处理好职责导致的,比如,经常有些父母带着小孩,一边玩手机,导致小孩弄丢、发生事故等等

其实意思也就是一个方法,一个页面组件最好只有一个功能或者职责,尽量少耦合多个功能放在同一个方法里面,这样对于后期查找问题,复用逻辑来说都是很好的习惯。

摘自掘友:LieBrother

  • 命名规范

命名规范是老生常谈了,我们常常吐槽别人起名的同时也需要关注一下自己的命名规则,命名好像你衣橱的衣服,一目了然更要整齐划一。命名规则参考

  • 风格统一 Uniform style

代码风格统一,我们每个人开发都有自己的风格,但很多时候同一个人写出的代码会有多种风格,这导致自己写的代码自己后期维护也不好找问题。如果公司团队有规范,我们尽量遵守,或者你认为公司的规范不对,可以提出来,然后团队开发尽量向其靠拢,这样我们后期维护也不至于有太多的怨言,哈哈哈哈....

  • 重复代码-- Repeat Code

关于重复代码这个我建议如果多于 1 处以上的要提成页面级的功能组件,如果超过 3 处以上的提到公共组件。我们尽量减少重复代码的书写,一方面浪费时间,另一方面维护起来也困难,当然功能组件和公共组件也可以是差异化的,根据传参不同,返回不同内容。

最后的最后:

最后说说我的想法吧:当代码有了坏味道,我们要做的第一件事就是标记起来,安排时间及时改善。打个比方,你发现每天路过的那个墙面有人挖了个洞,过两天发现洞越来越大,可能是因为从这里过可以抄近路,可是当走的人多了,洞口越来越大,这面墙也越来越危险,随时可能有塌方的可能。我们在看到这个洞的时候是不是可以想一想:既然这个捷径是给大家带来了方便,那是否可以通知物业或者居委会把这面墙拿掉?如果是规范问题,无法拿掉,或者研究如何把墙面修补好? 而不是看着它一天天越来越危险。 要知道,雪崩时没有一片雪花是无辜的 。

二、ES6 - ES13

这里说的 ES6 是一个广义的概念,包括并不限于ES6、ES7、ES8、ES9、ES10 ... 等的一个统筹概念。我今天就给大家分享一下,那些我平常工作中关于常用的ES6的使用习惯,如有误,望指正。

src=http___image.bubuko.com_info_201708_20180111000417558065.jpg&refer=http___image.bubuko.jpeg

1. 变量定义:

首先就是变量的定义,现在代码里已经没有再出现过var,取而代之的就是letconst,最多的就是常量const,原因就在他们的区别之处了👇。

letconstvar 区别

  1. let const 是块级作用域,变量只在声明的代码块中生效,var 做不到;

  2. var 变量声明提升到代码块的头部,这违反了变量先声明后使用的原则

  3. 还有一个原因是 js 编辑器会对 const 进行优化,有利于提高程序的运行效率;

  4. const 声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。

  5. 常量 const 声明必须赋值,只声明一次,赋值一次,const 声明的对象,是可改变其属性的;

  6. 变量 let 可以先声明再赋值 只可声明一次,赋值多次

总结:从 varlet const ,这个习惯是要改变自己的认知的。看到这里如果你早已是这样做的,你很棒。如果你的意识还停留在 var 的时代,那么现在就需要自查一下喽,毕竟为了让自己的代码更严谨些,这个习惯是必须要养成的。


2. 默认参数:foo(a=1,b=2)=>{}

一个有行参的函数,当实参缺失或值为undefined时则提供一个初始值,然后将根据不同情况来判断是否启用默认值。

当参数值缺失时:

const foo = (name='xiaoqiu', age = 18) => { 

      console.log(name,age)
}

// 当缺失时会自动启用默认参数的值

foo('zhangsan') // zhangsan 18

当参数为 undefined时:

const foo = (name='xiaoqiu', age = 18) => {
      
      console.log(name,age)
}

// 当第二个参数为undefined时,会被识别为未赋值,就会自动启用默认参数的值

foo('zhangsan',undefined) // zhangsan 18 

当参数为 nullfasle时:

const foo = (name='xiaoqiu', age = 18) => {
      
      console.log(name,age)
}

// 当第二个参数为 null / false时,会被识别为当前有值,只是被赋值为 null / false 了

foo('zhangsan',null) // zhangsan null

foo('zhangsan',false)  // zhangsan false

3. 模版字符串

升级了我们使用字符串的方式,在模版字符串之前,我们想要在字符串中拼接变量需要‘字符串’+变量+‘字符串’来组装,看起来既不好看,写起来也很麻烦。而当我们有了模版字符串了以后,变量拼接变得容易,让字符串执行计算,执行函数也都成为可能。

变量拼接

const name = `xiaoming`; 
const age = 18 ;

console.log(`My name is ${name},${age}。`,) // My name is xiaoming,18。

换行拼接

const a = `123 
    456`; 
console.log(a)

// 123
        // 456

执行计算

const x=10;
const y=100;

console.log(`a=${x-5},b=${x+y}`);  // x=5,y=110

执行函数

function answer(){
 return "今天是星期一";
}
console.log(`我:今天是星期几?

你:稍等我看看,${answer()}`);

// 我:今天是星期几?

// 你:稍等我看看,今天是星期一

4. 解构赋值

允许按照一定模式,从数组和对象中提取值,对变量按顺序进行赋值,这被称为解构,概念比较抽象,下面就结合实际示例来说明。

利用数组实现联合声明:

const [a, b, c] = [1, 2, 3]; 

数组的解构:

const [x, y, z] = [1, 2, 3];

x // 1
y // 2
z // 3

数组的解构--剩余参数的赋值:

const [x, y, ...z] = [1, 2, 3, 4, 5];

x // 1
y // 2
z // 3,4,5

json数据的解构:

const obj = { a: 'haha', b: 'gaga' ,c: 'guagua'};
const {a, b, c} =obj; 

a // haha
b // gaga
c // guagua

复杂的json数据解构:

let node = {
    type: "Identifier",
    name: "foo",
    obj: {
        start: {
            line: 1,
            column: 1
        },
        end: {
            line: 1,
            column: 4
        }
    }
};

// 解构 node.obj.start
let { obj: { start: localStart }} = node;
console.log(localStart.line); // 1
console.log(localStart.column); // 1

字符串的解构:

const [a, b, c, d, e] = 'hello';

a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

函数参数的解构:

function add([x, y]){
  return x + y;
}

add([1, 2]); // 3

直接将函数返回值解构:

function example() {
  return [1, 2, 3];
}

const [a, b, c] = example();

交换变量的值:

let a = 1;
let b = 2;

[a, b] = [b, a];

模块的指定方法解构:

import { useRequest } from 'ahooks';

解构赋值使用的场景是非常多的,上面只是将我的常用罗列了出来,欢迎各位同学补充哦!

5. 箭头函数

允许使用“箭头”(=>)来定义函数。意为将原函数的function关键字和函数名都删掉,并使用=>连接参数列表和函数体。它没有自己的thisargumentssupernew.target

箭头函数和普通函数区别

  1. 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。不能直接修改箭头函数的 this 指向;
  2. 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会报错;
  3. 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替;
  4. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数;
  5. 箭头函数不支持重命名函数参数。

箭头函数的this指向:

  1. 普通函数的 this 对象的指向是可变的,但是在箭头函数中 this 是固定的,实际原因是箭头函数根本没有自己的 this ,导致内部的 this 就是外层代码块的 this
  2. 箭头函数的 this 指向的是父级作用域的 this ,是通过查找作用域链来确定 this 的值,也就是说它指向的是定义它的对象,而不是使用时所在的对象;普通函数指向的是它的直接调用者;
  3. 箭头函数外层没有普通函数,严格模式和非严格模式下它的 this 都会指向 window (全局对象)。

书写方式:

// 普通函数
const f = function (a) {
  return a;
};

// 箭头函数
const f = a => a;

//当无参数时,用圆括号代替
const f = () => {
    return "1"
};

rest代替 arguments

const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5)

6. 运算符的扩展

运算符即执行程序代码运算,会针对一个以上操作数项目来进行运算。下面整理了新规则中各个运算符的使用场景,最后一个我还真没有用过🤔

扩展运算符 ...

扩展运算符 spread 是三个点 ...,它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列,当放在数组中时只能放在参数的最后一位。

展开数值、去掉数组的中括号、合并数组、:

console.log(...[1, 2, 3])           // 1 2 3  

console.log(1, ...[2, 3, 4], 5)     // 1 2 3 4 5 

console.log([ 1, ...[2, 3, 4], 5 ])  // [1, 2, 3, 4, 5]

合并声明:

const [first, ...rest] = [1, 2, 3, 4, 5];  

console.log(rest)  // [2, 3, 4, 5]

合并数组 / 对象:

const a=[2,4,7,9]

// ES5  
[1, 2].concat(a)    // [1, 2, 2, 4, 7, 9]
// ES6  
[1, 2, ...a]        // [1, 2, 2, 4, 7, 9]

字符串扩展成数组:

[...'happy']  

// [ "h", "a", "p", "p", "y" ]  

展开 Iterator 接口的对象:

扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符。

let map = new Map([  [1, 'one'],  
[2, 'two'],  
[3, 'three'],  
]); 

[...map.keys()];     // [1, 2, 3]
[...map.values()];   // ['one', 'two', 'three']

数组和对象的拷贝:

还可以使用扩展运算符拷贝对象,之前有分享过,这里是传送门

配合new Set数组去重:

const arr = [1, 2, 1, 2, 6, 3, 5, 69, 66, 7, 2, 1, 4, 3, 6, 8, 9663, 8];
 
console.log([...new Set(arr)]);  // [1, 2, 6, 3, 5, 69, 66, 7, 4, 8, 9663]

可选链 ?.

可选链操作符(  ?.  )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined?.运算符相当于一种短路机制,只要不满足条件,就不再往下执行。

当我们有一个变量message,我们想拿到它的某个节点上的值,原来的做法需要一层一层的判断,保证不报错

// ES5:
const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';

现在做法,直接使用链式调用,检查 ? 前面的内容在属性的上层对象是否存在,是否为null 或者 undefined,当不存在即返回 undefined,如果存在,继续向后调用,此时的message?.body 等同于 message.body,简化了原来的书写。

// ?.:
const firstName = message?.body?.user?.firstName

与函数调用一起使用时,如果给定的函数不存在,则返回 undefined

 onCancel={() => {
    setVisible(false);
    onClosed?.();
 }}

空值合并运算符 ??

空值合并操作符( ?? )是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

const foo = null ?? 'default string';
console.log(foo);   // "default string"

const baz = 0 ?? 42;
console.log(baz);   // 0

排除非 nullundefined 的假值:

下面这行代码,其实我们是想实现当左侧结果为 undefined / null 时,再使用右侧的默认值,可实际情况是,如果左侧给的入参是false 、0、‘’这些假值,都会返回右侧的默认值,这就有点事与愿违了

ES5:
const okText = props?.buttonProps?.loading || false;

配合可选链运算符设置默认值,就可以实现我们的需求

?? :
const okText = props?.buttonProps?.loading ?? false

逻辑运算符

新增的逻辑运算符||=&&=??= ,这三个运算符相当于先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算。

或(OR)逻辑运算符 ||=

逻辑OR赋值(x ||=y)运算符仅在 xfalsy时赋值。

const b = { a : 2 } ;

// b.a 左侧返回2,非false ,结果返回 2
b.a ||= 4 ;
console.log(b.a) ;    // 2

// b.d 左侧b的原型链不存在d,返回false,向右赋值,结果b.d赋值为8
b.d ||= 8 ;
console.log(b.d) ;  // 8

b.c = 23
console.log(b) ;    // {a: 2, d: 8, c: 23}

给属性值设置默认值:

// ES5
user.age = user.age || 18;

// ES6
user.age ||= 18;

逻辑(AND)赋值运算符 &&=

逻辑 AND 赋值 (x &&= y) 运算符仅在 x 为真时赋值。

let a = 1;
let b = 0;

// 此时运算符左侧为真,赋值后返回结果 2
a &&= 2;
console.log(a);  // 2

// 此时运算符左侧为假,不进行赋值
b &&= 2;
console.log(b);  // 0

逻辑空赋值 ??=

逻辑空赋值运算符 (x ??= y) 仅在 x 是 nullish (null 或 undefined) 时对其赋值。

我们上面是不是刚学过一个 ?? ,那现在这个 ??= 和它有什么区别呢?主要区别就是??=是赋值,??只是判断逻辑,上例子🌰:

// 首先来一个对象
const a = {b : 2, c : 0, d : false} ;

// 使用 ?? 来判断,结果并不会赋值给 a.e
a.e??99 ;
console.log(a.e) ; // undefined 

// 需要单独声明并赋值,所以 ?? 只是起判断的作用
const l = a.e ?? 99 
console.log(l)     // 99

// 接下来使用 ??= 来赋值,结果是可以添加到 a 对象上的
a.f??=100 ;
console.log(a) ;    // {b: 2, c: 0, d: false, f: 100}

// 最原始的赋值方法
a.ee=null ;
console.log(a) ;    // {b: 2, c: 0, d: false, f: 100, ee: null} ;

给参数的属性值赋值:

// 旧的写法(判断一堆,想要弄懂还得好好捋捋逻辑):
function example(params) {

    params?.loading =  params?.loading ?? false
    
    (params?.okText) ?? (params.okText ='确定')

}
// 新的写法(简洁不少,而且通俗易懂):
function example(params) {

    params?.loading ??= false
    
    params?.okText ??= '确定'

}

求幂运算符 **

求幂运算符(**)返回将第一个操作数加到第二个操作数的幂的结果。它等效于Math.pow,不同之处在于它也接受BigInts作为操作数。

Math.pow(3,4)              // 81

console.log(3 ** 4);       // 81

console.log(10 ** -2);     // 0.01

console.log(2 ** 3 ** 2);   // 512

console.log((2 ** 3) ** 2);  // 64

7. 数组方法的扩展:

关于数组的操作,可以结合之前分享的数组的方法篇来看,接下来我们就来分析下关于新规则中新增的数组的操作:

Array.of() 创建数组实例

Array.of() 方法创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。

Array.of() 和 Array区别:

区别在于处理整数参数:Array.of(7)****创建一个具有单个元素 7 的数组,而 Array(7) 创建一个长度为7的空数组(注意: 这是指一个有 7 个空位(empty)的数组,而不是由7个undefined组成的数组),链接地址在这里

includes 判断一个数组是否包含一个指定的值

includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。这个方法和我们原来一个比较熟悉的方法很相似,那就是indexOf,但是indexOf返回的是-1,而不是布尔值当true/false,这在结果判断时有些不太优雅。跳转链接

Array.from 创建新数组

Array.from()方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。此方法在原来的数组方法整理中有分享过,链接在这了Array.from


find()findIndex() 查找数组的某个复合条件的值

find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 [undefined] find && findIndex


fill() 填充数组

fill() 方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。跳转链接


copyWithin 复制数组

copyWithin() 方法浅复制数组的一部分到同一数组中的另一个位置,并返回它(会改变原数组),不会改变原数组的长度。跳转链接


flat()flatMap() 数组扁平化

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。参考原来的数组方法的flat


关于ES6中数组的空位:

数组的空位


8. 字符串的扩展方法:

关于其他的字符串方法,请看这里,下面只讲解es6中出现的最新的方法

trimStarttrimEnd 删除前后空格,不会影响原数组

trimStart() 方法从字符串的开头删除空格。trimLeft() 是此方法的别名。链接地址

更多去除空格的正则方法,请看这里


padStart 和 padEnd 填充字符串

padStart()  方法用另一个字符串填充当前字符串(如果需要的话,会重复多次),以便产生的字符串达到给定的长度。padEnd()用于尾部补全。链接地址


9. Promise.all( [ a, b ] ), 同时执行多个promise

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。这个Promiseresolve回调执行是在所有输入的promiseresolve回调都结束,或者输入的iterable里没有promise了的时候。它的reject回调执行是,只要任何一个输入的promisereject回调执行或者输入不合法的promise就会立即抛出错误,并且reject的是第一个抛出的错误信息。

const p = Promise.all([p1, p2, p3]);

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

// 此方法在集合多个 `promise` 的返回结果时很有用
Promise.all([...formList.map(item => item.validateFields()), validateFields()]).then(data => {
     ...dosomething
});

promise 和 async await 的区别?

  • async/await 是基于Promise实现的async/awaitPromise一样,是非阻塞的

  • async/await使用同步的方式来处理异步,写起来更优雅,可以让代码简洁很多,不需要像Promise一样需要些then

  • Promise 的错误可以通过catch 来捕获,async await 既可以用 then ,又可以用try catch

三、React篇

这一趴来分享一下在react框架下的那些优雅的书写方式和方法,还有关于相似方法的区别使用,以及性能提升等方面。

src=http___image.codes51.com_Article_image_20171113_20171113192033_1230.png&refer=http___image.codes51.jpeg

注:这里我默认你已经熟悉react框架,并了解它的基本使用,如不太熟悉,关于react的入门,我之前有单出过一篇文章,可以看这里

下面我就用问答的形式来分享一下,关于在react中,如何更好的提升性能优化和提高工作效率。

1. umiReact 一站式生成构建工具

这个一个可以开箱即用的一站式构建工具,包含了我们开发过程中必备的工具库,我们只需要关心业务即可,剩下的都交给它。可能大家可能会觉得,umi 有啥特别的,工具用 webpack + webpack-dev-server + babel + postcss + ... ,路由用 react-router 不就完了吗?

👆这是上一代的使用方式,工具是工具,库是库,泾渭分明。而近来,我们发现工具和库其实可以糅合在一起,工具也是框架的一部分。 通过约定、自动生成和解析代码等方式来辅助开发,减少开发者要写的代码量。next.js 如此,umi 也如此,Compilers are the New Frameworks

按照文档,我们开始生成项目:

首先保证 node 版本10.13或以上,并且已经安装了 yarn / npm / cnpm 包管理工具

// 开始下载
$ yarn create @umijs/umi-app   // 或 npx @umijs/create-umi-app

// 安装依赖
yarn install

// 启动项目
$ yarn start

@umijs/preset-react

我们可以看到我们生成的项目中在pack.json中已经安装有:@umijs/preset-react,这是生成框架自带的插件,里面包含了一些常用组件组件:

包含:

基本上覆盖大部分的场景使用,如并没有你需要的,可以去插件库里找,然后添加进来,插件库位置,如果使用其他开源插件,可以使用 npm 去安装。

除了上面的插件,umi 还支持 约定式路由运行时配置mock数据等。

如果你的项目起初不是umi创建的,也没有关系,umi还提供了 迁移 create-react-app 到 umi的修改方案,

umi-ui

自带了umi UI ,可快速构建组件内容模块,左侧选择你要插入的模块,右侧是样式的示例,通过点击即可添加到项目代码中(react x):

1636343590787.jpg

选择你需要的组件,添加到项目即可:

1636344306110.jpg

umi官网

umi文档

2. 数据状态、数据通信管理哪家强?Mobx or Dva or Redux

react中关于数据管理的江湖上,MobxDvaRedux 三家相对来说都很强,那么哪个是你的最爱呢?可以评论区告诉我哦!

Mobx

Mobx是一个功能强大,上手非常容易的状态管理工具。就连Redux的作者也曾经向大家推荐过它,在不少情况下你的确可以使用Mobx来替代掉Redux

项目中暂时未用到,正在学习中....

mobx官方文档

Dva

Dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。

开整:

首先我们需要有一个model文件,放我们所有的dva操作方法,但是在umi中还有关于model文件夹存放位置的说法,那我们来看看

符合以下规则的文件会被认为是 model 文件,

  • src/models 下的文件
  • src/pages 下,子目录中 models 目录下的文件
  • src/pages 下,所有 model.ts 文件(不区分任何字母大小写)

比如:

+ src
  + models/a.ts
  + pages
    + foo/models/b.ts
    + bar/model.ts

其中 a.tsb.ts 和 model.ts 如果其内容是有效 dva model 写法,则会被认为是 model 文件。大概意思就是你要么放在 src/models 文件夹下面,要么放在 src/pages 文件夹下面,至于叫啥名,你看着办,只要和文件里面的 namespace 相同即可,下面我就写一个简单的 model 文件来做示范:

首先先看文件目录:

1636447189083.jpg

tags.ts 文件代码如下:


export default {

  // 调用 model 的是通过命名空间调用,不要和其他的 model 同名
  namespace: 'tags',

  //状态 和 react 中的state类似,在这里定义你需的变量的初始值
  state: {
    tagListType:'array'
  },

  // 调用服务端的接口获取数据
  effects: {
    *fetchTags({payload,challback},{put,call}) {
        
        // payload:其他组件调用model 组件传来的如参数
        // callback: 回调函数
        // put:调用reducers 改变数据的方法
        // call:调用接口的方法
    
        // 获取接口数据
      // const response = yield call(getTags)


      // 调用reducers
      yield put({
        // 类似于redux 中 action 的type
        // type 对应reducer中的方法
        type: 'setTagsList',
        // payload 如果使用的是调用接口的方式,此处将response 回填到此处
        payload:[{a:2,b:3}]  // 
      })
    }
  },

  // 更新state
  reducers: {
    // 方法用做给effect时来调用,将数据处理完成后,更新数据
    setTagsList(state,action) {
      return {...state,tagList:action.payload}
    }
    // ...other methods
  }
}

接下来我们来看一下如何在页面中拿到model 中定义的数据,以及如何通过调用model中的方法来改变数据:

child页面:

import { PureComponent } from "react";
// 在这里引入connect用做将model 和 组件连接
import { connect } from 'dva'
import {Button} from 'antd'

class Child extends PureComponent {
  constructor(props) {
    super(props);
    console.log("触发前的props",props)
  }

  render() {
    return (
      <div>
     // 这里为了偷懒就写了个行内函数,大家可以将它写在外面
        <Button type='primary' onClick={() => {
          // 我们在props中可以拿到model中的dispatch 方法,
          // 此处就调用它去改变一下我们的数据,
          // type的结构是'model名/effect中调用接口的自定义方法名'
          // payload 是fetchTags中的入参,没有就给个null占位
          
          this.props.dispatch({
            type: 'tags/fetchTags',
            payload:null
          })

        }}>触发reducer</Button>
      
        {console.log('触发后的结果:',this.props.tags)}
      </div>
    );
  }
}

// 这里的tag 仍然是我们当初定义的namespace,使用connect将其连接,然后通过函数返回暴露tags到组件的props中
export default  connect((tags)=>tags)(Child)  ;

来看结果: 1636452253033.jpg

1636452286830.jpg

首先可以肯定的是我们无论是否触发了事件,我们定义在 modelstate 中的原始数据是可以通过 connect 的连接拿到的,然后就是我们通过 dispatch 的方法拿到了请求的数据。下面我我们试试在其他页面是否能够同步拿到原始数据和请求的数据。

demo 页面:

import React, { useState, useMemo,useEffect } from 'react';
import { connect } from 'dva';

const App = (props) => {


  useEffect(() => {
  
  console.log(props.tags,"demo")

  }, [props])
  
  return <div>111</div>
}
// 仍然用connect来连接,看看是否能拿到值
export default  connect((tags)=>tags)(App)  ;

来看一下结果:

1636453878313.jpg

功夫不负有心人,我们成功拿到原始数据和请求的数据,你学会了吗?

Redux

ReduxJavaScript 状态容器,提供可预测化的状态管理

项目中暂时未用到,正在学习中....

redux官方文档

3. Egg.js

Egg.js是阿里旗下的一个基于nodejskoa2的企业级应用框架,基于es6es7 和nodejs

关于node的入门指南,请看这里,关于egg的介绍请点击这里


Egg.js官网

Egg.js文档


4. React + Typescript 的结合

ts可以说是严格模式下的js,当我们写的代码越来越多,我们就会越明白规矩有多重要。ts就是在不改变原本js书写的情况下在其上添加的静态类型校验,我们再也不用担心接手的项目没文档,没注释,现在有了ts,类型系统实际上是最好的文档,直接清晰的标注:哪些是必填,哪些是可选,以及具体的数据类型都一目了然。

react 配合ts的实践,写起来真的很有必要的,既严格要求了自己,又统一了我们的书写习惯,这样无论是以后自己看代码,还是交给其他同学维护,整体都是很清晰的。

typescript 实操 及 react 配合 typescript的实践

5. Ant Design Pro 工具选对,效率翻倍

Ant Design Pro是基于 Ant Designumi 的封装的一整套企业级中后台前端/设计解决方案,致力于在设计规范和基础组件的基础上,继续向上构建,提炼出典型模板/业务组件/配套设计资源,进一步提升企业级中后台产品设计研发过程中的『用户』和『设计者』的体验。

Ant Design Pro 在力求提供开箱即用的开发体验,为此提供完整的脚手架,涉及国际化权限,mock,数据流网络请求等各个方面。为这些中后台中常见的方案提供了最佳实践来减少学习和开发成本。

同时为了提供更加高效的开发体验,提供了一系列模板组件,ProLayoutProTableProList 都是开发中后台的好帮手,可以显著的减少样板代码。

我们可以通过下面 👇 的大图来了解整个 Ant Design Pro 的架构。

yuque_diagram.jpeg

Ant Design Pro 官网

Ant Design Pro 文档

6. 提升效率的方法库ahooks

ahooks 是一个 React Hooks库,致力提供常用且高质量的 Hooks。是自认为是当前比较好用的库,里面包含了一些比较常用的函数方法,也有方法是在react函数上做的封装,使之更贴近业务的需要,之前的文章也做过ahooks的分享,地址在这里,相信我,你值得拥有,哈哈 😘

引入和使用方式也是极其简洁:

import React from "react";
import { useToggle } from "ahooks";


export default () => {
  const [ state, { toggle } ] = useToggle();


  return (
    <div>
      <p>Current Boolean: {String(state)}</p>
      <p>
        <button onClick={() => toggle()}>Toggle</button>
      </p>
    </div>
  );
};

关于 ahooks 的官网地址

7. ReactHooks 组件相比Class类组件有什么优势和劣势

Class组件,由render返回一个html组件。Class类组件不仅拥有State状态,还拥有生命周期。每个组件的实例,从创建、到运行、到销毁,在这个过程中,会发出一系列的事件,这些事件就叫做组件的生命周期函数。

下面是class类组件

import { PureComponent } from "react";

class Parent extends PureComponent {
  constructor(props) {
    super(props);
    this.state = { id: 1 };
  }

  render() {
    return (
        <div>这里是内容<div/>
    );
  }
}

export default Parent;

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。不仅在书写上很简洁,也不用再关心this指向问题。

下面是hooks函数组件

import React from 'react';
import { Button } from 'antd';

const App = () => {
  const handleClick = () => {
    console.log('按钮点击');
  };

  return <Button onClick={handleClick}>按钮</Button>;
};

export default App;

关于两者的区别:

  1. 写法上是最大的,Hooks会更加简洁,直接返回的就是一个函数体; class 组件需要构造函数的 constructor ,以及需要 render return 出一个 html 体。
  2. 其次是关于 this 指向问题,在 class 组件内,我们使用状态,添加事件都要考虑this的指向问题,在 hooks 就完全没有这些考虑了。
  3. 然后就是关于生命周期的分类,在 clas 中我们可以得到很多的生命周期,看这里 ,我们可以在不同阶段做相应的操作,在 hooks 里,把大部分的生命周期做了合并,我们几乎可以在 useEffect 中做大部分的操作。

当我们更多习惯使用hooks之后,再回来看class类组件,会发现其实class相对臃肿些,就像一个在过夏天,一个在过冬天一样,我们可以用更少的代码量,更优雅的方法来实现相同的功能,那何乐而不为呢?

关于两种写法是否要统一,这里是官方的建议

8. 根据场景来选择,使用useState 还是 useReducer

我们都知道 useStateuseReducer 两个都可以来存储状态,那么有什么区别呢?看这里

9. setState到底是同步还是异步?当 setState 返回一样的引用时,render会执行吗?

这个问题需要看当前的react采用的是什么模式加载的。

  • Legacy 模式下,结果:有时是同步,有时是异步的,在setTimeout setInterval中是同步的,其余在react 中直接使用时,它是异步的。

  • Concurrent模式下,结果:无论哪种书写方式都是异步的,也就是都会被合并处理。

我们都知道在react里不允许直接修改 state 的值,我们需要使用 setState 来改变数据,而它不是马上就会生效的,这时它是异步的。所以不要认为调用完 setState 后可以立马获取到最新的值。多个顺序执行的 setState 不是同步的一个接着一个的执行,会加入一个异步队列,然后最后一起执行,即批量处理


关于什么是批处理,下面来列举两个例子说明:

例子 1:

假设我们需要一些食材需要去逛超市采购,我们不能拿一件就去柜台结账,然后再拿一个再去结账吧,这样非常浪费时间,我们一般都是选购好加入到购物车,最后一起去结账,这样一次性去结账,少去了跑来跑去的时间,更高效。

例子 2:

之前在网上有看过一个例子,也比较形象,在这里分享给大家:从前有一位老人以打渔为生,假设老人打到一条鱼就去集市上卖,那么恐怕连来回的路费都不够,所以老人都是打到鱼先存放到船舱里,一段时间后再去一趟集市,去批量卖掉它,这样既节约了成本,又挣到了钱。 现在回到react 上来说,就是state 攒够几个一起去更新。


setState 返回一样的引用时,render 不会执行

如下示例:

import { PureComponent } from "react";
import { Button } from "antd";

class Workplace extends PureComponent {
  constructor(props) {
    super(props);
    this.state = { a: 1 };
  }

  action = {
    handleParams: () => {
     // 当`setState`执行的赋值和初始值相同:
      this.setState({ a: 1 })
      console.log('setState 执行了')
    }
  };

  render() {
    return (
      <div>
        <h1>工作台</h1>

        <Button onClick={this.action.handleParams}>执行 setState</Button>
        {console.log('render 执行了')}
      </div>
    );
  }
}
export default Workplace;

初次会执行一次render这个是必须的,然后我们点击一下按钮,执行了setState,看控制台的输出结果,并没有第二个render

1635950010412.jpg

下面我把这里改一下,当点击按钮时,将a的值改为3,其余内容同上不做更改:

 handleParams: () => {
      this.setState({ a: 3 })
      console.log('setState 执行了')
    }

现在看一下控制台的结果返回:

1635950712951.jpg

相比上一次的操作,这次的返回多了一个 render 的打印,也就是当 setState 返回一样的引用时,是不会触发 render 的渲染。猜测是因为 reactdiff 算法,判断当两个值相同,将不再继续向下执行render渲染。

关于 fiber:

好似一个潜水员,当它一头扎进水里,就要往最底层一直游,直到找到最底层的组件,然后他再上岸。在这期间,岸上发生的任何事,都不能对他进行干扰,如果有更重要的事情需要他去做(如用户操作),也必须得等他上岸

fiber的功效

使得潜水员会每隔一段时间就上岸,看是否有更重要的事情要做。

具体的执行过程

将原来的一次更新过程会分成多个分片完成,把一个耗时长的任务分成很多小片每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

关于调度任务的安排

render为分界:将react的组件更新分为两个时期:前为 phase1 ,后为 phase2

phase1 的生命周期是可以被打断的,每隔一段时间它会跳出当前渲染进程,去确定是否有其他更重要的任务,react 在工作进程树上复用当前节点来构建新的树,标记需要更新的节点放入到队列。

phase2 的生命周期是不可以被打断的,react 将其所有的变更一次性更新到dom 上。

关于diff算法:

react diff 是从 fiber 树的 Root 节点开始,从上往下一层一层的对新老节点进行比较。期间组件的 key 以及 type 决定是否需要复用老的节点。节点的 index 最终决定了 dom 是否需要被移动。没有被复用的节点会被删除,也就不需要对其子树进行 diff,从而不需要跨层级的 diff

对比不同类型的元素:

当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。举个例子,当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment>,或从 <Button> 变成 <div> 都会触发一个完整的重建流程。

对比同一类型的元素:

当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。

<div className="before" title="stuff" />

<div className="after" title="stuff" />

通过对比这两个元素,React 知道只需要修改 DOM 元素上的 className 属性。

对子节点进行递归:

默认情况下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React 会先匹配两个 <li>first</li> 对应的树,然后匹配第二个元素 <li>second</li> 对应的树,最后插入第三个元素的 <li>third</li> 树。


推荐文章:

diff算法(妈妈再也不担心我的diff面试了)

react官网diff介绍

当然如果你真的需要拿到赋值过后的值,可以借助setTimout来获取到,如下:

changeText() {
  setTimeout(() => {
    this.setState({
      message: "我是新消息"
    });
    console.log(this.state.message);  // 我是新消息
  }, 0);
}

10. useCallbackuseMemouseEffect 的区别?

共同点: 都是仅在某个依赖项改变时才会更新;

不同的是:useMemo 是缓存变量,useCallback 是缓存回调函数,useEffect无论是否有参数的监听,都会等待浏览器完成画面渲染之后才会延迟调用,并且在依赖改变时还会更新。

各个方法的监听过程?

都是使用函数的第二个参数做依赖收集,然后通过监听第二个参数的变化来判断是否重新渲染。一旦发生变化,即刻拿到最新的值,更新缓存区的函数 / 变量等内容。下面就拿useMemo做个示例:

使用useMemo之前:

import React, { useState } from 'react';

const App = () => {

    const [count, setCount] = useState(0);

    const userInfo = {
        name: "Jace",
        age: count
      };

  return <div>姓名:{userInfo.name}</div>
}

export default App

使用useMemo之后:

import React, { useState, useMemo } from 'react';

const App = () => {

    const [count, setCount] = useState(0);

    const userInfo = useMemo(() => {
      return {
        name: "Jace",
        age: count
      };
    }, [count]);

  return <div>姓名:{userInfo.name}</div>
}

export default App

很明显的上面的 userInfo 每次都将是一个新的对象,无论 count 发生改变没,都会导致组件重新渲染,而下面的则会在 count 改变后才会返回新的对象。

useEffectreturn会在什么时候执行?

当组件销毁时执行的一些内容,可以在 useEffect 里 return出 一个函数,这样 return 的函数体内的内容会在销毁时执行:

useEffect(() => {
   handelColumns(tabVal || 'intention');
   // 需要是一个函数哦!!!
   return () => {
   // 以下内容只会在销毁的时候执行
     console.log(1);
   };
}, []);

所以当我们使用时,根据其功能选择适合的即可。


11. 高阶函数

高阶组件(HOC)React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。高阶作用用于强化组件,复用逻辑,提升渲染性能等作用。 关于高阶函数,我的理解还不深刻,等我羽翼丰富了再给大家分享,这里推荐看这篇


12. 自定义hooks

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook这里是之前分享的自定义hooks的示例


13. 哪些数据改变会触发重新渲染?

组件的正常渲染是必须的,但当有不必要的渲染发生时,一方面对性能会有耗费,另一方面也可能给用户造成页面卡顿的感觉。那么哪些数据的改变会触发渲染呢?

  1. 组件的状态 state 的值被改变,即触发了 setState 方法,默认会触发渲染。当然也有特殊情况:当 setState 返回一样的引用时,并不会触发 render
  2. 父组件的 props 被修改后,所有的子组件默认也会被重新渲染;
  3. 子组件的的 state 状态改变不会影响父组件,父组件不会重新渲染;

14. 性能优化,如何避免不必要的渲染?

React 中, 我们知道 当你更新了一个组件时,会触发所有的子组件进行更新。整个组件树会依次重新渲染,这就意味着APP中的每个修改都会引起整个应用的重新渲染。即使React并没有真正地改变DOM,它也会计算VDOM并做出比较。那是非常大的工作量,你会发现整个UI都会变慢,特别是在一个大型应用中。这也正是React组件要求不变性的一个原因。我们实际的开发过程中就要按照不同场景判断是否要按需加载,下面就分别来看一下,然后大家根据自己的需求来选择即可:


React.lazy 初次不加载,当依赖被修改再加载

React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。React.lazy,适用于初次不加载,当有更新再去加载,具体的使用方法请参考这里

React.Memo 初次加载,有改变会再更新,否则不重新渲染

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React跳过渲染组件的操作并直接复用最近一次渲染的结果React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染,具体的使用方法请参考这里

shouldComponentUpdate

根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。在大部分情况下,你可以继承 React.PureComponent 以代替手写 shouldComponentUpdate()。它用当前与之前 props 和 state 的浅比较覆写了 shouldComponentUpdate() 的实现。

shouldComponentUpdate(nextProps, nextState) {
  // 执行比较判断,最后组件根据返回的布尔值来判断,组件是否需要重新render
  return true;
}

React.PureComponent

React.Memo类似,PureComponent 会对 propsstate 进行浅层比较,并减少了跳过必要更新的可能性。,但React.PureComponent适用于类组件,不适用函数组件。

我们都知道React.Component 是使用 ES6 classes 方式定义 React 组件的基类:

class Greeting extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

React.PureComponent 与 React.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 propstate 的方式来实现了该函数。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。下面就是一个PureComponent的示例:

父组件:

import React, { PureComponent } from "react";
import { Button } from "antd";
import Child from "./child";

class Analysis extends PureComponent {
  constructor(props) {
    super(props);
    this.state = { b: 1 };
  }
  action = {
    handleParams: () => {
      this.setState({b:2})

    }
  };

  render() {
    return (
      <div>
        <h1>分析页</h1>

        <Button onClick={this.action.handleParams}>handleParams</Button>
        {console.log('父组件渲染')}
        
        <div>子组件:
          <Child />
        </div>
      </div>
    );
  }
}
export default Analysis;

子组件:

import { PureComponent } from "react";

class Child extends PureComponent {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div>
        <h1>child-page</h1>
        {console.log('子组件重新渲染')}
      </div>
    );
  }
}

export default Child;

image.png

当我们更新父组件的props时,此时控制台并未输出子组件的渲染,因为当前子组件并未使用到父组件的props,所以说减少了不必要的渲染。

15. 关于React 17 版本的修改

关于这个版本没有新特性的新增,而是在运行和渲染方面做了一些优化,改进,并且修复了一些bug 官方文档在这里

优化:

  • 异步运行 useEffect 清理函数, 之前是同步执行,等待渲染后再执行清理

  • 运行下一个副作用之前,清理所有副作用

  • 禁止在onScroll 事件时冒泡

  • 服务端渲染 useCallbackuseMemo 一致

16. Reacthooks的实现原理

hooksreact 16.8 的新特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 react 特性

hooks 主要是利用闭包来保存状态state 当做一个单链表的形式,用一个useState 就去表里取一下,使用链表保存一系列hooks,将链表中的第一个hooksfiber关联,在fiber 树更新时,就能从hooks 中计算出最终输出的状态和执行相关的副作用。


文章写到这里还没有结束,我希望自己能继续更新,如果正在看文章的你有任何疑惑点🤔,欢迎评论区留言。

1636002554133.jpg

1636002503362.jpg


如果亲感觉我的文章还不错的话,可以一下添加关注哦!

内容还在持续更新中,怕错过更新的点个关注再走呗!点个关注不迷路哦!😄