react-hooks 原理分析

387 阅读8分钟

背景

React Hooks是React 16.8发布以来最吸引人的特性之一。

react-router、redux等主流包,都已经提供了完备的 Hooks api,且官网上的 demo 都已经切换成 Hooks 书写的形式。

Redux 的官方教程里明确指出,要求阅读文档的人需要了解 Hooks 的使用。

antd 团队也完成了底层使用Hooks重写一部分组件的功能。

为什么用hooks

React提供的组件化和自上而下的数据流帮助我们把一个大的UI交互拆分为小的、可重用的、独立的小块。但是,对于一些复杂的组件,因为逻辑依赖没法进一步拆分

类组件进行逻辑复用一般通过HOC和renderProps的形式,出现的问题如:层级嵌套,难以区分props的层级来源,props丢失,逻辑复用时必须渲染ui...

历史的函数组件无法进行状态管理和生命周期函数...

hooks的优点:

  • 写法简单:每一个Hook都是一个函数,因此它的写法十分简单,而且开发者更容易理解。
  • 组合简单:Hook组合起来十分简单,组件只需要同时使用多个hook就可以使用到它们所有的功能。
  • 容易扩展:Hook具有很高的可扩展性,你可以通过自定义Hook来扩展某个Hook的功能。
  • 没有wrapper hell:Hook不会改变组件的层级结构,也就不会有wrapper hell问题的产生。
  • 为函数组件提供的一套api,使得我们的函数式组件能拥有和 Class 一样的功能和特性,进行状态管理生命周期控制、context、ref等

hooks使用和原理

定义全部变量hookStates保存hooks,定义全部hookIndex保存hooks对应的索引。react内部,并不是全局的,而是挂载每个fiber上

let hookStates = []; // 存放所有的状态
let hookIndex = 0; // 表示当前hook

useState

  • 介绍

参数:状态初始值

返回:状态和状态更新函数。执行更新函数时,不会将旧的状态和新的状态合并

初始渲染时,返回的state为初始值。更新函数,接收新的状态值,并将组件的更新渲染加入队列

  • 原理
function useState(initialState) {
  hookStates[hookIndex] = hookStates[hookIndex] || initialState
  let currentIndex = hookIndex
  function setState(newState) {
    hookStates[currentIndex] = newState
    // scheduleUpdate(); // 状态改变后,更新应用 先改变hookStates中的state,然后改变vdom,然后改变真实dom
  }
  return [hookStates[hookIndex++], setState]
}

scheduleUpdate方法,主要用于更新vdom,然后改变真实dom,然后重置hookIndex。

不能在if语句中使用hooks,是因为每个hook有个对应的索引,当放在条件语句时,可能导致索引和hook对应不上

useMemo

  • 介绍

参数:对象等创建函数+依赖项

返回:创建函数执行结果。仅当依赖项变化时才会重新计算memoized值,避免在每次渲染时进行高开销的计算

主要用于缓存对象

  • 使用
function Counter() {
    let [name, setName] = useState('hh');
    let [number, setNumber] = useState(0);
    const addClick = useCallback(() => setNumber(number => number+1),[number]);
    const data = useMemo(() => ({number}), [number]);
    return (
        <div>  
            <input type='text' value={name} onChange={e => setName(e.target.value)}/>
            <Child addClick={addClick} data={data}/>         
        </div>
    )
}
  • 原理
function useMemo(factory, deps) {
  if(hookStates[hookIndex]) {
    let [lastMemo, lastDeps] = hookStates[hookIndex]
    let same = deps.every((item, index) => item === lastDeps[index]) // 新旧依赖一一对比
    if(same) { // 返回上一个memo
      hookIndex++
      return lastMemo
    } else { // 执行factory,创建新的memo
      let newMemo = factory();
      hookStates[hookIndex++] = [newMemo, deps]
      return newMemo
    }
  } else {
    let newMemo = factory();
    hookStates[hookIndex++] = [newMemo, deps]
    return newMemo
  }
}

useCallback

  • 介绍

参数:内联回调函数+依赖项。依赖项应该添加的值:所有effect内部用到的变量和函数

返回:内联回调函数的memoized版本,这个函数仅在依赖项改变时更新

主要用于缓存函数

  • 原理
function useCallback(callback, deps) {
  if(hookStates[hookIndex]) {
    let [lastCallback, lastDeps] = hookStates[hookIndex]
    let same = deps.every((item, index) => item === lastDeps[index]) // 新旧依赖一一对比
    if(same) { // 返回lastCallback
      hookIndex++
      return lastCallback
    } else { // 存放新的callback
      hookStates[hookIndex++] = [callback, deps]
      return callback
    }
  } else {
    hookStates[hookIndex++] = [callback, deps]
    return callback
  }
}

