优秀的编程知识分享平台

网站首页 > 技术文章 正文

「Preact」逐行解析hooks源码(「preact」逐行解析hooks源码分析)

nanyue 2024-08-06 18:03:24 技术文章 4 ℃


前言

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发起的action,initialState是state状态的初始值,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

useEffect会在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}/>
}

Tags:

最近发表
标签列表