前言

我工作中的技术栈主要是 React + TypeScript,这篇文章我想总结一下如何在项目中运用 React 的一些技巧去进行性能优化,或者更好的代码组织。

性能优化的重要性不用多说,谷歌发布的很多调研精确的展示了性能对于网站留存率的影响,而代码组织优化则关系到后续的维护成本,以及你同事维护你代码时候“口吐芬芳”的频率????,本篇文章看完,你一定会有所收获。

神奇的 children

我们有一个需求,需要通过 Provider 传递一些主题信息给子组件:

看这样一段代码:

import React, { useContext, useState } from "react";const ThemeContext = React.createContext();export function ChildNonTheme() {console.log("不关心皮肤的子组件渲染了");return <div>我不关心皮肤,皮肤改变的时候别让我重新渲染!</div>;
}export function ChildWithTheme() {const theme = useContext(ThemeContext);return <div>我是有皮肤的哦~ {theme}</div>;
}export default function App() {const [theme, setTheme] = useState("light");const onChangeTheme = () => setTheme(theme === "light" ? "dark" : "light");return (<ThemeContext.Provider value={theme}><button onClick={onChangeTheme}>改变皮肤</button><ChildWithTheme /><ChildNonTheme /><ChildNonTheme /><ChildNonTheme /><ChildNonTheme /><ChildNonTheme /><ChildNonTheme /><ChildNonTheme /></ThemeContext.Provider>);
}

这段代码看起来没啥问题,也很符合撸起袖子就干的直觉,但是却会让 ChildNonTheme 这个不关心皮肤的子组件,在皮肤状态更改的时候也进行无效的重新渲染。

这本质上是由于 React 是自上而下递归更新,<ChildNonTheme /> 这样的代码会被 babel 翻译成 React.createElement(ChildNonTheme) 这样的函数调用,React官方经常强调 props 是immutable 的,所以在每次调用函数式组件的时候,都会生成一份新的 props 引用。

来看下 createElement 的返回结构:

const childNonThemeElement = {type: 'ChildNonTheme',props: {} // <- 这个引用更新了
}

正是由于这个新的 props 引用,导致 ChildNonTheme 这个组件也重新渲染了。

那么如何避免这个无效的重新渲染呢?关键词是「巧妙利用 children」。

import React, { useContext, useState } from "react";const ThemeContext = React.createContext();function ChildNonTheme() {console.log("不关心皮肤的子组件渲染了");return <div>我不关心皮肤,皮肤改变的时候别让我重新渲染!</div>;
}function ChildWithTheme() {const theme = useContext(ThemeContext);return <div>我是有皮肤的哦~ {theme}</div>;
}function ThemeApp({ children }) {const [theme, setTheme] = useState("light");const onChangeTheme = () => setTheme(theme === "light" ? "dark" : "light");return (<ThemeContext.Provider value={theme}><button onClick={onChangeTheme}>改变皮肤</button>{children}</ThemeContext.Provider>);
}export default function App() {return (<ThemeApp><ChildWithTheme /><ChildNonTheme /><ChildNonTheme /><ChildNonTheme /><ChildNonTheme /><ChildNonTheme /><ChildNonTheme /><ChildNonTheme /></ThemeApp>);
}

没错,唯一的区别就是我把控制状态的组件和负责展示的子组件给抽离开了,通过 children 传入后直接渲染,由于 children 从外部传入的,也就是说 ThemeApp 这个组件内部不会再有 React.createElement 这样的代码,那么在 setTheme 触发重新渲染后,children 完全没有改变,所以可以直接复用。

让我们再看一下被 ThemeApp 包裹下的 <ChildNonTheme />,它会作为 children 传递给 ThemeAppThemeApp 内部的更新完全不会触发外部的 React.createElement,所以会直接复用之前的 element 结果:

// 完全复用,props 也不会改变。
const childNonThemeElement = {type: ChildNonTheme,props: {}
}

在改变皮肤之后,控制台空空如也!优化达成。

总结下来,就是要把渲染比较费时,但是不需要关心状态的子组件提升到「有状态组件」的外部,作为 children 或者props传递进去直接使用,防止被带着一起渲染。

神奇的 children - 在线调试地址

当然,这个优化也一样可以用 React.memo 包裹子组件来做,不过相对的增加维护成本,根据场景权衡选择吧。

Context 读写分离

想象一下,现在我们有一个全局日志记录的需求,我们想通过 Provider 去做,很快代码就写好了:

import React, { useContext, useState } from "react";
import "./styles.css";const LogContext = React.createContext();function LogProvider({ children }) {const [logs, setLogs] = useState([]);const addLog = (log) => setLogs((prevLogs) => [...prevLogs, log]);return (<LogContext.Provider value={{ logs, addLog }}>{children}</LogContext.Provider>);
}function Logger1() {const { addLog } = useContext(LogContext);console.log('Logger1 render')return (<><p>一个能发日志的组件1</p><button onClick={() => addLog("logger1")}>发日志</button></>);
}function Logger2() {const { addLog } = useContext(LogContext);console.log('Logger2 render')return (<><p>一个能发日志的组件2</p><button onClick={() => addLog("logger2")}>发日志</button></>);
}function LogsPanel() {const { logs } = useContext(LogContext);return logs.map((log, index) => <p key={index}>{log}</p>);
}export default function App() {return (<LogProvider>{/* 写日志 */}<Logger1 /><Logger2 />{/* 读日志 */}<LogsPanel /></div></LogProvider>);
}