useReducer

  • 介绍

参数:形如 (state, action) => newState 的 reducer + 初始状态

返回:当前state + dispatch

主要用于state更改逻辑较为复杂的场景

  • 使用
let initialState = {number:0};
let ADD = 'ADD';
let MINUS = 'MINUS';
function  reducer(state, action) {
    switch (action.type) {
        case ADD:
            return {number: state.number + 1}
        case MINUS:
            return {number: state.number - 1}    
        default:
            break;
    }
}

function Counter() {
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <div>  
           <p>{state.number}</p>
            <button onClick= {()=> dispatch({type: ADD})}>+</button>
            <button onClick= {()=> dispatch({type: MINUS})}>-</button>
        </div>
    )
}
  • 原理
function useReducer(reducder, initialState) {
  hookStates[hookIndex] = hookStates[hookIndex] || initialState)
  let currentIndex = hookIndex 
  function dispatch(action) { 
    let lastState = hookStates[currentIndex]
    hookStates[hookIndex] =  reducder(lastState, action)
    // scheduleUpdate(); // 更新渲染
  }
  return [hookStates[hookIndex++], dispatch]
}

  • useState为useReducer的语法糖
function useReducer(reducder, initialState) {
  hookStates[hookIndex] = hookStates[hookIndex] || initialState
  let currentIndex = hookIndex 
  function dispatch(action) {
    let lastState = hookStates[currentIndex]
    let nextState
    if(reducder) { // reducer存在时,执行得到新的state
      nextState = reducder(lastState, action) 
    } else {// reducer为null时,相当于action为新的状态值;  
      nextState = typeof action === 'function' ? action(lastState) : action
    }
    hookStates[hookIndex] = nextState
    // scheduleUpdate();
  }
  return [hookStates[hookIndex++], dispatch]
}

----------------------------------------------------

function newUseState(initialState) {
  return useReducer(null, initialState)
}

useEffect

  • 介绍

参数:callback + 依赖项deps。callback会在组件挂载和更新完成后执行。如果deps不存在,callback在每次render后都会执行;如果存在,当依赖项发生变化后,才会执行;如果是个空数组,依赖项即不会改变,相当于只执行了一次

返回:清理函数。会在下次执行useEffect的时候执行

它是个effect hook,给函数组件增加了操作副作用的能力。如果定时器,生命周期等

赋值给useEffect的函数会在组件渲染到屏幕后执行

function useEffect(callback,deps){
  if(hookStates[hookIndex]){
      let [destroyFunc,lastDeps] = hookStates[hookIndex];
      let same = deps && deps.every((item,index)=>item === lastDeps[index]);
      if(same){
        hookIndex++;
      }else{
        destroyFunc && destroyFunc();
        setTimeout(() => {
          let destroyFunc = callback();
          hookStates[hookIndex++] = [destroyFunc, deps]
        });
      }
  }else{ // 第一次渲染 开启一个宏任务,当render后执行callback
    setTimeout(() => {
      let destroyFunc = callback();
      hookStates[hookIndex++] = [destroyFunc, deps]
    });
  }
}

useLayoutEffect

与useEffect相对,useEffect在浏览器渲染完成后执行。但,useLayoutEffect是在dom更新完成浏览器绘制前执行。所以,useLayoutEffect会阻塞浏览器渲染

所有的dom变更 ==> useLayoutEffect ==> painting ==> useEffect

function useLayoutEffect(callback,deps){
  if(hookStates[hookIndex]){
      let [destroyFunc,lastDeps] = hookStates[hookIndex];
      let same = deps && deps.every((item,index)=>item === lastDeps[index]);
      if(same){
        hookIndex++;
      }else{
        destroyFunc && destroyFunc();
        queueMicrotask(() => { // 和useEffect的区别为queueMicrotask,将函数放在微任务队列,微任务队列在绘制前执行
          let destroyFunc = callback();
          hookStates[hookIndex++] = [destroyFunc, deps]
        });
      }
  }else{
    queueMicrotask(() => {
      let destroyFunc = callback();
      hookStates[hookIndex++] = [destroyFunc, deps]
    });
  }
}

useRef

返回的值在组件的整个生命周期内保持不变,用于缓存一个不变的值。类似于类组件的this;当ref.current发生改变时,不会re-render

用途:如:类组件时,可以通过实例拿到属性和方法。函数组件没有自己的实例,因此可以通过ref.current拿到缓存的属性和方法

