Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【源码】React hooks 分析 #30

Open
negativeentropy9 opened this issue Apr 2, 2020 · 0 comments
Open

【源码】React hooks 分析 #30

negativeentropy9 opened this issue Apr 2, 2020 · 0 comments

Comments

@negativeentropy9
Copy link
Owner

negativeentropy9 commented Apr 2, 2020

昨天 司徒正美 过世,据传病因为颈椎病,本人无处证实其真实病因。说是屌丝逆袭的典范,虽早已脱离穷的状态,仍然与人合租,生活简朴,一件冬衣穿多年,关心社会,具有责任感。R.I.P.愿大家都能珍惜当下,积极乐观生活,不留遗憾。

入口为 详见 packages/react-reconciler/src/ReactFiberHooks.js 中的 renderWithHooks 函数,下面主要从组件的生命周期中比较重要的 mountupdate 分析 hooks api 中的 useStateuseEffect

代码中混杂着 schedulesuspense 相关的,在这里可以暂时先忽略。本文章适用于对 hooks 有一定使用经验的开发者,如果您刚刚开始学习 React,建议先把 React 官方文档 看一遍,并且经过一定练习,然后按照下面的途径走一遍;如果您刚刚开始接触 hooks,建议先把 hooks 官方文档 看一遍,并且经过一定练习。

术语表

  • hooks api (useState/useEffect 等)
  • mount 阶段为组件挂载时
  • update 阶段为组件更新时

存储结构

采用 单链表 结构在 mount 函数组件时根据 hooks api 的调用顺序进行初始化(详见 packages/react-reconciler/src/ReactFiberHooks.js 中的 mountWorkInProgressHook 函数)

hooks 存储数据结构

其中 memoizedState 字段存储 state 值(useState),或者 effect 相关信息(useEffect);next 指向下一个 hooks 节点;queueuseState 下存储 dispatch 函数和 fiber 队列。

fiber 节点存储类型

fiber 节点存储类型

hooks 相关数据存储在 fiber 节点下的 memoizedState 字段下。

useState 节点存储类型

useState 节点存储类型

useEffect 节点存储类型

useEffect 节点存储类型

阶段

mount

mount 阶段,每调用一次 hooks api 都会按照顺序初始化单链表(详见 packages/react-reconciler/src/ReactFiberHooks.js 中的 mountWorkInProgressHook 函数)

update

update 阶段,每调用一次 hooks api 都会从单链表中取已经存储的 hook 节点数据(详见 packages/react-reconciler/src/ReactFiberHooks.js 中的 updateWorkInProgressHook 函数),useState 取出 memoizedStatequeue.dispatch

源码调试

使用 create-react-app 创建项目,React 基于 16.3.1 版本,node_moduels 下面的 react & react-dom 目录下的 cjs/*(react/react-dom).development.js 直接 debug,可以直接克隆项目进行调试。

$ git clone [email protected]:doudounannan/react-debugger.git
$ cd react-debugger
$ ck hooks
$ yarn install
$ yarn start

简单实现一个 hooks

以下代码只是简单实现了一个 mountupdate 初始化和更新的操作

class Event {
  name = "";
  poor = {};

  constructor(name) {
    this.name = name;
  }

  onOne(eventName, eventCb) {
    this.poor[eventName] = eventCb;
  }

  emit(eventName, data) {
    this.poor[eventName]();
  }
}

const event = new Event("simulation");

const currentFiberNode = {
  memoizedState: null
};

let currentHook = null;

let hasMounted = false;

function mountState() {
  const hooks = {
    next: null,
    dispatch: null
  };

  if (currentHook) {
    currentHook.next = hooks;
  } else {
    currentFiberNode.memoizedState = hooks;
  }

  currentHook = hooks;

  return hooks;
}

function updateState() {
  const hooks = {
    ...currentHook
  };

  currentHook = currentHook.next;

  return hooks;
}

function mountUseState(state) {
  if (typeof state === "function") {
    state = state();
  }

  const hooks = mountState();

  hooks.memoizedState = state;
  hooks.dispatch = action => {
    if (typeof action === "function") {
      hooks.memoizedState = action(hooks.memoizedState);
    } else {
      hooks.memoizedState = action;
    }
  };

  return [hooks.memoizedState, hooks.dispatch];
}

function updateUseState(state) {
  const hooks = updateState();

  return [hooks.memoizedState, hooks.dispatch];
}

function useState(state) {
  if (hasMounted) {
    return updateUseState(state);
  } else {
    return mountUseState(state);
  }
}

function commit(...data) {
  console.log(data);

  if (!hasMounted) {
    hasMounted = true;
  }

  currentHook = currentFiberNode.memoizedState;
}

function FC() {
  const [count, setCount] = useState(0);
  const [isHappy, setIsHappy] = useState(false);

  event.onOne("update-count", function updateCount() {
    console.log("debug-after-update-count");
    setCount(count => count + 1);
    FC();
  });

  event.onOne("update-isHappy", function updateIsHappy() {
    console.log("debug-after-update-isHappy");
    setIsHappy(!isHappy);
    FC();
  });

  console.log("debug-render: count & isHappy");
  commit(count, isHappy);
}

// like mount
FC();

// like update
event.emit("update-count");
event.emit("update-isHappy");

后续

  • reconciler
  • render phase
  • commit phase

FAQ

官方文档 其实已经介绍的非常详细了,常见问题 也已经总结的很全面。在这里主要从源码的角度上详细讲几个为什么要这样设计的原因(官方文档上很多是讲如果不这样做,会有怎样的问题,没有明确指出为什么)。

为什么使用单链表形式存储 hooks

现在 React 提供给 functional component 维护 local stateapi 只有一个 useState。跟 class componentsetState 不同的是:setState 将所有数据存储在一个对象中,调用其进行更新时,更新后的 state 和旧有 state 做浅合并;而 useState 提供了一个选项把每个 state 单值分别存储,再加上还有其他 hooks api,比如说 useEffectuseReduceruseContext 等,而且需要维护所有 hooks api render 时的状态,是不是单链表更好呢。是否有更合适的数据结构用来存储 hooks,暂时想不到

为什么在使用 函数 进行初始化 useState 时,可以做到惰性执行?

首先,无论是表达式还是函数都是作为 useState 的参数存在的,表达式作为形参来传递,每次函数调用都会运行表达式(详见 packages/react-reconciler/src/ReactFiberHooks.js 中的 updateState 函数)。看下面的例子,可能理解起来就没有那么困难了。

var count1 = 0;
var count2 = 0;

var expansiveComputeVariable = (function() {
  count1++;
  console.log("debug-expansiveComputeVariable-call");

  for (let i = 0; i < 10000; i++);

  return count1;
})();

var expansiveComputeFun = () => {
  count2++;
  console.log("debug-expansiveComputeFun-call");

  for (let i = 0; i < 10000; i++);

  return count2;
};

function fun(data) {
  return data;
}

console.log(
  "debug-fun(expansiveComputeVariable)",
  fun(expansiveComputeVariable)
);
console.log("debug-fun(expansiveComputeFun)", fun(expansiveComputeFun));

以上分析是按照个人认为能够理解的脉络阐述的,如果您认为哪里比较混乱,欢迎与我联系。如果哪里有理解上的错误,请一定帮忙指出。如果您也有阅读源码的兴趣,欢迎一起交流。

@negativeentropy9 negativeentropy9 changed the title React hooks 源码分析 【源码】React hooks 分析 Apr 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant