阅读 React 官方文档 Part-2
Published by powerfulyang on Nov 21, 2022
事件处理
你必须谨慎对待 JSX 回调函数中的 this
,在 JavaScript 中,class 的方法默认不会绑定 this
。如果你忘记绑定 this.handleClick
并把它传入了 onClick
,当你调用这个函数的时候 this
的值为 undefined
。
这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。通常情况下,如果你没有在方法后面添加 ()
,例如 onClick={this.handleClick}
,你应该为这个方法绑定 this
。
如果觉得使用 bind
很麻烦,这里有两种方式可以解决。你可以使用 public class fields 语法 to correctly bind callbacks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class LoggingButton extends React.Component { // This syntax ensures `this` is bound within handleClick. handleClick = () => { console.log('this is:', this); }; render() { return ( <button onClick={this.handleClick}> Click me </button> ); } }
如果你没有使用 class fields 语法,你可以在回调中使用箭头函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class LoggingButton extends React.Component { handleClick() { console.log('this is:', this); } render() { // 此语法确保 `handleClick` 内的 `this` 已被绑定。 return ( <button onClick={() => this.handleClick()}> Click me </button> ); } }
此语法问题在于每次渲染 LoggingButton
时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。
避免兜底
任何组件都可能因渲染而暂停,甚至是已经展示给用户的组件。为了使屏幕内容始终一致,如果一个已经显示的组件暂停,React 必须隐藏它的树,直到最近的 <Suspense>
边界。然而,从用户的角度来看,这可能会使人很困惑。
参考这个标签切换的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import React, { Suspense } from 'react'; import Tabs from './Tabs'; import Glimmer from './Glimmer'; const Comments = React.lazy(() => import('./Comments')); const Photos = React.lazy(() => import('./Photos')); function MyComponent() { const [tab, setTab] = React.useState('photos'); function handleTabSelect(tab) { setTab(tab); }; return ( <div> <Tabs onTabSelect={handleTabSelect} /> <Suspense fallback={<Glimmer />}> {tab === 'photos' ? <Photos /> : <Comments />} </Suspense> </div> ); }
在这个示例中,如果标签从 'photos'
切换为 'comments'
,但 Comments
会暂停,用户会看到屏幕闪烁。这符合常理,因为用户不想看到 'photos'
,而 Comments
组件还没有准备好渲染其内容,而 React 为了保证用户体验的一致性,只能显示上面的 Glimmer
,别无选择。
然而,有时这种用户体验并不可取。特别是在准备新 UI 时,展示 “旧” 的 UI 会体验更好。你可以尝试使用新的 startTransition
API 来让 React 实现这一点:
1 2 3 4 5
function handleTabSelect(tab) { startTransition(() => { setTab(tab); }); }
此处代码会告知 React,将标签切换为 'comments'
不会标记为紧急更新,而是标记为需要一些准备时间的 transition。然后 React 会保留旧的 UI 并进行交互,当它准备好时,会切换为 <Comments />
,具体请参阅 Transitions 以了解更多相关信息。
Context
1
const MyContext = React.createContext(defaultValue);
创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider
中读取到当前的 context 值。
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue
参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined
传递给 Provider 的 value 时,消费组件的 defaultValue
不会生效。
1
<MyContext.Provider value={/* 某个值 */}>
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。
Provider 接收一个 value
属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value
值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext)的传播不受制于 shouldComponentUpdate
函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。
通过新旧值检测来确定变化,使用了与 Object.is
相同的算法。
注意
当传递对象给
value
时,检测变化的方式会导致一些问题:详见注意事项。
HOC
当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。
1 2 3 4 5 6 7
// 定义静态函数 WrappedComponent.staticMethod = function() {/*...*/} // 现在使用 HOC const EnhancedComponent = enhance(WrappedComponent); // 增强组件没有 staticMethod typeof EnhancedComponent.staticMethod === 'undefined' // true
为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:
1 2 3 4 5 6
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} // 必须准确知道应该拷贝哪些方法 :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance; }
但要这样做,你需要知道哪些方法应该被拷贝。你可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法:
1 2 3 4 5 6
import hoistNonReactStatic from 'hoist-non-react-statics'; function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} hoistNonReactStatic(Enhance, WrappedComponent); return Enhance; }
除了导出组件,另一个可行的方案是再额外导出这个静态方法。
1 2 3 4 5 6 7 8 9
// 使用这种方式代替... MyComponent.someFunction = someFunction; export default MyComponent; // ...单独导出该方法... export { someFunction }; // ...并在要使用的组件中,import 它们 import MyComponent, { someFunction } from './MyComponent.js';