前言
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通常不会单独使用会与useRef和forwardRef配合使用,暴露子组件的方法。
补充
关于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参数传递给子组件中。