我们已经用上了上一章节的优化小技巧,单独的把 LogProvider 封装起来,并且把子组件提升到外层传入。

先思考一下最佳的情况,Logger 组件只负责发出日志,它是不关心logs的变化的,在任何组件调用 addLog 去写入日志的时候,理想的情况下应该只有 LogsPanel 这个组件发生重新渲染。

但是这样的代码写法却会导致每次任意一个组件写入日志以后,所有的 LoggerLogsPanel 都发生重新渲染。

这肯定不是我们预期的,假设在现实场景的代码中,能写日志的组件可多着呢,每次一写入就导致全局的组件都重新渲染?这当然是不能接受的,发生这个问题的本质原因官网 Context 的部分已经讲得很清楚了:

LogProvider 中的 addLog 被子组件调用,导致 LogProvider重渲染之后,必然会导致传递给 Provider 的 value 发生改变,由于 value 包含了 logssetLogs 属性,所以两者中任意一个发生变化,都会导致所有的订阅了 LogProvider 的子组件重新渲染。

那么解决办法是什么呢?其实就是读写分离,我们把 logs(读)和 setLogs(写)分别通过不同的 Provider 传递,这样负责写入的组件更改了 logs,其他的「写组件」并不会重新渲染,只有真正关心 logs 的「读组件」会重新渲染。

function LogProvider({ children }) {const [logs, setLogs] = useState([]);const addLog = useCallback((log) => {setLogs((prevLogs) => [...prevLogs, log]);}, []);return (<LogDispatcherContext.Provider value={addLog}><LogStateContext.Provider value={logs}>{children}</LogStateContext.Provider></LogDispatcherContext.Provider>);
}

我们刚刚也提到,需要保证 value 的引用不能发生变化,所以这里自然要用 useCallbackaddLog 方法包裹起来,才能保证 LogProvider 重渲染的时候,传递给的LogDispatcherContext的value 不发生变化。

现在我从任意「写组件」发送日志,都只会让「读组件」LogsPanel 渲染。

Context 读写分离 - 在线调试

Context 代码组织

上面的案例中,我们在子组件中获取全局状态,都是直接裸用 useContext

import React from 'react'
import { LogStateContext } from './context'function App() {const logs = React.useContext(LogStateContext)
}

但是是否有更好的代码组织方法呢?比如这样:

import React from 'react'
import { useLogState } from './context'function App() {const logs = useLogState()
}
// context
import React from 'react'const LogStateContext = React.createContext();export function useLogState() {return React.useContext(LogStateContext)
}

在加上点健壮性保证?

import React from 'react'const LogStateContext = React.createContext();
const LogDispatcherContext = React.createContext();export function useLogState() {const context = React.useContext(LogStateContext)if (context === undefined) {throw new Error('useLogState must be used within a LogStateProvider')}return context
}export function useLogDispatcher() {const context = React.useContext(LogDispatcherContext)if (context === undefined) {throw new Error('useLogDispatcher must be used within a LogDispatcherContext')}return context
}

如果有的组件同时需要读写日志,调用两次很麻烦?

export function useLogs() {return [useLogState(), useLogDispatcher()]
}
export function App() {const [logs, addLogs] = useLogs()// ...
}

根据场景,灵活运用这些技巧,让你的代码更加健壮优雅~

组合 Providers

假设我们使用上面的办法管理一些全局的小状态,Provider 变的越来越多了,有时候会遇到嵌套地狱的情况:

const StateProviders = ({ children }) => (<LogProvider><UserProvider><MenuProvider><AppProvider>{children}</AppProvider></MenuProvider></UserProvider></LogProvider>
)function App() {return (<StateProviders><Main /></StateProviders>)
}

有没有办法解决呢?当然有,我们参考 redux 中的 compose 方法,自己写一个 composeProvider 方法:

function composeProviders(...providers) {return ({ children }) =>providers.reduce((prev, Provider) => <Provider>{prev}</Provider>,children,)
}

代码就可以简化成这样:

const StateProviders = composeProviders(LogProvider,UserProvider,MenuProvider,AppProvider,
)function App() {return (<StateProvider><Main /></StateProvider>)
}

总结

本篇文章主要围绕这 Context 这个 API,讲了几个性能优化和代码组织的优化点,总结下来就是:

  1. 尽量提升渲染无关的子组件元素到「有状态组件」的外部。

  2. 在需要的情况下对 Context 进行读写分离。

  3. 包装Context 的使用,注意错误处理。

  4. 组合多个 Context,优化代码。

