实现简易版useState

214 阅读3分钟

先简单来看一下useState的用法

function App() {
  const [num, updateNum] = useState(0);
  const [num2, updateNum2] = useState(0);

  return (
    <div>
      <button id="btn1" onClick={updateNum(num => num + 1)}>按钮1</button>
      <button id="btn2" onClick={updateNum2(num2 => num2 + 1)}>按钮2</button>
      <div>{num}-{num2}</div>
    </div>
  )
}

在组件中,可以多次调用useState,那么怎么知道调用的是哪个hook呢,这里就需要使用链表来保存多个hook,每个组件对应的fiber上都有一条链表

let isMount = true; //是否是首次挂载
let workInProgressHook = null; //正在处理的hook

let fiber = {
  stateNode: App,
  //保存的是一条链表
  memorizeState: null
}

function App() {
  const [num, updateNum] = useState(0);
  const [num2, updateNum2] = useState(0);

  return {
    onClick() {
      updateNum(num => num + 1);
    },
    onClick2() {
      updateNum2(num => num + 10);
    }
  }
}

调用useState,就是往链表上添加hook(挂载时),并返回initialState

function useState(initialState) {
  
  let hook;
  if(isMount) {
    //首次挂载
    
    //构造新的hook
    hook = {
      memorizeState: initialState,
      next: null, //指向下一个hook
      queue : {
        pending: null
      }
    }

    if(!fiber.memorizeState) {
      //作为头节点
      fiber.memorizeState = hook;
    }else {
      //非头节点
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;   //当前hook就是正在工作的节点
  }

  let baseState = hook.memorizeState;
 
  //bind函数第二个参数相当于给dispatchAction加了默认值
  return [baseState, dispatchAction.bind(null, hook.queue)];
}
  

以上就是hook在挂载时流程,可以看到有一个dispatchAction方法,dispatch方法就是我们执行的updateNum方法,它的作用就是往对应的hook上添加更新,并重新调度

function dispatchAction(queue, action) {
  //目的:将更新加入到链表中,然后调度组件重新执行
  const update = {
    action,
    next: null
  }
  if(!queue.pending) {
    //环状链表,因为真实react中,每次更新是有优先级的
    //u0 -> u0 ->u0
    update.next = update;
  }else {
    //之前有更新,多次调用情况多次调用
    // //u0 -> u0
    // //u1 -> u0 -> u1
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  queue.pending = update;  //queue.pending始终指向最新的更新

  //调度
  schedule();
}

这里可以看到,hook.queue.pending是环状链表,需要进行环状链表的插入操作,并且queue.pending始终指向最新的更新,queue.pending指向的是更新的开始。插入操作完成后,接下来就是调度组件重新渲染了。

function schedule() {
//更新时workInProgressHook一开始指向的链表的头节点
  workInProgressHook = fiber.memorizeState; 
  const app = App();
  isMount = false;
  return app;
}

调度之后,App会重新执行,hook也会重新执行,此时应该走的是更新流程(isMount = false),更新流程中做了什么呢,更新时需要计算新的state,并进行返回。因此这里我们补充useState中的逻辑,增加更新时的条件分支

function useState(initialState) {
  let hook;
  if(isMount) {
    //首次挂载
    
    //构造新的hook
    hook = {
      memorizeState: initialState,
      next: null, //指向下一个hook
      queue : {
        pending: null
      }
    }

    if(!fiber.memorizeState) {
      //作为头节点
      fiber.memorizeState = hook;
    }else {
      //非头节点
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;   //当前hook就是正在工作的节点
  }else {
    //更新
    hook = workInProgressHook;  //此时hook指向正在更新的hook
    workInProgressHook = workInProgressHook.next;
  }


  //将环状链表剪开,计算新的state
  let baseState = hook.memorizeState;
  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;  //更新完成后清空更新
    
  }
  hook.memorizeState = baseState;  //计算完成后,保存state,以便下次更新使用


  //bind函数第二个参数相当于给dispatchAction加了默认值
  return [baseState, dispatchAction.bind(null, hook.queue)];
}

至此,简易版的useState就完成了,大体的工作流程如下

image.png

最后,看一下完整的代码吧

<!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>Document</title>
</head>
<body>
  <button id="btn1">按钮1</button>
  <button id="btn2">按钮2</button>

<script>
let isMount = true; //是否是首次挂载
let workInProgressHook = null; //正在处理的hook

let fiber = {
  stateNode: App,
  //保存的是一条链表
  memorizeState: null
}

function useState(initialState) {
  
  let hook;
  if(isMount) {
    //首次挂载
    
    //构造新的hook
    hook = {
      memorizeState: initialState,
      next: null, //指向下一个hook
      queue : {
        pending: null
      }
    }

    if(!fiber.memorizeState) {
      //作为头节点
      fiber.memorizeState = hook;
    }else {
      //非头节点
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;   //当前hook就是正在工作的节点
  }else {
    //更新
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }


  //将环状链表剪开,计算新的state
  let baseState = hook.memorizeState;
  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;  //更新完成后清空更新
    
  }
  hook.memorizeState = baseState;


  //bind函数第二个参数相当于给dispatchAction加了默认值
  return [baseState, dispatchAction.bind(null, hook.queue)];
}


function dispatchAction(queue, action) {
  //将更新加入到链表中,然后调度组件重新执行
  const update = {
    action,
    next: null
  }
  if(!queue.pending) {
    //环状链表,因为真实react中,每次更新是有优先级的
    //u0 -> u0 ->u0
    update.next = update;
  }else {
    //之前有更新,多次调用情况多次调用
    // // //u0 -> u0
    // // //u1 -> u0 -> u1
    // update.next = queue.pending.next;
    // queue.pending.next = update;
  }
  queue.pending = update;  //queue.pending始终指向最新的更新

  //调度
  schedule();

}

function schedule() {
  workInProgressHook = fiber.memorizeState;
  const app = App();
  isMount = false;
  return app;

}

function App() {
  const [num, updateNum] = useState(0);
  const [num2, updateNum2] = useState(0);

  return {
    onClick() {
      updateNum(num => num + 1);
      updateNum(num => num + 1);
      updateNum(num => num + 1);
    },
    onClick2() {
      updateNum2(num => num + 10);
    }
  }
}

window.app = schedule();

</script>

  <script>

    const btn1 = document.getElementById('btn1');
    const btn2 = document.getElementById('btn2');
    btn1.onclick = window.app.onClick;
    btn2.onclick = window.app.onClick2;
  </script>
</body>
</html>