React源码解析18(6)------ 实现useState

摘要

在上一篇文章中,我们已经实现了函数组件。同时可以正常通过render进行渲染。

而通过之前的文章,beginWork和completeWork也已经有了基本的架子。现在我们可以去实现useState了。

实现之前,我们要先修改一下我们的index.js文件:

javascript 复制代码
import jsx from '../src/react/jsx.js'
import ReactDOM from '../src/react-dom/index'
import { useState } from './react-dom/filberHook.js';

const root = document.querySelector('#root');

function App() {
  const [name, setName] = useState('kusi','key');
  window.setName = setName;
  const [age, setAge] = useState(20)
  window.setAge = setAge;
  return jsx("div", {
    ref: "123",
    children: jsx("span", {
      children: name + age
    })
  });
}

ReactDOM.createRoot(root).render(<App />)

由于我们这一篇并不会实现React的事件机制,所以我们先将setState的方法挂载在window上进行调试。有了基础,我们现在开始实现useState。

1.renderWithHook

在实现之前,我们先来思考一个问题。在之前实现beginWork机制的时候,我们为了兼容函数组件。获取子FilberNode的时候,函数组件是直接调用拿到返回值。

那么如果函数直接调用,是不是就已经调用了我们在函数里写的Hook。

所以我们把这一部分拆出来:

javascript 复制代码
function updateFunctionComponent(filberNode) {
  const nextChildren = renderWithHook(filberNode);
  const newFilberNode = reconcileChildren(nextChildren);
  filberNode.child = newFilberNode;
  newFilberNode.return = filberNode;
  beginWork(newFilberNode)
}

在更新函数节点的时候,通过renderWithHook拿到函数执行的返回值:

那我们在renderWithHook里除了拿到函数执行的返回值,还要做什么呢?

这里值得注意的是,我们知道通过setState,函数组件会重新执行渲染。在这里,我们将函数的执行分为两种:第一次mount和后面的update。

就是执行useState这个过程,要分为两种,一种是mount下的useState,一种是update下的useState。OK,现在我们用一个标志去表示这两种状态,并且在renderWithHook下去改变它。

javascript 复制代码
let hookWithStatus;
let workInPropgressFilber = null;

export const renderWithHook = (filberNode) => {
  if(filberNode.child){
    //更新
    hookWithStatus = 'update'
  }else{
    //mount
    hookWithStatus = 'mount'
  }
  workInPropgressFilber = filberNode;
  const nextChildren = filberNode.type();
  return nextChildren;
}

2.实现mountState和Hook结构

现在我们在beginWork执行完后,会执行renderWithHook,执行后会改变hookWithStatus这个标志。再然后就是调用函数本身了。

所以现在我们根据这个标志实现两种不同的useState:

javascript 复制代码
export const useState = (state) => {
  if(hookWithStatus === 'mount'){
    return mountState(state)
  }else if(hookWithStatus === 'update'){
    return updateState(state)
  }
}

也就是页面第一次渲染时,执行函数组件里的内容,我们要调用mountState。现在我们实现mountState。

实现之前,我们先说一下在React中,是如何将组件中的Hook存储的。在React中是通过链表的方式,将不同的Hook存储起来。现在我们定义一下Hook的结构:

它具有三个属性。memoizedState表示存储的state值,updateQueue表示需要更新的值,next表示指向的下一个hook。

javascript 复制代码
class Hook {
  constructor(memoizedState, updateQueue, next){
    this.memoizedState = memoizedState
    this.updateQueue = updateQueue
    this.next = next;
  }
}

所以在mountStaet中,我们要将这个链表结构实现出来:

这里我们定义一个headHook指向最外层的hook,workinProgressHook指向当前的hook。

javascript 复制代码
function mountState(state) {
  const memoizedState = typeof state === 'function' ? state() : state;
  const hook = new Hook(memoizedState);
  hook.updateQueue= createUpdateQueue()
  if(workInPropgressHook === null){
    workInPropgressHook = hook;
    headHook = hook;
  }else{
    workInPropgressHook.next = hook;
    workInPropgressHook = hook;
  }
  return [memoizedState]
}

现在我们可以看一下HOOK的结构:

可以看出它是一个链表的结构,memoizedState保存的就是setState的初始值。

3.实现dispach更新

现在经过mount阶段后,我们已经有了一个基本的Hook链表。现在如果我在window下调用setState,那肯定是什么都不会发生的。

所以我们要实现setState方法,但是要调用setState方法是一定要更新的,所以我们将beginWork中的updateContainer方法修改一下,并且暴露出来:

javascript 复制代码
function updateContainer(root, element) {
  const hostRootFilber = root.current;
  const update = createUpdate(element);
  hostRootFilber.updateQueue = createUpdateQueue()
  enqueueUpdate(hostRootFilber.updateQueue, update);
  wookLoop(root,hostRootFilber)
}

export const wookLoop = (root,hostRootFilber) => {
  if(!hostRootFilber){
    hostRootFilber = root.current
  }
  beginWork(hostRootFilber);
  completeWork(hostRootFilber);
  root.finishedWork = hostRootFilber;
  console.log(root)
  commitWork(root)
}

这样我就可以在hook的机制里面调用wookLoop了。现在我们实现dispatch:

javascript 复制代码
function disaptchState(filber, hook, action) {
  const update = createUpdate(action);
  enqueueUpdate(hook.updateQueue, update);
  workUpdateHook = hook;
  wookLoop(filber.return.stateNode)
}

dispatchState方法传入当前的filberNode, 还有就是对应的hook,以及需要更新的action。

同时我们将准备更新的hook进行标记。

所以在mountState中:

javascript 复制代码
function mountState(state) {
  const memoizedState = typeof state === 'function' ? state() : state;
  const hook = new Hook(memoizedState);
  //其他代码。。。
  const disaptch = disaptchState.bind(null,workInPropgressFilber,hook)
  return [memoizedState,disaptch]
}

我们将dispatch需要的参数传进去,并且只给外面放开action。这样就实现好了dispatch方法。

4.实现updateState方法

当我们将上面的过程实现完之后,如果在控制台调用setState。那么就会触发workLoop,同时会再走一次beginWork。

此时再进入renderWithHook之后,就不会再走mountState了,而是进入updateState。

而在updateState中,我们要做的事情也不是很复杂,只需要从头遍历Hook链表,如果是标记更新的Hook,就返回更新的内容。如果不是,就正常返回它的memoizedState就好了。

javascript 复制代码
function updateState(state) {
  if(currentHook === workUpdateHook){
    const newHook = new Hook(workUpdateHook.updateQueue.shared.pending.action)
    newHook.updateQueue = createUpdateQueue();
    const disaptch = disaptchState.bind(null,workInPropgressFilber,newHook)
    currentHook = currentHook.next;
    const result = [workUpdateHook.updateQueue.shared.pending.action,disaptch];
    return result;
  }else{
    let result = currentHook.memoizedState;
    const disaptch = disaptchState.bind(null,workInPropgressFilber,currentHook)
    currentHook = currentHook.next;
    return [result,disaptch]
  }
}

所以这也是为什么,在React中,不能在条件语句里面使用Hook,如果你mountState生成的Hook链表会发生变化。那么在updateState里面,遍历链表的时候,就会出现值错位的情况。

OK,到这里useState的方法也已经实现完了。

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax