Profile
GitHub

React hooks 数据结构

代数效应

第一部分我们首先介绍一下react hooks践行的理念——代数效应,以及它与react hooks的联系 首先是定义:

把副作用从 函数 中剥离 Alt text

下面请看一些例子

同步

如果我们想从一个函数同步地获取值,可以得到如下代码

function getTotalCommentNum(id1, id2) {
	const num1 = getCommentNum(id1)
	const num2 = getCommentNum(id2)
	
	return num1 + num2
}

异步

如果一个函数的返回值是异步的,最简单的方法如下

async function getTotalCommentNum(id1, id2) {
	const num1 = await getCommentNum(id1)
	const num2 = await getCommentNum(id2)
	
	return num1 + num2
}

缺点:async\await 具有传染性,调用函数也需要变成async函数

代数效应

假设,我们可以 虚构一个如下语法(协程)

function getCommentNum(id){
	const num = perform id
	
	return num
}

perform {
	getTotalCommentNum(1, 2)
} handle(id) {
	
	// switch(id) {
		// case 1:
			// resume with 111
			// break
		// case 2:
			// resume with 222
			// break
		// default: 
		// resume with 0;
	// }
	
	// 异步(比如fetch)
	fetch("xx").then(res => res.json()).then(({num}) => resume with num)
}

这样我们就可以顺利地把一些副作用从我们的函数中** 剥离 **出去

与react 关系

那么上面这段虚构的语法与react hooks有什么关系呢?

下面是我们常用的 用useEffect获取数据的写法

function TotalCommentNum(id1, id2) {
	const num1 = useCommentNum(id1)
	const num2 = useCommentNum(id2)
	
	return <div> { num1 + num2 } </div>
}

function useCommentNum(id){
	const [num, setNum] = useState(0)
	
	useEffect(() => {
		fetch("xx").then(res => res.json()).then(({num}) => setNum(num))
	
	}, [id])
	
	return num
}

可以发现,我们利用useEffect顺利地规避了async/await的传染 所以,我们可以得到结论:react hooks 虽然不是真正的代数效应,但是其利用了某些特性践行了代数效应的思想

引申(server component)

根据上面我们获取数据的自定义hooks,我们发现,其实如果把组件放到服务端直接与接口、数据库交互,那么我们前后端代码都会变得非常组件化和统一,这样前端就得以数据、逻辑、ui真正的闭环了

hooks数据结构 实现

下面这一小节我将介绍react hooks 的基本数据结构和执行流程,并给出对应的简易代码实现

使用

首先,在忽略jsx的前提下,为了简化代码,我们的组件将会以如下方式定义

function App() {
  const [num, setNum] = useState(0);

  console.log('isMount?', isMount);
  console.log('num:', num)

  return {
    onClick() {
      setNum(num => num + 1)
      setNum(num => num + 2)
      setNum(num => num + 3)
    }
  }
}

它会返回一个包含setState 的对象

运行时环境

为了组件能运行,react定义了一些运行时环境,保存整个组件的状态

fiber节点

在react 中每个组件对应一个fiber node

const fiber = {
    // 对应组件实例
  stateNode: App,
  // 保存该FunctionComponent对应的Hooks链表
  memoizedState: null
}
  • stateNode: 对应的组件
  • memoizedState: 对应组件的hooks(链表)

全局变量

let isMount = true

此变量用来控制hook的初始化(与真实react有区别,下文会介绍)

schdule调度函数

用来组织整个fiber的运行流程

function schedule() {
  workInProgressHook = fiber.memoizedState;
  const app = fiber.stateNode();
  isMount = false;
  return app;
}
  • workInProgressHook:指向fiber的第一个hook节点,后文再详细解释
  • app:对应的组件
  • isMount:为了方便,我们用一个全局变量来区分是否为第一次渲染(mount),真实react如何处理后文会进行对比

调用组件

以下是我们如何对一个组件进行调用,入口即为schedule函数

const  app = schedule()

const clickMe = () =>{
    app.onClick()
}

schedule函数会对我们的app组件进行渲染

顺序如下:

useState

首先useState在mountrerender阶段需要分两种情况讨论

mount

初始化hook

let workInProgressHook = null

