函数组件和类组件
- 类组件:以class声明,拥有正常的生命周期函数,state,props
- 函数组件: 本质是一个函数,接受一个
props
为参数,只相当于一个render
方法,不存在生命周期函数,也不能维护自己到state
为什么要使用hooks
- 可以让传统的函数组件有内部状态
state
,并且可以通过一些hooks来模拟/替换class组件中的生命周期函数。 - 在传统的react开发流程中,我们的自定义组件通常需要定义几个生命周期函数,在不同的生命周期处理各自的业务逻辑,有很多情况下他们是重复的。使用hooks可以简化这些重复的逻辑。
- this指向问题,class组件内部需要手动绑定this,而函数组件本身是一个函数,不需要绑定
this
useState 函数组件有状态了
const [state, setState] = useState(initialState);
useState
: 是一个方法,接收一个初始值initialState
作为参数,返回一个数组,第一项为当前的state
的值,第二项为更新state
的方法- initialState可以是一个方法, 也可以是基本数据类型或者一个对象。
- 这里的
setState
方法与class组件中的setState
有所不同,此setState
不会合并state
中的值,而是整体的替换。hooks里需要通过setState({ ...state, changedState:changedValue})
的方式手动merge
。 hooks
中的setState
是不支持第二个参数的- 和class组件中
this.setState()
一样,hooks
中的setState
也是异步的,连续调用两次setState,数据只改变一次。可以通过setState((preValue) => preValue + 1)
使用多个hooks,顺序很重要
import React, { useState } from 'react';
function Example(){
const [ age , setAge ] = useState(18)
const [ sex , setSex ] = useState('男')
const [ work , setWork ] = useState('前端程序员')
return (
<div>
<p onClick={() => setAge(age + 1)}> 今年:{age}岁</p>
<p>性别:{sex}</p>
<p>工作是:{work}</p>
</div>
)
}
export default Example;
- 所有的
hooks
保存在一个全局变量上,这个变量是一个链式结构 - 函数组件重新
render
读取state
的时候是根据这个链式结构来读取到 - 当我们使用多个
hooks
时,不能在if...else.../ for循环
语句里使用hooks,并且它只能使用在最顶级的作用域里。 - 需要保证每次
rerender
的时候这些hooks
都被执行到并且执行顺序不能改变
// hook的基本结构
{
memoizedState: 当前值,
queue: 更新队列,
next: 指向下一个hook
}
const [ age , setAge ] = useState(18)
const [ sex , setSex ] = useState('男')
const [ work , setWork ] = useState('前端程序员')
// 首次render
memoizedState: {
memoizedState: 18,
queue: null,
next: {
memoizedState: '男',
queue: null,
next: {
memoizedState: '前端程序员',
queue: null,
next : {
...
next : {
...
}
}
},
}
}
useEffect 替换生命周期函数,整合重复操作
为什么使用useEffect
在类组件中,我们经常在一些生命周期函数里处理一些额外的操作(数据请求、js事件绑定/解绑、DOM操作、样式的修改),我们把这些操作叫做副作用
,很多时候这些操作都是重复的。
useEffect 就是用来替换常用的生命周期函数(componentDidMount, ComponentDidUpdate, componentWillUnmount),并把这些重复的操作整合到一起。
useEffect(() => {
// DOM更新之后要执行某些操作。
return () => {
// 清除副作用
}
},deps)
useEffect接受两个参数:
effect
- 是一个匿名函数,这个函数会在DOM 更新之后被执行
- 可以返回一个匿名函数,这个函数叫清除函数,它会在组件卸载前执行(替换componentWillUnmount)。
deps
- deps是一个可选参数,它是一个数组,数组里项可以是
state、props、function
,表示这个effect
依赖的对象 - 默认情况下: 会在dom每次更新(包括第一次渲染)后调用effect(替换componentDidMount, ComponentDidUpdate)。
- 第二个参数是个空数组: 表示只会在第一次render结束后调用一次effect(替换ComponentDidMount)
- 第二个是非空数组: 表示数组里依赖的某属性变化后就会执行effect,注意这里的变化进行的是引用地址的比较。
关于清除函数的执行时机
- 默认情况下或
deps
不为空时,如果非首次渲染,它的执行次序是
// setState -> rerender -> dom更新、ui渲染 -> 执行上一次的清除函数 -> 执行effect函数
- deps的数组为空:则清除函数会在组件销毁前执行
useEffect注意点:
- 需要保证在effect里使用的state、props都必须存在与deps里。
- 一般不在useEffect的effect函数中执行操作DOM/样式的相关操作:useEffect中定义的函数的执行不会阻碍浏览器更新视图,在浏览器完成布局与绘制之后,会延迟调用effect。 而componentDidMonut和componentDidUpdate中的代码都是同步执行的。
useLayoutEffect
它和 useEffect
的结构相同,区别只是调用时机不同。它的effect函数执行是同步执行的,所以一般操作DOM或修改样式都使用这个hook
useContext: 全局共享数据
Context API
Context
是React中用来共享那些对于一个组件树而言是“全局”的数据(主题/语言/用户信息)。它解决的是多级组件之间传参的问题
// 祖先组件 创建一个context对象
const MyContext = React.createContext(defaultValue);
// 生成的context对象具有两个组件类对象
{
Provider: React.ComponentType<{value: T}>,
Consumer: React.ComponentType<{children: (value: T)=> React.ReactNode}>
}
// 祖先组件 MyContext.Provider
<MyContext.Provider value={/* 某个值,可以在我的圈子内共享 */}>
<ComponentA />
<ComponentB />
</MyContext.Provider>
// 子孙组件 MyContext.Consumer
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染, 当前的 value 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。*/}
</MyContext.Consumer>
useContext 让父子组件传值更简单
useContext
是基于Context API
实现的,它可以帮助我们跨越组件层级直接传递变量,实现共享。
使用useContext
就表示当前组件被<MyContext.Consumer>
包裹,并且它的返回值就是<MyContext.Provider>
上的value
属性
const context = useContext(MyContext)
// context相当于 <MyContext.Provider>上接受的value属性
需要注意的是useContext
和redux
的作用是不同的,一个解决的是组件之间值传递的问题,一个是应用中统一管理状态的问题,但通过和useReducer
的配合使用,可以实现类似Redux
的作用。
useReducer
什么是reducer
reducer
其实就是一个函数,这个函数接收两个参数,一个是状态state
,一个用来控制业务逻辑的判断参数action
。
function countReducer(state, action) {
switch(action.type) {
case 'add':
return state + 1;
case 'sub':
return state - 1;
default:
return state;
}
}
- useReducer的使用
useState
的替代方案,一般用在state
逻辑较复杂且包含多个子值,或者下一个 state
依赖于之前的state
等场景下。useReducer
可以将更新和操作解耦
两种使用方式
// 指定初始值的使用方式
const [state, dispatch] = useReducer(reducer, initState);
/**
* reducer:reducer函数
* initState:初始值
*
**/
// 惰性初始化,初始值需要经过比较复杂的计算时使用
const [state, dispatch] = useReducer(reducer, initialArg, init);
/**
* reducer:reducer函数
* initialArg:传给init的参数
* init:指定的初始化函数
*
**/
/**
* state: 返回的状态值
* dispatch: 触发reducer的方法
**/
useReducer配合useContext来实现简易的redux
我们知道实现redux
需要满足两点条件
- 一个全局的状态,并且做统一管理
- 更新这些状态,实现业务逻辑
useContext
:可访问全局状态,避免一层层的传递状态。
useReducer
:通过action的传递,更新复杂逻辑的状态,可以实现类似Redux中的Reducer部分
需要注意的是,useReducer
的dispatch
操作必须是同步的,如果需要执行异步操作,需要模拟类似react-redux
的实现方式。
useCallback
useCallback
主要用来解决使用React hooks
产生的无用渲染的性能问题。
在class组件中,我们渲染时的性能优化一般可以通过shouldCompnentUpdate
函数来进行, 但是在函数组件里,由于它不具备生命周期函数,也就是说我们没有办法通过组件更新前条件来决定组件是否更新。函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗。useMemo和useCallback都是解决上述性能问题的。
组件多次复用的性能问题
// Counter.tsx 多次复用Count, Button, Title
function Counter() {
const [age, setAge] = useState(18);
const [salary, setSalary] = useState(5000);
const incrementAge = () => {
setAge(age + 1)
}
const incrementSalary = () => {
setSalary(salary + 1000)
}
return (
<div>
<Title/>
<Count text="age" count={age}/>
<Button handleClick={incrementAge}>
修改年龄
</Button>
<Count text="salary" count={salary}/>
<Button handleClick={incrementSalary}>
修改工资
</Button>
</div>
);
}
// Count.tsx
function Count(props: {
text: string,
count: number
}) {
console.log(`Rendering ${props.text}`)
return (
<div>
{props.text} - {props.count}
</div>
)
}
// Title.tsx
function Title() {
console.log('Rendering Title')
return (
<h2>useCallback</h2>
)
}
// Button.tsx
function Button(props: {
handleClick: () => void
children: string
}) {
console.log('Rendering button', props.children)
return (
<button onClick={props.handleClick}>
{props.children}
</button>
)
}
当我们每次点击按钮时,看到以下日志:
Rendering Title
Rendering age
Rendering button 修改年龄
Rendering salary
Rendering button 修改工资
每次状态改变都触发了所有组件的rerender
,然而我们期望是当我们修改年龄时,只有依赖age
的那个组件rerender
。
使用 React.memo 优化
不同class组件中可以使用shoudComponentUpdate
, PureComponent
来做性能优化, React为函数式组件提供了叫React.memo
一个高阶组件.
我们可以通过将组件包装在React.memo
中调用,通过这种记忆组件渲染结果的方式来提高组件的性能。这意味着当props
没有变化时, React
将跳过渲染组件的操作并直接复用最近一次渲染的结果。
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});
React.memo
仅检查 props
变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState
或 useContext
的 Hook,当 context
发生变化时,它仍会重新渲染。
使用了 React.memo 后,我们看到点击增加年龄的按钮时,日志变为了
// Rendering age
// Rendering button 修改年龄
// Rendering button 修改工资
依然有不相关的 rerender
Rendering button 修改工资
出现。说明修改工资
这个组件的props发生里变化。
简单分析一下:
- 点击
增加年龄
按钮触发setAge()
方法 age
改变导致组件重新渲染,重新执行Counter()
方法Counter()
内部的方法重新被创建修改工资 Button
传入的props
发生了变化Button()
重新render
因此这个 Button
传入的 props
发生了变化,这时候React.memo
没有阻止 rerender
。而我们的useCallback
这个`hook就是为了解决这个问题。
在js中,当函数执行时,会创建一个被称为执行环境的对象,这个对象在每次函数执行时都是不同的,当多次执行该函数时会创建多个执行环境。这个执行环境会在函数执行完毕后销毁。所以每次rerender时都会创建新的执行环境,并为其内部的方法重新分配空间
什么是 useCallback`
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回一个memoized
回调函数。
把内联回调函数
及依赖项数组
作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本
,该回调函数仅在某个依赖项改变时才会更新。
在上述例子中
const incrementAge = useCallback(
() => {
setAge(age + 1)
},
[age],
)
// Rendering salary
// Rendering button 修改工资
useMemo
useMemo和useCallback类似,都是用来做性能优化的。
- useMemo:缓存的是值
- useCallback: 缓存的是函数
先来看一个例子
import React, { useState } from 'react'
function Counter() {
const [counterOne, setCounterOne] = useState(0)
const [counterTwo, setCounterTwo] = useState(0)
const incrementOne = () => {
setCounterOne(counterOne + 1)
}
const incrementTwo = () => {
setCounterTwo(counterTwo + 1)
}
const isEven = () => {
let i = 0
while (i < 1000000000) i += 1
return counterOne % 2 === 0
}
return (
<div>
<button
onClick={incrementOne}
>Count One = {counterOne}</button>
<span>
{
isEven() ? 'even' : 'odd'
}
</span>
<br />
<button
onClick={incrementTwo}
>Count Two = {counterTwo}</button>
</div>
)
}
export default Counter
点击第一个按钮有较长的延迟,因为我们的判断偶数的逻辑中包含了大量的计算逻辑。但是,我们点击第二个按钮,也有较长的延迟!
这是因为,每次 state 更新时,组件会 rerender,isEven 会被执行,这就是我们点击第二个按钮时,也会卡的原因。我们需要优化,告诉 React 不要有不必要的计算,特别是这种计算量复杂的。
这时就需要 useMemo hook 登场了。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
-
返回一个 memoized 值。 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
-
传入 useMemo 的函数会在渲染期间执行。不要在这个函数内部执行与渲染无关的操作。
-
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
useRef
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中。
useRef
一般有两种用途
- 获取
DOM
节点,这一点和class组件中的ref
类似。 - 用来保存变量
获取上一轮的 props 或 state
function Example () {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count
});
const prevCount = prevCountRef.current
console.log(prevCount, count, '之前的状态和现在的状态')
return (
<div>
<div>{count}</div>
<button onClick={() => {setCount(count+1)}}>+</button>
</div>
)
}
自定义hook
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
通过自定义 Hook,可以将组件中重复的逻辑提取到可重用的函数中。
function Example () {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count)
console.log(prevCount, count, '之前的状态和现在的状态')
return (
<div>
<div>{count}</div>
<button onClick={() => {setCount(count+1)}}>+</button>
</div>
)
}
function usePrevious (value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
总结
- useState: 用来声明状态
state
,修改值需要手动合并 - useEffect: 用来替换类组件中的生命周期函数,简化重复的操作
- useContext: 全局共享状态,解决祖先/子孙组件之间的传参问题
- useReducer: useState的替换方案,将操作和更新解绑,配合useContxet可以实现简易redux
- useCallback: 对函数进行缓存,优化性能
- useMemo: 对值进行缓存,优化性能
- useRef:获取DOM节点或组件实例, 保存变量
Capture Value 捕获属性
react
会在每次rerender
时捕获自己独立的state、props、effects、事件处理函数
闭包
函数每次执行时会形成新的执行环境,这个对象上存在一个[[Scope]]
的属性,它指向到是它所在环境的作用域链.之后会生成一个活动对象(AO),这个AO上保存这当前函数到变量、参数、方法,并且会将这个AO对象放在[[Scope]]
的最顶端。一般来说,这个AO对象会在函数执行完成时随执行环境清除而清除。
但是,当我们在函数内部返回一个函数并在其外部被一个变量接收时,这个变量(返回的函数)的作用域链指向的是它所处环境的的作用域链,只要这个函数存在则它的作用域链就会一直存在,这样它的作用域链上的变量得不到释放,即能在函数外部访问作用域内部的变量,这样就形成里闭包
形成闭包最简单的方式就是在函数内部返回另一个函数。
function a() {
var b = 2;
function c() {
var d = 4;
console.log(b)
}
return c
}
var d = a() // a的[[scope]]指向全局环境,并生成自己的AO,放在[[scope]]的最顶端
d() // 2 c的[[scope]]指向a的[[scope]],并生成自己的AO,放在[[scope]]的最顶端
而在函数组件内部,正是因为js闭包机制,所以才有了Capture Value
属性
每次 Render 都有自己的 Props、State
export default class ClassCounter extends React.Component{
constructor(props){
super(props);
this.state = {
count: 0
}
}
handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + this.state.count);
}, 3000);
}
return (
<div>
<p>You clicked {this.state.count} times in class Component</p>
<button onClick={() => this.setState(this.state.count + 1)}>
Click me
</button>
<button onClick={this.handleAlertClick.bind(this)}>
Show alert
</button>
</div>
);
}
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
// 连续点击3次`Click me`
// 再点击一次`Show alert`,并在3s内点击两次`Click me`
// alert时count的值是多少?
let _state = null;
function useState(initialValue) {
const state = _state | initialValue;
function setState(newState) {
_state = newState;
// 会重新执行组件函数
// render();
}
return [state, setState];
}
function Component() {
const [count, setCount] = useState(0);
const handleAlertClick = () => {
setTimeout(() => {
console.log("You clicked on: " + count);
}, 3000);
};
// 暴露页面上可以执行的函数
return [handleAlertClick, setCount];
}
// 首次 render count = 0
const [handleAlertClick, setCount] = Component();
// 点击按钮,形成闭包,此时闭包[[Scope]]上的count=0,执行setCount,_count 变为1,
// 此时会重新执行Component(),生成新的执行环境,并返回新的 handleClick,setCount,
setCount(count + 1);
setCount(count + 1); // count = 1 _count=2
setCount(count + 1); // count = 2 _count=3
// 模拟点击showAlert,
handleAlertClick(); // count = 3 3s后alert的是3
setCount(count + 1); // count = 3 _count=4
setCount(count + 1); // count = 4 _count=5
每一次渲染都有它自己的 Props、State and Effects,每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state,并且在这次渲染中它的state
是固定不变的。也就是说他们都有Capture Value
属性,这是函数组件区别与class组件到的特性之一。
如何绕过 Capture Value
function Counter() {
const [count, setCount] = useState(0);
const latestCount = useRef(count)
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + latestCount.current);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => {
setCount(count + 1);
latestCount.current = count + 1;
}}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
由于Capture Value
的存在,我们在class组件中有些比较合理
的想法,在函数组件中使用似乎就会有点问题
不要对 Dependencies 撒谎
考虑这么一个需求: 定义一个count
,让这个count
每秒加一,并且显示在页面上。
按照我们class组件的想法,在componentDidMount里,定义一个计时器setInterval, 并且在componentWillUnmount里清除计时器
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []); // 传空数组只执行一次
return <h1>{count}</h1>;
}
这看起来似乎没什么问题,但是由于 useEffect
符合 Capture Value
的特性,拿到的 count
值永远是初始化的 0。相当于 setInterval 永远在 首次render的Scope 中执行,你后续的 setCount 操作并不会产生任何作用。
这显然和我们的需求不符,于是我们在deps
里添加一个属性count
,告诉react
当count
变化后再执行
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
这种方式满足了了我们的需求,但是我们的诚实
也带来了一定的代价
- 计时器不准了,因为每次 count 变化时都会销毁并重新计时。
- 频繁 生成/销毁 定时器带来了一定性能负担。
怎么既诚实又高效?
setState有一种回调函数式的调用方式setState((preState) => preState + 1)
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
当某个值依赖多个值变化时?
某一天,我们改变了需求,希望显示在页面上的值,依赖两个数据的变化
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
我们会发现不得不依赖step
这个变量,那有没有什么办法能将更新和动作解耦呢?
金手指模式
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" }); // Instead of setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, []);
更新变成了dispatch({ type: "tick" })
所以不管更新时需要依赖多少变量,在调用更新的动作里都不需要依赖任何变量。 具体更新操作在 reducer 函数里写就可以了。
关于组件内部的函数
- 如果某些函数仅在某个
effect
中调用,可以把它们的定义移到effect中 - 如果某些函数不依赖于组件中的任何数据,可以把它们的定义移到组件外部
- 如果这个函数需要通过
props
传给子组件,一般最好使用useCallback
做一下缓存 - 如果这个函数的作用是做大批量的计算,且返回会值需要显示在页面上,最好使用
useMemo
做一下缓存