共计 39218 个字符,预计需要花费 99 分钟才能阅读完成。
react-redux 这个库想必相熟 react 的人都不生疏,用一句话形容它就是:它作为『redux 这个框架无关的数据流治理库』和『react 这个视图库』的桥梁,使得 react 中能更新 redux 的 store,并能监听 store 的变动并告诉 react 的相干组件更新,从而能让 react 将状态放在内部治理(有利于 model 集中管理,能利用 redux 单项数据流架构,数据流易预测易保护,也极大的不便了任意层级组件间通信等等益处)。
react-redux 版本来自截止 2022.02.28 时的最新版本 v8.0.0-beta.2(有点悲催的是,读源码的时候还是 7 版本,没想到刚读完 git pull
一下就升到 8 了,所以把 8 又看了一遍)
react-redux 8
相比于 7 版本包含但不限于这些扭转:
- 全副用 typescript 重构
- 原来的 Subscription class 被 createSubscription 重构,用闭包函数代替 class 的益处,讲到那局部代码的时候会提到。
- 应用 React18 的 useSyncExternalStore 代替原来本人实现的订阅更新(外部是
useReducer
),useSyncExternalStore
以及它的前身 useMutableSource 解决了 concurrent 模式下的tearing
问题,也让库自身的代码更简洁,useSyncExternalStore
相比于前辈useMutableSource
不必关怀selector
(这里说的是useSyncExternalStore
的 selector,不是 react-redux)的 immutable 心智累赘。
上面的局部和源码解析没有间接关系,但读了也能有所播种,也能明确为什么要写这篇文章。想间接看源码解析局部的能够跳转到 React-Redux 源码解析局部
注释前的吹水阶段 1:既然是『再读』,那『首读』呢?
不晓得大家平时在逛技术论坛的时候,有没有看见过相似这样的评论:redux 性能不好,mobx 更香……
喜爱刨根问底的人(比方我)看到了不禁想问更多问题:
- 到底是 redux 性能不好还是 react-redux 性能不好?
- 具体不好在哪里?
- 能不能防止?
这些问题你问了,可能失去的也是喋喋不休,不够深刻。与此同时还有一个问题,react-redux 是如何关联起 redux 和 react 的?这个问题倒是有不少源码解析的文章,我已经看过一篇很具体的,不过很惋惜是老版本的,还在用 class component,所以过后的我决定本人去看源码。过后属于是粗读,读完之后的简略总结就是 Provider 中有 Subscription 实例,connect 这个高阶组件中也有 Subscription 实例,并且有负责本身更新的 hooks: useReducer,useReducer 的 dispatch 会被注册进 Subscription 的 listeners,listeners 中有一个办法 notify 会遍历调用每个 listener,notify 会被注册给 redux 的 subscribe,从而 redux 的 state 更新后会告诉给所有 connect 组件,当然每个 connect 都有查看本人是否须要更新的办法 checkForUpdates 来防止不必要的更新,具体细节就不说了。
总之,过后我只粗读了整体逻辑,然而能够解答我下面的问题了:
- react-redux 的确有可能性能不好。而至于 redux,每次 dispatch 都会让 state 去每个 reducer 走一遍,并且为了保证数据 immutable 也会有额定的创立复制开销。不过
mutable
营垒的库如果频繁批改对象也会导致 V8 的对象内存构造由程序构造变成字典构造,查问速度升高,以及内联缓存变得高度超态,这点上 immutable 算拉回一点差距。不过为了一个清晰牢靠的数据流架构,这种级别的开销在大部分场景算是值得,甚至忽略不计。 - react-redux 性能具体不好在哪里?因为每个 connect 不论需不需要更新都会被告诉一次,开发者定义的 selector 都会被调用一遍甚至多遍,如果 selector 逻辑低廉,还是会比拟耗费性能的。
- 那么 react-redux 肯定会性能不好吗?不肯定,依据下面的剖析,如果你的 selector 逻辑简略(或者将简单派生计算都放在 redux 的 reducer 里,然而这样可能不利于构建一个正当的 model),connect 用的不多,那么性能并不会被 mobx 这样的细粒度更新拉开太多。也就是说 selector 里业务计算不简单、应用全局状态治理的组件不多的状况下,齐全不会有可感知的性能问题。那如果 selector 外面的业务计算简单怎么办呢?能不能完全避免呢?当然能够,你能够用 reselect 这个库,它会缓存 selector 的后果,只有原始数据变动时才会从新计算派生数据。
这就是我的『首读』,我带着目标和问题去读源码,当初问题曾经解决了,按理说所有都完结了,那么『再读』是因何而起的呢?
注释前的吹水阶段 2:为什么要『再读』?
前段时间我关注了一个 github 上的 React 状态治理库zustand
。
zustand 是一个十分时尚的基于 hooks 的状态治理库,基于简化的 flux 架构,也是 2021 年 Star 增长最快的 React 状态治理库。能够说是 redux + react-redux 的无力竞争者。
它的 github 结尾是这样介绍的
粗心是:它是一个玲珑、疾速、可扩大的、应用简化的 flux 架构的状态治理解决方案。有基于 hooks 的 api,应用起来非常舒服、人性化。
不要因为它很可恶而漠视它(貌似作者把它比喻成小熊了,封面图也是一个可恶的小熊)。它有很多的爪子,花了大量的工夫去解决常见的陷阱,比方可怕的子代僵尸问题(zombie child problem),react 并发模式(react concurrency),以及应用 portals 时多个 render 之间的 context 失落问题(context loss)。它可能是 React 畛域中惟一一个可能正确处理所有这些问题的状态管理器。
外面讲到一个货色:zombie child problem。当我点进 zombie child problem 时,是 react-redux 的官网文档,让咱们一起来看看这个问题是什么以及 react-redux 是如何解决的。想看原文能够间接点链接。
“Stale Props” and “Zombie Children”(过期 Props 和僵尸子节点问题)
自 v7.1.0 版本公布当前,react-redux 就能够应用 hooks api 了,官网也举荐应用 hooks 作为组件中的默认应用办法。然而有一些边缘状况可能会产生,这篇文档就是让咱们意识到这些事的。
react-redux 实现中最难的中央之一就是:如果你的 mapStateToProps 是 (state, ownProps) 这样应用的,它将会每次被传入『最新的』props。始终到版本 4 都始终有边缘场景下的反复的 bug 被报告,比方:有一个列表 item 的数据被删除了,mapStateToProps 外面就报错了。
从版本 5 开始,react-redux 试图保障
ownProps
的一致性。在版本 7 外面,每个connect()
外部都有一个自定义的 Subscription 类,从而当 connect 外面又有 connect,它能造成一个嵌套的构造。这确保了树中更低层的 connect 组件只会在离它最近的先人 connect 组件更新后才会承受到来自 store 的更新。然而,这个实现依赖于每个connect()
实例外面覆写了外部 React Context 的一部分(subscription 那局部),用它本身的 Subscription 实例用于嵌套。而后用这个新的 React Context (\<ReactReduxContext.Provider\>) 渲染子节点。如果用 hooks,没有方法渲染一个 context.Provider,这就代表它不能让 subscriptions 有嵌套的构造。因为这一点,”stale props” 和 “zombie child” 问题可能在『用 hooks 代替 connect』的利用里从新产生。
具体来说,”stale props” 会呈现在这种场景:
- selector 函数会依据这个组件的 props 计算出数据
- 父组件会从新 render,并传给这个组件新的 props
- 然而这个组件会在 props 更新之前就执行 selector(译者注:因为子组件的来自 store 的更新是在 useLayoutEffect/useEffect 中注册的,所以子组件先于父组件注册,redux 触发订阅会先触发子组件的更新办法)
这种旧的 props 和最新 store state 算进去的后果,很有可能是谬误的,甚至会引起报错。
“Zombie child” 具体是指在以下场景:
- 多个嵌套的 connect 组件 mounted,子组件比父组件更早的注册到 store 上
- 一个 action dispatch 了在 store 里删除数据的行为,比方一个 todo list 中的 item
- 父组件在渲染的时候就会少一个 item 子组件
- 然而,因为子组件是先被订阅的,它的 subscription 先于父组件。当它计算一个基于 store 和 props 计算的值时,局部数据可能曾经不存在了,如果计算逻辑不留神的话就会报错。
useSelector()
试图这样解决这个问题:它会捕捉所有来自 store 更新导致的 selector 计算中的报错,当谬误产生时,组件会强制更新,这时 selector 会再次执行。这个须要 selector 是个纯函数并且你没有逻辑依赖 selector 抛出谬误。如果你更喜爱本人解决,这里有一个可能有用的事项能帮忙你在应用
useSelector()
时防止这些问题
- 不要在 selector 的计算中依赖 props
- 如果在:你必须要依赖 props 计算并且 props 未来可能发生变化、依赖的 store 数据可能会被删除,这两种状况下时,你要防范性的写 selector。不要间接像
state.todos[props.id].name
这样读取值,而是先读取state.todos[props.id]
,验证它是否存在再读取todo.name
因为connect
向 context provider 减少了必要的Subscription
,它会提早执行子 subscriptions 直到这个 connected 组件 re-rendered。组件树中如果有 connected 组件在应用useSelector
的组件的下层,也能够防止这个问题,因为父 connect 有和 hooks 组件同样的 store 更新(译者注:父 connect 组件更新后才会更新子 hooks 组件,同时 connect 组件的更新会带动子节点更新,被删除的节点在此次父组件的更新中曾经卸载了:因为上文中说state.todos[props.id].name
,阐明 hooks 组件是下层通过 ids 遍历进去的。于是后续来自 store 的子 hooks 组件更新不会有被删除的)
以上的解释可能让大家明确了 “Stale Props” 和 “Zombie Children” 问题是如何产生的以及 react-redux 大略是怎么解决的,就是通过子代 connect 的更新被嵌套收集到父级 connect,每次 redux 更新并不是遍历更新所有 connect,而是父级先更新,而后子代由父级更新后才触发更新。然而仿佛 hooks 的呈现让它并不能完满解决问题了,而且具体这些设计的细节也没有说到。这部分的纳闷和缺失就是我筹备再读 react-redux 源码的起因。
React-Redux 源码解析
react-redux 版本来自截止 2022.02.28 时的最新版本 v8.0.0-beta.2
浏览源码期间在 fork 的 react-redux 我的项目中写下了一些中文正文,作为一个新我的项目放在了 react-redux-with-comment 仓库,阅读文章须要对照源码的能够看一下,版本是 8.0.0-beta.2
在讲具体细节之前我想先说一下总体的形象设计,让大家心中带着设计蓝图去读其中的细节,否则只看细节很难让它们之间串联起来明确它们是如何独特合作实现整个性能的。
React-Redux 的 Provider 和 connect 都提供了本人的贯通子树的 context,它们的所有的子节点都能够拿到它们,并会将本人的更新办法交给它们。最终造成了 根 <– 父 <– 子 这样的收集程序。根收集的更新办法会由 redux 触发,父收集的更新办法在父更新后再更新,于是保障了父节点被 redux 更新后子节点才更新的程序。
简略的宏观设计就如上所示,首次看不能了解的很深刻,不过没关系,多看几遍源码和源码剖析后再回过头看看这里会有新的播种。
首先从我的项目构建入口看起
能够看出它的 umd 包是通过 rollup 构建的(build:umd
、build:umd:min
),esm 和 commonjs 包是通过 babel 编译输入的(build:commonjs
、build:es
)。咱们只看 build:es
:"babel src --extensions \".js,.ts,.tsx\"--out-dir es"
。意思是应用 babel 转换 src 目录下的.js,.ts,.tsx
文件并输入到 es 目录(这一点和业务我的项目有些区别,因为 npm 包并不需要打包为一个文件,否则装置的不同 npm 包之间可能会打包进反复依赖,每个文件仍然放弃 import 引入只是内容编译就能够了,最终在开发者的我的项目里会把它们构建到一起的)。
上面看一下.babelrc.js 做了什么
能够看到 babel 的 presets 中的 @babel/preset-typescript
负责将 ts 编译为 js,@babel/preset-env
负责将 ECMA 最新语法编译为 es5(只能编译 syntax,api 须要额定插件)。对于 babel 的 plugins,@babel/transform-modules-commonjs
解决了 babel 反复 helper 的问题,能够按需引入对立的 corejs 库中的 api polyfill,这里是通过 useESModules
的配置来决定采纳 esm 还是 commonjs 的 helper,但官网文档中在 7.13.0 开始曾经废除这个配置了,能够间接通过 package.json
的exports
来判断。其余的 plugin 也都是和语法编译相干的,比方公有办法、公有属性、动态属性、jsx、装璜器等语法的编译,以及 @babel/plugin-transform-modules-commonjs
这个将 esm 引入语法编译为 commonjs 的库,由环境变量 NODE_ENV 决定是否应用,它决定了最终输入的是 esm 库还是 commonjs 库。
依据 package.json 的 module 字段(对于 main、module、browser 字段的优先级),最终入口是根目录下的 es/index.js,因为它是由 babel 依据源目录输入的,所以源代码入口就是src/index.ts
。
从罕用的 api 切入
从上图能够看出,入口文件的输入只有 batch
和exports.ts
文件的全副 export,所以咱们去看 exports.ts
其中的 Provider
、connect
、useSelector
、useDispatch
占据了咱们平时应用的大部分场景,所以咱们从这 4 个 api 切入。
Provider
Provider
来自src/components/Provider.tsx
。
它是一个 React 组件,自身并没有任何视图内容,最终展现的是 children,只不过给 children 里面加了一层 Context Provider,这也是这个 api 为什么叫 Provider 的起因。那具体这个组件想往下面透传什么呢。
const contextValue = useMemo(() => {const subscription = createSubscription(store);
return {
store,
subscription,
getServerState: serverState ? () => serverState : undefined,};
}, [store, serverState]);
能够看到透传的是一个由 store
、subscription
、getServerState
组成的对象。上面别离讲一下对象的 3 个属性作用。
store
是 redux 的 store,是开发者通过 store prop 传给 Provider 组件的。
subscription
是由 createSubscription 这个对象工厂创立的,它生成了 subscription 对象,它是后续嵌套收集订阅的 要害。对于 createSubscription 的代码细节前面会说。
getServerState
是 8.0.0 版本新加的,它用于在 SSR 中,当初始『注水』hydrate
时获取服务器端状态快照的,以便保障两端状态一致性。它的控制权齐全在开发者,只有把状态快照通过 serverState 这个 prop 给 Provider 组件即可。不理解 SSR、hydrate 相干概念的能够去读一下 Dan Abramov 的一篇 discussions,尽管它的主题不是专门讲 SSR 的,然而结尾介绍了它的相干概念,而且 Dan 的文章一贯形象而通俗易懂。
Provider 组件紧接着做的事件是:
const previousState = useMemo(() => store.getState(), [store]);
useIsomorphicLayoutEffect(() => {const { subscription} = contextValue;
subscription.onStateChange = subscription.notifyNestedSubs;
subscription.trySubscribe();
if (previousState !== store.getState()) {subscription.notifyNestedSubs();
}
return () => {subscription.tryUnsubscribe();
subscription.onStateChange = undefined;
};
}, [contextValue, previousState]);
const Context = context || ReactReduxContext;
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
获取了一次最新 state 并命名为 previousState
,只有 store 单例不发生变化,它是不会更新的。个别我的项目中也不太会扭转 redux 单例。
useIsomorphicLayoutEffect
只是一个 facade,从 isomorphic 的命名也能够看出它是和同构相干的。它外部会在 server 环境时应用 useEffect,在浏览器环境时应用 useLayoutEffect
它的代码很简略:
import {useEffect, useLayoutEffect} from "react";
// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed
// Matches logic in React's `shared/ExecutionEnvironment` file
export const canUseDOM = !!(
typeof window !== "undefined" &&
typeof window.document !== "undefined" &&
typeof window.document.createElement !== "undefined"
);
export const useIsomorphicLayoutEffect = canUseDOM
? useLayoutEffect
: useEffect;
然而这样做的起因并不简略:首先,在服务端应用 useLayoutEffect 会抛出正告,为了绕过它于是在服务端转而应用 useEffect。其次,为什么肯定要在 useLayoutEffect/useEffect 外面做?因为一个 store 更新可能产生在 render 阶段和副作用阶段之间,如果在 render 时就做,可能会错过更新,必须要确保 store subscription 的回调领有来自最新更新的 selector。同时还要确保 store subscription 的创立必须是同步的,否则一个 store 更新可能产生在订阅之前(如果订阅是异步的话),这时订阅还没有被创立,从而有了不统一的状态。
如果起因看了不是很明确,联合上面的例子就明确了。
Provider 在 useIsomorphicLayoutEffect
里做了这样的事:
subscription.trySubscribe();
if (previousState !== store.getState()) {subscription.notifyNestedSubs();
}
首先收集 subscription 的订阅,而后看最新的状态和之前在 render 的状态是否统一,如果不统一则告诉更新。如果这一段不放在 useLayoutEffect/useEffect 里,而是放在 render 里,那么当初仅仅订阅了它本人,它的子组件并没有订阅,如果子组件在渲染过程中更新了 redux store,那么子组件们就错过了更新告诉。同时 react 的 useLayoutEffect/useEffect 是自下而上调用的,子组件的先调用,父组件的后调用。这里因为是 react-redux 的根节点了,它的 useLayoutEffect/useEffect 会在最初被调用,这时能确保子组件该注册订阅的都注册了,同时也能确保子组件渲染过程中可能产生的更新都曾经产生了。所以再最初读取一次 state,比拟一下是否要告诉它们更新。这就是为什么要抉择 useLayoutEffect/useEffect。
接下来咱们残缺的看一下 Provider 在 useIsomorphicLayoutEffect
中做的事件
useIsomorphicLayoutEffect(() => {const { subscription} = contextValue;
subscription.onStateChange = subscription.notifyNestedSubs;
subscription.trySubscribe();
if (previousState !== store.getState()) {subscription.notifyNestedSubs();
}
return () => {subscription.tryUnsubscribe();
subscription.onStateChange = undefined;
};
}, [contextValue, previousState]);
首先是设置 subscription 的 onStateChange(它初始是个空办法,须要注入实现),它会在触发更新时调用,它这里心愿未来调用的是 subscription.notifyNestedSubs
,subscription.notifyNestedSubs
会触发这个 subscription 收集的所有子订阅。也就是说这里的更新回调和『更新』没有间接关系,而是触发子节点们的更新办法。
而后调用了subscription.trySubscribe()
,它会将本人的 onStateChange 交给父级 subscription 或者 redux 去订阅,未来由它们触发 onStateChange
最初它会判断之前的 state 和最新的是否统一,如果不统一会调用subscription.notifyNestedSubs()
,它会触发这个 subscription 收集的所有子订阅从而更新它们。
返回了登记相干的函数,它会登记在父级的订阅,将 subscription.onStateChange
从新置为空办法。这个函数会在组件卸载或 re-render(仅 store 变动时)时被调用(react useEffect 的个性)。
Provider 有很多中央都波及到了 subscription,subscription 的那些办法只是讲了大略性能,对于 subscription 的细节会在前面 subscription 的局部讲到。
残缺的 Provider
源码和正文如下:
function Provider<A extends Action = AnyAction>({
store,
context,
children,
serverState,
}: ProviderProps<A>) {
// 生成了一个用于 context 透传的对象,蕴含 redux store、subscription 实例、SSR 时可能用到的函数
const contextValue = useMemo(() => {const subscription = createSubscription(store);
return {
store,
subscription,
getServerState: serverState ? () => serverState : undefined,};
}, [store, serverState]);
// 获取一次以后的 redux state,因为后续子节点的渲染可能会批改 state,所以它叫 previousState
const previousState = useMemo(() => store.getState(), [store]);
// 在 useLayoutEffect 或 useEffect 中
useIsomorphicLayoutEffect(() => {const { subscription} = contextValue;
// 设置 subscription 的 onStateChange 办法
subscription.onStateChange = subscription.notifyNestedSubs;
// 将 subscription 的更新回调订阅给父级,这里会订阅给 redux
subscription.trySubscribe();
// 判断 state 通过渲染后是否变动,如果变动则触发所有子订阅更新
if (previousState !== store.getState()) {subscription.notifyNestedSubs();
}
// 组件卸载时的登记操作
return () => {subscription.tryUnsubscribe();
subscription.onStateChange = undefined;
};
}, [contextValue, previousState]);
const Context = context || ReactReduxContext;
// 最终 Provider 组件只是为了将 contextValue 透传下去,组件 UI 齐全应用 children
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}
总结一下 Provider 其实很简略,Provider 组件只是为了将 contextValue 透传下去,让子组件可能拿到 redux store、subscription 实例、服务器端状态函数。
Subscription/createSubscription 订阅工厂函数
这里会讲到 Provider 中出镜率很高的 subscription 局部,它是 react-redux 可能嵌套收集订阅的要害。其实这个局部的题目叫做 Subscription
曾经不太适合了,在 8.0.0 版本之前,react-redux 的确是通过 Subscription class 实现它的,你能够通过 new Subscription()
应用创立 subscription 实例。但在 8.0.0 之后,曾经变成了 createSubscription
函数创立 subscription 对象,外部用闭包代替原先的属性。
用函数代替 class 有一个益处是,不须要关怀 this 的指向,函数返回的办法批改的永远是外部的闭包,不会呈现 class 办法被赋值给其余变量后呈现 this 指向变动的问题,升高了开发时的心智累赘。闭包也更加私有化,减少了变量平安。同时在一个反对 hooks 的库里,用函数实现也更合乎开发范式。
上面咱们先看一下 createSubscription 形象后的代码,每个的职责都写在正文里了
注:下文呈现的『订阅回调』具体是指,redux 状态更新后触发的组件的更新办法。组件更新办法被父级订阅收集,是订阅公布模式。
function createSubscription(store: any, parentSub?: Subscription) {
// 本人是否被订阅的标记
let unsubscribe: VoidFunc | undefined;
// 负责收集订阅的收集器
let listeners: ListenerCollection = nullListeners;
// 收集订阅
function addNestedSub(listener: () => void) {}
// 告诉订阅
function notifyNestedSubs() {}
// 本人的订阅回调
function handleChangeWrapper() {}
// 判断本人是否被订阅
function isSubscribed() {}
// 让本人被父级订阅
function trySubscribe() {}
// 从父级登记本人的订阅
function tryUnsubscribe() {}
const subscription: Subscription = {
addNestedSub,
notifyNestedSubs,
handleChangeWrapper,
isSubscribed,
trySubscribe,
tryUnsubscribe,
getListeners: () => listeners,};
return subscription;
}
createSubscription
函数是一个对象工厂,它定义了一些变量和办法,而后返回一个领有这些办法的对象subscription
首先看一下 handleChangeWrapper
,通过名字能够看出它只是一个外壳
function handleChangeWrapper() {if (subscription.onStateChange) {subscription.onStateChange();
}
}
其外部理论调用了 onStateChange
办法。究其原因是因为在订阅回调被父级收集时,可能本人的回调还没有确定,所以定义了一个外壳用于被收集,外部的回调办法在确定时会被重置,但外壳的援用不变,所以未来仍然能够触发回调。这也是为什么在 Provider.ts
的源码里,在收集订阅之前先做一下 subscription.onStateChange = subscription.notifyNestedSubs
的起因。
而后看 trySubscribe
function trySubscribe() {if (!unsubscribe) {
unsubscribe = parentSub
? parentSub.addNestedSub(handleChangeWrapper)
: store.subscribe(handleChangeWrapper);
listeners = createListenerCollection();}
}
它的作用是让父级的 subscription 收集本人的订阅回调。首先它会判断如果 unsubscribe
标记了它曾经被订阅了,那么不做任何事。其次它会判断过后创立 subscription
时的第二个参数 parentSub
是否为空,如果有 parentSub
则代表它下层有父级 subscription
,那么它会调用父级的addNestedSub
办法,将本人的订阅回调注册给它;否则则认为本人在顶层,所以注册给 redux store。
由此引申到须要看看 addNestedSub
办法是什么
function addNestedSub(listener: () => void) {trySubscribe();
return listeners.subscribe(listener);
}
addNestedSub
十分奇妙的使用了递归,它外面又调用了 trySubscribe
。于是它们就会达到这样的目标,当最底层subscription
发动 trySubscribe
想被父级收集订阅时,它会首先触发父级的 trySubscribe
并持续递归直到根 subscription
,如果咱们把这样的层级构造设想成树的话(其实 subscription.trySubscribe 也的确产生在组件树中),那么就相当于从根节点到叶子节点顺次会被父级收集订阅。因为这是由叶子节点先发动的,这时除了叶子节点,其余节点的订阅回调还没有被设置,所以才设计了handleChangeWrapper
这个回调外壳,注册的只是这个回调外壳,在未来非叶子节点设置好回调后,能被外壳触发。
在『递』过程完结后,从根节点开始到这个叶子节点的订阅回调 handleChangeWrapper
都正在被父级收集了,『归』的过程回溯做它的本职工作 return listeners.subscribe(listener)
,将子subscription
的订阅回调收集到收集器 listeners
中(未来更新产生时会触发相干的handleChangeWrapper
,而它会间接的调用收集到所有的 listener)。
所以每个 subscription
的addNestedSub
都做了两件事:1. 让本人的订阅回调先被父级收集;2. 收集子 subscription
的订阅回调。
联合 addNestedSub
的解释再回过头来看 trySubscribe
,它想让本人的订阅回调被父级收集,于是当它被传入父级subscription
时,就会调用它的 addNestedSub
,这会导致从根subscription
开始每一层 subscription
都被父级收集了回调,于是每个 subscription
都嵌套收集了它们子 subscription
,从而父级更新后子级才更新成为了可能。同时,因为unsubscribe
这个锁的存在,如果某个父级 subscription
的trySubscribe
被调用了,并不会反复的触发这个『嵌套注册』。
下面咱们剖析了『嵌套注册』时产生了什么,上面咱们看看注册的实质性操作 listeners.subscribe
干了什么,注册的数据结构又是如何设计的。
function createListenerCollection() {const batch = getBatch();
// 对 listener 的收集,listener 是一个双向链表
let first: Listener | null = null;
let last: Listener | null = null;
return {clear() {
first = null;
last = null;
},
// 触发链表所有节点的回调
notify() {batch(() => {
let listener = first;
while (listener) {listener.callback();
listener = listener.next;
}
});
},
// 以数组的模式返回所有节点
get() {let listeners: Listener[] = [];
let listener = first;
while (listener) {listeners.push(listener);
listener = listener.next;
}
return listeners;
},
// 向链表开端增加节点,并返回一个删除该节点的 undo 函数
subscribe(callback: () => void) {
let isSubscribed = true;
let listener: Listener = (last = {
callback,
next: null,
prev: last,
});
if (listener.prev) {listener.prev.next = listener;} else {first = listener;}
return function unsubscribe() {if (!isSubscribed || first === null) return;
isSubscribed = false;
if (listener.next) {listener.next.prev = listener.prev;} else {last = listener.prev;}
if (listener.prev) {listener.prev.next = listener.next;} else {first = listener.next;}
};
},
};
}
listeners
对象是由 createListenerCollection
创立的。listeners
办法不多且逻辑易懂,是由 clear
、notify
、get
、subscribe
组成的。
listeners 负责收集 listener(也就是订阅回调),listeners 外部将 listener 保护成了一个双向链表,头结点是first
,尾节点是last
。
clear
办法如下:
clear() {
first = null
last = null
}
用于清空收集的链表
notify
办法如下:
notify() {batch(() => {
let listener = first
while (listener) {listener.callback()
listener = listener.next
}
})
}
用于遍历调用链表节点,batch
这里能够简略的了解为调用入参的那个函数,其中的细节能够衍生出很多 React 原理(如批量更新、fiber 等),放在文章的最初说。
get
办法如下:
get() {let listeners: Listener[] = []
let listener = first
while (listener) {listeners.push(listener)
listener = listener.next
}
return listeners
}
用于将链表节点转为数组的模式并返回
subscribe
办法如下:
subscribe(callback: () => void) {
let isSubscribed = true
// 创立一个链表节点
let listener: Listener = (last = {
callback,
next: null,
prev: last,
})
// 如果链表曾经有了节点
if (listener.prev) {listener.prev.next = listener} else {
// 如果链表还没有节点,它则是首节点
first = listener
}
// unsubscribe 就是个双向链表的删除指定节点操作
return function unsubscribe() {
// 阻止无意义执行
if (!isSubscribed || first === null) return
isSubscribed = false
// 如果增加的这个节点曾经有了后续节点
if (listener.next) {
// next 的 prev 应该为该节点的 prev
listener.next.prev = listener.prev
} else {
// 没有则阐明该节点是最初一个,将 prev 节点作为 last 节点
last = listener.prev
}
// 如果有前节点 prev
if (listener.prev) {
// prev 的 next 应该为该节点的 next
listener.prev.next = listener.next
} else {
// 否则阐明该节点是第一个,把它的 next 给 first
first = listener.next
}
}
}
用于向 listeners 链表增加一个订阅以及返回一个登记订阅的函数,波及链表的增删操作,具体看正文即可。
所以每个 subscription
收集订阅实则是保护了一个双向链表。
subscription
最初须要说的的局部只有 notifyNestedSubs
和tryUnsubscribe
了
notifyNestedSubs() {this.listeners.notify()
}
tryUnsubscribe() {if (this.unsubscribe) {this.unsubscribe()
this.unsubscribe = null
this.listeners.clear()
this.listeners = nullListeners
}
}
notifyNestedSubs
调用了listeners.notify
,依据下面无关 listeners 的剖析,这里会遍历调用所有的订阅
tryUnsubscribe
则是进行登记相干的操作,this.unsubscribe
在 trySubscribe
办法的执行中被注入值了,它是 addNestedSub
或者 redux subscribe
函数的返回值,是勾销订阅的 undo 操作。在 this.unsubscribe()
之下的别离是革除 unsubscribe
、革除listeners
操作。
至此 subscription
就剖析完了,它次要用于在嵌套调用时,能够嵌套收集订阅,以此做到父级更新后才执行子节点的订阅回调从而在父级更新之后更新。不太分明 react-redux 的人可能会纳闷,不是只有 Provider
组件应用了 subscription
吗,哪里来的嵌套调用?哪里来的收集子订阅?不要焦急,后续讲到 connect
高阶函数,它外面也用到了subscription
,就是这里嵌套应用的。
connect 高阶组件
8.0.0 开始由 connect.tsx
代替 connectAdvanced.js
,实质上都是多层高阶函数,但重构后的connect.tsx
构造显得更加清晰直观。
咱们都晓得在应用 connect 的时候都是:connect(mapStateToProps, mapDispatchToProps, mergeProps, connectOptions)(Component)
,因而它入口应该是接管 mapStateToProps
、mapDispatchToProps
等参数,返回一个接管 Component
参数的高阶函数,这个函数最终返回JSX.Element
。
如果简略看 connect 的构造就如下所示:
function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,
forwardRef,
context,
}
) {const wrapWithConnect = (WrappedComponent) => {return <WrappedComponent />;};
return wrapWithConnect;
}
如果把 connect 做的事件合成的话,我认为有这几块:向父级订阅本人的更新、从 redux store select 数据、判断是否须要更新等其余细节
connect 的 selector
const initMapStateToProps = match(
mapStateToProps,
// @ts-ignore
defaultMapStateToPropsFactories,
"mapStateToProps"
)!;
const initMapDispatchToProps = match(
mapDispatchToProps,
// @ts-ignore
defaultMapDispatchToPropsFactories,
"mapDispatchToProps"
)!;
const initMergeProps = match(
mergeProps,
// @ts-ignore
defaultMergePropsFactories,
"mergeProps"
)!;
const selectorFactoryOptions: SelectorFactoryOptions<any, any, any, any> = {
pure,
shouldHandleStateChanges,
displayName,
wrappedComponentName,
WrappedComponent,
initMapStateToProps,
initMapDispatchToProps,
// @ts-ignore
initMergeProps,
areStatesEqual,
areStatePropsEqual,
areOwnPropsEqual,
areMergedPropsEqual,
};
const childPropsSelector = useMemo(() => {
// The child props selector needs the store reference as an input.
// Re-create this selector whenever the store changes.
return defaultSelectorFactory(store.dispatch, selectorFactoryOptions);
}, [store]);
const actualChildPropsSelector = childPropsSelector(store.getState(),
wrapperProps
);
match
函数是首个须要被剖析的
function match<T>(
arg: unknown,
factories: ((value: unknown) => T)[],
name: string
): T {for (let i = factories.length - 1; i >= 0; i--) {const result = factories[i](arg);
if (result) return result;
}
return ((dispatch: Dispatch, options: { wrappedComponentName: string}) => {
throw new Error(`Invalid value of type ${typeof arg} for ${name} argument when connecting component ${options.wrappedComponentName}.`
);
}) as any;
}
factories
作为一个工厂数组,会被传入 arg
参数遍历调用,每个工厂都会检测解决 arg
,而这里的arg
就是咱们开发中写的 mapStateToProps
、mapDispatchToProps
、mergeProps
,直到factories[i](arg)
有值才会 return,如果始终都不是 truly 值,则会报错。factories
就像 责任链模式 一样,属于本人的工厂职责就会解决并返回。
factories
在 initMapStateToProps
、initMapDispatchToProps
、initMergeProps
中是不同的,别离是defaultMapStateToPropsFactories
、defaultMapDispatchToPropsFactories
、defaultMergePropsFactories
,咱们来看看它们是什么。
// defaultMapStateToPropsFactories
function whenMapStateToPropsIsFunction(mapStateToProps?: MapToProps) {
return typeof mapStateToProps === "function"
? wrapMapToPropsFunc(mapStateToProps, "mapStateToProps")
: undefined;
}
function whenMapStateToPropsIsMissing(mapStateToProps?: MapToProps) {return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined;
}
const defaultMapStateToPropsFactories = [
whenMapStateToPropsIsFunction,
whenMapStateToPropsIsMissing,
];
遍历 defaultMapStateToPropsFactories
是调用了 whenMapStateToPropsIsFunction
、whenMapStateToPropsIsMissing
这两个工厂,由名字能够看出第一个是当 mapStateToProps
是函数时解决,第二个是省略 mapStateToProps
时解决。
外面的 wrapMapToPropsFunc
函数(即whenMapStateToPropsIsFunction
)将 mapToProps
包装在一个代理函数中,它做了几件事:
- 检测被调用的 mapToProps 函数是否依赖于 props,其中 selectorFactory 应用它来决定它是否应该在 props 更改时从新调用。
- 在第一次调用时,如果
mapToProps
返回另一个函数,则解决mapToProps
,并解决把新函数作为后续调用的真正 mapToProps。 - 在第一次调用时,验证后果是否为一个平层的对象,以正告开发人员的 mapToProps 函数未返回无效后果。
wrapMapToPropsConstant
函数(即 whenMapStateToPropsIsMissing
)在缺省时未来会返回空对象(并不是立刻返回,返回的是高阶函数),有值期间望那个值是函数,将dispatch
传入函数,最初返回这个函数的返回值(同样不是立刻返回)
另外两个工厂组 defaultMapDispatchToPropsFactories
、defaultMergePropsFactories
,职责和defaultMapStateToPropsFactories
一样,实质上就是负责解决不同 case 时的arg
const defaultMapDispatchToPropsFactories = [
whenMapDispatchToPropsIsFunction,
whenMapDispatchToPropsIsMissing,
whenMapDispatchToPropsIsObject,
];
const defaultMergePropsFactories = [
whenMergePropsIsFunction,
whenMergePropsIsOmitted,
];
置信大家通过名字也能大略猜出它们负责什么,就不一一细说了。
通过 match
解决后,返回了 initMapStateToProps
、initMapDispatchToProps
、initMergeProps
这 3 个高阶函数
,最终这些函数的目标是返回 select 的值
const selectorFactoryOptions: SelectorFactoryOptions<any, any, any, any> = {
pure,
shouldHandleStateChanges,
displayName,
wrappedComponentName,
WrappedComponent,
initMapStateToProps,
initMapDispatchToProps,
// @ts-ignore
initMergeProps,
areStatesEqual,
areStatePropsEqual,
areOwnPropsEqual,
areMergedPropsEqual,
};
它们以及其余属性组成名为 selectorFactoryOptions
的对象
最终交给 defaultSelectorFactory
应用
const childPropsSelector = useMemo(() => {
// The child props selector needs the store reference as an input.
// Re-create this selector whenever the store changes.
return defaultSelectorFactory(store.dispatch, selectorFactoryOptions);
}, [store]);
而 childPropsSelector
就是最终返回真正须要值的函数(它真的是高阶函数的起点了~)
所以最初只须要看 defaultSelectorFactory
函数做了什么,它理论叫finalPropsSelectorFactory
export default function finalPropsSelectorFactory<
TStateProps,
TOwnProps,
TDispatchProps,
TMergedProps,
State = DefaultRootState
>(
dispatch: Dispatch<Action>,
{
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
...options
}: SelectorFactoryOptions<
TStateProps,
TOwnProps,
TDispatchProps,
TMergedProps,
State
>
) {const mapStateToProps = initMapStateToProps(dispatch, options);
const mapDispatchToProps = initMapDispatchToProps(dispatch, options);
const mergeProps = initMergeProps(dispatch, options);
if (process.env.NODE_ENV !== "production") {verifySubselectors(mapStateToProps, mapDispatchToProps, mergeProps);
}
return pureFinalPropsSelectorFactory<
TStateProps,
TOwnProps,
TDispatchProps,
TMergedProps,
State
// @ts-ignore
>(mapStateToProps!, mapDispatchToProps, mergeProps, dispatch, options);
}
mapStateToProps
、mapDispatchToProps
、mergeProps
是会返回各自最终值的函数。更多应该关注的重点是 pureFinalPropsSelectorFactory
函数
export function pureFinalPropsSelectorFactory<
TStateProps,
TOwnProps,
TDispatchProps,
TMergedProps,
State = DefaultRootState
>(
mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State> & {dependsOnOwnProps: boolean;},
mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps> & {dependsOnOwnProps: boolean;},
mergeProps: MergeProps<TStateProps, TDispatchProps, TOwnProps, TMergedProps>,
dispatch: Dispatch,
{
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
}: PureSelectorFactoryComparisonOptions<TOwnProps, State>
) {
let hasRunAtLeastOnce = false;
let state: State;
let ownProps: TOwnProps;
let stateProps: TStateProps;
let dispatchProps: TDispatchProps;
let mergedProps: TMergedProps;
function handleFirstCall(firstState: State, firstOwnProps: TOwnProps) {
state = firstState;
ownProps = firstOwnProps;
// @ts-ignore
stateProps = mapStateToProps!(state, ownProps);
// @ts-ignore
dispatchProps = mapDispatchToProps!(dispatch, ownProps);
mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
hasRunAtLeastOnce = true;
return mergedProps;
}
function handleNewPropsAndNewState() {
// @ts-ignore
stateProps = mapStateToProps!(state, ownProps);
if (mapDispatchToProps!.dependsOnOwnProps)
// @ts-ignore
dispatchProps = mapDispatchToProps(dispatch, ownProps);
mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
return mergedProps;
}
function handleNewProps() {if (mapStateToProps!.dependsOnOwnProps)
// @ts-ignore
stateProps = mapStateToProps!(state, ownProps);
if (mapDispatchToProps.dependsOnOwnProps)
// @ts-ignore
dispatchProps = mapDispatchToProps(dispatch, ownProps);
mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
return mergedProps;
}
function handleNewState() {const nextStateProps = mapStateToProps(state, ownProps);
const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps);
// @ts-ignore
stateProps = nextStateProps;
if (statePropsChanged)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
return mergedProps;
}
function handleSubsequentCalls(nextState: State, nextOwnProps: TOwnProps) {const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps);
const stateChanged = !areStatesEqual(nextState, state);
state = nextState;
ownProps = nextOwnProps;
if (propsChanged && stateChanged) return handleNewPropsAndNewState();
if (propsChanged) return handleNewProps();
if (stateChanged) return handleNewState();
return mergedProps;
}
return function pureFinalPropsSelector(
nextState: State,
nextOwnProps: TOwnProps
) {
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps);
};
}
它的闭包 hasRunAtLeastOnce
用以辨别是否首次调用,首次和后续是不同的函数,如果是首次调用则是应用 handleSubsequentCalls
函数,它外面产生 stateProps
、产生dispatchProps
,而后将它们放入mergeProps
计算出最终的 props,同时把 hasRunAtLeastOnce
设置为true
,示意曾经不是第一次执行了。
后续调用都走 handleSubsequentCalls
,它的次要目标是如果 state 和 props 都没有变动则应用缓存数据(state、props 是否变动的判断办法是内部传进来的,组件当然能晓得本人有没有变动),如果 state、props 都有变动或者只是其中一个有变动,再别离调用各自的函数(外面次要是依据动态属性dependsOnOwnProps
判断是否要从新执行)失去新值。
于是 childPropsSelector
函数就是返回的 pureFinalPropsSelector
函数,外部拜访了闭包,闭包保留了长久值,从而在组件屡次执行的状况下,能够决定是否须要应用缓存来优化性能。
selector 相干的剖析完了。
总的来说,如果想实现一个最简略的
selector
,只须要const selector = (state, ownProps) => {const stateProps = mapStateToProps(reduxState); const dispatchProps = mapDispatchToProps(reduxDispatch); const actualChildProps = mergeProps(stateProps, dispatchProps, ownProps); return actualChildProps; };
那为什么 react-redux 会写的如此简单呢。就是为了
connect
组件在屡次执行时能利用细粒度缓存的mergedProps
值晋升性能,React 只能做到在wrapperProps
不变时应用 memo,但难以做更细粒度的辨别,比方晓得 selector 是否依赖 props,从而就算 props 变动了也不须要更新。要实现这一点须要大量嵌套的高阶函数贮存长久化的闭包两头值,能力在组件屡次执行时不失落状态从而判断更新。
上面咱们筹备讲点别的了,如果你对一系列调用栈有点头晕,你只有记住看到了 childPropsSelector
就是返回 selector 后的值就好了。
connect 更新的注册订阅
function ConnectFunction<TOwnProps>(props: InternalConnectProps & TOwnProps) {const [propsContext, reactReduxForwardedRef, wrapperProps] = useMemo(() => {const { reactReduxForwardedRef, ...wrapperProps} = props;
return [props.context, reactReduxForwardedRef, wrapperProps];
}, [props]);
// …………
// …………
}
首先从 props 里划分出了理论业务 props 和行为管制相干的 props,所谓的业务 props 就是指我的项目中的父级组件理论传给 connect 组件的 props,行为管制 props 则是 forward ref、context 等业务无关的、和外部注册订阅无关的 props。并且应用 useMemo 缓存了解构后的值。
const ContextToUse: ReactReduxContextInstance = useMemo(() => {
return propsContext &&
propsContext.Consumer &&
// @ts-ignore
isContextConsumer(<propsContext.Consumer />)
? propsContext
: Context;
}, [propsContext, Context]);
这一步确定了 context。还记得在 Provider 组件里的那个 context 吗,connect 这里就能够通过 context 拿到它。不过这里做了个判断,如果用户通过 props 传入了自定义的 context,那么优先用自定义 context,否则应用应用那个『能够看做全局『的 React.createContext
(也是 Provider 或者其余 connect、useSelector 等应用的)
const store: Store = didStoreComeFromProps ? props.store! : contextValue!.store;
const getServerState = didStoreComeFromContext
? contextValue.getServerState
: store.getState;
const childPropsSelector = useMemo(() => {
// The child props selector needs the store reference as an input.
// Re-create this selector whenever the store changes.
return defaultSelectorFactory(store.dispatch, selectorFactoryOptions);
}, [store]);
接着获取 store(它可能来自 props 也可能来自 context),还获取了服务端渲染状态(如果有的话)。而后创立了一个能返回 selected 值的 selector 函数,selector 的细节下面讲过了。
上面呈现了订阅的重点!
const [subscription, notifyNestedSubs] = useMemo(() => {if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY;
const subscription = createSubscription(
store,
didStoreComeFromProps ? undefined : contextValue!.subscription
);
const notifyNestedSubs = subscription.notifyNestedSubs.bind(subscription);
return [subscription, notifyNestedSubs];
}, [store, didStoreComeFromProps, contextValue]);
const overriddenContextValue = useMemo(() => {if (didStoreComeFromProps) {return contextValue!;}
return {
...contextValue,
subscription,
} as ReactReduxContextValue;
}, [didStoreComeFromProps, contextValue, subscription]);
通过 createSubscription 函数创立了一个订阅实例,createSubscription 的细节下面讲过了,它外面有一个 嵌套订阅 的逻辑,这里就会用到。createSubscription 的第 3 个参数传入了 context 里的 subscription 订阅实例,依据嵌套订阅逻辑(忘了的能够回头看看函数创立了一个订阅实例,createSubscription 的第 3 个参数起到了什么作用),这个 connect 里的订阅回调实际上是注册给父级的这个 contextValue.subscription
的,如果这个父级是顶层的 Provider,那么它的订阅回调才真正注册给redux
,如果父级还不是顶层的话,那么还是会像这样一层层的嵌套注册回调。通过这个实现了『父级先更新 - 子级后更新』从而防止过期 props 和僵尸节点问题。
为了让子级 connect 的订阅回调注册给本人,于是用本人的 subscription 生成了一个新的 ReactReduxContextValue: overriddenContextValue
,以便后续的嵌套注册。
const lastChildProps = useRef<unknown>();
const lastWrapperProps = useRef(wrapperProps);
const childPropsFromStoreUpdate = useRef<unknown>();
const renderIsScheduled = useRef(false);
const isProcessingDispatch = useRef(false);
const isMounted = useRef(false);
const latestSubscriptionCallbackError = useRef<Error>();
而后定义了一批『长久化数据』(不会随着组件反复执行而初始化),这些数据次要为了未来的『更新判断』和『由父组件带动的更新、来自 store 的更新不反复产生』,前面会用到它们。
后面只看到了 subscription 的创立,并没有具体更新相干的,接下来的代码会走到。
const subscribeForReact = useMemo(() => {// 这里订阅了更新,并且返回一个登记订阅的函数}, [subscription]);
useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
childPropsFromStoreUpdate,
notifyNestedSubs,
]);
let actualChildProps: unknown;
try {
actualChildProps = useSyncExternalStore(
subscribeForReact,
actualChildPropsSelector,
getServerState
? () => childPropsSelector(getServerState(), wrapperProps)
: actualChildPropsSelector
);
} catch (err) {if (latestSubscriptionCallbackError.current) {
(err as Error).message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`;
}
throw err;
}
subscribeForReact
前面再看,外面次要是判断是否要更新的,它是发动更新的次要入口。
useIsomorphicLayoutEffectWithArgs
是一个工具函数,外部是 useIsomorphicLayoutEffect
,这个函数后面也讲过。它们最终做的是:将第 2 个数组参数的每项作为参数给第一个参数调用,第 3 个参数是useIsomorphicLayoutEffect
的缓存依赖。
被执行的第一个参数captureWrapperProps
,它次要性能是判断如果是来自 store 的更新,则在更新实现后(比方 useEffect)触发subscription.notifyNestedSubs
,告诉子订阅更新。
接着它想生成 actualChildProps
,也就是 select 进去的业务组件须要的 props,其中次要应用了useSyncExternalStore
,如果你追到useSyncExternalStore
的代码里看,会发现它是一个空办法,间接调用会抛出谬误,所以它是由内部注入的。在入口 index.ts
里,initializeConnect(useSyncExternalStore)
对它进行初始化了,useSyncExternalStore
来自 React。所以 actualChildProps
理论是 React.useSyncExternalStore(subscribeForReact, actualChildPropsSelector, getServerState ? () => childPropsSelector(getServerState(), wrapperProps) : actualChildPropsSelector)
的后果。
useSyncExternalStore 是 react18 的新 API,前身是 useMutableSource,为了避免在 concurrent 模式下,工作中断后第三方 store 被批改,复原工作时呈现 tearing
从而数据不统一。内部 store 的更新能够通过它引起组件的更新。在 react-redux8
之前,是由 useReducer
手动实现的,这是 react-redux8
首次应用新 API。这也意味着你必须跟着应用 React18+。但我认为其实 react-redux8
能够用 shim: import {useSyncExternalStore} from 'use-syncexternal-store/shim';
来做到向下兼容。
useSyncExternalStore
第一个参数是一个订阅函数,订阅触发时会引起该组件的更新,第二个函数返回一个 immutable 快照,用于标记该不该更新,以及失去返回的后果。
上面看看订阅函数 subscribeForReact
做了什么。
const subscribeForReact = useMemo(() => {const subscribe = (reactListener: () => void) => {if (!subscription) {return () => {};}
return subscribeUpdates(
shouldHandleStateChanges,
store,
subscription,
// @ts-ignore
childPropsSelector,
lastWrapperProps,
lastChildProps,
renderIsScheduled,
isMounted,
childPropsFromStoreUpdate,
notifyNestedSubs,
reactListener
);
};
return subscribe;
}, [subscription]);
首先用 useMemo
缓存了函数,用 useCallback
也能够,而且集体感觉 useCallback
更合乎语义。这个函数理论调用的是subscribeUpdates
,那咱们再看看subscribeUpdates
。
function subscribeUpdates(
shouldHandleStateChanges: boolean,
store: Store,
subscription: Subscription,
childPropsSelector: (state: unknown, props: unknown) => unknown,
lastWrapperProps: React.MutableRefObject<unknown>,
lastChildProps: React.MutableRefObject<unknown>,
renderIsScheduled: React.MutableRefObject<boolean>,
isMounted: React.MutableRefObject<boolean>,
childPropsFromStoreUpdate: React.MutableRefObject<unknown>,
notifyNestedSubs: () => void,
additionalSubscribeListener: () => void) {if (!shouldHandleStateChanges) return () => {};
let didUnsubscribe = false;
let lastThrownError: Error | null = null;
const checkForUpdates = () => {if (didUnsubscribe || !isMounted.current) {return;}
const latestStoreState = store.getState();
let newChildProps, error;
try {
newChildProps = childPropsSelector(
latestStoreState,
lastWrapperProps.current
);
} catch (e) {
error = e;
lastThrownError = e as Error | null;
}
if (!error) {lastThrownError = null;}
if (newChildProps === lastChildProps.current) {if (!renderIsScheduled.current) {notifyNestedSubs();
}
} else {
lastChildProps.current = newChildProps;
childPropsFromStoreUpdate.current = newChildProps;
renderIsScheduled.current = true;
additionalSubscribeListener();}
};
subscription.onStateChange = checkForUpdates;
subscription.trySubscribe();
checkForUpdates();
const unsubscribeWrapper = () => {
didUnsubscribe = true;
subscription.tryUnsubscribe();
subscription.onStateChange = null;
if (lastThrownError) {throw lastThrownError;}
};
return unsubscribeWrapper;
}
其中的重点是 checkForUpdates
,它外面获取了最新的 Store 状态: latestStoreState
(留神这里仍然是手动获取的,未来 react-redux 会把它交给uSES
做)、最新的要交给业务组件的 props: newChildProps
,如果 childProps 和上一次一样,那么不会更新,只会告诉子 connect 尝试更新。如果 childProps 变了,则会调用 React.useSyncExternalStore
传入的更新办法,这里叫 additionalSubscribeListener
,它会引起组件更新。react-redux8 以前这里用的是 useReducer
的 dispatch
。checkForUpdates
会被交给 subscription.onStateChange
,后面咱们剖析过,subscription.onStateChange
最终会在 redux store 更新的时候被嵌套调用。
subscribeUpdates
函数外面还调用了 subscription.trySubscribe()
将onStateChange
收集到父级订阅中。接着调用了 checkForUpdates
以防首次渲染时数据就变了。最初返回了一个登记订阅的函数。
由上述剖析可知,组件理论的更新是 checkForUpdates
实现的。它会由两个路径调用:
- redux store 更新后,被父级级联调用
- 组件本身 render(父级 render 带动、组件本身 state 带动),同时
useSyncExternalStore
的快照产生了变动,导致调用
咱们会发现在一次总更新中,单个 connect
的 checkForUpdates
是会被屡次调用的。比方一次来自 redux 的更新导致父级 render 了,它的子元素有 connect 组件,个别咱们不会对 connect 组件做 memo,于是它也会被 render,正好它的 selectorProps 也变动了,所以在 render 期间 checkForUpdates
调用。当父级更新实现后,触发本身 listeners,导致子 connect 的 checkForUpdates
再次被调用。这样不会让组件 re-render 屡次吗?当初我首次看代码的时候,就有这样的疑难。通过大脑模仿各种场景的代码调度,发现它是这样防止反复 render 的,归纳起来能够分为这几种场景:
- 来自 redux store 更新,且本身的 stateFromStore 有更新
- 来自 redux store 更新,且本身的 stateFromStore 没有更新
- 来自父组件 render 的更新,且本身的 stateFromStore 有更新
- 来自父组件 render 的更新,且本身的 stateFromStore 没有更新
- 来自 本身 state 的更新,且本身的 stateFromStore 有更新
- 来自 本身 state 的更新,且本身的 stateFromStore 没有更新
其中 6 的 stateFromStore 和 props 都没有变动,actualChildProps
间接应用缓存后果,并不会调用checkForUpdates
,不会放心屡次 render 的问题
1 和 2 的更新来自 redux store,所以必然是父组件先更新(除非该 connect 是除 Provider 的顶层)该 connect 后更新,connect render 时,来自父组件的 props 可能变了,本身的 stateFromStore 可能也变了,于是 checkForUpdates
被调用,useRef childPropsFromStoreUpdate
被设置新的 childProps,中断以后 render,从新 render, 组件在 render 中取得新 childProps 值。接着由父 connect 组件的 useEffect
带来第二波checkForUpdates
,这时 childProps 曾经和上一次没有不同了,所以并不会更新,只是触发更底层子 connect 的checkForUpdates
,更底层 connect 逻辑同理。
3 和 4 类型的更新其实是 1 和 2 中的一部分,就不细讲了。
5 类型的更新可能产生在同时调用了 setState 和 redux dispatch,依据 react-redux 的嵌套策略,redux dispatch 的更新必定产生在 setState 之后的,在 render 过程中 childPropsSelector(store.getState(), wrapperProps)
获取到最新的 childProps
,它显然是变了。于是checkForUpdates
,后续的 redux dispatch 更新childProps
曾经和上次雷同了,所以只走notifyNestedSubs
。
至此所有场景所有链路的更新都有了闭环。
在 connect 组件的最初:
const renderedWrappedComponent = useMemo(() => {
return (
// @ts-ignore
<WrappedComponent {...actualChildProps} ref={reactReduxForwardedRef} />
);
}, [reactReduxForwardedRef, WrappedComponent, actualChildProps]);
const renderedChild = useMemo(() => {if (shouldHandleStateChanges) {
return (<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
);
}
return renderedWrappedComponent;
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue]);
return renderedChild;
WrappedComponent
就是用户传入的业务组件,ContextToUse.Provider
会将该 connect 的 subscription
传给上层,如果业务组件里还有 connect 就能够嵌套订阅。是否须要 context 透传是由 shouldHandleStateChanges
变量决定的,如果没有 mapStateToProps
的话,它则是 false
。也就是说如果连mapStateToProps
都没有,那这个组件及其子组件也就没有订阅 redux 的必要。
useSelector
而后咱们看一下useSelector
:
function createSelectorHook(context = ReactReduxContext): <TState = DefaultRootState, Selected = unknown>(selector: (state: TState) => Selected,
equalityFn?: EqualityFn<Selected>
) => Selected {
const useReduxContext =
context === ReactReduxContext
? useDefaultReduxContext
: () => useContext(context);
return function useSelector<TState, Selected extends unknown>(selector: (state: TState) => Selected,
equalityFn: EqualityFn<Selected> = refEquality
): Selected {const { store, getServerState} = useReduxContext()!;
const selectedState = useSyncExternalStoreWithSelector(
store.subscribe,
store.getState,
getServerState || store.getState,
selector,
equalityFn
);
useDebugValue(selectedState);
return selectedState;
};
}
useSelector
是由 createSelectorHook()
创立的
和 connect
一样,通过 ReactReduxContext
拿到 Provider
的 store
等数据。
useSyncExternalStoreWithSelector
同样是空办法,被 /src/index.ts
设置为 import {useSyncExternalStoreWithSelector} from 'use-sync-external-store/with-selector'
的useSyncExternalStoreWithSelector
,和 useSyncExternalStore
作用相似。它间接订阅给了redux.store.subscribe
。redux store 更新时,会触发应用它的组件更新,从而拿到新的selectedState
。
hooks
只是状态逻辑,它不能像 connect
组件那样做到给子组件提供 Context,于是它只能平级的间接订阅在 redux 里,这就是文章结尾局部讲到的『僵尸节点』问题时提到的:hooks 没有嵌套订阅的起因。useSelector
的代码比 7 版本的简洁多了,能够发现去除了非生产环境代码后并没有多少,相比之下 7 版本的要简短不少(165 行),有趣味的能够去看看。
衍生进去的 React 原理加餐
useSelector
和 7 版本还有一个重要区别!理解它能够帮忙你晓得更多 React 外部的细节!
在 7 版本里,注册订阅是在 useEffect/useLayoutEffect
里执行的。而依据 React 的 fiber 架构逻辑,它会以前序遍历的程序遍历 fiber 树,首先应用 beginWork
解决 fiber,当到了叶子节点时调用 completeWork
,其中 completeWork
会将诸如 useEffect
、useLayoutEffect
等放入 effectList,未来在 commit 阶段程序执行。而依照前序遍历的程序,completeWork
是自下而上的,也就是说子节点的 useEffect
会比父节点先执行,于是在 7 版本里,子组件 hooks 比父组件更早注册,未来执行时也更早执行,这就典型地陷入了结尾说的『stale props』、『zombie children』问题。
因为我晓得 React 的外部机制,所以刚开始我认为 react-redux7 的 hooks 是会出 bug 的,于是我通过 npm link
用几个测试用例本地跑了代码,后果出乎我预料,listener
的确被调用了屡次,这意味着有多个 connect 组件将会更新,就当我认为子组件将先于父组件被更新时,但最终 render 只有一次,是由最上层的父 connect render 的,它将带动上面的子 connect 更新。
这就引出了 React 的批量更新策略。比方 React16 外面,所有的 React 事件、生命周期都被装璜了一个逻辑,结尾会设置一个锁,于是外面的所有 setState 这样的更新操作都不会真的发动更新,等代码的最初放开锁,再批量的一起更新。于是 react-redux 正好借用这个策略,让须要更新的组件整体自上而下的批量更新了,这源于它的一处不起眼的中央:setBatch(batch)
,而我也是因为没留神这里的用途,而误判它会出问题,setBatch(batch)
理论做了什么前面会讲到。
对于批量更新,再举个例子,比方 A 有子组件 B,B 有子组件 C,别离顺序调用 C、B、A 的 setState,失常来说 C、B、A 会被按程序各自更新一次,而批量更新会将三次更新合并成一个,间接从组件 A 更新一次,B 和 C 就顺带被更新了。
不过这个批量更新策略的『锁』是在同一个『宏工作』里的,如果代码中有异步工作,那么异步工作中的 setState 是会『逃脱』批量更新的,也就是说这种状况下每次 setState 就会让组件更新一次。比方 react-redux 不能保障用户不会在一个申请回调里调用 dispatch
(实际上这么做太广泛了),所以 react-redux 在/src/index.ts
中做了 setBatch(batch)
操作,batch
来自 import {unstable_batchedUpdates as batch} from './utils/reactBatchedUpdates'
,unstable_batchedUpdates
是由 react-dom
提供的手动批量更新办法,能够帮忙脱离管控的 setState 从新批量更新。在Subscription.ts
中的 createListenerCollection
里用到了batch
:
const batch = getBatch();
// ............
return {notify() {batch(() => {
let listener = first;
while (listener) {listener.callback();
listener = listener.next;
}
});
},
};
所以 subscription
里的 listeners
的notify
办法,是会对所有的更新订阅手动批量更新的。从而在 react-redux7 中,就算 hooks 注册的订阅是自下而上的,也不会引起问题。
而 react-redux8 间接应用新 API useSyncExternalStoreWithSelector
订阅,是在 render 期间产生的,所以订阅的程序是自上而下的,防止了子订阅先执行的问题。然而 8 版本仍然有上述 batch
的逻辑,代码和 7 截然不同,因为批量更新能节俭不少性能。
useDispatch
最初的局部是useDispatch
function createDispatchHook<S = RootStateOrAny, A extends Action = AnyAction>(context?: Context<ReactReduxContextValue<S, A>> = ReactReduxContext) {
const useStore =
context === ReactReduxContext ? useDefaultStore : createStoreHook(context);
return function useDispatch<
AppDispatch extends Dispatch<A> = Dispatch<A>
>(): AppDispatch {const store = useStore();
return store.dispatch;
};
}
export const useDispatch = createDispatchHook();
useDispatch
非常简单,就是通过 useStore()
拿到 redux store,而后返回 store.dispatch
,用户就能应用这个dispatch
派发 action
了。
除了上述 4 个 api 以外,/src/index.ts
里还有一些 api,不过最难的局部咱们曾经剖析完了,剩下的置信能够交给大家自行钻研。
浏览源码期间在 fork 的 react-redux 我的项目中写下了一些中文正文,作为一个新我的项目放在了 react-redux-with-comment 仓库,阅读文章须要对照源码的能够看一下,版本是 8.0.0-beta.2
最初的最初
对 react-redux
源码的解析就到这里了。从最后对 react-redux 性能的纳闷于是首次浏览源码,到起初对官网中『stale props』、『zombie children』问题的解决方案的好奇,驱使咱们探索更深刻的细节。通过原理的探索,而后反哺咱们在业务上的利用,并借鉴优良框架的设计引发更多思考,这是一个良性循环。带着你的好奇与问题去浏览,并最终利用于我的项目,而不是为了达成某些目标而浏览(比方应酬面试),一个不能利用于工程的技术没有任何价值。如果你看到了最初,阐明你对技术是很有好奇心的,心愿你始终保持一颗好奇的心。
欢送关注我的 github,share-technology 这个仓库会不定期有高质量前端技术文章分享。