记录一下
一.判断对象为空的方法
1.利用JSON.stringify()方法强制转换为字符串
let a = {};
JSON.stringify(a) == '{}'//true
2.利用Object.getOwnPropertyNames方法来判断
let a = {};
Object.getOwnPropertyNames(a).length == 0
3.利用ES6的Object.keys()来判断
let a = {};
let arr = Object.keys(a);
arr.length == 0; //true
二.当执行正则表达式时
const regx = new RegExp(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/g);
regx.test(4.4.3.2); // true
regx.test(4.4.3.2);//false
当全局执行完第一次匹配之后,会停留在当前匹配的位置,所以第二次重复执行的时候就返回false,如果非全局执行则不会出现这种情况。
也可以设置
regx.lastIndex = 0;
regx.test('4.4.3.2') //true
三.css3的transform能够避免浏览器的重排和回流?
ref
1.保存不变的实例或者属性
useRef在函数组件中使用,当拿不到最新的值时,可以将最新的值挂在 ref 上来保证这些 hook 在回调中拿到的都是最新的值,同时避免不必要的重新渲染。因为useRef返回的Ref对象在组件的整个声明周期内保持不变
比如在防抖函数中,将传入的函数挂载在ref上
const UseDebounce = (fn: Function, delay: any, dep: []) => {
const { current } = useRef({ fn, timer: null });
useEffect(() => {
current.fn = fn;
}, [fn]);
return useCallback((...args) => {
if (current.timer) {
// @ts-ignore
clearTimeout(current.timer);
}
// @ts-ignore
current.timer = setTimeout(() => {
current.fn(...args);
}, delay);
}, dep);
};
2.传递ref
Ref 转发是一项将 ref 自动地通过组件传递到子组件的技巧,其允许某些组件接收ref,并将其向下传递给子组件,函数组件中使用forwardRef是为了在函数组件中传递ref,而class组件中自带ref进行传递,而函数组件中不自带,在使用forwardRef后,可以多接受一个ref参数,正常使用情况:
const AntdSearch = forwardRef((props,ref) => {
React.useImperativeHandle(ref, () => ({
// name:'hhhhh',
//获取选中项
//获取数据
reset: () => {
handleRedo()
// return [...prop.dataSource];
},
//重新加载
}));
})
此时,父组件可以拿到挂载到子组件Dom上的节点,而想获取子组件的属性和方法,则需要配合useImperativeHandle 来获取。在使用forwardRef暴露子组件实例的时候,如遇到该组件已经被高阶组件包裹的时候,此时拿到的是最外层的容器组件即高阶组件的实例,则需要修改为:
const Search = (props) => {
let {refInstance} = props;
React.useImperativeHandle(refInstance, () => ({
// name:'hhhhh',
//获取选中项
//获取数据
reset: () => {
handleRedo()
// return [...prop.dataSource];
},
// getVal:() => {
// // console.log(form.getFieldsValue())
// return form.getFieldsValue();
// },
getFormInstance:() => {
return form
}
//重新加载
}));
}
const AntdSearch = functionHoc(Search)
//@ts-ignore
export default forwardRef((props,ref) => <AntdSearch locale={props?.locale} refInstance={ref} {...props} />);
不能对已经卸载的组件执行状态更新
问题描述:
//语言设置
const loadLocales = () => {
// console.log('loadLocales',locale)
let currentLocale = intl.determineLocale({
localStorageLocaleKey:"locale"
})
intl.init({
currentLocale,
locales
}).then(()=> {
if(!mountedRef.current){
return;
}
setInitDone(true);
// console.log('currentLocale')
})
}
useEffect(() => {
// console.log('loadlocale')
mountedRef.current = true;
loadLocales();
return () => {
mountedRef.current = false;
}
},[]);
let isMountedRef = useRef(false)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
async function handleSubmit() {
setPending(true)
await post('/someapi')
if (!isMountedRef.current) {
setPending(false)
}
}
对象序列化
出现的问题:当JSON.stringify遇到对象值为undefined时,会忽略其值并跳过该值的序列化。
- 序列化与反序列化:在OSI七层协议模型中展现层(Presentation Layer)的主要功能是把应用层的对象转换成一段连续的二进制串,或者反过来,把二进制串转换成应用层的对象 -- 这两个功能就是序列化和反序列化。
- JSON.stringify(value[, replacer [, space]]):用来将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串,如果指定了 replacer 是一个函数,则可以选择性地替换值,或者如果指定了 replacer 是一个数组,则可选择性地仅包含数组指定的属性。
- 当undefined、任意的函数、正则以及 symbol 作为对象属性值时 JSON.stringify() 对跳过(忽略)它们进行序列化
const data = {
a: "aaa",
b: undefined,
c: Symbol("dd"),
fn: function() {
return true;
}
};
JSON.stringify(data); // 输出:?
// "{"a":"aaa"}"
4.当undefined、任意的函数、正则以及 symbol 作为数组元素值时,JSON.stringify() 将会将它们序列化为 null
JSON.stringify(["aaa", undefined, function aa() {
return true
}, Symbol('dd')]) // 输出:?
// "["aaa",null,null,null]"
5.当undefined、任意的函数、正则以及 symbol 被 JSON.stringify() 作为单独的值进行序列化时,都会返回 undefined
JSON.stringify(function a (){console.log('a')})
// undefined
JSON.stringify(undefined)
// undefined
JSON.stringify(Symbol('dd'))
// undefined
6.转换值如果有 toJSON() 函数,该函数返回什么值,序列化结果就是什么值,并且忽略其他属性的值
JSON.stringify({
say: "hello JSON.stringify",
toJSON: function() {
return "today i learn";
}
})
// "today i learn"
7.NaN和Infinity格式的数值及null都会被当做null。
JSON.stringify(NaN)
// "null"
JSON.stringify(null)
// "null"
JSON.stringify(Infinity)
// "null"
8.基本数据类型布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
JSON.stringify([new Number(1), new String("false"), new Boolean(false)]);
// "[1,"false",false]"
9.可以实现深拷贝:JSON.parse(JSON.stringify()),
造成的问题:
obj_after =JSON.parse(JSON.stringify(obj))会导致 obj_after.proto.constructor 指向发生变化,统一指向 Object,而不是和obj的一致(不够优雅),
如果对象中有undefined,function,symbol和正则的时候,不能完全深拷贝,
当对象中包含循环引用的时候,会直接抛出错误
// 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
const obj = {
name: "loopObj"
};
const loopObj = {
obj
};
// 对象之间形成循环引用,形成闭环
obj.loopObj = loopObj;
// 封装一个深拷贝的函数
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
// 执行深拷贝,抛出错误
deepClone(obj)
/**
VM44:9 Uncaught TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'loopObj' -> object with constructor 'Object'
--- property 'obj' closes the circle
at JSON.stringify (<anonymous>)
at deepClone (<anonymous>:9:26)
at <anonymous>:11:13
*/
for循环的不当使用
问题:通过这个方法,只能拿到数组的第一项的值,然后一直报错,
useEffect(() => {
getDataList({});
let val = 'id=43&type=monitor&code=824b74e0eb9699f3fbae4d29b135808a&state=1'.split('&');
console.log(text(val));
},[]);
const text = (val:any) => {
for (let i = 0, len = val.length; i < len; i++) {
let pair = val[i].split('=');
if (pair[0] === 'code') {
return pair[1];
}
return null;
}
}
1.for方法和for of方法可以使用break或者continue跳过或中断,for ...of直接访问的是实际元素,for遍历数组索引
const list = [1, 2, 3, 4, 5, 6, 7, 8,, 10, 11];
for (let i = 0, len = list.length; i < len; i++) {
if (list[i] === 5) {
break; // 1 2 3 4
// continue; // 1 2 3 4 6 7 8 undefined 10 11
}
console.log(list[i]);
}
for (const item of list) {
if (item === 5) {
break; // 1 2 3 4
// continue; // 1 2 3 4 6 7 8 undefined 10 11
}
console.log(item);
}
2.forEach方法用于调用数组的每个元素,并将元素传递给回调函数,数组中的每个值都会调用回调函数,并且无法跳出循环且没有返回值。
array.forEach(function(currentVal,index,arr),thisVal);
currentVal:必需,当前元素
index:可选,当前元素索引值
arr:可选,当前元素所属的数组对象
let arr = [1,2,3,4,5];
arr.forEach((item,index,arr)=>{
console.log(index+':'+item)
})
该方法还可以有第二个参数,用来绑定回调函数内部this变量(前提是回调函数不能是箭头函数,因为箭头函数没有this)
let arr = [1,2,3,4,5];
let arr1 = [2,4,6,8,10];
arr.forEach(function(item,index,arr){
console.log(this[index]);// 2,4,6,8,10
},arr1)
3.map方法生成一个新数组,都不会改变原数组,跳过空元素,将回调函数的返回值组成一个新数组,数组长度与原数组一致,无法跳出循环,有返回值
const newList = list.map(item => {
console.log(item);
return item.id;
});
// newList: [1, empty, 2, 3]
list: [
{ name: '头部导航', type: 'nav', id: 1 },
empty,
{ name: '轮播', type: 'content', id: 2 },
{ name: '页脚', type: 'nav', id: 3 },
]
list.forEach((item: any) => {
return (
<div
className="content-item"
key={item.id}
draggable="true"
onDragStart={() => handleDragStart(item)}
>
<span style={{ marginRight: 10 }}>{item.label}</span>
</div>
);
})}
页面无任何显示,因为forEach没有返回值
4.some和every,二者都是用来做数组条件判断的,都是返回一个布尔值,some若某一元素满足条件,返回 true,every 与 some 相反,若所有元素不满足条件,返回 false
const list = [
{ name: '头部导航', backward: false },
{ name: '轮播', backward: true },
{ name: '页脚', backward: false },
];
const someBackward = list.some(item => item.backward);
// someBackward: true
const everyNewest = list.every(item => !item.backward);
// everyNewest: false
5.性能比较
var list = Array(100000).fill(1)
console.time('for');
for (let index = 0, len = list.length; index < len; index++) {
}
console.timeEnd('for');
// for: 2.427642822265625 ms
console.time('every');
list.every(() => { return true })
console.timeEnd('every')
// some: 2.751708984375 ms
console.time('some');
list.some(() => { return false })
console.timeEnd('some')
// some: 2.786590576171875 ms
console.time('foreach');
list.forEach(() => {})
console.timeEnd('foreach');
// foreach: 3.126708984375 ms
console.time('map');
list.map(() => {})
console.timeEnd('map');
// map: 3.743743896484375 ms
console.time('forof');
for (let index of list) {
}
console.timeEnd('forof')
// forof: 6.33380126953125 ms
| 是否可终止 | 是否跳出本次循环 | |||
|---|---|---|---|---|
| *** | break | continue | return | 性能(ms) |
| for | 可以 | 可以 | 可以 跳出循环 | 2.42 |
| forEach | 不可以 | 不可以 | 不可以 | 3.12 |
| map | 不可以 | 不可以 | 可以 不跳出循环 | 3.74 |
| for of | 可以 | 可以 | 可以 跳出循环 | 6.33 |
| some | 不可以 | 不可以 | 可以,返回Boolean | 2.78 |
| every | 不可以 | 不可以 | 可以,返回Boolean | 2.75 |
高阶组件的使用
在table组件的封装中,使用高阶组件来控制组件的渲染,因此也打开了了解高阶组件的神秘大门
import React, { useState, useEffect, useRef } from 'react';
//根据locale语言切换控制组件的刷新
function functionHoc (InnerComp) {
// console.log(InnerComp);
return function Index({...props}){
// console.log(props,props.locale)
const [forceUpdate,setForceUpdate] = useState(true);
const mountRef = useRef(null);
let timer = null;
useEffect(() => {
// console.log(locale)
mountRef.current = true
if(props.locale){
// console.log('return')
Promise.resolve(() => {
// console.log('promise');
return 'success'
}).then(() => {
// console.log('then')
if(!mountRef.current){
return;
}
setForceUpdate(false);
})
clearTimeout(timer);
// @ts-ignore
timer = setTimeout(() => {
setForceUpdate(true);
},1);
}
return () => {
mountRef.current = false
timer = null;
clearTimeout(timer);
}
},[props.locale])
return forceUpdate?<InnerComp {...props}/>:null
}
}
export default functionHoc;
高阶组件就是将组件转换成另一个组件,而经过包装过后的组件获得了强化,节省了逻辑,解决了原有组件的缺陷,这是高阶组件及其意义。高阶组件的作用是强化组件、复用逻辑、提升渲染性能等作用,输入一个组件,输出一个组件。
1.两种不同的使用高阶组件的方式:装饰器模式和函数式模式
@withRouter
@keepaliveLifeCycle
class Index extends React.Componen{
/* ... */
}
function Index(){
/* .... */
}
export default withRouter( keepaliveLifeCycle(Index) ))
2.两种不同的高阶组件
1)正向代理
就是用组件包裹一层代理组件,在代理组件上,我们可以做一些,对源组件的代理操作。在fiber tree上,先mounted代理组件,然后才是我们的业务组件。我们可以理解为父子组件关系,父组件对子组件进行一系列强化操作。
function HOC(WrapComponent){
return class Advance extends React.Component{
state={
name:'alien'
}
render(){
return <WrapComponent { ...this.props } { ...this.state } />
}
}
}
优点:
① 正常属性代理可以和业务组件低耦合,零耦合,对于条件渲染和props属性增强,只负责控制子组件渲染和传递额外的props就可以,所以无须知道,业务组件做了些什么。所以正向属性代理,更适合做一些开源项目的hoc,目前开源的HOC基本都是通过这个模式实现的。
② 同样适用于class声明组件,和function声明的组件。
③ 可以完全隔离业务组件的渲染,相比反向继承,属性代理这种模式。可以完全控制业务组件渲染与否,可以避免反向继承带来一些副作用,比如生命周期的执行。
④ 可以嵌套使用,多个hoc是可以嵌套使用的,而且一般不会限制包装HOC的先后顺序。
缺点:
① 一般无法直接获取业务组件的状态,如果想要获取,需要ref获取组件实例。
② 无法直接继承静态属性。如果需要继承需要手动处理,或者引入第三方库。
2) 反向继承
反向继承和属性代理有一定的区别,在于包装后的组件继承了业务组件本身,所以我们无须在去实例化我们的业务组件。当前高阶组件就是继承后,加强型的业务组件。这种方式类似于组件的强化,所以你没必要知道当前的业务组件
class Index extends React.Component{
render(){
return <div> hello,world </div>
}
}
function HOC(Component){
return class wrapComponent extends Component{ /* 直接继承需要包装的组件 */
}
}
export default HOC(Index)
优点:
① 方便获取组件内部状态,比如state,props ,生命周期,绑定的事件函数等
② es6继承可以良好继承静态属性。我们无须对静态属性和方法进行额外的处理
缺点:
① 无状态组件无法使用。
② 和被包装的组件强耦合,需要知道被包装的组件的内部状态,具体是做什么?
③ 如果多个反向继承hoc嵌套在一起,当前状态会覆盖上一个状态。这样带来的隐患是非常大的,比如说有多个componentDidMount,当前componentDidMount会覆盖上一个componentDidMount。这样副作用串联起来,影响很大。
3.高阶组件的作用:
1).逻辑复用:高阶组件更像是一个加工react组件的工厂,批量对原有组件进行加工,包装处理。我们可以根据业务需求定制化专属的HOC,这样可以解决复用逻辑。
2).强化props:这个是HOC最常用的用法之一,高阶组件返回的组件,可以劫持上一层传过来的props,然后混入新的props,来增强组件的功能。代表作react-router中的withRouter
function classHOC(WrapComponent){
return class Idex extends React.Component{
constructor(){
super()
this.state={
name:'alien'
}
}
changeName(name){
this.setState({ name })
}
render(){
return <WrapComponent
{ ...this.props }
{ ...this.state }
changeName={this.changeName.bind(this)}
/>
}
}
}
function Index(props){
const [ value ,setValue ] = useState(null)
const { name ,changeName } = props
return <div>
<div> hello,world , my name is { name }</div>
改变name <input onChange={ (e)=> setValue(e.target.value) } />
<button onClick={ ()=> changeName(value) } >确定</button>
</div>
}
export default classHOC(Index)
import React from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";
import invariant from "tiny-invariant";
import RouterContext from "./RouterContext.js";
/**
* A public higher-order component to access the imperative API
*/
function withRouter(Component) {
const displayName = `withRouter(${Component.displayName || Component.name})`;
const C = props => {
// 如果想要设置被 withRouter 包裹的组件的 ref,使用 wrappedComponentRef
const { wrappedComponentRef, ...remainingProps } = props;
return (
<RouterContext.Consumer>
{context => {
invariant(
context,
`You should not use <${displayName} /> outside a <Router>`
);
// 将 context 加入到 Component 中
return (
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
};
C.displayName = displayName;
C.WrappedComponent = Component;
// 当你给一个组件添加一个HOC时,原来的组件会被一个container的组件包裹。这意味着新的组件不会有原来组件任何静态方法。
// 为了解决这个问题,可以在return container之前将 static方法copy到container上面
// 用 hoist-non-react-statics来自动复制所有non-React的static methods
return hoistStatics(C, Component);
}
export default withRouter;
3).赋能组件:HOC有一项独特的特性,就是可以给被HOC包裹的业务组件,提供一些拓展功能,比如说额外的生命周期,额外的事件,但是这种HOC,可能需要和业务组件紧密结合。典型案例react-keepalive-router中的keepaliveLifeCycle就是通过HOC方式,给业务组件增加了额外的生命周期。
import {lifeCycles} from '../core/keeper'
import hoistNonReactStatic from 'hoist-non-react-statics'
function keepaliveLifeCycle(Component) {
class Hoc extends React.Component {
cur = null
handerLifeCycle = type => {
if (!this.cur) return
const lifeCycleFunc = this.cur[type]
isFuntion(lifeCycleFunc) && lifeCycleFunc.call(this.cur)
}
componentDidMount() {
const {cacheId} = this.props
cacheId && (lifeCycles[cacheId] = this.handerLifeCycle)
}
componentWillUnmount() {
const {cacheId} = this.props
delete lifeCycles[cacheId]
}
render=() => <Component {...this.props} ref={cur => (this.cur = cur)}/>
}
return hoistNonReactStatic(Hoc,Component)
}
keepaliveLifeCycle 的原理就是通过ref或获取 class 组件的实例,在hoc初始化时候进行生命周期的绑定, 在 hoc 销毁阶段,对生命周期进行解绑, 然后交给keeper统一调度,keeper通过调用实例下面的生命周期函数,来实现缓存生命周期功能的。
4).控制渲染:劫持渲染是HOC一个特性,在wrapComponent包装组件中,可以对原来的组件,进行条件渲染,节流渲染,懒加载等功能,典型代表做react-redux中connect和 dva中 dynamic 组件懒加载。
function renderHOC(WrapComponent){
return class Index extends React.Component{
constructor(props){
super(props)
this.state={ visible:true }
}
setVisible(){
this.setState({ visible:!this.state.visible })
}
render(){
const { visible } = this.state
return <div className="box" >
<button onClick={ this.setVisible.bind(this) }>挂载组件</button>
{visible?<WrapComponent{ ...this.props }
setVisible={ this.setVisible.bind(this) }
/>
:
<div className="icon" ><SyncOutlined spin className="theicon" /></div> }
</div>
}
}
}
class Index extends React.Component{
render(){
const { setVisible } = this.props
return <div className="box" >
<p>hello,my name is alien</p>
<img src='https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=294206908,2427609994&fm=26&gp=0.jpg'
/>
<button onClick={() => setVisible()} > 卸载当前组件 </button>
</div>
}
}
export default renderHOC(Index)
import store from './redux/store'
import { ReactReduxContext } from './Context'
import { useContext } from 'react'
function connect(mapStateToProps){
/* 第一层: 接收订阅state函数 */
return function wrapWithConnect (WrappedComponent){
/* 第二层:接收原始组件 */
function ConnectFunction(props){
const [ , forceUpdate ] = useState(0)
const { reactReduxForwardedRef ,...wrapperProps } = props
/* 取出Context */
const { store } = useContext(ReactReduxContext)
/* 强化props:合并 store state 和 props */
const trueComponentProps = useMemo(()=>{
/* 只有props或者订阅的state变化,才返回合并后的props */
return selectorFactory(mapStateToProps(store.getState()),wrapperProps)
},[ store , wrapperProps ])
/* 只有 trueComponentProps 改变时候,更新组件。 */
const renderedWrappedComponent = useMemo(
() => (
<WrappedComponent
{...trueComponentProps}
ref={reactReduxForwardedRef}
/>
),
[reactReduxForwardedRef, WrappedComponent, trueComponentProps]
)
useEffect(()=>{
/* 订阅更新 */
const checkUpdate = () => forceUpdate(new Date().getTime())
store.subscribe( checkUpdate )
},[ store ])
return renderedWrappedComponent
}
/* React.memo 包裹 */
const Connect = React.memo(ConnectFunction)
/* 处理hoc,获取ref问题 */
if(forwardRef){
const forwarded = React.forwardRef(function forwardConnectRef( props,ref) {
return <Connect {...props} reactReduxForwardedRef={ref} />
})
return hoistStatics(forwarded, WrappedComponent)
}
/* 继承静态属性 */
return hoistStatics(Connect,WrappedComponent)
}
}
export default Index
connect 涉及到的功能点还真不少,首先第一层接受订阅函数,第二层接收原始组件,然后用forwardRef处理ref,用hoistStatics 处理静态属性的继承,在包装组件内部,合并props,useMemo缓存原始组件,只有合并后的props发生变化,才更新组件,然后在useEffect内部通过store.subscribe()订阅更新。这里省略了Subscription概念,真正的connect中有一个Subscription专门负责订阅消息
异步组件:dva里面的dynamic就是应用的HOC模式实现的组件异步加载
export default function AsyncRouter(loadRouter) {
return class Content extends React.Component {
state = {Component: null}
componentDidMount() {
if (this.state.Component) return
loadRouter()
.then(module => module.default)
.then(Component => this.setState({Component},
))
}
render() {
const {Component} = this.state
return Component ? <Component {
...this.props
}
/> : null
}
}
}
const Index = AsyncRouter(()=>import('../pages/index'))
require与import
require是在脚本执行中调用,在代码的任何地方,是执行赋值是模块内部变量值的拷贝,从exports属性上去取导出的值,import是在预编译阶段执行的,当遇到import,会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用到被加载模块中取值,当原始值变化时,import的值也会跟着变化,不会缓存值
- 导入require 导出 exports/module.exports 是 CommonJS 的标准,通常适用范围如 Node.js
- import/export 是 ES6 的标准,通常适用范围如 React
- require 是赋值过程并且是运行时才执行,也就是同步加载
- require 可以理解为一个全局方法,因为它是一个方法所以意味着可以在任何地方执行。
- import 是解构过程并且是编译时执行, 它会生成外部模块的引用而不是加载模块, 等到真正使用到该模块的时候才会去加载模块中的值,理解为异步加载
- import 会提升到整个模块的头部,具有置顶性,但是建议写在文件的顶部
- import表达式可以模拟require的导入,并返回一个promise;
webpack的Babel会将es6语法转换成es5语法,会将es6的import和export default转换成commonjs规范
export default 123;
export const a = 123;
const b = 3;
const c = 4;
export { b, c };
import a from './a.js';
exports.default = 123;
exports.a = 123;
exports.b = 3;
exports.c = 4;
exports.__esModule = true;
var a = require(./a.js)
我们在使用各大 UI 组件库时都会被介绍到为了避免引入全部文件,请使用 babel-plugin-component 等babel 插件。
import { Button, Select } from 'antd'
var a = require('antd');
var Button = a.Button;
var Select = a.Select;
import Button from 'antd/lib/button'
import Select from 'antd/lib/select'
tree-shaking
减少无用的代码,减少包的体积,比如检查变量的引用,如果没有该引用,会直接剔除,有些深一点的无法判断,需要使用插件的方式来剔除,比如:webpack-deep-scope-analysis-plugin
useMemo,useCallback,useEffect依赖尽量使用基本类型
lazy loading懒加载和bundle/code splitting代码分割
包体积越小,app越快,使用source-map-explore或者@next/bundle-analyzes进行包体积分析
推荐使用react-hook-forms表单库