function useState(initialState) {
  let hook;

  if (isMount) {//首次加载组件, 新建对应状态hook
    hook = {
      memoizedState: initialState,// 状态
      next: null, // 下一个hook
      queue: { // 一个state的更新循环链表
        pending: null
      }
    }
    // 这里是更新链表的过程:新增节点
    if (!fiber.memoizedState) { // fiber.hook1 -> hook2 -> hook3 ->.....
      fiber.memoizedState = hook;
    } else {
      workInProgressHook.next = hook; // 让上一个hook指向当前的hook,更新workInProgressHook
    }
    /**
     *  fiber.hook1 -> hook2 -> hook3 ->.....
     *                            ↑
     *                     workInProgressHook
     */
    workInProgressHook = hook; // workInProgressHook 记录链表最后一个 节点
  } 

这里需要解释的是,在第一次mount阶段,我们要为每一个hook定一个数据结构,

  • memoizedState: 改hook的状态
  • next:下一个hook
  • queue:update

并借助新定义的 workInProgressHook指针(总是指向最后一个hook节点),插入到前文提到的fiber.memoizedState链表的末尾,最后产生的链表如下图所示 hooks 数据结构queue.gif

useState的返回

众所周知,useState返回一个数组,包含 state 和 dispatch函数,代码如下:

return [hook.memoizedState, dispatchAction.bind(null, hook.queue)]

在mount阶段,state是initState

render(主动触发dispatchAction)

setState(1)

dispatchAction

实质就是调用了dispatchAction这个函数

dispatchAction为一个被传入hook update 队列的函数:

function dispatchAction(queue, action) {
  // 待更新状态的链表节点
  const update = {
    action,
    next: null
  }

  if (queue.pending === null) {
        update.next = update;
      } else {
        update.next = queue.pending.next;
        queue.pending.next = update;
      }
      queue.pending = update;
 
  schedule();
}

可以看到 dispatchAction接受两个参数

  • queue: hook节点的update队列
  • action: state的更新函数

其中这个 queue 需要特别说明:

由于我们可能会同时调用多次setState

function conClick(){
	setState(1)
	setState(2)
	setState(3)
}

所以我们同样需要将他们放入一个链表当中 而由于真实react 更新有优先级。比如:点击 > 请求。为了使用户交互更顺滑,环状链表更方便定位action,所以这边的update action会被放入一个环状链表中 如图所示:

  • p: pending
  • u: update

hooks 数据结构update.gif

调用schedule

同上

再次渲染(fiber.stateNode)

此时的useState主要需要做两件事

  • 确认当前调用的是哪个hook
  • 根据之前状态,更新该hook的state
function useState(initialState) {
  let hook;

  if (isMount) {//首次加载组件, 新建对应状态hook
   //
  } else { 
  	// 定位当前hook
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }

  let baseState = hook.memoizedState;

    // 遍历更新队列
    if (hook.queue.pending) {
        let firstUpdate = hook.queue.pending.next;
    
        do {
          const action = firstUpdate.action;
          baseState = typeof action === 'function' ?action(baseState) : action;
          firstUpdate = firstUpdate.next;
        } while (firstUpdate !== hook.queue.pending.next)
    }
    //更新当前hook的state
    hook.memoizedState = baseState;
    hook.queue.pending = null;

    return [baseState, dispatchAction.bind(null, hook.queue)]
}

至此,一个hook的mountrerender都已经闭环,可以完整的运行了

与真实react 的区别

reactFiberHooks

isMount

真实的hooks 区分mount不实用 isMount变量,而是各自定义一个不同状态的state(多态?)

useState

不同状态下useState不一样 useState-1 useState-2

mountState

Alt text Alt text

updateState

Alt text Alt text

包含跳出重入机制等等..

Alt text Alt text

基于baseState 更新state

Alt text Alt text Alt text ...200行

useReducer

useState === 预置useReducer的 useReducer

Alt text Alt text

useEffect

mountEffect

Alt text Alt text Alt text

updateEffect

Alt text Alt text Alt text

useRef

mountRef

Alt text

updateRef

Alt text

useMemo

总结

问题

  1. setState()调用后,组件是怎么更新的?
  2. 多个不同hooks是怎么区分,怎么按什么顺序执行的?
  3. 不同的setState()被调用,是怎么对应到各自的state值的
  4. useEffect 是在什么阶段执行的?

数据结构

const fiber = {
    stateNode: App, // 组件函数本身
    memoizedState: { // 指向第一个hook的链表
        memoizedState: 0, // 此hook的状态
        queue: { // 此hook的更新队列,是一个环状链表
            pending: { // 指向最新的更新
                action: someActionFunction, // 更新函数
                next: null, // 在链表中指向下一个更新
            },
        },
        next: { // 指向下一个hook的指针
            memoizedState: null, // 下一个hook的状态
            queue: { // 下一个hook的更新队列
                pending: null, // 下一个hook的更新
            },
            next: null, // 如果没有更多的hooks了,那么这里是null
        },
    },
    // Fiber节点还有其他属性,比如child、sibling、return等,用于构建Fiber树
};

拓展

  1. fiberHook如何实现优先级
  2. 如何合并多个更新
  3. commit函数的实现、调度原理

附录

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button onclick="clickMe()">点击 setNumber</button>
    <button onclick="clickBlur()">点击 setName</button>
    <script src="./main.js">
    </script>
</body>
</html>
// main.js
const updateState = (hook) => {
    let baseState = hook.memoizedState;
    let firstUpdate = hook.queue.pending.next;

    do {
      const action = firstUpdate.action;
      baseState = typeof action === 'function' ?action(baseState) : action;
      firstUpdate = firstUpdate.next;
    } while (firstUpdate !== hook.queue.pending.next)

    return baseState

}

const createUpdateQueue = (update, queue) => {
    if (queue.pending === null) {
        // 只有一个的时候 update1 -> update1
        update.next = update;
      } else {
      /**
       * 两个:update2 -> update1 -> update2  ,  queue.pending = update2
       * 三个:update3 -> update1 -> update2 -> update3
       * 假设有3个update,queue.pending对应最新那个update
       *                    ↓ ←   ←  ←   ←   ←  ← ↑
       * queue.pending = update3 -> update1 -> update2
       *                    ↑      
       *                queue.penging
       */
        update.next = queue.pending.next;
        queue.pending.next = update;
      }
      queue.pending = update;
  }


let isMount = true; // 是否首次加载
let workInProgressHook = null; // 当前运行的hook

// 组件fiber的状态节点,这里全局
const fiber = {
    // 指向App函数
  stateNode: App,
  // 保存该FunctionComponent对应的Hooks链表
  memoizedState: null
}

function useState(initialState) {
  let hook;

  if (isMount) {//首次加载组件, 新建对应状态hook
    hook = {
      memoizedState: initialState,// 状态
      next: null, // 下一个hook
      queue: { // 一个state的更新循环链表
        pending: null
      }
    }
    // 这里是更新链表的过程:新增节点
    if (!fiber.memoizedState) { // fiber.hook1 -> hook2 -> hook3 ->.....
      fiber.memoizedState = hook;
    } else {
      workInProgressHook.next = hook; // 让上一个hook指向当前的hook,更新workInProgressHook
    }
    /**
     *  fiber.hook1 -> hook2 -> hook3 ->.....
     *                            ↑
     *                     workInProgressHook
     */
    workInProgressHook = hook; // workInProgressHook 记录链表最后一个 节点
  } else { // 组件已mount
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }

  console.log("hook:", hook)
  console.log("workInProgressHook:",workInProgressHook)
  let baseState = hook.memoizedState;

  if (hook.queue.pending) {
    baseState = updateState(hook)
    hook.queue.pending = null;
  }
  //更新当前hook的state
  hook.memoizedState = baseState;
  return [baseState, dispatchAction.bind(null, hook.queue)]
}
/**
 * 
 * @param {Object} queue 
 * @param {Function|String|Object|Number|...} action 要更新的状态
 */
let timer = null
function dispatchAction(queue, action) {
  // 待更新状态的链表节点
  const update = {
    action,
    next: null
  }

  createUpdateQueue(update, queue)
 
//   console.log("queue:",queue.pending)
//   if(timer)return;
//   timer = setTimeout(()=>{
//     timer = null
    // react 使用scheduleUpdateOnFiber 来更新
    // 会有 任务合并、优先级高的任务先执行、可中断等优化,有兴趣可以看看
    schedule();
//   },100)
}

function schedule() {
  workInProgressHook = fiber.memoizedState;
  const app = fiber.stateNode();
  isMount = false;
  return app;
}

function App() {
  const [num, setNum] = useState(0);
  const [num22, setNum22] = useState(0);
  const [name, setName] = useState('aaa');

  

  console.log('isMount?', isMount);
  console.log('num:', num)
//   console.log('num22:', num22)
  console.log('name:', name)


  return {
    onClick() {
      setNum(num => num + 1)
      setNum(num => num + 2)
      setNum(num => num + 3)
      // updateNum22(num => num + 10)
    },
    // onFocus() {
    //   setNum22(num => num + 10)
    // },
    onBlur(){
      setName('leofhe')
    }
  }
}


const  app = schedule()

const clickMe = () =>{
    app.onClick()
}

const clickBlur = () =>{
    app.onBlur()
}


引用

react 技术揭秘