一、React Hook 的基本介绍
Hook 的含义
Hook 这个单词的意思是"钩子"。
React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。
你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。
所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用use前缀命名,便于识别。你要使用 xxx 功能,钩子就命名为 usexxx。
React 默认常用的钩子:
- useState()
- useContext()
- useReducer()
- useEffect()
- useMomo()
- useCallback()
Hook 引入的原因
React Hooks 是 React 16.8 引入的新特性,允许我们在不使用 Class 的前提下使用生命周期、 state 和其他特性。
React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 之后的第三种状态逻辑复用方案,不会产生 JSX 嵌套地狱问题。1
二、在React Hook 之前
React mixin
React mixin 是通过React.createClass创建组件时使用的,现在主流是通过ES6方式创建react组件,官方因为mixin不好追踪变化以及影响性能,所以放弃了对其支持,同时也不推荐使用。这里简单介绍下mixin。
mixin的原理其实就是将[mixin]里面的方法合并到组件的prototype上。
var logMixin = {
alertLog:function(){
alert('alert mixin...')
},
componentDidMount:function(){
console.log('mixin did mount')
}
}
var MixinComponentDemo = React.createClass({
mixins:[logMixin],
componentDidMount:function(){
document.body.addEventListener('click',()=>{
this.alertLog()
})
console.log('component did mount')
}
})
// 打印如下
// component did mount
// mixin did mount
// 点击页面
// alert mixin
可以看出来mixin就是将logMixn的方法合并到MixinComponentDemo组件中,如果有重名的生命周期函数都会执行(render除外,如果重名会报错)。但是由于mixin的问题比较多这里不展开讲。
高阶组件
组件是 React 中代码复用的基本单元。但你会发现某些模式并不适合传统组件。
function logTimeHOC(WrappedComponent,options={time:true,log:true}){
return class extends React.Component{
constructor(props){
super(props);
this.state = {
index: 0
}
this.show = 0;
}
componentDidMount(){
options.time&&this.timer = setInterval(()=>{
this.setState({
index: ++index
})
},1000)
options.log&&console.log('组件渲染完成----')
}
componentDidUpdate(){
options.log&&console.log(`我背更新了${++this.show}`)
}
componentWillUnmount(){
this.timer&&clearInterval(this.timer)
options.log&&console.log('组件即将卸载----')
}
render(){
return(<WrappedComponent {...this.state} {...this.props}/>)
}
}
}
class InnerLogComponent extends React.Component{
render(){
return(
<div>我是打印日志组件</div>
)
}
}
// 使用高阶组件`logTimeHOC`包裹下
export default logTimeHOC(InnerLogComponent,{log:true})
class InnerSetTimeComponent extends React.Component{
render(){
return(
<div>
<div>我是计时组件</div>
<span>{`我显示了${this.props.index}s`}</span>
</div>
)
}
}
// 使用高阶组件`logTimeHOC`包裹下
export default logTimeHOC(InnerSetTimeComponent,{time:true})
上面就实现了简单的日志和计时器组件。
这样不仅复用了业务逻辑提高了开发效率,同时还方便后期维护。当然上面的案例只是为了举例而写的案例,实际场景需要自己去合理抽取业务逻辑。高阶组件虽然很好用,但是也有一些自身的缺陷:
- 高阶组件的props都是直接透传下来,无法确实子组件的props的来源。
- 可能会出现props重复导致报错。
- 组件的嵌套层级太深。
- 会导致ref丢失。
React Hook
上面例子可以看出来,虽然解决了功能复用但是也带来了其他问题。
由此官方带来React Hook,它不仅仅解决了功能复用的问题,还让我们以函数的方式创建组件,摆脱Class方式创建,从而不必在被this的工作方式困惑,不必在不同生命周期中处理业务。
import React,{ useState, useEffect } from 'react'
function useLogTime(data={log:true,time:true}){
const [count,setCount] = useState(0);
useEffect(()=>{
data.log && console.log('组件渲染完成----')
let timer = null;
if(data.time){
timer = setInterval(()=>{setCount(c=>c+1)},1000)
}
return ()=>{
data.log && console.log('组件即将卸载----')
data.time && clearInterval(timer)
}
},[])
return {count}
}
我们通过React Hook的方式重新改写了上面日志时间记录高阶组件。可以重写上面的组件:
export default function LogComponent(){
useLogTime({log:true})
return(
<div>我是打印日志组件</div>
)
}
export default function SetTimeComponent (){
const {count} = useLogTime({time:true})
return(
<div>
<div>我是计时组件</div>
<span>{`我显示了${count}s`}</span>
</div>
)
}
用React Hook实现的这三个组件和高阶组件一比较,代码会更简洁一点。
三、React Hook 生产应用
模拟React的生命周期
- constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。
- componentDidMount:通过 useEffect 传入第二个参数为[]实现。
- componentDidUpdate:通过 useEffect 传入第二个参数为空或者为值变动的数组。
- componentWillUnmount:主要用来清除副作用。通过 useEffect 函数 return 一个函数来模拟。
- shouldComponentUpdate:你可以用 React.memo 包裹一个组件来对它的 props 进行浅比较。来模拟是否更新组件。
- componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。
自定义hook
我们一个程序会有多个组件,很多组件都会有请求接口的逻辑,不能每个需要用到这个逻辑的时候都重新写或者Ctrl+C。所以我们需要将这个逻辑抽离出来作为一个公共的Hook来调用,那么我们就要用到自定义Hook。
function useFetchHook(config, watch) {
const [data, setData] = useState(null);
const [status, setStatus] = useState(0);
useEffect(
() => {
const fetchData = async () => {
try {
const result = await axios(config);
setData(result.data);
setStatus(1);
} catch (err) {
setStatus(2);
}
};
fetchData();
},
watch ? [watch] : []
);
return { data, status };
}
网上有很多第三方的插件已经包装好了自定义hook,比如 ahooks.js。
提高性能的操作
function App(){
const buttonClick = useCallback(
() => { console.log('do something'),[]
)
return(
<div>
<Button onClick={ buttonClick } />
</div>
)
}
直接用useCallback生成一个记忆函数,这样更新时就不会发生渲染了。在react Hook 中 还有一个useMemo也能实现同样的效果。
四、React Hook 的原理
接下来通过一些例子来理解Hook的原理2。
实现useState
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(`update--${count}`)
},[count])
return(
<div>
<button onClick={()=>setCount(count+1)}>
{`当前点击次数:${count}`}
</button>
</div>
)
}
function useState(initialValue) {
var state = initialValue;
function setState(newState) {
state = newState;
render();
}
return [state, setState];
}
不出意外当我们点击页面上的按钮时候,按钮中数字并不会改变;看控制台中每次点击都会输出0,说明useState是执行了。由于val是在函数内部被声明的,每次useState都会重新声明val从而导致状态无法被保存,因此我们需要将val放到全局作用域声明。
let val; // 放到全局作用域
function useState(initVal) {
val = val|| initVal; // 判断val是否存在 存在就使用
function setVal(newVal) {
val = newVal;
render(); // 修改val后 重新渲染页面
}
return [val, setVal];
}
修改useState后,点击按钮时按钮就发生改变了。
实现useEffect
useEffect是一个函数,有两个参数一个是函数,一个是可选参数-数组,根据第二个参数中是否有变化,来判断是否执行第一个参数的函数:
let watchArr; // 为了记录状态变化 放到全局作用域
function useEffect(fn,watch){
// 判断是否变化
const hasWatchChange = watchArr?
!watch.every((val,i)=>{ val===watchArr[i] }):true;
if( hasWatchChange ){
fn();
watchArr = watch;
}
}
打开测试页面每次点击按钮,控制台会打印当前更新的count;到目前为止,我们模拟实现了useState和useEffect可以正常工作了。不知道大家是否还记得我们通过全局变量来保证状态的实时更新;如果组件中要多次调用,就会发生变量冲突的问题,因为他们共享一个全局变量。如何解决这个问题呢?
解决同时调用多个 useState useEffect的问题
let memoizedState = [];
let currentCursor = 0;
function useState(initVal) {
memoizedState[currentCursor] = memoizedState[currentCursor] || initVal;
function setVal(newVal) {
memoizedState[currentCursor] = newVal;
render();
}
// 返回state 然后 currentCursor+1
return [memoizedState[currentCursor++], setVal];
}
function useEffect(fn, watch) {
const hasWatchChange = memoizedState[currentCursor]
? !watch.every((val, i) => val === memoizedState[currentCursor][i])
: true;
if (hasWatchChange) {
fn();
memoizedState[currentCursor] = watch;
currentCursor++; // 累加 currentCursor
}
}
修改核心是将useState,useEffect按照调用的顺序放入memoizedState中,每次更新时,按照顺序进行取值和判断逻辑。
将hook依次存入数组memoizedState中,每次存入时都是将当前的currentcursor作为数组的下标,将其传入的值作为数组的值,然后在累加currentcursor,所以hook的状态值都被存入数组中memoizedState。
由此可以得知,使用 Hooks 的注意事项:
- 不要在循环,条件或嵌套函数中调用 Hooks。
- 只在 React 函数中调用 Hooks。
因为我们是根据调用hook的顺序依次将值存入数组中,如果在判断逻辑循环嵌套中,就有可能导致更新时不能获取到对应的值,从而导致取值混乱。同时useEffect第二个参数是数组,也是因为它就是以数组的形式存入的。
实现useReducer
useReducer的使用
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return {total: state.total + 1};
case 'decrement':
return {total: state.total - 1};
default:
throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, { count: 0});
它的实现只需要结合 useState 和 reducer 就好了。
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
const update = (state, action) => {
const result = reducer(state, action);
setState(result);
}
const dispatch = update.bind(null, state);
return [state, dispatch];
}
实现useMemo & useCallback
这两个都用通过依赖用来优化提高 React 性能的,针对处理一些消耗大的计算。useMemo 会返回一个值,而 useCallback 会返回一个函数。
useMemo
function useMemo(fn, deps) {
const hook = memoizedState[currentCursor];
const _deps = hook && hook._deps;
const hasChange = _deps ? !deps.every((v, i) => _deps[i] === v) : true;
const memo = hasChange ? fn() : hook.memo;
memoizedState[currentCursor++] = {_deps: deps, memo};
return memo;
}
useCallback
function useCallback(fn, deps) {
return useMemo(() => fn, deps);
}
总结
做一个简单的总结,虽然我们用数组基本实现了一个可用的 Hooks,了解了 Hooks 的原理,但在 React 中,是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook3。
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null,// UpdateQueue 通过
next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
}
type Effect = {
tag: HookEffectTag, // effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
create: () => mixed, // 初始化 callback
destroy: (() => mixed) | null, // 卸载 callback
deps: Array<mixed> | null,
next: Effect, // 同上
};
memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?
我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上。
组件中的hook利用闭包来保存状态,使用链表保存一系列 Hooks,将链表中的第一个 Hook 与 Fiber 关联。在 Fiber 树更新时,就能从 hooks 中计算出最终输出的状态和执行相关的副作用。
接下来就真正来看useState的源码。
五、真正的 React 实现
useState的实现
//myReact.js
import { useState } from 'react';
//react\src\index.js
export { useState } from './src/React';
//react\src\React.js
import { useState } from './ReactHooks';
//react\src\ReactHooks.js
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
//react\src\ReactCurrentDispatcher.js
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
//react-reconsiler\src\ReactInternalTypes.js
export type Dispatcher = ...
找到这里发现居然是一个type,这肯定不对,全文搜索Dispatcher关键字,最终
//react-reconciler\src\ReactFiberHooks.new.js
const HooksDispatcherOnMount: Dispatcher = {
useState: mountState,
};
const HooksDispatcherOnUpdate: Dispatcher = {
useState: updateState,
};
const HooksDispatcherOnRerender: Dispatcher = {
useState: rerenderState,
};
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
){
...
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
...
// Check if there was a render phase update
if (didScheduleRenderPhaseUpdateDuringThisPass) {
let numberOfReRenders: number = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
...
ReactCurrentDispatcher.current = __DEV__
? HooksDispatcherOnRerenderInDEV
: HooksDispatcherOnRerender;
...
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}
}
根据是否是第一次渲染调用不同的实现。
renderWithHooks是在Fiber中根据类型是 FunctionComponent时调用的。这里先不管Fiber的整个流程。
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
终于找到了真正的 useState。
useState做了什么
const [a, setA] = useState(“a”);
const [b, setB] = useState(“b”);
const [c, setC] = useState(“c”);
根据源码来看,他生成了下面这样一个链表,然后返回了初始state(如果是函数则是计算结果)和一个叫dispatchAction的函数。
setXXX 做了什么
通常使用的setXXX就是调用的dispatchAction函数。
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// Append the update to the end of the list.
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
...
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
讲 setXXX放入循环链表队列,然后等待执行。
如果我们按以下顺序调用
setA(“a1);
setA(“a2);
setA(“a3);
将会生成如下的链表,然后等待Fiber调度重新渲染。
state是怎么被更新的
现在就等Fiber调度更新了,我们知道他会再次调用renderWithHooks,但是这次会使用HooksDispatcherOnUpdate的实现,因此源码如下:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
let baseQueue = current.baseQueue;
// 把上图中的pending链表挂载到baseQueue上,pending链表置空
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 把未被处理的更新也放入到baseQueue上
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
// 把链表中的所有更新依次执行完成
do {
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
...
} else {
...
// 处理更新
if (update.eagerReducer === reducer) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
//reducer会判断是否是函数还是值,如果传入setXXX的是函数,则进行计算结果
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
...
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
//返回新的state值和缓存的dispatch函数
return [hook.memoizedState, dispatch];
}
六、hook相关的面试题
- useState为什么返回数组
- class与hook的区别
- 自定义hook什么时候使用
- hook的闭包、链表
- hook的原理
参考链接:
深入 React hooks — 原理 & 实现
React Hooks 入门教程
React Hooks 原理与最佳实践
29行代码深入React Hooks原理
Web高级 React useState底层结构