请选择 进入手机版 | 继续访问电脑版
React推出新的Context API时,很多人都高呼:"终于可以丢掉Redux了!"
But,事实是,新Context API出来也大半年了,依然不见它完全淘汰Redux。我个人也倾向于能用React解决的事情就不劳烦Redux这样的第三方工具,但是,不得不承认,有些事情不要想得太天真,Redux说到底就是专门为状态管理而诞生的,再差也能管理好状态;React Context说到底只是组件,让它去管理状态,不得不多费一些心。
因为要多费一些心,所以还是有门槛的,如果你不费心,很容易就把事情做错。
今天说的,就是Context没用好,可能毁你应用的性能。
Redux有action和reducer,用这两样东西来更新状态的确很啰嗦,现在,假设我们用Context来代替Redux,一个很大的动因就是不想太啰嗦,我们不想写reducer和action,那该怎么做呢?
第一感觉,是直接在Consumer中拿到value之后去修改其中的值,就像下面这样,可是,这样不行。
  1. [/code]上面这样做,虽然真的能够改变value上一个属性的值,但是没有改变value本身,简单说就是React并不知道Context的value被修改了,所以React也不会通知其他Consumer这个变化,也不会引起任何重新渲染,但是,我们改value就是要引起重新渲染,可见这招不行。
  2. 你要是在上面的代码中直接去改value,像下面这样,更没有任何作用,因为你只不过改了一个函数参数值而已,甚至都没有改变Context中的value。
  3. [code]
复制代码
所以,还是需要其他办法。
一个惯常的做法,是让Context的value包含函数类型的属性,让Consumer可以通过调用这个函数来修改组件的state,从而引发重新渲染。
用一个简单的切换主题(Theme)的例子来说明问题,代码如下。
  1. [/code]上面的App组件是应用的最顶层组件,渲染出一个Context的Provider,Provider的子组件是应用中其他组件,这非常合理。
  2. App组件的state保存当前主题,给Provider的value里,除了包含当前主题,还给一个函数switchTheme,给Consumer一个机会来切换主题,一个Consumer的例子就是Content组件,代码如下(这里我也赶时髦用Hooks的useContext实现)。
  3. [code]
复制代码
界面是这样,点击"Red Theme"或者"Green Theme"就可以切换Hello World的颜色。
避免React Context导致的重复渲染-1.jpg
我们来梳理一遍这个过程。
    点击按钮,调用了Context value中的switchTheme函数;switchTheme函数调用了顶层组件App中的setState,修改了state中的theme;因为App的状态被改变,所以App被重新渲染;App重新渲染,Context.Provider被重新渲染;Context.Provider被重新渲染,发现value和上次的不同,所以会引发所有的Consumer被重新渲染;Content组件作为一个Consumer,被重新渲染,用新的Context value主题重画。
步骤有一点多,但是的确就是这么一个过程,一切都工作正常。
真的正常吗?
功能虽然正常,但是有一件事不是我们想要的,就是当Context.Provider重新渲染的时候,它所有的子组件都被重新渲染了,比如上面例子中子组件有Header和Content,Content作为Consumer之一重画没问题,但是Header不是Consumer,也不依赖于Context的value,根本没有必要重画啊!
大家可以在这里试验一下 context-rerender-demo - CodeSandbox ,在console里可以看到,每一次切换主题,Header的render都会被调用。
在上面的例子中,如果除了Header还有其他比较重的组件,而且这些组件没有用shouldComponentUpdate守住,那么每一次Context的改变,都会引发整个应用组件树的重画,代价就有一点大了。
这就是我说的,如果不费心,就容易把事情做错。
其实,Context.Provider说到底还是组件,也按照组件基本法来办事,当value发生变化时,它也可以不引发子组件的渲染,前提是,子组件作为一个属性(this.props.children)也要保持不变才行,如果子组件变了,Context.Provider 也不知道你是不是以前的你,只好让你重画了。
表面上看,下面的JSX中Context.Provider的子组件任何一次渲染都是一样的。
  1. [/code] 其实并不是这样,JSX会被转译成React.CreateElement,所以上面的JSX运行时是类似这样。
  2. [code]
复制代码
你看,每一次渲染都调用React.createElement,所以每一次渲染产生的子组件都是不一样的啊。
所以,我们需要一个方法“说服”Context.Provider,告诉他你的子组件没有变化,方法也很简单,就是建一个独立的组件来管理state和Provider,把子组件的JSX写在这个组件之外。
我们改进上面的代码,制造一个ThemeProvider,代码如下。
[code][/code] 现在App成了一个无状态组件,只渲染一次,因为state改为ThemeProvider来管理,每次当ThemeProvider的state被switchTheme改变而重新渲染的时候,它看到的子组件(this.props.children)是App传给他的,不需要重新用React.createElement穿件,所以this.props.children是不变的,于是Context.Provider也就不会让this.props.children重新渲染了。
改进的代码在这里 context-avoid-rerender-demo - CodeSandbox,大家可以试试 ,现在切换Theme不会引发Header的重新渲染了。
总结一下,就是Context虽然是一个好东西,但是不要指望无脑使用就能用它替换掉Redux,你要是乱用的话,可能给自己带来更大麻烦。
(P.S. 预告一下,我近期会出一个关于React设计模式的电子读物,这次尝试一下非传统出版方式,希望大家能够喜欢,敬请期待)
(加入知识星球《进击的React》,随时了解React技术和社区动态)
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|翁笔

© 2001-2018 Wengbi.com

返回顶部