手写 useState

569 阅读3分钟

极简前端史

了解 react 的诞生

image

  • 1994 年,Netscape(网景公司) 公开发行了 Netscape 浏览器。这个版本的浏览器只能用来浏览静态页面,不具备与访问者互动的能力;

  • 1995 年,微软获得了旧版 Mosaic 的代码授权,建立 IE;

  • 1995 年 5 月,艾克只用了 10 天,就设计完成了 javascript 的第一版,但是他并不喜欢这个产物;

  • 2009 年,jQuery 通过 Sizzle 选择器引擎,取得了压倒性的优势;

  • 随着前端代码复杂性的提升

  • 三大框架出现,Angular(2009)、React(2011)、Vue(2014);

历史的启发:

1、发现问题-解决问题是前进的动力;

2、分治是解决问题的重要方法,比如:前端三剑客、MVC/MVVM、模块化、组件化;

3、后发优势,少走弯路,比如前端借鉴后端成熟的框架设计;

4、未来是难以预测的,否则艾克也不会消极对待 javascript;

React 底层机制

image

解决了什么问题?

react 解决了两个问题:快速构建和快速响应。快速构建,是从开发者视角来说的,提升生产效率;

快速响应,是从用户视角来说的,提升用户体验。

如何才能构建快速响应的应用?这里有两类制约因素:CPU 瓶颈IO 瓶颈

  • CPU 瓶颈:CPU 密集型操作,如果在 16.6 ms 中无法完成 js 执行和页面渲染就会导致掉帧,这部分是可以从框架层面去优化的,优化思路有两个:减少 cpu 计算和将 cpu 计算拆分成多个任务

  • IO 瓶颈:比如网络请求、文件读写,这部分是不可控的,但是可以从交互的角度去优化;

如何解决这 IO 和 CPU 瓶颈呢?IO 瓶颈是不可控的部分,框架层面无法解决,这里先不考虑,那 CPU 瓶颈如何解决呢?

react 采用的策略是异步可中断更新,它并没有减少 cpu 计算,而是将 cpu 计算拆分成多个任务去调度。react 的渲染分成 render 阶段和 commit 阶段,render 阶段主要是构建 fiber 树、打 effectTag,commit 阶段根据 effectTag 做 DOM 变更和渲染。

其中 render 阶段是比较耗时,所以 react 把 render 阶段设计成可中断的结构,通过时间片的方式调度。

image

渲染机制

首先看一下 react 的虚拟 DOM 结构(Fiber 树)

image

image

react 的渲染逻辑

image

Hooks 出现的原因

image

  1. 困难:难以复用带状态的逻辑

    1. renderProps/HOC 存在问题

      1. 需要改写原有组件结构

      2. wrapper hell

  2. 困境:复杂组件变得难以理解

    1. 无关逻辑混合在一起或者状态散落在各地,不能拆分也不容易测试
  3. class 的原罪:让人和机器都很困惑

    1. AOT 编译优化经常会失效;

    2. 代码不能很好的压缩

    3. 让热更新变得不可预测;

写一个 useState

假设有一个组件 App

image

定义 useState 以及相关的数据结构

image

处理 fiber 的 memoizedState( hook 链表)

image

计算 hook 的最新状态值

image

实现 dispatch 函数

image

image

实现调度函数

image

完整代码:code.byted.org/biz-platfor…


let isMounted = false;
// 在 App 渲染前
let workInProgressHook = null;

let isFlushing = false;

let fiber = {
  render: App,
  memoizeState: null,
}

function scheduleUpdateOnFiber() {
  workInProgressHook = fiber.memoizeState;
  const app = fiber.render();
  isMounted = true;
  return app;
} 

function dispatchSetState(queue, action) {
  const update = {
    action,
    next: null,
  };

  if (queue.pending == null) {
    // 1 -> 1
    // 1 -> 2 -> 1
    update.next = update;
  } else {
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  queue.pending = update;

  if (!isFlushing) {
    isFlushing = true;
    queueMicrotask(() => {
      scheduleUpdateOnFiber();
      isFlushing = false;
    });
  }
}

function useState(initState) {
  // 1. 创建 hook 对象,拼接到 fiber.memoizedState
  let hook = null;
  if (!isMounted) {
    hook = {
      memoizeState: initState,
      queue: {
        // update 对象链表
        pending: null,
      },
      next: null,
    };

    if (fiber.memoizeState === null) {
      fiber.memoizeState = hook;
    } else {
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;
  } else {
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }

  // 2. 计算最新的 baseState
  let baseState = hook.memoizeState;
  const queue = hook.queue;

  if (queue.pending !== null) {
    let curUpdate = queue.pending.next;
    do {
      baseState = curUpdate.action(baseState);
      curUpdate = curUpdate.next;
    } while(curUpdate && curUpdate !== queue.pending.next);
    queue.pending = null;
    hook.memoizeState = baseState;
  }

  // 3. return [baseState, dispatch];
  return [baseState, dispatchSetState.bind(null, queue)];
}


function App() {
  const [counter, setCounter] = useState(0);
  const [counter1, setCounter1] = useState(0);

  console.log('render ->', counter);

  return {
    onClick() {
      setCounter(c => c + 1);
      setCounter(c => c + 1);
      setCounter(c => c + 1);
      setCounter(c => c + 1);
    }
  }
}

window.app = scheduleUpdateOnFiber();

参考

React技术揭秘

通俗易懂的代数效应

React Fiber Architecture

Introducing Hooks – React

尤雨溪 - 在框架设计中寻求平衡