一、深入JSX
1、JSX是语法糖
JSX本质上是为React.createElement(component, props, ...children)
方法提供的语法糖,例如:
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
会被编译为:
React.createElement(
MyButton,
{ color: 'blue', shadowSize: 2 },
'Click Me'
)
在没有子代的情况下,可以使用自闭和标签,如:
<Comp className="sidebar" />
这会被编译为:
React.createElement(
Comp,
{ className: 'sidebar' },
null
)
2、React元素类型
JSX的标签名决定了React元素的类型,标签名会在编译后成为同名变量被引用,所以作用域中应该实现存在相应的变量。在React中,有:
1)HTML自有标签的标签名为小写开头,如<div>
2)调用React组件,标签名则应以大写开头,如<Comp>
3)由于编译后会调用React.createElement
,所以应该引入React
,即事先声明:
import React from 'react'
4)可以使用点表示法
来引用React组件,如:
import React from 'react'
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>
}
}
class App extends React.Component {
render() {
return (
<MyComponents.DatePicker color="blue" />
)
}
}
5)运行时类型,应该事先用一个变量求值,如下例子:
import React from 'react'
import { PhotoStory, VideoStory } from './stories'
const components = {
photo: PhotoStory,
video: VideoStory
}
如果我们以下面的方式引用,是错误的(因为 JSX标签名不能是一个表达式),如:
render(props) {
return <components[props.storyType] story={props.story} />
}
解决这个问题,可以事先保存变量,如下:
render(props) {
const Component = components[props.storyType]
return <Component story={props.story} />
}
3、属性
在JSX里,可以传递任何合法的JS表达式作为{}
里包裹的值,一些注意点如下:
1)属性值不需要用"
包裹,用"
包裹的会被视为字符串,如下两者是等价的:
<MyComponent message="Hello" />
<MyComponent message={'Hello'} />
2)当传递一个字符串常量时,它不会进行HTML转义
3)缺少属性值的情况下,默认值为true
,即以下两者是等价的:
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
4)可以使用...
语法传递多个属性,即以下写法是等价的:
function App1() {
return <Greeting firstName="Ruphi" lastName="Lau" />
}
function App2() {
const props = {
firstName: 'Ruphi',
lastName: 'Lau'
}
return <Greeting {...props} />
}
4、子代
1)可以通过props.children
引用到开始标签和闭合标签之间的内容,另外,props.children
获取到的内容,JSX会进行处理:移除空行和始末处的空格、标签相邻的新行,压缩字符串常量内部的换行为一个空格
2)可以通过数组的形式,返回多个元素(但是需要提供key),如:
render() {
return [
<li key="A">First item</li>
<li key="B">Second item</li>
<li key="C">Third item</li>
]
}
3)由于{}
里可以包含任意合法JS表达式,所以自然也可以传递一个函数,如下:
function Repeat(props) {
let items = []
for (let i = 0; i < props.numTimes; ++i) {
items.push(props.children(i))
}
return <div>{items}</div>
}
function App() {
return (
<Repeat numTimes={10}>
{(index) => <div key={index}>第{index}项</div>}
</Repeat>
)
}
4)false
、null
、undefined
、true
等都是有效的子代,但是不会被直接渲染(如果想要被渲染出来,则可以这么处理:{String(false)}
),所以可以进行条件渲染,如:
<div>
{showHeader && <Header />}
</div>
当showHeader
为false
时,相当于渲染<div>{false}</div>
,从而得到<div></div>
5)对于那些falsy
值(如),仍然可被渲染,所以对于以下情况需要注意:
<div>
{props.messages.length && <MessageList messages={props.messages} />}
</div>
当props.messages
为空时,会渲染为<div>0</div>
,所以正确的做法是,明确条件为:props.messages.length > 0
二、使用propTypes
进行prop
类型检查
随着应用日渐庞大,我们可以通过类型检查
捕获大量的错误。除了可以使用flow
和TypeScript
,我们还能用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
}
1、详细的使用示例
使用PropTypes
的方式有如下几种:
import PropTypes from 'prop-types'
MyComponent.propTypes = {
// 表明prop值类型只能为对应的类型
prop1: PropTypes.array,
prop2: PropTypes.bool,
prop3: PropTypes.func,
prop4: PropTypes.number,
prop5: PropTypes.object,
prop6: PropTypes.string,
prop7: PropTypes.symbol,
// 表明prop值类型可以是任何可被渲染的元素(数字、字符串、子元素、数组)
prop8: PropTypes.node,
// 表明prop值类型是一个React元素
prop9: PropTypes.element,
// 表明prop值是某个类的实例
prop10: PropTypes.instanceOf(Message),
// 表明prop值是某几个特定值之一
prop11: PropTypes.oneOf(['A', 'B', 'C']),
// 表明prop值类型是某几个特定类型之一
prop12: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
]),
// 表明prop值是一个数组,且数组中元素的值类型为特定类型
prop13: PropTypes.arrayOf(PropTypes.number),
// 表明prop值是一个对象,且对象中元素的值类型为特定类型
prop14: PropTypes.objectOf(PropTypes.number),
// 表明prop值是一个对象,且对对象属性及属性类型进行限制
prop15: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),
// 可以在任意PropTypes属性后面加上`isRequired`,表明属性必须提供
prop16: PropTypes.func.isRequired,
prop17: PropTypes.any.isRequired,
// 还可以指定自定义的验证器,在验证失败时应该返回Error对象
prop18: function(props, propName, component) {
if (!/matchme/.test(props[propName])) {
return new Error()
}
},
// 自定义验证器还可以结合`arrayOf`、`objectOf`表语
prop19: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName)) {
// ...
})
}
2、限制单个子代
可以通过限制props.children
的类型,限制子节点只能有一个,如:
import PropTypes from 'prop-types'
class MyComponent extends React.Component {
render() {
const children = this.props.children
return (
<div>{children}</div>
)
}
}
MyComponent.propTypes = {
children: PropTypes.element.isRequired
}
3、属性默认值
可以使用defaultProps
为属性指定默认值,如:
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
)
}
}
Greeting.defaultProps = {
name: 'World'
}
也可以使用实验性语法static
指定类属性,简化写法如:
class Greeting extends React.Component {
static defaultProps = {
name: 'World'
}
render() {
return (
<h1>Hello, {this.props.name}</h1>
)
}
}
三、Refs & DOM
React数据流中,prop
是父子组件交互的唯一方式。要修改子组件,父组件要给子组件新的prop来重新渲染子组件。但是在某些情况下,我们仍然需要在数据流外强制修改子代(子代可以是子组件或者DOM元素),那么这种情况下,我们可以用refs
1、使用refs的时机
我们可以在下列情况下使用refs:
- 处理焦点、文本选择或者媒体控制
- 触发强制动画
- 集成第三方DOM库
但是不应该过度使用refs,能够通过声明式实现的应该避免使用refs实现
2、为DOM元素添加ref
React支持给任意组件添加特殊属性,而ref
属性则接收一个回调函数,这个回调函数在组件加载或卸载时会立即执行,并且将底层的DOM元素作为参数传给回调函数,如:
class CustomTextInput extends React.Component {
constructor(props) {
super(props)
this.focus = this.focus.bind(this)
}
focus() {
this.textInput.focus()
}
render() {
return (
<div>
<input type="text" ref={input => this.textInput = input} />
<button type="button" onClick={this.focus}>聚焦文本框</button>
</div>
)
}
}
例子中,当组件加载或卸载时,<input>
的DOM节点就会作为参数input传入回调函数(加载时传入DOM元素,卸载时传入null),ref
回调会在componentDidMount
或componentDidUpate
这些生命周期回调之前执行
3、类定义组件的ref
类定义组件的ref,获取到的则是已加载的React实例,即:
class AutoFocusTextInput extends React.Component {
componentDidMount() {
this.textInput.focusTextInput() // 调用的是CustomTextInput组件里的方法
}
render() {
return (
<CustomTextInput
ref={(input) => { this.textInput = input }} />
)
}
}
4、函数定义组件,没有ref
由于函数定义组件
没有实例,所以不能在函数定义组件
上使用ref,如:
function MyFunctionalComponent() {
return <input />
}
class Parent extends React.Component {
render() {
// 以下ref无效
return (
<MyFunctionalComponent ref={(input) => { this.textInput = input }}
)
}
}
但是函数定义组件
内部引用的组件,如果是类定义组件
或者DOM元素
,那么是可以使用ref来引用的。
5、对父组件暴露DOM节点
某些情况下,我们需要从父组件访问子组件的DOM节点(因为在父组件里用ref引用到的只是子组件的实例),那么我们可以这么做:父组件给子组件传递一个回调作为属性,子组件则获取这个回调,绑定到ref上,如下:
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
)
}
class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
)
}
}
四、不使用ES6
如果不使用ES6里的class
关键字来创建React组件的话,那么可以使用create-react-class
模块来创建,如:
var createReactClass = require('creat-react-class')
var Greeting = createReactClass({
render: function() {
return <h1>Hello, {this.props.name}</h1>
}
})
其中,还有一些区别需要注意:
1、声明默认属性
使用class
关键字创建组件,可以用defaultProps
声明默认属性,而createReactClass
方式,则需要在getDefaultProps
方法中返回一个对象来表明默认属性,即:
// class方式
class Comp extends React.Component {
// ...
}
Comp.defaultProps = {
name: 'React'
}
// createReactClass方式
var Comp = createReactClass({
getDefaultProps: function() {
return {
name: 'React'
}
}
})
2、设置初始状态
使用class
方式,可以在constructor
里给this.state
赋值来定义组件的初始状态,而creactReactClass
方式则需要通过getInitialState
方法来定义,如下:
// class方式
class Comp extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'React'
}
}
}
// createReactClass方式
var Comp = createReactClass({
getInitialState: function() {
return {
name: 'React'
}
}
})
3、自动绑定
使用createReactClass
方式创建的组件,会自动进行this
的绑定为当前组件,如:
var SayHello = createReactClass({
getInitialState: function() {
return { message: 'Hello' }
},
handleClick: function() {
alert(this.state.message)
},
render: function() {
return (
<button onClick={this.handleClick}>Say Hello</button>
)
}
})
4、Mixin
使用createReactClass
方式创建的组件,支持mixin,如下:
var SomeMixinObj = {
// ...
}
var Comp = createReactClass({
mixins: [SomeMixinObj],
// ...
})
如果一个组件里有多个Mixin,且Mixin之间定义了相同的生命周期方法,那么这些生命周期方法都会被调用,调用顺序为:组件自身方法 > Mixin方法 > Mixin定义的顺序
五、Context
在某些场景下,可能要跨多级子组件传递prop,而我们又不想向下每层都手动地传递需要的prop,那么React中原生的解决方法就是采用Context
1、用法
使用Context
的做法为:在父组件类中添加childContextTypes
属性声明要跨层级传递的prop,子组件类里添加contextTypes
获得prop,然后用this.context
引用,如下:
const PropTypes = require('prop-types')
class Button extends React.Component {
render() {
return (
<button style={{background: this.context.color}}>
{this.props.children}
</button>
)
}
}
Button.contextTypes = {
color: PropTypes.string
}
class Message extends React.Component {
render() {
return (
<div>{this.props.text} <Button>Delete</Button></div>
)
}
}
class MessageList extends React.Component {
getChildContext() {
return { color: 'purple' }
}
render() {
const children = this.props.messages.map(message =>
<Message text={message.text} />
)
return <div>{children}</div>
}
}
MessageList.childContextProps = {
color: PropTypes.string
}
注意: 如果在子组件里contextTypes
没有定义,那么context
将会是个空对象
2、生命周期函数中引用Context
如果组件中定义了contextTypes
,那么以下的生命周期函数中将会接收到额外的context
对象,即:
constructor(props, context)
componentWillReceiveProps(nextProps, nextContext)
shouldComponentUpdate(nextProps, nextState, nextContext)
componentWillUpdate(nextProps, nextstate, nextContext)
componentDidUpdate(prevProps, prevState, prevContext)
3、无状态的函数定义组件使用context
无状态的函数式组件也可以使用context
,如下:
const PropTypes = require('prop-types')
function Button(props, context) {
return (
<button style={{background: context.color}}>{this.props.children}</button>
)
}
Button.contextProps = {
color: PropTypes.string
}
4、更新context
React提供了更新context的API,但是基本已经被废除了,不建议使用。
当state或者props更新时getChildContext
方法会被调用。为了在context中更新数据,使用 this.setState来更新本地state。这将会生成一个新的context,所有的子组件会接收到更新,如:
const PropTypes = require('prop-types')
class MediaQuery extends React.Component {
constructor(props) {
super(props)
this.state = { type: 'desktop' }
}
getChildContext() {
return { type: this.state.type }
}
componentDidMount() {
const checkMediaQuery = () => {
const type = window.matchMedia('(min-width:1025px)').matches
? 'desktop'
: 'mobile'
if (type !== this.state.type) {
this.setState({ type })
}
}
window.addEventListener('resize', checkMediaQuery)
checkMediaQuery()
}
render() {
return this.props.children
}
}
MediaQuery.childContextTypes = {
type: PropTypes.string
}
问题则在于:组件更新会产生新的context
,若有一个中间的父组件的shouldComponentUpdate
返回了false
,那么接下来的子组件中的context
是不会被更新的,如此一来使用context
,组件就失控了
六、Fragments(片段)
React中经常会有一个组件返回多个元素的场景,但是又有 只能有一个根组件 的限定。通常的做法则是使用<div>
进行包裹,但是这样子会在DOM中增加额外的节点,那么Fragment
就是为了解决这一问题的方案,如下:
render() {
return (
<>
<ChildA />
<ChildB />
</>
)
}
1、动机
之所以需要有这种特性,是因为通常情况下<div>
包裹不会有什么问题,但对于table渲染而言,如下例子:
class Table extends React.Component {
render() {
return (
<table>
<tr><Columns /></tr>
</table>
)
}
}
class Columns extends React.Component {
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
)
}
}
这种情况下,最终渲染会得到:
<table>
<tr>
<div>
<td>Hello</td>
<td>World</td>
</div>
</tr>
</table>
最终的HTML元素则是无效的
2、用法
Fragment的简写形式为:<></>
,使用它不会渲染出额外的DOM元素。事实上,<></>
是<React.Fragment>
的语法糖,我们也可以这么写:
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
)
}
}
应该注意的是:<></>
不能接受任何key
或者属性,如果使用key
,请用<React.Fragment>
,它可以接受且目前也只能接收key
这一属性
七、Portals
Portals提供了一种将子节点渲染到父组件外DOM节点的方式,它的语法形式为:
ReactDOM.createPortal(child, container)
// 用法:
render() {
// React不会创建新的div,而是将子节点渲染到domNode容器中
// domNode可以是任意有效的DOM节点,它在不在DOM内都是可以的
return ReactDOM.createPortal(
this.props.children,
domNode
)
}
典型的应用场景:父组件有overflow: hidden
或者z-index
,需要子组件能够在视觉上“跳出”容器,如:对话框、提示框、hovercard
事件冒泡
虽然portal可以放置在DOM树的任何地方,但其行为和普通React子节点一致,其上下文特性依然可以正确地工作,如:
<html>
<body>
<div id="app-root"></div>
<did id="modal-root"></did>
</body>
</html>
使用portal的情况下,#app-root
里的组件能够捕获到#modal-root
里冒泡上来的事件:
const 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.setState(prevState => ({
clicks: prevState.clicks + 1
}))
}
render() {
return (
<div onClick={this.handleClick}>
<p>点击次数:{this.state.clicks}</p>
<Modal>
<Child />
</Modal>
</div>
)
}
}
function Child() {
return (
<div className="modal">
<button>Click</button>
</div>
)
}
ReactDOM.render(<Parent />, appRoot);