乐趣区

Preact逐行解析hooks源码

前言

Preact 是什么?React 的 3kb 轻量化方案,拥有同样的 ES6API

虽然 Preact 和 React 有着相同的 API, 但是其内部实现机制的差异依然是巨大。但是这并不妨碍我们阅读以及学习 Preact 的源码。说一句题外话,今年年初的时候,我的一位哥们 @小寒,在北京某家公司面试时遇到了来自 Facebook 的大牛,这位 Facebook 的大牛也曾推荐过他,阅读学习 Preact 的源码。

hooks 不是什么魔法,hooks 的设计也与 React 无关(Dan Abramov)。在 Preact 中也是如此,所以即使你没有阅读过 Preact 或者 React 源码,也不妨碍理解 hooks 的实现。

希望下面的分享,对大家理解 hooks 背后的实现能有所启示。

关于 hooks 的规则

React 中 hooks 的使用规则如下。我们可以看出 hooks 的使用,高度的依赖执行顺序。在阅读完源码后,我们就会知道,为什么 hooks 的使用会有这两条规则。

  1. ✅ 只在最顶层使用 hook。不要在循环,条件或嵌套函数中调用 hook。
  2. ✅ 不要在普通的 JavaScript 函数中调用 Hook。

hooks 源码解析

getHookState

getHookState函数,会在 当前组件 的实例上挂载 __hooks 属性。__hooks为一个对象,__hooks对象中的 _list 属性使用 数组 的形式,保存了所有类型 hooks(useState, useEffect…………) 的执行的结果,返回值等。因为 _list 属性是使用数组的形式存储状态,所以每一个 hooks 的执行顺序尤为重要。

function getHookState(index) {if (options._hook) options._hook(currentComponent);
  // 检查组件,是否有__hooks 属性,如果没有,主动挂载一个空的__hooks 对象
  const hooks =
    currentComponent.__hooks ||
    (currentComponent.__hooks = {_list: [], // _list 中存储了所有 hooks 的状态
      _pendingEffects: [], // _pendingEffects 中存储了 useEffect 的 state
      _pendingLayoutEffects: [], // _pendingLayoutEffects 中存储了 useLayoutEffects 的 state
      _handles: []});
  // 根据索引 index。判断__hooks._list 数组中,是否有对应的状态。// 如果没有, 将主动添加一个空的状态。if (index >= hooks._list.length) {hooks._list.push({});
  }
  // 返回__hooks._list 数组中,索引对应的状态
  return hooks._list[index];
}

一些需要使用到的关键全局变量

getHookState 中,我们使用了全局变量 currentComponent。变量currentComponent 指向的是当前的组件的实例。我们是如何拿到当前组件实例的引用的呢?结合 hooks 的源码以及 preact 源码后发现,当 preact 进行 diff 时,会将当前组件的虚拟节点 VNode,传递给 options._render 函数,这样我们就可以顺利获取当前组件的实例了。

// 当前 hooks 的执行顺序指针
let currentIndex;

// 当前的组件的实例
let currentComponent;

let oldBeforeRender = options._render;

// vnode 是
options._render = vnode => {if (oldBeforeRender) oldBeforeRender(vnode);
  // 当前组件的实例
  currentComponent = vnode._component;
  // 重置索引,每一个组件 hooks state list 从 0 开始累加
  currentIndex = 0;

  if (currentComponent.__hooks) {
    currentComponent.__hooks._pendingEffects = handleEffects(currentComponent.__hooks._pendingEffects);
  }
};
// 省略后的 diff 方法
function diff() {
  let tmp, c;

  // ...

  // 在 VNode 上挂载当前组件的实例
  newVNode._component = c = new Component(newProps, cctx);

  // ...

  // 将 VNode 传递给 options._render 函数, 这样我们就可以拿到当前组件的实例
  if ((tmp = options._render)) tmp(newVNode);
}

useState && useReducer

useState

useState是基于 useReducer 的封装。详情请看下面的useReducer

// useState 接受一个初始值 initialState,初始化 state
function useState(initialState) {return useReducer(invokeOrReturn, initialState);
}
invokeOrReturn

invokeOrReturn是一个简单的工具函数,这里不作赘述。

function invokeOrReturn(arg, f) {return typeof f === "function" ? f(arg) : f;
}

useReducer

useReducer接受三个参数。reducer负责处理 dispatch 发起的 actioninitialStatestate状态的初始值,init是惰性化初始值的函数。useReducer返回 [state, dispatch] 格式的内容。