function useRef(initialState) {
  hookStates[hookIndex] =  hookStates[hookIndex] || { current: initialState };
  return hookStates[hookIndex++];
}

forwardRef + useImperativeHandle

函数组件没有实例,直接在函数组件上使用ref时会报错。forwardRef,创建一个 React 组件,该组件能够将其接收的 ref 属性转发到内部的一个组件中

useImperativeHandle可以让你在使用ref时,自定义暴露给父组件的实例值,避免到子组件误操作

function Child(props,ref){
  const inputRef = useRef();
  useImperativeHandle(ref,()=>(
    {
    	focus(){ inputRef.current.focus();},
    }
  ));
  return (
    <input type="text" ref={inputRef}/>
  )
}
Child = forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0); 
  const inputRef = useRef();
  function getFocus(){
    inputRef.current.focus();
    inputRef.current.remove(); // 不会执行
  }
  return (
      <>
        <Child ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

Fiber上的hooks实现

链表结构

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。

每个结点包括两个部分:数据和指针

LinkedList类包含三个属性:firstUpdate,指向第一个节点;lastUpdate,指向链表的最后一个节点;nextUpdate指向下个节点

image.png

// 表示一个节点
class Update {
  constructor(payload, nextUpdate) {
    // payload挂载数据,nextUpdate指向下一个节点
    this.payload = payload
    this.nextUpdate = nextUpdate
  }
}
// 模拟链表
class UpdateQueue {
  constructor() {
    this.firstUpdate = null
    this.lastUpdate = null
  }
  
  enqueueUpdate(update) { // 将更新放在队列中
    if (!this.firstUpdate) {
      this.firstUpdate = this.lastUpdate = update
    } else {
      this.lastUpdate.nextUpdate = update
      this.lastUpdate = update
    }
  }
  
  forceUpdate() { // forceUpdate将所有节点挂载的数据合并
    let currentState = this.baseState || {}
    let currentUpdate = this.firstUpdate
    while(currentUpdate) {
      const nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
      currentState = {
        ...currentState,
        ...nextState
      }
      currentUpdate = currentUpdate.nextUpdate
    }
    this.firstUpdate = this.lastUpdate = null
    return this.baseState = currentState
  }
}

模拟useReducer

let workInProgressFiber = null; //正在工作中的fiber
let hookIndex = 0; //hooks索引             
// updateFunctionComponent: 每次render时,对函数组件进行调度,并进行hooks和hookIndex的初始化,给当前fiber添加hooks数组
function updateFunctionComponent(currentFiber) {
    workInProgressFiber = currentFiber; // 初始化workInProgressFiber,hooks和hookIndex,在执行hooks是使用
    hookIndex = 0;
    workInProgressFiber.hooks = [];
    const newChildren = [currentFiber.type(currentFiber.props)];
    reconcileChildren(currentFiber, newChildren);
}
// useReducer: 首次render时,将hook存到hooks数组中;再次render时,根据索引获取上次hook,执行forceUpdate更新state
function useReducer(reducer, initialValue) {
    let newHook = workInProgressFiber.alternate.hooks[hookIndex]; // 上次fiber🌲上的hooks数组--对应索引
    if (newHook) {
        newHook.state = newHook.updateQueue.forceUpdate(newHook.state);
    } else {
        newHook = {
            state: initialValue,
            updateQueue: new UpdateQueue()
        };
    }
    const dispatch = action => { // 执行dispatch时,在链表上增加update对象
        newHook.updateQueue.enqueueUpdate(
            new Update(reducer ? reducer(newHook.state, action) : action)
        );
        scheduleRoot(); // 重新调度
    }
    workInProgressFiber.hooks[hookIndex++] = newHook;
    return [newHook.state, dispatch];
}
function useState(initState) {
    return useReducer(null, initState)
}

思考题

function useLoading() {
  const [loading, setLoading] = useState(false);
  useEffect(() => {
    setLoading(true);
  }, [])

  return loading;
}

function useRefObject() {
  const ref = useRef();
  const [ready, setReady] = useState(false);

  useEffect(() => {
    if(ref.current) {
      setReady(true);
    }
  }, [ref])

  return [ref, ready];
}

const App = (props) => {
  const loading = useLoading();
  const [ref, ready] = useRefObject();

  return (
    <div>
      <div>ref with useEffect</div>
        {loading && <div ref={ref}>
          {ready.toString()}
          期望的结果是true,但真实的结果是?为什么?怎么得到期望的?
        </div>}
    </div>
  )
}

参考: useEffect与ref对象使用指南

Hooks snippet 模板

理解JavaScript中的数据结构(链表) 推荐使用eslint-plugin-react-hooks