最后

  • 欢迎加我微信(winty230),拉你进技术群,长期交流学习...

  • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...

点个在看支持我吧

我在大厂写React学到了什么?性能优化篇相关推荐

  1. 在网上看到和篇关于sql server 2005的性能优化篇,觉得写得很好。

    在网上看到和篇关于sql server 2005的性能优化篇,觉得写得很好. SQL Server2005扩展函数已经不是一件什么新鲜的事了,但是我看网上的大部分都是说聚合函数,例子也比较浅,那么这里 ...

  2. react循环key值_React性能优化的几个知识点

    各位同学大家晚上好,今天来说说react相关的东西.<从零玩转React全家桶核心(21)>正在更新,视频版请登录官网(www.it666.com)查看,或者扫码直达: Diff算法 开发 ...

  3. React之SCU(性能优化篇)

    一.概念 shouldComponentUpdate简称SCU,是React中性能优化的重要一环. shouldComponentUpdate(nextProps, nextState) {// 判断 ...

  4. 【凯子哥带你学Android】Andriod性能优化之列表卡顿——以“简书”APP为例

    这几天闲得无聊,就打开手机上的开发者模式里面的"GPU过度绘制"功能,看看别家的App做的咋样,然后很偶然的打开了"简书",然后就被它的过度绘制惊呆了,于是写了 ...

  5. react 组件遍历】_从 Context 源码实现谈 React 性能优化

    (给前端大全加星标,提升前端技能) 转自:魔术师卡颂 学完这篇文章,你会收获: 了解Context的实现原理 源码层面掌握React组件的render时机,从而写出高性能的React组件 源码层面了解 ...

  6. 50家大厂面试万字精华总结,关于Android性能优化的几点建议

    什么是中年危机 根据权威数据显示,国内IT程序员鼎盛时期是在25-27岁左右,30岁对于程序员而言完全是一个38线,接着就是转业转岗的事情,这一点在业界也算是一个共识了. 大学毕业步入IT行业普遍年龄 ...

  7. React Native性能优化总结

    React Native开源已经接近2年时间,京东.携程.58同城等互联网公司都在使用,公司于今年也开始使用,并推广到各个新项目.本文重点分享我们遇到的一些问题以及优化方案. 一.为什么会引入Reac ...

  8. HBase写性能优化策略

    HBase写入通常会遇到两种问题: # 写的性能很差 # 根本写不进去 一 HBase写入性能优化 1.1 是否需要写WAL? WAL是否需要同步写? WAL机制可以确保数据即使写入缓存的数据丢失了, ...

  9. react 日期怎么格式化_手写React的Fiber架构,深入理解其原理

    熟悉React的朋友都知道,React支持jsx语法,我们可以直接将HTML代码写到JS中间,然后渲染到页面上,我们写的HTML如果有更新的话,React还有虚拟DOM的对比,只更新变化的部分,而不重 ...

最新文章

  1. LeetCode 26 Remove Duplicates from Sorted Array [Array/std::distance/std::unique] c++
  2. 在二叉树中找到累加和为指定值的最长路径长度
  3. ASP.NET MVC编程——控制器
  4. [ZJOI2007]报表统计(链表法+set)
  5. Liunx 命令大全
  6. 计算机的双一流学校,分数不够上双一流大学计算机专业,上这些大学也不错,实力非常强...
  7. 极具设计感的专辑分类设计,给你带来不一样的灵感
  8. cmd命令行怎样运行python_在CMD命令行中运行python脚本的方法
  9. NASA的10条代码编写原则
  10. RabbitMQ学习总结(一)——基础概念详细介绍
  11. 蚂蚁课堂视频笔记思维导图-4期 四、微服务安全
  12. 网络安全法学习整理笔记
  13. freeswitch SIP信令的接收
  14. 【踩坑笔记】java使用poi导出word文档换行
  15. upc 9367 雷涛的小猫
  16. transition、-moz-transition、-webkit-transition、-o-transition是什么意思?怎样用?
  17. 统计信息:SQL执行优化之密钥
  18. Nginx (一) Nginx介绍 正向代理 反向代理 及配置
  19. Windows 10(Win10) 怎么删除设备和驱动里的CD驱动器
  20. 金融危机殃及色*情业,女*优转行做黑客?

热门文章

  1. 关于python字符编码以下选项中错误的是_关于Python文件打开模式的描述,以下选项中错误的是...
  2. “东信杯”广西大学第一届程序设计竞赛(同步赛)D、数论只会GCD 【博弈 分类讨论】...
  3. word 中字号与数字(磅pt)对应关系
  4. 关于寄存器ESP和EBP的一些理解
  5. MySQL数据库机器配置的3个网络参数
  6. Eureka的UNKNOWN
  7. SHOGUN toolbox的一些使用心得
  8. 解决json string转object,value值存在英语双引号,无法解析问题
  9. 范式存在定理及其证明
  10. QT TCP局域网通讯工具 V1.0