Read the latest article and comment on Notion.
所有的理解都基于React V16.11
两篇很有用的文章
-
Hooks 有什么好处?
赋予了函数式组件状态,用类组件来管理状态可能有如下缺点:
- 逻辑分散,处理同一个东西的逻辑可能会被分散在
componentDidMount
componentDidUpdate
componentDidUnmount
里面。
特别是useEffect
把一些副作用操作都统一了。 - 范式复杂,class 可能把一些原可以简单的组件弄复杂了。
- 逻辑分散,处理同一个东西的逻辑可能会被分散在
-
涉及到的数据结构有哪些?
-
这些数据结构的具体定义和交互是怎么样的?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3raXW2aU-1644039451266)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/91445c9d-3adb-49af-8b73-8b4e6c04cc8a/Untitled.png)]
Hook
export type hook = { memorizedState: any, baseState: any, baseUpdate: Update<any, any> | null, queue: UpdateQueue<<any, any> | null, next: Hook | null, };
-
hooks 通过链表连接在一起。
-
有
memorizedState
和baseState
的区别,两者的不同之处目前还不得而知,不过每次调用hook 返回的都是memorizedState
。 -
memorizedState
和baseState
有可能不同吗?有可能。
memorizedState
代表的是上一次hook 返回给组件的 state,而这个 state 有可能是“非法状态”。这个“非法状态”会在
updateReducer
里面诞生,当遍历queue
中的update
,计算新 state 的时候,有可能发生update.expirationTime < renderExpirationTime
的情况。此时hook 会选择不去执行update.action
,非法状态就就诞生了。当然,为了之后修正当前的这个非法状态,hook 就把
baseState
和baseUpdate
置成了第一次skip 时的 state 和 update,某个时刻重新从baseState
和baseUpdate
计算真正合法的 state。这是为了在资源不足的情况下优先完成高优先级任务。
不管怎么说,在资源和时间充足的情况下,从
baseState
和baseUpdate
重新追赶后就能得到正确的state。 -
为什么hook 的
queue
有时是循环链表,有时不是?参照后面的更新状态callback 具体实现的section。
Update
type Update<S, A> = { expirationTime: ExpirationTime, suspenseConfig: null | SuspenseConfig, action: A, eagerReducer: ((S, A) => S) | null, eagerState: S | null, next: Update<S, A> | null, priority?: ReactPriorityLevel, };
-
updates 也是通过链表连接在一起的。
-
update 有
expirationTime
suspenseConfig
priority
各种配置参数。 -
expirationTime
的具体作用?不知道。
-
啥是
suspenseConfig
?不知道。
-
为啥要有
eagerReducer
,和传入updateReducer()
中的reducer
参数互相区分?不知道。
UpdateQueue
type UpdateQueue<S, A> = { last: Update<S, A> | null, dispatch: (A => mixed) | null, lastRenderedReducer: ((S, A) => S) | null, lastRenderedState: S | null, };
-
dispatch
被封装过,供外界调用使用。所有的hook 都对同一个公共的dispatch
进行了封装。 -
queue
里面为什么还要特意封装一下lastRenderedReducer
和lastRenderedState
?用hook 自带的一些信息不好吗?不知道。
HooksDispatcherOnMount & HooksDispatcherOnUpdate
const HooksDispatcherOnMount: Dispatcher = { readContext, useCallback: mountCallback, useContext: readContext, useEffect: mountEffect, useImperativeHandle: mountImperativeHandle, useLayoutEffect: mountLayoutEffect, useMemo: mountMemo, useReducer: mountReducer, useRef: mountRef, useState: mountState, useDebugValue: mountDebugValue, useResponder: createResponderListener, }; const HooksDispatcherOnUpdate: Dispatcher = { readContext, useCallback: updateCallback, useContext: readContext, useEffect: updateEffect, useImperativeHandle: updateImperativeHandle, useLayoutEffect: updateLayoutEffect, useMemo: updateMemo, useReducer: updateReducer, useRef: updateRef, useState: updateState, useDebugValue: updateDebugValue, useResponder: createResponderListener, };
第一次mount 组件时用mount 系列的hooks 来执行相关的准备、创建操作;之后update 组件用的是update 系列的hooks ,根据之前的状态和操作来返回新的状态。
renderWithHooks()
里面有一句:nextCurrentHook = current !== null ? current.memoizedState: null; ReactCurrentDispatcher.current = nextCurrentHook === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;
其中
current
是传入renderWithHooks()
的第一个参数,是一个fiber,若current === null
则目前没有fiber,是首次加载。 -
-
-
首次加载时,hooks 相关的代码都做了什么?
-
具体创建hook 的代码是什么逻辑?
function mountWorkInProgressHook(): Hook { const hook: Hook = { memoizedState: null, baseState: null, queue: null, baseUpdate: null, next: null, }; if (workInProgressHook === null) { firstWorkInProgressHook = workInProgressHook = hook; } else { workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
创建一个hook,并将其设置为全局变量
workInProgressHook
,同时维护一个hooks 链表,每次都把新创建的hook 放在这个链表的最后。
-
-
useState
和useReducer
系列的hooks 可以返回一个更改hooks 状态的callback,这里的状态更新是咋实现的,有没有race condition?useState
其实就是特殊情况下的useReducer
,两者返回的callback 其实就是首次加载中bind 到hook.queue.dispatch
上的dispatch 函数。-
状态更新就是把传入的action 给添加到queue 的最后,并发出一个请求re-render 的事件。
-
当然可能存在race condition,这个时候就创建一个key 为queue 的map,缓存一下在re-render 期间发起的action,如果race condition 被引发多次,就把actions 串成一个链表。
- 除此之外,根据这个文件中的说明,fiber 级别还会有对race condition 的处理,当进行re-render 时,update 链表会分别在current pointer 和work-in-progress pointer 里面被维护,这两个pointer 我理解被存在了不同的fiber 里面。每当有一个新update 事件被添加时,这个update 会被同时添加进这两个pointer or fiber 里,以防止在某些race condition 下被添加的新update 事件丢失。
-
把action 添加到queue 的操作是咋样的,这个queue 是单链表对吗?
不是单链表,当
queue.last === null
的时候,被创建出的是一个循环链表。至于为什么是一个循环链表,是因为
queue.last === null
这个条件只会在mount 的时候发生,mount or commit 之后的re-render 是要从第一个update 开始的,考虑到:- 循环链表的
last.next
就是链表的头节点。 - 除了第一次re-render 后,之后的更新都是从最后一次update 之后的 update 开始更新了,那么寻求的就是update.next。
我觉得使用循环链表可以达到以下目的:
- 少存一个头节点,只有mount or commit 前才需要头节点,这个时候从
last.next
取就好了,其他情况也不需要头节点,此时queue 就不是循环链表了。 - 范式的统一。
- 循环链表的
-
-
react render 的时候是怎么知道此时是mount 还是update?
根据当前fiber 是否为null 来判断。
-
在首次加载后,再次调用hooks 发生了什么?
首次加载之后的hooks 就是update 系列的hooks 了,再拿
useState
和useReducer
来看,useState
会直接调用updateReducer
,而首次加载后的useReducer
就是updateReducer
。当
updateReducer
被调用时,其会判断当前是否处于re-render 阶段,如果是,按我们上面说的一样,去找key 为queue 的map,取出update 事件链表,计算出新的state。如果不是re-render 阶段,那么就遍历queue 里面的update ,计算出新的state。
不过这里要注意一下特殊情况,会有资源不足而跳过一些update 的情况,参照上文
*memorizedState
和baseState
有可能不同吗?* 这个问题。-
调用hook 后,具体被操作的hook 是怎么被拿到的?
function updateWorkInProgressHook(): Hook { if (nextWorkInProgressHook !== null) { // There's already a work-in-progress. Reuse it. workInProgressHook = nextWorkInProgressHook; nextWorkInProgressHook = workInProgressHook.next; currentHook = nextCurrentHook; nextCurrentHook = currentHook !== null ? currentHook.next : null; } else { // Clone from the current hook. currentHook = nextCurrentHook; const newHook: Hook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, queue: currentHook.queue, baseUpdate: currentHook.baseUpdate, next: null, }; if (workInProgressHook === null) { workInProgressHook = firstWorkInProgressHook = newHook; } else { workInProgressHook = workInProgressHook.next = newHook; } nextCurrentHook = currentHook.next; } return workInProgressHook; }
先看一看有没有workInProgressHook,如果有的话,直接使用,如果没有,从之前留着的hooks 链表里面拷贝需要的hook 出来,这样的结果就是每次re-render 完之后,都多了一个新版本的hooks 链表,我猜这里可能和fiber 涉及到的调解算法有关。
-
-
useEffect
的实现?function commitHookEffectList( unmountTag: number, mountTag: number, finishedWork: Fiber, ) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQuere: any); let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { if ((effect.tag & unmountTag) !== NoHookEffect) { // Unmount const destory = effect.destroy; effect.destory = undefined; if (destory !== undefined) { destory(); } } if ((effect.tag & mountTag) !== NoHookEffect) { // Mount const create = effect.create; effect.destory = create(); } effect = effect.next; } whlie (effect !== firstEffect); } }
这里可以发现,effect 的链表始终是循环链表,因为effect 一旦mount 了之后数量就是恒定的,永远不会受到其他事件的影响,所以每次从
lastEffect.next
走起就好了。 -
useState
和useEffect
有何不同?useState
是在fiber 里面创建初始状态,然后返回一个callback 让用户来schedule 新一轮的re-render。useEffect
是在fiber 里面创建初始状态,然后在每次fiber commit 之后(render 完成之后),依次执行一些副作用操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-umykITuK-1644039451267)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7d16a0c5-e596-41a7-b913-d228b5dd19db/Untitled.png)]
-
update 的
expirationTime
是何时计算的?有何作用?是在创建update 的时候就决定的,用来帮助fiber 确定update 的优先级,fiber 因为其充分利用device 资源的特性,会根据当前的资源来决定是否进行当前的update,如果资源不足,则跳过,记录被跳过的第一个update 为
baseUpdate
待之后又获得资源后从baseUpdate
重新更新。 -
talk about fiber
这可能是最通俗的 React Fiber(时间分片) 打开方式
-
啥是fiber
解读一:fiber 相当于是react 中的协程。因为协程之间是主动协调任务的,所以其实react 无法对fiber 进行抢占式调度,全凭使用者的自觉,一般fiber 会主动检查是否超过了浏览器给的运行时间,如果超过就保存现场,把控制权还给浏览器(一般来说这个运行时间是16ms,1000ms / 60)。
解读二:react 中的执行单元,每执行完一个fiber 就检查一下剩余时间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NzUEiDLE-1644039451268)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3fc0f219-6bea-4400-a886-60e57ae173be/Untitled.png)]
-