不同团队编写进去的 React 代码也不尽相同,程度有个有高下,就像十个读者就有十种哈姆雷特,然而以下八点可能是你编写 React 代码的基本准则。
这篇 性能优化小册 – React 搜寻优化:防抖、缓存、LRU 文章提到,最近要做 React 我的项目的一些重构和优化等相干工作,过了这么久来总结一下(借鉴网上的一些文章和本人的实际,提取一些 React 代码优化上的共性)。
一、可选的 props 和空对象 {}
遵循繁多目标组件哲学,防止过于简单的多行组件,并尽可能地将组件合成。
假如咱们有一个简略的 <userCard/>
组件,此组件的惟一用处是接管一个 user
对象并显示相应的用户数据。
代码如下:
import React from 'react';
import propsTypes from 'props-types';
const UserCard = ({user}) => {
return (
<ul>
<li>{user.name}</li>
<li>{user.age}</li>
<li>{user.email}</li>
</ul>
)
}
UserCard.propsTypes = {user: propsTypes.object}
UserCard.defaultTypes = {user: {}
}
这个组件须要一个 user props
并且它是组件的惟一数据源,然而这个 props
不是必须的(没有设置 isRequired
),所以设置了一个默认值为 {}
,以防止 Can not access property‘name’of… errors 等谬误。
那么,如果没有为 <UserCard>
组件在期待渲染时提供一个回退值(fallback
),并且 UserCard
没有任何数据也没有任何逻辑运行,就没有理由出现这个组件。
那么,props
什么时候是必须的,什么时候不是必须的呢?
有时候你只须要问问本人,怎么实现才是最正当的。
假如咱们有一个转换货币的组件 <CurrencyConverter/>
,它有三个 props
:
-
value
– 咱们要转换的数值。 -
givenCurrency
– 咱们正在转换的货币。 -
targetCurrency
– 咱们要转换成的货币。
然而,如果咱们的 value
值有余,那么进行任何转换都毫无意义,那时基本不须要出现组件。
因而,value props
必定是必须的。
二、条件在父组件中出现
咱们有时候在一个子组件中常常会看到相似如下代码:
import React, {useState} from "react";
import PropTypes from "prop-types";
// 子组件
export const UserCard = ({user}) => {const keys = Object.keys(user)
return (
keys.length ?
<ul>
<li>{user.name}</li>
<li>{user.age}</li>
<li>{user.email}</li>
</ul>
: "No Data"
);
};
咱们看到一个组件带有一些逻辑,却徒劳地执行,只是为了显示一个 spinner
或一条信息。
针对这种状况,请记住,在父组件外部实现此操作总是比在组件自身外部实现更为洁净。
按着这个准则,子组件和父组件应该像这样:
import React, {useState} from "react";
import PropTypes from "prop-types";
// 子组件
export const UserCard = ({user}) => {
return (
<ul>
<li>{user.name}</li>
<li>{user.age}</li>
<li>{user.email}</li>
</ul>
);
};
UserCard.propTypes = {user: PropTypes.object.isRequired};
// 父组件
export const UserContainer = () => {const [user, setUser] = useState(null);
// do some apiCall here
return (
<div>
{user && <UserCard user={user} />}
</div>
);
};
通过这种形式,咱们将 user
初始化为 null
,而后简略的运行一个 falsy
检测,如果 user
不存在,!user
将返回 true
。
如果设置为 {}
则不然,咱们必须通过 Object.keys()
查看对象 key
的长度,通过不创立新的对象援用,咱们节俭了一些内存空间,并且只有在取得了所需的数据之后,咱们才渲染子组件 <UserCard/>
。
如果没有数据,显示一个 spinner
也会很容易做到。
export const UserContainer = () => {const [user, setUser] = useState(null);
// do some apiCall here
return (
<div>
{user ? <UserCard user={user} /> : 'No data available'}
</div>
);
};
子组件 <UserCard/>
只负责显示用户数据,父组件 <UserContainer/>
是用来获取数据并决定出现什么的。这就是为什么父组件是显示回退值(fallback
)最佳地位的起因。
三、不满足时,及时 return
即便咱们应用的是失常的编程语言,嵌套也是一团糟,更不用说 JSX(它是 JavaScript、HTML 的混合体)了。
你可能常常会看到应用 JSX 编写的相似的代码:
const NestedComponent = () => {
// ...
return (
<>
{!isLoading ? (
<>
<h2>Some heading</h2>
<p>Some description</p>
</>
) : <Spinner />}
</>
)
}
该怎么做才是更正当的呢?
const NestedComponent = () => {
// ...
if (isLoading) return <Spinner />
return (
<>
<h2>Some heading</h2>
<p>Some description</p>
</>
)
}
咱们解决 render
逻辑时,在解决是否有可用的数据,页面是否正在加载,咱们都能够抉择提前 return
。
这样咱们就能够防止嵌套,不会把 HTML 和 JavaScript 混合在一起,而且代码对于不同技术水平或没有技术背景的人来说也是可读的。
四、在 JSX 中尽量少写 JavaScript
JSX 是一种混合语言,能够写 JS 代码,能够写表达式,能够写 HTML,当三者混合起来后,应用 JSX 编写的代码可读性就会差很多。
尽管有教训的人能够了解组件外部产生了什么,但并不是每个人都能了解。
const CustomInput = ({onChange}) => {
return (
<Input onChange={e => {const newValue = getParsedValue(e.target.value);
onChange(newValue);
}} />
)
}
咱们正在解决一些内部对 input
的一些输出,应用自定义处理程序解析该输出 e.target.value
,而后将解析后的值传给 <CustomInput/>
组件接管的 onChange prop
。尽管这个示例能够失常工作,然而会让代码变得很难了解。
在理论我的项目中会有更多的元素和更简单的 JS 逻辑,所以咱们将逻辑从组件中抽离进去,会使 return()
更清晰。
const CustomInput = ({onChange}) => {const handleChange = (e) => {const newValue = getParsedValue(e.target.value);
onChange(newValue);
};
return (<Input onChange={handleChange} />
)
}
当在 render
中返回 JSX 时,不要应用内联的 JavaScript 逻辑。
五、userCallback & userEffect
随着 v16.8 Hooks
问世后,人们开始大量应用函数组件,当应用函数进行编写组件时,如果须要在外部执行 API 接口的调用,须要用到 useEffect
生命周期钩子。
useEffect 用于解决组件中的 effect,通常用于申请数据,事件处理,订阅等相干操作。
在最后版本的文档指出,避免 useEffect
呈现有限循环,须要提供空数组 []
作为 useEffect
依赖项,将使钩子只能在组件的挂载和卸载阶段运行。因而,咱们会看到在很多应用 useEffect
的中央将 []
作为依赖项传入。
应用 useEffect 呈现有限循环的起因是,useEffect 在组件
mount
时执行,但也会在组件更新时执行。因为咱们在每次申请数据之后基本上都会设置本地的状态,所以组件会更新,因而useEffect
会再次执行,因而呈现了有限循环的状况。
然而,这种解决形式就会呈现 react-hooks/exhaustive-deps
规定的正告,因而代码中经常会通过正文疏忽此正告。
// eslint-disable-next-line react-hooks/exhaustive-deps
import React, {useState, useEffect} from 'react'
import {fetchUserAction} from '../api/actions.js'
const UserContainer = () => {const [user, setUser] = useState(null);
const handleUserFetch = async () => {const result = await fetchUserAction();
setUser(result);
};
useEffect(() => {handleUserFetch();
// 疏忽正告
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!user) return <p>No data available.</p>
return <UserCard data={user} />
};
最后,很多人认为这个正告毫无意义,从而抉择进行疏忽,而不去试图摸索它是如何产生的。
其实,有些人没有意识到,handleUserFetch()
办法在组件每次渲染的时候都会从新创立(组件有多少次更新就会创立多少次)。
对于 react-hooks/exhaustive-deps
具体的探讨,能够看下这个 issue。
useCallback 的作用在于利用 memoize 缩小有效的 re-render,来达到性能优化的作用。
这就是为什么咱们须要在 useEffect
中调用的办法上应用 useCallback
的起因。通过这种形式,咱们能够避免 handleUserFetch()
办法从新创立(除非其依赖项发生变化 ),因而这个办法能够用作 useEffect
钩子的依赖项,而不会导致有限循环。
上边的例子应该这样重写:
import React, {useState, useEffect, useCalllback} from 'react'
import {fetchUserAction} from '../api/actions.js'
const UserContainer = () => {const [user, setUser] = useState(null);
// 应用 useCallback 包裹
const handleUserFetch = useCalllback(async () => {const result = await fetchUserAction();
setUser(result);
}, []);
useEffect(() => {handleUserFetch();
}, [handleUserFetch]); /* 将 handleUserFetch 作为依赖项传入 */
if (!user) return <p>No data available.</p>
return <UserCard data={user} />
};
咱们将 handleUserFetch
作为 useEffect
的依赖项,并将它包裹在 useCallback
中。如果此办法应用内部参数,例如 userId
(在理论开发中,可能心愿获取特定的用户),则此参数能够作为 useCallback
的依赖项传入。只有 userId
发生变化时,依赖它的 handleUserFetch
才重写扭转。
六、抽离独立逻辑
假如咱们在组件中有一个办法,它能够解决组件的一些变量,并为咱们返回一个输入。
例如:
const UserCard = ({user}) => {const getUserRole = () => {const { roles} = user;
if (roles.includes('admin')) return 'Admin';
if (roles.includes('maintainer')) return 'Maintainer';
return 'Developer';
}
return (
<ul>
<li>{user.name}</li>
<li>{user.age}</li>
<li>{user.email}</li>
<li>{getUserRole()}</li>
</ul>
);
}
这个办法和前一个例子中的办法一样,在组件每次渲染时都会从新创立,(然而没必要应用 useCallback
进行包裹,因为它没有被作为一个依赖项传入)。
组件外部定义的许多逻辑能够从组件中抽离,因为它的实现并不真正与组件相干。
改良后的代码:
const getUserRole = (roles) => {if (roles.includes('admin')) return 'Admin';
if (roles.includes('maintainer')) return 'Maintainer';
return 'Developer';
}
const UserCard = ({user}) => {
return (
<ul>
<li>{user.name}</li>
<li>{user.age}</li>
<li>{user.email}</li>
<li>{getUserRole(user.roles)}</li>
</ul>
);
}
通过这种形式,能够在一个独自的文件中定义函数,并在须要时导入,进而可能会达到复用的目标。
晚期将逻辑从组件中形象进去,能够让咱们领有更简洁的组件和易于重用的实用函数。
七、不要应用内联款式
CSS 作用域在 React 中是通过 CSS-in-JS 的计划实现的,这引入了一个新的面向组件的款式范例,它和一般的 CSS 撰写过程是有区别的。另外,尽管在构建时将 CSS 提取到一个独自的样式表是反对的,但 bundle 里通常还是须要一个运行时程序来让这些款式失效。当你可能利用 JavaScript 灵活处理款式的同时,也须要衡量 bundle 的尺寸和运行时的开销。— 来自 Vue 官网
以前,网页开发有一个准则,叫做「关注点拆散」,次要是以下三种技术拆散:
- HTML 语言:负责网页的构造,又称语义层。
- CSS 语言:负责网页的款式,又称
- JavaScript 语言:负责网页的逻辑和交互,又称逻辑层或交互层。
对 CSS 来说,就是不要写内联款式(inline style
),如下:
<div style="width: 100%; height: 20px;">
然而组件化(Vue、React)风行当前,突破了这个准则,它要求把 HTML、CSS、JavaScript 写在一起。
应用 React 编写款式能够这么做:
const style = {fontSize: "14px"}
const UserCard = ({user}) => {
return (<ul style={style}>
<li>{user.name}</li>
<li>{user.age}</li>
<li>{user.email}</li>
<li>{getUserRole(user.roles)}</li>
</ul>
);
}
React 这么做有利于组件的隔离,每个组件蕴含了所有须要用到的代码,不依赖内部,组件之间没有耦合,很不便复用。
这里,本文不倡议在 React 中应用内联款式基于两点:
- 他会让你的 HTML 构造变得臃肿。
- 如果款式过多,保护起来很麻烦,无奈通过内部批改 CSS。
const style1 = {fontSize: "14px"}
const style2 = {
fontSize: "12px",
color: "red"
}
const style = {...}
const UserCard = ({user}) => {
return (<ul style={style}>
<li style={style2}>{user.name}</li>
<li style={color: "#333"}>{user.age}</li>
<li style={color: "#333"}>{user.email}</li>
<li style={color: "#333"}>{getUserRole(user.roles)}</li>
</ul>
);
}
看到这里,有人可能会反驳:「你能够应用 props
有条件地对 CSS 内嵌款式进行款式化」,这是可行的,然而,你的组件不应该只有 10
个解决 CSS 的 props
,而不做其余事件。
如果非要在组件中编写 CSS,倡议应用 style-components CSS-in-JS 库。
styled-components
编写的组件款式存在于 style
标签内,而且选择器名字是一串随机的哈希字符串,实现了部分 CSS 作用域的成果(scoping styles),各个组件的款式不会发生冲突。
如果不借助治理 CSS 的类库,把 CSS 和 JS 混合在一起,如果做的好,能够无效的做到组件隔离。如果做的不好,这个组件不仅会变得臃肿难以了解,你的 CSS 也会变得越来越难以保护。
八、编写无效的 HTML
很多人对 HTML 技术的关注度都是不够的,然而编写 HTML 和 CSS 依然是咱们前端工程师的必备工作。
React 是乏味的,Hooks 也是乏味的,但咱们最终关怀的是渲染 HTML 和使它看起来更敌对。
对于 HTML 元素来说,如果它是一个按钮,它应该是一个 <button>
,而不是一个可点击的 div
;如果它不进行表单提交,那么它应该是 type="button"
;如果它应该基于文本大小自适应,它不应该有一个固定的宽度,等等。
对一些人来说,这是最根本的,但对于一部分人来说,状况并非如此。
咱们常常会看到相似的表单提交代码:
import React, {useState} from 'react';
const Form = () => {const [name, setName] = useState('');
const handleChange = e => {setName(e.target.value);
}
const handleSubmit = () => {// api call here}
return (
<div>
<input type="text" onChange={handleChange} value={name} />
<button type="button" onClick={handleSubmit}>
Submit
</button>
</div>
)
}
这个示例所做的事是在 <input/>
上更新 name
值,并且在 <button/>
上绑定 click
事件,通过点击调用 handleSubmit
来提交数据。
这个性能对于通过应用按钮进行提交的用户是能够的,然而对于应用 Enter
键提交表单的用户来说就不行了。
对于 form
表单反对 Enter
提交,能够这么做,无需对 Enter
进行监听:
<form onsubmit="myFunction()">
Enter name: <input type="text">
<input type="submit">
</form>
具体 https://www.w3schools.com/jsref/event_onsubmit.asp
在 React 中 应用 onSubmit
是等效的:
import React, {useState} from 'react';
const Form = () => {const [name, setName] = useState('');
const handleChange = e => {setName(e.target.value);
}
const handleSubmit = e => {e.preventDefault();
// api call here
}
return (<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} value={name} />
<button type="submit">
Submit
</button>
</form>
)
}
当初,这个表单提交实用于任何触发场景,而不仅仅是一个只反对通过按钮点击的表单。
总结
- 遵循繁多目标组件哲学,防止过于简单的多行组件,并尽可能地将组件合成。
- 请记住,在父组件外部实现此操作总是比在组件自身外部实现更为洁净。
- 当在
render
中返回 JSX 时,不要应用内联的 JavaScript 逻辑。 - 组件外部定义的许多逻辑能够从组件中抽离,因为它的实现并不真正与组件相干。
- 晚期将逻辑从组件中形象进去,能够让咱们领有更简洁的组件和易于重用的实用函数。
- 如果不借助治理 CSS 的类库,把 CSS 和 JS 混合在一起,如果做的好,能够无效的做到组件隔离。如果做的不好,这个组件不仅会变得臃肿难以了解,你的 CSS 也会变得越来越难以保护。
- React 是乏味的,Hooks 也是乏味的,但最终咱们关怀的是渲染 HTML 和使它看起来更敌对。
参考:
- https://itnext.io/write-clean…(次要案例波及于此)
- https://zhuanlan.zhihu.com/p/…(介绍 useCallback)
- https://stackoverflow.com/que…(react-hooks-exhaustive-deps)
- https://zhuanlan.zhihu.com/p/…(介绍 CSS-in-Js)