一、什么是Hooks?
React一直都提倡使用函数组件,但是有时候需要使用state或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有。Hooks是React 16.8新增的特性,它可以让你在不编写class的情况下使用state以及其他的React特性。- 如果你在编写函数组件并意识到需要向其添加一些
state,以前的做法是必须将其它转化为class。现在你可以直接在现有的函数组件中使用 Hooks。 - 凡是
use开头的React API都是Hooks。
二、Hooks 解决的问题
- 类组件的不足
-
-
- 状态逻辑难以复用:在组件之间复用状态逻辑很难也很复杂,可能要用到
render props(渲染属性)或者是HOC(高阶组件),但这两种都需要在最外层包裹一层父容器,导致层级冗余。 - 趋向复杂难以维护
- 在生命周期中混杂一些可能不相干的逻辑,如在
componentDidMount中注册事件或者是其他逻辑,在componentWillUnmont中卸载事件,这样分散不集中的写法,就很容易写出bug。 - 类组件中到处都是对
state的访问处理,导致组件难以拆分成更小的组件。 this指向问题:父组件向子组件传递函数时必须绑定this。
- 状态逻辑难以复用:在组件之间复用状态逻辑很难也很复杂,可能要用到
-
react类组件绑定this的四种方法:
前提:子组件内部做了性能优化,如React.PureComponent
- 第一种是在构造函数中绑定
this:那么每次父组件刷新的时候,如果传递给子组件其他的props值不变,那么子组件就不会刷新; - 第二种是在 render函数里面绑定
this:因为bind函数会返回一个新的函数,所以每次父组件刷新时,都会重新生成一个函数,即使父组件传递给子组件其他的props值不变,子组件每次都会刷新; - 第三种是使用箭头函数:父组件刷新的时候,即使两个箭头函数的函数体是一样的,都会生成一个新的箭头函数,所以子组件每次都会刷新;
- 第四种是使用类的静态属性:原理和第一种方法差不多,比第一种更简洁;
综上所述,如果不注意的话,很容易写成第三种写法,导致性能上有所损耗。
- Hooks的优势
-
-
- 能优化类组件的三大问题;
- 能在无需修改组件结构的情况下复用状态逻辑(自定义
Hooks); - 能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据);
- 副作用的关注点分离:副作用指那些没有发生在数据向视图转换过程中的逻辑,如
ajax请求、访问原生dom元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。而useEffect在全部渲染完毕后才会执行,useLayoutEffect会在浏览器layout之后,painting之前执行;
-
三、注意事项
- 只能在函数内部的最外层调用
Hook,不要在循环、条件判断或者子函数中调用; - 只能在
React的函数组件中调用Hook,不要在其他JavaScript函数中调用;
四、React Hooks API
- useState
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
state = { name: 'zhangsan', age: 18, };
this.setState({ name: 'lisi' });
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}> Click me </button>
</div>
);
}
我们在日常开发中可以声明多个state,useState也并不是只能接收简单number,string,boolean作为初始值,同时也可以接收复杂的object。
注意:
- 使用规则:必须放在函数组件的顶层,不允许放在循环,条件或者嵌套函数里。我们必须确保组件在每一次渲染时候都按照同样的顺序去调用
Hooks;为什么?因为React是根据调用顺序将单个state和关联的useState对应起来。 - 初始值使用问题:初始值只要在首次渲染的时候才会被使用,再次渲染的时候将会取用当前
state的值进行赋值操作。所以我们在使用useState时需避免使用props的变量进行初始化。 - 与
setState的差异:setState是对结果进行合并,而useState是直接将值进行替换。
// ------------ // 首次渲染 // ------------
// 1. 使用 'Mary' 初始化变量名为 name 的 state
const [name, setName] = useState('Mary')
// 2. 添加 effect 以保存 form 操作
useEffect(persistForm)
// 3. 使用 'Poppins' 初始化变量名为 surname 的 state
const [surname, setSurname] = useState('Poppins')
// 4. 添加 effect 以更新标题 useEffect(updateTitle)
// ------------- // 二次渲染 // -------------
// 1. 读取变量名为 name 的 state(参数被忽略)
const [name, setName] useState('Mary')
// 2. 替换保存 form 的 effect
useEffect(persistForm)
// 3. 读取变量名为 surname 的 state(参数被忽略)
const [surname, setSurname] = useState('Poppins')
// 4. 替换更新标题的 effect
useEffect(updateTitle)
- useEffect
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
return () => {
// do something
}
}, [state, props]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}> Click me </button>
</div>
);
}
注意:
- 默认情况下,
useEffect在第一次渲染之后和每次更新之后都会执行;
useEffect接收两个参数,第一个参数的是fn(不能是async函数),第二个参数是一个array。第二个参数是useEffect的依赖项,即当依赖发生变化时才会执行该副作用。
每个useEffect的第一个fn参数都可以有一个返回函数,除了第一次渲染外,每次执行useEffect都会先执行返回函数再执行函数体的逻辑。如果 useEffect 第一个参数传入async,返回值则变成了 Promise,会导致 react在调用销毁函数的时候报错 :function.apply is undefined
useEffect(() => {
console.log('useEffect1')
return () => { console.log('useEffect1 卸载了') }
});
useEffect(() => {
console.log('useEffect2')
return () => { console.log('useEffect2 卸载了') }
});
useEffect(() => {
console.log('useEffect3')
return () => { console.log('useEffect3 卸载了') }
} , []);
思考一下上面的执行顺序?
更新阶段
- 执行新的的
useEffect函数 , 并将effect函数存入队列等待执行; - 执行返还函数队列, 并观察返还函数书否有依赖参数, 有依赖参数, 追踪依赖参数是否改变, 改变执行, 没有改变不执行;
- 执行
effect函数队列, 观察effect函数是否有依赖参数,有依赖参数, 追踪依赖参数是否改变, 改变执行, 没有改变不执行;
- useContext
const themes = {
light: { foreground: "#000000", background: "#eeeeee" },
dark: { foreground: "#ffffff", background: "#222222" }
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div> <ThemedButton /> </div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
了解useContext之前,可以先了解一下Context
Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法 Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
需要注意点是:组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的value时,消费组件的defaultValue不会生效。
- useReducer
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
第一个参数:reducer函数。第二个参数:初始化的state。返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)
- useCallback
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
前提知识:我们知道每次组件渲染时这个自定义handle函数时都是重新创建的一个新函数。我们将这个函数传递给子组件后,因为handle函数的引用地址变了,导致子组件使用PureComponent、shouldComponentUpdate、React.memo等相关优化失效。
在a和b的变量值不变的情况下,memoizedCallback的引用不变。即:useCallback的第一个入参函数会被缓存,从而达到渲染性能优化的目的。
- useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
在a和b的变量值不变的情况下,memoizedValue的值不变。即:useMemo函数的第一个入参函数不会被执行,从而达到节省计算量的目的
两者差异:两者相似99%,useCallback和useMemo都可缓存函数的引用或值,但是从更细的使用角度来说useCallback缓存函数的引用,useMemo缓存计算数据的返回值。