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

198 阅读4分钟

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

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

  1. 实现了mini useState hook函数

有兴趣的可以点这里查看useState hook的迷你版实现

这篇将会实现一个useEffect hook 的迷你版本

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

  • 代码不会使用typescript,

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

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

1.1 useEffect 的函数签名

useEffect的常见使用方法。

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

function MyComponent() {
  const [count, setCount] = useState(0);
  
  useEffect(()=>{
      console.log('this is the  effect')
      return ()=>{
        console.log('this cleans up the  effect')
      }
  }, [count])
}

那么根据函数的签名可以知道,useEffect 函数接受2个参数,第一个参数就是effect,一个函数。第二个参数是dpes (依赖项)。每当依赖deps的任何一个值有变化,则effect会运行。effect 在首次组件渲染也会运行一次。

由此我们可先有这样的实现,

const hooks = []
let currentHook = 0

function useEffect(effect, deps) {
   const oldDeps = hooks[currentHook] ? hooks[currentHook].deps : undefined
   const hasUpdated = !oldDeps || !deps.every((dep, i)=> dep===oldDeps[i])
  
   if (hasUpdated) {
       if (hooks[currentHook] && hooks[currentHook].cleanUp) {
           hooks[currentHook].cleanUp()
       }
       
       const  cleanup = effect();
       hooks[currentHook] = {
           deps,
           cleanup
       }
   }
   
   currentHook++
}

每当useEffect 的依赖有变化,组件会重新渲染,比如useEffect 的依赖项可以是 useState 的状态值。重新渲染当前的组件就会重新运行 useEffect。 每次运行useEffect都会把当前依赖项和前一次对应的每一个依赖项进行对比来确认是否有任何依赖项有改变。

如果有依赖项改变了,会先运行cleanup清除上一次effect, 用来确保副作用不会产生冲突,并且资源(如事件监听器、定时器等)能够正确释放,避免内存泄露。

又基于useEffect一个组件内部可以多次使用,我们需要创建一个数据结构可以顺序记录每一个useEffect。所以使用了 currentHook 顺序记录每一个 useEffect 的顺序。

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

代码测试

<!DOCTYPE html>
<html>
<head>
  <title>Mini React 测试 - useEffect</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];
    }

    // useEffect 实现
    function useEffect(effect, deps) {
      const hookIndex = currentHook;
      const oldHook = hooks[hookIndex];
      const hasChanged = !oldHook || !deps.every((dep, i) => dep === oldHook.deps[i]);

      if (hasChanged) {
        if (oldHook && oldHook.cleanup) {
          oldHook.cleanup();
        }

        const cleanup = effect();
        hooks[hookIndex] = { deps, cleanup };
      }

      currentHook++;
    }

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

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

      useEffect(() => {
        console.log(`Component mounted / count updated: ${count}`);

        return () => {
          console.log(`Cleaning up effect for count: ${count}`);
        };
      }, [count]);

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

    // 初始渲染
    renderApp();

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

useEffect 钩子在每次依赖变化或组件卸载时执行以下步骤:

  1. 检测依赖变化

    • 如果依赖项数组发生了变化(或者初次执行),useEffect 会重新执行副作用。
  2. 先运行上一次的清理函数(如果存在)。

    • 这一步确保之前的副作用不会残留,如:

      • 移除事件监听器。
      • 清除定时器。
      • 取消 API 请求等。
  3. 执行新的副作用

下一篇,我们将自己实现迷你版本的diff 算法以帮助我们增强对其原理的理解。

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

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

啥也不是,散会。