React-Redux (1): @connect

0 缘起

之前有次面试被问起 connect 方法是如何把 store 的 state 传递到 Component 的,当时没有回答到点子上,拖到现在终于强迫自己坐下来搞清楚这个事情。

1 API

先看看官方文档关于 connect() 方法的说明:Connect · React Redux | 中文版

大概翻译一下:
connect() 方法把一个 React 组件绑定到 Redux store 上。
方法返回一个新组件,新组件包含所有传入组件需要从 store 上获取的数据或者向 store 触发的方法
connect 不会修改组件,而是返回一个新的绑定组件包裹原组件。

1
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

更多内容请参考官方文档。

2 实现

那么 connect 底层是如何实现的呢?
通过源码看一下:
源代码位于:github.com/reduxjs/react-redux/connect/connect.js
这里对 connect 的参数进行了一个默认值处理,没有涉及核心功能,继续调用了 connectAdvanced
github.com/reduxjs/react-redux/components/connectAdvanced.js
方法很长,前面都是对一些过期选项的警告和一些值的处理。核心代码位于 ConnectFunction 内部
第 171 行处 获取到当前使用的 context

1
2
3
4
5
6
7
8
9
10
11
12
const ContextToUse = useMemo(() => {
// Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
// Memoize the check that determines which context instance we should use.
return propsContext &&
propsContext.Consumer &&
isContextConsumer(<propsContext.Consumer />)
? propsContext
: Context
}, [propsContext, Context])

// Retrieve the store and ancestor subscription via context, if available
const contextValue = useContext(ContextToUse)

第 385 行处,执行了具体的 connect 过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  // Now that all that's done, we can finally try to actually render the child component.
// We memoize the elements for the rendered child component as an optimization.
const renderedWrappedComponent = useMemo(
() => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
[forwardedRef, WrappedComponent, actualChildProps]
)

// If React sees the exact same element reference as last time, it bails out of re-rendering
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
// If this component is subscribed to store updates, we need to pass its own
// subscription instance down to our descendants. That means rendering the same
// Context instance, and putting a different value into the context.
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}

return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

return renderedChild
}

可以看到,其实底层就是使用 Context.Provider 向被绑定的组件注入需要的属性(包括 mapStateToProps 和 dispatch相关方法)。

3 踩坑

最初看代码的时候搞不清楚 ReactReduxContext 这个 Context 是如何跟外层的 store 关联起来的,因为漏掉了最外层的 Provider https://react-redux.js.org/api/provider
react-redux 提供两种引用方式:在最外层使用 react-redux 提供的 Provider 组件包裹 或者 每次 connect 时候都要传入 context 实例的方式,以此保证唯一数据源。

4 其他

整个实现过程非常巧妙,包括一些默认值的处理,一些方法重载的实现,非常值得参考。