React Hook的实现原理

一、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相关的面试题

  1. useState为什么返回数组
  2. class与hook的区别
  3. 自定义hook什么时候使用
  4. hook的闭包、链表
  5. hook的原理

参考链接:
深入 React hooks — 原理 & 实现
React Hooks 入门教程
React Hooks 原理与最佳实践
29行代码深入React Hooks原理
Web高级 React useState底层结构


  1. JSX 嵌套地狱问题 ↩︎

  2. 干货 | React Hook的实现原理和最佳实践 ↩︎

  3. 真正的 React 实现 ↩︎

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值