function useReducer(reducer, initialState, init) {
  // currentIndex 自增一,创建一个新的状态,状态会存储在 currentComponent.__hooks._list 中
  const hookState = getHookState(currentIndex++);

  if (!hookState._component) {
    // state 存储当前组件的引用
    hookState._component = currentComponent;

    hookState._value = [
      // 如果没有指定第三个参数 `init, 返回 initialState
      // 如果指定了第三个参数,返回,经过惰性化初始值的函数处理的 initialState

      // `useState` 是基于 `useReducer` 的封装。// 在 `useState` 中,hookState._value[0],默认直接返回 initialState
      !init ? invokeOrReturn(null, initialState) : init(initialState),

      // hookState._value[1],接受一个 `action`, {type: `xx`}
      // 由于 `useState` 是基于 `useReducer` 的封装,所以 action 参数也可能是一个新的 state 值,或者 state 的更新函数作为参数
      action => {
        // 返回新的状态值
        const nextValue = reducer(hookState._value[0], action);
        // 使用新的状态值,更新状态
        if (hookState._value[0] !== nextValue) {hookState._value[0] = nextValue;
          // ⭐️调用组件的 setState, 重新进行 diff 运算(在 Preact 中,diff 的过程中会同步更新真实的 dom 节点)hookState._component.setState({});
        }
      }
    ];
  }

  // 对于 useReduer 而言, 返回[state, dispath]
  // 对于 useState 而言,返回[state, setState]
  return hookState._value;
}

⭐️useEffect

useEffect 可以让我们在函数组件中执行副作用操作。事件绑定,数据请求,动态修改 DOM。useEffect 将会在每一次 React 渲染之后执行。无论是初次挂载时,还是更新。useEffect 可以返回一个函数,当 react 进行清除时, 会执行这个返回的函数。每当执行本次的 effect 时,都会对上一个 effect 进行清除。组件卸载时也会执行进行清除。

function useEffect(callback, args) {
  // currentIndex 自增 1,向 currentComponent.__hooks._list 中增加一个新的状态
  const state = getHookState(currentIndex++);

  // argsChanged 函数,会检查 useEffect 的依赖是否发生了变化。// 如果发生了变化,argsChanged 返回 true,会重新执行 useEffect 的 callback。// 如果没有变化,argsChanged 返回 false,不执行 callback
  // 在第一次渲染中,state._args 等于 undefined 的,argsChanged 直接返回 true
  if (argsChanged(state._args, args)) {

    state._value = callback;
    // 在 useEffect 的 state 中保存上一次的依赖,下一次会使用它进行比较
    state._args = args;

    // 将 useEffect 的 state 存储到__hooks._pendingEffects 中
    currentComponent.__hooks._pendingEffects.push(state);

    // 把需要执行 useEffect 的 callback 的组件,添加到到 afterPaintEffects 数组中暂时保存起来
    // 因为我们需要等待渲染完成后,执行 useEffect 的 callback
    afterPaint(currentComponent);
  }
}

argsChanged

argsChanged 是一个简单的工具函数, 用来比较两个数组之间的差异。如果数组中每一项相等返回 false,如果有一项不相等返回 true。主要用途是比较 useEffect,useMemo 等 hooks 的依赖。

function argsChanged(oldArgs, newArgs) {return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
}

afterPaint

afterPaint函数,负责将需要执行 useEffect 的 callback 的 componennt,push 到全局 afterPaintEffects 数组中。

let afterPaintEffects = [];

let afterPaint = () => {};

if (typeof window !== "undefined") {
  let prevRaf = options.requestAnimationFrame;
  afterPaint = component => {  
    if (
      // _afterPaintQueued 属性,确保了每一个 component 只能被 push 一次到 afterPaintEffects 中
      (!component._afterPaintQueued &&
        (component._afterPaintQueued = true) &&
        // afterPaintEffects.push(component) === 1,确保了在清空前 `safeRaf` 只会被执行一次
        // 将 component 添加到 afterPaintEffects 数组中
        afterPaintEffects.push(component) === 1) ||
      prevRaf !== options.requestAnimationFrame
    ) {
      prevRaf = options.requestAnimationFrame;
      // 执行 safeRaf(flushAfterPaintEffects)
      (options.requestAnimationFrame || safeRaf)(flushAfterPaintEffects);
    }
  };
}

safeRaf

safeRaf会开启一个requestAnimationFrame,它会在 diff(在 Preact 中的 diff 是同步的过程,相当于一个宏任务)完成后,调用flushAfterPaintEffects,处理 useEffect 的 callback。

