React - 高级指引
代码分割
打包
- 大多数 React 应用都会使用 Webpack、Rollup、Browserify 这类构建工具打包文件
- 打包是一个将文件引入并合并到一个单独文件的过程,最终形成一个 “bundle”
代码分割
- 打包器 Rollup- Webpack
- Browserify
- factor-bundle 能够创建多个包并在运行时动态加载
 
- 引入代码分割的最佳方式是动态 import()
| 1 | // Webpack 解析该语法时,会自动进行代码分割 | 
- 使用 Babel 时,确保 Babel 能够解析动态 import 语法而不是将其转换。对于这一要求需要 babel-plugin-syntax-dynamic-import 插件
React.lazy
- React.lazy 函数能像渲染常规组件一样处理动态引入(的组件)
- 想要在使用服务端渲染的应用中使用,推荐 Loadable Components 这个库,它有一个很棒的服务端渲染打包指南
| 1 | import React, {Suspense} from 'react'; | 
基于路由的代码分割
| 1 | // 使用 React.lazy 和 React Router 这类的第三方库,来配置基于路由的代码分割 | 
Context
何时用 Context
- Context 提供一种在组件之间共享此类值的方式,不必显式地通过组件树逐层传递 props
- 应用场景:很多不同层级的组件需要访问同样的数据,谨慎使用,因为这会使得组件的复用性变差,包括管理当前的 locale、theme、或者一些缓存数据
- 如果只是想避免层层传递一些属性,组件组合(component composition)有时是比 context 更好的解决方案
| 1 | // Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。 | 
API
- React.createContext
| 1 | // React 渲染订阅Context 对象的组件,组件会从组件树中离自身最近匹配的 Provider 中读取到当前的 context 值 | 
- Context.Provider- 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化
- Provider 接收一个 value 属性,传递给消费组件
- 一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据
- value 变化时,它内部所有消费组件都会重新渲染
- Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate函数,当 consumer 组件在其祖先组件退出更新的情况下也能更新
 
| 1 | <MyContext.Provider value={/* 某个值 */}> | 
- Class.contextType- 此属性可以让你使用 this.context 来获取最近 Context 上的值
 
| 1 | // 可以在任何生命周期中访问到它,包括 render 函数中 | 
- Context.Consumer
| 1 | // 传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值,没有Provider 就是createContext的defaultValue | 
- Context.displayName- context 对象接受 displayName 的 property,字符串类型。React DevTools 使用该字符串来确定 context 要显示的内容1 
 2
 3
 4
 5const MyContext = React.createContext(/* some value */); 
 MyContext.displayName = 'MyDisplayName';
 <MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
 <MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
 
- context 对象接受 displayName 的 property,字符串类型。React DevTools 使用该字符串来确定 context 要显示的内容
- 动态 Context- 在嵌套组件中更新 Context(通过 context 传递一个函数,使得消费组件更新 context)1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23// 确保传递给 createContext 的默认值数据结构是调用组件能匹配的 
 export const ThemeContext = React.createContext({
 theme: themes.dark,
 toggleTheme: () => {},
 });
 // 调用方
 function ThemeTogglerButton() {
 // Theme Toggler 按钮不仅仅只获取 theme 值,
 // 它也从 context 中获取到一个 toggleTheme 函数
 return (
 <ThemeContext.Consumer>
 {({theme, toggleTheme}) => (
 <button
 onClick={toggleTheme}
 style={{backgroundColor: theme.background}}>
 Toggle Theme。。。
 </button>
 )}
 </ThemeContext.Consumer>
 );
 }
 
- 在嵌套组件中更新 Context(通过 context 传递一个函数,使得消费组件更新 context)
- 注意- context根据引用标识决定何时渲染(本质上是 value 属性值的浅比较),陷阱:provider 父组件进行重渲染时,可能会在消费组件中触发意外的渲染- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- // 每一次 Provider 重渲染时,value 属性总是被赋值为新的对象,会重新渲染下面所有的消费组件 
 // 改进:value 状态提升到父节点的 state
 class App extends React.Component {
 render() {
 return (
 <MyContext.Provider value={{something: 'something'}}> // value={this.state.value}
 <Toolbar />
 </MyContext.Provider>
 );
 }
 }
 
- 错误边界- 为了解决:部分UI的js错误导致整个应用崩溃
- 它是React 组件,可以捕获并打印发生在其子组件树任何位置的js错误,会渲染出备用 UI,而不是渲染那些崩溃了的子组件树
- 在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误
- 无法捕获的错误- 事件处理
- 异步代码(setTimeout 或 requestAnimationFrame 回调函数)
- 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
 
- class 组件定义了 static getDerivedStateFromError() 或 componentDidCatch() 任意一个(或两个)时,它就变成一个错误边界1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25class ErrorBoundary extends React.Component { 
 constructor(props) {
 super(props);
 this.state = { hasError: false };
 }
 static getDerivedStateFromError(error) {
 // 更新 state 使下一次渲染能够显示降级后的 UI
 return { hasError: true };
 }
 componentDidCatch(error, errorInfo) {
 // 你同样可以将错误日志上报给服务器
 logErrorToMyService(error, errorInfo);
 }
 render() {
 if (this.state.hasError) {
 // 你可以自定义降级后的 UI 并渲染
 return <h1>Something went wrong.</h1>;
 }
 return this.props.children;
 }
 }
