完全是不可能滴, 这辈子都不可能完全! — 来自某非著名码农
本文总结 React 实用的特性,部分实验性和不实用的功能将不会纳入进来,或许未来可期~
1、setState
面试题
class App extends Component {
state = {
val: 0
}
// 震惊! 隔壁老王也有失手的时候~
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // ?
this.setState((prevState) => ({val: prevState.val + 1}));
console.log(this.state.val); // ?
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // ?
this.setState({val: this.state.val + 1});
console.log(this.state.val); // ?
}, 1000)
}
render() {
return <h2>App 组件 </h2>;
}
}
总结
setState 只在 React 合成事件和钩子函数中是“异步”的,在原生 DOM 事件和定时器中都是同步的。
如果需要获取“异步”场景的 setState 的值 –> this.setState(partial, callback) 在 callback 中拿到最新的值
如果要在“异步”场景保证同步更新多次 setState –> this.setState((prevState, props) => {return newState})
能保证同步更新, 但是外面获取的值还是之前的值
2、Fragment
before
代码
export default class App extends Component {
render() {
return (
<div>
<h2>App 组件 </h2>
<p> 这是 App 组件的内容 </p>
</div>
);
}
}
效果
after
代码
export default class App extends Component {
render() {
return (
<Fragment>
<h2>App 组件 </h2>
<p> 这是 App 组件的内容 </p>
</Fragment>
);
}
}
效果
总结:使用 Fragment,可以不用添加额外的 DOM 节点
3、React 性能优化
shouldComponentUpdate
// 举个栗子:
shouldComponentUpdate(nextProps, nextState) {
if (nextProps !== this.props) {
return true; // 允许更新
}
if (nextState !== this.state) {
return true;
}
return false; // 不允许更新
}
PureComponent 组件
使用
// 实现了对 state 和 props 的浅比较
// 相等就不更新,不相等才更新
class App extends PureComponent {}
浅比较源码
// 实现 Object.is() 方法, 判断 x y 是否完全相等
function is(x, y) {
// (x !== 0 || 1 / x === 1 / y) 用于判断 0 和 -0 不相等
// x !== x && y !== y 用于判断 NaN 等于 NaN
return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y
;
}
// 提取了 hasOwnProperty 方法,缓存
var hasOwnProperty$1 = Object.prototype.hasOwnProperty;
// 返回 false 为更新,true 为不更新
function shallowEqual(objA, objB) {
// 如果 A 和 B 完全相等,返回 true
if (is(objA, objB)) {
return true;
}
// 如果 A 和 B 不相等,并且不是对象,说明就是普通值,返回 false
if (typeof objA !== ‘object’ || objA === null || typeof objB !== ‘object’ || objB === null) {
return false;
}
// 提取 A 和 B 的所有属性
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
// 如果长度不相等,返回 false
if (keysA.length !== keysB.length) {
return false;
}
// 检测 A 的属性 和 B 的属性是否一样
for (var i = 0; i < keysA.length; i++) {
if (!hasOwnProperty$1.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
return false;
}
}
return true;
}
问题:如果使用 pureComponent 只能进行浅比较,如果修改了原数据再更新,就会导致地址值一样从而不会更新。但实际需要更新。
解决:
手动保证每次都是新的值
使用 immutable-js 库,这个库保证生成的值都是唯一的
var map1 = Immutable.Map({a: 1, b: 2, c: 3});
// 设置值
var map2 = map1.set(‘a’, 66);
// 读取值
map1.get(‘a’); // 1
map2.get(‘a’); // 66
总结:使用以上方式,可以减少不必要的重复渲染。
4、React 高阶组件
基本使用
// WrappedComponent 就是传入的包装组件
function withHoc(WrappedComponent) {
return class extends Component {
render () {
return <WrappedComponent />;
}
}
}
// 使用
withHoc(App)
向其中传参
function withHoc(params) {
return (WrappedComponent) => {
return class extends Component {
render () {
return <WrappedComponent />;
}
}
}
}
// 使用
withHoc(‘hello hoc’)(App)
接受 props
function withHoc(params) {
return (WrappedComponent) => {
return class extends Component {
render () {
// 将接受的 props 传递给包装组件使用
return <WrappedComponent {…this.props}/>;
}
}
}
}
定义组件名称
function withHoc(params) {
return (WrappedComponent) => {
return class extends Component {
// 定义静态方法,修改组件在调试工具中显示的名称
static displayName = `Form(${getDisplayName(WrappedComponent)})`
render () {
return <WrappedComponent {…this.props}/>;
}
}
}
}
// 封装获取包装组件的 displayName 的方法
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || ‘Component’;
}
原文链接
5、render props
原文太长,直接上 链接 官网真香, 建议大家将 React 官网过一遍~
6、React 懒加载
react-loadable
import Loadable from ‘react-loadable’;
import Loading from ‘./components/loading’
const LoadableComponent = Loadable({
loader: () => import(‘./components/home’),
loading: Loading,
});
export default class App extends Component {
render() {
return (
<div>
<h2>App 组件 </h2>
<LoadableComponent />
</div>
);
}
}
Suspense 和 lazy
import React, {Component, Suspense, lazy} from ‘react’;
import Loading from ‘./components/loading’;
const LazyComponent = lazy(() => import(‘./components/home’));
export default class App extends Component {
render() {
return (
<div>
<h2>App 组件 </h2>
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
</div>
);
}
}
区别
react-loadable 是民间 –> 需要额外下载引入
Suspense 和 lazy 是官方 –> 只需引入
react-loadable 支持服务器渲染
Suspense 和 lazy 不支持服务器渲染
总结:使用 create-react-app 会将其单独提取成一个 bundle 输出,从而资源可以懒加载和重复利用。
7、虚拟 DOM diff 算法
虚拟 DOM diff 算法主要就是对以下三种场景进行优化:
tree diff
对树进行分层比较,两棵树只会对同一层次的节点进行比较。(因为 DOM 节点跨层级的移动操作少到可以忽略不计)
如果父节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。
注意:
React 官方建议不要进行 DOM 节点跨层级的操作,非常影响 React 性能。
在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。
component diff
如果是同一类型的组件,按照原策略继续比较 virtual DOM tree(tree diff)。
对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
如果不是,直接替换整个组件下的所有子节点。
element diff
对处于同一层级的节点进行对比。
这时 React 建议:添加唯一 key 进行区分。虽然只是小小的改动,性能上却发生了翻天覆地的变化!
如:A B C D –> B A D C
添加 key 之前:发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。
添加 key 之后:B、D 不做任何操作,A、C 进行移动操作,即可。
建议:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
总结
React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
React 通过分层求异的策略,对 tree diff 进行算法优化;
React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
React 通过设置唯一 key 的策略,对 element diff 进行算法优化;
建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
原文链接
8、Fiber
Fiber 是为了解决 React 项目的性能问题和之前的一些痛点而诞生的。
Fiber 的核心流程可以分为两个部分:
可中断的 render/reconciliation 通过构造 workInProgress tree 得出 change。
不可中断的 commit 应用这些 DOM change。
异步实现不同优先级任务的协调执行:
requestIdleCallback: 在线程空闲时期调度执行低优先级函数;
requestAnimationFrame: 在下一个动画帧调度执行高优先级函数;
总结
可切分,可中断任务。
可重用各分阶段任务,且可以设置优先级。
可以在父子组件任务间前进 / 后退切换任务。
render 方法可以返回多元素(即可以返回数组)。
支持异常边界处理异常。
原文链接: https://mp.weixin.qq.com/s/uD…https://juejin.im/post/5a2276…
9、Redux
作用: 集中管理多个组件共享的状态
特点: 单一数据源、纯函数、只读 state
redux 核心模块定义:
store.js
import {createStore, applyMiddleware} from ‘redux’;
// 异步 actions 使用的中间件
import thunk from ‘redux-thunk’;
// redux 开发 chrome 调试插件
import {composeWithDevTools} from ‘redux-devtools-extension’;
import reducers from ‘./reducers’;
export default createStore(reducers, composeWithDevTools(applyMiddleware(thunk)));
reducers.js
import {combineReducers} from ‘redux’;
import {TEST1, TEST2} from ‘./action-types’;
function a(prevState = 0, action) {
switch (action.type) {
case TEST1 :
return action.data + 1;
default :
return prevState;
}
}
function b(prevState = 0, action) {
switch (action.type) {
case TEST2 :
return action.data + 1;
default :
return prevState;
}
}
// 组合两个 reducer 函数并暴露出去
export default combineReducers({a, b});
actions.js
import {TEST1, TEST2} from ‘./action-types’;
// 同步 action creator,返回值为 action 对象
export const test1 = (data) => ({type: TEST1, data});
export const test2 = (data) => ({type: TEST2, data});
// 异步 action creator,返回值为函数
export const test2Async = (data) => {
return (dispatch) => {
setTimeout(() => {
dispatch(test2(data));
}, 1000)
}
};
action-types.js
export const TEST1 = ‘test1’;
export const TEST2 = ‘test2’;
组件内使用:
App.jsx
import React, {Component} from ‘react’;
import PropTypes from ‘prop-types’;
import {connect} from ‘react-redux’;
import {test1, test2Async} from ‘./redux/actions’;
class App extends Component {
static propTypes = {
a: PropTypes.number.isRequired,
b: PropTypes.number.isRequired,
test1: PropTypes.func.isRequired,
test2Async: PropTypes.func.isRequired,
}
componentDidMount() {
const {a, b, test1, test2Async} = this.props;
// 测试
test1(a + 1);
test2Async(b + 1);
}
render() {
return (
<div>App 组件 </div>
);
}
}
/* =============== redux 相关代码 ================== */
// 将状态数据映射为属性以 props 方式传入组件
const mapStateToProps = (state) => ({a: state.a, b: state.b});
// 将操作状态数据的方法映射为属性以 props 方式传入组件
const mapDispatchToProps = (dispatch) => {
return {
test1(data) {
dispatch(test1(data));
},
test2Async(data) {
dispatch(test2Async(data));
}
}
}
// connect 就是一个典型的 HOC
export default connect(mapStateToProps, mapDispatchToProps)(App);
/*
// 上面写的太复杂了,但是好理解。而以下就是上面的简写方式
export default connect(
(state) => ({…state}),
{test1, test2Async}
)(App);
*/
index.js
// 入口文件的配置
import React from ‘react’;
import ReactDOM from ‘react-dom’;
import {Provider} from ‘react-redux’;
import App from ‘./App’;
import store from ‘./redux/store’;
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>, document.getElementById(‘root’));
总结:
我们会发现使用 Redux 会变得更加复杂,以及多了很多模板代码 (例如: action creators)
但是,这是 Redux 能帮助我们更好操作状态,追踪和调试错误等。
并且 Redux 有着一整套丰富的生态圈,这些你都能在 官方文档 找到答案
总之,目前比起世面上 mobx 等库,更适用于大型项目开发~
10、未来可期
其实还有很多技术没有说,像 context 和 React Hooks 等,但受限于笔者的眼界,目前没有发现大规模使用的场景(如果有,请小伙伴们指正),所以就不谈了~ 有兴趣的小伙伴去找找看吧~