第十二章:useImperativeHandle源码解析

375 阅读4分钟

前言

react遵循单项数据流,但也有会向子组件获取一些实例,或者方法,这时候就需要用到useImperativeHandle,也涉及到组件安全,将一些安全可暴露方法,属性暴露给外部组件;
useImperativeHandle 是一个强大的工具,允许我们更细粒度地控制组件的 ref 行为

源码解析

useImperativeHandle(ref, createHandle, dependencies?)

mount阶段

按照常规流程,调用HooksDispatcherOnMountInDEV.useImperativeHandle的方法,前置check检查方法,核心方法mountImperativeHandle

function imperativeHandleEffect(create,ref){
    if(typeof ref === 'function'){
        const refCallback = ref;
        const inst = create();
        refCallback(inst);
        return () => {
          refCallback(null);
        };
    }else if(ref !== null && ref !== undefined){
        const refObject = ref;
        const inst = create();
        refObject.current = inst;
        return () => {
          refObject.current = null;
        };
    }
}
const Update = 0b00000000000000000000000100;
const LayoutStatic = 0b00010000000000000000000000;
const Layout =  0b0100;
mountEffectImpl(
    Update | LayoutStatic,
    Layout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
);

mountEffectImpl也是useEffect的mount阶段核心调用方法,回顾一下mountEffectImpl方法,

const HasEffect = 0b0001;
const hook = mountWorkInProgressHook();
fiber.flags |= Update | LayoutStatic;
hook.memoizedState = pushEffect(HasEffect | Layout, create, undefined, nextDeps);

mountWorkInProgressHook就是创建hook对象,挂载到fiber属性上;

const hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
};
fiber.memoizedState = hook;

更新fiber.flags标记,pushEffect也是创建effect对象,

const effect = {
    tag: HasEffect | Layout,
    create: create,
    destroy: destroy,
    deps: deps,
    // Circular
    next: null
};
fiber.updateQueue = {
    lastEffect: effect,
    stores: null
}

由于有effect副作用,在commitRoot阶段中会有执行createHandle方法,因为flags和useLayoutEffect一样,也是在commitLayoutEffects方法中执行,即是在dom改变之后触发;
在执行commitLayoutEffects,判断逻辑也是一致的,

const Callback = 0b00000000000000000001000000; 
const Ref = 0b00000000000000001000000000; 
const Visibility = 0b00000000000010000000000000; 
const LayoutMask = Update | Callback | Ref | Visibility; 
if(fiber.flags & LayoutMask !== 0){
 //...
     const flags = Layout | HasEffect; 
     if(flags & effect.tag === flags;){
         effect.destory = effect.create();
     }
}

这里和useLayoutEffect执行时机和逻辑都是一致的,那个先执行时按照代码自上而下的代码顺序;
create代码逻辑执行逻辑,imperativeHandleEffect通常ref是通过useRef创建的,将createHandle的返回值赋值给ref.current; 如果是函数,将createHandle的返回值赋值ref函数;

update阶段

调用HooksDispatcherOnUpdateInDEV.useImperativeHandle方法,调用check方法,核心方法updateImperativeHandle
update阶段和useEffect也一样调用updateEffectImpl方法,首先调用updateWorkInProgressHook方法,copy之前的hook对象,
判断dependencies和之前的做对比,如果和之前一致的话,

 hook.memoizedState = pushEffect(Layout, create, destroy, nextDeps);

这里deps是不是第三个参数,如果第三个参数传值的话,就是deps.concat([ref]),也就是说ref发生了改变也会有在update阶段触发更新;

fiber.flags |= Update;
hook.memoizedState = pushEffect(HasEffect | Layout, create, destroy, nextDeps);

同样和useLayoutEffect执行时机一致,
在dom修改之后执行destroy方法,

commitMutationEffects(root, finishedWork, lanes); //修改dom和执行destory方法
commitLayoutEffects(finishedWork, root, lanes); //执行create方法

commitMutationEffects执行逻辑

commitReconciliationEffects //dom更新
commitHookEffectListUnmount // 执行destory方法执行

// commitHookEffectListUnmount 核心逻辑
if(fiber.flags & Update){
    if(effect.tag & (Layout | HasEffect) === (Layout | HasEffect)){
        effect.destroy();
        effect.destroy = undefined;
    }
}

commitLayoutEffects 就是判断加执行create方法;

总结

useImperativeHandle的flags和tag都是和useLayoutEffect相同,是不是就把useImperativeHandle就是在useLayoutEffect不同版本,就是把ref, createHandle结合变成useLayoutEffect的第一个参数;
useImperativeHandle通常不会单独使用会与useRefforwardRef配合使用,暴露子组件的方法。

补充

关于useImperativeHandle源码还是很简单的,但是useImperativeHandle不会单独使用,否则一点实际作用都没有;
首先要知道在jsx解析dom的时候,会把key和ref的props给过滤掉,所以在普通函数式组件中传递props是在原有的基础上删除部分属性之后的结果,
所以在函数式组件中是没办法接受ref的props参数的,当然,你可以重新命名一个key然后把ref的值传递给子组件中

function Parent(){
  const selfRef = useRef();
  // selfRef.current.childFn() 可以获取子组件的方法
  return <Child selfRef={selfRef} />
}
function Child(props){
  // props.selfRef.current
  useImperativeHandle(props.selfRef,()=>{
      return {
          childFn:()=>{}
      }
  })
}

甚至不用ref也可实现,根据源码可知,传入对象并且有current值,或者是一个方法都可以接受子组件内部的属性/方法。

// current写法
const cacheCurrent = {
  current : null
}

function Parent(){
  // cacheCurrent.current.childFn()
  return <Child selfRef={cacheCurrent} />
}

// 函数式写法
const cacheFn = (chileHandle)=>{
  // chileHandle.childFn()
}

function Parent(){
  return <Child selfRef={cacheFn} />
}

以上几种方法都是可以通过的,但是我们写代码不是简单为了实现功能,也要遵守程序规范,为了方便他人理解我们写的代码

function Parent(){
  const ref = useRef();
  // ref.current.childFn()
  return <ChildForward ref={ref} />
}
const ChildForward = forwardRef(function (props, ref){
  useImperativeHandle(ref,()=>{
      return {
          childFn:()=>{}
      }
  })
})

forwardRef就是为了解决ref不能传递子组件的问题,所以在第二个参数将ref参数传递给子组件中。