前言
什么是预反应? React 中使用相同ES6API 的3kb 轻量级解决方案
尽管Preact和React具有相同的API,但内部实现机制的差异仍然很大。不过,这并不妨碍您阅读和学习Preact 源代码。顺便说一句,今年早些时候,我的一个朋友@Xiaohan 在北京一家公司面试时遇到了Facebook 的一位大人物。 Facebook的大佬还鼓励他阅读和学习源代码。普雷克特的。
Hook 并不是魔法,它们的设计与React 无关(Dan Abramov)。 Preact 也是如此,因此您不必阅读Preact 或React 源代码来了解钩子实现。
希望下面的分享能让大家了解hooks背后的实现。
关于钩子的规则
以下是在React 中使用hooks 的规则:我们可以看到,钩子的使用高度依赖于执行顺序。如果你读过源码,你就会明白为什么有这两条使用钩子的规则。
仅在顶层使用钩子。不要在循环、条件或嵌套函数内调用钩子。 不要从常规JavaScript 函数中调用钩子。 Hook源码分析
获取钩子状态
getHookState 函数在当前组件实例上安装__hooks 属性。 __hooks对象的_list属性是用于存储所有类型hooks(useState、useEffect…………)的执行结果和返回值的对象。 _list属性使用数组来存储状态,因此每个钩子的执行顺序尤其重要。
function getHookState(index) { if (options._hook) options._hook(currentComponent); //检查组件是否有__hooks 属性,如果没有,则主动挂载一个空的__hooks 对象。 const hooks=currentComponent.__hooks || (currentComponent.__hooks={ _list: [], //_list 中保存了所有hooks 的状态_pendingEffects: [], //useEffect 的状态保存在_pendingEffects 中_pendingLayoutEffects: [], //useLayoutEffects 的state 状态保存在_pendingLayoutEffects _handles:[] }); //根据索引index.检查__hooks._list数组是否有相应的状态。 //否则,将空状态添加到活动状态。 if (index=hooks._list.length) {hooks._list.push({}); } //返回__hooks._list 数组的索引对应的状态returnhooks._list[index];} 代码主要部分您需要复制一个全局变量
getHookState 使用全局变量currentComponent。变量currentComponent 指向当前组件实例。如何获取当前组件实例的引用呢?结合hook的源码和preact的源码后,preact运行diff时,将当前组件的虚拟节点VNode设置为options._render函数I发现我可以成功检索该实例。当前组件。
//当前钩子执行顺序指针let currentIndex; //vnode is options._render=vnode={ if (oldBeforeRender) oldBeforeRender(vnode); //当前组件实例currentComponent=vnode ._component;重置索引,每个组件的钩子状态列表从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 是基于useReducer 的封装。有关详细信息,请参阅下面的useReducer。
//useState 接受初始值InitialState,初始化状态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是状态的初始值,init是一个延迟初始化初始值的函数。 useReducer 返回[state,dispatch] 形式的内容。
function useReducer(reducer,initialState,init) { //通过将currentIndex 加1 创建一个新状态并将其保存到currentComponent.__hooks._list constookState=getHookState(currentIndex++) if (!hookState._component) { //state 存储引用到当前组件。 hookState._component=currentComponent;ookState._value=[ //如果未指定第三个参数`init,则返回initialState。 //如果指定了第三个参数,则返回由InitialState 处理的延迟初始化值。 Function //`useState` 是基于`useReducer` 的封装。 //`useState` 返回hookState._value[0],直接默认初始状态!invokeOrReturn(null,InitialState) : init(initialState), //ookState._value[1], `action` Accepts, { type: `xx ` } //`useState`是基于`useReducer`的封装,所以action参数也可以是新的状态值,或者状态更新函数作为参数action={ //返回新的状态值const nextValue=raiser(hookState._value[0], action); //使用新的状态值更新状态if (hookState._value[0] !==nextValue) { hookState._value[0]=nextValue/ 调用SetState;再次执行diff 操作(在Preact 中,diff 过程中会同步更新实际的dom 节点) handleState._component.setState( {}); //对于useReduer,返回[state , dispath] //对于useState, return [state, setState] returnookState._value;}复制代码useEffect
useEffect 允许您在函数组件中执行副作用。事件绑定、数据请求和DOM 的动态更改。 useEffect 在每个React 渲染上执行。无论是首次安装还是更新。 useEffect可以返回一个函数,当react被清除时,返回的函数将被执行。当执行该效果时,先前的效果将被清除。卸载组件时也会执行清理。
function useEffect(callback, args) { //currentIndex 增加1 并向currentComponent.__hooks._list 添加新状态const state=getHookState(currentIndex++); //argsChanged 函数确定useEffect 的依赖项是否已更改。 //如果发生更改,argsChanged 返回true 并且重新运行useEffect 回调。 //如果没有变化,argsChanged 返回false,回调不执行。 //第一次渲染时,state._args等于undefined,argsChanged直接返回true if (argsChanged(state._args, args)) { state._value=callback ; //最终依赖useEffect的state保存关系并用于下次比较state._args=args //将useEffect的状态保存在__hooks._pendingEffects中currentComponent.__hooks._pendingEffects.push(state); //执行useEffect的回调将需要的组件添加到afterPaintEffects数组中并保存暂时地。 //因为我们需要等到渲染完成后才执行useEffect回调。复制代码argsChanged;
argsChanged 是一个简单的实用函数,用于比较两个数组之间的差异。如果数组中的每一项都相等,则返回false;如果其中一项不相等,则返回true。主要目的是比较useEffect、useMemo等hook的依赖关系。
function argsChanged(oldArgs, newArgs) { return !oldArgs || newArgs.some((arg,index)=arg !==oldArgs[index]);}复制代码后绘制
afterPaint函数负责将需要useEffect的回调组件推入全局afterPaintEffects数组中。
let afterPaintEffects=[];let afterPaint=()={};if (typeof window !==\’unknown\’) { let prevRaf=options.requestAnimationFrame; afterPaint=Component={ if ( //_afterPaintQueued 属性允许每个组件afterPaintEffects (!component._afterPaintQueued (component._afterPaintQueued=true) //afterPaintEffects.push(component)===只能推送一次到1。确保`safeRaf` 在清除之前只运行一次。 //该组件将被添加到afterPaintEffects 数组afterPaintEffects.push(component)===1) || prevRaf !==options.requestAnimationFrame ) { prevRaf=options.requestAnimationFrame //运行safeRaf(flushAfterPaintEffects) (options.requestAnimationFrame ||safeRaf)(flushAfterPaintEffects) ; };}复制代码
safeRaf打开requestAnimationFrame,调用flushAfterPaintEffects来处理diff(Preact中的diff是一个同步过程,相当于宏任务)完成后的useEffect回调。
const RAF_TIMEOUT=100; function safetyRaf(callback) { constned=()={ cancelAnimationFrame(raf); setTimeout(done, RAF_TIMEOUT); 是同步的,requestAnimationFrame 在diff 完成后执行(宏任务完成后)。 requestAnimationFrame(done);} 复制代码flashAfterPaintEffects
flashAfterPaintEffects 处理afterPaintEffects 数组中的所有组件。
functionlushAfterPaintEffects() { //循环遍历afterPaintEffects 数组中的所有待处理组件afterPaintEffects.some(component={component._afterPaintQueued=false; if (component._parentDom) { //使用handleEffects currentComponent.__hooks 清除所有处于._pendingEffects 状态的组件useEffect //handleEffects 清除效果并执行效果的逻辑//handleEffects 最终返回一个空数组并调用component.__hooks._pendingEffects Reset } ); //清除afterPaintEffects afterPaintEffects=[];} 复制代码。
清除组件的useEffect并运行
function handleEffects(Effects) { //清除效果Effects.forEach(invokeCleanup) //运行所有效果Effects.forEach(invokeEffect) return [];} 复制代码invokeCleanup;
//运行清理效果function invokeCleanup(hook) { if (hook._cleanup)ook._cleanup();}复制代码invokeEffect
function invokeEffect(hook) { const result=hook._value(); //如果useEffect的回调的返回值是一个函数//该函数记录在useEffect的_cleanup属性中if (typeof result===\’function \’ ) {hook._cleanup=result }}复制代码。
useMemo 返回一个记忆值。 useCallback 返回一个记忆的回调函数。当依赖数组发生变化时,useMemo 会重新计算记忆值。当依赖项数组更改时,useCallback 返回一个新函数。
使用说明
function useMemo(callback, args) { //CurrentIndex 加1,并将新状态添加到currentComponent.__hooks._list const state=getHookState(currentIndex++) //判断依赖数组是否发生变化//如果发生变化,回调将会重新执行并返回一个新的返回值。 //否则,返回最后的返回值。 if (argsChanged(state._args, args)) {state._args=args=callback;/state._value 被记录一次性返回值(如果useCallback,则记录最后一次回调) return state._value=callback() }//返回回调的返回值return state._value;} 复制代码useCallback
useCallback 是基于useMemo 的包装器。仅当依赖项数组发生更改时,useCallback 才会返回新函数。否则,它始终返回第一个接收回调。
function useCallback(callback, args) {return useMemo(()=callback, args);} 复制代码useRef
useRef 也是基于useMemo 封装的。但是,不同之处在于依赖数组作为空数组传递。这意味着useRef 每次都会重新计算。
function useRef(initialValue) {return useMemo(()=({ current:InitialValue }), []);} 复制代码useRef Application
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 打印new value function Bar () { const count=useRef(0) const showMessage=()={ console.log(`count: ${count.current}`) } setTimeout(()={ //新值为`1 ` and count.current 获取最新值。 showMessage() }, 2000) setTimeout(()={ 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++); //如果依赖数组则没有变化,更新将会是跳过了。 //如果依赖于数组,则参数变化时执行回调if (argsChanged(state._args, args)) { state._value=callback; //记录之前的依赖数组state._args=currentComponent.__hooks._pendingLayoutEffects .push;(状态);复制代码。 //options.diffed 是options.diffed=vnode={ if (oldAfterDiff) oldAfterDiff(vnode); const c=vnode._component if (!c ) return; if (hooks) {hooks._handles=bindingHandles(hooks. _handles); //执行组件的useLayoutEffects 回调hooks._pendingLayoutEffects=handleEffects(hooks._pendingLayoutEffects) }}; 复制代码//省略diff 方法function diff() { let tmp, c; //在浏览器绘制之前更新diff 算法后运行useLayoutEffect 回调if (tmp=options.diffed) tmp(newVNode); //更新dom,浏览器将重绘return newVNode._dom;} 复制代码使用命令句柄
useImperativeHandle 可以自定义暴露给父组件的实例值。 useImperativeHandle 必须与forwardRef 一起使用。那么,首先我们来看看preact中forwardRef的具体实现。
前向参考
forwardRef 创建一个接受ref 属性的React 组件,但将ref 转发到组件的子节点。对子节点上元素实例的引用访问。
如何使用forwardRef
const FancyButton=React.forwardRef((props, ref)=( button ref={ref} className=\’FancyButton\’ {props.children} /button))const ref=React.createRef()//组件接受ref 属性但是,它在按钮上执行前向参考。 FancyButton ref={ref}点我!/FancyButton 复制Preact的forwardRef的源代码。
//fn 是一个渲染函数,以(props, ref) 作为参数functionforwardRef(fn) { function Forwarded(props) { //props.ref 是forwardRef创建的组件的ref let ref=props. props.ref; //调用渲染函数,渲染组件,并将ref 发送给渲染函数return fn(props, ref); } Forwarded._forwarded=true;name) + \’)\’; return Forwarded;} 复制代码。
function useImperativeHandle(ref, createHandle, args) { ////将currentIndex 加1 并向currentComponent.__hooks._list 添加新状态const state=getHookState(currentIndex++) //确定依赖项是否已更改if (argsChanged (state) . _args, args)) { //保存useEffect的state的最后一个依赖,用于下次比较state._args=args; //__hooks useImperativeHandle的state添加到_handles数组中//ref是forwardRef转发的ref//createHandle 的返回值是通过useImperativeHandle 暴露给父组件的自定义值currentComponent.__hooks._handles.push({ ref, createHandle }) }} 代码//调用bindingHandles 处理options.diffed __hooks._handles function bindingHandles(handles ) { handles.some(handle={ if (handle.ref) { //ForwardRef 转发的ref Current Replace //替换的内容是useImperativeHandle 的第二部分每个返回参数handle.ref.current=handle.createHandle() ; } }); 复制代码来演示该示例。
function Bar(props, ref) { useImperativeHandle(ref, ()=({ hello: ()={alert(\’Hello\’) } })); Bar=forwardRef(Bar)function App() { const ref=useRef(\’ \’) setTimeout(()={ //useImperativeHandle 改变ref 的当前值//当前值是useImperativeHandle 第二个参数的返回值//因此,调用hello 方法ref.current.hello 可以通过useImperativeHandle 暴露出来() }, 3000) 返回Bar ref={ref}/}。
原创文章,作者:小条,如若转载,请注明出处:https://www.sudun.com/ask/84663.html