愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

React学习笔记(四):高阶知识(上)

一、深入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)falsenullundefinedtrue等都是有效的子代,但是不会被直接渲染(如果想要被渲染出来,则可以这么处理:{String(false)}),所以可以进行条件渲染,如:

<div>
    {showHeader && <Header />}
</div>

showHeaderfalse时,相当于渲染<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类型检查

随着应用日渐庞大,我们可以通过类型检查捕获大量的错误。除了可以使用flowTypeScript,我们还能用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回调会在componentDidMountcomponentDidUpate这些生命周期回调之前执行

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);