You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
看下方代码,实际上,this.props.children 这个 react element 的引用并没有变化,所以它不会导致 re-render
<divonClick={()=>this.setState({})}>{this.props.children}</div>// is equal toReact.createElement('div',{onClick: ()=>this.setState({})},this.props.children)
还有一种情况,Context api 导致的 re-render
不 re-render 的情况
If React sees the exact same element reference as last time, it bails out of re-renderin__g that child
利用 useMemo,每次 store 中数据变化时,connect 了 store 的组件首先会计算 ContextToUse、renderedWrappedComponent、overriddenContextValue,看它们是否变了,如果没变,那么还是取上次渲染的 react element 的 reference,react 发现 react element 的 reference 没变,那么就不会对该组件进行 re-render。
constMainLayout=React.memo(props=>{console.log("------render in MainLayout------");constprev=usePrevious(props);if(prev.children!==props.children){console.log("children changed");}return(<div><div>======== Main Layout =======</div>{props.children}</div>);});
constDefault=props=>{const[key,setKey]=useState(0);useEffect(()=>{console.log("one second later");setTimeout(()=>{setKey(key+1);},1000);},[]);return(<MainLayout><h3>每次 re-render 时,MainLayout 的 children 属性都改变了</h3><Child/><Hello/></MainLayout>);};
原因,和上方解释的原因相同,看非 jsx 的 react 代码:
classHelloextendsReact.Component{render(){returnReact.createElement("div",null,[React.createElement("div",null,"i am a div")]);}}
react key 和 diff 机制
Tree Diff 策略
对树进行分层比较(dom 跨层级移动操作特别少)。
对于同一层级的一组节点,它们可以通过唯一 id 进行区分。
拥有相同类的两个组件将生成相似的树形结构,拥有不同类的两个组件将生成不同的树形结构。
React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。 React 官方建议不要进行 DOM 节点跨层级的操作。 在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。
React 通过设置唯一 key的策略,对 element diff 进行算法优化,避免频繁的删除和插入操作
react-redux 数据流
基本数据流实现分析
注册监听,订阅事件 (在 create store & Provider 中)
create store -> new Subscription in Provider -> subscription.onStateChange = subscription.notifyNestedSubs -> subscription subscribe onStateChange listener to store,and set listeners (this.listeners = createListenerCollection())
发布事件
dispatch -> call store listeners -> call subscription.onStateChange (equal to subscription.notifyNestedSubs) ->
Connect
订阅到最近 connect 的祖先组件或者 store 中,当被订阅者 notifyNestedSubs 时,将触发 checkForUpdates
自上而下,基于组件层级的层层订阅逻辑
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
注意下方的:this.parentSub.addNestedSub,将订阅到最近的 connect 祖先组件的 subscription 中或者订阅到 store 中
可以看到,当 store 中的 state change 的时候,将触发 notifyNestedSubs。但是,这些 nestedSubs 从哪里来呢?答案是下层组件在 Connect 时,在最近 connect 的祖先组件的 subscription 或者 store 中 addNestedSub。这里的逻辑即是上方提到的自上而下,基于组件层级的层层订阅逻辑。
Connect
下方是 Connect 实现的伪代码,并非真实实现:
functionconnect(mapStateToProps,mapDispatchToProps){// It lets us inject component as the last step so people can use it as a decorator.// Generally you don't need to worry about it.returnfunction(WrappedComponent){// It returns a componentreturnclassextendsReact.Component{render(){return(// that renders your component<WrappedComponent{/* with its props */}{...this.props}{/* and additional props calculated from Redux store */}{...mapStateToProps(store.getState(),this.props)}{...mapDispatchToProps(store.dispatch,this.props)}/>)}componentDidMount(){// it remembers to subscribe to the store so it doesn't miss updatesthis.unsubscribe=store.subscribe(this.handleChange.bind(this))}componentWillUnmount(){// and unsubscribe laterthis.unsubscribe()}handleChange(){// and whenever the store state changes, it re-renders.this.forceUpdate()}}}}
子在上方的简单模型中,在 store 中注册监听到 store 变化的回调函数,当 store 值变化时,将触发注册组件的 forceUpdate 方法,从而 re-render 组件。实际的实现中比这要复杂得多,也不是这种实现方式。不过,最基本的模型是如此的。这里涉及到一个问题,如何将 store 传递给对应的组件,前面的代码中其实已经看到,可以通过 Context 将 store 传递过去。 在实际实现中,要复杂得多,下方是实际 connect 函数,而 connect 大部分实现逻辑都封装在了 connectAdvanced 中。
functioncreateConnect({
connectHOC =connectAdvanced,
mapStateToPropsFactories =defaultMapStateToPropsFactories,
mapDispatchToPropsFactories =defaultMapDispatchToPropsFactories,
mergePropsFactories =defaultMergePropsFactories,
selectorFactory =defaultSelectorFactory}={}){returnfunctionconnect(mapStateToProps,mapDispatchToProps,mergeProps,{
pure =true,
areStatesEqual =strictEqual,
areOwnPropsEqual =shallowEqual,
areStatePropsEqual =shallowEqual,
areMergedPropsEqual =shallowEqual,
...extraOptions}={}){constinitMapStateToProps=match(mapStateToProps,mapStateToPropsFactories,'mapStateToProps')constinitMapDispatchToProps=match(mapDispatchToProps,mapDispatchToPropsFactories,'mapDispatchToProps')constinitMergeProps=match(mergeProps,mergePropsFactories,'mergeProps')returnconnectHOC(selectorFactory,{// used in error messagesmethodName: 'connect',// used to compute Connect's displayName from the wrapped component's displayName.getDisplayName: name=>`Connect(${name})`,// if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changesshouldHandleStateChanges: Boolean(mapStateToProps),// passed through to selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,// any extra options args can override defaults of connect or connectAdvanced
...extraOptions})}}
functioncreateChildSelector(store){returnselectorFactory(store.dispatch,selectorFactoryOptions)}constchildPropsSelector=useMemo(()=>{returncreateChildSelector(store)},[store])// 非 store 更新导致的重新计算constactualChildProps=usePureOnlyMemo(()=>{if(childPropsFromStoreUpdate.current&&wrapperProps===lastWrapperProps.current){returnchildPropsFromStoreUpdate.current}returnchildPropsSelector(store.getState(),wrapperProps)},[store,previousStateUpdateResult,wrapperProps])// store 更新导致的重新计算newChildProps=childPropsSelector(latestStoreState,lastWrapperProps.current)
前言
react/redux 数据流已经是一个老生常谈的问题了,似乎现在谈已经失去了新鲜感。然而,深入理解 react/redux 数据流应该是一个专业 react 前端需要完全掌握的技能,如果未能充分理解,那么很多情况下,你并不知道你开发的应用是如何工作的,这很容易带来问题,从而影响项目的持续发展和可维护性。另一方面,随着 react hooks 和 react-redux 7.x 的发布,在数据流方面又出现了一些新的知识点。react-redux 7.x 全面拥抱了 hooks,并且重新回到了基于 Subscriptions 的实现。这使得 react-redux 7.x 彻底解决了 6.x 的性能问题,甚至是所有 react-redux 版本中性能最好的。所以,是时候重新研究 react/redux 数据流,并基于其对我们应用的性能进行优化了。
react 数据流和渲染机制
基本 re-render 机制
不 re-render 的情况
If React sees the exact same element reference as last time, it bails out of re-renderin__g that child
在上方的例子中,其实已经提到了这条准则,我们来看一个 react-redux 中的实际例子:
利用 useMemo,每次 store 中数据变化时,connect 了 store 的组件首先会计算 ContextToUse、renderedWrappedComponent、overriddenContextValue,看它们是否变了,如果没变,那么还是取上次渲染的 react element 的 reference,react 发现 react element 的 reference 没变,那么就不会对该组件进行 re-render。
children 相关的 re-render 机制
组件内渲染 this.props.children 不 re-render
如前所述,如果子组件是 this.props.children, 父组件 re-render 不会导致子组件 re-render
上方已做分析。
children 属性变化导致 re-render
react 组件每次 re-render,如果它 render 中渲染的A组件包含有子组件,A组件的 children props 就会变化,从而导致 A 组件即使用 React.memo 优化后也会 re-render,例子:https://codesandbox.io/s/children-diff-re-render-mxx5g
原因,和上方解释的原因相同,看非 jsx 的 react 代码:
react key 和 diff 机制
React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。
React 官方建议不要进行 DOM 节点跨层级的操作。
在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。
react-redux 数据流
基本数据流实现分析
Store
基本的 Store 模型:
通过 subscirbe 方法注册监听,在 dispatch 时,直接触发所有监听器。处理组件是否真的需要更新的逻辑不在 store 中。
Provider
从以上代码可以看到,Provider 是对 react Context.Provider 的封装。Context.Provider 中的值 contextValue 为:
可以看到,当 store 中的 state change 的时候,将触发 notifyNestedSubs。但是,这些 nestedSubs 从哪里来呢?答案是下层组件在 Connect 时,在最近 connect 的祖先组件的 subscription 或者 store 中 addNestedSub。这里的逻辑即是上方提到的自上而下,基于组件层级的层层订阅逻辑。
Connect
下方是 Connect 实现的伪代码,并非真实实现:
子在上方的简单模型中,在 store 中注册监听到 store 变化的回调函数,当 store 值变化时,将触发注册组件的 forceUpdate 方法,从而 re-render 组件。实际的实现中比这要复杂得多,也不是这种实现方式。不过,最基本的模型是如此的。这里涉及到一个问题,如何将 store 传递给对应的组件,前面的代码中其实已经看到,可以通过 Context 将 store 传递过去。
在实际实现中,要复杂得多,下方是实际 connect 函数,而 connect 大部分实现逻辑都封装在了 connectAdvanced 中。
性能优化实现
性能优化实现是 react-redux 的关键部分。我们都可以自己实现一个类似于 eventEmitter 的发布订阅类,并基于其解决 react 应用的发布订阅更新组件逻辑。然而,如果我们都自己写,那么需要自己解决性能、开发规范、各种边界情况等等各种问题。而 react-redux 存在的意义就是把这些问题统一解决了,它是社区开发者们花了大量时间的结晶,让我们无需再去担心那些问题,而是可以放心地使用其提供的 API。在其实现中,下面3个问题是我们需要关注的。
1. 每次
store
变动,都会触发根组件setState
从而导致re-render
。我们知道当父组件re-render
后一定会导致子组件re-render
。然而,引入 react-redux 并没有这个副作用,这是如何处理的?其实在 react-redux v6 中,需要关心这个问题,在 v6 中,每次 store 的变化会触发根组件的 re-render。但是根组件的子组件不应该 re-render。其实是用到了我们上文中提到的 this.props.children。避免了子组件 re-render。在 v7 中,其实不存在该问题,store 中值的变化不会触发根组件 Provider 的 re-render。
2. 不同的子组件,需要的只是
store
上的一部分数据,如何在store
发生变化后,仅仅影响那些用到 store 变化部分 state 的组件?首先,我们抛开 store,对于父组件传入的 props,基于 React.memo,只在 connect 后的组件被传入的 props 真的改变后才会重新获取组件,否则直接使用 memo 的元组件,无需任何 react 相关的渲染计算:
ConnectFunction 是原组件被 connect 后返回的新组件,它的 props 来自于父组件的传入。所以,如果父组件传入的 props 没有变,那么 connect 后的组件将不会被父组件触发 re-render。相当于是 PureComponent。
那么 dispatch 导致的 store 中 state 的变化是如何影响 connect 后的组件的 re-render 的呢?被 connect 的组件最终是否要 re-render,取决于被 connect 组件被传入的 props 是否变了。所以,关键点在于计算被 connect 组件最终被传入的 props。关键函数为:
关键点就在于 selectorFactory 的实现:
可以看到,最终通过 mapStateToProps、mapDispatchToProps、mergeProps 计算得到最终的 props。
根据 props 是否变了,决定是否要触发组件 re-render。所以,在写 mapStateToProps 时,一定要按需从 store 的 state 中取需要的 state map 到 props 中,否则每次 dispatch 都可能导致 re-render。
对于被 connect wrapped 的组件,需要在 forwardedRef, WrappedComponent, actualChildProps 有更新时,才会重新计算触发 re-render,这样又能对性能进一步优化。
最后是前面提过的,最终渲染的 Child,也用 useMemo 进行性能优化。
3. 如何保障 connect 后的组件在 store 变化时,按组件层级顺序渲染?
react-redux 实现了自上而下的订阅逻辑。处于低层的组件会订阅到最近的 connect 了 store 的父组件 Subscriptions 实例。而当触发 re-render 时,父组件将先被触发,如果需要 re-render,将先触发父组件的 setState(useReducer),之后,当父组件渲染完成才在 useLayoutEffect 中触发子组件的回调事件。如果父组件不需要 re-render,那么直接触发子组件回调事件。这样,保证了组件是按层级顺序渲染的。
Components higher in the tree always subscribe to the store before their children do
代码:
react-redux 7 和 context 的关系
React-redux 只用 context 来传递 store,然后用 store.subscribe() 来注册监听 store 中 state 的变化。不直接用 context 传递数据能很大提升性能。因为 Context.Provider 中 value 每次变化都会导致用到 useContext 的子孙组件的 re-render。
v6 直接用 context 传递数据,性能不好,在 v7 重新实现了相关算法:React-Redux Roadmap: v6, Context, Subscriptions, and Hooks reduxjs/react-redux#1177
react-redux 5、6、7 数据流对比
详细对比见 reduxjs/react-redux#1177 (comment)。这里不重复了。这个例子有助于我们理解嵌套的 connect 后的组件的 re-render 的详细流程。
dva 对 redux 数据流的封装
loading state 的添加
antd 的 CongfigProvider
antd 的 ConfigProvider 是基于 react context api 的封装,在其实现中,每次 ConfigProvider re-render 会导致 context provider 中的 value 变化,从而导致订阅了 ConfigProvider 的组件都 re-render。
context 导致 re-render 对比例子:
bug:https://codesandbox.io/s/unstated-next-with-memo-9k4rb
fix:https://codesandbox.io/s/unstated-next-with-memo-fix-stpjw
性能优化
Context api 的性能其实比 redux 要差,所以 ConfigProvider 的优化也是十分要紧的,将 antd 的 ConfigProvider 往上层提,避免组件 re-render 导致 ConfigProvider re-render 从而导致所有订阅了 ConfigProvider 的 antd 组件 re-render
redux 很快,但是不恰当使用,将使你的应用非常的慢:https://react-redux.js.org/api/connect
connect 包装后的组件相当于 PureComponent,此时父组件传 props 给 connect 后的子组件时,尽量不要传子组件不会用到的属性,因为如果这些属性变了也会导致子组件的 re-render,而如果不传不必要的属性,则能避免这些不必要的 re-render
恰当地使用 PureComponent 或 React.memo,避免不必要的 re-render,优化性能。但是,如果你确定你的组件在每次父组件 re-render 或者组件自身调用 setState 时都要 re-render,此时可以不用 PureComponent 或 React.memo,在一些情况下 PureComponent 可能会引起不想要的结果
不需要修改全局 store 的操作,例如发送数据请求给该组件自己用,可以不调用 dispatch,彻底避免掉 redux 的工作,这里涉及到 dva 对 dispatch 请求的封装,会修改 store 中的 loading state,导致订阅了该属性的组件被 re-render
React 官方建议不要进行 DOM 节点跨层级的操作。在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。
参考文献
The text was updated successfully, but these errors were encountered: