React源码解析24-hooks

330 阅读4分钟

1.hook对象数据结构

一个函数组件中可以有多个hook,那么怎么保存呢?

函数组件fiber数据结构

let fiber = {
	memoiezdState:null,//函数组件的memoiezdState是保存函数组件内的所有的hook对象链表,不像类组件是保存state,state是单独的hook对象里面保存
    stateNode:App,
    updateQueue:null//函数组件的updateQueue是保存需要执行useeffect的hook的链表
}

hook对象数据结构

let hook = {
	queue:{
        pending:update // 这个才是保存useState中updateNumber产生的update的链表 指向最后一个update
    },
    merizedState:null//保存useState中的state变量 相当于小的class组件
    next:null//执行函数组件中下一个hook对象
}

//创建hook的时候,fiber.memoiezdState = firstHook,然后后面创建的Hook对象就.next下去,执行的时候也是根据fiber.memoiezdState这个列表来顺序执行。

hook的update数据结构

let update = {
	action:function/state;//存储updateNum的新的state
	next:null//下一个update
}

2.极简版useState实现

在render阶段都会重新调用函数组件的函数 去diffchildren 所以都会走useState,所以无论是调用updateNum还是mount阶段都会进入render阶段都会重新执行函数

其他hooks实现只是hook.memerized不同和触发时机不同而已

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>useState的极简版实现</title>
</head>
<body>
  
</body>
<script>
  let mouted = true
  let workingProgressHook = null
  let fiber = {
    stateNode:App,
    updateQueue:null,
    memorizedState:null,
  }
  function App(){
    const [number,updateNumber] = useState(0)
    const [status,updateStatus] = useState(false)
    console.log('number',number);
    console.log('status',status);
    console.log('mouted',mouted);
    return {
      onclick(){
        updateNumber(number=>number+1)
        updateNumber(number=>number+1)
        updateNumber(number=>number+1)
      },
      trigger(){
        updateStatus(status=>!status)
      },
    }
  }
  function run(){
  
    workingProgressHook = fiber.memorizedState//第一个hook
    let app = fiber.stateNode()//!render阶段执行函数 如果执行这个函数触发啦更新就叫render阶段触发的更新
    mouted = false
    return app
  }
  function useState(baseState){
    let hook = null
    if(!mouted){//!当前hook已经存在,说明不是初始化
      hook = workingProgressHook
      workingProgressHook = workingProgressHook.next
     

    }else{
      newState = baseState
      hook  ={
        queue:{
          pending:null
        },
        memorizedState:baseState,
        next:null
      }
      if(!fiber.memorizedState){
        fiber.memorizedState = hook
      }else{
        workingProgressHook.next = hook
      }
      workingProgressHook = hook//函数组件中下一个hook
    }

     baseState = hook.memorizedState
    if(hook.queue.pending){
      let firstUpdate = hook.queue.pending.next
      do{
        const action = firstUpdate.action
        baseState = action(baseState)
        firstUpdate = firstUpdate.next
      }while(firstUpdate!==hook.queue.pending.next)
    }
    hook.queue.pending = null//这些update计算完了 删除
    hook.memorizedState = baseState
    return [baseState,dispatchAction.bind(null,hook.queue)]
  }
  //!必须拿到queue才能加入update到queue里
  function dispatchAction(queue,action){
    //创建update

    let update = {
      action,
      next:null
    }
    //加入queue
    if(queue.pending){
      update.next = queue.pending.next
      queue.pending.next = update
    }else{
      
      update.next=update
    }
    queue.pending = update
    //开始render
    run()
  }
  window.app = run()
</script>
</html>

3.useEffect/useLayoutEffect

useEffect的hook对象的memorizedState对应的是effect对象 effect对象的数据结构如下

 const effect: Effect = {
    tag,
    create,//回调函数
    destroy,//卸载函数
    deps,
    // Circular
    next: (null: any),
  };

同时这些effect会以链表形式存储到函数组件fiber这次的updateQueue中,但是只有deps改变的effect我们才会执行这个effect 且这个函数fiber才会到commit阶段的effect-list上

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();//!更新阶段是当前fiber的memorizedState链表下的Hook 或者mount阶段新建的hook
  const nextDeps = deps === undefined ? null : deps;//!依赖项
  currentlyRenderingFiber.flags |= fiberFlags;//!fiberFlags 就是useEffect/layputeffect 在fiber上加这个tag用于区分这两个
  hook.memoizedState = pushEffect(//!useState的memoizedState 是State的值 而useEffect的memoizedState就是effect数据结构
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;//!上一次useEffect的effect对象
    destroy = prevEffect.destroy;//!上一次useEffect的effect对象的销毁函数
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {//!deps相同的时候 我们会把这个effect加入到fiber的udpatequeue中 但是不会执行 因为tag只穿了hookFlags
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(/!只有deps不相同的时候 我们会把这个effect加入到fiber的udpatequeue中 才是需要执行的effect 因为tag传啦HookHasEffect | hookFlags,
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

为什么deps没改变的时候也要push effect到fiber.updateQueue里面呢 因为我们一个函数组件里面的effect是有顺序的 我们不能少了某一个那么后面的索引关系全乱啦

uselayoutEffect 只是第二个tag不同 逻辑一样 只是在commit阶段执行时机不同

4.useRef

ref可以作为一种数据结构和一种生命周期

hostcomponent,classcomponent,forwardRef都可以赋值ref属性,其中forwardRef只是把ref传参下去,不执行ref的逻辑

useRef 这个hook对象的memoizedState就是一个对象 {current:value} 返回的也是这个对象 所以这个对象就是存储到hook对象的memoizedState上的

ref在render阶段和commit阶段都会有操作

render阶段:

判断如果是首屏渲染阶段或者更新阶段两次ref不一样 会给这个函数fiber加上ref这个tag

commit阶段:

首先是ref移除阶段:即在commitmutationEffect这个函数里

在mutationEffects函数,对应是更新阶段的即两次ref不一样的情况下 我们会移除之前的ref(即ref.current=null)且如果某个fiber被移除 他的ref和子节点的fiber会全部移除

接下来是ref赋值阶段即在commitlayouteffects这个函数

对应不同的fiber类型 我们把ref赋值为新的对应的实例

5.useCallback&useMemo

useMemo:hook对象的memorizedState就存储这个值和依赖项

update时候 usememo就是获取hook的memorizedState的deps和本次传入的deps对比,看是否返回这次还是这次的

useCallback类似:hook对象的memorizedState就存储这个函数和依赖项