React文档漫步-高级指引
代码分割
import()
// 使用前
import { add } from './math';
console.log(add(16, 26));
// 使用后
import("./math").then(math => {
console.log(math.add(16, 26));
});
React.lazy
+ Suspense
// 使用前
import OtherComponent from './OtherComponent';
// 使用后
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
基于路由的代码分割
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);
命名导出
React.lazy
目前只支持默认导出(default exports)。可以使用中间模块来重新导出默认模块。
// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));
Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
-
如果只是避免层层传递某些属性,组件组合可能会是比context更好的解决方案
-
// 原方案 <Page user={user} avatarSize={avatarSize} /> // ... 渲染出 ... <PageLayout user={user} avatarSize={avatarSize} /> // ... 渲染出 ... <NavigationBar user={user} avatarSize={avatarSize} /> // ... 渲染出 ... <Link href={user.permalink}> <Avatar user={user} size={avatarSize} /> </Link> // 可以用 function Page(props) { const user = props.user; const userLink = ( <Link href={user.permalink}> <Avatar user={user} size={props.avatarSize} /> </Link> ); return <PageLayout userLink={userLink} />; } // 现在,我们有这样的组件: <Page user={user} avatarSize={avatarSize} /> // ... 渲染出 ... <PageLayout userLink={...} /> // ... 渲染出 ... <NavigationBar userLink={...} /> // ... 渲染出 ... {props.userLink}
-
-
context值最好采用引用值
-
// 不推荐,value 属性总是被赋值为新的对象 class App extends React.Component { render() { return ( <MyContext.Provider value={{something: 'something'}}> <Toolbar /> </MyContext.Provider> ); } } // 推荐 class App extends React.Component { constructor(props) { super(props); this.state = { value: {something: 'something'}, }; } render() { return ( <MyContext.Provider value={this.state.value}> <Toolbar /> </MyContext.Provider> ); } }
-
-
可以使用
Context.displayName
在DevTools 中显示自定义名称
Refs and the DOM
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
说人话,何时使用Refs?
- 管理焦点,文本选择或媒体播放。
- 触发强制动画。
- 集成第三方 DOM 库。
ref的值根据节点的类型有所不同
- 当
ref
属性用于 HTML 元素时,构造函数中使用React.createRef()
创建的ref
接收底层 DOM 元素作为其current
属性。 - 当
ref
属性用于自定义 class 组件时,ref
对象接收组件的挂载实例作为其current
属性。 - 你不能在函数组件上使用
ref
属性,因为他们没有实例。
Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref
Refs 转发
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
- 我们通过调用
React.createRef
创建了一个 React ref 并将其赋值给ref
变量。 - 我们通过指定
ref
为 JSX 属性,将其向下传递给<FancyButton ref={ref}>
。 - React 传递
ref
给forwardRef
内函数(props, ref) => ...
,作为其第二个参数。 - 我们向下转发该
ref
参数到<button ref={ref}>
,将其指定为 JSX 属性。 - 当 ref 挂载完成,
ref.current
将指向<button>
DOM 节点。
可以使用forwardRef.displayName
在DevTools 中显示自定义名称
Fragments
短语法:<> </>
长语法:React.Fragment
。只有长语法可以传递key。
高阶组件
HOC不是API,是一种设计模式。
高阶组件是参数为组件,返回值为新组件的函数。组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。例如 Redux 的 connect
。
-
HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。
-
HOC 是纯函数,没有副作用。
-
HOC 应该透传与自身无关的 props。大多数 HOC 都应该包含一个类似于下面的 render 方法
-
render() { // 过滤掉非此 HOC 额外的 props,且不要进行透传 const { extraProp, ...passThroughProps } = this.props; // 将 props 注入到被包装的组件中。 // 通常为 state 的值或者实例方法。 const injectedProp = someStateOrInstanceMethod; // 将 props 传递给被包装组件 return ( <WrappedComponent injectedProp={injectedProp} {...passThroughProps} /> ); }
-
-
最常见的HOC
-
// React Redux 的 `connect` 函数 const ConnectedComment = connect(commentSelector, commentActions)(CommentList); // connect 是一个函数,它的返回值为另外一个函数。 const enhance = connect(commentListSelector, commentListActions); // 返回值为 HOC,它会返回已经连接 Redux store 的组件 const ConnectedComment = enhance(CommentList); // connect 是一个返回高阶组件的高阶函数! // 不推荐如下写法... const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)) // ... 建议编写组合工具函数 // compose(f, g, h) 等同于 (...args) => f(g(h(...args))) const enhance = compose( // 这些都是单参数的 HOC withRouter, connect(commentSelector) ) const EnhancedComponent = enhance(WrappedComponent)
-
许多第三方库都提供了
compose
工具函数,包括 lodash (比如lodash.flowRight
)
-
-
包装名称
-
function withSubscription(WrappedComponent) { class WithSubscription extends React.Component {/* ... */} WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; return WithSubscription; } function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; }
-
-
静态方法需要自行复制
-
// 定义静态函数 WrappedComponent.staticMethod = function() {/*...*/} // 现在使用 HOC const EnhancedComponent = enhance(WrappedComponent); // 增强组件没有 staticMethod typeof EnhancedComponent.staticMethod === 'undefined' // true // 使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法 import hoistNonReactStatic from 'hoist-non-react-statics'; function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} hoistNonReactStatic(Enhance, WrappedComponent); return Enhance; } // 或者额外导出这个方法 / 使用这种方式代替... MyComponent.someFunction = someFunction; export default MyComponent; // ...单独导出该方法... export { someFunction }; // ...并在要使用的组件中,import 它们 import MyComponent, { someFunction } from './MyComponent.js';
-
错误边界
错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。
class 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;
}
}
-
错误边界可以捕获
- 发生在整个子组件树的渲染期间
- 生命周期方法中的错误
- 构造函数中的错误
-
错误边界无法捕获:
- 事件处理
- 异步代码
- 服务端渲染
- 组件自身错误
-
自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。
-
如需要在事件处理器内部捕获错误,使用普通的 JavaScript
try
/catch
语句-
class MyComponent extends React.Component { constructor(props) { super(props); this.state = { error: null }; this.handleClick = this.handleClick.bind(this); } handleClick() { try { // 执行操作,如有错误则会抛出 } catch (error) { this.setState({ error }); } } render() { if (this.state.error) { return <h1>Caught an error.</h1> } return <button onClick={this.handleClick}>Click Me</button> } }
-
JSX深入
语法糖
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)
<div className="sidebar" />
React.createElement(
'div',
{className: 'sidebar'}
)
React 必须在作用域内
import React from 'react';
import CustomButton from './CustomButton';
function WarningButton() {
// return React.createElement(CustomButton, {color: 'red'}, null);
return <CustomButton color="red" />;
}
在 JSX 类型中使用点语法
import React from 'react';
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
}
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
在运行时选择类型
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 正确!JSX 类型可以是大写字母开头的变量。
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
JSX中的props
展开运算符
unction App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
JSX子元素
包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children
传递给外层组件
-
函数作为子元素
// 用法并不常见,但可以用于扩展 JSX // 调用子元素回调 numTimes 次,来重复生成组件 function Repeat(props) { let items = []; for (let i = 0; i < props.numTimes; i++) { items.push(props.children(i)); } return <div>{items}</div>; } function ListOfTenThings() { return ( <Repeat numTimes={10}> {(index) => <div key={index}>This is item {index} in the list</div>} </Repeat> ); }
-
布尔类型、Null、Undefined会忽略
// 仅当showHeader为true时,才会渲染<Header />组件 <div> {showHeader && <Header />} <Content /> </div>
性能优化
React Profiler
在开发模式下,React 开发者工具会出现分析器标签。
虚拟化长列表
shouldComponentUpdate
// React比较更新时时浅比较的
// 使用shouldComponentUpdate 仅检查了 props.color 或 state.count 是否改变。如果这些值没有改变,那么这个组件不会更新。
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
// 大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate.但是他是浅比较的
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
// 当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了
Profiler
Profiler
测量一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。
render(
<App>
<Profiler id="Panel" onRender={callback}>
<Panel {...props}>
<Profiler id="Content" onRender={callback}>
<Content {...props} />
</Profiler>
<Profiler id="PreviewPane" onRender={callback}>
<PreviewPane {...props} />
</Profiler>
</Panel>
</Profiler>
</App>
);
function onRenderCallback(
id, // 发生提交的 Profiler 树的 “id”
phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
actualDuration, // 本次更新 committed 花费的渲染时间
baseDuration, // 估计不使用 memoization 的情况下渲染整棵子树需要的时间
startTime, // 本次更新中 React 开始渲染的时间
commitTime, // 本次更新中 React committed 的时间
interactions // 属于本次更新的 interactions 的集合
) {
// 合计或记录渲染时间。。。
}
-
phase: "mount" | "update"
- 判断是组件树的第一次装载引起的重渲染,还是由 props、state 或是 hooks 改变引起的重渲染。 -
actualDuration: number
- 本次更新在渲染Profiler
和它的子代上花费的时间。 这个数值表明使用 memoization 之后能表现得多好。(例如React.memo
,useMemo
,shouldComponentUpdate
)。 理想情况下,由于子代只会因特定的 prop 改变而重渲染,因此这个值应该在第一次装载之后显著下降。 -
baseDuration: number
- 在Profiler
树中最近一次每一个组件render
的持续时间。 这个值估计了最差的渲染时间。(例如当它是第一次加载或者组件树没有使用 memoization)。 -
interactions: Set
- 当更新被制定时,“interactions” 的集合会被追踪。(例如当render
或者setState
被调用时)。Interactions 能用来识别更新是由什么引起的,尽管这个追踪更新的 API 依然是实验性质的。
Portals
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
ReactDOM.createPortal(child, container)
render() {
// React 挂载了一个新的 div,并且把子元素渲染其中
return (
<div>
{this.props.children}
</div>
);
}
render() {
// React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
// `domNode` 是一个可以在任何位置的有效 DOM 节点。
return ReactDOM.createPortal(
this.props.children,
domNode
);
}
- 一个 portal 的典型用例是当父组件有
overflow: hidden
或z-index
样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框 - Portal事件冒泡
- 虽然portal可以被放在DOM书的任何地方,但他仍是一个普通React子节点,也就是说,portal仍在React树中,context等功能特性都不变。
- 一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。
协调
有关diffing算法的一些设计
首先比较两棵树的根节点
对比不同类型的元素
-
根节点为不同类型的元素时,拆卸原有的树、建立新的树
-
卸载树时,对应的DOM节点也会被销毁
componentWillUnmount()
-
建立新树时,对应的DOM节点会被创建、插入到DOM中
-
UNSAFE_componentWillMount()
-
componentDidMount()
<div> <Counter /> </div> <span> <Counter /> </span> // React会销毁Couter组件并重新装载
-
对比同一类型的元素
-
保留DOM节点,仅比较更新改变的属性
// React知道仅修改DOM元素上的className属性 <div className="before" title="stuff" /> <div className="after" title="stuff" /> // 仅更新color <div style={{color: 'red', fontWeight: 'bold'}} /> <div style={{color: 'green', fontWeight: 'bold'}} />
处理完当前节点后,对子节点进行递归
对比同类型的组件元素
- 组件更新时,组件实例会保持不变,以保持不同的渲染时保持state一致
- 更新组件实例的props以保证更新
UNSAFE_componentWillReceiveProps()
- ``UNSAFE_componentWillUpdate()`
componentDidUpdate()
- 调用
render()
方法,diff算法利用旧结果和新结果递归
子节点递归
key
属性用于匹配新旧树上的子元素- 当基于下标的组件进行重新排序时,非受控组件的state可能会相互篡改
Render Props
指在React组件之间使用一个值为函数的prop共享代码
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>
render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{/*
使用 `render`prop 动态决定要渲染的内容,
而不是给出一个 <Mouse> 渲染结果的静态表示
*/}
{this.props.render(this.state)}
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移动鼠标!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
使用Render prop实现大多数HOC
// 如果你出于某种原因真的想要 HOC,那么你可以轻松实现
// 使用具有 render prop 的普通组件创建一个!
function withMouse(Component) {
return class extends React.Component {
render() {
return (
<Mouse render={mouse => (
<Component {...this.props} mouse={mouse} />
)}/>
);
}
}
}
React.PureComponent
在Render方法里创建函数,使用render prop会抵消使用React.PureCompont带来的优势。原因在于:浅比较总会得到false(render会生成一个新函数作为prop).
你应该:
class MouseTracker extends React.Component {
// 定义为实例方法,`this.renderTheCat`始终
// 当我们在渲染中使用它时,它指的是相同的函数
renderTheCat(mouse) {
return <Cat mouse={mouse} />;
}
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={this.renderTheCat} />
</div>
);
}
}
静态类型检查
Flow略过,简单看下ts。
Create React App忠使用TS
npx create-react-app my-app --template typescript
// To add TypeScript to an existing Create React App project,
// first install it:
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
// next
// rename any file to be TS file
// e.g. src/index.js to src/index.tsx
非受控组件
很多时候,我们使用受控组件来处理表单数据,表单数据由React管理;另一种替代方案是,表单数据由DOM节点来处理,也就是非受控组件。
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.input = React.createRef();
}
handleSubmit(event) {
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
默认值defaultValue
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
defaultValue="Bob"
type="text"
ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
文件输入
React中<input type="file" />
始终是个非受控组件
class FileInput extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.fileInput = React.createRef();
}
handleSubmit(event) {
event.preventDefault();
alert(
`Selected file - ${this.fileInput.current.files[0].name}`
);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Upload file:
<input type="file" ref={this.fileInput} />
</label>
<br />
<button type="submit">Submit</button>
</form>
);
}
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(<FileInput />);
其他未写模块
- 不使用ES6
- 不使用JSX
- 严格模式
- PropTypes
- Web Components
相关待读
本文来自博客园,作者:沧浪浊兮,转载请注明原文链接:https://www.cnblogs.com/shixiu/p/16564264.html