前言
大部分语言或者框架库的使用,一旦涉及到进阶部分,绝对离不开三点:
1、性能优化 2、组件封装 3、语言难点
本文将会围绕这3个方向讲解React相关的进阶知识。
React 性能优化
SCU
在 React 中默认父组件更新,子组件无条件更新,有很多时候子组件的状态是没有变化的,并不需要更新,那么有没有什么办法可以处理吗?答案是肯定的,可以使用生命周期函数shouldComponentUpdate
简称SCU对组件的状态进行对比从而判断组件是否需要更新。
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.color !== this.props.color) {
return true // 可以渲染
}
if (nextState.count !== this.state.count) {
return true // 可以渲染
}
return false // 不重复渲染
}
通过判断组件
- 当前state与变化之后的nextState
- 当前props与变化之后的nextProps
来决定组件自身是否更新。
我们来看一个简单的例子:
import React from "react";
class Scu extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0,
content:"我是 ScuChild 组件"
}
}
onIncrease = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
console.log("Scu,渲染了~");
return <div>
<ScuChild content={this.state.content} />
<span>{this.state.count}</span>
<button onClick={this.onIncrease}>increase</button>
</div>
}
}
class ScuChild extends React.Component{
render() {
console.log("ScuChild 渲染了~")
return <div>
{this.props.content}
</div>
}
}
export default Scu
父组件每次点击时 count 增加 1,这么个简单的例子,每次都是改变了Scu组件自身的状态,而它的子组件 ScuChild 获取到的props.content从未改变过,但是每次点击都更新了它,这很明显不合理,但是React框架就是这样设计的,没有帮我们处理,只能自己动手了。
在 ScuChild 组件中添加:
shouldComponentUpdate(nextProps, nextState) {
// 当content不一样时,我们进行更新,否则不更新
return nextProps.content !== this.props.content;
}
当content值不变化时,它是不会再更新了。
PureComponent
每次都手写 shouldComponentUpdate
函数太麻烦了,React提供了 PureComponent
基类给我们继承,它用当前与之前 props 和 state 的浅比较覆写了 shouldComponentUpdate()
的实现。
也就是说,如果是引用类型的数据,只会比较是不是同一个地址,而不会比较具体这个地址存的数据是否完全一致。
这也解答了为什么setState时,我们必须要使用不可变值,每次都需要浅复制state中的对象,而不能直接使用push,pop等改变对象或数组的方法。
setState({
list:this.state.list.push(1)
})
如果我们这样 setState 的话,如果是它的子组件接收 list 来渲染,那么 PureComponent
对比 list 对象的地址没有变化就不会去渲染,实际上已经 push 了一个值进去了。
class ScuChild extends React.PureComponent{
render() {
console.log("ScuChild 渲染了~")
return <div>
{this.props.content}
</div>
}
}
改造过后是不是写起来非常方便。
Memo
在函数组件中并没有提供 shouldComponentUpdate
生命周期钩子给我们,可以使用React.Memo
函数进行包装
function ScuChild2(props) {
console.log("ScuChild2");
return (
<div>
{props.content}
</div>
)
}
export const MemodScuChild = React.memo(ScuChild2);
Reack.memo 还可以接受一个比较函数:
export const MemodScuChild = React.memo(ScuChild2,(prevProps, nextProps)=> prevProps.content===nextProps.content);
组件懒加载
通过懒加载来优化项目性能是常规的操作手段了,TC39提案有 import()
动态导入方法,它使得懒加载变的非常简单。React v16.6 发布了 lazy 函数让 React 实现组件懒加载更加方便。
import React from 'react'
const Scu = React.lazy(() => import('./Scu'));
class Lazy extends React.Component {
constructor(props) {
super(props)
}
render() {
return <div>
<p>引入一个动态组件</p>
<hr />
<React.Suspense fallback={<div>Loading...</div>}>
<Sku />
</React.Suspense>
</div>
// 刷新,可看到 loading (看不到就限制一下 chrome 网速)
}
}
export default Lazy
Scu 组件就是上面编写好的组件,现在通过 Lazy 组件的懒加载的方式引入。
有两点需要注意的:
- 需要懒加载的组件必须通过
export default
导出组件; React.lazy
和Suspense
技术不支持服务端渲染。
懒加载原理分析
lazy 函数源码
export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
return {
?typeof: REACT_LAZY_TYPE,
_ctor: ctor,
_status: -1, // 资源的状态
_result: null, // 存放加载文件的资源
};
}
const Scu = React.lazy(() => import('./Scu'));
就相当于
const Scu = {
?typeof: REACT_LAZY_TYPE, // 表明懒加载类型
_ctor: () => import('./Scu'),
_status: -1, // 初始化的状态 -1 == pending
_result: null,
}
还有另外一个关键函数 readLazyComponentType
它是React中负责解析懒加载的函数:
export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
const status = lazyComponent._status;
const result = lazyComponent._result;
switch (status) {
case Resolved: {
const Component: T = result;
return Component;
}
case Rejected: {
const error: mixed = result;
throw error;
}
case Pending: {
const thenable: Thenable<T, mixed> = result;
throw thenable;
}
default: {
lazyComponent._status = Pending;
const ctor = lazyComponent._ctor;
const thenable = ctor();
thenable.then(
moduleObject => {
if (lazyComponent._status === Pending) {
const defaultExport = moduleObject.default;
lazyComponent._status = Resolved;
lazyComponent._result = defaultExport;
}
},
error => {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
},
);
lazyComponent._result = thenable;
throw thenable;
}
}
}
源码解释:
首先 switch (status)
是通过组件的状态进行执行,默认状态是 -1 因此会走defalut
分支。defalut
有一段核心代码:
import('./Scu').then(
moduleObject => {
if (lazyComponent._status === Pending) {
const defaultExport = moduleObject.default; // {1}
lazyComponent._status = Resolved; // {2}
lazyComponent._result = defaultExport; // {3}
}
},
error => {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
},
);
- {1} 获取异步加载组件的输出对象的
default
属性,还记得上面有个注意点吗?“需要懒加载的组件必须通过export default
导出组件”,是不是从源码const defaultExport = moduleObject.default
就可以理解为什么要这样做了。 - {2} 更改它的状态为完成
Resolved
- {3}
lazyComponent._result
属性赋值为组件的实际内容
因此可以分析出其实核心依然是ES2020提出的 import("...")
动态加载模块的方案。
webpack
打包 import()
的基本原理:
- 遇到
import()
加载的组件,则打包成一个单独的chunk
- 当需要满足加载条件时,则动态插入一个
<script src="chunk path"></script>
- 当加载成功则触发了
import('./Scu').then
里面的第一个函数,当加载失败则触发第二个函数。
Suspense 原理
在使用 lazy
组件时必须使用 React.Suspense
进行包裹,当懒加载组件还没有加载完成时,会显示自定义 loading
状态。我们通过一个 Suspense
的伪代码来理解其原理
import React from 'react'
class Suspense extends React.Component {
state = {
promise: null
}
componentDidCatch(e) {
if (e instanceof Promise) {
this.setState({
promise: e
}, () => {
e.then(() => {
this.setState({
promise: null
})
})
})
}
}
render() {
const { fallback, children } = this.props;
const { promise } = this.state;
return <>
{ promise ? fallback : children }
</>
}
}
代码解释:
核心代码就在 componentDidCatch
生命周期函数中,它可以捕获子组件树发生的任何错误。然后通过错误类型去 setState promise
的值,最后根据 promise
的值决定渲染什么内容。
这样就完全可以理解为什么 readLazyComponentType
会 throw thenable
,因为 Suspense
是通过捕获错误的方式来实现的。
截获到对应的错误时就去展示懒加载的组件,否则就展示设置好的loading组件。
React 组件封装
提起封装,在 JavaScript
中我们第一个想到的肯定是高阶函数(例如回调方法的封装),那么在 React
中我们也是使用类似的思想去做封装,叫做高阶组件 HOC
。
HOC
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
我们想象一个汽车装配流水线,一个空架子的汽车,经过一个流水线就装配上了底盘,再经过另外一条流水线就装配上了方向盘...一次经过。把能力抽象出来,给所有不具备该能力的组件加上一个能力。这就是高阶组件的具象说明了。
在将设计模式那篇文章时也将了,HOC其实就是装饰罩模式,而且只需要通过更改配置文件就支持装饰罩模式的写法 @HOC
在编程的世界里面我们没有方向盘、车载音响安装。但是我们会有我们的实际业务也是同样需要装配的。
实现高阶组件
1、属性代理
函数返回一个我们自己定义的组件,然后在 render 中返回要包裹的组件,这样我们就可以代理所有传入的 props,并且决定如何渲染,实际上 ,这种方式生成的高阶组件就是原组件的父组件
function proxy(WrappedComponent) {
return class extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
}
2、反向继承
返回一个组件,继承原组件,在 render中调用原组件的 render。由于继承了原组件,能通过this访问到原组件的 生命周期、props、state、render等,相比属性代理它能操作更多的属性。
function inherit(WrappedComponent) {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
HOC 可以实现的功能
组合渲染
function CombProxy(WrappedComponent) {
return class extends React.Component {
render() {
return (
<>
<div>{this.props.title}</div>
<WrappedComponent {...this.props} />
</>
)
}
}
}
class Child extends React.Component{
render() {
return (
<div>child 组件</div>
)
}
}
export default CombProxy(Child);
这样任意组件经过 CombProxy
组件的包装,都具备可以渲染标题的能力了,而不再需要自己单独添加该功能。
双向数据绑定
import React from "react";
function proxyHoc(WrappedComponent){
return class extends React.Component{
constructor(props) {
super(props);
this.state = {value:""}
}
onChange = (event)=>{
const {onChange} = this.props;
this.setState({
value: event.target.value
},()=>{
if(typeof onChange === 'function'){
onChange(this.state.value);
}
})
}
render(){
const newProps = {
value: this.state.value,
onChange: this.onChange
}
const props = Object.assign({},this.props,newProps);
return <WrappedComponent {...props} />
}
}
}
class HOC extends React.Component{
render() {
return <input {...this.props} />
}
}
export default proxyHoc(HOC);
代码解释:
- 把
input
表单元素传递进入高阶组件 - 高阶组件中自定义了
value
状态,以及onChange
事件,并且传递到input
表单元素上 - 这样表单元素就相当于
<input onChange={...} value="...">
并且双向绑定的逻辑都在高阶组件完成。
表单校验
我们基于上面这个高阶函数继续给表单一个校验的能力。
import React from "react";
function proxyHoc(WrappedComponent){
return class extends React.Component{
constructor(props) {
super(props);
this.state = {
value:"",
error:""
}
}
onChange = (event)=>{
const {onChange,validator} = this.props;
if(validator && typeof validator.func === 'function'){
if(validator.func(event.target.value)){
this.setState({
error : ""
})
}else{
this.setState({
error : validator.msg
})
}
}
this.setState({
value: event.target.value
},()=>{
if(typeof onChange === 'function'){
onChange(this.state.value);
}
})
}
render(){
const newProps = {
value: this.state.value,
onChange: this.onChange
}
const props = Object.assign({},this.props,newProps);
return (
<>
<WrappedComponent {...props} />
<div>{this.state.error}</div>
</>
)
}
}
}
class HOC extends React.Component{
render() {
return <input {...this.props} />
}
}
export default proxyHoc(HOC);
同时还是这个高阶组件,既完成 onChange
双向绑定的功能,又可以根据传递进来的校验规则进行校验
import Input from "./input";
function Hoc() {
const validatorName = {
func: (val) => val && val.length > 2,
msg : "名字必须大于2位"
}
return (
<div className="App">
<Input validator={validatorName} onChange={(val)=>{console.log(val)}} />
</div>
);
}
export default Hoc;
调用起来也是非常简单的。这样进行抽象组件层次分明,代码易于维护。
高阶组件的缺陷:
-
HOC
需要在原组件上进行包裹或者嵌套,如果大量使用HOC
,将会产生非常多的嵌套,这让调试变得非常困难。 -
HOC
可以劫持props
,在不遵守约定的情况下也可能造成冲突。
function 组件
Function Component 是更彻底的状态驱动抽象,甚至没有 Class Component 生命周期的概念,只有一个状态,而 React 负责同步到 DOM。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
函数组件容易阅读和测试,没有状态或生命周期。因此可以让我们快速的写一个渲染UI的组件,这也与React推崇函数式编程的思想契合。
在React v16.8 推出之前函数组件只能简单的渲染组件,没有自身的状态,Hook的到来改变了这一现状,它赋予函数组件更多的能力。
通过useEffect
Hook让函数组件拥有“生命周期”,它跟 class 组件中的componentDidMount
、componentDidUpdate
和componentWillUnmount
具有相同的用途,只不过被合并成了一个 API。
通过useState
让函数组件拥有了自身的状态。
好吧,讲了这么多了,估计你应该对函数组件已经有所了解了。那么就让我们深入学习下函数组件的特性。
Capture Value
先看一个例子:
class App extends React.Component{
state = {
count: 0
}
show = ()=>{
setTimeout(()=>{
console.log(`1秒前 count = 0,现在 count = ${this.state.count}`);
},1000)
}
render() {
return (
<div onClick={()=>{
this.show();
this.setState({
count: 5
})
}}>
点击Class组件
</div>
);
}
}
点击输出为:1秒前 count = 0,现在 count = 5。
这个很好理解,定时器1秒后获取到的是组件实例更改后的状态,符合我们正常的思维。
function App(){
const [count , setCount] = React.useState(0);
const show = ()=>{
setTimeout(()=>{
console.log(`1秒前 count = 0,现在 count = ${count}`);
},1000)
}
return (
<div onClick={()=>{
show();
setCount(5);
}}>
点击函数组件
</div>
);
}
点击输出为:1秒前 count = 0,现在 count = 0。
这个结果与Class组件的结果就完全不一样了,1秒之后获取到的状态任然是之前的状态。这个现象我们称之为函数组件的 Capture Value
特性,就像每次点击拍了一张快照保存了当时的数据,我们来分析具体原理。
每次 Render 都有自己的 Props 与 State
当第一点击时相当于这样:
function App(){
const count = 0;
const show = ()=>{
setTimeout(()=>{
console.log(`1秒前 count = 0,现在 count = ${count}`); // 1秒前 count = 0,现在 count = 0
},1000)
}
...
}
当第二次点击时相当于这样:
function App(){
const count = 5;
const show = ()=>{
setTimeout(()=>{
console.log(`1秒前 count = 0,现在 count = ${count}`); // 1秒前 count = 0,现在 count = 5
},1000)
}
...
}
可以认为每次 Render
的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender
时,就形成了 N 个 Render
状态,而每个 Render
状态都拥有自己固定不变的 Props
与 State
。
绕过 Capture Value 特性
利用 useRef
就可以绕过 Capture Value
的特性。可以认为 ref
在所有 Render
过程中保持着唯一引用,因此所有对 ref
的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render
间存在隔离。
function App(){
const [count , setCount] = React.useState(0);
const lastCount = React.useRef(count);
const show = ()=>{
setTimeout(()=>{
console.log(`1秒前 count = 0,现在 count = ${lastCount.current}`);
},1000)
}
return (
<div onClick={()=>{
show();
lastCount.current = 5;
setCount(5);
}}>
点击函数组件(useRef)
</div>
);
}
关于 Capture Value
的结论就是:一切均可 Capture
,除了 Ref
。
class 组件与 function 组件的区别
function component
它是普通函数,不能使用setState
,因此也称之为无状态组件function component
没有生命周期的概念function component
有Capture Value
特性
React Hook
前面讲解函数组件时其实已经大量运用了Hook技术,但是还是需要单独来讲,主要是作为新特性,它实在是太重要了,几乎聊起React技术都会提及它,因此我们还是有必要学习它,并且知道它的实现原理。
useState 使用
它使得函数组件拥有了状态,而且使用也是非常简单:
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
- 第一行: 引入 React 中的
useState
Hook。它让我们在函数组件中存储内部state
。 - 第三行: 在
Example
组件内部,我们通过调用useState
Hook 声明了一个新的state
变量。它返回一对值给到我们命名的变量上。我们把变量命名为count
,因为它存储的是点击次数。我们通过传0
作为useState
唯一的参数来将其初始化为0
。第二个返回的值本身就是一个函数。它让我们可以更新count
的值,所以我们叫它setCount
。 - 第七行: 当用户点击按钮后,我们传递一个新的值给
setCount
。React 会重新渲染Example
组件,并把最新的count
传给它。
useState 原理
基于上面这个最简单的使用,我们来一个最简单的实现,揭开 useState
神秘的面纱。
import React from "react";
import { render } from "../../index";
let _state; // 把 state 存储在外面
function useState(initialValue) {
// 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
_state = _state || initialValue;
function setState(newState) {
_state = newState;
render();
}
return [_state, setState];
}
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<div>{count}</div>
<button onClick={() => { setCount(count + 1); }}>
点击 UseState
</button>
</div>
);
}
export default Counter;
render 函数:
export const render = ()=>{
ReactDOM.render(
<App />,
document.getElementById('root')
);
}
render();
useState
函数使用闭包原理把值存储起来了,setState
则是每次调用render
方法重新渲染组件。
useEffect 使用
它使得函数组件拥有了生命周期,等同于以下三个生命周期:
componentDidMount
组件已经挂载成 DOMcomponentDidUpdate
组件已经更新componentWillUnmount
组件卸载
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
},[count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
代码分析:
- 当组件挂载成
DOM
结构后,会调用effect
,会去改变页面的标题 - 当发生点击事件后,组件执行更新后,又会去调用
effect
,从而更新了页面的标题 useEffect
第二个参数是一个数组,里面写count
,意思是count
值变化了才会去执行effect
React
保证了每次运行 effect
的同时,DOM
都已经更新完毕。
useEffect 原理
上面介绍了简单的使用,那么我们来总结下它的特性:
- 有两个参数
callback
和dependencies
数组 - 如果
dependencies
不存在,那么callback
每次都会执行 - 如果
dependencies
存在,只有当它发生了变化,callback
才会执行
import React,{useState} from "react";
let _deps; // _deps 记录 useEffect 上一次的 依赖
function useEffect(callback, depArray) {
const hasNoDeps = !depArray; // 如果 dependencies 不存在
const hasChangedDeps = _deps
? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
: true;
/* 如果 dependencies 不存在,或者 dependencies 有变化*/
if (hasNoDeps || hasChangedDeps) {
callback();
_deps = depArray;
}
}
function Counter() {
const [count, setCount] = useState(0);
useEffect(()=>{
document.title = `You clicked ${count} times`;
});
return (
<div>
<div>{count}</div>
<button onClick={() => { setCount(count + 1); }}>
点击 UseState
</button>
</div>
);
}
export default Counter;
到这里,我们又实现了一个可以工作的 useEffect
,似乎没有那么难。
到现在为止,我们已经实现了可以工作的 useState
和 useEffect
。但是有一个很大的问题:它俩都只能使用一次,因为只有一个 _state
和 一个 _deps
。比如
const [count, setCount] = useState(0);
const [username, setUsername] = useState('fan');
count
和 username
永远是相等的,因为他们共用了一个 _state
,并没有地方能分别存储两个值。我们需要可以存储多个 _state
和 _deps
。
此时我们肯定会想到例如对象,数组,链表等数据结构都可以处理这种情况。
关键在于:
- 初次渲染的时候,按照
useState
,useEffect
的顺序,把state
,deps
等按顺序塞到memoizedState
数组中。 - 更新的时候,按照顺序,从
memoizedState
中把上次记录的值拿出来。
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
代码解释:
- 初始化的时候,按照顺序依次把所有的
state
放进数组中。 - 当执行
set
操作时,利用了闭包原理,每个state
相对应的currentCursor
都在内存中已经有一份存储,而且互相不会污染。这样就可以获取到相应的下标去更新数组中的值,然后执行render
操作更新界面。
在React
中,每个组件存储自己的Hook
信息,并且不是使用数组实现而是使用链表实现。
像一些高阶的面试题:
- 函数组件是如何从无状态组件变为有状态的?
useState
与useEffect
的工作原理 ?
这些题目是不是心里已经有答案了。