实现一个 Mini React:核心功能详解 - useState hook的迷你版实现

157 阅读4分钟

xdm,又要到饭了,又更新代码了!

总结一下上一篇完成的内容,

  1. 描述了虚拟dom的概念以及带来的好处
  2. 实现了生成虚拟dom的函数
  3. 虚拟dom转换真实dom的实现
  4. 函数组件的初步实现

有兴趣的可以点这里查看# 虚拟dom + 实现组件

接下来几篇我们会尝试自己实现几个常用的react hooks的mini版本以加强理解他们的底层实现原理。

为了最大化的考虑广大读者,

  • 代码不会使用typescript,

  • 代码的实现考虑了所有读者的层次,哪怕你是新手也依然没有什么难度,

  • 实现的细节不会包含所有边界情况(mini react的目的让你理解react关键技术实现原理),

1.1 实现 props 和 state 以及 useState

为了使组件具有更强大的功能,我们需要支持 propsstateprops 是组件的输入,而 state 则用于管理组件内部的状态。useState hook提供了我们组件内部状态的简单管理。

// 平时我们使用useState,一般类似于这样的

function MyComponent() {
  const [count, setCount] = useState(0);
}

那么根据函数的签名可以知道,useState 函数接受一个参数返回一个数组,数组的第一个值就是内部状态值,第二个则是更新内部状态值的一个函数。由此我们可先有这样的实现,

let state
function useState(initial) {
    state = state || initial
    const setState = newState=>{
      
      // 这里会更新组件内部状态值,
      state = newState
      
      // 也会更新ui
      renderApp()
    }
    
    return [state, setState]
}

依据我们现在的进度,你可以理解renderApp会重新运行相关组件以更新ui,至于如何找出哪几个组件需要更新这个会在后续的 diff 算法讲解。这里你可以简单理解renderApp 会更新对应的ui即可。

又基于useState一个组件内部可以多次使用,我们需要创建一个数据结构可以顺序记录每一个useState的内部状态值。所以就有了下面的更新,

//let state
let hooks = []
let currentHook = 0
function useState(initial) {
    //state = state || initial
    const hookIndex = currentHook
    hooks[hookIndex] = hooks[hookIndex] || initial
    
    const setState = newState=>{ 
      
      // 这里会更新组件内部状态值,
      //state = newState
      hooks[hookIndex] = newState
      
      // 也会更新ui
      renderApp()
    }
    
    const state = hooks[hookIndex]
    // 这里的index的改变控制了对应的多个usestate 的状态值
    currentHook++
    
    return [state, setState]
}

那么一个迷你版本的useState就实现了。

测试

<!DOCTYPE html>
<html>
<head>
  <title>Mini React 测试 - useState</title>
  <style>
    #app-container {
      border: 2px solid #000;
      padding: 20px;
      margin: 20px;
      font-size: 18px;
    }
    button {
      padding: 10px 20px;
      font-size: 16px;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <div id="root"></div>
  <script>
    // Mini React 基本实现
    function createElement(type, props, ...children) { 
      return { 
        type, 
        props: props || {}, 
        children: children.map(child => typeof child === 'object' ? child : createTextElement(child) ), 
      }; 
    } 
    
    function createTextElement(text) { 
      return { 
        type: 'TEXT_ELEMENT', 
        props: { nodeValue: text }, 
        children: [], 
      }; 
    }
    
    function render(vdom, container) {
      if (typeof vdom.type === 'function') {
        const component = vdom.type;
        const props = vdom.props;
        const childVdom = component(props);
        render(childVdom, container);
        return;
      }

      const dom = vdom.type === 'TEXT_ELEMENT'
        ? document.createTextNode(vdom.props.nodeValue || '')
        : document.createElement(vdom.type);

      Object.keys(vdom.props).forEach(name => {
        if (name.startsWith('on')) {
          const eventType = name.slice(2).toLowerCase();
          dom.addEventListener(eventType, vdom.props[name]);
        } else if (name === 'style') {
          Object.assign(dom.style, vdom.props[name]);
        } else {
          dom[name] = vdom.props[name];
        }
      });

      vdom.children.forEach(child => render(child, dom));
      container.appendChild(dom);
    }

    // useState 实现
    let hooks = [];
    let currentHook = 0;

    function useState(initial) {
      const hookIndex = currentHook;
      hooks[hookIndex] = hooks[hookIndex] || initial;

      const setState = newState => {
        hooks[hookIndex] = newState;
        renderApp();  // 更新UI
      };

      const state = hooks[hookIndex];
      currentHook++;
      
      return [state, setState];
    }

    // 定义 renderApp 函数,用于重新渲染应用
    function renderApp() {
      currentHook = 0; // 重置 currentHook,确保状态按顺序应用
      document.getElementById('root').innerHTML = ''; // 清空容器内容
      render(createElement(MyStatefulComponent), document.getElementById('root')); // 重新渲染组件
    }

    // 定义测试组件 MyStatefulComponent
    function MyStatefulComponent() {
      const [count, setCount] = useState(0);

      return createElement(
        'div',
        { id: 'app-container' },
        `Count: ${count}`,
        createElement(
          'button',
          { onClick: () => setCount(count + 1) },
          'Increment'
        )
      );
    }

    // 初始渲染
    renderApp();

  </script>
</body>
</html>

上述代码中,useState 实现了简单的状态管理。每次组件渲染时,currentHook 确保状态的正确对应。点击按钮时,调用 setCount 更新状态并重新渲染应用。

这里的 renderApp 成功的更新了 ui,但react真正的更新过程需要使用 fiber 以及 reconcile, 即 fiber 结构配合调和算法以及diff 算法 找出需要更新的地方才进行更新。

因为我们还没有实现fiber以及 reconcile的算法以及结构,所以这里暂时使用renderApp 更新了ui。 我们之后的章节有讲解所有你需要知道的 fiber 等如何实现的,等你了解了fiber之后,我们还有一个章节会重构 useState hook

下一篇,我们将自己实现常用的 useEffect hook,以帮助我们增强对其原理的理解。

如果这样的长度/强度你觉得可以接受,觉得有帮助,可以继续阅读下一篇,实现一个 Mini React:核心功能详解 - useEffect等hook的实现。

如果文章对你有帮助,请点个赞支持一下!

啥也不是,散会。