const RAF_TIMEOUT = 100;

function safeRaf(callback) {const done = () => {clearTimeout(timeout);
    cancelAnimationFrame(raf);
    setTimeout(callback);
  };
  const timeout = setTimeout(done, RAF_TIMEOUT);
  // diff 过程是同步的,requestAnimationFrame 将会在 diff 完成后(宏任务完成后)执行
  const raf = requestAnimationFrame(done);
}

flushAfterPaintEffects

flushAfterPaintEffects负责处理 afterPaintEffects 数组中的所有组件

function flushAfterPaintEffects() {
  // 循环处理 afterPaintEffects 数组中,所有待处理的 component
  afterPaintEffects.some(component => {
    component._afterPaintQueued = false;
    if (component._parentDom) {
      // 使用 handleEffects 清空 currentComponent.__hooks._pendingEffects 中所有的 useEffect 的 state
      // handleEffects 会进行清除 effect 和执行 effect 的逻辑
      // handleEffects 最后会返回一个空数组,重置 component.__hooks._pendingEffects
      component.__hooks._pendingEffects = handleEffects(component.__hooks._pendingEffects);
    }
  });
  // 清空 afterPaintEffects
  afterPaintEffects = [];}

handleEffects

清除和执行组件的useEffect

function handleEffects(effects) {
  // 清除 effect
  effects.forEach(invokeCleanup);
  // 执行所有的 effect
  effects.forEach(invokeEffect);
  return [];}
invokeCleanup
// 执行清除 effect
function invokeCleanup(hook) {if (hook._cleanup) hook._cleanup();}
invokeEffect
function invokeEffect(hook) {const result = hook._value();
  // 如果 useEffect 的 callback 的返回值是一个函数
  // 函数会被记录到 useEffect 的_cleanup 属性上
  if (typeof result === "function") {hook._cleanup = result;}
}

useMemo && useCallback

useMemo会返回一个 memoized 值。useCallback会返回一个 memoized 回调函数。useMemo会在依赖数组发生变化的时候,重新计算 memoized 值。useCallback会在依赖数组发生变化的时候,返回一个新的函数。

useMemo