- 工作方式类似于js的 catch {},不同于错误边界只针对 React 组件
- 只有 class 组件才可以成为错误边界组件。大多数情况下, 只需要声明一次错误边界组件, 可以在整个应用中使用它
- 可以包装在最顶层的路由组件展示一个 “Something went wrong” 的错误信息,就像服务端框架处理崩溃一样
 
未捕获错误(Uncaught Errors)的新行为
- 任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载
组件栈追踪
- 渲染期间发生的所有错误打印到控制台,仅用于开发环境,生产环境必须将其禁用
- 组件名称在栈追踪中的显示依赖于 Function.name 属性
- 如要支持 未提供该功能的旧版浏览器和设备(例如 IE 11),在打包(bundled)应用程序中包含一个 Function.name 的 polyfill(function.name-polyfill)或在所有组件上显式设置
 displayName 属性
关于 try/catch
- 仅用于命令式代码
- 错误边界无法捕获事件处理器内部的错误 使用try/catch
- 错误边界保留了 React 的声明性质,例:即使错误发生在 componentDidUpdate 方法中,由某一个深层组件树的 setState 引起,仍然能冒泡到最近的错误边界
Refs
- Refs 可以 - 访问DOM节点、- React元素
- 使用场景(勿过度使用) - 处理焦点,文本选择或媒体播放
- 触发强制动画
- 集成第三方DOM库
 
