react高级指引
参考网站
https://react.docschina.org/docs/jsx-in-depth.html
深入 JSX
本质上来讲,JSX 只是为 React.createElement(component, props, ...children)
方法提供的语法糖。
<MyButton color="blue" shadowSize={2}> Click Me
</MyButton>
编译为:
React.createElement( MyButton, // 组件名称
{color: 'blue', shadowSize: 2}, // props
'Click Me' //children
)
指定 React 元素类型(确认组件名)
React 必须在作用域中
由于 JSX 编译成React.createElement
方法的调用,所以在你的 JSX 代码中,React
库必须也始终在作用域中。
// 尽管 React 和 CustomButton 都没有在代码中被直接调用,React库必须也始终在作用域中import React from 'react';
import CustomButton from './CustomButton';
function WarningButton() {
// return React.createElement(CustomButton, {color: 'red'}, null);
return <CustomButton color="red" />;
}
如果用<script>
标签加载React,它已经在作用域中,以React
全局变量的形式。
点表示法用于JSX类型(组件的命名空间)
你可以方便地从一个模块中导出许多 React 组件。
import React from 'react';const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
},
DatePicker2: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
DatePicker3: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
}
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
用户定义组件必须首字母大写
当元素类型以小写字母开头时,它表示一个内置的组件,如 <div>
或 <span>
,将导致字符串 'div'
或 'span'
传递给 React.createElement
。 以大写字母开头的类型,如 <Foo />
编译为 React.createElement(Foo)
,并且它正对应于你在 JavaScript 文件中定义或导入的组件。
在运行时选择类型
你不能使用一个通用的表达式来作为 React 元素的标签。如果你的确想使用一个通用的表达式来确定 React 元素的类型,请先将其赋值给大写开头的变量。这种情况一般发生于当你想基于属性值渲染不同的组件时:
import React from 'react';import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 错误!JSX 标签名不能为一个表达式。
return <components[props.storyType] story={props.story} />;
}
// 修改
function Story(props) {
// 正确!JSX 标签名可以为大写开头的变量。
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
JSX的属性(Props)
使用 JavaScript 表达式作为属性
<MyComponent foo={1 + 2 + 3 + 4} />
if
语句和 for
循环在 JavaScript 中不是表达式,因此它们不能直接在 JSX 中使用,但是你可以将它们放在周围的代码中。例如:
function NumberDescriber(props) { let description;
if (props.number % 2 == 0) {
description = <span>even</span>;
} else {
description = <i>odd</i>;
}
return <div>{props.number} is an {description} number</div>;
}
字符串常量
你可以将字符串常量作为属性值传递。下面这两个 JSX 表达式是等价的:
<MyComponent message="hello world" /><MyComponent message={'hello world'} />
当传递一个字符串常量时,该值为HTML非转义的,所以下面两个 JSX 表达式是相同的:
<MyComponent message="<3" /><MyComponent message={'<3'} />
属性默认为“True”
如果你没有给属性传值,它默认为 true
。因此下面两个 JSX 是等价的:
<MyTextBox autocomplete /><MyTextBox autocomplete={true} />
一般情况下,我们不建议这样使用,因为它会与 ES6 对象简洁表示法 混淆。比如 {foo}
是 {foo: foo}
的简写,而不是 {foo: true}
。这里能这样用,是因为它符合 HTML 的做法。
展开属性
如果你已经有了个 props
对象,并且想在 JSX 中传递它,你可以使用 ...
作为“展开(spread)”操作符来传递整个属性对象。下面两个组件是等效的:
function App1() { return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
JSX中的子代
在既包含开始标签又包含结束标签的 JSX 表达式中,这两个标签之间的内容被传递为专门的属性:props.children
。
字符串字面量
<MyComponent>Hello world!</MyComponent>// props.children 就是那个字符串
JSX 会移除空行和开始与结尾处的空格。标签邻近的新行也会被移除,字符串常量内部的换行会被压缩成一个空格,所以下面这些都等价:(要换行用标签包裹)
<div>Hello World</div><div>
Hello World
</div>
<div>
Hello
World
</div>
<div>
Hello World
</div>
JSX子代
你可以提供更多个 JSX 元素作为子代,这对于嵌套显示组件非常有用:
<MyContainer> <MyFirstComponent />
<MySecondComponent />
</MyContainer>
React 组件也可以返回包含多个元素的一个数组:
render() { // 不需要使用额外的元素包裹数组中的元素!
return [
// 不要忘记 key :)
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>,
];
}
JavaScript 表达式作为子代
这对于渲染任意长度的 JSX 表达式的列表很有用。例如,下面将会渲染一个 HTML 列表:
function Item(props) { return <li>{props.message}</li>;
}
function TodoList() {
const todos = ['finish doc', 'submit pr', 'nag dan to review'];
return (
<ul>
{todos.map((message) => <Item key={message} message={message} />)}
</ul>
);
}
(回调)函数作为子代
通常情况下,插入 JSX 中的 JavaScript 表达式将被认作字符串、React 元素或这些的一个列表。然而,props.children
可以像其它属性一样传递任何种类的数据,而不仅仅是 React 知道如何去渲染的数据种类。例如,如果你有一个自定义组件,你能使其取一个回调作为props.children
:
// Calls the children callback numTimes to produce a repeated componentfunction Repeat(props) {
let items = [];
for (let i = 0; i < props.numTimes; i++) {
// 取一个回调作为props.children
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 被忽略
false
、null
、undefined
和 true
都是有效的子代,只是它们不会被渲染。下面的JSX表达式将渲染为相同的东西:
<div /><div></div>
<div>{false}</div>
<div>{null}</div>
<div>{undefined}</div>
<div>{true}</div>
这在根据条件来确定是否渲染React元素时非常有用。以下的JSX只会在showHeader
为true
时渲染<Header />
组件。
<div> {showHeader && <Header />}
<Content />
</div>
请确保 &&
前面的表达式始终为布尔值,否则将会被打印
<div> // 因为当 props.message 为空数组时,它会打印0
{props.messages.length &&
<MessageList messages={props.messages} />
}
</div>
// 正确写法
<div>
{props.messages.length > 0 &&
<MessageList messages={props.messages} />
}
</div>
相反,如果你想让类似 false
、true
、null
或 undefined
出现在输出中,你必须先把它转换成字符串 :
<div> My JavaScript variable is {String(myVariable)}.
</div>
使用 PropTypes 进行类型检查
注意: React.PropTypes
自 React v15.5 起已弃用。请使用 prop-types
库代替。
对于某些应用来说,你还可以使用 Flow 或 TypeScript 这样的 JavsScript 扩展来对整个应用程序进行类型检查。然而即使你不用它们,React 也有一些内置的类型检查功能。要检查组件的属性,你需要配置特殊的 propTypes
属性:
import PropTypes from 'prop-types';class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
Greeting.propTypes = {
name: PropTypes.string
};
PropTypes
包含一整套验证器,可用于确保你接收的数据是有效的。在这个示例中,我们使用了 PropTypes.string
。当你给属性传递了无效值时,JavsScript 控制台将会打印警告。出于性能原因,propTypes
只在开发模式下进行检查。
各类PropTypes
import PropTypes from 'prop-types';MyComponent.propTypes = {
// 你可以将属性声明为以下 JS 原生类型
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,
// 任何可被渲染的元素(包括数字、字符串、子元素或数组)。
optionalNode: PropTypes.node,
// 一个 React 元素
optionalElement: PropTypes.element,
// 你也可以声明属性为某个类的实例,这里使用 JS 的
// instanceof 操作符实现。
optionalMessage: PropTypes.instanceOf(Message),
// 你也可以限制你的属性值是某个特定值之一
optionalEnum: PropTypes.oneOf(['News', 'Photos']),
// 限制它为列举类型之一的对象
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
]),
// 一个指定元素类型的数组
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
// 一个指定类型的对象
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
// 一个指定属性及其类型的对象
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),
// 你也可以在任何 PropTypes 属性后面加上 `isRequired`
// 后缀,这样如果这个属性父组件没有提供时,会打印警告信息
requiredFunc: PropTypes.func.isRequired,
// 任意类型的数据
requiredAny: PropTypes.any.isRequired,
// 你也可以指定一个自定义验证器。它应该在验证失败时返回
// 一个 Error 对象而不是 `console.warn` 或抛出异常。
// 不过在 `oneOfType` 中它不起作用。
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
},
// 不过你可以提供一个自定义的 `arrayOf` 或 `objectOf`
// 验证器,它应该在验证失败时返回一个 Error 对象。 它被用
// 于验证数组或对象的每个值。验证器前两个参数的第一个是数组
// 或对象本身,第二个是它们对应的键。
customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
if (!/matchme/.test(propValue[key])) {
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
})
};
限制单个子代
使用 PropTypes.element
你可以指定只传递一个子代
import PropTypes from 'prop-types';class MyComponent extends React.Component {
render() {
// This must be exactly one element or it will warn.
const children = this.props.children;
return (
<div>
{children}
</div>
);
}
}
MyComponent.propTypes = {
// 必填
children: PropTypes.element.isRequired
};
属性默认值
你可以通过配置 defaultProps
为 props
定义默认值:
class Greeting extends React.Component { render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
// 为属性指定默认值:
Greeting.defaultProps = {
name: 'Stranger'
};
// 渲染 "Hello, Stranger":
ReactDOM.render(
<Greeting />,
document.getElementById('example')
);
如果你在使用像 transform-class-properties 的 Babel 转换器,你也可以在React 组件类中声明 defaultProps
作为静态属性。这个语法还没有最终通过,在浏览器中需要一步编译工作。更多信息,查看类字段提议。
class Greeting extends React.Component { static defaultProps = {
name: 'stranger'
}
render() {
return (
<div>Hello, {this.props.name}</div>
)
}
}
静态类型检查
像 Flow 和 TypeScript 这样的静态类型检查器可以在运行代码之前识别某些类型的问题。 他们还可以通过添加自动完成功能来改善开发人员的工作流程。
Flow
Flow 是一个针对 JavaScript 代码的静态类型检查器。它是在Facebook开发的,经常和React一起使用。 它可以让你使用特殊的类型语法来注释变量,函数和React组件,并尽早地发现错误。 您可以阅读 Flow 介绍 来了解基本知识。
在一个项目中添加 Flow
如果你使用 npm, 运行:
npm install --save-dev flow-bin// 第二个命令创建一个您需要提交的 Flow 配置文件。
npm run flow init
最后,将 flow
添加到你的 package.json
中的 "scripts"
部分:
{ // ...
"scripts": {
"flow": "flow",
// ...
},
// ...
}
从编译过的代码中剥离 Flow 语法
Flow 通过使用特殊的语法为类型注释扩展了 JavaScript 语言。 然而,浏览器并不知道这个语法,所以我们需要确保它不会在发送到浏览器的已编译的 JavaScript 包中结束。
确切的做法取决于你用来编译 JavaScript 的工具。
Create React App
如果你的项目是使用 Create React App 建立的,恭喜! Flow 此时已经被默认剥离,所以在这一步你不需要做任何事情。
其他的工具
运行 Flow
如果你遵循了上述的说明,你应该能够在第一次就运行 Flow。
npm run flow
你应该会看到一条这样的消息:
No errors!✨ Done in 0.17s.
添加 Flow 类型注释
默认情况下, Flow 仅检查包含此批注的文件:
通常它被放置在文件的顶部。
// @flow
也有一个选择可以强制 Flow 不考虑注释检查所有文件。对于现有的项目它可能太繁琐了,但对于一个新项目如果你想完全用 Flow 来组织,那会是合理的。
现在你们都准备好了! 我们建议查看以下资源以了解有关 Flow 的更多信息:
- Flow 文档:类型注释
- Flow 文档:编辑器
- Flow 文档: React
- Linting in Flow
TypeScript
TypeScript 是一门由微软开发的编程语言。 它是 JavaScript 的一个类型超集,包含它自己的编译器。 作为一种类型化语言,Typescript 可以早在您的应用程序上线之前在构建时发现错误。 你可以在这里了解更多关于在 React 中使用 TypeScript 的知识。
在一个项目中添加 TypeScript
npm install --save-dev typescript
安装 TypeScript 让我们可以访问 tsc
命令。 在配置之前,让我们将 tsc
添加到 package.json
中的 “scripts” 部分:
{ // ...
"scripts": {
"build": "tsc",
// ...
},
// ...
}
配置 TypeScript 编译器
除非我们告诉编译器要做什么,否则它对我们将毫无用处。在 TypeScript 中,这些规则定义在一个叫 tsconfig.json
的特殊文件中。运行如下命令生成该文件:
tsc --init
看看现在生成的 tsconfig.json
,你可以看到有很多选项可以用来配置编译器。 有关所有选项的详细说明,请点击这里。
在许多选项中,我们会看到 rootDir
和 outDir
。编译器将以真实的情况接收 typescript 文件然后生成 javascript 文件。然而我们不想混淆源文件和编译后的输出。
我们将通过两个步骤解决这个问题:
- 首先,让我们像这样安排我们的项目结构。我们将所有的源代码放在 src 目录中。
├── package.json├── src
│ └── index.ts
└── tsconfig.json
- 接下来,我们会告诉编译器源代码在哪以及编译后输出该放哪。
// tsconfig.json{
"compilerOptions": {
// ...
"rootDir": "src",
"outDir": "build"
// ...
},
}
非常棒!现在当我们运行构建脚本时编译器将会将生成的 javascript 代码输出到 build
文件夹。TypeScript React Starter 提供了一个带有一套配置的 tsconfig.json
文件让你上手。
通常,您不希望将生成的JavaScript保留在源代码管理中,因此请确保将生成文件夹添加到 .gitignore
中。
文件扩展名
在 React 中,你最有可能在 .js
文件中编写你的组件。在 TypeScript 中我们有两个文件扩展名:
.ts
是默认的文件扩展名, .tsx
是一个为包含 JSX
代码使用的特殊扩展名。
运行 TypeScript
npm run build
如果你没有看到输出,这意味着它完全编译成功了。
类型定义
todo...
和 Create React App 一起使用 TypeScript
react-scripts-ts 自动配置了一个 create-react-app
项目支持 TypeScript。你可以像这样使用:
create-react-app my-app --scripts-version=react-scripts-ts
请注意它是一个第三方项目,而且不是 Create React App 的一部分。
你也可以尝试 typescript-react-starter。
你已经准备好写代码了!我们建议查看以下资源来了解有关 TypeScript 的更多信息:
- TypeScript 文档:基本类型
- TypeScript 文档:从 Javascript 迁徙
- TypeScript 文档: React 和 Webpack
Reason
todo...
Kotlin
todo...
Refs & DOM
Refs 提供了一种方式,用于访问在 render 方法中创建的 DOM 节点或 React 元素。(vue dom的ref属性 )
在典型的 React 数据流中, 属性(props)是父组件与子组件交互的唯一方式。要修改子组件,你需要使用新的 props 重新渲染它。但是,某些情况下你需要在典型数据流外强制修改子组件。要修改的子组件可以是 React 组件的实例,也可以是 DOM 元素。对于这两种情况,React 提供了解决办法。
何时使用 Refs
下面是几个适合使用 refs 的情况:
- 处理焦点、文本选择或媒体控制。
- 触发强制动画。
- 集成第三方 DOM 库
个人认为就是在单个组件内,直接获取子组件或元素的dom节点信息(包含属性,方法)
如果可以通过声明式实现,则尽量避免使用 refs。
例如,不要在 Dialog
组件上直接暴露 open()
和 close()
方法,最好传递 isOpen
属性。
不要过度使用 Refs
你可能首先会想到在你的应用程序中使用 refs 来更新组件。如果是这种情况,请花一点时间,重新思考一下 state 属性在组件层中位置。通常你会想明白,提升 state 所在的组件层级会是更合适的做法。
下面的例子已经用 React v16.3 引入的 React.createRef()
API 更新。如果你正在使用 React 更早的发布版,我们推荐使用回调形式的 refs。
创建 Refs
使用 React.createRef()
创建 refs,通过 ref
属性来获得 React 元素。当构造组件时,refs 通常被赋值给实例的一个属性,这样你可以在组件中任意一处使用它们.
class MyComponent extends React.Component { constructor(props) {
super(props);
// React.createRef创建ref
this.myRef = React.createRef();
}
render() {
// refs 通常被赋值给实例的一个属性
return <div ref={this.myRef} />;
}
}
访问 Refs
当一个 ref 属性被传递给一个 render
函数中的元素时,可以使用 ref 中的 current
属性对节点的引用进行访问。
// 对节点的引用进行访问const node = this.myRef.current;
ref的值取决于节点的类型:
- 当
ref
属性被用于一个普通的 HTML 元素时,React.createRef()
将接收底层 DOM 元素作为它的current
属性以创建ref
。 - 当
ref
属性被用于一个自定义类组件时,ref
对象将接收该组件已挂载的实例作为它的current
。 你不能在函数式组件上使用
ref
属性,因为它们没有实例。
以下代码使用 ref
存储对 DOM 节点的引用:
class CustomTextInput extends React.Component { constructor(props) {
super(props);
// 创建 ref 存储 textInput DOM 元素
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// 直接使用原生 API 使 text 输入框获得焦点
// 注意:通过 "current" 取得 DOM 节点
this.textInput.current.focus();
}
render() {
// 告诉 React 我们想把 <input> ref 关联到构造器里创建的 `textInput` 上
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
React 会在组件加载时将 DOM 元素传入 current
属性,在卸载时则会改回 null
。ref
的更新会发生在componentDidMount
或 componentDidUpdate
生命周期钩子之前。
如果我们想要包装上面的 CustomTextInput
,来模拟挂载之后立即被点击的话,我们可以使用 ref 来访问自定义输入,并手动调用它的 focusTextInput
方法:
class AutoFocusTextInput extends React.Component { constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
this.textInput.current.focusTextInput();
}
render() {
return (
<CustomTextInput ref={this.textInput} />
);
}
}
你不能在函数式组件上使用 ref
属性,因为它们没有实例:(不能在函数式元素上绑定ref)
function MyFunctionalComponent() { return <input />;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
// 这将 *不会* 工作!
return (
<MyFunctionalComponent ref={this.textInput} />
);
}
}
但是,你可以在函数式组件内部使用 ref
,只要它指向一个 DOM 元素或者 class 组件:(能在函数式内部绑定ref)
function CustomTextInput(props) { // 这里必须声明 textInput,这样 ref 回调才可以引用它
let textInput = null;
function handleClick() {
textInput.focus();
}
return (
<div>
<input
type="text"
ref={(input) => { textInput = input; }} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
对父组件暴露 DOM 节点
虽然你可以向子组件添加 ref,但这不是一个理想的解决方案,因为你只能获取组件实例而不是 DOM 节点。并且,它还在函数式组件上无效。
如果你使用 React 16.3 或更高, 这种情况下我们推荐使用 ref 转发。 Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。关于怎样对父组件暴露子组件的 DOM 节点,在 ref 转发文档 中有一个详细的例子。
如果你使用 React 16.2 或更低,或者你需要比 ref 转发更高的灵活性,你可以使用 这个替代方案 将 ref 作为特殊名字的 prop 直接传递。
回调 Refs
React 也支持另一种设置 ref 的方式,称为“回调 ref”,更加细致地控制何时 ref 被设置和解除。
你会传递一个函数。这个函数接受 React 组件的实例或 HTML DOM 元素作为参数,以存储它们并使它们能被其他地方访问。
class CustomTextInput extends React.Component { constructor(props) {
super(props);
this.textInput = null;
// 使用 ref 回调函数,在实例的属性中存储对 DOM 节点的引用。
// 接受 React 组件的实例或 HTML DOM 元素作为参数,以存储它们并使它们能被其他地方访问
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// 直接使用原生 API 使 text 输入框获得焦点
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// 渲染后文本框自动获得焦点
this.focusTextInput();
}
render() {
// 使用 `ref` 的回调将 text 输入框的 DOM 节点存储到 React
// 实例上(比如 this.textInput)
return (
<div>
<input
type="text"
ref={this.setTextInputRef}
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
React 将在组件挂载时将 DOM 元素传入ref
回调函数并调用,当卸载时传入 null
并调用它。ref
回调函数会在 componentDidMount
和 componentDidUpdate
生命周期函数前被调用(跟访问ref一致)
你可以在组件间传递回调形式的 refs,就像你可以传递通过 React.createRef()
创建的对象 refs 一样。
function CustomTextInput(props) { return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
);
}
}
在上面的例子中,Parent
传递给它的 ref 回调函数作为 inputRef
传递给 CustomTextInput
,然后 CustomTextInput
通过 ref
属性将其传递给 <input>
。最终,Parent
中的 this.inputElement
将被设置为与 CustomTextIput
中的 <input>
元素相对应的 DOM 节点(即parent的this.inputElement等于CustomTextInput的input DOM节点)
非受控组件
在大多数情况下,我们推荐使用 受控组件 来实现表单。 在受控组件中,表单数据由 React 组件处理。如果让表单数据由 DOM 处理时,替代方案为使用非受控组件。
要编写一个非受控组件,而非为每个状态更新编写事件处理程序,你可以 使用 ref 从 DOM 获取表单值。
class NameForm extends React.Component { constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
alert('A name was submitted: ' + this.input.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
{/* 通过ref完成一个非受控组件 */}
<input type="text" ref={(input) => this.input = input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
由于非受控组件将真实数据保存在 DOM 中,因此在使用非受控组件时,更容易同时集成 React 和非 React 代码。如果你想快速而随性,这样做可以减小代码量。否则,你应该使用受控组件。
如果依然不清楚在哪种特定情况下选择哪种类型的组件,那么你应该阅读 这篇关于受控和非受控的表单输入 了解更多。
默认值
在 React 的生命周期中,表单元素上的 value
属性将会覆盖 DOM 中的值。使用非受控组件时,通常你希望 React 可以为其指定初始值,但不再控制后续更新。你可以指定一个 defaultValue
属性而不是 value
。
render() { return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
{/* 使用defaultValue设置默认值 */}
<input
defaultValue="Bob"
type="text"
ref={(input) => this.input = input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
同样,<input type="checkbox">
和 <input type="radio">
支持 defaultChecked
,<select>
和 <textarea>
支持 defaultValue
.
文件输入标签
在HTML中,<input type="file">
可以让用户从其设备存储中选择一个或多个文件上传到服务器,或通过File API进行操作。
<input type="file" />
在React中,<input type="file" />
始终是一个不受控制的组件,因为它的值只能由用户设置,而不是以编程方式设置。
以下示例显示如何创建ref节点以访问提交处理程序中的文件:
class FileInput extends React.Component { constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
event.preventDefault();
alert(
/* 通过ref得到的对象获取相应的值 */
`Selected file - ${this.fileInput.files[0].name}`
);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Upload file:
{/* 通过一个ref回调函数来获取input的值 */}
<input
type="file"
ref={input => {
this.fileInput = input;
}}
/>
</label>
<br />
<button type="submit">Submit</button>
</form>
);
}
}
ReactDOM.render(
<FileInput />,
document.getElementById('root')
);
性能优化
更新UI时,React在内部使用几种巧妙的技术来最小化DOM操作的数量。
使用生产版本
在React应用中检测性能问题时,请务必使用压缩过的生产版本。
默认情况下,React包含很多在开发过程中很有帮助的警告。然而,这会导致React更大更慢。因此,在部署应用时,请确认使用了生产版本。
如果你不确定构建过程是否正确,可以安装React开发者工具(chrome)。当你访问一个生产模式的React页面时,这个工具的图标会有一个黑色的背景:
当你访问一个开发模式的React页面时,这个工具的图标会有一个红色的背景:
最好在开发应用时使用开发模式,部署应用时换为生产模式。
以下是构建生产应用的流程。
Create React App方式
如果你的项目是以Create React App创建的,运行如下代码:
npm run build
这将会在该项目的build/
文件夹内创建一个生产版本的应用。
注意只有发布项目时才有必要这样做,正常开发时,使用npm start
。
其他创建方式使用生产版本方法
使用 Chrome Performance 归档组件
在开发模式下, 在支持的浏览器内使用性能工具可以直观的了解组件何时挂载,更新和卸载。例如:
Chrome浏览器内:
在项目地址栏内添加查询字符串
?react_perf
(例如,http://localhost:3000/?react_perf
)。打开Chrome开发工具Performance 标签页点击Record.
执行你想要分析的动作。不要记录超过20s,不然Chrome可能会挂起。
停止记录。
React事件将会被归类在 User Timing标签下。
更多的详细操作,请参考 BenSchwarz 的这篇文章。
注意由于这些数字是相对的,因此组件在生产版本中会运行更快。然而,这也能够帮助你了解何时会有无关的组件被错误的更新,以及你的组件更新的深度和频率。
目前浏览器中仅有Chrome,Edge和IE支持此特性,但是我们使用此标准用户Timing API,因此我们期待更多的浏览器对其添加支持。
虚拟化长列表
todo...
避免协调
todo...
shouldComponentUpdate(nextProps, nextState) { return true;
}
如果你知道在某些情况下你的组件不需要更新,你可以在shouldComponentUpdate
内返回false
来跳过整个渲染进程,该进程包括了对该组件和之后的内容调用render()
指令。
shouldComponentUpdate实战
这是一个组件的子树。对其中每个组件来说,SCU
表明了shouldComponentUpdate
的返回内容,vDOMEq
表明了待渲染的React元素与原始元素是否相等,最后,圆圈的颜色表明这个组件是否需要重新渲染。
由于以C2为根的子树的shouldComponentUpdate
返回了false
,React不会试图渲染C2,甚至不会在C4和C5上调用shouldComponentUpdate
。
对C1和C3来说,shouldComponentUpdate
返回了true
,因此React会深入到分支中并检查它们。C6的shouldComponentUpdate
返回了true
,由于待渲染的元素与原始元素并不相等,React会更新这个DOM节点。
最后一个有趣的情况是C8,React需要渲染这个组件,但是由于组件元素返回值与原元素相等,因此它并没有更新这个DOM节点。
注意React只需更新C6,因为它是不可避免的。对C8来说,它通过比较待渲染元素与原始元素避免了渲染,对C2的子树和C7,它们甚至都没有执行比较,因为我们设置了shouldComponentUpdate
为false
,render
没有被调用。
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>
);
}
}
不会突变的数据的力量
避免此类问题最简单的方式是避免使用值可能会突变的属性或状态。例如,上面例子中的handleClick
应该用concat(数组拼接)
重写成:
handleClick() { this.setState(prevState => ({
words: prevState.words.concat(['marklar'])
}));
}
ES6支持数组的展开语法可以让它变得更容易。如果你使用的是Create React App
,那么此语法默认可用。
handleClick() { this.setState(prevState => ({
words: [...prevState.words, 'marklar'],
}));
};
想要实现代码而不突变原始对象,我们可以使用Object.assign方法:
function updateColorMap(colormap) { return Object.assign({}, colormap, {right: 'blue'});
}
有一个JavaScript提议来添加对象展开属性以使其更容易地更新对象并且不会突变对象:
function updateColorMap(colormap) { return {...colormap, right: 'blue'};
}
使用不可突变的数据结构
todo...
使用 ES6
todo...
声明默认属性
如果使用 class
关键字创建组件,可以直接把自定义属性对象写到类的 defaultProps
属性中:
class Greeting extends React.Component { // ...
}
Greeting.defaultProps = {
name: 'Mary'
};
自动绑定
对于使用 class
关键字创建的 React 组件,组件中的方法是不会自动绑定 this
的。类似地,通过 ES6 class
生成的实例,实例上的方法也不会绑定 this
。因此,你需要在 constructor
中为方法手动添加 .bind(this)
:
class SayHello extends React.Component { constructor(props) {
super(props);
this.state = {message: 'Hello!'};
// 这一行很关键
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
alert(this.state.message);
}
render() {
// 由于 `this.handleClick` 已经绑定至实例,因此我们才可以用它来处理点击事件
return (
<button onClick={this.handleClick}>
Say hello
</button>
);
}
}
Mixin(混入)
ES6 本身是不包含混入支持的。因此,如果你使用 class
关键字创建组件,那就不能使用混入功能了。
我们也发现了很多使用混入然后出现了问题的代码库。因此,我们并不推荐在 ES6 中使用混入.
以下内容仅作为参考
如果完全不同的组件有相似的功能,这就会产生 “横切关注点”问题。针对这个问题,在使用 createReactClass
创建 React 组件的时候,引入混入
功能会是一个很好的解决方案。
一个常见的使用情景是,当一个组件想要每隔一段时间更新,那么最简单的方法就是使用 setInterval()
。但更重要的是,如果后续代码中不需要这个功能,为了节省内存,你应该把它删除。React 提供了 生命周期方法,这样你就可以知道某一个组件什么时候要被创建或者什么时候会被销毁。我们先来创建一个使用 setInterval()
的混入,它会在组件销毁的时候也销毁。
var SetIntervalMixin = { componentWillMount: function() {
this.intervals = [];
},
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount: function() {
this.intervals.forEach(clearInterval);
}
};
var createReactClass = require('create-react-class');
var TickTock = createReactClass({
mixins: [SetIntervalMixin], // 使用混入
getInitialState: function() {
return {seconds: 0};
},
componentDidMount: function() {
this.setInterval(this.tick, 1000); // 调用混入的方法
},
tick: function() {
this.setState({seconds: this.state.seconds + 1});
},
render: function() {
return (
<p>
React has been running for {this.state.seconds} seconds.
</p>
);
}
});
ReactDOM.render(
<TickTock />,
document.getElementById('example')
);
如果一个组件有多个混入,且其中几个混入中定义了相同的生命周期方法(比如都会在组件被摧毁的时候执行),那么这些生命周期方法是一定会被调用的。通过混入定义的方法,执行顺序也与定义时的顺序一致,且会在组件上的方法执行之后再执行。
不使用 JSX
编写React的时候,JSX并不是必须的。当你不想在你的构建环境中安装相关编译工具的时候,不使用JSX编写React会比较方便。
每一个JSX元素都只是 React.createElement(component, props, ...children)
的语法糖。
class Hello extends React.Component { render() {
return <div>Hello {this.props.toWhat}</div>;
}
}
ReactDOM.render(
<Hello toWhat="World" />,
document.getElementById('root')
);
可以被编译成下面这段不使用JSX的代码:
class Hello extends React.Component { render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}
ReactDOM.render(
React.createElement(Hello, {toWhat: 'World'}, null),
document.getElementById('root')
);
协调(Reconciliation,计算最小树)
React提供了一组声明式API以让你不必担心每次更新精确地改变了什么。这使得应用的编写容易了很多,但这是在React中如何实现并不是很明显。这篇文章解释了在React中的“差分(diffing)”算法中我们所做出的选择,以让组件更新是可预测的,并且足够快以适应高性能应用。
译者注:diffing算法用来找出两棵树的所有不同点,类似于游戏“找别扭”。
目的
当你使用React,在某一个时间点,你可以认为render()
函数是在创建React元素树。在下一状态或属性更新时,render()
函数将返回一个不同的React元素树。React需要算出如何高效更新UI以匹配最新的树。
若我们在React中使用,展示1000个元素则需要进行10亿次的比较。这太过昂贵。与此不同,React基于两点假设,实现了一个启发的O(n)算法:
- 两个不同类型的元素将产生不同的树。
- 开发者可以使用
key
属性来提示哪些子元素贯穿不同渲染是稳定的。
实践中,上述这些假设适用于大部分应用场景。
差分算法
当差分两棵树时,React首先比较两个根元素。依赖于根元素的类型不同,其行为也不同。
不同类型的元素
每当根元素有不同类型,React将拆除旧树并且从零开始重新构建新树。从<a>
到<img>
或从<Article>
到<Comment>
,或从<Button>
到 <div>
————这些都会导致充分地重新构建。
当拆除一棵树时,旧的DOM节点被销毁。组件实例收到componentWillUnmount()
。当构建一棵新树时,新的DOM节点被插入到DOM中。组件实例先收到componentWillMount()
,然后收到componentDidMount()
。任何与旧树有关的状态都被丢弃。
这个根下任何组件也都将被卸载,他们的状态被销毁。例如,当定义:
<div> <Counter />
</div>
<!-- 根标签修改,这将销毁旧的Counter并重装载一个新的。 -->
<span>
<Counter />
</span>
相同类型的DOM元素
当比较两个相同类型的React DOM元素时,React则会观察二者的属性(attributes),保持相同的底层DOM节点,并仅更新变化的属性。例如:
<div className="before" title="stuff" /><!-- 通过比较这两个元素,React知道仅更改底层DOM元素的className -->
<div className="after" title="stuff" />
当更新style
时,React同样知道仅更新改变的属性(properties)。例如:
<div style={{color: 'red', fontWeight: 'bold'}} /><div style={{color: 'green', fontWeight: 'bold'}} />
相同类型的组件元素
当组件更新时,实例保持相同,这样状态跨渲染被维护。React通过更新底层组件实例的属性(props)来匹配新元素,并在底层实例上调用componentWillReceiveProps()
和 componentWillUpdate()
。
下一步,render()
方法被调用,差分算法递归处理前一次的结果和新的结果。
子代们上的递归
默认时,当递归DOM节点的子节点时,React就是迭代在同一时间点的两个子节点列表,并在不同时产生一个变更。
例如,当在子节点末尾增加一个元素,两棵树的转换效果很好:
<ul> <li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React将会匹配两棵树的<li>first</li>
,并匹配两棵树的<li>second</li>
节点,并插入<li>third</li>
节点树。
Keys
当子节点有key时,React使用key来匹配原始树的子节点和随后树的子节点。例如,增加一个key
到上面低效的示例,能让树的转换变得高效:
<ul> <li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
现在React知道带有'2014'
的key的元素是新的,带有'2015'
和'2016'
的key的元素仅需要移动。
key必须是唯一的,只在其兄弟中(即同一个map循环里的对象),不用全局唯一。
最好不要用数组中的索引(index)作为key,因为增加还是减少都回改变索引
权衡
重点要记住协调算法是一个实现细节。React可以在每次操作时重新渲染整个应用;最终结果仍是相同的。清晰起见,在此上下文中的重新渲染意味着对于所有组件调用render
。不意味着React将卸载并重新装载他们。将只是应用不同的部分,按照前几节的规则得出的不同。
Context
Context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。
在一个典型的 React 应用中,数据是通过 props 属性由上向下(由父及子)的进行传递的,但这对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI主题),这是应用程序中许多组件都所需要的。 Context 提供了一种在组件之间共享此类值的方式,而不必通过组件树的每个层级显式地传递 props 。
何时使用 Context
Context 设计目的是为共享那些被认为对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。例如,在下面的代码中,我们通过一个“theme”属性手动调整一个按钮组件的样式:
使用 context, 我可以避免通过中间元素传递 props:
// 创建一个 theme Context, 默认 theme 的值为 lightconst ThemeContext = React.createContext('light');
function ThemedButton(props) {
// ThemedButton 组件从 context 接收 theme
return (
// ThemeContext.Consumer
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
}
// 中间组件
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
class App extends React.Component {
render() {
// ThemeContext.Provider
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
不要仅仅为了避免在几个层级下的组件传递 props 而使用 context,它是被用于在多个层级的多个组件需要访问相同数据的情景。
API
React.createContext
const {Provider, Consumer} = React.createContext(defaultValue);
创建一对 { Provider, Consumer }
。当 React 渲染 context 组件 Consumer(可以理解为使用该context值得子组件) 时,它将从组件树的上层中最接近的匹配的 Provider 读取当前的 context 值。
如果上层的组件树没有一个匹配的 Provider,而此时你需要渲染一个 Consumer 组件,那么你可以用到 defaultValue
。这有助于在不封装它们的情况下对组件进行测试。
Provider
<Provider value={/* some value */}>// value可更改默认属性值
React 组件允许 Consumers 订阅 context 的改变。
接收一个 value
属性传递给 Provider 的后代 Consumers。一个 Provider 可以联系到多个 Consumers。Providers 可以被嵌套以覆盖组件树内更深层次的值。
Consumer
<Consumer> {value => /* render something based on the context value */}
</Consumer>
一个可以订阅 context 变化的 React 组件。
接收一个 函数作为子节点. 函数接收当前 context 的值并返回一个 React 节点。传递给函数的 value
将等于组件树中上层 context 的最近的 Provider 的 value
属性。如果 context 没有 Provider ,那么 value
参数将等于被传递给 createContext()
的 defaultValue
。
每当Provider的值发生改变时, 作为Provider后代的所有Consumers都会重新渲染。
从Provider到其后代的Consumers传播不受shouldComponentUpdate方法的约束,因此即使祖先组件退出更新时,后代Consumer也会被更新。
通过使用与Object.is相同的算法比较新值和旧值来确定变化。
父子耦合
经常需要从组件树中某个深度嵌套的组件中更新 context(即子组件修改context)。在这种情况下,可以通过 context 向下传递一个函数,以允许 Consumer 更新 context :
theme-context.js
// 确保默认值按类型传递// createContext() 匹配的属性是 Consumers 所期望的
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
theme-toggler-button.js
import {ThemeContext} from './theme-context';function ThemeTogglerButton() {
// Theme Toggler 按钮不仅接收 theme 属性
// 也接收了一个来自 context 的 toggleTheme 函数
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}
export default ThemeTogglerButton;
app.js
import {ThemeContext, themes} from './theme-context';import ThemeTogglerButton from './theme-toggler-button';
class App extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
// State 包含了 updater 函数 所以它可以传递给底层的 context Provider
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme,
};
}
render() {
// 入口 state 传递给 provider
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}
function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}
ReactDOM.render(<App />, document.root);
作用于多个上下文
为了保持 context 快速进行二次渲染, React 需要使每一个 Consumer 在组件树中成为一个单独的节点。
// 主题上下文, 默认lightconst ThemeContext = React.createContext('light');
// 登陆用户上下文
const UserContext = React.createContext();
// 一个依赖于两个上下文的中间组件
function Toolbar(props) {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// App组件提供上下文的初始值
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Toolbar />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
如果两个或者多个上下文的值经常被一起使用,也许你需要考虑你自己渲染属性的组件提供给它们。
在生命周期方法中访问 Context
在生命周期方法中从上下文访问值是一种相对常见的用例。而不是将上下文添加到每个生命周期方法中,只需要将它作为一个 props 传递,然后像通常使用 props 一样去使用它。
class Button extends React.Component { componentDidMount() {
// ThemeContext value is this.props.theme
}
componentDidUpdate(prevProps, prevState) {
// Previous ThemeContext value is prevProps.theme
// New ThemeContext value is this.props.theme
}
render() {
const {theme, children} = this.props;
return (
<button className={theme ? 'dark' : 'light'}>
{children}
</button>
);
}
}
export default props => (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
高阶组件中的 Context
某些类型的上下文被许多组件(例如主题或者地点信息)共用。使用 <Context.Consumer>
元素显示地封装每个依赖项是冗余的。这里higher-order component可以帮助我们解决这个问题。
我们可以创建一个命名为 withTheme
高阶组件:(将ThemedComponent组件函数封装起来)
const ThemeContext = React.createContext('light');// 在函数中引入组件
export function withTheme(Component) {
// 然后返回另一个组件
return function ThemedComponent(props) {
// 最后使用context theme渲染这个被封装组件
// 注意我们照常引用了被添加的属性
return (
<ThemeContext.Consumer>
{theme => <Component {...props} theme={theme} />}
</ThemeContext.Consumer>
);
};
}
目前任何组件都依赖于主题 context,它们都可以很容易的使用我们创建的 withTheme
函数进行订阅。
function Button({theme, ...rest}) { return <button className={theme} {...rest} />;
}
const ThemedButton = withTheme(Button);
转发 Refs
一个关于渲染属性API的问题是 refs 不会自动的传递给被封装的元素。为了解决这个问题,使用 React.forwardRef
:
fancy-button.js
class FancyButton extends React.Component { focus() {
// ...
}
// ...
}
// 使用 context 传递当前的 "theme" 给 FancyButton.
// 使用 forwardRef 传递 refs 给 FancyButton 也是可以的.
export default React.forwardRef((props, ref) => (
<ThemeContext.Consumer>
{theme => (
<FancyButton {...props} theme={theme} ref={ref} />
)}
</ThemeContext.Consumer>
));
app.js
import FancyButton from './fancy-button';const ref = React.createRef();
// ref属性将指向 FancyButton 组件,
// ThemeContext.Consumer 没有包裹它
// 这意味着我们可以调用 FancyButton 的方法就像这样 ref.current.focus()
<FancyButton ref={ref} onClick={handleClick}>
Click me!
</FancyButton>;
告诫(提升 value
到父节点的 state里)
因为 context 使用 reference identity
确定何时重新渲染,在 Consumer 中,当一个 Provider 的父节点重新渲染的时候,有一些问题可能触发意外的渲染。例如下面的代码,所有的 Consumner 在 Provider 重新渲染之时,每次都将重新渲染,因为一个新的对象总是被创建对应 Provider 里的 value
:
class App extends React.Component { render() {
return (
<Provider value={{something: 'something'}}>
<Toolbar />
</Provider>
);
}
}
为了防止这样, 提升 value
到父节点的 state里:
class App extends React.Component { constructor(props) {
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
Fragments
React 中一个常见模式是为一个组件返回多个元素。Fragments 可以让你聚合一个子元素列表,并且不在DOM中增加额外节点。
Fragments 看起来像空的 JSX 标签:
render() { return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
);
}
动机
一个常见模式是为一个组件返回一个子元素列表。以这个示例的 React 片段为例:
class Table extends React.Component { render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
}
为了渲染有效的 HTML , <Columns />
需要返回多个 <td>
元素。如果一个父 div 在 <Columns />
的 render()
函数里面使用,那么最终的 HTML 将是无效的。
class Columns extends React.Component { render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
}
在 <Table />
组件中的输出结果如下:
<table> <tr>
<!-- 以下jsx会报错 -->
<div>
<td>Hello</td>
<td>World</td>
</div>
</tr>
</table>
所以,我们介绍 Fragments
。
使用
class Columns extends React.Component { render() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
);
}
}
在正确的 <Table />
组件中,这个结果输出如下:
<table> <tr>
<td>Hello</td>
<td>World</td>
</tr>
</table>
<></>
是 <React.Fragment/>
的语法糖。
带 key 的 Fragments
<></>
语法不能接受键值或属性。
如果你需要一个带 key 的片段,你可以直接使用 <React.Fragment />
。 一个使用场景是映射一个集合为一个片段数组 — 例如:创建一个描述列表:
function Glossary(props) { return (
<dl>
{props.items.map(item => (
// 没有`key`,将会触发一个key警告
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
);
}
key
是唯一可以传递给 Fragment
的属性。在将来,我们可能增加额外的属性支持,比如事件处理。
Portals
Portals 提供了一种很好的将子节点渲染到父组件以外的 DOM 节点的方式。
ReactDOM.createPortal(child, container)
第一个参数(child
)是任何可渲染的 React 子元素,例如一个元素,字符串或碎片。第二个参数(container
)则是一个 DOM 元素。
用法
通常讲,当你从组件的 render 方法返回一个元素,该元素仅能装配 DOM 节点中离其最近的父元素:
render() { // React mounts a new div and renders the children into it
return (
<div>
{this.props.children}
</div>
);
}
然而,有时候将其插入到 DOM 节点的不同位置也是有用的:
render() { // React does *not* create a new div. It renders the children into `domNode`.
// `domNode` is any valid DOM node, regardless of its location in the DOM.
return ReactDOM.createPortal(
this.props.children,
domNode,
);
}
对于 portal 的一个典型用例是当父组件有 overflow: hidden
或 z-index
样式,但你需要子组件能够在视觉上“跳出(break out)”其容器。例如,对话框、hovercards以及提示框:
通过 Portals 进行事件冒泡
尽管 portal 可以被放置在 DOM 树的任何地方,但在其他方面其行为和普通的 React 子节点行为一致。无论其子节点是否是 portal,上下文特性依然能够如之前一样正确地工作。由于 portal 仍存在于 React 树中,而不用考虑其在 DOM 树中的位置。
这包含事件冒泡。一个从 portal 内部会触发的事件会一直冒泡至包含 React 树 的祖先。假设如下 HTML 结构:
<html> <body>
<div id="app-root"></div>
<div id="modal-root"></div>
</body>
</html>
在 #app-root
里的 Parent
组件能够捕获到未被捕获的从兄弟节点 #modal-root
冒泡上来的事件。
// These two containers are siblings in the DOMconst appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount() {
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
modalRoot.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {clicks: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// This will fire when the button in Child is clicked,
// updating Parent's state, even though button
// is not direct descendant in the DOM.
this.setState(prevState => ({
clicks: prevState.clicks + 1
}));
}
render() {
return (
<div onClick={this.handleClick}>
<p>Number of clicks: {this.state.clicks}</p>
<p>
Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.
</p>
<Modal>
<Child />
</Modal>
</div>
);
}
}
function Child() {
// 这里的按钮点击事件将会冒泡到父组件上,
// 应为这里onClick没有绑定事件
return (
<div className="modal">
<button>Click</button>
</div>
);
}
ReactDOM.render(<Parent />, appRoot);
在父组件里捕获一个来自 portal 的事件冒泡能够在开发时具有不完全依赖于 portal 的更为灵活的抽象。例如,若你在渲染一个 <Modal />
组件,父组件能够捕获其事件而无论其是否采用 portal 实现。
错误边界
错误边界介绍
部分 UI 的异常不应该破坏了整个应用。为了解决 React 用户的这一问题,React 16 引入了一种称为 “错误边界” 的新概念。
错误边界是用于捕获其子组件树 JavaScript 异常,记录错误并展示一个回退的 UI 的 React 组件,而不是整个组件树的异常。错误边界在渲染期间、生命周期方法内、以及整个组件树构造函数内捕获错误。
错误边界无法捕获如下错误:
- 事件处理 (了解更多)
- 异步代码 (例如
setTimeout
或requestAnimationFrame
回调函数) - 服务端渲染
- 错误边界自身抛出来的错误 (而不是其子组件)
一个类组件变成一个错误边界。如果它定义了生命周期方法 static getDerivedStateFromError()
或者componentDidCatch()
中的任意一个或两个。当一个错误被扔出后,使用static getDerivedStateFromError()
渲染一个退路UI。使用componentDidCatch()
去记录错误信息。
class ErrorBoundary extends React.Component { constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
错误边界工作机制类似于JavaScript catch {}
,只是应用于组件。仅有类组件可以成为错误边界。
实践中,大多数时间,你希望定义一个错误边界组件一次并将它贯穿你的整个应用。
注意错误边界仅可以捕获组件在树中比他低的组件的错误。错误边界无法捕获其自身的错误。如果一个错误边界无法渲染错误信息,则错误会向上冒泡至最接近的错误边界。这也类似于 JavaScript 中 catch {}
的工作机制。
错误边界放到哪里
错误边界的粒度是由你决定。你可以将其包装在最顶层的路由组件显示给用户”有东西出错”消息,就像服务端框架经常处理崩溃一样。你也可以将单独的插件包装在错误边界内以保护应用其他部分不崩溃。
未捕获错误的新行为
这个改变有一个重要的暗示。从React 16起,任何未被错误边界捕获的错误将导致卸载整个 React 组件树。
组件栈追踪
React 16 会将渲染期间所有在开发环境下的发生的错误打印到控制台,即使应用程序意外的将其掩盖。除了错误信息和 JavaScript 栈外,其还提供了组件栈追踪。现在你可以准确地查看发生在组件树内的错误信息:
你也可以在组件追踪堆栈中查看文件名和行号。这一功能在 Create React App 项目中默认开启:
关于事件处理器
错误边界无法捕获事件处理器内部的错误。
React 不需要错误边界恢复位于事件处理器内的错误。不像渲染方法或生命周期钩子,不同于render方法和生命周期方法,事件处理器不是在渲染时发生。因此若他们抛出异常,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 {
// Do something that could throw
} catch (error) {
this.setState({ error });
}
}
render() {
if (this.state.error) {
return <h1>Caught an error.</h1>
}
return <div onClick={this.handleClick}>Click Me</div>
}
}
自 React 15 的名称变更
React 15 在一个不同的方法名下:unstable_handleError
包含了一个支持有限的错误边界。这一方法不再能用,同时自 React 16 beta 发布起你需要在代码中将其修改为 componentDidCatch
。
为这一改变,我们已提供了一个 codemod 来帮助你自动迁移你的代码。
Web Components
React 和 web组件 被用以解决不同问题。Web组件为可重用组件提供了强大的封装能力,而React则是提供了保持DOM和数据同步的声明式库。二者目标互补。作为开发者,你可以随意地在Web组件里使用React,或者在React里使用Web组件,或都有。
在React中使用Web组件
class HelloMessage extends React.Component { render() {
return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
}
}
一个普遍的困扰是 Web组件 使用 “class” 而非 “className”。
function BrickFlipbox() { return (
{/* 使用的时class,而非jsx的className */}
<brick-flipbox class="demo">
<div>front</div>
<div>back</div>
</brick-flipbox>
);
}
在Web组件中使用React
const proto = Object.create(HTMLElement.prototype, { attachedCallback: {
value: function() {
const mountPoint = document.createElement('span');
this.createShadowRoot().appendChild(mountPoint);
const name = this.getAttribute('name');
const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
}
}
});
document.registerElement('x-search', {prototype: proto});
高阶组件
高阶组件(HOC)是react中的高级技术,用来重用组件逻辑。但高阶组件本身并不是React API。它只是一种模式,这种模式是由react自身的组合性质必然产生的。
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
对比组件将props属性转变成UI,高阶组件则是将一个组件转换成另一个组件。
高阶组件在React第三方库中很常见,比如Redux的connect
方法和Relay的createContainer
.
使用高阶组件(HOC)解决横切关注点
我们曾经介绍了混入(mixins)技术来解决横切关注点。现在我们意识到混入(mixins)技术产生的问题要比带来的价值大。更多资料介绍了为什么我们要移除混入(mixins)技术以及如何转换你已经使用了混入(mixins)技术的组件。
在React中,组件是代码复用的主要单元
例如,假设你有一个CommentList
组件,该组件从外部数据源订阅数据并渲染评论列表:
class CommentList extends React.Component { constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" is some global data source
comments: DataSource.getComments()
};
}
componentDidMount() {
// Subscribe to changes
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// Clean up listener
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// Update component state whenever the data source changes
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
然后,你又写了一个订阅单个博客文章的组件,该组件遵循类似的模式:
class BlogPost extends React.Component { constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
CommentList
和 BlogPost
组件并不相同——他们调用 DataSource
的方法不同,并且他们渲染的输出也不相同。但是,他们有很多实现是相同的:
- 挂载组件时, 向
DataSource
添加一个改变监听器。 - 在监听器内, 每当数据源发生改变时,调用
setState
。 - 卸载组件时, 移除改变监听器。
设想一下,在一个大型的应用中,这种从DataSource
订阅数据并调用setState
的模式将会一次又一次的发生。我们希望一个抽象允许我们定义这种逻辑,在单个地方,并且许多组件都可以共享它,这就是高阶组件的杰出所在。
我们可以写一个创建组件的函数,创建的组件类似CommonList
和BlogPost
一样订阅到DataSource
。该函数接受它的参数之一作为一个子组件,子组件又接受订阅的数据作为一个属性(prop)。让我们称这个函数为withSubscription
:
const CommentListWithSubscription = withSubscription( CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
第一个参数是被包裹的组件,第二个参数检索所需要的数据,从给定的DataSource
和当前props属性中(这里应该是高阶组件的props属性)。
当渲染 CommentListWithSubscription
和 BlogPostWithSubscription
时, 会向CommentList
和 BlogPost
传递一个 data
属性,该 data
属性带有从 DataSource
检索的最新数据:
// This function takes a component...function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// 用新数据重新渲染被包裹的组件
// 有任何新的props通知我门
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
注意,高阶组件既不会修改输入组件,也不会使用继承拷贝它的行为。而是,高阶组件 组合(composes) 原始组件,通过用一个容器组件 包裹着(wrapping) 原始组件。高阶组件就是一个没有副作用的纯函数。
就是这样!被包裹的组件接收容器的所有props属性以及一个新属性data
用于渲染输出。高阶组件并不关心数据使用的方式和原因,而被包裹的组件也不关心数据来自何处。
不要改变原始组件,使用组合
抵制诱惑,不要在高阶组件内修改一个组件的原型(或以其它方式修改组件)。
function logProps(InputComponent) { InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
};
// The fact that we're returning the original input is a hint that it has
// been mutated.
return InputComponent;
}
// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
上面的示例有一些问题。首先就是,输入组件不能独立于增强型组件(enhanced component)被重用。更致命的是,如果你在EnhancedComponent
上应用另一个高阶组件,同样也去改变componentWillReceiveProps
,第一个高阶组件的功能就会被覆盖。这样的高阶组件对没有生命周期方法的函数式组件也是无效的。
修改高阶组件泄露了组件的抽象性——使用者必须知道他们的实现方式,才能避免与其它高阶组件的冲突。(不要修改原先有的方法和属性)
与修改组件相反,高阶组件应该使用组合技术,将输入组件包裹到一个容器组件中:
function logProps(WrappedComponent) { return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
// 用容器包裹输入组件,不要修改它,漂亮!
return <WrappedComponent {...this.props} />;
}
}
}
这个高阶组件和那个更改型版本有着同样的功能,但却避免了潜在的冲突。它对类组件和函数式组件适用性同样好。而且,因为它是纯函数,它是可组合的,可以和其它高阶组件,甚至和它自身组合。
你可能发现了高阶组件和容器组件模式的相似之处。容器组件是专注于在高层和低层关注之间进行责任分离的策略的一部分。容器管理的事情诸如订阅和状态,传递props属性给某些组件。这些组件处理渲染UI等事情。高阶组件使用容器作为他们实现的一部分。你也可以认为高阶组件就是参数化的容器组件定义。
以上是 react高级指引 的全部内容, 来源链接: utcz.com/z/383199.html