function useMemo(callback, args) {
  // currentIndex 自增 1,向 currentComponent.__hooks._list 中增加一个新的状态
  const state = getHookState(currentIndex++);
  // 判断依赖数组是否发生变化
  // 如果发生了变化,会重新执行 callback,返回新的返回值
  // 否则返回上一次的返回值
    if (argsChanged(state._args, args)) {
        state._args = args;
    state._callback = callback;
    // state._value 记录上一次的返回值(对于 useCallback 而言,记录上一次的 callback)return state._value = callback();}
  // 返回 callback 的返回值
    return state._value;
}

useCallback

useCallback是基于 useMemo 的封装。只有当依赖数组产生变化时,useCallback才会返回一个新的函数,否则始终返回第一次的传入 callback。


function useCallback(callback, args) {return useMemo(() => callback, args);
}

useRef

useRef同样是是基于 useMemo 的封装。但不同的是,依赖数组传入的是一个空数组,这意味着,每一次 useRef 都会重新计算。


function useRef(initialValue) {return useMemo(() => ({current: initialValue}), []);
}

useRef 的应用

⭐️正是因为 useRef 每一次都会重新计算,我们可以利用特性,避免闭包带来的副作用


// 会打印出旧值
function Bar () {const [ count, setCount] = useState(0)

  const showMessage = () => {console.log(`count: ${count}`)
  }

  setTimeout(() => {
    // 打印的出的依然是 `0`, 形成了闭包
    showMessage()}, 2000)

  setTimout(() => {setCount((prevCount) => {return prevCount + 1})
  }, 1000)

  return <div/>
}


// 利用 useRef 会打印出新值
function Bar () {const count = useRef(0)

  const showMessage = () => {console.log(`count: ${count.current}`)
  }

  setTimeout(() => {
    // 打印的出的是新值 `1`,count.current 拿到的是最新的值
    showMessage()}, 2000)

  setTimout(() => {count.current += 1}, 1000)

  return <div/>
}

useLayoutEffect

useEffec t 会在 diff 算法完成对 dom 渲染后执行。与 useEffect 不同的是,useLayoutEffect会在 diff 算法完成对 dom 更新之后,浏览器绘制之前 的时刻执行。useLayoutEffect是如何做到呢?和获取当前组件的方法类似,preact 会在 diff 算法最后返回 dom 前,插入了一个 options.diffed 的钩子。


function useLayoutEffect(callback, args) {
  // currentIndex 自增 1,向 currentComponent.__hooks._list 中增加一个新的状态
  const state = getHookState(currentIndex++);
  // 如果依赖数组,没有变化跳过更新
  // 如果依赖数组,参生变化执行 callback
  if (argsChanged(state._args, args)) {
    state._value = callback;
    // 记录前一次的依赖数组
    state._args = args;
    currentComponent.__hooks._pendingLayoutEffects.push(state);
  }
}

// options.diffed 会在 diff 算法,完成对浏览器的重绘前更新
options.diffed = vnode => {if (oldAfterDiff) oldAfterDiff(vnode);

  const c = vnode._component;
  if (!c) return;

  const hooks = c.__hooks;
  if (hooks) {hooks._handles = bindHandles(hooks._handles);
    // 执行组件的 useLayoutEffects 的 callback
    hooks._pendingLayoutEffects = handleEffects(hooks._pendingLayoutEffects);
  }
};

// 省略后的 diff 方法
function diff() {
  let tmp, c;

  // ...

  // ...

  // 在浏览器绘制前,diff 算法更新后,执行 useLayoutEffect 的 callback
  if (tmp = options.diffed) tmp(newVNode);

  // 返回更新后的 dom, 浏览器重绘
  return newVNode._dom;
}

useImperativeHandle

useImperativeHandle可以自定义向父组件暴露的实例值。useImperativeHandle应当与 forwardRef 一起使用。所以我们首先看一下 preact 中 forwardRef 的具体实现。

forwardRef

forwardRef 会创建一个 React 组件,组件接受 ref 属性,但是会将 ref 转发到组件的子节点上。我们 ref 访问到子节点上的元素实例。

forwardRef 的使用方式

const FancyButton = React.forwardRef((props, ref) => (<button ref={ref} className="FancyButton">
    {props.children}
  </button>
))

const ref = React.createRef()

// 组件接受 ref 属性,但是会将 ref 转发到 <button> 上
<FancyButton ref={ref}>Click me!</FancyButton>
Preact 中 forwardRef 的源码
// fn 为渲染函数,接受 (props, ref) 作为参数
function forwardRef(fn) {function Forwarded(props) {
    // props.ref 是 forwardRef 创建的组件上的 ref
    let ref = props.ref;
    delete props.ref;
    // 调用渲染函数,渲染组件,并将 ref 转发给渲染函数
    return fn(props, ref);
  }
  Forwarded.prototype.isReactComponent = true;
  Forwarded._forwarded = true;
  Forwarded.displayName = 'ForwardRef(' + (fn.displayName || fn.name) + ')';
  return Forwarded;
}

useImperativeHandle && bindHandles


function useImperativeHandle(ref, createHandle, args) {
  // // currentIndex 自增 1,向 currentComponent.__hooks._list 中增加一个新的状态
  const state = getHookState(currentIndex++);
  // 判断依赖是否产生了变化
  if (argsChanged(state._args, args)) {
    // 在 useEffect 的 state 中保存上一次的依赖,下一次会使用它进行比较
    state._args = args;
    // 将 useImperativeHandle 的 state 添加到__hooks._handles 数组中
    // ref,是 forwardRef 转发的 ref
    // createHandle 的返回值,是 useImperativeHandle 向父组件暴露的自定义值
    currentComponent.__hooks._handles.push({ref, createHandle});
  }
}

// options.diffed 中调用 bindHandles,对__hooks._handles 处理
function bindHandles(handles) {
  handles.some(handle => {if (handle.ref) {
      // 对 forwardRef 转发的 ref 的 current 进行替换
      // 替换的内容就是 useImperativeHandle 的第二个参数的返回值
      handle.ref.current = handle.createHandle();}
  });
  return [];}

举一个例子????


function Bar(props, ref) {useImperativeHandle(ref, () => ({hello: () => {alert('Hello')
    }
  }));
  return null
}

Bar = forwardRef(Bar)

function App() {const ref = useRef('')

  setTimeout(() => {
    // useImperativeHandle 会修改 ref 的 current 值
    // current 值是 useImperativeHandle 的第二个参数的返回值
    // 所以我们可以调用 useImperativeHandle 暴露的 hello 方法
    ref.current.hello()}, 3000)

  return <Bar ref={ref}/>
}

推荐阅读

  • React hooks: not magic, just arrays
  • How Are Function Components Different from Classes?
退出移动版