- 创建 Refs 
| 1 | // React.create()创建,通过 ref 属性附加到元素上 | 
- 访问 Refs,节点的类型影响ref的取值
- ref属性指向一个 DOM 元素或 class组件
- 不能在函数组件上使用ref属性,因为它没有实例
| 1 | function MyFunctionComponent() { | 
- 在父组件中引用子节点的DOM节点,使用ref转发
转发 refs 到 DOM 组件
- Ref 转发是一个可选特性,组件可以像暴露自己的ref一样暴露子组件的ref
| 1 | const FancyButton = React.forwardRef((props, ref) => ( | 
在高阶组件(HOC)中转发 refs
- ref 不是 属性,如果对 HOC 添加 ref,ref 会引用最外层的容器组件,而不是被包裹的组件
| 1 | function logProps(Component) { | 
回调 Refs
- 内联函数定义的 ref 回调,更新执行两次,第一次传参为null,第二次才是DOM元素- 因为每次渲染会创建新的函数实例,定义为class 绑定函数的方式可避免
 
| 1 | function CustomTextInput(props) { | 
Fragments
- 一个组件返回多个元素,Fragments 将子列表分组,不用向 DOM 添加额外节点
| 1 | class Columns extends React.Component { | 
高阶组件
- 基于 React 的组合特性而形成的设计模式
- 高阶组件是参数为组件,返回值为新组件的函数(组件转化为另一个组件),纯函数
- HOC 不会修改传入的组件,也不会使用继承来复制其行为
不要改变原始组件,使用组合
| 1 | function logProps(InputComponent) { | 
将不相关的 props 传递给被包裹的组件
- (HOC 应该透传与自身无关的 props)[https://react.docschina.org/docs/higher-order-components.html]
最大化可组合性
- 最常见的HOC
| 1 | // React Redux 的 `connect` 函数 | 
| 1 | // connect 是一个函数,它的返回值为另外一个函数。 | 
不要在 render 方法中使用 HOC
| 1 | render() | 
务必复制静态方法
- 新组件没有原始组件的任何静态方法
| 1 | // 在返回之前把这些方法拷贝到容器组件上 | 
与第三方库协同
- React 不会理会 React 自身之外的 DOM 操作
- 它根据内部虚拟 DOM 来决定是否需要更新,如果同一个 DOM 节点被另一个库操作了,React 会觉得困惑而且没有办法恢复
- 避免冲突的最简单方式就是防止 React 组件更新(渲染无需更新的 React 元素,比如一个空的 )
性能优化
使用生产版本
虚拟化长列表
- 应用渲染长列表(上百甚至上千的数据)使用“虚拟滚动”
- 热门的虚拟滚动库:react-window
 和 react-virtualized
避免调停
- 组件的props或state变更,React会将最新返回的元素与之前渲染的元素对比,决定是否有必要更新真实的 DOM。如不相同,React 会更新该 DOM
- React 只更新改变了的 DOM 节点,重新渲染仍然花费一些时间,如果很慢,可用shouldComponentUpdate提速,该方法会在重新渲染前被触发
| 1 | // 默认实现总是返回 true | 
- 大部分情况可以继承 React.PureComponent以代替手写 shouldComponentUpdate()
shouldComponentUpdate 的作用
| 1 | class CounterButton extends React.Component { | 
| 1 | // 这个代替上面,仅做对象的浅比较,无法检查深层差别 | 
- 避免上面问题:改变指针
| 1 | handleClick() | 
| 1 | function updateColorMap(colormap) { | 
Portals
- 将子节点渲染到父组件以外的DOM节点
| 1 | ReactDOM.createPortal(child, container) | 
| 1 | render() | 
- 应用:父组件有 overflow: hidden或z-index需要在视觉上跳出容器
- portal 存在于 React 树, 且与 DOM 树 中的位置无关
- 从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先
Profiler
- 可以添加在 React 树中的任何地方测量树中这部分渲染所带来的开销
- 两个prop: id(组件)、组件更新被调用的回调函数
| 1 | render( | 
回调参数
| 1 | function onRenderCallback( | 
没有使用ES6
- class 关键字定义组件,还可以使用 create-react-class模块
| 1 | var createReactClass = require('create-react-class'); | 
- 函数组件和class组件,都有defaultProps属性
| 1 | class Greeting extends React.Component { | 
- 初始化 state
| 1 | class Counter extends React.Component { | 
- createReactClass()创建组件,方法自动绑定到实例不需要在constructor中.bind(this)
Mixins
- es6本身不包含任何 mixin,class组件不支持
- mixins(js对象:通过它封装通用的函数) 调用在组件之前
协调
Diffing 算法
- 对比不同类型的元素:根结点为不同类型元素时,React会拆卸原有的树建起新的树。根节点以下的组件也会被卸载,它们的状态会被销毁
- 对比同一类型的元素:会保留DOM节点,仅对比更新有改变的属性
- 对比同类型的组件元素:组件实例不变,更新组件实例的props 跟 最新的元素保持一致
- 对子节点进行递归:递归DOM节点的子元素时,React会同时遍历两个子元素列表,产生差异时,生成一个mutation
| 1 | // 匹配完前两个最后插入第三个 | 
- keys:为了解决以上消耗性能,React使用key匹配原有树上的子元素与最新树上的子元素- 避免使用index,有顺序修改,diff变慢
 
Render Props
- 用于告知组件需要渲染什么内容的函数prop
| 1 | // 追逐鼠标 | 
| 1 | // 如果你出于某种原因真的想要 HOC,那么你可以轻松实现 | 
| 1 | // 不必使用render也可以 | 
- Render Props与- React.PureComponent避免一起使用,每一个 render 对于render prop总会生成新值,浅比较props的时候总会是false
| 1 | class MouseTracker extends React.Component { | 
静态类型检查
- 运行前识别类型问题,使用 Flow或TypeScript来代替PropTypes
Flow
- 通过类型注释的特殊语法 扩展js,浏览器解析不了这种语法
- 编译后的代码去除 Flow 语法
TypeScript
- 在tsconfig.json中定义配置项
- .tsx 包含 JSX 代码的 ts文件
类型定义
- 判断一个库是否包含类型,index.d.ts或者 package.json 文件的typings或types属性中指定类型文件
- DefinitelyTyped为没有声明文件的js库提供类型定义
| 1 | // yarn | 
- 局部声明:使用的包里没有声明文件,在 DefinitelyTyped 上也没有,创建本地定义文件,根目录declarations.d.ts 文件
| 1 | declare | 
严格模式
- StrictMode显示应用程序中潜在问题的工具,不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告
| 1 | <React.StrictMode> | 
识别不安全的生命周期
使用过时字符串 ref API 的警告
- createRef方式会警告,回调 ref 依旧适用
使用废弃的 findDOMNode 方法的警告
- 只读一次的 API:调用只会返回第一次查询的结果。子组件渲染不同的节点,无法跟踪更改
- findDOMNode使父组件需要单独渲染子组件,产生重构;
- 仅在组件返回单个且不可变的 DOM节点才有效
- ref传递、转发
检测意外的副作用
- React 工作的两个阶段- 渲染:确定进行的更改(新旧DOM树对比)
- 渲染阶段的声明周期可能会被多次调用- constructor
- componentWillMount (or UNSAFE_componentWillMount)
- componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
- componentWillUpdate (or UNSAFE_componentWillUpdate)
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- setState 更新函数(第一个参数)
 
- 提交:React 应用变化时(React DOM插入、更新、删除节点)调用生命周期方法
 
检测过时的 context API
使用 PropTypes 进行类型检查
- 限制单个元素
| 1 | import PropTypes from 'prop-types'; | 
- 默认 Prop 值- 类型检查适用 defaultProps,发生在它赋值后
 
| 1 | class Greeting extends React.Component { | 
非受控组件
- 受控组件:表单数据React组件管理
- 非受控组件:表单数据由DOM节点处理(ref操作)
- 默认值
| 1 | <input | 
- <input type="file"/>始终是一个非受控组件
Web Components
- 为可复用组件提供了强大的封装,而 React 提供了声明式的解决方案,使 DOM 与数据保持同步
| 1 | class XSearch extends HTMLElement { | 



