React详解

React的安装和配置

https://zh-hans.reactjs.org/docs/getting-started.html

JSX 简介

const element = <h1>Hello, world!</h1>;

它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。
在 JSX 中嵌入表达式

在下面的例子中,我们声明了一个名为 name 的变量,然后在 JSX 中使用它,并将它包裹在大括号中:

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

ReactDOM.render(
  element,
  document.getElementById('root')
);

在 JSX 语法中,你可以在大括号内放置任何有效的 JavaScript 表达式。例如,2 + 2,user.firstName 或 formatName(user) 都是有效的 JavaScript 表达式。

在下面的示例中,我们将调用 JavaScript 函数 formatName(user) 的结果,并将结果嵌入到

元素中。

function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}

const user = {
  firstName: 'Harper',
  lastName: 'Perez'
};

const element = (
  <h1>
    Hello, {formatName(user)}!
  </h1>
);

ReactDOM.render(
  element,
  document.getElementById('root')
);
JSX 也是一个表达式

在编译之后,JSX 表达式会被转为普通 JavaScript 函数调用,并且对其取值后得到 JavaScript 对象。

也就是说,你可以在 if 语句和 for 循环的代码块中使用 JSX,将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX:

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}

JSX 特定属性

你可以通过使用引号,来将属性值指定为字符串字面量:

const element = <div tabIndex="0"></div>;

也可以使用大括号,来在属性值中插入一个 JavaScript 表达式:

const element = <img src={user.avatarUrl}></img>;

在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。你应该仅使用引号(对于字符串值)或大括号(对于表达式)中的一个,对于同一属性不能同时使用这两种符号。

JSX 防止注入攻击、

你可以安全地在 JSX 当中插入用户输入内容:

const title = response.potentiallyMaliciousInput;
// 直接使用是安全的:
const element = <h1>{title}</h1>;

React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。

JSX 表示对象

Babel 会把 JSX 转译成一个名为React.createElement() 函数调用。

以下两种示例代码完全等效:

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

React.createElement() 会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:

// 注意:这是简化过的结构
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};

这些对象被称为 “React 元素”。它们描述了你希望在屏幕上看到的内容。React 通过读取这些对象,然后使用它们来构建 DOM 以及保持随时更新。

元素渲染

元素是构成 React 应用的最小砖块。

元素描述了你在屏幕上想看到的内容。

const element = <h1>Hello, world</h1>;

与浏览器的 DOM 元素不同,React 元素是创建开销极小的普通对象。React DOM 会负责更新 DOM 来与 React 元素保持一致。

将一个元素渲染为 DOM

假设你的 HTML 文件某处有一个

<div id="root"></div>

我们将其称为“根” DOM 节点,因为该节点内的所有内容都将由 React DOM 管理。

想要将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入 ReactDOM.render():

const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
更新已渲染的元素

React 元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。一个元素就像电影的单帧:它代表了某个特定时刻的 UI。

根据我们已有的知识,更新 UI 唯一的方式是创建一个全新的元素,并将其传入 ReactDOM.render()。

考虑一个计时器的例子:

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(element, document.getElementById('root'));
}

setInterval(tick, 1000);

React 只更新它需要更新的部分

React DOM 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 DOM 达到预期的状态。

你可以使用浏览器的检查元素工具查看上一个例子来确认这一点。
在这里插入图片描述
尽管每一秒我们都会新建一个描述整个 UI 树的元素,React DOM 只会更新实际改变了的内容,也就是例子中的文本节点。

组件 & Props

组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。

组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。

函数组件与 class 组件

定义组件最简单的方式就是编写 JavaScript 函数:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。
你同时还可以使用 ES6 的 class 来定义组件:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
渲染组件

之前,我们遇到的 React 元素都只是 DOM 标签:

const element = <div />;

不过,React 元素也可以是用户自定义的组件:

const element = <Welcome name="Sara" />;

当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”。

例如,这段代码会在页面上渲染 “Hello, Sara”:

function Welcome(props){
    return <h1>Hello,{props.name}</h1>
}


const element = <Welcome name="sara"/>
ReactDOM.render(element,document.getElementById('root'))
  • 我们调用 ReactDOM.render() 函数,并传入 作为参数。
  • React 调用 Welcome 组件,并将 {name: ‘Sara’} 作为 props 传入。
  • Welcome 组件将

    Hello, Sara

    元素作为返回值。
    *React DOM 将 DOM 高效地更新为

    Hello, Sara

注意: 组件名称必须以大写字母开头。
React 会将以小写字母开头的组件视为原生 DOM 标签。例如,

代表 HTML 的 div 标签,而 则代表一个组件,并且需在作用域内使用 Welcome。

组合组件

组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。按钮,表单,对话框,甚至整个屏幕的内容:在 React 应用程序中,这些通常都会以组件的形式表示。

例如,我们可以创建一个可以多次渲染 Welcome 组件的 App 组件:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

通常来说,每个新的 React 应用程序的顶层组件都是 App 组件。但是,如果你将 React 集成到现有的应用程序中,你可能需要使用像 Button 这样的小组件,并自下而上地将这类组件逐步应用到视图层的每一处。

提取组件

将组件拆分为更小的组件。

例如,参考如下 Comment 组件:

function Component(props){
    return (
        <div className="Component">
            <div className="UseInfo">
                <img className="Avatar" src={props.author.avatarUrl} alt={props.author.name}/>
                <div className="UserInfo-name">{props.author.name}</div>
            </div>
            <div className="Component-text">
                {props.text}
            </div>
            <div className="Component-date">{formatDate(props.date)}</div>
        </div>
    )
}

该组件用于描述一个社交媒体网站上的评论功能,它接收 author(对象),text (字符串)以及 date(日期)作为 props。

该组件由于嵌套的关系,变得难以维护,且很难复用它的各个部分。因此,让我们从中提取一些组件出来。

首先,我们将提取 Avatar 组件:

function Avatar(props){
    return (
        <img className="Avatar" src={props.user.avatarUrl} alt={props.user.name}/>
    )
}

Avatar 不需知道它在 Comment 组件内部是如何渲染的。因此,我们给它的 props 起了一个更通用的名字:user,而不是 author。

我们建议从组件自身的角度命名 props,而不是依赖于调用组件的上下文命名。

接下来,我们将提取 UserInfo 组件,该组件在用户名旁渲染 Avatar 组件

function UserInfo(props){
    return (
        <div className="UserInfo">
            <Avatar user={props.user}/>
            <div className="UserInfo-name">{props.user.name}</div>
        </div>
    )
}

进一步简化 Comment 组件:

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

最初看上去,提取组件可能是一件繁重的工作,但是,在大型应用中,构建可复用组件库是完全值得的。根据经验来看,如果 UI 中有一部分被多次使用(Button,Panel,Avatar),或者组件本身就足够复杂(App,FeedStory,Comment),那么它就是一个可提取出独立组件的候选项

Props 的只读性

组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。来看下这个 sum 函数:

function sum(a, b) {
  return a + b;
}

这样的函数被称为“纯函数”,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。
相反,下面这个函数则不是纯函数,因为它更改了自己的入参:

function withdraw(account, amount) {
  account.total -= amount;
}

React 非常灵活,但它也有一个严格的规则:
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

当然,应用程序的 UI 是动态的,并会伴随着时间的推移而变化
在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。

State & 生命周期

封装真正可复用的 Clock 组件。它将设置自己的计时器并每秒更新一次。
我们可以从封装时钟的外观开始:


function Clock(props) {
  return (
    <div>
      <h1>Hello,word!</h1>
      <h2>it is {props.date.toLocaleTimeString()}</h2>
    </div>
  )
}
function tick(){
    ReactDOM.render(
        <Clock date={new Date()}/>,
        document.getElementById('root')
    )
}

然而,它忽略了一个关键的技术细节:Clock 组件需要设置一个计时器,并且需要每秒更新 UI。

理想情况下,我们希望只编写一次代码,便可以让 Clock 组件自我更新:
我们需要在 Clock 组件中添加 “state” 来实现这个功能。
State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。

将函数组件转换成 class 组件

通过以下五步将 Clock 的函数组件转成 class 组件:

*创建一个同名的 ES6 class,并且继承于 React.Component。

  • 添加一个空的 render() 方法。
    *将函数体移动到 render() 方法之中。
    *在 render() 方法中使用 this.props 替换 props。
    *删除剩余的空函数声明。
class Clock extends Component{
    render(){
        return(
            <div>
              <h2>It is {this.props.date.toLocaleTimeString()}</h2>
            </div>
        )
    }
}

现在 Clock 组件被定义为 class,而不是函数。

每次组件更新时 render 方法都会被调用,但只要在相同的 DOM 节点中渲染 ,就仅有一个 Clock 组件的 class 实例被创建使用。这就使得我们可以使用如 state 或生命周期方法等很多其他特性

向 class 组件中添加局部的 state

我们通过以下三步将 date 从 props 移动到 state 中:

  • 1 把 render() 方法中的 this.props.date 替换成 this.state.date :
  • 2 添加一个 class 构造函数,然后在该函数中为 this.state 赋初值:
    通过以下方式将 props 传递到父类的构造函数中:
constructor(props){
    super(props)
    this.state = {date: new Date()}
}
  • 3 移除 元素中的 date 属性:
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

完整代码

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);
将生命周期方法添加到 Class 中

在具有许多组件的应用程序中,当组件被销毁时释放所占用的资源是非常重要的。
当 Clock 组件第一次被渲染到 DOM 中的时候,就为其设置一个计时器。这在 React 中被称为“挂载(mount)”。

同时,当 DOM 中 Clock 组件被删除的时候,应该清除计时器。这在 React 中被称为“卸载(unmount)”。

我们可以为 class 组件声明一些特殊的方法,当组件挂载或卸载时就会去执行这些方法:


class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }
  componentDidMount(){
      
  }
  componentWillUnmount(){
      
  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

这些方法叫做“生命周期方法”。

componentDidMount() 方法会在组件已经被渲染到 DOM 中后运行,所以,最好在这里设置计时器:

 componentDidMount(){
      this.timeID = setInterval(() =>this.tick(), 1000);
  }

接下来把计时器的 ID 保存在 this 之中(this.timerID)。

尽管 this.props 和 this.state 是 React 本身设置的,且都拥有特殊的含义,但是其实你可以向 class 中随意添加不参与数据流(比如计时器 ID)的额外字段。

我们会在 componentWillUnmount() 生命周期方法中清除计时器:

 componentWillUnmount(){
      clearInterval(this.timeID)
  }

最后,我们会实现一个叫 tick() 的方法,Clock 组件每秒都会调用它。

使用 this.setState() 来时刻更新组件 state:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }
  componentDidMount(){
      this.timeID = setInterval(() =>this.tick(), 1000);
  }
  componentWillUnmount(){
      clearInterval(this.timeID)
  }
  tick(){
      this.setState({
          date:new Date()
      })
  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

这些方法的调用顺序:

  • 1 当 被传给 ReactDOM.render()的时候,React 会调用 Clock 组件的构造函数。因为 Clock 需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化 this.state。我们会在之后更新 state。
  • 2 之后 React 会调用组件的 render() 方法。这就是 React 确定该在页面上展示什么的方式。然后 React 更新 DOM 来匹配 Clock 渲染的输出。
    3 当 Clock 的输出被插入到 DOM 中后,React 就会调用 ComponentDidMount() 生命周期方法。在这个方法中,Clock 组件向浏览器请求设置一个计时器来每秒调用一次组件的 tick() 方法
    4 浏览器每秒都会调用一次 tick() 方法。 在这方法之中,Clock 组件会通过调用 setState() 来计划进行一次 UI 更新。得益于 setState() 的调用,React 能够知道 state 已经改变了,然后会重新调用 render() 方法来确定页面上该显示什么。这一次,render() 方法中的 this.state.date 就不一样了,如此以来就会渲染输出更新过的时间。React 也会相应的更新 DOM。
    5 一旦 Clock 组件从 DOM 中被移除,React 就会调用 componentWillUnmount() 生命周期方法,这样计时器就停止了。
State 的更新可能是异步的

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。

因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。

例如,此代码可能会无法更新计数器:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:

this.setState((state,props)=>({
    counter:state.counter + props.increament
}))

上面使用了箭头函数,不过使用普通的函数也同样可以:

// Correct
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

State 的更新会被合并

当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。

例如,你的 state 包含几个独立的变量:

constructor(props){
    super(props);
    this.state = {
        post:[],
        comments:[]
    }
}

然后你可以分别调用 setState() 来单独地更新它们:

componentDidMount(){
    fetchPosts().then((response)=>{
        this.setState({
            posts:response.posts
        })
    });
    fetchPosts().then(response=>{
        this.setState({
            comments:response.comments
        })
    })
}

这里的合并是浅合并,所以 this.setState({comments}) 完整保留了 this.state.posts, 但是完全替换了 this.state.comments。

数据是向下流动的

不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。

这就是为什么称 state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。

组件可以选择把它的 state 作为 props 向下传递到它的子组件中:

<FormattedDate date={this.state.date}/>

FormattedDate 组件会在其 props 中接收参数 date,但是组件本身无法知道它是来自于 Clock 的 state,或是 Clock 的 props,还是手动输入的:

 function FormattedDate(props){
    return <h2>it is {props.date.toLocaleTimeString()}</h2>
}

这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。

如果你把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。

在 React 应用中,组件是有状态组件还是无状态组件属于组件实现的细节,它可能会随着时间的推移而改变。你可以在有状态的组件中使用无状态的组件,反之亦然。

事件处理

React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:

React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
例如,传统的 HTML:

<button onclick="activateLasers()">
  Activate Lasers
</button>

在 React 中略微不同:

<button onClick={activateLasers}>
  Activate Lasers
</button>

在 React 中另一个不同点是你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault 。例如,传统的 HTML 中阻止链接默认打开一个新页面,你可以这样写:

<a href="#" onclick="console.log('The link was clicked.'); return false">
  Click me
</a>

在 React 中,可能是这样的:

function ActionLink(){
    function handleClick(e){
        e.preventDefault()
        console.log('the link was clicked');
        
    }
    return (
        <a href="#" onClick={handleClick}>
        Click Me
        </a>
    )
}

在这里,e 是一个合成事件。React 根据 W3C 规范来定义这些合成事件,所以你不需要担心跨浏览器的兼容性问题。React 事件与原生事件不完全相同。如果想了解更多,请查看 SyntheticEvent 参考指南。

使用 React 时,你一般不需要使用 addEventListener 为已创建的 DOM 元素添加监听器。事实上,你只需要在该元素初始渲染的时候添加监听器即可。

当你使用 ES6 class 语法定义一个组件的时候,通常的做法是将事件处理函数声明为 class 中的方法。例如,下面的 Toggle 组件会渲染一个让用户切换开关状态的按钮:

class Toggle extends React.Component{
    constructor(props){
        super(props)
        this.state = {isToggleOn:true}
        //.为了在回调中使用'this' 这个绑定是必不可少的
        this.handleClick = this.handleClick.bind(this)
    }
    handleClick(){
        this.setState({
            isToggleOn:!state.isToggleOn
        })
    }
    render(){
        return (
            <button onClick={this.handleClick}>
            {this.state.isToggleOn?'On':'OFF'}
            </button>
        )
    }
}
ReactDOM.render(<Toggle />,document.getElementById('root'))

你必须谨慎对待 JSX 回调函数中的 this,在 JavaScript 中,class 的方法默认不会绑定 this。如果你忘记绑定 this.handleClick 并把它传入了 onClick,当你调用这个函数的时候 this 的值为 undefined。

这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。通常情况下,如果你没有在方法后面添加 (),例如 onClick={this.handleClick},你应该为这个方法绑定 this。

如果觉得使用 bind 很麻烦,这里有两种方式可以解决。如果你正在使用实验性的 public class fields 语法,你可以使用 class fields 正确的绑定回调函数:

class LoggingButton extends React.Component{
    //此语法确保`handleClick`内的`this`已被绑定
    handleClick = ()=>{
        console.log('this is',this);
        
    }
    render(){
        return (
            <button onClick={this.handleClick}>
                Click Me
            </button>
        )
    }
}

Create React App 默认启用此语法。

如果你没有使用 class fields 语法,你可以在回调中使用箭头函数:

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 语法来避免这类性能问题。

向事件处理程序传递参数

在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:

<button onClick={(e)=>this.deleteRow(id,e)}> Delete Row</button>
<button onClick={this.delete.bind(this,id)}>Delete Row</button>

上述两种方式是等价的,分别通过箭头函数和 Function.prototype.bind 来实现。

在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。

条件渲染

在 React 中,你可以创建不同的组件来封装各种你需要的行为。然后,依据应用的不同状态,你可以只渲染对应状态下的部分内容。
React 中的条件渲染和 JavaScript 中的一样,使用 JavaScript 运算符 if 或者条件运算符去创建元素来表现当前的状态,然后让 React 根据它们来更新 UI。

观察这两个组件:

function UserGreeting(props) {
  return <h1>Welcome back!</h1>;
}

function GuestGreeting(props) {
  return <h1>Please sign up.</h1>;
}

再创建一个 Greeting 组件,它会根据用户是否登录来决定显示上面的哪一个组件。

function Greeting(props){
    const isLoggedIn = props.isLoggedIn
    if(isLoggedIn){
        return <UserGreeting />
    }
    return <GuestGreeting />
}

ReactDOM.render(<Greeting/>,
document.getElementById('root')
)
元素变量

你可以使用变量来储存元素。 它可以帮助你有条件地渲染组件的一部分,而其他的渲染部分并不会因此而改变。

观察这两个组件,它们分别代表了注销和登录按钮:

function LoginButton(props){
    return (
        <button onClick={props.onClick}>Login</button>
    )
}
function LogoutButton(props){
    return (
        <button onClick={props.onClick}>Logout</button>
    )
}

在下面的示例中,我们将创建一个名叫 LoginControl 的有状态的组件。

它将根据当前的状态来渲染 或者 。同时它还会渲染上一个示例中的 。

class LoginControl extends React.Component{
    constructor(props){
        super(props);
        this.handleLoginClick = this.handleLoginClick.bind(this)
        this.handleLogoutClick = this.handleLogoutClick.bind(this)
        this.state = {isLoggedIn:false}
    }
    handleLoginClick(){
        this.setState({
            isLoggedIn:true
        })
    }
    handleLogoutClick(){
        this.setState({
            isLoggedIn:false
        })
    }
    render(){
        const isLoggedIn = this.state.isLoggedIn
        let button
        if(isLoggedIn){
            button = <LogoutButton onClick={this.handleLogoutClick}/>
        }else{
            buttonv = <LoginButton onClick={this.handleLoginClick}/>
        }
        return (
            <div>
                <Greeting isLoggedIn={isLoggedIn}/>
                {button}
            </div>
        )
    }
}

ReactDOM.render(<LoginControl />,document.getElementById('root'))

声明一个变量并使用 if 语句进行条件渲染是不错的方式,但有时你可能会想使用更为简洁的语法。
在 JSX 中内联条件渲染的方法

与运算符 &&

通过花括号包裹代码,你可以在 JSX 中嵌入表达式。这也包括 JavaScript 中的逻辑与 (&&) 运算符。它可以很方便地进行元素的条件渲染:

function Mailbox(props){
    const unreadMessage = props.unreadMessage
    return (
        <div>
            <h1>hello,word</h1>

           {unreadMessage.length>0&& 
           <h2>You have {unreadMessage.length} unread messages</h2>
           }
        </div>
    )
}
const messages = ['React','Re:React','Re2:React']
ReactDOM.render(
    <Mailbox unreadMesssage={messages}/>,
    document.getElementById('root')
)

之所以能这样做,是因为在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false。

因此,如果条件是 true,&& 右侧的元素就会被渲染,如果是 false,React 会忽略并跳过它。

请注意,返回 false 的表达式会使 && 后面的元素被跳过,但会返回 false 表达式。在下面示例中,render 方法的返回值是

0

render(){
    const count = 0
    return(
        <div>
            {count &&<h1>Messages:{count}</h1>}
        </div>
    )
}
三目运算符

另一种内联条件渲染的方法是使用 JavaScript 中的三目运算符 condition ? true : false。

在下面这个示例中,我们用它来条件渲染一小段文本

render(){
    const isLoginIn = this.state.isLoginIn
    return(
        <div>
            The user is <b>{isLoginIn?'Currently':'not'}</b> logged in
        </div>
    )
}

同样的,它也可以用于较为复杂的表达式中,虽然看起来不是很直观:

render(){
    const isLoginIn = this.state.isLoginIn
    return (
        <div>
            {isLoginIn ? <LogoutButton onClick={this.handleLogoutClick}></LogoutButton>:<LoginButton onClick={this.handleLoginClick}></LoginButton>
            }
        </div>
    )
}

如果条件变得过于复杂,那你应该考虑如何提取组件。

阻止组件渲染

在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让 render 方法直接返回 null,而不进行任何渲染。

下面的示例中, 会根据 prop 中 warn 的值来进行条件渲染。如果 warn 的值是 false,那么组件则不会渲染:

function WarningBanner(props){
    if(props.warn){
        return false
    }
    return (
        <div className="warning">Warning</div>
    )
}

class Page extends React.Component{
    constructor(props){
        super(props)
        this.state = {
            showWarning:false
        }
        this.handleToggleClick = this.handleToggleClick.bind(this)
    }
    handleToggleClick(){
        this.setState((state)=>({
            showWarning:!state.showWarning
        }))
    }
    render(){
        return (
            <div>
                <warningBanner warn={this.state.showWarning}/>
                <button onClick={this.handleToggleClick}>
                    {this.state.showWarning?'Hide':'Show'}
                </button>
            </div>
        )
    }
}

ReactDOM.render(<Page/>,document.getElementById('root'))

在组件的 render 方法中返回 null 并不会影响组件的生命周期。例如,上面这个示例中,componentDidUpdate 依然会被调用。

列表 & Key

在 Javascript 中如何转化列表。

如下代码,我们使用 map() 函数让数组中的每一项变双倍,然后我们得到了一个新的列表 doubled 并打印出来:

const numbers = [1,2,3,4]
const doubled = numbers.map((number)=>number *2)
console.log(doubled)

代码打印出 [2, 4, 6, 8, 10]。

在 React 中,把数组转化为元素列表的过程是相似的。

渲染多个组件
你可以通过使用 {} 在 JSX 内构建一个元素集合。

下面,我们使用 Javascript 中的 map() 方法来遍历 numbers 数组。将数组中的每个元素变成

  • 标签,最后我们将得到的数组赋值给 listItems:
  • const numbers = [1,2,3,4]
    const listItems = numbers.map((number)=>
        <li>{number}</li>
    )
    

    我们把整个 listItems 插入到

    • 元素中,然后渲染进 DOM:

    ReactDom.render(<ul>{listLitems}</ul>,document.getElementById('root'))
    

    基础列表组件
    通常你需要在一个组件中渲染列表。

    我们可以把前面的例子重构成一个组件,这个组件接收 numbers 数组作为参数并输出一个元素列表。

    function NumberList(props){
     const numbers = props.numbers
     const listLitems = numbers.map(number=><li key={number.toString()}>{number}</li>)
     return (
         <ul>{listLitems}</ul>
     )
    }
    const numbers = [1,2,3,4,5]
    ReactDOM.render(<NumberList numbers={numbers}/>,document.getElementById('root'))
    
    key

    key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。
    一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key:
    当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key

    如果列表项目的顺序可能会变化,我们不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。可以看看 Robin Pokorny 的深度解析使用索引作为 key 的负面影响这一篇文章。如果你选择不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。

    要是你有兴趣了解更多的话,这里有一篇文章深入解析为什么 key 是必须的可以参考。

    用 key 提取组件
    元素的 key 只有放在就近的数组上下文中才有意义。

    比方说,如果你提取出一个 ListItem 组件,你应该把 key 保留在数组中的这个 元素上,而不是放在 ListItem 组件中的

  • 元素上。
  • function ListItem(props){
        return <li>{props.value}</li>
    }
    function NumberList(props){
        const numbers = props.numbers
        const listItems = numbers.map(number=>
            <ListItem key={number.toString()} value={number}/>
        )
        return (
            <ul>{listItems}</ul>
        )
    }
    
    const numbers = [1,2,3,4,5]
    ReactDOM.render(<NumberList numbers={numbers}/>,document.getElementById('root'))
    

    一个好的经验法则是:在 map() 方法中的元素需要设置 key 属性

    key 只是在兄弟节点之间必须唯一
    数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的 key 值:

    function Blog(props){
        const sildeBar = (
            <ul>{
                props.posts.map(post=>{
                    <li key={post.id}>{post.title}</li>
                })
            }</ul>
        )
        const content = (
            <ul>
                {
                 props.posts.map(post=>{
                     <h1 key={post.id}>{post.title}</h1>
                     <p>{post.content}</p>
                 })
                }      
            </ul>
        )
        return (
            <div>
                {sildeBar}
                <hr />
                {content}
            </div>
        )
    }
    
    const posts = [
      {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
      {id: 2, title: 'Installation', content: 'You can install React from npm.'}
    ];
    ReactDOM.render(<Blog posts={posts}/>,document.getElementById("root"))
    

    key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key 属性的值,请用其他属性名显式传递这个值:

    const content = posts.map(post=>{
        <Post key={post.id} id={post.id} title={post.title}/>
    })
    

    上面例子中,Post 组件可以读出 props.id,但是不能读出 props.key。

    在 JSX 中嵌入 map()
    在上面的例子中,我们声明了一个单独的 listItems 变量并将其包含在 JSX 中:

    function NumberList(props) {
      const numbers = props.numbers;
      const listItems = numbers.map((number) =>
        <ListItem key={number.toString()}
                  value={number} />
      );
      return (
        <ul>
          {listItems}
        </ul>
      );
    }
    

    JSX 允许在大括号中嵌入任何表达式,所以我们可以内联 map() 返回的结果:

    function NumberList(props) {
      const numbers = props.numbers;
      return (
        <ul>
          {numbers.map((number) =>
            <ListItem key={number.toString()}
                      value={number} />
          )}
        </ul>
      );
    }
    

    这么做有时可以使你的代码更清晰,但有时这种风格也会被滥用。就像在 JavaScript 中一样,何时需要为了可读性提取出一个变量,这完全取决于你。但请记住,如果一个 map() 嵌套了太多层级,那可能就是你提取组件的一个好时机。

    表单

    受控组件
    在 HTML 中,表单元素(如、 和 )通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
    React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
    例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写为受控组件:

    class NameForm extends React.Component{
        constructor(props){
            super(props)
            this.state = {value:''}
            this.handleChang= this.handleChang.bind(this)
            this.handleSubmit = this.handleSubmit.bind(this)
        }
        handleChang(e){
            this.setState({
                value:e.target.value
            })
        }
        handleSubmit(event){
            alert('提交的名字是:'+this.state.value)
            event.preventDefault()
        }
        render(){
            return (
                <form onSubmit={this.handleSubmit}>
                    <label>
                        名字
                        <input type="text" value={this.state.value} onChange={this.handleChange}/>
                    </label>
                    <input type="submit" value="提交"/> 
                </form>
            )
        }
    }
    

    由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。由于 handlechange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
    对于受控组件来说,输入的值始终由 React 的 state 驱动。你也可以将 value 传递给其他 UI 元素,或者通过其他事件处理函数重置,但这意味着你需要编写更多的代码。

    textarea 标签

    在 HTML 中, 元素通过其子元素定义其文本:

    <textarea>
      你好, 这是在 text area 里的文本
    </textarea>
    

    而在 React 中, 使用 value 属性代替。这样,可以使得使用 的表单和使用单行 input 的表单非常类似:

    class EssayForm extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          value: '请撰写一篇关于你喜欢的 DOM 元素的文章.'
        };
    
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
      }
    
      handleChange(event) {
        this.setState({value: event.target.value});
      }
    
      handleSubmit(event) {
        alert('提交的文章: ' + this.state.value);
        event.preventDefault();
      }
    
      render() {
        return (
          <form onSubmit={this.handleSubmit}>
            <label>
              文章:
              <textarea value={this.state.value} onChange={this.handleChange} />
            </label>
            <input type="submit" value="提交" />
          </form>
        );
      }
    }
    

    请注意,this.state.value 初始化于构造函数中,因此文本区域默认有初值。

    在 HTML 中, 创建下拉列表标签。例如,如下 HTML 创建了水果相关的下拉列表:

    <select>
      <option value="grapefruit">葡萄柚</option>
      <option value="lime">酸橙</option>
      <option selected value="coconut">椰子</option>
      <option value="mango">芒果</option>
    </select>
    

    请注意,由于 selected 属性的缘故,椰子选项默认被选中。React 并不会使用 selected 属性,而是在根 select 标签上使用 value 属性。这在受控组件中更便捷,因为您只需要在根标签中更新它。例如:

    /*
     * @Author: yang
     * @Date: 2020-12-18 17:46:25
     * @LastEditors: yang
     * @LastEditTime: 2020-12-23 11:44:50
     * @FilePath: \react2\src\test.js
     */
    class NameForm extends React.Component{
        constructor(props){
            super(props)
            this.state = {value:'coconut'}
            this.handleChang= this.handleChang.bind(this)
            this.handleSubmit = this.handleSubmit.bind(this)
        }
        handleChang(e){
            this.setState({
                value:e.target.value
            })
        }
        handleSubmit(event){
            alert('你喜欢的风味是:'+this.state.value)
            event.preventDefault()
        }
        render(){
            return (
                <form onSubmit={this.handleSubmit}>
                    <label>
                        选择你喜欢的风味:
                        <select value={this.state.value} onChange={this.handleChang}>
                         <option value="grapefruit">葡萄柚</option>
                         <option value="lime">酸橙</option>
                         <option value="coconut">椰子</option>
                         <option value="mango">芒果</option>
                        </select>
                    </label>
                    <input type="submit" value="提交"/> 
                </form>
            )
        }
    }
    

    总的来说,这使得 , 和 之类的标签都非常相似—它们都接受一个 value 属性,你可以使用它来实现受控组件。

    你可以将数组传递到 value 属性中,以支持在 select 标签中选择多个选项:

    <select mutiple={true} value={['B','C']}/>
    
    文件 input 标签

    在 HTML 中, 允许用户从存储设备中选择一个或多个文件,将其上传到服务器,或通过使用 JavaScript 的 File API (https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications)进行控制。

    <input type="file" />
    

    因为它的 value 只读,所以它是 React 中的一个非受控组件。

    处理多个输入

    当需要处理多个 input 元素时,我们可以给每个元素添加 name 属性,并让处理函数根据 event.target.name 的值选择要执行的操作。

    例如:

    class Reservation extends React.Component{
        constructor(props){
            super(props)
            this.state = {
                isGoing:true,
                numberOfGuess:2
            }
            this.handleInputChange = this.handleInputChange.bind(this)
        }
        handleInputChange(event){
            const target = event.target;
            const value = target.type === 'checkbox'? target.checked:target.value
            const name = target.name
            this.setState({
                [name]:value
            })
        }
        render(){
            return(
                <form>
                    <label>
                        参与:
                        <input name="isGoing" type="checkbox" checked={this.state.isGoing} onChange={this.handleInputChange}/>
                    </label>
                    <br/>
                    <label>
                        来宾人数:
                        <input  type="number" name="numberOfGuests" value={this.state.numberOfGuess} onChange={this.handleInputChange}/>
                    </label>
                </form>
            )
        }
    }
    

    这里使用了 ES6 计算属性名称的语法更新给定输入名称对应的 state 值:

    this.setState({
      [name]: value
    });
    等同 ES5:
    
    var partialState = {};
    partialState[name] = value;
    this.setState(partialState);
    

    另外,由于 setState() 自动将部分 state 合并到当前 state, 只需调用它更改部分 state 即可。

    受控输入空值

    在受控组件上指定 value 的 prop 会阻止用户更改输入。如果你指定了 value,但输入仍可编辑,则可能是你意外地将value 设置为 undefined 或 null。
    下面的代码演示了这一点。(输入最初被锁定,但在短时间延迟后变为可编辑。)

    RreactDOM.render(<input value="hi"/>,mountNode)
    
    setTimeout(() => {
        RreactDOM.render(<input value={null}/>,mountNode)
    }, 1000);
    
    
    受控组件的替代品

    有时使用受控组件会很麻烦,因为你需要为数据变化的每种方式都编写事件处理函数,并通过一个 React 组件传递所有的输入 state。当你将之前的代码库转换为 React 或将 React 应用程序与非 React 库集成时,这可能会令人厌烦。在这些情况下,你可能希望使用非受控组件, 这是实现输入表单的另一种方式。

    成熟的解决方案

    如果你想寻找包含验证、追踪访问字段以及处理表单提交的完整解决方案,使用 Formik(https://formik.org/) 是不错的选择。然而,它也是建立在受控组件和管理 state 的基础之上 —— 所以不要忽视学习它们。

    状态提升

    通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。

    创建一个用于计算水在给定温度下是否会沸腾的温度计算器。

    我们将从一个名为 BoilingVerdict 的组件开始,它接受 celsius 温度作为一个 prop,并据此打印出该温度是否足以将水煮沸的结果。

    function BoilingVerdict(props){
        if(props.celsius >=100){
            return <p> The water would boli</p>
        }else{
            return <p>The water would not boil</p>
        }
    }
    

    创建一个名为 Calculator 的组件。它渲染一个用于输入温度的 ,并将其值保存在 this.state.temperature 中。

    另外, 它根据当前输入值渲染 BoilingVerdict 组件。

    class Calculator extends React.Component {
        constructor(props){
            super(props)
            this.handleChange = this.handleChange.bind(this)
            this.state = {
                temperature:''
            }
        }
        handleChange(e){
            this.setState({
                temperature:e.target.value
            })
        }
        render(){
            const temperature = this.state.temperature
            return (
                <filedset>
                    <legend>Enter temperature in Celsius</legend>
                    <input value={temperature} onChange={this.handleChange}/>
                    <BoilingVerdict celsius={parseFloat(temperature)}/>
                </filedset>
            )
        }
    }
    
    

    添加第二个输入框

    新需求是,在已有摄氏温度输入框的基础上,我们提供华氏度的输入框,并保持两个输入框的数据同步。

    我们先从 Calculator 组件中抽离出 TemperatureInput 组件,然后为其添加一个新的 scale prop,它可以是 “c” 或是 “f”:

    const scaleNames = {
        c:'Celsius',
        f:'Fahrenheit'
    }
    
    class TemperatureInput extends React.Component{
        constructor(props){
            super(props)
            this.handleChangeTempeture = this.handleChangeTempeture.bind(this)
            this.state = {
                temperture:''
            }
        }
        handleChangeTempeture(e){
            this.setState({
                temperture:e.target.value
            })
        }
        render(){
            return(
                const temperture = this.state.temperture
                const scale = this.props.scale
            <fieldset>
                <legend>
                Enter the temperture in {scaleNames[scale]}:
                </legend>
                <input value={temperture} onChange={this.handleChangeTempeture}/>
            </fieldset>
            )
        }
    }
    

    我们现在可以修改 Calculator 组件让它渲染两个独立的温度输入框组件:

    class Calculator extends React.Component{
        render(){
            return (
                <div>
                    <TemperatureInput scale="c"/> 
                    <TemperatureInput scale="f"/> 
                </div>
            )
        }
    }
    
    

    现在有了两个输入框,但当你在其中一个输入温度时,另一个并不会更新。这与我们的要求相矛盾:我们希望让它们保持同步。

    另外,我们也不能通过 Calculator 组件展示 BoilingVerdict 组件的渲染结果。因为 Calculator 组件并不知道隐藏在 TemperatureInput 组件中的当前温度是多少。

    编写转换函数
    首先,我们将编写两个可以在摄氏度与华氏度之间相互转换的函数:

    function toCelsiud(fahrenheit){
        return (fahrenheit-32)*5/9
    }
    
    function toFahrenheit(celsius){
        return (celsius*9/5) + 32
    }
    

    上述两个函数仅做数值转换。而我们将编写另一个函数,它接受字符串类型的 temperature 和转换函数作为参数并返回一个字符串。我们将使用它来依据一个输入框的值计算出另一个输入框的值。
    当输入 temperature 的值无效时,函数返回空字符串,反之,则返回保留三位小数并四舍五入后的转换结果:

    function tryConvert(temperature,convert){
        const input = parseFloat(temperature)
        if(Number.isNaN(input)){return ''}
        const output = convert(input)
        const rounded = Math.round(output*1000)/1000
        return rounded.toString()
    }
    

    例如,tryConvert(‘abc’, toCelsius) 返回一个空字符串,而 tryConvert(‘10.22’, toFahrenheit) 返回 ‘50.396’。
    状态提升
    到目前为止, 两个 TemperatureInput 组件均在各自内部的 state 中相互独立地保存着各自的数据。
    然而,我们希望两个输入框内的数值彼此能够同步。当我们更新摄氏度输入框内的数值时,华氏度输入框内应当显示转换后的华氏温度,反之亦然。
    在 React 中,将多个组件中需要共享的 state 向上移动到它们的最近共同父组件中,便可实现共享 state。这就是所谓的“状态提升”。接下来,我们将 TemperatureInput 组件中的 state 移动至 Calculator 组件中去。

    如果 Calculator 组件拥有了共享的 state,它将成为两个温度输入框中当前温度的“数据源”。它能够使得两个温度输入框的数值彼此保持一致。由于两个 TemperatureInput 组件的 props 均来自共同的父组件 Calculator,因此两个输入框中的内容将始终保持一致.

    首先,我们将 TemperatureInput 组件中的 this.state.temperature 替换为 this.props.temperature。现在,我们先假定 this.props.temperature 已经存在,尽管将来我们需要通过 Calculator 组件将其传入:

    render() {
        // Before: const temperature = this.state.temperature;
        const temperature = this.props.temperature;
        // ...
    

    我们知道 props 是只读的。当 temperature 存在于 TemperatureInput 组件的 state 中时,组件调用 this.setState() 便可修改它。然而,temperature 是由父组件传入的 prop,TemperatureInput 组件便失去了对它的控制权。

    在 React 中,这个问题通常是通过使用“受控组件”来解决的。与 DOM 中的 接受 value 和 onChange 一样,自定义的 TemperatureInput 组件接受 temperature 和 onTemperatureChange 这两个来自父组件 Calculator 的 props。

    现在,当 TemperatureInput 组件想更新温度时,需调用

    this.props.onTemperatureChange 来更新它:
    handleChange(e) {
        // Before: this.setState({temperature: e.target.value});
        this.props.onTemperatureChange(e.target.value);
        // ...
    

    自定义组件中的 temperature 和 onTemperatureChange 这两个 prop 的命名没有任何特殊含义。我们可以给它们取其它任意的名字,例如,把它们命名为 value 和 onChange 就是一种习惯。

    onTemperatureChange 的 prop 和 temperature 的 prop 一样,均由父组件 Calculator 提供。它通过修改父组件自身的内部 state 来处理数据的变化,进而使用新的数值重新渲染两个输入框。我们将很快看到修改后的 Calculator 组件效果。

    在深入研究 Calculator 组件的变化之前,让我们回顾一下 TemperatureInput 组件的变化。我们移除组件自身的 state,通过使用 this.props.temperature 替代 this.state.temperature 来读取温度数据。当我们想要响应数据改变时,我们需要调用 Calculator 组件提供的 this.props.onTemperatureChange(),而不再使用 this.setState()。

    class TemperatureInput extends React.Component{
        constructor(props){
            super(props)
            this.handleChange = this.handleChange.bind(this)
        }
        handleChange(e){
            this.props.changeTemperature(e.target.value)
        }
        return(){
            const temperature = this.props.temperature
            const scale = this.props.scale
            return(
                <fieldset>
                    <legend>Enter temperature in {scaleNames[scale]}</legend>
                    <input value={temperature} onChange={this.handleChange}>
                </fieldset>
            )
        }
    }
    
    

    现在,让我们把目光转向 Calculator 组件。

    我们会把当前输入的 temperature 和 scale 保存在组件内部的 state 中。这个 state 就是从两个输入框组件中“提升”而来的,并且它将用作两个输入框组件的共同“数据源”。这是我们为了渲染两个输入框所需要的所有数据的最小表示。

    例如,当我们在摄氏度输入框中键入 37 时,Calculator 组件中的 state 将会是:

    我们可以存储两个输入框中的值,但这并不是必要的。我们只需要存储最近修改的温度及其计量单位即可,根据当前的 temperature 和 scale 就可以计算出另一个输入框的值。

    由于两个输入框中的数值由同一个 state 计算而来,因此它们始终保持同步:

    class Calculatar extends React.Component{
        constructor(props){
            super(props)
            this.state = {
                temperature:'',
                scale:'c'
            }
            this.handleCelsiusChange = this.handleCelsiusChange.bind(this)
            this.handleFarenheitChange = this.handleFarenheitChange.bind(this)
        }
        handleCelsiusChange(temperature){
            this.setState({
                scale:'c',
                temperature
            })
        }
        handleFarenheitChange(temperature){
            this.setState({
                scale:'f',
                temperature
            })
        }
        render(){
            const temperature = this.state.temperature
            const scale = this.state.scale
            const celsius = scale === 'f'?tryConvert(temperature,toCelsius):temperature
            const fahreheit = scale === 'c'?tryConvert(temperature,toFarenheit):temperature
            return(
                <div>
                    <TemperatureInput scale="c" onTemperatureChange={this.handleCelsiusChange} temperature={celsius}/>
                    <TemperatureInput scale="f" onTemperatureChange={this.handleFarenheitChange} temperature={fahreheit}/>
                     <BoilingVerdict celsius={parseFloat(celsius)}>
                </div>
            )
        }
    }
    
    
    

    现在无论你编辑哪个输入框中的内容,Calculator 组件中的 this.state.temperature 和 this.state.scale 均会被更新。其中一个输入框保留用户的输入并取值,另一个输入框始终基于这个值显示转换后的结果。

    梳理一下当你对输入框内容进行编辑时会发生些什么:

    • React 会调用 DOM 中 的 onChange 方法。在本实例中,它是 TemperatureInput 组件的 handleChange 方法。
      TemperatureInput 组件中的 handleChange 方法会调用 this.props.onTemperatureChange(),并传入新输入的值作为参数。其 props 诸如 onTemperatureChange 之类,均由父组件 Calculator 提供。
      起初渲染时,用于摄氏度输入的子组件 TemperatureInput 中的 onTemperatureChange 方法与 Calculator 组件中的 handleCelsiusChange 方法相同,而,用于华氏度输入的子组件 TemperatureInput 中的 onTemperatureChange 方法与 Calculator 组件中的 handleFahrenheitChange 方法相同。因此,无论哪个输入框被编辑都会调用 Calculator 组件中对应的方法。
      在这些方法内部,Calculator 组件通过使用新的输入值与当前输入框对应的温度计量单位来调用 this.setState() 进而请求 React 重新渲染自己本身。
      React 调用 Calculator 组件的 render 方法得到组件的 UI 呈现。温度转换在这时进行,两个输入框中的数值通过当前输入温度和其计量单位来重新计算获得。
      React 使用 Calculator 组件提供的新 props 分别调用两个 TemperatureInput 子组件的 render 方法来获取子组件的 UI 呈现。
      React 调用 BoilingVerdict 组件的 render 方法,并将摄氏温度值以组件 props 方式传入。
      React DOM 根据输入值匹配水是否沸腾,并将结果更新至 DOM。我们刚刚编辑的输入框接收其当前值,另一个输入框内容更新为转换后的温度值。

    得益于每次的更新都经历相同的步骤,两个输入框的内容才能始终保持同步。

    在 React 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。

    虽然提升 state 方式比双向绑定方式需要编写更多的“样板”代码,但带来的好处是,排查和隔离 bug 所需的工作量将会变少。由于“存在”于组件中的任何 state,仅有组件自己能够修改它,因此 bug 的排查范围被大大缩减了。此外,你也可以使用自定义逻辑来拒绝或转换用户的输入。

    如果某些数据可以由 props 或 state 推导得出,那么它就不应该存在于 state 中。举个例子,本例中我们没有将 celsiusValue 和 fahrenheitValue 一起保存,而是仅保存了最后修改的 temperature 和它的 scale。这是因为另一个输入框的温度值始终可以通过这两个值以及组件的 render() 方法获得。这使得我们能够清除输入框内容,亦或是,在不损失用户操作的输入框内数值精度的前提下对另一个输入框内的转换数值做四舍五入的操作。

    当你在 UI 中发现错误时,可以使用 React 开发者工具 来检查问题组件的 props,并且按照组件树结构逐级向上搜寻,直到定位到负责更新 state 的那个组件。这使得你能够追踪到产生 bug 的源头:
    在这里插入图片描述

    组合 vs 继承

    React 有十分强大的组合模式。我们推荐使用组合而非继承来实现组件间的代码重用。

    包含关系

    有些组件无法提前知晓它们子组件的具体内容。在 Sidebar(侧边栏)和 Dialog(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。

    我们建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中:

    function FancyBorder(props){
        return (
            <div className={'FancyBorder FancyBorder-' + props.color}>
                {props.children}
            </div>
        )
    }
    

    这使得别的组件可以通过 JSX 嵌套,将任意组件作为子组件传递给它们。

    function WelcomeDialog(){
        return (
            <FancyBorder color="blue">
                <h1 className="Dialog-title">
                  Welcome
                </h1>
                <p className="Dialog-message">
                   Thank you for visiting our spacecraft!
                </p>
            </FancyBorder>
        )
    }
    

    JSX 标签中的所有内容都会作为一个 children prop 传递给 FancyBorder 组件。因为 FancyBorder 将 {props.children} 渲染在一个

    中,被传递的这些子组件最终都会出现在输出结果中。

    少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children,而是自行约定:将所需内容传入 props,并使用相应的 prop。

    function SplitPane(props){
        return(
            <div className="SplitPane">
              <div className="SplitPane-left">{props.left}</div>
              <div className="SplitPane-right">{props.right}</div>
            </div>
        )
    }
    
    function App(){
        return (
            <SplitPane left={<Contacts/>} right={<Chat/>}/>
        )
    }
    

    和 之类的 React 元素本质就是对象(object),所以你可以把它们当作 props,像其他数据一样传递。这种方法可能使你想起别的库中“槽”(slot)的概念,但在 React 中没有“槽”这一概念的限制,你可以将任何东西作为 props 进行传递。

    特例关系
    有些时候,我们会把一些组件看作是其他组件的特殊实例,比如 WelcomeDialog 可以说是 Dialog 的特殊实例。

    在 React 中,我们也可以通过组合来实现这一点。“特殊”组件可以通过 props 定制并渲染“一般”组件:

    function Dialog(props){
        return (
            <FancyBorder color="blue">
               <h1 className="Dialog-title">{props.title}</h1>
               <p className="Dialog-message">{props.message}</p>
            </FancyBorder> 
        )
    }
    
    function WelcomeDialog(){
        return(
            <Dialog title="Welcome" message="Thank you for visiting our spacecraft"/>
        )
    }
    

    组合也同样适用于以 class 形式定义的组件

    function Dialog(props){
        return (
            <FancyBorder color="blue">
                <h1 className="Dialog-title">{props.title}</h1>
                <p className="Dialog-message">{props.message}</p>
                {props.children}
            </FancyBorder>
        )
    }
    
    class SignUpDialog extends React.Component {
        constructor(props){
            super(props)
            this.handleChange = this.handleChange.bind(this)
            this.handleSignUp = this.handleSignUp.bind(this)
            this.state = {login:''}
        }
        handleSignUp(){
            alert(`Welocome abord,${this.state.login}!`)
        }
        handleChange(e){
            this.setState({
                login:e.target.value
            })
        }
        render(){
            return (
                <Dialog title="Mars Exploration Program" message="How should we refer to you?">
                    <input value="this.state.login" onChange={this.handleChange}>
                    <button onClick={this.handleSignUp}> 
                        Sign Me Up
                    </button>
                </Dialog>
            )
        }
    }
    

    Props 和组合为你提供了清晰而安全地定制组件外观和行为的灵活方式。注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。

    如果你想要在组件间复用非 UI 的功能,我们建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。

    无障碍辅助功能

    网络无障碍辅助功能 是一种可以帮助所有人获得服务的设计和创造。无障碍辅助功能是使得辅助技术正确解读网页的必要条件。
    React 对于创建可访问网站有着全面的支持,而这通常是通过标准 HTML 技术实现的。

    标准和指南

    网络内容无障碍指南(Web Content Accessibility Guidelines,WCAG)
    https://www.w3.org/WAI/standards-guidelines/wcag/ 为开发无障碍网站提供了指南。
    WCAG
    下面的 WCAG 检查表提供了一些概览:

    Wuhcag 提供的 WCAG 检查表(WCAG checklist from Wuhcag)(https://www.wuhcag.com/wcag-checklist/)

    WebAIM 提供的 WCAG 检查表(WCAG checklist from WebAIM)(https://webaim.org/standards/wcag/checklist)
    A11Y Project 提供的检查表(Checklist from The A11Y Project)(https://a11yproject.com/checklist.html)

    WAI-ARIA
    网络无障碍倡议 - 无障碍互联网应用(Web Accessibility Initiative - Accessible Rich Internet Applications) 文件包含了创建完全无障碍 JavaScript 部件所需要的技术。
    https://www.w3.org/WAI/standards-guidelines/aria/

    注意:JSX 支持所有 aria-* HTML 属性。虽然大多数 React 的 DOM 变量和属性命名都使用驼峰命名(camelCased),但 aria-* 应该像其在 HTML 中一样使用带连字符的命名法(也叫诸如 hyphen-cased,kebab-case,lisp-case)。

    <input
      type="text"
      aria-label={labelText}
      aria-required="true"
      onChange={onchangeHandler}
      value={inputValue}
      name="name"
    />
    
    语义化的 HTML

    语义化的 HTML 是无障碍辅助功能网络应用的基础。 利用多种 HTML 元素来强化您网站中的信息通常可以使您直接获得无障碍辅助功能。

    MDN 的 HTML 元素参照(MDN HTML elements reference)(https://developer.mozilla.org/en-US/docs/Web/HTML/Element)
    有时,语义化的 HTML 会被破坏。比如当在 JSX 中使用

    元素来实现 React 代码功能的时候,又或是在使用列表(
      • )和 HTML 时。 在这种情况下,我们应该使用 React Fragments 来组合各个组件

    import React,{Fragment} from 'react'
    
    function ListItem({item}){
        return (
            <Fragment>
              <dt>{item.term}</dt>
              <dd>{item.description}</dd>
            </Fragment>
        )
    }
    
    function Glossary(props){
        return (
           <dl>
             {props.items.map(item=>(
                 <ListItem item={item} key={item.id}>
             ))}
           </dl>
        )
    }
    

    和其他的元素一样,你可以把一系列的对象映射到一个 fragment 的数组中

    function Glossary(props){
        return (
            <dl>
                {props.items.map(item=>(
                    <Fragment>
                      <dt>{item.term}</dt>
                      <dd>{item.description}</dd>
                    </Fragment>
                ))}
            </dl>
        )
    }
    

    当你不需要在 fragment 标签中添加任何 prop 且你的工具支持的时候,你可以使用 短语法:

    function ListItem({item}){
        return(
            <>
                <dt>{item.term}</dt>
                <dd>{item.description}</dd>
            </>
        )
    }
    
    无障碍表单

    标记
    所有的 HTML 表单控制,例如 和 ,都需要被标注来实现无障碍辅助功能。我们需要提供屏幕朗读器以解释性标注。
    以下资源向我们展示了如何写标注:

    W3C 向我们展示如何标注元素
    https://www.w3.org/WAI/tutorials/forms/labels/
    WebAIM 向我们展示如何标注元素
    https://webaim.org/techniques/forms/controls
    Paciello Group 解释什么是无障碍名称
    https://developer.paciellogroup.com/blog/2017/04/what-is-an-accessible-name/

    尽管这些标准 HTML 实践可以被直接用在 React 中,请注意 for 在 JSX 中应该被写作 htmlFor:

    <label htmlFor="nameInput">Name:</label>
    <input id="nameInput" type="text" name="name"/>
    

    在出错时提醒用户
    当出现错误时,所有用户都应该知情。下面的链接告诉我们如何给屏幕朗读器设置错误信息:

    W3C 展示用户推送
    https://www.w3.org/WAI/tutorials/forms/notifications/
    WebAIM 关于表单校验的文章
    https://webaim.org/techniques/formvalidation/

    控制焦点

    确保你的网络应用在即使只拥有键盘的环境下正常运作。
    WebAIM 讨论使用键盘进行无障碍访问
    https://webaim.org/techniques/keyboard/

    键盘焦点及焦点轮廓

    键盘焦点的定义是:在 DOM 中,当前被选中来接受键盘信息的元素。我们可以在各处看到键盘焦点,它会被焦点轮廓包围,像下面的这个图像一样。
    在这里插入图片描述

    选中的链接被蓝色键盘焦点轮廓包围着。
    请不要使用 CSS 移除这个轮廓,比如设置 outline: 0,除非你将使用其他的方法实现焦点轮廓。

    跳过内容机制
    为了帮助和提速键盘导航,我们提供了一种机制,可以帮助用户跳过一些导航段落。

    跳转链接(Skiplinks),或者说跳转导航链接(Skip Navigation Links)是一种隐藏的导航链接,它只会在使用键盘导航时可见。使用网页内部锚点和一些式样可以很容易地实现它:

    WebAIM - 跳转导航链接(Skip Navigation Links)
    https://webaim.org/techniques/skipnav/
    另外,使用地标元素和角色,比如 和

    你可以通过下面的链接了解更多如何使用这些元素来增强无障碍辅助功能:

    无障碍地标
    https://www.scottohara.me/blog/2018/03/03/landmarks.html

    使用程序管理焦点

    我们的 React 应用在运行时会持续更改 HTML DOM,有时这将会导致键盘焦点的丢失或者是被设置到了意料之外的元素上。为了修复这类问题,我们需要以编程的方式让键盘聚焦到正确的方向上。比方说,在一个弹窗被关闭的时候,重新设置键盘焦点到弹窗的打开按钮上。

    MDN Web 文档关注了这个问题并向我们解释了可以如何搭建可用。
    键盘导航的 JavaScript 部件

    我们可以用 DOM 元素的 Refs 在 React 中设置焦点
    用以上技术,我们先在一个 class 组件的 JSX 中创建一个元素的 ref:

    class CustomTextInput extends React.Component{
        constructor(props){
            super(props)
            //创造一个textInput DOM元素的ref
            this.textInput = React.createRef()
        }
        render(){
            //使用ref 回调函数以在实例的一个变量中存储文本输入DOM元素
            //比如 this.textInput
            return(
                <input type="text" ref={this.textInput}/>
            )
        }
    }
    

    然后我们就可以在需要时于其他地方把焦点设置在这个组件上

    focus(){
        //使用原始的DOM API显式地聚焦在text input 上
        //注意 我们通过访问‘current’来获得dom节点
        this.textInput.current.focus()
    }
    

    有时,父组件需要把焦点设置在其子组件的一个元素上。
    我们可以通过在子组件上设置一个特殊的 prop
    来对父组件暴露 DOM refs 从而把父组件的 ref 传向子节点的 DOM 节点

    function CustomTextInput(props){
        return (
            <div>
                <input ref={props.inputRef}/>
            </div>
        )
        
    }
    
    class Parent extends React.Componennt{
        constructor(props){
            super(props)
            this.inputElement = React.createRef()
        }
        render(){
            return (
                <CustomTextInput inputRef={this.inputElement}/>
            )
        }
    }
    
    //现在你就可以在需要时设置焦点了
    this.inputElement.current.focus()
    

    当使用 HOC 来扩展组件时,我们建议使用 React 的 forwardRef 函数来向被包裹的组件转发 ref。如果第三方的 HOC 不支持转发 ref,上面的方法仍可以作为一种备选方案。

    react-aria-modal 提供了一个很好的焦点管理的例子。 这是一个少有的完全无障碍的模态窗口的例子。它不仅仅把初始焦点设置在了取消按钮上(防止键盘用户意外激活成功操作)和把键盘焦点固定在了窗口之内, 关闭窗口时它也会把键盘焦点重置到打开窗口的那一个元素上。

    虽然这是一个非常重要的无障碍辅助功能,但它也是一种应该谨慎使用的技术。 我们应该在受到干扰时使用它来修复键盘焦点,而不是试图预测用户想要如何使用应用程序。

    鼠标和指针事件

    确保任何可以使用鼠标和指针完成的功能也可以只通过键盘完成。只依靠指针会产生很多使键盘用户无法使用你的应用的情况。

    为了说明这一点,让我们看一下由点击事件引起的破坏无障碍访问的典型示例:外部点击模式,用户可以通过点击元素以外的地方来关闭已打开的弹出框。

    通常实现这个功能的方法是在 window 对象中附上一个 click 事件以关闭弹窗:

    class OuterClickExample extends React.Component{
        constructor(props){
            super(props)
            this.toggleContainer = React.createRef()
            this.onClickHandler = this.onClickHandler.bind(this)
            this.onClickOutSideHandler = this.onClickOutSideHandler.bind(this)
        }
        componentDidMount(){
            window.addEventListener('click',this.onClickOutSideHandler)
        }
        componentWillUnmount(){
            window.addEventListener('click',this.onClickOutSideHandler)
        }
        onClickHandler(){
            this.setState((currentState)=>({
                isOpen:!currentState.isOpen
            }))
        }
        onClickOutSideHandler(event){
            // contains:就是判断某个元素是不是选定元素的子元素(或本身);
            // https://www.w3schools.com/Jsref/met_node_contains.asp
            if(this.state.isOpen&&!this.toggleContainer.current.contains(event.target)){
                this.setState({
                    isOpen:false
                })
            }
        }
    
        render(){
            return(
                <div ref={this.toggleContainer}>
                    <button onClick={this.onClickHandler}>Select an option</button>
                    {this.state.isOpen&&(
                        <ul>
                            <li>Option1</li>
                            <li>Option2</li>
                            <li>Option3</li>
                        </ul>
                    )}
                </div>
            )
        }
    }
    

    当用户使用指针设备,比如鼠标时,这样做没有问题。但是当只使用键盘时,因为 window 对象不会接受到 click 事件,用户将无法使用 tab 切换到下一个元素。这样会导致用户无法使用你应用中的一些内容,导致不完整的用户体验。
    使用正确的事件触发器,比如 onBlur 和 onFocus,同样可以达成这项功能:

    class BlurExample extends React.Component{
        constructor(props){
            super(props)
             this.state = {isOpen: false}
            this.timed = null
            this.onClickHandler = this.onClickHandler.bind(this)
            this.onBlurHandler = this.onBlurHandler.bind(this)
            this.onFocusHandler = this.onFocusHandler.bind(this)
           
        }
        onClickHandler(e){
            this.setState((state)=>({
                isOpen:!state.isOpen
            }))
        }
        // 我们在下一个时间点使用setTimeout关闭弹框
        // 这是必要的 因为失去焦点事件会在新的焦点事件前触发
        //我们需要通过这个步骤确认这个元素的一个子节点
        // 是否得到了焦点
        onBlurHandler(){
            this.timeOutId = setTimeout(() => {
                this.setState({
                    isOpen:false
                })
            });
        }
    
        //如果一个子节点获得了焦点  不要关闭弹框
        onFocusHandler(){
            clearTimeOut(this.timeOutId)
        }
        render(){
            //React 通过把失去焦点和获得焦点事件传输给父节点 来帮助我们
            return(
                <div onBlur={this.onBlurHandler} onFocus={this.onFocusHandler}>
                {/* aria-haspopup :true表示点击的时候会出现菜单或是浮动元素; false表示没有pop-up效果。
    aria-expanded:表示展开状态。 */}
                    <button onClick={this.onClickHandler} aria-haspopup="true" aria-expanded={this.state.isOpen}>slect an Option</button>
                    {this.state.isOpen&&(
                        <ul>
                            <li>option1</li>
                            <li>option2</li>
                            <li>option3</li>
                        </ul>
                    )}
                </div>
            )
        }
    }
    

    以上代码使得键盘和鼠标用户都可以使用我们的功能。请注意我们添加了 aria-* props 以服务屏幕朗读器用户。作为一个简单的例子,我们没有实现使用方向键来与弹窗互动。
    这只是众多只依赖于鼠标和指针的程序破坏键盘用户的例子之一。始终使用键盘测试会让你迅速发现这些问题,你可以使用适用于键盘的事件处理器来修复这些问题。

    更复杂的部件

    一个更加复杂的用户体验并不意味着更加难以访问。通过尽可能接近 HTML 编程,无障碍访问会变得更加容易,即使最复杂的部件也可以实现无障碍访问。
    这里我们需要了解
    ARIA Roles ARIA States and Properties
    的知识。 其中有包含了多种 HTML 属性的工具箱,这些 HTML 属性被 JSX 完全支持并且可以帮助我们搭建完全无障碍,功能强大的 React 组件。

    每一种部件都有一种特定的设计模式,并且用户和用户代理都会期待使用相似的方法使用它:
    WAI-ARIA 创作实践 —— 设计模式和部件

    Heydon Pickering - ARIA Examples

    包容性组件(Inclusive Components)

    其他考虑因素

    设置语言
    为了使屏幕朗读器可以使用正确的语音设置,请在网页上设置正确的人类语言:

    WebAIM —— 文档语言

    设置文档标题
    为了确保用户可以了解当前网页的内容,我们需要把文档的 设置为可以正确描述当前页面的文字。

    WCAG —— 理解文档标题的要求
    在 React 中,我们可以使用 React 文档标题组件(React Document Title Component)来设置标题

    色彩对比度
    为了尽可能让视力障碍用户可以阅读你网站上的所有可读文字,请确保你的文字都有足够的色彩对比度。
    WCAG —— 理解色彩对比度要求

    有关色彩对比度的一切以及为何你应该重新考虑它
    A11yProject —— 什么是色彩对比度

    手工计算你网站上所有恰当的色彩组合会是乏味的。所以,作为代替,你可以使用
    Colorable 来计算出一个完全无障碍的调色板。

    下面介绍的 aXe 和 WAVE 都支持色彩对比度测试并会报告对比度错误。

    如果你想扩展对比度测试能力,可以使用以下工具:

    WebAIM —— 色彩对比度检验工具

    The Paciello Group —— 色彩对比度分析工具

    开发及测试
    我们可以利用很多工具来帮助我们创建无障碍的网络应用。

    键盘
    最最简单也是最最重要的检测是确保你的整个网站都可以被只使用键盘的用户使用和访问。你可以通过如下步骤进行检测:

    断开鼠标
    使用 Tab 和 Shift+Tab 来浏览。
    使用 Enter 来激活元素。
    当需要时,使用键盘上的方向键来和某些元素互动,比如菜单和下拉选项。

    开发辅助
    我们可以直接在 JSX 代码中检测一些无障碍复制功能。通常支持 JSX 的 IDE 会针对 ARIA roles,states 和 properties 提供智能检测。我们也可以使用以下工具:

    eslint-plugin-jsx-a11y
    ESLint 中的 eslint-plugin-jsx-a11y 插件为你的 JSX 中的无障碍问题提供了 AST 的语法检测反馈。许多 IDE 都允许你把这些发现直接集成到代码分析和源文件窗口中。

    Create React App中使用了这个插件中的一部分规则。如果你想启用更多的无障碍规则,你可以在项目的根目录中创建一个有如下内容的 .eslintrc 文件:

    {
      "extends": ["react-app", "plugin:jsx-a11y/recommended"],
      "plugins": ["jsx-a11y"]
    }
    
    

    在浏览器中测试无障碍辅助功能
    已有很多工具可以在您的浏览器内进行网页的无障碍性验证。 因为它们只能检测你 HTML 的技术无障碍性,所以请将它们与这里提到的无障碍检测工具一起使用。

    aXe,aXe-core 以及 react-axe
    Deque 系统提供了 aXe-core 以对你的应用进行自动及端至端无障碍性测试。这个组件包含了对 Selenium 的集成。

    无障碍访问引擎(The Accessibility Engine),简称 aXe,是一个基于 aXe-core 的无障碍访问性检测器。

    在开发和 debug 时,你也可以使用 react-axe 组件直接把无障碍访问的发现显示在控制台中。

    WebAIM WAVE
    网络无障碍性评估工具(Web Accessibility Evaluation Tool)也是一个无障碍辅助的浏览器插件。

    无障碍辅助功能检测器和无障碍辅助功能树
    无障碍辅助功能树是 DOM 树的一个子集, 其中包含了所有 DOM 元素中应该被暴露给无障碍辅助技术(比如屏幕朗读器)的无障碍辅助对象。

    在一些浏览器中,我们可以在无障碍辅助功能树中轻松的看到每个元素的无障碍辅助功能信息:
    在 Firefox 中使用无障碍辅助功能检测器
    在 Chrome 中激活无障碍辅助功能检测器
    在 OS X Safari 中使用无障碍辅助功能检测器
    屏幕朗读器
    使用屏幕朗读器测试应该是你无障碍辅助功能测试的一部分。

    请注意,浏览器与屏幕朗读器的组合很重要。我们建议在最适用于你的屏幕朗读器的浏览器中测试你的应用。

    常用屏幕朗读器
    火狐中的 NVDA
    NonVisual Desktop Access,简称 NVDA,是一个被广泛使用的 Windows 开源屏幕朗读器。

    想要了解怎么样最好的使用 NVDA,请参考下面的指南:

    WebAIM —— 使用 NVD A来评估网络的可无障碍访问性
    Deque —— NVDA 键盘快捷键
    Safari 中的 VoiceOver
    VoiceOver 是苹果设备的自带屏幕朗读器。

    想要了解如何激活以及使用 VoiceOver,请参考下面的指南:

    WebAIM —— 使用 VoiceOver 来评估网络的可无障碍访问性
    Deque —— OS X 中的 VoiceOver 键盘快捷键
    Deque —— iOS 中的 VoiceOver 快捷键
    Internet Explorer 中的 JAWS
    Job Access With Speech又称 JAWS,是一个常用的 Windows 屏幕朗读器。

    想要了解如何最好的使用 VoiceOver,请参考下面的指南:

    WebAIM —— 使用 JAWS 来评估网络的可无障碍访问性
    Deque —— JAWS 键盘快捷键

    其他屏幕朗读器
    Google Chrome 中的 ChromeVox
    ChromeVox是 Chromebook 的内置屏幕朗读器,同时也是 Google Chrome 中的一个插件。

    想要了解如何最好的使用 ChromeVox,请参考下面的指南:

    Google Chromebook 帮助 —— 使用内置屏幕朗读器
    ChromeVox 经典键盘快捷键参考

    代码分割

    打包
    大多数 React 应用都会使用 Webpack,Rollup 或 Browserify 这类的构建工具来打包文件。 打包是一个将文件引入并合并到一个单独文件的过程,最终形成一个 “bundle”。 接着在页面上引入该 bundle,整个应用即可一次性加载。

    示例
    App文件:
    // app.js

    import { add } from './math.js';
    
    console.log(add(16, 26)); // 42
    // math.js
    export function add(a, b) {
      return a + b;
    }
    

    打包后文件:

    function add(a, b) {
      return a + b;
    }
    
    console.log(add(16, 26)); // 42
    

    最终你的打包文件看起来会和上面的例子区别很大。

    如果你正在使用 Create React App,Next.js,Gatsby,或者类似的工具,你会拥有一个可以直接使用的 Webpack 配置来进行打包工作。

    如果你没有使用这类工具,你就需要自己来进行配置。例如,查看 Webpack 文档上的安装和入门教程。

    代码分割

    打包是个非常棒的技术,但随着你的应用增长,你的代码包也将随之增长。尤其是在整合了体积巨大的第三方库的情况下。你需要关注你代码包中所包含的代码,以避免因体积过大而导致加载时间过长。
    为了避免搞出大体积的代码包,在前期就思考该问题并对代码包进行分割是个不错的选择。 代码分割是由诸如 Webpack,Rollup 和 Browserify(factor-bundle)这类打包器支持的一项技术,能够创建多个包并在运行时动态加载。
    对你的应用进行代码分割能够帮助你“懒加载”当前用户所需要的内容,能够显著地提高你的应用性能。尽管并没有减少应用整体的代码体积,但你可以避免加载用户永远不需要的代码,并在初始加载的时候减少所需加载的代码量。

    import()
    在你的应用中引入代码分割的最佳方式是通过动态 import() 语法。

    使用之前:

    import { add } from './math';
    
    console.log(add(16, 26));
    

    使用之后:

    import ("./math").then(math=>{
        console.log(math.add(16,26))
    })
    

    当 Webpack 解析到该语法时,会自动进行代码分割。如果你使用 Create React App,该功能已开箱即用,你可以立刻使用该特性。Next.js 也已支持该特性而无需进行配置。

    如果你自己配置 Webpack,你可能要阅读下 Webpack 关于代码分割的指南。你的 Webpack 配置应该类似于此。

    当使用 Babel 时,你要确保 Babel 能够解析动态 import 语法而不是将其进行转换。对于这一要求你需要 babel-plugin-syntax-dynamic-import 插件。

    React.lazy

    React.lazy 和 Suspense 技术还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,我们推荐 Loadable Components 这个库。它有一个很棒的服务端渲染打包指南。

    React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)
    使用之前:

    import OtherComponent from './OtherComponent';
    

    使用之后:

    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    

    此代码将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包。

    React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。

    然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

    import React,{Suspense} from 'react'
    const OtherComponent = React.lazy(()=>import('./OtherComponent'))
    
    function MyComponent(){
        return(
            <div>
                <Suspense fallback={<div>Loading...</div>}>
                    <OtherComponent />
                </Suspense>
            </div>
        )
    }
    

    fallback 属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense 组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense 组件包裹多个懒加载组件。、

    import React,{Suspense} from 'react'
    const OtherComponent = React.lazy(()=>import('./OtherComponent'))
    const AnatherComponent = React.lazy(()=>import('./AnatherComponent'))
    
    function MyComponent(){
        return(
            <div>
                <Suspense fallback={<div>Loading...</div>}>
                    <section>
                        <OtherComponent />
                        <AnatherComponent />
                    </section>
                </Suspense>
            </div>
        )
    }
    
    异常捕获边界(Error boundaries)

    如果模块加载失败(如网络问题),它会触发一个错误。你可以通过异常捕获边界(Error boundaries)技术来处理这些情况,以显示良好的用户体验并管理恢复事宜

    import React,{Suspense} from 'react'
    import MyErrorBoundary from './MyErrorBoundary'
    const OtherComponent = React.lazy(()=>import('./OtherComponent'))
    const AnatherComponent = React.lazy(()=>import('./AnatherComponent'))
    
    function MyComponent(){
        return(
            <div>
                <MyErrorBoundary>
                <Suspense fallback={<div>Loading...</div>}>
                    <section>
                        <OtherComponent />
                        <AnatherComponent />
                    </section>
                </Suspense>
                </MyErrorBoundary>
            </div>
        )
    }
    
    基于路由的代码分割

    决定在哪引入代码分割需要一些技巧。你需要确保选择的位置能够均匀地分割代码包而不会影响用户体验。

    一个不错的选择是从路由开始。大多数网络用户习惯于页面之间能有个加载切换过程。你也可以选择重新渲染整个页面,这样您的用户就不必在渲染的同时再和页面上的其他元素进行交互。

    这里是一个例子,展示如何在你的应用中使用 React.lazy 和 React Router 这类的第三方库,来配置基于路由的代码分割。

    import React,{Suspense,lazy} from 'react'
    import {BrowserRouter as Router, Route,Switch} from 'react-router-dom'
    const Home = lazy(()=> import('./routes/Home'))
    const About = lazy(()=> import('./routes/About'))
    
    const App=>()=>(
           <Router>
                <Suspense fallback={<div>Loading...</div>}>
                    <Switch>
                        <Route exact path="/" component={Home}/>
                        <Route path="/about" component={About}/>
                    </Switch>
                </Suspense>
         </Router>
    )
    
    命名导出(Named Exports)

    React.lazy 目前只支持默认导出(default exports)。如果你想被引入的模块使用命名导出(named exports),你可以创建一个中间模块,来重新导出为默认模块。这能保证 tree shaking 不会出错,并且不必引入不需要的组件。

    //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,就能在组件树间进行数据传递的方法。
    在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

    何时使用 Context

    Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式:

    class App extends React.Component{
        render(){
            return <Toolbar theme="dark"/>
        }
    }
    
    function Toolbar(props){
        //Toobal组件接收一个额外的theme属性然后 传递给ThemeButton组件
        //如果应用中每一个单独的按钮都需要theme的值  这回事件麻烦事
        //因为必须将这个值层层传递给所有组件
        return(
            <div>
              <ThemeButtton theme={props.theme}></ThemeButtton>
            </div>
        )
    }
    
    class ThemeButtton extends React.Component{
        render(){
            return <Button theme={this.props.theme}></Button>
        }
    }
    

    使用 context, 我们可以避免通过中间元素传递 props:

    //Context  可以让我们无需明确的传遍每一个组件  就能将值深入传递进组件树。
    //为当前的theme创建一个context("light为默认值")
    const ThemeContext = React.createContext('light')
    class App extends React.Component{
        render(){
            //使用一个Provider来将当前的theme传递给以下的组件树
            //无论多深  任何组件都能读取这个值
            //在这个例子中  我们将dark作为当前的值传递下去
            return(
                <ThemeContext.Provider value="dark">
                    <Toolbar/>
                </ThemeContext.Provider>
            )
        }
    }
    
    
    //中间的组件再也不必指明传递theme了
    function Toolbar(){
        return (
            <div>
                <ThemedButton />
            </div> 
        )
    }
    
    
    class ThemedButton extends React.Component{
        //指定contextType读取当前的theme context
        //React 会往上找到最近的theme Provider  然后使用它的值
        //在这个例子中当前的theme值为dark
        static contextType = ThemeContexst
        render(){
            return <Button theme={this.context}/>
        }
    }
    
    使用 Context 之前的考虑

    Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
    如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 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>
    

    如果在最后只有 Avatar 组件真的需要 user 和 avatarSize,那么层层传递这两个 props 就显得非常冗余。而且一旦 Avatar 组件需要更多从来自顶层组件的 props,你还得在中间层级一个一个加上去,这将会变得非常麻烦。

    一种无需 context 的解决方案是将 Avatar 组件自身传递下去,因而中间组件无需知道 user 或者 avatarSize 等 props:

    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={....}></PageLayout>
    //渲染出
    <NavigationBar userLink={...}/>
    //渲染出
    {props.userLink}
    

    这种变化下,只有最顶部的 Page 组件需要知道 Link 和 Avatar 组件是如何使用 user 和 avatarSize 的。

    这种对组件的控制反转减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。

    而且你的组件并不限制于接收单个子组件。你可能会传递多个子组件,甚至会为这些子组件(children)封装多个单独的“接口(slots)”,正如这里的文档所列举的

    function Page(props){
        const user = props.user
        const content = <Feed user={user}/>
        const topBar = (
            <NavigationBar>
                <Link href={user.permalink}>
                  <Avatar user={user} size={props.avatarSize}/>
                </Link>
            </NavigationBar>
        )
        return (
            <PageLayout topBar={topBar} content={content}>
        )
    }
    

    这种模式足够覆盖很多场景了,在这些场景下你需要将子组件和直接关联的父组件解耦。如果子组件需要在渲染前和父组件进行一些交流,你可以进一步使用 render props。

    但是,有的时候在组件树中很多不同层级的组件需要访问同样的一批数据。Context 能让你将这些数据向组件树下所有的组件进行“广播”,所有的组件都能访问到这些数据,也能访问到后续的数据更新。使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据,这比替代方案要简单的多。

    API

    React.createContext

    const MyContext = React.createContext(defaultValue)
    

    创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

    只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
    Context.Provider

    <MyContext.Provider value={/*某个值*/} />
    

    每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

    Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
    当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

    通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。

    当传递对象给 value 时,检测变化的方式会导致一些问题
    Class.contextType

    class MyClass extends React.Component{
        componentDidMount(){
            let value = this.context
            // 在组件挂载完成后 使用MyContext组件的值来执行一些副作用的操作
        }
        componentDidUpdate(){
            let value = this.context
        }
        componentWillUnmount(){
            let value = this.context
        }
        render(){
            let value = this.context
            // 基于MyContext组件的值进行渲染
        }
    }
    MyClass.contextType = MyContext;
    

    挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

    你只通过该 API 订阅单一 context。如果你想订阅多个,阅读使用多个 Context 章节

    如果你正在使用实验性的 public class fields 语法,你可以使用 static 这个类属性来初始化你的 contextType。

    class MyClass extends React.Component{
        static contextType = MyContext
        render(){
            let value = this.context
            // 基于这个值进行渲染工作
        }
    }
    

    Context.Consumer

    <MyContext.Consumer>
        {value=>/*基于context值进行渲染*/}
    </MyContext.Consumer>
    

    这里,React 组件也可以订阅到 context 变更。这能让你在函数式组件中完成订阅 context。
    这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。
    如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。

    Context.displayName
    context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。
    示例,下述组件在 DevTools 中将显示为 MyDisplayName:

    const MyContext = React.createContext(/* some value */);
    MyContext.displayName = 'MyDisplayName';
    
    <MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
    <MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
    

    动态 Context
    对于上面的 theme 例子,使用动态值(dynamic values)后更复杂的用法:

    theme-context.js

    export const themes = {
      light: {
        foreground: '#000000',
        background: '#eeeeee',
      },
      dark: {
        foreground: '#ffffff',
        background: '#222222',
      },
    };
    
    export const ThemeContext = React.createContext(
      themes.dark //默认值
    )
    

    themed-button.js

    import {ThemeContext} from './ThemeContext'
    class ThemedButton extends React.Component{
      render(){
        let props = this.props
        let theme = this.context
        return (
          <button {...props} style={{backgroundColor:theme.background}}/>
        )
      }
    }
    ThemedButton.contextType = ThemeContext
    export default ThemedButton
    

    app.js

    import {ThemeContext,themes} from './theme-context'
    import ThemedButton from './themed-button'
    // 一个使用ThemedButton的中间组件
    function Toolbar(props) {
      return(
        <ThemedButton onClick={props.changeTheme}>Change Theme</ThemedButton>
      )
    }
    
    class App extends React.Component{
      constructor(props){
        super(props)
        this.state = {
          theme:themes.light
        }
        this.toggleTheme = () =>{
          this.setState(state=>({
            theme:state.theme === themes.dark?'themes.light':'themes.dark'
          }))
        }
      }
      render(){
      //在ThemeProvider内部的ThemedButton按钮组件使用state中的theme值
      //外部的组件使用默认的theme值
      return (
        <Page>
          <ThemeContext.Provider value={this.state.theme}>
           <Toolbar changeTheme={this.toggleTheme}/>
          <ThemeContext.Provider>
          <section>
            <ThemedButton>      
          </section>
        </Page>
      )
    }
    }
    
    ReactDOM.render(<App />,document.root)
    
    消费多个 Context

    为了确保 context 快速进行重渲染,
    React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。

    //Theme context,默认的theme是light值
    const ThemeContext = React.createContext('light')
    //用户登录context
    const UserContext = React.createContext({
        name:'Guest'
    })
    
    class App extends React.Component{
        render(){
            const {signedInUser,theme} = this.props
            //提供初始context值的app组件
            return(
                <ThemeContext.Provider value={theme}>
                  <UserContext.Provider value={signedInUser}>
                    <Layout />
                  </UserContext.Provider>
                </ThemeContext.Provider>
            )
         }
    }
    
    function Layout(){
        return(
            <div>
                <Sidebar />        
                <Content />
            </div>
        )
    }
    
    //一个组件可能会消费多个context
    function Content(){
        return (
            <ThemeContext.Consumer>
                {theme=>(
                    <UserContext.Consumer>
                        {user=>(
                            <ProfilePage user={user} theme={theme}/>
                        )}
                    </UserContext.Consumer>
                )}
            </ThemeContext.Consumer>
        )
    }
    

    如果两个或者更多的 context 值经常被一起使用,那你可能要考虑一下另外创建你自己的渲染组件,以提供这些值。

    注意事项

    因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:

      class App extends React.Component{
          render(){
              return (
                  <MyContext.Provider value={{something:'something'}}>
                    <Toolbar />
                  </MyContext.Provider>
              )
          }
      }
    

    为了防止这种情况,将 value 状态提升到父节点的 state 里:

    class App extends React.Component{
        constructor(props){
            super(props)
            this.state = {
                value:{something:'something'}
            }
        }
        render(){
            return(
                <Provider value={this.state.value}>
                    <Toolbar />
                </Provider>
            )
        }
    }
    
    过时的 API

    注意

    先前 React 使用实验性的 context API 运行,旧的 API 将会在所有 16.x 版本中得到支持,但用到它的应用应该迁移到新版本。过时的 API 将在未来的 React 版本中被移除。阅读过时的 context 文档了解更多。

    错误边界

    过去,组件内的 JavaScript 错误会导致 React 的内部状态被破坏,并且在下一次渲染时 产生 可能无法追踪的 错误。这些错误基本上是由较早的其他代码(非 React 组件代码)错误引起的,但 React 并没有提供一种在组件中优雅处理这些错误的方式,也无法从错误中恢复。

    错误边界

    部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
    错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

    错误边界无法捕获以下场景中产生的错误:
    事件处理(了解更多)
    异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
    服务端渲染
    它自身抛出来的错误(并非它的子组件)

    如果一个 class 组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

    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){
                return <h1>Something went wrong.</h1>
            }
            return this.props.children
        }
    }
    

    然后你可以将它作为一个常规组件去使用:

    <ErrorBoundary>
      <MyWidget />
    </ErrorBoundary>
    

    错误边界的工作方式类似于 JavaScript 的 catch {},不同的地方在于错误边界只针对 React 组件。只有 class 组件才可以成为错误边界组件。大多数情况下, 你只需要声明一次错误边界组件, 并在整个应用中使用它。

    注意错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界,这也类似于 JavaScript 中 catch {} 的工作机制。

    错误边界应该放置在哪?

    错误边界的粒度由你来决定,可以将其包装在最顶层的路由组件并为用户展示一个 “Something went wrong” 的错误信息,就像服务端框架经常处理崩溃一样。你也可以将单独的部件包装在错误边界以保护应用其他部分不崩溃。

    未捕获错误(Uncaught Errors)的新行为

    这一改变具有重要意义,自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。

    把一个错误的 UI 留在那比完全移除它要更糟糕。例如,在类似 Messenger 的产品中,把一个异常的 UI 展示给用户可能会导致用户将信息错发给别人。同样,对于支付类应用而言,显示错误的金额也比不呈现任何内容更糟糕。

    此变化意味着当你迁移到 React 16 时,你可能会发现一些已存在你应用中但未曾注意到的崩溃。增加错误边界能够让你在应用发生异常时提供更好的用户体验。

    例如,Facebook Messenger 将侧边栏、信息面板、聊天记录以及信息输入框包装在单独的错误边界中。如果其中的某些 UI 组件崩溃,其余部分仍然能够交互。
    使用 JS 错误报告服务(或自行构建),这样你能了解关于生产环境中出现的未捕获异常,并将其修复。

    组件栈追踪

    在开发环境下,React 16 会把渲染期间发生的所有错误打印到控制台,即使该应用意外的将这些错误掩盖。除了错误信息和 JavaScript 栈外,React 16 还提供了组件栈追踪。现在你可以准确地查看发生在组件树内的错误信息:
    在这里插入图片描述
    你也可以在组件栈追踪中查看文件名和行号,这一功能在 Create React App 项目中默认开启:
    在这里插入图片描述
    如果你没有使用 Create React App,可以手动将该插件添加到你的 Babel 配置中。注意它仅用于开发环境,在生产环境必须将其禁用 。

    组件名称在栈追踪中的显示依赖于 Function.name 属性。如果你想要支持尚未提供该功能的旧版浏览器和设备(例如 IE 11),考虑在你的打包(bundled)应用程序中包含一个 Function.name 的 polyfill,如 function.name-polyfill 。或者,你可以在所有组件上显式设置 displayName 属性。

    关于 try/catch ?

    try / catch 很棒但它仅能用于命令式代码(imperative code):

    try {
      showButton();
    } catch (error) {
      // ...
    }
    

    然而,React 组件是声明式的并且具体指出 什么 需要被渲染:

    <Button />
    

    错误边界保留了 React 的声明性质,其行为符合你的预期。例如,即使一个错误发生在 componentDidUpdate 方法中,并且由某一个深层组件树的 setState 引起,其仍然能够冒泡到最近的错误边界。

    关于事件处理器

    错误边界无法捕获事件处理器内部的错误。
    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{
    
            }catch(err){
                this.setState({error})
            }
        }
        render(){
            if(this.state.error){
                return <h1>Caught an error.</h1>
            }
            return <button onClick={this.handleClick}>Click</button>
        }
    }
    

    上述例子只是演示了普通的 JavaScript 行为,并没有使用错误边界。

    自 React 15 的命名更改

    React 15 中有一个支持有限的错误边界方法 unstable_handleError。此方法不再起作用,同时自 React 16 beta 发布起你需要在代码中将其修改为 componentDidCatch。

    对此,我们已提供了一个 codemod 来帮助你自动迁移你的代码。

    Refs 转发

    Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。最常见的案例如下所述。

    转发 refs 到 DOM 组件

    考虑这个渲染原生 DOM 元素 button 的 FancyButton 组件:

    function FancyButton(props){
        return(
            <button className="FancyButton">
             {props.children}
            </button>
        )
    }
    

    React 组件隐藏其实现细节,包括其渲染结果。其他使用 FancyButton 的组件通常不需要获取内部的 DOM 元素 button 的 ref。这很好,因为这防止组件过度依赖其他组件的 DOM 结构。

    虽然这种封装对类似 FeedStory 或 Comment 这样的应用级组件是理想的,但其对 FancyButton 或 MyTextInput 这样的高可复用“叶”组件来说可能是不方便的。这些组件倾向于在整个应用中以一种类似常规 DOM button 和 input 的方式被使用,并且访问其 DOM 节点对管理焦点,选中或动画来说是不可避免的。

    Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

    在下面的示例中,FancyButton 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button:

     const FancyButton = React.forwardRef((props,ref)=>(
         <button ref={ref} className="FancyButton">{props.childrens}</button>
     ))
    
     //你可以直接获取DOM button的ref
     const ref = React.createRef()
     <FancyButton ref={ref}>Click Me</FancyButton>
    

    这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。

    以下是对上述示例发生情况的逐步解释:

    • 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
    • 我们通过指定 ref 为 JSX 属性,将其向下传递给 。
    • React 传递 ref 给 forwardRef 内函数 (props, ref) => …,作为其第二个参数。
      我们向下转发该 ref 参数到 ,将其指定为 JSX 属性。
      当 ref 挂载完成,ref.current 将指向 DOM 节点。

    第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。
    Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。

    组件库维护者的注意事项

    当你开始在组件库中使用 forwardRef 时,你应当将其视为一个破坏性更改,并发布库的一个新的主版本。 这是因为你的库可能会有明显不同的行为(例如 refs 被分配给了谁,以及导出了什么类型),并且这样可能会导致依赖旧行为的应用和其他库崩溃。
    出于同样的原因,当 React.forwardRef 存在时有条件地使用它也是不推荐的:它改变了你的库的行为,并在升级 React 自身时破坏用户的应用。

    在高阶组件中转发 refs

    这个技巧对高阶组件(也被称为 HOC)特别有用。让我们从一个输出组件 props 到控制台的 HOC 示例开始:

    function logProps(WrappedComponent) {
        class LogProps extends React.Component{
            componentDidUpdate(prevProps){
                console.log('old props',prevProps)
                console.log('new props',this.props);
            }
            render(){
                return <WrappedComponent {...this.props}/>
            }
        }
        return LogProps
    }
    

    “logProps” HOC 透传(pass through)所有 props 到其包裹的组件,所以渲染结果将是相同的。例如:我们可以使用该 HOC 记录所有传递到 “fancy button” 组件的 props:

    class FancyButton extends React.Component{
        focus(){}
    }
    //我们导出LogProps 而不是FancyButton
    //虽然它也会渲染一个FancyButton
    export default logProps(FancyButton)
    

    上面的示例有一点需要注意:refs 将不会透传下去。这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。

    这意味着用于我们 FancyButton 组件的 refs 实际上将被挂载到 LogProps 组件:

    import FancyButton from './FancyButton'
    const ref = React.createRef()
    //我们导入的FancyButton组件是高阶组件(HOC)logProps
    //尽管渲染结果将是一样的
    //但我们的ref将指向LogProps 而不是内容的FancyButton组件
    //这意味着我们不能调用例如ref.current.focus()  这样的方法
    <FancyButton label="Click Me" handleClick={handleClick} ref={ref}/>
    

    幸运的是,我们可以使用 React.forwardRef API 明确地将 refs 转发到内部的 FancyButton 组件。React.forwardRef 接受一个渲染函数,其接收 props 和 ref 参数并返回一个 React 节点。例如:

    
    function logProps(Compoent){
        class LogProps extends React.Component {
            componentDidUpdate(prevProps){
                console.log('old props',prevProps);
                console.log('new props',this.props);
            }
            render(){
                const {forwardedRef,...rest} = this.props
                //将自定义的prop属性 "forwardedRef" 定义为ref
                return <Component ref={forwardedRef} {...rest}/>
            }
        }
        //注意React.forwardRef 回调的第二个参数ref
        // 我们可以将其作为常规prop属性传递给 LogProps,例如“forwardedRef”
        //然后它就可以被挂载到被LogProps包裹的子组件上
        return React.forwardedRef((props,ref)=>{
            return <LogProps {...props} forwardedRef={ref}>
        })
    }
    
    在 DevTools 中显示自定义名称

    React.forwardRef 接受一个渲染函数。React DevTools 使用该函数来决定为 ref 转发组件显示的内容。

    例如,以下组件将在 DevTools 中显示为 “ForwardRef”:

    const WrappedComponent = React.forwardRef((props,ref)=>{
        return <LogProps {...props} forwardedRef={ref}/>
    })
    

    如果你命名了渲染函数,DevTools 也将包含其名称(例如 “ForwardRef(myFunction)”):

    const WrappedComponent = React.forwardRef(
        function myFunction(props,ref){
            return <LogProps {...props} forwardedRef={ref}/>
        }
    )
    

    你甚至可以设置函数的 displayName 属性来包含被包裹组件的名称:

    function logProps(Component){
        class LogProps extends React.Component{
            //....
        }
        function forwardRef(props,ref){
            return <LogProps {...props} forwardedRef={ref}/>
        }
    
        //在DevTools中为该组件提供一个更有用的显示名
        //例如 "ForwardRef(logProps(MyComponent))"
        const name = Component.displayName || Component.name;
        forwardedRef.displayName = `logProps(${name})`
        return React.forwardedRef(forwardRef)
    }
    

    Fragments

    React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点

    render{
        return(
            <React.Fragment>
                <ChildA />
                <ChildB />
                <ChildC />
            </React.Fragment>
        )
    }
    

    还有一种新的短语法可用于声明它们。

    动机

    一种常见模式是组件返回一个子元素列表。以此 React 代码片段为例:

    class Table extends React.Component{
        render(){
            return (
                <table> 
                    <tr>
                        <Columns />
                    </tr>
                </table>
            )
        }
    }
    

    需要返回多个 元素以使渲染的 HTML 有效。如果在 的 render() 中使用了父 div,则生成的 HTML 将无效。

    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>
    

    Fragments 解决了这个问题。

    用法
     class Columns extends React.Component{
         render(){
             return (
                 <React.Fragment>
                    <td>Hello</td>
                    <td>World</td>
                 <React.Fragment>
             )
         }
     }
    
    

    这样可以正确的输出

    <table>
      <tr>
        <td>Hello</td>
        <td>World</td>
      </tr>
    </table>
    
    短语法

    你可以使用一种新的,且更简短的语法来声明 Fragments。它看起来像空标签:

    class Columns extends React.Component{
        render(){
            return(
                <>
                <td>Hello</td>
                <td>World</td>
                </>
            )
        }
    }
    

    你可以像使用任何其他元素一样使用 <> </>,除了它不支持 key 或属性。

    带 key 的 Fragments

    使用显式 <React.Fragment> 语法声明的片段可能具有 key。一个使用场景是将一个集合映射到一个 Fragments 数组 - 举个例子,创建一个描述列表:

    function Glossary(props){
        return (
            <dl>
                {props.items.map(item=>(
                    //没有key React 会发出一个关键警告
                    <React.Fragment key={item.id}>
                      <dt>{item.term}</dt>
                      <dd>{item.description}</dd>
                    </React.Fragment>
                ))}
            </dl>
        )
    }
    

    key 是唯一可以传递给 Fragment 的属性。未来我们可能会添加对其他属性的支持,例如事件。

    高阶组件

    高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式
    具体而言,高阶组件是参数为组件,返回值为新组件的函数

    const EnhancedComponent = higherOrderComponent(WrappedComponent)
    

    组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。

    HOC 在 React 的第三方库中很常见,例如 Redux 的 connect 和 Relay 的 createFragmentContainer。

    在本文档中,我们将讨论为什么高阶组件有用,以及如何编写自己的 HOC 函数

    使用 HOC 解决横切关注点问题

    我们之前建议使用 mixins 用于解决横切关注点相关的问题。但我们已经意识到 mixins 会产生更多麻烦。阅读更多 以了解我们为什么要抛弃 mixins 以及如何转换现有组件。

    组件是 React 中代码复用的基本单元。但你会发现某些模式并不适合传统组件。

    例如,假设有一个 CommentList 组件,它订阅外部数据源,用以渲染评论列表:

    class ComponentList extends React.Component{
        constructor(props){
            super(props)
            this.handleChange = this.handleChange.bind(this)
            thisw.state = {
                //假设DateSource 是个全局范围内的数据变量
                comments:DateSouce.getComments()
            }
        }
        componentDidMount(){
            //订阅更改
            DateSource.addChangeListener(this.handleChange)
        }
        componentWillUnmount(){
            //清除订阅
            DateSource.removeChangeListener(this.handleChange)
        }
        handleChange(){
            //当数据源更新时  更新组件状态
            this.setState({
                comments:DateSource.getComments()
            })
        }
        render(){
            return(
                <div>
                    {this.state.comments.map((comment)=>(
                        <Component comment = {comment} key={comment.id}/>
                    ))}
                </div>
            )
        }
    }
    

    稍后,编写了一个用于订阅单个博客帖子的组件,该帖子遵循类似的模式:

    class BlogPost extends React.Component{
        constructor(props){
            super(props)
            this.handleChange = this.handleChange.bind(this)
            this.state = {
                blogPost:DateSource.getBlogPost(props.id)
            }
        }
        componentDidMount(){
            DateSource.addChangeListener(this.handleChange)
        }
        componentWillUnmount(){
            DateSource.removeChangeListener(this.handleChange)
        }
        handleChange(){
            this.setState({
                blogPost:DateSource.getBlogPost(this.props.id)
            })
        }
        render(){
            return <TextBlock text={this.state.blogPost}/>
        }
    }
    

    CommentList 和 BlogPost 不同 - 它们在 DataSource 上调用不同的方法,且渲染不同的结果。但它们的大部分实现都是一样的:

    在挂载时,向 DataSource 添加一个更改侦听器。
    在侦听器内部,当数据源发生变化时,调用 setState。
    在卸载时,删除侦听器。

    你可以想象,在一个大型应用程序中,这种订阅 DataSource 和调用 setState 的模式将一次又一次地发生。我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。这正是高阶组件擅长的地方。

    对于订阅了 DataSource 的组件,比如 CommentList 和 BlogPost,我们可以编写一个创建组件函数。该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。让我们调用函数 withSubscription:

    const CommentListWithSubscription = withSubscription(
        CommentList,
        (DataSource)=>DataSource.getComments()
    )
    const BlogPostWithSubscription = withSubscription(
        BlogPost,
        (DataSource,props)=>DataSource.getBlogPost(props.id)
    )
    

    第一个参数是被包装组件。第二个参数通过 DataSource 和当前的 props 返回我们需要的数据。

    当渲染 CommentListWithSubscription 和 BlogPostWithSubscription 时, CommentList 和 BlogPost 将传递一个 data prop,其中包含DataSource 检索到的最新数据:

    //此函数接收一个组件...
    function withSubscription(WrappedComponent, selectData) {
      //...并返回另一个组件
      return class extends React.Component {
        constructor(props) {
          super(props)
          this.handleChange = this.handleChange.bind(this)
          this.state = {
            data: selectData(DataSource, props),
          }
        }
        componentDidMount() {
          //...负责订阅相关的操作...
          DataSource.addChangeListener(this.handleChange)
        }
        componentWillUnmount() {
          DataSource.removeChangeListener(this.handleChange)
        }
        handleChange() {
          this.setState({
            data: selectData(DataSource, this.props),
          })
        }
        render() {
          //...并使用新数据渲染被包装的组件
          //请注意,我们可能还会传递其他属性
          return <WrappedComponent data={this.state.data} {...this.props} />
        }
      }
    }
    
    

    请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。

    被包装组件接收来自容器组件的所有 prop,同时也接收一个新的用于 render 的 data prop。HOC 不需要关心数据的使用方式或原因,而被包装组件也不需要关心数据是怎么来的。

    因为 withSubscription 是一个普通函数,你可以根据需要对参数进行增添或者删除。例如,您可能希望使 data prop 的名称可配置,以进一步将 HOC 与包装组件隔离开来。或者你可以接受一个配置 shouldComponentUpdate 的参数,或者一个配置数据源的参数。因为 HOC 可以控制组件的定义方式,这一切都变得有可能。

    与组件一样,withSubscription 和包装组件之间的契约完全基于之间传递的 props。这种依赖方式使得替换 HOC 变得容易,只要它们为包装的组件提供相同的 prop 即可。例如你需要改用其他库来获取数据的时候,这一点就很有用。

    不要改变原始组件。使用组合。

    不要试图在 HOC 中修改组件原型(或以其他方式改变它)。

    function logProps(InputComponent){
        InputComponent.prototype.componentDidUpdate = function (prevProps){
            console.log('Current props',this.props)
            console.log('Previous props',prevProps);
        }
        //返回原始的input组件  暗示它已经被修改
        return InputComponent
    }
    //每次调用logProps时,增强组件都会有log输出
    const EnhancedComponent = logProps(InputComponent)
    

    这样做会产生一些不良后果。其一是输入组件再也无法像 HOC 增强之前那样使用了。更严重的是,如果你再用另一个同样会修改 componentDidUpdate 的 HOC 增强它,那么前面的 HOC 就会失效!同时,这个 HOC 也无法应用于没有生命周期的函数组件。

    修改传入组件的 HOC 是一种糟糕的抽象方式。调用者必须知道他们是如何实现的,以避免与其他 HOC 发生冲突。

    HOC 不应该修改传入组件,而应该使用组合的方式,通过将组件包装在容器组件中实现功能:

    function logProps(WrappedComponent){
        return class extends React.Component{
            componentDidUpdate(prevProps){
                console.log('Current props',this.props);
                console.log('Previous props',prevProps);
            }
            render(){
                //将input组件包装在容器中  而不对其进行修改
                return <WrappedComponent {...this.props}/>
            }
        }
    }
    

    该 HOC 与上文中修改传入组件的 HOC 功能相同,同时避免了出现冲突的情况。它同样适用于 class 组件和函数组件。而且因为它是一个纯函数,它可以与其他 HOC 组合,甚至可以与其自身组合。

    您可能已经注意到 HOC 与容器组件模式之间有相似之处。容器组件担任分离将高层和低层关注的责任,由容器管理订阅和状态,并将 prop 传递给处理渲染 UI。HOC 使用容器作为其实现的一部分,你可以将 HOC 视为参数化容器组件。

    约定:将不相关的 props 传递给被包裹的组件

    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 的灵活性以及可复用性

    约定:最大化可组合性

    并不是所有的 HOC 都一样。有时候它仅接受一个参数,也就是被包裹的组件:

    const NavbarWithRouter = withRouter(Navbar);
    

    HOC 通常可以接收多个参数。比如在 Relay 中,HOC 额外接收了一个配置对象用于指定组件的数据依赖:

    const CommentWithRelay = Relay.createContainer(Comment, config);
    

    最常见的 HOC 签名如下:

    // React Redux 的 `connect` 函数
    const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
    

    刚刚发生了什么?!如果你把它分开,就会更容易看出发生了什么。

    //connect是一个函数  它的返回值为另外一个函数
    const enhance = connect(commentListSelector,commentListActions)
    //返回值为HOC,它会返回已经连接Redux store的组件
    const ConnectedComment = enhance(CommentList)
    

    换句话说,connect 是一个返回高阶组件的高阶函数!

    这种形式可能看起来令人困惑或不必要,但它有一个有用的属性。 像 connect 函数返回的单参数 HOC 具有签名 Component => Component。 输出类型与输入类型相同的函数很容易组合在一起。

    //而不是这样...
    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)
    

    同样的属性也允许 connect 和其他 HOC 承担装饰器的角色,装饰器是一个实验性的 JavaScript 提案。)

    许多第三方库都提供了 compose 工具函数,包括 lodash (比如 lodash.flowRight), Redux 和 Ramda。

    约定:包装显示名称以便轻松调试

    HOC 创建的容器组件会与任何其他组件一样,会显示在 React Developer Tools 中。为了方便调试,请选择一个显示名称,以表明它是 HOC 的产物。

    最常见的方式是用 HOC 包住被包装组件的显示名称。比如高阶组件名为 withSubscription,并且被包装组件的显示名称为 CommentList,显示名称应该为 WithSubscription(CommentList):

    function withSubscription(WrappedComponent){
        class WithSubscription extends React.Component{/*.....*/}
        WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`
        return WithSubscription
    }
    
    function getDisplayName(WrappedComponent){
        return WrappedComponent.displayName || WrappedComponent.name || "Component"
    }
    
    注意事项

    高阶组件有一些需要注意的地方,对于 React 新手来说可能并不容易发现。

    不要在 render 方法中使用 HOC
    React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。
    通常,你不需要考虑这点。但对 HOC 来说这一点很重要,因为这代表着你不应在组件的 render 方法中对一个组件应用 HOC:

    render() {
      // 每次调用 render 函数都会创建一个新的 EnhancedComponent
      // EnhancedComponent1 !== EnhancedComponent2
      const EnhancedComponent = enhance(MyComponent);
      // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
      return <EnhancedComponent />;
    }
    

    这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。

    如果在组件之外创建 HOC,这样一来组件只会创建一次。因此,每次 render 时都会是同一个组件。一般来说,这跟你的预期表现是一致的。

    在极少数情况下,你需要动态调用 HOC。你可以在组件的生命周期方法或其构造函数中进行调用。

    务必复制静态方法

    有时在 React 组件上定义静态方法很有用。例如,Relay 容器暴露了一个静态方法 getFragment 以方便组合 GraphQL 片段。

    但是,当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。

    //定义静态函数
    WrappedComponent.staticMethod = function(){}
    //现在使用HOC
    const EnhancedComponent = enhance(WrappedComponent)
    //增强组件没有staticMethods
    typeof EnhancedComponent.staticMethod === 'undefined' //true
    

    为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:

    function enhance(WrappedComponent){
        class Enhance extends React.Component{}
        //必须准确知道应该拷贝哪些方法
        Enhance.staticMethod = WrappedComponent.staticMethod
        return Enhance
    }
    

    除了导出组件,另一个可行的方案是再额外导出这个静态方法。

    //使用这种方式替代
    MyComponent.someFunction = someFunction
    export default MyComponent
    //...单独导出该方法...
    export {someFunction}
     
    
     //并在要使用的组件中  import它们
     import MyComponent,{someFunction} from './MyComment.js'
    
    Refs 不会被传递

    虽然高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。

    与第三方库协同

    React 可以被用于任何 web 应用中。它可以被嵌入到其他应用,且需要注意,其他的应用也可以被嵌入到 React。本指南将介绍一些更常见的用例,专注于与 jQuery 和 Backbone 进行整合,同样的思路还可以应用于将组件与任意现有代码集成

    集成带有 DOM 操作的插件

    React 不会理会 React 自身之外的 DOM 操作。它根据内部虚拟 DOM 来决定是否需要更新,而且如果同一个 DOM 节点被另一个库操作了,React 会觉得困惑而且没有办法恢复。

    这并不意味着 React 与其他操作 DOM 的方式不能结合,也不一定结合困难,只不过需要你去关注每个库所做的事情。

    避免冲突的最简单方式就是防止 React 组件更新。你可以渲染无需更新的 React 元素,比如一个空的

    如何解决这个问题

    为了证明这一点,我来草拟一个用于通用 jQuery 插件的 wrapper

    我们会添加一个 ref 到这个根 DOM 元素。 在 componentDidMount 中,我们能够获取它的引用这样我们就可以把它传递给 jQuery 插件了。

    为了防止 React 在挂载之后去触碰这个 DOM,我们会从 render() 函数返回一个空的

    。这个
    元素既没有属性也没有子元素,所以 React 没有理由去更新它,使得 jQuery 插件可以自由的管理这部分的 DOM:

    class SomePlugin extends React.Component{
        componentDidMount(){
            this.$el = $(this.el)
            this.$el.somePlugin()
        }
        componentWillUnmount(){
            this.$el.somePlugin('destroy')
        }
        render(){
            return <div ref={el=>this.el=el}/>
        }
    }
    

    注意我们同时定义了 componentDidMount 和 componentWillUnmount 生命周期函数。许多 jQuery 插件绑定事件监听到 DOM 上,
    所以在 componentWillUnmount 中注销监听是很重要的。如果这个插件没有提供一个用于清理的方法,你很可能会需要自己来提供一个,为了避免内存泄漏要记得把所有插件注册的监听都移除掉。

    集成 jQuery Chosen 插件

    对于应用这些概念的更具体的一个例子,我们给这个用于增强 输入的 Chosen 插件写一个最小的 wrapper。

    首先,我们来看下 Chosen 对 DOM 做了哪些操作
    如果你在一个 DOM 节点上调用了它,它会读取原 DOM 节点的属性,使用行内样式隐藏它,然后紧挨着这个 之后增加一个独立的具有它自身显示表现的 DOM 节点。然后它会在值变化的时候触发 jQuery 事件来通知我们这些变化。

    以下代码是我们最终要实现的效果:

    function Example(){
        return (
            <Chosen onChange={value=>console.log(value)}>
                <option>vanilla</option>
                <option>chocolate</option>
                <option>strawberry</option>
            </Chosen>
        )
    }
    

    为了简化,我们将它实现为 uncontrolled component

    首先,我会创建一个空的组件,它的 render() 函数我们返回一个包含 的

    :

    class Chosen extends React.Component{
        render(){
            return(
                <div>
                    <select className="Chosen-select" ref={el=>this.el=el}>
                        {this.props.children}
                    </select>
                </div>
            )
        }
    }
    

    注意我们为什么要把 使用一个额外的

    包裹起来。这是很必要的,因为 Chosen 会紧挨着我们传递给它的 节点追加另一个 DOM 元素。然而,对于 React 来说
    总是只有一个子节点。这样我们就能确保 React 更新不会和 Chosen 追加的额外 DOM 节点发生冲突。在 React 工作流之外修改 DOM 是非常重大的事情,你必须确保 React 没有理由去触碰那些节点。

    接下来,我们会实现生命周期函数。我们需要在 componentDidMount 中使用 的引用初始化 Chosen,并且componentWillUnmount 中将其销毁:

    componentDidMount(){
        this.$el = $(this.el)
        this.$el.chosen()
    }
    componentWillUnmount(){
        this.$el.chosen('destroy')
    }
    

    在 CodePen 上运行
    注意 React 不会给 this.el 字段赋予特殊的含义。它能够工作只是因为我们之前在 render() 函数中把一个 ref 赋值给了这个字段:

    <select className="Chosen-select" ref={el => this.el = el}>
    

    到此已经足够让我们的组件去渲染了,但我们同时希望在值变化的时候被通知到。要做到这点,我们需要在订阅由 Chosen 管理的 上的 jQuery change 事件:
    我们不直接把 this.props.onChange 传递给 Chosen 是因为组件的 props 可能随时变化,并且这也包括事件处理函数。对应的,我们会定义一个 handleChange() 方法来调用 this.props.onChange,并且订阅 jQuery 的 change 事件:

    componentDidMount(){
        this.$el = $(this.el)
        this.$el.chosen()
    
        this.handleChange = this.handleChange.bind(this)
        this.$el.on('change',this.handleChange)
    }
    
    componentWillUnmount(){
        this.$el.off('change',this.handleChange)
        this.$el.chosen('destroy')
    }
    handleChange(e){
        this.props.onChange(e.target.value)
    }
    

    后,还剩下一件事情需要处理。在 React 中,props 可以在不同的时间有不同的值。例如,如果父组件的状态发生变化 组件可能得到不同的 children。这意味着从集成的角度来看,我们因应 prop 的更新而手动更新 DOM 这一点是非常重要的,因为我们已经不再使用 React 来帮我们管理 DOM 了。

    Chosen 的文档建议我们使用 jQuery trigger() API 来通知原始 DOM 元素这些变化。我们会让 React来管理在 中 this.props.children 的更新,但是我们同样需要增加一个 componentDidUpdate() 生命周期函数来通知 Chosen 关于 children 列表的变化:

    componentDidUpdate(prevProps){
        if(prevProps.children !== this.props.children){
            this.$el.trigger('chosen:updated')
        }
    }
    

    通过这种方法,当由 React 管理的 children 改变时, Chosen 会知道如何更新它的 DOM 元素。。

    Chosen 组件的完整实现看起来是这样的:

    class Chosen extends React.Component{
        componentDidMount(){
            this.$el = $(this.el)
            this.$el.chosen()
            this.handleChange = this.handleChange.bind(this)
            this.$el.on('change',this.handleChange)
        }
        
        componentDidUpdate(prevProps){
            if(prevProps.children !==  this.props.children){
                this.$el.trigger("chosen:updated")
            }
        }
    
        componentWillUnmount(){
            this.$el.off('change',this.handleChange)
            this.$el.chosen('destroy')
        }
        handleChange(e){
            this.props.onChange(e.target.value)
        }
        render(){
            return (
                <div>
                    <select className="Chosen-select" ref={el=>this.el=el}>
                      {this.props.children}
                    </select>
                </div>
            )
        }
    }
    
    和其他视图库集成

    得益于 ReactDOM.render() 的灵活性 React 可以被嵌入到其他的应用中。

    虽然 React 通常被用来在启动的时候加载一个单独的根 React 组件到 DOM 上,ReactDOM.render() 同样可以在 UI 的独立部分上多次调用,这些部分可以小到一个按钮,也可以大到一个应用。

    事实上,这正是 Facebook 如何使用 React 的。这让我们小块小块地在应用中使用 React,并且把他们结合到我们现存的服务端产生的模板和其他客户端代码中。

    利用 React 替换基于字符串的渲染

    在旧的 web 应用中一个通用的模式就是使用一个字符串描述 DOM 块并且通过类似 $el.html(htmlString) 这样的方式插入到 DOM 中。代码库中的这种例子是非常适合引入 React 的。直接把基于字符串的渲染重写成 React 组件即可。

    …可以使用 React 组件重写为:

    $('#container').html('<button id="btn">Say Hello</button>')
    $('#btn').click(function(){
        alert('Hello')
    })
    

    …可以使用 React 组件重写为:

    function Button(){
        return <button id="btn">Say Hello</button>
    }
    
    ReactDOM.render(
        <Button />,
        document.getElementById('container'),
        function(){
            $('#btn').click(function(){
                alert('hello')
            })
        }
    )
    

    从这起你可开始以把更多的逻辑移动到组件中,并且开始应用更多通用 React 实践。例如,在组件中最好不要依赖 ID 因为同一个组件可能会被渲染多次。相反的,我们会使用 React 事件系统 并且直接注册 click 处理函数到 React 元素:

    function Button(props){
        return <button onClick={props.onClick}>Say Hello</button>
    }
    function HelloButton(){
        function handleClick(){
            alert('Hello')
        }
        return <Button onClick={handleClick}/>
    }
    ReactDOM.render(
        <HelloButton />,
        document.getElementById('container')
    )
    

    只要你喜欢你可以有不限数量的这种独立组件,并且使用 ReactDOM.render() 把他们渲染到不同的容器中。逐渐的,随着你把越来越多的应用转换到 React,你就可以把它们结合成更大的组件,并且把 ReactDOM.render() 的调用移动到更上层的结构。

    把 React 嵌入到 Backbone 视图

    Backbone 视图通常使用 HTML 字符串,或者产生字符串的模板函数,来创建 DOM 元素的内容。这个过程,同样的,可以通过渲染一个 React 组件来替换掉。

    如下,我们会创建一个名为 ParagraphView 的 Backbone 视图。他会重载 Backbone 的 render() 函数来渲染一个 React 组件到 Backbone (this.el) 提供的 DOM 元素中。这里,同样的,我们将会使用 ReactDOM.render():

    function Paragraph(props){
        return <p>{props.text}</p>
    }
    
    const ParagraphView =Backbone.View.extend({
        render(){
            const text = this.model.get('text');
            ReactDOM.render(<Paragraph text={text}/>,this.el);
            return this;
        },
        remove(){
            ReactDOM.unmountComponentAtNode(this.el);
            Backbone.View.prototype.remove.call(this)
        }
    })
    

    在 remove 方法中我们也需要调用 ReactDOM.unmountComponentAtNode() 以便在它解除的时候 React 清理组件树相关的事件处理的注册和其他的资源,这点是是很重要的。

    当一个组件在 React 树中从内部删除的时候,清理工作是自动完成的,但是因为我们现在手动移除整个树,我们必须调用这个方法。

    和 Model 层集成

    虽然通常是推荐使用单向数据流动的,例如 React state,Flux,或者 Redux,React 组件也可以使用一个其他框架和库的 Model 层。

    在 React 组件中使用 Backbone 的 Model
    在 React 组件中使用 Backbone 的 model 和 collection 最简单的方法就是监听多种变化事件并且手动强制触发一个更新。
    负责渲染 model 的组件会监听 ‘change’ 事件,而负责渲染 collection 的组件需要监听 ‘add’ 和 ‘remove’ 事件。在这两种情况中,调用 this.forceUpdate() 来使用新的数据重新渲染组件。

    在下面的例子中,List 组件渲染一个 Backbone collection,使用 Item 组件来渲染独立的项。

    class Item extends React.Component{
        constructor(props){
            super(props)
            this.handleChange = this.handleChange.bind(this)
        }
        handleChange(){
            this.forceUpdate()
        }
        componentDidMount(){
            this.props.model.on('change',this.handleChange)
        }
        componentWillUnmount(){
            this.props.model.off('change',this.handleChange)
        }
        render(){
            return <li>{this.props.model.get('text')}</li>
        }
    }
    
    class List extends React.Component{
        constructor(props){
            super(props)
            this.handleChange = this.handleChange.bind(this)
        }
        handleChange(){
            this.forceUpdate()
        }
        componentDidMount(){
            this.props.collection.on('add','remove',this.handleChange)
        }
        componentWillUnmount(){
            this.props.collection.off('add','remove',this.handleChange)
        }
    
        render(){
            return (
                <ul>
                    {this.props.collection.map(model=>{
                        <Item key={model.cid} model={model}/>
                    })}            
                </ul>
            )
        }
    }
    
    从 Backbone Model 提取数据

    前面的方式需要你的 React 组件知道 Backbone 的 model 和 collection。如果你计划迁移到另一个数据管理方案,你可能希望将关于Backbone的知识集中在尽可能少的代码部分中。

    其中一个解决方案就是每当 model 中的属性变化时都把它提取成简单数据,并且把这个逻辑放在一个独立的地方。下面是一个高阶组件,它提取了 Backbone model 的所有数据存放到 state 中,并将数据传递到被包裹的组件中。

    通过这种方法,只有高阶组件需要知道 Backbone model 的内部构造,而且应用中大多数的组件可以保持和 Backbone 无关。

    在下面的例子中,我们会拷贝一份 model 的属性来形成初始的 state。我们订阅 change 事件(并且在取消挂载时停止订阅),而当变化发生时,我们使 model 的当前属性更新这个 state。最终,我们确保了只要 model 属性本身变化的时候,我们不要忘了停止旧 model 的订阅并开始订阅新的 model。

    请注意,这个例子并不是为了彻底完整展示如何与 Backbone 集成,而是它应该让你了解如何以通用的方式处理此问题:

    function connectToBackboneModel(WrappedComponent){
        return class BackboneComponent extends React.Component{
            constructor(props){
                super(props)
                this.state = Object.assign({},props.model.attributes)
                this.handleChange = this.handleChange.bind(this)
            }
            componentDidMount(){
                this.props.model.on('change',this.handleChange)
            }
            componentWillReceiveProps(nextProps){
                this.setState(Object.assign({},nextProps.model.attributes))
                if(nextProps.model!== this.props.model){
                    this.props.model.off('change',this.handleChange)
                    nextProps.model.on('change',this.handleChange)
                }
            }
            componentWillUnmount(){
                this.props.model.off('change',this.handleChange)
            }
            handleChange(model){
                this.setState(model.changedAttributes())
            }
            render(){
                const propsExceptModel = Onject.assign({},this.props)
                delete propsExceptModel.model;
                return <WrappedComponent {...propsExceptModel} {...this.state}/>
            }
        }
    }
    

    要演示如何使用它,我们会链接一个 NameInput React 组件到一个 Backbone model,并且每当输入框变化时更新它的 firstName 属性:

    
    function NameInput(props){
        return (
            <p>
                <input value={props.firstName} onChange={props.handleChange}/> 
                <br />
                My name is {props.firstName}
            </p>
        )
    }
    
    const BackboneNameInput = connectToBackboneModel(NameInput)
    function Example(props){
        function handleChange(e){
            props.model.set('firstName',e.target.value)
        }
        return (
            <BackboneNameInput model={props.model} handleChange={handleChange} /> 
        )
    }
    cosnt model = new Backbone.Model({firstName:'Frodo'});
    ReactDOM.render(
        <Example model={model}/>,
        document.getElementById('root')
    )
    

    这个技术并不仅限于 Backbone。你可以通过在生命周期方法中订阅其更改并,并选择性地,拷贝数据到本地 React state,来将 React 用于任何 model 库。

    深入 JSX

    实际上,JSX 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。如下 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'}
    )
    

    如果你想测试一些特定的 JSX 会转换成什么样的 JavaScript,你可以尝试使用在线的 Babel 编译器。

    指定 React 元素类型

    JSX 标签的第一部分指定了 React 元素的类型。
    大写字母开头的 JSX 标签意味着它们是 React 组件。这些标签会被编译为对命名变量的直接引用,所以,当你使用 JSX 表达式时,Foo 必须包含在作用域内。

    React 必须在作用域内

    由于 JSX 会编译为 React.createElement 调用形式,所以 React 库也必须包含在 JSX 代码作用域内。

    例如,在如下代码中,虽然 React 和 CustomButton 并没有被直接使用,但还是需要导入:

    import React from 'react';
    import CustomButton from './CustomButton';
    
    function WarningButton() {
      // return React.createElement(CustomButton, {color: 'red'}, null);
      return <CustomButton color="red" />;
    }
    

    如果你不使用 JavaScript 打包工具而是直接通过

    在 JSX 类型中使用点语法

    在 JSX 中,你也可以使用点语法来引用一个 React 组件。当你在一个模块中导出许多 React 组件时,这会非常方便。例如,如果 MyComponents.DatePicker 是一个组件,你可以在 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" />
    }
    

    用户定义的组件必须以大写字母开头

    以小写字母开头的元素代表一个 HTML 内置组件,比如

    或者 会生成相应的字符串 ‘div’ 或者 ‘span’ 传递给 React.createElement(作为参数)。大写字母开头的元素则对应着在 JavaScript 引入或自定义的组件,如 会编译为 React.createElement(Foo)。

    我们建议使用大写字母开头命名自定义组件。如果你确实需要一个以小写字母开头的组件,则在 JSX 中使用它之前,必须将它赋值给一个大写字母开头的变量。

    例如,以下的代码将无法按照预期运行:

    import React from 'react';
    
    // 错误!组件应该以大写字母开头:
    function hello(props) {
      // 正确!这种 <div> 的使用是合法的,因为 div 是一个有效的 HTML 标签
      return <div>Hello {props.toWhat}</div>;
    }
    
    function HelloWorld() {
      // 错误!React 会认为 <hello /> 是一个 HTML 标签,因为它没有以大写字母开头:
      return <hello toWhat="World" />;
    }
    

    要解决这个问题,我们需要重命名 hello 为 Hello,同时在 JSX 中使用 :

    import React from "react"
    
    //正确 组件 需要以大写字母开头
    function Hello(props){
        //正确  这种<div>使用时合法的 因为div是一个有效的HTML标签
        return <div>Hello {props.toWhat}</div>
    }
    function HelloWorld(){
        //正确  React 知道<Hello />是一个组件  因为它是大写字母开头的
        return <Hello toWhat="World"/>
    }
    
    在运行时选择类型

    你不能将通用表达式作为 React 元素类型。如果你想通过通用表达式来(动态)决定元素类型,你需要首先将它赋值给大写字母开头的变量。这通常用于根据 prop 来渲染不同组件的情况下:、

    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} />;
    }
    

    要解决这个问题, 需要首先将类型赋值给一个大写字母开头的变量:

    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

    有多种方式可以在 JSX 中指定 props。

    JavaScript 表达式作为 Props
    你可以把包裹在 {} 中的 JavaScript 表达式作为一个 prop 传递给 JSX 元素。例如,如下的 JSX:

    <MyComponent foo={1 + 2 + 3 + 4} />
    

    在 MyComponent 中,props.foo 的值等于 1 + 2 + 3 + 4 的执行结果 10。
    if 语句以及 for 循环不是 JavaScript 表达式,所以不能在 JSX 中直接使用。但是,你可以用在 JSX 以外的代码中。比如:

    function NumberDescriber(props){
        let description;
        if(props.number%2==0){
            description = <strong>even</strong>
        }else{
            description = <i>odd</i>
        }
        return <div>{props.number}is an {description} number</div>
    }
    

    字符串字面量
    你可以将字符串字面量赋值给 prop. 如下两个 JSX 表达式是等价的:

    <MyComponent message="Hello World"/>
    <MyComponent message={'Hello World'}/>
    

    当你将字符串字面量赋值给 prop 时,它的值是未转义的。所以,以下两个 JSX 表达式是等价的:

    <MyComponent message="$lt;3"/>
    <MyComponent message={'<3'}/>
    

    这种行为通常是不重要的,这里只是提醒有这个用法。

    Props 默认值为 “True”

    如果你没给 prop 赋值,它的默认值是 true。以下两个 JSX 表达式是等价的:

     <MyText autocomplete/>
     <Mytext autocomplete={true}/>
    

    通常,我们不建议不传递 value 给 prop,因为这可能与 ES6 对象简写混淆,{foo} 是 {foo: foo} 的简写,而不是 {foo: true}。这样实现只是为了保持和 HTML 中标签属性的行为一致。

    属性展开

    如果你已经有了一个 props 对象,你可以使用展开运算符 … 来在 JSX 中传递整个 props 对象。以下两个组件是等价的:

    function App1(){
        return <Greeting firstName="Ben" lastName="Hector"/>
    }
    
    function App2(){
        const Props = {firstName:'Ben',lastName:'Hector'};
        return <Greeting {...Props}/>
    }
    

    你还可以选择只保留当前组件需要接收的 props,并使用展开运算符将其他 props 传递下去。

    const Button = props=>{
        const {kind,...other} = props
        const className = kind === 'primary'?'PrimaryButton':"SecondaryButton"
        return <button className={className} {...other}/>
    }
    
    const App =()=>{
        return (
            <div>
                <Button kind="primary" onClick={()=>console.log('clicked!')}>
                    Hello World!
                </Button>
            </div>
        )
    }
    

    在上述例子中,kind 的 prop 会被安全的保留,它将不会被传递给 DOM 中的 元素。 所有其他的 props 会通过 …other 对象传递,使得这个组件的应用可以非常灵活。你可以看到它传递了一个 onClick 和 children 属性。

    属性展开在某些情况下很有用,但是也很容易将不必要的 props 传递给不相关的组件,或者将无效的 HTML 属性传递给 DOM。我们建议谨慎的使用该语法。

    JSX 中的子元素

    包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children 传递给外层组件。有几种不同的方法来传递子元素

    字符串字面量
    你可以将字符串放在开始和结束标签之间,此时 props.children 就只是该字符串。这对于很多内置的 HTML 元素很有用。例如:

    <MyComponent>Hello world!</MyComponent>
    

    这是一个合法的 JSX,MyComponent 中的 props.children 是一个简单的未转义字符串 “Hello world!”。因此你可以采用编写 HTML 的方式来编写 JSX。如下所示:

    <div>This is valid HTML &amp; JSX at the same time.</div>
    

    JSX 会移除行首尾的空格以及空行。与标签相邻的空行均会被删除,文本字符串之间的新行会被压缩为一个空格。因此以下的几种方式都是等价的:

    <div>Hello World</div>
    
    <div>
      Hello World
    </div>
    
    <div>
      Hello
      World
    </div>
    
    <div>
    
      Hello World
    </div>
    
    JSX 子元素

    子元素允许由多个 JSX 元素组成。这对于嵌套组件非常有用:

    <MyContainer>
      <MyFirstComponent />
      <MySecondComponent />
    </MyContainer>
    

    你可以将不同类型的子元素混合在一起,因此你可以将字符串字面量与 JSX 子元素一起使用。这也是 JSX 类似 HTML 的一种表现,所以如下代码是合法的 JSX 并且也是合法的 HTML:

    <div>
      Here is a list:
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
      </ul>
    </div>
    

    React 组件也能够返回存储在数组中的一组元素:

    render(){
        //不需要用额外的元素包裹列表元素
        return [
            //不要忘记设置key
            <li key="A">First Item</li>
            <li key="B">Second Item</li>
            <li key="C">Third Item</li>
        ]
    }
    
    JavaScript 表达式作为子元素

    JavaScript 表达式可以被包裹在 {} 中作为子元素。例如,以下表达式是等价的:

    <MyComponent>foo</MyComponent>
    
    <MyComponent>{'foo'}</MyComponent>
    

    这对于展示任意长度的列表非常有用。例如,渲染 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>
        )
    }
    

    JavaScript 表达式也可以和其他类型的子元素组合。这种做法可以方便地替代模板字符串:

    function Hello(props) {
      return <div>Hello {props.addressee}!</div>;
    }
    
    
    函数作为子元素

    通常,JSX 中的 JavaScript 表达式将会被计算为字符串、React 元素或者是列表。不过,props.children 和其他 prop 一样,它可以传递任意类型的数据,而不仅仅是 React 已知的可渲染类型。例如,如果你有一个自定义组件,你可以把回调函数作为 props.children 进行传递:

    //调用子元素回调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>
        )
    }
    

    你可以将任何东西作为子元素传递给自定义组件,只要确保在该组件渲染之前能够被转换成 React 理解的对象。这种用法并不常见,但可以用于扩展 JSX。

    布尔类型、Null 以及 Undefined 将会忽略

    false, null, undefined, and true 是合法的子元素。但它们并不会被渲染。以下的 JSX 表达式渲染结果相同:

    <div />
    
    <div></div>
    
    <div>{false}</div>
    
    <div>{null}</div>
    
    <div>{undefined}</div>
    
    <div>{true}</div>
    

    这有助于依据特定条件来渲染其他的 React 元素。例如,在以下 JSX 中,仅当 showHeader 为 true 时,才会渲染

    组件:

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

    值得注意的是有一些 “falsy” 值,如数字 0,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,0 仍然会被渲染:

    
     <div>
        {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>
    

    优化性能

    UI 更新需要昂贵的 DOM 操作,而 React 内部使用几种巧妙的技术以便最小化 DOM 操作次数。对于大部分应用而言,使用 React 时无需专门优化就已拥有高性能的用户界面。尽管如此,你仍然有办法来加速你的 React 应用。

    使用生产版本

    当你需要对你的 React 应用进行 benchmark,或者遇到了性能问题,请确保你正在使用压缩后的生产版本。

    React 默认包含了许多有用的警告信息。这些警告信息在开发过程中非常有帮助。然而这使得 React 变得更大且更慢,所以你需要确保部署时使用了生产版本。

    如果你不能确定你的编译过程是否设置正确,你可以通过安装 Chrome 的 React 开发者工具 来检查。如果你浏览一个基于 React 生产版本的网站,图标背景会变成深色
    在这里插入图片描述
    如果你浏览一个基于 React 开发模式的网站,图标背景会变成红色:

    在这里插入图片描述
    推荐你在开发应用时使用开发模式,而在为用户部署应用时使用生产模式。

    你可以在下面看到几种为应用构建生产版本的操作说明。

    Create React App

    如果你的项目是通过 Create React App 构建的,运行:

    npm run build
    

    这段命令将在你的项目下的 build/ 目录中生成对应的生产版本。

    注意只有在生产部署前才需要执行这个命令。正常开发使用 npm start 即可。

    单文件构建

    我们提供了可以在生产环境使用的单文件版 React 和 React DOM:

    <script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
    

    注意只有以 .production.min.js 为结尾的 React 文件适用于生产。

    Brunch

    通过安装 terser-brunch 插件,来获得最高效的 Brunch 生产构建:

    如果你使用 npm
    npm install --save-dev terser-brunch
    
    如果你使用 Yarn
    
    yarn add --dev terser-brunch
    

    接着,在 build 命令后添加 -p 参数,以创建生产构建:

    brunch build -p
    

    请注意,你只需要在生产构建时这么做。你不需要在开发环境中使用 -p 参数或者应用这个插件,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。

    Browserify

    为了最高效的生产构建,需要安装一些插件:

    # 如果你使用 npm
    npm install --save-dev envify terser uglifyify
    
    # 如果你使用 Yarn
    yarn add --dev envify terser uglifyify
    

    为了创建生产构建,确保你添加了以下转换器 (顺序很重要):

    envify 转换器用于设置正确的环境变量。设置为全局 (-g)。
    uglifyify 转换器移除开发相关的引用代码。同样设置为全局 (-g)。
    最后,将产物传给 terser 并进行压缩(为什么要这么做?)。

    browserify ./index.js \
      -g [ envify --NODE_ENV production ] \
      -g uglifyify \
      | terser --compress --mangle > ./bundle.js
    

    请注意,你只需要在生产构建时用到它。你不需要在开发环境应用这些插件,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。

    Rollup

    为了最高效的 Rollup 生产构建,需要安装一些插件:

    # 如果你使用 npm
    npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser
    
    # 如果你使用 Yarn
    yarn add --dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser
    

    为了创建生产构建,确保你添加了以下插件 (顺序很重要):

    replace 插件确保环境被正确设置。
    commonjs 插件用于支持 CommonJS。
    terser 插件用于压缩并生成最终的产物。

    plugins: [
      // ...
      require('rollup-plugin-replace')({
        'process.env.NODE_ENV': JSON.stringify('production')
      }),
      require('rollup-plugin-commonjs')(),
      require('rollup-plugin-terser')(),
      // ...
    ]
    

    请注意,你只需要在生产构建时用到它。你不需要在开发中使用 terser 插件或者 replace 插件替换 ‘production’ 变量,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。

    webpack

    注意:

    如果你使用了 Create React App,请跟随上面的说明进行操作。
    只有当你直接配置了 webpack 才需要参考以下内容。

    在生产模式下,Webpack v4+ 将默认对代码进行压缩:

    const TerserPlugin = require('terser-webpack-plugin');
    
    module.exports = {
      mode: 'production',
      optimization: {
        minimizer: [new TerserPlugin({ /* additional options here */ })],
      },
    };
    

    你可以在 webpack 文档中了解更多内容。

    请注意,你只需要在生产构建时用到它。你不需要在开发中使用 TerserPlugin 插件,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。

    使用 Chrome Performance 标签分析组件

    在开发模式下,你可以通过支持的浏览器可视化地了解组件是如何 挂载、更新以及卸载的。例如:
    在这里插入图片描述
    在 Chrome 中进行如下操作:

    临时禁用所有的 Chrome 扩展,尤其是 React 开发者工具。他们会严重干扰度量结果!
    确保你是在 React 的开发模式下运行应用。
    打开 Chrome 开发者工具的 Performance 标签并按下 Record。
    对你想分析的行为进行复现。尽量在 20 秒内完成以避免 Chrome 卡住。
    停止记录。

    在 User Timing 标签下会显示 React 归类好的事件。
    你可以查阅 Ben Schwarz 的文章以获取更详尽的指导。

    需要注意的是在生产环境中组件会相对渲染得更快些。当然了,这能帮助你查看是否有不相关的组件被错误地更新,以及 UI 更新的深度和频率。

    目前只有 Chrome、Edge 和 IE 支持该功能,但是我们使用的是标准的用户计时 API

    使用开发者工具中的分析器对组件进行分析

    react-dom 16.5+ 和 react-native 0.57+ 加强了分析能力。在开发模式下,React 开发者工具会出现分析器标签。 你可以在《介绍 React 分析器》这篇博客中了解概述。
    react-dom 的生产分析包也可以在 react-dom/profiling 中找到。 通过查阅 fb.me/react-profiling 来了解更多关于使用这个包的内容。

    虚拟化长列表

    如果你的应用渲染了长列表(上百甚至上千的数据),我们推荐使用“虚拟滚动”技术。这项技术会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量。

    react-window 和 react-virtualized 是热门的虚拟滚动库。 它们提供了多种可复用的组件,用于展示列表、网格和表格数据。 如果你想要一些针对你的应用做定制优化,你也可以创建你自己的虚拟滚动组件,就像 Twitter 所做的。

    避免调停

    React 构建并维护了一套内部的 UI 渲染描述。它包含了来自你的组件返回的 React 元素。该描述使得 React 避免创建 DOM 节点以及没有必要的节点访问,因为 DOM 操作相对于 JavaScript 对象操作更慢。虽然有时候它被称为“虚拟 DOM”,但是它在 React Native 中拥有相同的工作原理。

    当一个组件的 props 或 state 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。当它们不相同时,React 会更新该 DOM。

    即使 React 只更新改变了的 DOM 节点,重新渲染仍然花费了一些时间。在大部分情况下它并不是问题,不过如果它已经慢到让人注意了,你可以通过覆盖生命周期方法 shouldComponentUpdate 来进行提速。该方法会在重新渲染前被触发。其默认实现总是返回 true,让 React 执行更新:

    shouldComponentUpdate(nextProps, nextState) {
      return true;
    }
    

    如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。

    在大部分情况下,你可以继承 React.PureComponent 以代替手写 shouldComponentUpdate()。它用当前与之前 props 和 state 的浅比较覆写了 shouldComponentUpdate() 的实现。

    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 需要渲染这个组件,但是由于其返回的 React 元素和之前渲染的相同,所以不需要更新 DOM。

    显而易见,你看到 React 只改变了 C6 的 DOM。对于 C8,通过对比了渲染的 React 元素跳过了渲染。而对于 C2 的子节点和 C7,由于 shouldComponentUpdate 使得 render 并没有被调用。因此它们也不需要对比元素了。

    示例

    如果你的组件只有当 props.color 或者 state.count 的值改变才需要更新时,你可以使用 shouldComponentUpdate 来进行检查:

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

    在这段代码中,shouldComponentUpdate 仅检查了 props.color 或 state.count 是否改变。如果这些值没有改变,那么这个组件不会更新。如果你的组件更复杂一些,你可以使用类似“浅比较”的模式来检查 props 和 state 中所有的字段,以此来决定是否组件需要更新。React 已经提供了一位好帮手来帮你实现这种常见的模式 - 你只要继承 React.PureComponent 就行了。所以这段代码可以改成以下这种更简洁的形式:

    class CounterButton extends React.PureComponent{
        constructor(props){
            super(props)
            this.state = {count:1}
        }
    
        render(){
            return (
                <button color={this.props.color} onClick={()=>({count:state.count+1})}>
                  Count:{this.state.count}
                </button>
            )
        }
    }
    

    大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。当数据结构很复杂时,情况会变得麻烦。例如,你想要一个 ListOfWords 组件来渲染一组用逗号分开的单词。它有一个叫做 WordAdder 的父组件,该组件允许你点击一个按钮来添加一个单词到列表中。以下代码并不正确:

    class ListOfWords extends React.PureComponent {
      render() {
        return <div>{this.props.words.join(',')}</div>
      }
    }
    
    class WordAdder extends React.Component {
      constructor(props) {
        this.state = {
          words: ['marklar'],
        }
        this.handleClick = this.handleClick.bind(this)
      }
      handleClick() {
        //这部分代码很遭 而且还有bug
        const words = this.state.words
        words.push('marklar')
        this.setState({ words: words })
      }
      render() {
          return (
              <div>
                <button onClick={this.handleClick}></button>
                <ListOfWords words={this.state.words}/>
              </div>
          )
      }
    }
    
    

    问题在于 PureComponent 仅仅会对新老 this.props.words 的值进行简单的对比。由于代码中 WordAdder 的 handleClick 方法改变了同一个 words 数组,使得新老 this.props.words 比较的其实还是同一个数组。即便实际上数组中的单词已经变了,但是比较结果是相同的。可以看到,即便多了新的单词需要被渲染, ListOfWords 却并没有被更新。

    不可变数据的力量

    避免该问题最简单的方式是避免更改你正用于 props 或 state 的值。例如,上面 handleClick 方法可以用 concat 重写:

    handleClick(){
        this.setState(state=>({
            words:state.words.concat(['marklar'])
        }))
    }
    

    你可以用类似的方式改写代码来避免可变对象的产生。例如,我们有一个叫做 colormap 的对象。我们希望写一个方法来将 colormap.right 设置为 ‘blue’。我们可以这么写:

    function updateColorMap(colormap) {
      colormap.right = 'blue';
    }
    

    为了不改变原本的对象,我们可以使用 Object.assign 方法:

    function updateColorMap(colormap){
        return Object.assign({},colormap,{right:'blue'})
    }
    

    现在 updateColorMap 返回了一个新的对象,而不是修改老对象。Object.assign 是 ES6 的方法,需要 polyfill。

    这里有一个 JavaScript 的提案,旨在添加对象扩展属性以使得更新不可变对象变得更方便:

    function updateColorMap(colorMap){
        return {...colormap,right:'blue'}
    }
    

    如果你在使用 Create React App,Object.assign 以及对象扩展运算符已经默认支持了。

    当处理深层嵌套对象时,以 immutable (不可变)的方式更新它们令人费解。如遇到此类问题,请参阅 Immer 或 immutability-helper。这些库会帮助你编写高可读性的代码,且不会失去 immutability (不可变性)带来的好处。

    Portals

    Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

    ReactDOM.createPortal(child, container)
    

    第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。
    用法
    通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点

    render(){
        //react 挂载了一个新的div  并且不会把子元素渲染其中
        return (
            <div>
             {this.props.children}
            </div>
        )
    }
    
    

    然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

    render(){
        //React 并没有创建一个新的div  它只是把子元素渲染到'domNode'中
        //domNode  是一个可以在任何位置有效的Dom节点
        return ReactDOM.createPortal(
            this.props.children,
            domNode
        )
    }
    

    一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

    在这里插入代码片
    

    注意:

    当在使用 portal 时, 记住管理键盘焦点就变得尤为重要。

    对于模态对话框,通过遵循 WAI-ARIA 模态开发实践,来确保每个人都能够运用它。

    通过 Portal 进行事件冒泡

    尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 仍存在于 React 树, 且与 DOM 树 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。

    这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。假设存在如下 HTML 结构:

    <html>
      <body>
        <div id="app-root"></div>
        <div id="modal-root"></div>
      </body>
    </html>
    

    在 #app-root 里的 Parent 组件能够捕获到未被捕获的从兄弟节点 #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 = docuemnt.createElement('div')
        }
        componentDidMount(){
            //在Modal 的所有子元素被挂载后
            //这个portal元素会被嵌入到Dom树中
            //这意味着子元素将被挂载到一个分离的DOM节点中
            //如果要求子组件在挂载时可以立即接入DOM树
            //比如衡量一个Dom节点
            // 或者在后代节点中使用‘autoFocus’,
            //则需要添加state到Modal中
            //仅当Modal被插入DOM树中才能渲染子组件
            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 = {click:0}
            this.handleClick = this.handleClick.bind(this)
        }
        handleClick(){
            //当子元素里的按钮被点击时
            //这个将会被触发更新父元素的state
            //即使这个按钮在DOM中不是直接关连的后代
            this.setState(state=>({
                clicks:state.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 的更为灵活的抽象。例如,如果你在渲染一个 组件,无论其是否采用 portal 实现,父组件都能够捕获其事件。

    Profiler

    Profiler 测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。

    注意
    Profiling 增加了额外的开支,所以它在生产构建中会被禁用。

    为了将 profiling 功能加入生产环境中,React 提供了使 profiling 可用的特殊的生产构建环境。 从 fb.me/react-profiling了解更多关于如何使用这个构建环境的信息。

    用法
    Profiler 能添加在 React 树中的任何地方来测量树中这部分渲染所带来的开销。 它需要两个 prop :一个是 id(string),一个是当组件树中的组件“提交”更新的时候被React调用的回调函数 onRender(function)。

    例如,为了分析 Navigation 组件和它的子代:

    render(){
        <App>
         <Profiler id="Navigation" onRender={callback}>
            <Navigation {...props}/>
         </Profiler>
          <Main {...props}/>
        </App>
    }
    

    多个 Profiler 组件能测量应用中的不同部分:

    render(
        <App>
            <Profiler id="Navigation" onRender={callback}>
             <Navigation {...props}/>
            </Profiler>
            <Profiler id="main" onRender={callback}>
                <Main {...props}/>        
            </Profiler>
        </App>
    )
    

    嵌套使用 Profiler 组件来测量相同一个子树下的不同组件。

    render(
        <App>
            <Profiler id="Panel" onRender={callback}>
                <Panel {...props}>
                    <Profiler id="Content" onRender={callback}>
                        <Content {...props}/>                
                    </Profiler>
                    <Profiler>
                        <PreviewPane {...props}/>               
                    </Profiler>
                </Panel>
            </Profiler>
        </App>
    )
    

    尽管 Profiler 是一个轻量级组件,我们依然应该在需要时才去使用它。对一个应用来说,每添加一些都会给 CPU 和内存带来一些负担。

    onRender 回调

    Profiler 需要一个 onRender 函数作为参数。 React 会在 profile 包含的组件树中任何组件 “提交” 一个更新的时候调用这个函数。 它的参数描述了渲染了什么和花费了多久。

    function onRenderCallback(
        id,//发生提交的Profiler树的id
        phase,//'mount'(如果组件树刚加载) 或者 ‘update’ (如果它重渲染了) 之一
        actualDuration, //本次committed 花费的渲染时间
        baseDuration,//估计不使用memoization的情况下渲染整颗子树需要的时间
        startTime,//本次更新React开始渲染的时间
        commitTime,//本次更新React committed的时间
        interactions//属于本次更新的interactions的集合
    ){
        //合计或记录渲染时间...
    }
    

    让我们来仔细研究一下各个 prop:

    • id: string - 发生提交的 Profiler 树的 id。 如果有多个 profiler,它能用来分辨树的哪一部分发生了“提交”。
      phase: “mount” | “update” - 判断是组件树的第一次装载引起的重渲染,还是由 props、state 或是 hooks 改变引起的重渲染。
      actualDuration: number - 本次更新在渲染 Profiler 和它的子代上花费的时间。 这个数值表明使用 memoization 之后能表现得多好。(例如 React.memo,useMemo,shouldComponentUpdate)。 理想情况下,由于子代只会因特定的 prop 改变而重渲染,因此这个值应该在第一次装载之后显著下降。
      baseDuration: number - 在 Profiler 树中最近一次每一个组件 render 的持续时间。 这个值估计了最差的渲染时间。(例如当它是第一次加载或者组件树没有使用 memoization)。
      startTime: number - 本次更新中 React 开始渲染的时间戳。
      commitTime: number - 本次更新中 React commit 阶段结束的时间戳。 在一次 commit 中这个值在所有的 profiler 之间是共享的,可以将它们按需分组。
      commitTime: number - 本次更新中 React commit 阶段结束的时间戳。 在一次 commit 中这个值在所有的 profiler 之间是共享的,可以将它们按需分组。

    注意

    Interactions 能用来识别更新是由什么引起的,尽管这个追踪更新的 API 依然是实验性质的。

    不使用 ES6

    通常我们会用 JavaScript 的 class 关键字来定义 React 组件:

    class Greeting extends React.Component{
        render(){
            return <h1> Hello,{this.props.name}</h1>
        }
    }
    

    如果你还未使用过 ES6,你可以使用 create-react-class 模块:

    var createReactClass = require('create-react-class')
    var Greeting = createReactClass({
        render:function(){
            return <h1>Hello,{this.props.name}</h1>
        }
    })
    

    ES6 中的 class 与 createReactClass() 方法十分相似,但有以下几个区别值得注意。

    声明默认属性

    无论是函数组件还是 class 组件,都拥有 defaultProps 属性:

    class Greeting extends React.Component{
        //...
    }
    
    Greeting.defaultProps = {
        name:'Mary'
    }
    

    如果使用 createReactClass() 方法创建组件,那就需要在组件中定义 getDefaultProps() 函数:

    var Greeting = createReactClass({
        getDefaultProps:function(){
            return {
                name:'Mary'
            }
        }
        //...
    })
    
    初始化 State

    如果使用 ES6 的 class 关键字创建组件,你可以通过给 this.state 赋值的方式来定义组件的初始 state:

    class Count extends React.Component{
        constructor(props){
            super(props)
            this.state = {count:props.initialCount}
        }
        // ...
    }
    

    如果使用 createReactClass() 方法创建组件,你需要提供一个单独的 getInitialState 方法,让其返回初始 state:

    var Counter = createReactClass({
        getInitialState:function(){
            return {count:this.props.initialCount}
        }
        //...
    })
    
    自动绑定

    对于使用 ES6 的 class 关键字创建的 React 组件,组件中的方法遵循与常规 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(){
            return(
                //由于`this.handleClick` 已经绑定到实例,因此我们才可以用它来处理点击事件
                <button onClick={this.handleClick}>Say Hello</button>
            )
        }
    }
    

    如果使用 createReactClass() 方法创建组件,组件中的方法会自动绑定至实例,所以不需要像上面那样做:

    var SayHello = createReactClass({
        getInitialState:function(){
            return {message:'Hello'}
        }
        handleClick:function(){
            alert(this.state.message)
        }
        render:function(){
            return (
                <button onClick={this.handleClick}>Say Hello</button>
            )
        }
    })
    

    这就意味着,如果使用 ES6 class 关键字创建组件,在处理事件回调时就要多写一部分代码。但对于大型项目来说,这样做可以提升运行效率。

    如果你觉得上述写法很繁琐,那么可以尝试使用目前还处于试验性阶段的 Babel 插件 Class Properties

    class SayHello extends React.Component{
        constructor(props){
            super(props)
            this.state = {message:'Hello'}
        }
        //警告: 这种语法还处于试验阶段
        //在这里使用箭头函数就可以把方法绑定给实例
        handleClick = () => {
            alert(this.state.message)
        }
        render(){
            return (
                <button onClick={this.handleClick}>Say SayHello</button>
            )
        }
    }
    

    请注意,上面这种语法目前还处于试验性阶段,这意味着语法随时都可能改变,也存在最终不被列入框架标准的可能。

    为了保险起见,以下三种做法都是可以的:

    • 在 constructor 中绑定方法。
    • 使用箭头函数,比如:onClick={(e) => this.handleClick(e)}。
    • 继续使用 createReactClass。
    Mixins

    注意

    ES6 本身是不包含任何 mixin 支持。因此,当你在 React 中使用 ES6 class 时,将不支持 mixins 。

    我们也发现了很多使用 mixins 然后出现了问题的代码库。并且不建议在新代码中使用它们

    如果完全不同的组件有相似的功能,这就会产生“横切关注点(cross-cutting concerns)“问题。针对这个问题,在使用createReactClass创建React组件的时候,引入mixins功能会是一个很好的解决方案。

    比较常见的用法是,组件每隔一段时间更新一次。使用setInterval()可以很容易实现这个功能,但需要注意的是,当你不再需要它时,你应该清除它以节省内存。React提供了生命周期方法,这样你就可以知道一个组件何时被创建或被销毁了。让我们创建一个简单的mixin,它使用这些方法提供一个简单的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('craete-react-class')
    
    var TickTock = createReactClass({
        mixins:[SetIntervalMixin],//使用mixin
        getInitialState:function(){
            return {second: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')
    )
    

    如果组件拥有多个mixins,且这些mixins 中定义了相同的生命周期方法(例如,当组件被销毁时,几个mixins 都想要进行一些清理工作),那么这些生命周期方法都会被调用的。使用mixins 时,mixins 会先按照定义时的顺序执行,最后调用组件上对应的方法。

    不使用JSX 的React

    React 并不强制要求使用JSX。当你不想在构建环境中配置有关JSX 编译时,不在React 中使用JSX 会更加方便。
    每个JSX元素只是调用React.createElement(component, props, …children)的语法糖。因此,使用JSX可以完成的任何事情都可以通过纯JavaScript完成。

    例如,用JSX 编写的代码:

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

    如果你想了解更多JSX转换为JavaScript的示例,可以尝试使用在线Babel编译器。
    在线编译

    组件可以是字符串,也可以是React.Component的子类,它还能是一个普通的函数。

    如果你不想每次都键入React.createElement,通常的做法是创建快捷方式:

    const e = React.createElement;
    ReactDOM.render(
        e('div',null,'Hello,World'),
        document.getElementById('root')
    )
    

    如果你使用了React.createElement的快捷方式,那么在没有JSX的情况下使用React几乎一样方便。

    或者,你也可以参考社区项目,如:react-hyperscript和hyperscript-helpers,它们提供了更简洁的语法。

    协调

    React 提供的声明式API 让开发者可以在对React 的底层实现没有具体了解的情况下编写应用。在开发者编写应用时虽然保持相对简单的心智,但开发者无法了解内部的实现机制。本文描述了在实现React 的“diffing” 算法中我们做出的设计决策以保证组件满足更新具有可预测性,以及在繁杂业务下依然保持应用的高性能性。

    设计动力

    在某一时间节点调用React的render()方法,会创建一棵由React元素组成的树。在下一次state或props更新时,相同的render()方法会返回一棵不同的树。React需要基于这两棵树之间的差别来判断如何有效率的更新UI以保证当前UI与最新的树保持同步。
    这个算法问题有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作数。然而,即使在最前沿的算法中,该算法的复杂程度为O(n 3 ),其中n是树中元素的数量。
    果在React 中使用了该算法,那么展示1000 个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。于是React 在以下两个假设的基础之上提出了一套O(n) 的启发式算法:

    *两个不同类型的元素会产生出不同的树;

    • 开发者可以通过keyprop来暗示哪些子元素在不同的渲染下能保持稳定;

    在实践中,我们发现以上假设在几乎所有实用的场景下都成立。

    Diffing 算法

    当对比两颗树时,React 首先比较两棵树的根节点。不同类型的根节点元素会有不同的形态。

    比对不同类型的元素

    当根节点为不同类型的元素时,React会拆卸原有的树并且建立起新的树。举个例子,当一个元素从变成,从

    变成,或从变成
    都会触发一个完整的重建流程。

    当拆卸一棵树时,对应的DOM节点也会被销毁。组件实例将执行componentWillUnmount()方法。当建立一棵新的树时,对应的DOM节点会被创建以及插入到DOM中。组件实例将执行componentWillMount()方法,紧接着componentDidMount()方法。所有跟之前的树所关联的state也会被销毁。

    在根节点以下的组件也会被卸载,它们的状态会被销毁。比如,当比对以下更变时:

    <div>
      <Counter />
    </div>
    
    <span>
      <Counter />
    </span>
    

    React会销毁Counter组件并且重新装载一个新的组件。

    比对同一类型的元素

    当比对两个相同类型的React 元素时,React 会保留DOM 节点,仅比对及更新有改变的属性。比如:

    <div className="before" title="stuff" />
    
    <div className="after" title="stuff" />
    
    

    通过比对这两个元素,React知道只需要修改DOM元素上的className属性。

    当更新style属性时,React仅更新有所更变的属性。比如:

    <div style={{color: 'red', fontWeight: 'bold'}} />
    
    <div style={{color: 'green', fontWeight: 'bold'}} />
    

    通过比对这两个元素,React知道只需要修改DOM元素上的color样式,无需修改fontWeight。

    在处理完当前节点之后,React 继续对子节点进行递归。

    比对同类型的组件元素

    当一个组件更新时,组件实例保持不变,这样state在跨越不同的渲染时保持一致。React将更新该组件实例的props以跟最新的元素保持一致,并且调用该实例的 componentWillReceiveProps()和componentWillUpdate()方法。

    下一步,调用render()方法,diff算法将在之前的结果以及新的结果中进行递归。

    对子节点进行递归

    在默认条件下,当递归DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个mutation。

    在子元素列表末尾新增元素时,更变开销比较小。比如:

    <ul>
      <li>first</li>
      <li>second</li>
    </ul>
    
    <ul>
      <li>first</li>
      <li>second</li>
      <li>third</li>
    </ul>
    

    React会先匹配两个

    • first
    • 对应的树,然后匹配第二个元素
    • second
    • 对应的树,最后插入第三个元素的
    • third
    • 树。

      如果简单实现的话,那么在列表头部插入会很影响性能,那么更变开销会比较大。比如:

      <ul>
        <li>Duke</li>
        <li>Villanova</li>
      </ul>
      
      <ul>
        <li>Connecticut</li>
        <li>Duke</li>
        <li>Villanova</li>
      </ul>
      

      React会针对每个子元素mutate而不是保持相同的

    • Duke
    • Villanova
    • 子树完成。这种情况下的低效可能会带来性能问题。

      Keys

      为了解决以上问题,React支持key属性。当子元素拥有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 并不困难。你要展现的元素可能已经有了一个唯一ID,于是key 可以直接从你的数据中提取:

      <li key={item.id}>{item.name}</li>
      

      当以上情况不成立时,你可以新增一个ID 字段到你的模型中,或者利用一部分内容作为哈希值来生成一个key。这个key 不需要全局唯一,但在列表中需要保持唯一。

      最后,你也可以使用元素在数组中的下标作为key。这个策略在元素不进行重新排序时比较合适,但一旦有顺序修改,diff 就会变得慢。

      当基于下标的组件进行重新排序时,组件state 可能会遇到一些问题。由于组件实例是基于它们的key 来决定是否更新以及复用,如果key 是一个下标,那么修改顺序时会修改当前的key,导致非受控组件的state(比如输入框)可能相互篡改导致无法预期的变动。
      ** 不使用下标作为 key 的例子的版本**

      const ToDo = props => (
          <tr>
           <td>
              <label>{props.id}</label>
           </td>
           <td>
              <input />      
           </td>
           <td>
             <label>
              {props.createdAt.toTimeString()}       
             </label>
           </td>
          </tr>
      )
      
      class ToDoList extends React.Component{
          constructor(props){
              super(props)
              const date = new Date()
              const todoCounter = 1
              this.state = {
                  todoCounter:todoCounter,
                  list:[
                      {
                          id:todoCounter,
                          createdAt:date
                      }
                  ]
              }
          }
          sortByEarliest(){
              const sortedList = this.state.list.sort((a,b)=>{
                  retrun a.createdAt - b.createdAt
              })
              this.setState({
                  list:[...sortedList]
              })
          }
          sortByLatest(){
              const sortedList = this.state.list.sort((a,b)=>{
                  retrun a.createdAt - b.createdAt
              })
              this.setState({
                  list:[...sortedList]
              })
          }
          addToEnd(){
              const date = new Date()
              const nextId = this.state.todoCounter + 1
              const newList = [
                  {
                      id:nextId,
                      createdAt:date
                  },
                  ...this.state.list
              ]
              this.setState({
                  list:newList,
                  todoCounter:nextId
              })
          }
          addToStart(){
              const date = new Date()
              const nextId = this.state.todoCounter + 1
              const newList = [
                  {id:nextId,createdAt:date},
                  ...this.state.list
              ]
              this.setState({
                  list:newList,
                  todoCounter:nextId
              })
          }
          render(){
              retrun(
                  <div>
                    <code>
                      key=index
                    </code>
                    <br/>
                    <button onClick={this.addToStart.bind(this)}>Add New to Start</button>
                    <button onclick={this.addToEnd.bind(this)}>Add New to End</button>
                    <button onClick={this.sortByEarliest.bind(this)}>Sort by Earliest</button>
                    <button onClick={this.sortByLatest.bind(this)}>Sort by Latest</button>
                    <table>
                       <tr>
                         <th>ID</th>
                         <th />
                         <th>
                           created at
                         </th>
                       </tr>
                         {this.state.list.map((todo,index)=>(
                                <ToDo key={todo.id} {...todo}/>
                            ))
                         }
                    </table>
                  </div>
              )
          }
      }
      
      ReactDOM.render(
          <ToDoList />,
          document.getElementById('root')
      )
      
      权衡

      请谨记协调算法是一个实现细节。React 可以在每个 action 之后对整个应用进行重新渲染,得到的最终结果也会是一样的。在此情境下,重新渲染表示在所有组件内调用 render 方法,这不代表 React 会卸载或装载它们。React 只会基于以上提到的规则来决定如何进行差异的合并。

      我们定期探索优化算法,让常见用例更高效地执行。在当前的实现中,可以理解为一棵子树能在其兄弟之间移动,但不能移动到其他位置。在这种情况下,算法会重新渲染整棵子树。

      由于 React 依赖探索的算法,因此当以下假设没有得到满足,性能会有所损耗。

      • 该算法不会尝试匹配不同组件类型的子树。如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型。在实践中,我们没有遇到这类问题。
      • Key 应该具有稳定,可预测,以及列表内唯一的特质。不稳定的 key(比如通过 Math.random() 生成的)会导致许多组件实例和 DOM 节点被不必要地重新创建,这可能导致性能下降和子组件中的状态丢失。

      Refs and the DOM

      Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素

      在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。

      何时使用 Refs
      下面是几个适合使用 refs 的情况:

      • 管理焦点,文本选择或媒体播放。
      • 触发强制动画。
      • 集成第三方 DOM 库。

      避免使用 refs 来做任何可以通过声明式实现来完成的事情。

      举个例子,避免在 Dialog 组件里暴露 open() 和 close() 方法,最好传递 isOpen 属性。

      勿过度使用 Refs

      你可能首先会想到使用 refs 在你的 app 中“让事情发生”。如果是这种情况,请花一点时间,认真再考虑一下 state 属性应该被安排在哪个组件层中。通常你会想明白,让更高的组件层级拥有这个 state,是更恰当的。查看 状态提升 以获取更多有关示例。

      下面的例子已经更新为使用在 React 16.3 版本引入的 React.createRef() API。如果你正在使用一个较早版本的 React,我们推荐你使用回调形式的 refs。

      创建 Refs
      Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。

      class MyComponent extends React.Component{
          constructor(props){
              super(props)
              this.myRef = React.createRef()
          }
          render(){
              return <div ref={this.myRef}/>
          }
      }
      
      访问 Refs

      当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

      const node = this.myRef.current;
      

      ref 的值根据节点的类型而有所不同:

      • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
      • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
      • 你不能在函数组件上使用 ref 属性,因为他们没有实例。

      以下例子说明了这些差异。

      为 DOM 元素添加 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 会在组件挂载时给 current 属性传入 DOM 元素,并在组件卸载时传入 null 值。ref 会在 componentDidMount 或 componentDidUpdate 生命周期钩子触发前更新。

      为 class 组件添加 Ref

      如果我们想包装上面的 CustomTextInput,来模拟它挂载之后立即被点击的操作,我们可以使用 ref 来获取这个自定义的 input 组件并手动调用它的 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}/>
              )
          }
      }
      

      请注意,这仅在 CustomTextInput 声明为 class 时才有效:

      class CustomTextInput extends React.Component {
        // ...
      }
      
      Refs 与函数组件

      默认情况下,你不能在函数组件上使用 ref 属性,因为它们没有实例:

      function MyFunctionComponent(){
          return <input />
      }
      
      class Parent extends React.Component{
          constructor(props){
              super(props)
              this.textInput = React.createRef()
          }
          render(){
              return(
                  //this will not work !
                  <MyFunctionComponent ref={this.textInput}/>
              )
          }
      }
      

      如果要在函数组件中使用 ref,你可以使用 forwardRef(可与 useImperativeHandle 结合使用),或者可以将该组件转化为 class 组件。

      不管怎样,你可以在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件:

      function CustomTextInput(props){
          //这里必须声明textInput  这样ref才可以引用它
          const textInput = useRef(null)
          function handleClick(){
              textInput.current.focus()
          }
      
          return (
              <div>
                  <input type="text" ref={textInput}/> 
                  <input type="button" value="Focus the text input" onClick={handleClick}/> 
              </div>
          )
      }
      
      将 DOM Refs 暴露给父组件

      在极少数情况下,你可能希望在父组件中引用子节点的 DOM 节点。通常不建议这样做,因为它会打破组件的封装,但它偶尔可用于触发焦点或测量子 DOM 节点的大小或位置。
      虽然你可以向子组件添加 ref,但这不是一个理想的解决方案,因为你只能获取组件实例而不是 DOM 节点。并且,它还在函数组件上无效。

      如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发。Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。关于怎样对父组件暴露子组件的 DOM 节点,在 ref 转发文档中有一个详细的例子。

      如果你使用 16.2 或更低版本的 React,或者你需要比 ref 转发更高的灵活性,你可以使用这个替代方案将 ref 作为特殊名字的 prop 直接传递

      可能的话,我们不建议暴露 DOM 节点,但有时候它会成为救命稻草。注意这个方案需要你在子组件中增加一些代码。如果你对子组件的实现没有控制权的话,你剩下的选择是使用 findDOMNode(),但在严格模式 下已被废弃且不推荐使用。

      回调 Refs

      React 也支持另一种设置 refs 的方式,称为“回调 refs”。它能助你更精细地控制何时 refs 被设置和解除。

      不同于传递 createRef() 创建的 ref 属性,你会传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。

      下面的例子描述了一个通用的范例:使用 ref 回调函数,在实例的属性中存储对 DOM 节点的引用。

      class CustomTextInput extends React.Component{
          constructor(props){
              super(props)
              this.textInput = null
              this.setTextInputRef = element =>{
                  this.textInput = element
              }
              this.foucsTextInput = () => {
                  //使用原生DOM API使text输入框获得焦点、
                  if(this.textInput) this.textInput.focus()
              }
          }
          componentDidMount(){
              //组件挂载后 让文本自动获取焦点
              this.foucsTextInput()
          }
          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.foucsTextInput}/>
                  </div> 
              )
          }
      }
      

      React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 refs 一定是最新的。

      你可以在组件间传递回调形式的 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.inputRefElement = el}/>
              )
          }
      }
      

      在上面的例子中,Parent 把它的 refs 回调函数当作 inputRef props 传递给了 CustomTextInput,而且 CustomTextInput 把相同的函数作为特殊的 ref 属性传递给了 。结果是,在 Parent 中的 this.inputElement 会被设置为与 CustomTextInput 中的 input 元素相对应的 DOM 节点。

      过时 API:String 类型的 Refs

      如果你之前使用过 React,你可能了解过之前的 API 中的 string 类型的 ref 属性,例如 “textInput”。你可以通过 this.refs.textInput 来访问 DOM 节点。我们不建议使用它,因为 string 类型的 refs 存在 一些问题。它已过时并可能会在未来的版本被移除。

      注意

      如果你目前还在使用 this.refs.textInput 这种方式访问 refs ,我们建议用回调函数或 createRef API 的方式代替。

      关于回调 refs 的说明

      如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。

      Render Props

      术语 “render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
      具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。

      <DataProvider render={data=>(
          <h1>Hello,{data.target}</h1>
      )}/>
      

      使用 render prop 的库有 React Router、Downshift 以及 Formik。

      在这个文档中,我们将讨论为什么 render prop 是有用的,以及如何写一个自己的 render prop 组件。

      使用 Render Props 来解决横切关注点(Cross-Cutting Concerns)

      组件是 React 代码复用的主要单元,但如何分享一个组件封装到其他需要相同 state 组件的状态或行为并不总是很容易。

      例如,以下组件跟踪 Web 应用程序中的鼠标位置:

      class MouseTracker 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}>
                    <h1>移动鼠标</h1>
                    <p>当前的鼠标位置是({this.state.x},{this.state.y})</p>
                  </div>
              )
          }
      }
      

      当光标在屏幕上移动时,组件在

      中显示其(x,y)坐标。

      现在的问题是:我们如何在另一个组件中复用这个行为?换个说法,若另一个组件需要知道鼠标位置,我们能否封装这一行为,以便轻松地与其他组件共享它??

      由于组件是 React 中最基础的代码复用单元,现在尝试重构一部分代码使其能够在 组件中封装我们需要共享的行为。

      //<Mouse>组件封装了我们需要的行为...
      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}>
                    {/* ...但我们如何渲染<p>以外的东西 */}
                    <p>The current mouse position is({this.state.x},{this.state.y})</p>
                  </div>
              )
          }
          
      }
      
      class MouseTracker extends React.Component{
          render(){
              return (
                  <>
                    <h1>移动鼠标</h1>
                    <Mouse/> 
                  </>
              )
          }
      }
      

      现在 组件封装了所有关于监听 mousemove 事件和存储鼠标 (x, y) 位置的行为,但其仍不是真正的可复用。

      举个例子,假设我们有一个 组件,它可以呈现一张在屏幕上追逐鼠标的猫的图片。我们或许会使用 <Cat mouse={{ x, y }} 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 MouseWithCat extends React.Component{
          constructor(prop){
              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}>
                    {/* 我们可以在这里换掉<p>的<Cat>
                       但是接着我们需要创建一个单独的<MouseWithSomethingElse>
                       每次我们需要使用它时 <MouseWithCat> 是不是真的可以重复使用
                     */}
                     <Cat mouse={this.state}/>
                  </div>
              )
          }
      
      }
      
      class MouseTracker extends React.Component{
          render(){
              return(
                  <div>
                    <h1>移动鼠标</h1>
                    <MouseWithCat/>
                  </div>
              )
          }
      }
      

      这种方法适用于我们的特定用例,但我们还没有达到以可复用的方式真正封装行为的目标。现在,每当我们想要鼠标位置用于不同的用例时,我们必须创建一个新的组件(本质上是另一个 ),它专门为该用例呈现一些东西.

      这也是 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 MouseWithCat extends React.Component{
          constructor(prop){
              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}>
                    {/* 我们可以在这里换掉<p>的<Cat>
                       但是接着我们需要创建一个单独的<MouseWithSomethingElse>
                       每次我们需要使用它时 <MouseWithCat> 是不是真的可以重复使用
                     */}
                      {this.props.render(this.state)}
                  </div>
              )
          }
      
      }
      
      class MouseTracker extends React.Component{
          render(){
              return(
                  <div>
                    <h1>移动鼠标</h1>
                    <Mouse render={mouse=>(
                        <Cat mouse={mouse}/>
                    )}/>
                  </div>
              )
          }
      }
      
      

      现在,我们提供了一个 render 方法 让 能够动态决定什么需要渲染,而不是克隆 组件然后硬编码来解决特定的用例。

      更具体地说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
      这项技术使我们共享行为非常容易。要获得这个行为,只要渲染一个带有 render prop 的 组件就能够告诉它当前鼠标坐标 (x, y) 要渲染什么。
      关于 render prop 一个有趣的事情是你可以使用带有 render prop 的常规组件来实现大多数高阶组件 (HOC)。 例如,如果你更喜欢使用 withMouse HOC而不是 组件,你可以使用带有 render prop 的常规 轻松创建一个:

      //如果你处于某种原因真的想要HOC  那么你可以轻松实现
      //使用具有render prop的普通组件创建一个
      function withMouse(component){
          return class extends React.component{
              render(){
                  return(
                      <Mouse render={mouse=>(
                          <Component {...this.props} mouse={mouse}>
                      )}/>
                  )
              }
          }
      }
      

      因此,你可以将任一模式与 render prop 一起使用。

      使用 Props 而非 render

      重要的是要记住,render prop 是因为模式才被称为 render prop ,你不一定要用名为 render 的 prop 来使用这种模式。事实上, 任何被用于告知组件需要渲染什么内容的函数 prop 在技术上都可以被称为 “render prop”.

      尽管之前的例子使用了 render,我们也可以简单地使用 children prop!

      <Mouse children={mouse=>(
          <p>
            鼠标的位置是{mouse.x},{mouse.y}
          </p>
      )}/>
      

      记住,children prop 并不真正需要添加到 JSX 元素的 “attributes” 列表中。相反,你可以直接放置到元素的内部!

      <Mouse>
       {mouse=>(
           <p>鼠标的位置是{mouse.x},{mouse.y}</p>
       )}
      </Mouse>
      

      你将在 react-motion 的 API 中看到此技术。

      由于这一技术的特殊性,当你在设计一个类似的 API 时,你或许会要直接地在你的 propTypes 里声明 children 的类型应为一个函数。

      Mouse.propTypes = {
          children:PropTypes.func.isRequired
      }
      
      注意事项

      将 Render Props 与 React.PureComponent 一起使用时要小心
      如果你在 render 方法里创建函数,那么使用 render prop 会抵消使用 React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render prop 将会生成一个新的值。
      例如,继续我们之前使用的 组件,如果 Mouse 继承自 React.PureComponent 而不是 React.Component,我们的例子看起来就像这样:

      class Mouse extends React.PureComponent {
        // 与上面相同的代码......
      }
      
      class MouseTracker extends React.Component {
        render() {
          return (
            <div>
              <h1>Move the mouse around!</h1>
      
              {/*
                这是不好的!
                每个渲染的 `render` prop的值将会是不同的。
              */}
              <Mouse render={mouse => (
                <Cat mouse={mouse} />
              )}/>
            </div>
          );
        }
      }
      

      在这样例子中,每次 渲染,它会生成一个新的函数作为 的 prop,因而在同时也抵消了继承自 React.PureComponent 的 组件的效果!

      为了绕过这一问题,有时你可以定义一个 prop 作为实例方法,类似这样:

      class MouseTracker extends React.Component{
          //定义为实例方法 this.renderTheCat始终
          //当我们在渲染使用它时 它指的是相同函数
          renderTheCat(mouse){
              return <Cat mouse={mouse}/>
          }
          render(){
              return(
                  <div>
                  <h1> Mouse the mouse around</h1>
                   <Mouse render={this.renderTheCat}/>
                  </div>
              )
          }
      }
      

      如果你无法静态定义 prop(例如,因为你需要关闭组件的 props 和/或 state),则 应该扩展 React.Component。

      静态类型检查

      像 Flow 和 TypeScript 等这些静态类型检查器,可以在运行前识别某些类型的问题。他们还可以通过增加自动补全等功能来改善开发者的工作流程。出于这个原因,我们建议在大型代码库中使用 Flow 或 TypeScript 来代替 PropTypes。

      Flow

      Flow 是一个针对 JavaScript 代码的静态类型检测器。Flow 由 Facebook 开发,经常与 React 一起使用。Flow 通过特殊的类型语法为变量,函数,以及 React 组件提供注解,帮助你尽早地发现错误。你可以阅读 introduction to Flow 来了解它的基础知识
      完成以下步骤,便可开始使用 Flow:

      • 将 Flow 添加到你的项目依赖中。
      • 确保编译后的代码中去除了 Flow 语法。
      • 添加类型注解并且运行 Flow 来检查它们。
        下面我们会详细解释这些步骤。
      在项目中添加 Flow

      首先,在终端中进入到项目根目录下。然后你需要执行以下命令:

      如果你使用 Yarn,执行:

      yarn add --dev flow-bin
      

      如果你使用 npm,执行:

      npm install --save-dev flow-bin
      

      这个命令将在你的项目中安装最新版的 Flow。
      接下来,将 flow 添加到项目 package.json 的 “scripts” 部分,以便能够从终端命令行中使用它:

      {
        // ...
        "scripts": {
          "flow": "flow",
          // ...
        },
        // ...
      }
      

      最后,执行以下命令之一:

      如果你使用 Yarn,执行:

      yarn run flow init
      

      如果你使用 npm,执行:

      npm run flow init
      

      这条命令将生成你需要提交的 Flow 配置文件。

      从编译后的代码中去除 Flow 语法

      Flow 通过这种类型注释的特殊语法扩展了 JavaScript 语言。但是,浏览器不能够解析这种语法,所以我们需要确保它不会被编译到在浏览器执行的 JavaScript bundle 中。

      具体方法取决于你使用的 JavaScript 编译工具。

      Create React App
      如果你的项目使用的是 Create React App,那么 Flow 注解默认会被去除,所以在这一步你不需要做任何事情。

      Babel
      注意:

      这些说明不适用于使用 Create React App 的用户。虽然 Create React App 底层也使用了 Babel,但它已经配置了去除 Flow。如果你没有使用 Create React App,请执行此步骤。
      如果你的项目手动配置了 Babel,你需要为 Flow 安装一个特殊的 preset。

      如果你使用 Yarn,执行:

      yarn add --dev @babel/preset-flow
      

      如果你使用 npm,执行:

      npm install --save-dev @babel/preset-flow
      

      接下来将 flow preset 添加到你的 Babel 配置 配置中。例如,如果你通过 .babelrc 文件配置 Babel,它可能会如下所示:

      {
        "presets": [
          "@babel/preset-flow",
          "react"
        ]
      }
      

      这将让你可以在代码中使用 Flow 语法。

      注意:

      Flow 不需要 react preset,但他们经常一起使用。Flow 内置了 JSX 的语法识别。

      其他构建工具设置
      如果没有使用 Create React App 或 Babel 来构建项目,可以通过 flow-remove-types 去除类型注解。

      运行 Flow
      如果你按照上面的说明操作,你应该能运行 Flow 了。

      yarn flow
      

      如果你使用 npm,执行:

      npm run flow
      

      你应该会看到如下消息:

      No errors!
      ✨  Done in 0.17s.
      

      添加 Flow 类型注释
      默认情况下,Flow 仅检查包含此注释的文件:

      // @flow
      通常,它位于文件的顶部。试着将其添加到项目的某些文件中,然后运行 yarn flow 或 npm run flow 来查看 Flow 是否已经发现了一些问题。

      还可以通过这个选项开启所有文件(包括没有注解的文件)的强制检查。通过 Flow 来检查全部文件对于现有的项目来说,可能导致大量修改,但对于希望完全集成 Flow 的新项目来说开启这个选项比较合理。

      现在一切就绪!我们建议你查看以下资源来了解有关 Flow 的更多信息:

      Flow 文档:类型注解
      Flow 文档:编辑器
      Flow 文档:React
      在 Flow 中进行 lint
      https://react.docschina.org/docs/static-type-checking.html

      TypeScript

      TypeScript是一种由微软开发的编程语言。它是 JavaScript 的一个类型超集,包含独立的编译器。作为一种类型语言,TypeScript 可以在构建时发现 bug 和错误,这样程序运行时就可以避免此类错误。您可以通过 此文档了解更多有关在 React 中使用 TypeScript 的知识。

      完成以下步骤,便可开始使用 TypeScript:

      将 TypeScript 添加到你的项目依赖中。
      配置 TypeScript 编译选项
      使用正确的文件扩展名
      为你使用的库添加定义
      下面让我们详细地介绍一下这些步骤:

      在 Create React App 中使用 TypeScript

      Create React App 内置了对 TypeScript 的支持。

      需要创建一个使用 TypeScript 的新项目,在终端运行:

      npx create-react-app my-app --template typescript
      

      如需将 TypeScript 添加到现有的 Create React App 项目中,.
      请参考此文档
      注意:

      如果你使用的是 Create React App,可以跳过本节的其余部分。其余部分讲述了不使用 Create React App 脚手架,手动配置项目的用户。

      添加 TypeScript 到现有项目中

      这一切都始于在终端中执行的一个命令。

      如果你使用 Yarn,执行:

      yarn add --dev typescript
      

      如果你使用 npm,执行:

      npm install --save-dev typescript
      

      恭喜!你已将最新版本的 TypeScript 安装到项目中。安装 TypeScript 后我们就可以使用 tsc 命令。在配置编译器之前,让我们将 tsc 添加到 package.json 中的 “scripts” 部分:

      {
        // ...
        "scripts": {
          "build": "tsc",
          // ...
        },
        // ...
      }
      
      
      配置 TypeScript 编译器

      没有配置项,编译器提供不了任何帮助。在 TypeScript 里,这些配置项都在一个名为 tsconfig.json 的特殊文件中定义。可以通过执行以下命令生成该文件:

      如果你使用 Yarn,执行:

      yarn run tsc --init
      

      如果你使用 npm,执行:

      npx 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
      如果你按照上面的说明操作,现在应该能运行 TypeScript 了。

      yarn build
      

      如果你使用 npm,执行:

      npm run build
      

      如果你没有看到输出信息,这意味着它编译成功了。

      类型定义

      为了能够显示来自其他包的错误和提示,编译器依赖于声明文件。声明文件提供有关库的所有类型信息。这样,我们的项目就可以用上像 npm 这样的平台提供的三方 JavaScript 库。

      获取一个库的声明文件有两种方式:
      Bundled - 该库包含了自己的声明文件。这样很好,因为我们只需要安装这个库,就可以立即使用它了。要知道一个库是否包含类型,看库中是否有 index.d.ts 文件。有些库会在 package.json 文件的 typings 或 types 属性中指定类型文件。

      DefinitelyTyped - DefinitelyTyped 是一个庞大的声明仓库,为没有声明文件的 JavaScript 库提供类型定义。这些类型定义通过众包的方式完成,并由微软和开源贡献者一起管理。例如,React 库并没有自己的声明文件。但我们可以从 DefinitelyTyped 获取它的声明文件。只要执行以下命令。

      # yarn
      yarn add --dev @types/react
      
      # npm
      npm i --save-dev @types/react
      

      局部声明 有时,你要使用的包里没有声明文件,在 DefinitelyTyped 上也没有。在这种情况下,我们可以创建一个本地的定义文件。因此,在项目的根目录中创建一个 declarations.d.ts 文件。一个简单的声明可能是这样的:

      declare module 'querystring' {
        export function stringify(val: object): string
        export function parse(val: string): object
      }
      

      你现在已做好编码准备了!我们建议你查看以下资源来了解有关 TypeScript 的更多知识:

      TypeScript 文档:基本类型
      TypeScript 文档:JavaScript 迁移
      TypeScript 文档:React 与 Webpack

      Reason

      Reason 不是一种新的语言;它是一种新的语法和工具链,底层使用的是经过实战验证的 OCaml 语言。Reason 在 OCaml 之上提供了 JavaScript 程序员的熟悉语法,而且集成了现有的 NPM/Yarn 工作流。

      Reason 是由 Facebook 开发,并且运用在一些现有产品中比如 Messager。虽然它有一定的实验性质,但它拥有由 Facebook 维护的专门的 React 绑定和一个活跃的社区。

      Kotlin

      Kotlin 是由 JetBrains 开发的一门静态类型语言。其目标平台包括 JVM、Android、LLVM 和 JavaScript。

      JetBrains 专门为 React 社区开发和维护了几个工具:React bindings 以及 Create React Kotlin App。后者可以通过 Kotlin 快速编写 React 应用程序,并且不需要构建配置。

      其他语言

      注意,还有其他静态类型语言可以编译成 JavaScript,也与 React 兼容。例如,和 elmish-react 一起使用的 F#/Fable。查看他们各自的网站以获取更多信息,并欢迎添加更多和与 React 结合的静态类型语言到这个页面!

      严格模式

      StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。

      注意:

      严格模式检查仅在开发模式下运行;它们不会影响生产构建。

      你可以为应用程序的任何部分启用严格模式。例如:

      import React from 'react'
      function ExampleApplication(){
          render(){
              <div>
                 <Header />
                 <React.StrickMode>
                    <div>
                      <ComponentOne />
                      <ComponentTwo />
                    </div>
                 </React.StrickMode>
              </div>
          }
      }
      

      在上述的示例中,不会对 Header 和 Footer 组件运行严格模式检查。但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查。

      StrictMode 目前有助于:

      • 识别不安全的生命周期
      • 关于使用过时字符串 ref API 的警告
      • 关于使用废弃的 findDOMNode 方法的警告
      • 检测意外的副作用
      • 检测过时的 context API

      未来的 React 版本将添加更多额外功能。

      识别不安全的生命周期

      正如这篇博文所述,某些过时的生命周期方法在异步 React 应用程序中使用是不安全的。但是,如果你的应用程序使用了第三方库,很难确保它们不使用这些生命周期方法。幸运的是,严格模式可以帮助解决这个问题!

      当启用严格模式时,React 会列出使用了不安全生命周期方法的所有 class 组件,并打印一条包含这些组件信息的警告消息,如下所示:

      strict mode unsafe lifecycles warning

      此时解决项目中严格模式所识别出来的问题,会使得在未来的 React 版本中使用 concurrent 渲染变得更容易。

      关于使用过时字符串 ref API 的警告

      以前,React 提供了两种方法管理 refs 的方式:已过时的字符串 ref API 的形式及回调函数 API 的形式。尽管字符串 ref API 在两者中使用更方便,但是它有一些缺点,因此官方推荐采用回调的方式。

      React 16.3 新增了第三种选择,它提供了使用字符串 ref 的便利性,并且不存在任何缺点:

      class MyComponent extends React.Component{
          constructor(props){
              super(props)
              this.inputRef = React.createRef()
          }
          render(){
              return <input type="text" ref={this.inputRef}/> 
          }
          componentDidMount(){
              this.inputRef.current.focus()
          }
      }
      

      由于对象 ref 主要是为了替换字符串 ref 而添加的,因此严格模式现在会警告使用字符串 ref。

      注意:

      除了新增加的 createRef API,回调 ref 依旧适用。

      你无需替换组件中的回调 ref。它们更灵活,因此仍将作为高级功能保留。

      关于使用废弃的 findDOMNode 方法的警告

      React 支持用 findDOMNode 来在给定 class 实例的情况下在树中搜索 DOM 节点。通常你不需要这样做,因为你可以将 ref 直接绑定到 DOM 节点。

      findDOMNode 也可用于 class 组件,但它违反了抽象原则,它使得父组件需要单独渲染子组件。它会产生重构危险,你不能更改组件的实现细节,因为父组件可能正在访问它的 DOM 节点。findDOMNode 只返回第一个子节点,但是使用 Fragments,组件可以渲染多个 DOM 节点。findDOMNode 是一个只读一次的 API。调用该方法只会返回第一次查询的结果。如果子组件渲染了不同的节点,则无法跟踪此更改。因此,findDOMNode 仅在组件返回单个且不可变的 DOM 节点时才有效。

      你可以通过将 ref 传递给自定义组件并使用 ref 转发来将其传递给 DOM 节点。
      你也可以在组件中创建一个 DOM 节点的 wrapper,并将 ref 直接绑定到它。

      class MyComponent extends React.Component{
          constructor(props){
              super(props)
              this.wrapper = React.createRef()
          }
          render(){
              return <div ref={this.wrapper}>{this.props.children}</div>
          }
      }
      

      注意:

      在 CSS 中,如果你不希望节点成为布局的一部分,则可以使用 display: contents 属性。

      检测意外的副作用

      从概念上讲,React 分两个阶段工作:

      渲染 阶段会确定需要进行哪些更改,比如 DOM。在此阶段,React 调用 render,然后将结果与上次渲染的结果进行比较。
      提交 阶段发生在当 React 应用变化时。(对于 React DOM 来说,会发生在 React 插入,更新及删除 DOM 节点的时候。)在此阶段,React 还会调用 componentDidMount 和 componentDidUpdate 之类的生命周期方法。

      提交阶段通常会很快,但渲染过程可能很慢。因此,即将推出的 concurrent 模式 (默认情况下未启用) 将渲染工作分解为多个部分,对任务进行暂停和恢复操作以避免阻塞浏览器。这意味着 React 可以在提交之前多次调用渲染阶段生命周期的方法,或者在不提交的情况下调用它们(由于出现错误或更高优先级的任务使其中断)。

      渲染阶段的生命周期包括以下 class 组件方法:

      • constructor
      • componentWillMount (or UNSAFE_componentWillMount)
      • componentWillReceiveProps (orUNSAFE_componentWillReceiveProps)
      • componentWillUpdate (or UNSAFE_componentWillUpdate)
      • getDerivedStateFromProps
      • shouldComponentUpdate
      • render
      • setState 更新函数(第一个参数)

      因为上述方法可能会被多次调用,所以不要在它们内部编写副作用相关的代码,这点非常重要。忽略此规则可能会导致各种问题的产生,包括内存泄漏和或出现无效的应用程序状态。不幸的是,这些问题很难被发现,因为它们通常具有非确定性。

      严格模式不能自动检测到你的副作用,但它可以帮助你发现它们,使它们更具确定性。通过故意重复调用以下函数来实现的该操作:

      • class 组件的 constructor,render 以及 shouldComponentUpdate 方法
      • class 组件的生命周期方法 getDerivedStateFromProps
      • 函数组件体
      • 状态更新函数 (即 setState 的第一个参数)
      • 函数组件通过使用 useState,useMemo 或者 useReducer

      注意:

      这仅适用于开发模式。生产模式下生命周期不会被调用两次

      例如,请考虑以下代码:

      class TopLevelRoute extends React.Component{
          constructor(props){
              super(props)
              SharedApplicationState.recordEvent('ExampleComponent')
          }
      }
      

      这段代码看起来似乎没有问题。但是如果 SharedApplicationState.recordEvent 不是幂等的情况下,多次实例化此组件可能会导致应用程序状态无效。这种小 bug 可能在开发过程中可能不会表现出来,或者说表现出来但并不明显,并因此被忽视。

      严格模式采用故意重复调用方法(如组件的构造函数)的方式,使得这种 bug 更容易被发现。

      检测过时的 context API

      过时的 context API 容易出错,将在未来的主要版本中删除。在所有 16.x 版本中它仍然有效,但在严格模式下,将显示以下警告:
      在这里插入图片描述

      使用 PropTypes 进行类型检查

      注意:
      自 React v15.5 起,React.PropTypes 已移入另一个包中。请使用 prop-types 库 代替。
      我们提供了一个 codemod 脚本来做自动转换

      随着你的应用程序不断增长,你可以通过类型检查捕获大量错误。对于某些应用程序来说,你可以使用 Flow 或 TypeScript 等 JavaScript 扩展来对整个应用程序做类型检查。但即使你不使用这些扩展,React 也内置了一些类型检查的功能。要在组件的 props 上进行类型检查,你只需配置特定的 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。当传入的 prop 值类型不正确时,JavaScript 控制台将会显示警告。出于性能方面的考虑,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,
      
          //任何可被渲染的元素(包括数字. 字符串。元素或数组)
          //(或Fragment) 也包含这些类型
          optionalNode:PropTypes.node,
          
          //一个react元素
          optionalElement:PropTypes.element,
      
          //一个React元素类型(即 MyComponent)
          optionalElementType:PropTypes.elementType,
          
          //你也可以声明prop为类的实例  这里使用
          //JS的instanceof 操作符
          optionalMessage:PropTypes.intanceOf(Message),
      
      
          //你也让你的prop只能是特定的值  指定它为枚举类型
          optionalEnum:PropTypes.oneOf(['News','Photos']),
      
          //一个对象可以是几种类型中的任意一个类型、
          optionalUnion:PropTypes.onOfType([
              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
         }),
      
         //An object with wranings on extra properties
         optionalObjectWithStrictShape:PropTypes.exact({
             name:PropTypes.string,
             quantity:PropTypes.number
         }),
      
         //你可以在任何PropTypes属性后面加上 isRequired ,确保
         //这个prop没有被提供时 会打印警告信息
         requiredFunc:PropTypes.func.isRequired,
         
         //任意类型的数据
         requiredAny:PropTypes.any.isRequired,
      
         //你可以指定一个自定义验证器,它在验证失败时应返回一个error对象
         //请不要使用console.wran 或抛出异常 因为这在onOfType 中不会起作用
         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 来确保传递给组件的 children 中只包含一个元素。

      import PropTypes from 'prop-types'
      class MyComponent extends React.Component{
          render(){
              //这必须只有一个元素  否则控制台会打印警告
              const children = this.props.children
              render(
                  <div>
                      {children}
                  </div>
              )
          }
      }
      MyComponent.propTypes={
          children:PropTypes.element.isRequired
      }
      
      默认 Prop 值

      您可以通过配置特定的 defaultProps 属性来定义 props 的默认值:

      class Greeting extends React.Component{
          render(){
              return (
                  <h1>Hello,{this.props.name}</h1>
              )
          }
      }
      
      //指定props的默认值
      Greeting.defaultProps = {
          name:'Stranger'
      }
      
      //渲染出 Hello,Stranger
      ReactDOM.render(
          <Greeting />,
          document.getElementById('example')
      )
      

      如果你正在使用像 transform-class-properties 的 Babel 转换工具,你也可以在 React 组件类中声明 defaultProps 作为静态属性。此语法提案还没有最终确定,需要进行编译后才能在浏览器中运行。要了解更多信息,请查阅 class fields proposal。

      class Greeting extends React.Component{
          static defaultProps = {
              name:'stanger'
          }
      
          render(){
              return (
                  <div>
                   Hello,{this.props.name}
                  </div>
              )
          }
      
      }
      

      defaultProps 用于确保 this.props.name 在父组件没有指定其值时,有一个默认值。propTypes 类型检查发生在 defaultProps 赋值后,所以类型检查也适用于 defaultProps。

      非受控组件

      在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。

      要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 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>
              ) 
          }
          
      }
      

      因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

      如果你还是不清楚在某个特殊场景中应该使用哪种组件,那么 这篇关于受控和非受控输入组件的文章 会很有帮助。

      默认值

      在 React 渲染生命周期时,表单元素上的 value 将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value。

      render(){
          return(
              <form onSubmit={this.handleSubmit}>
                <label>
                  Name:
                  <input defaultValue="Bob" type="text" ref={this.input}/> 
                </label>
                <input type="submit" value="Submit"/>
              </form>
          )
      }
      

      同样, 和 支持 defaultChecked, 和 支持 defaultValue。

      文件输入

      在 HTML 中, 可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API 进行操作。

      <input type="file" />
      

      在 React 中, 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。

      您应该使用 File API 与文件进行交互。下面的例子显示了如何创建一个 DOM 节点的 ref 从而在提交表单时获取文件的信息。

      class FileInput extends React.Component{
          constructor(props){
              super(props)
              this.handleSubmit = this.handleSubmit.bind(this)
              this.fileInput = React.createRef()
          }
          handleSubmit(event){
              event.preventDefault()
              alert(`Select 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>
              )
          }
      }
      
      ReactDOM.render(
          <FileInput />,
          document.getElementById('root')
      )
      

      Web Components

      React 和 Web Components 为了解决不同的问题而生。Web Components 为可复用组件提供了强大的封装,而 React 则提供了声明式的解决方案,使 DOM 与数据保持同步。两者旨在互补。作为开发人员,可以自由选择在 Web Components 中使用 React,或者在 React 中使用 Web Components,或者两者共存。

      大多数开发者在使用 React 时,不使用 Web Components,但可能你会需要使用,尤其是在使用 Web Components 编写的第三方 UI 组件时。

      在 React 中使用 Web Components
      class HelloMessage extends React.Component{
          render(){
              return <div>Hello,<x-search>{this.props.name}</x-search>!</div>
          }
      }
      

      注意

      Web Components 通常暴露的是命令式 API。例如,Web Components 的组件 video 可能会公开 play() 和 pause() 方法。要访问 Web Components 的命令式 API,你需要使用 ref 直接与 DOM 节点进行交互。如果你使用的是第三方 Web Components,那么最好的解决方案是编写 React 组件包装该 Web Components。

      Web Components 触发的事件可能无法通过 React 渲染树正确的传递。 你需要在 React 组件中手动添加事件处理器来处理这些事件。

      常见的误区是在 Web Components 中使用的是 class 而非 className。

      function BrickFlipbox(){
          return (
              <brick-flipbox>
                  <div>front</div>
                  <div>back</div>
              </brick-flipbox>
          )
      }
      
      在 Web Components 中使用 React
      class XSearch extends HTMLElement{
          connectedCallback(){
              const mountPoint = document.createElement('span')
              this.attachShadow({mode:'open'}).appendChild(mountPoint)
              const name = this.getAttribute('name')
              const url = 'https://www.goole.com/search?q='+ encodeURIComponent(name)
              ReactDOM.render(<a href={url}>{name}</a>,mountPoint)
          }
      }
      customElement.define('x-search',XSearch)
      

      注意:

      如果使用 Babel 来转换 class,此代码将不会起作用。请查阅该 issue 了解相关讨论。 在加载 Web Components 前请引入 custom-elements-es5-adapter 来解决该 issue。

      React 顶层 API

      React 是 React 库的入口。如果你通过使用

      React.Component

      React.Component 是使用 ES6 classes 方式定义 React 组件的基类:
      class Greeting extends React.Component {
      render() {
      return

      Hello, {this.props.name}

      ;
      }
      }
      请参阅 React.Component API 参考,获取与基类 React.Component 相关方法和属性的详细列表。

      class Greeting extends React.Component {
        render() {
          return <h1>Hello, {this.props.name}</h1>;
        }
      }
      
      React.PureComponent

      React.PureComponent 与 React.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。

      如果赋予 React 组件相同的 props 和 state,render() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。

      注意

      React.PureComponent 中的 shouldComponentUpdate() 仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。仅在你的 props 和 state 较为简单时,才使用 React.PureComponent,或者在深层数据结构发生变化时调用 forceUpdate() 来确保组件被正确地更新。你也可以考虑使用 immutable 对象加速嵌套数据的比较。

      此外,React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。

      React.memo
      const MyComponent = React.memo(function MyComponent(props){
          //使用props渲染
      })
      

      React.memo 为高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件,而不适用 class 组件。

      如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

      React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。
      默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

      function MyComponent(props){
      //使用props渲染
      }
      function areEqual(prevProps,nextProps){
          //如果把nextProps传入render方法的返回结果与prevProps传入render
          //方法返回的结果一致则返回true 否则返回false
      }
      
      export default React.memo(MyComponent,areEqual)
      

      此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。

      注意

      与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

      createElement()
      React.createElement(
          type,
          [props],
          [...children]
      )
      

      创建并返回指定类型的新 React 元素。其中的类型参数既可以是标签名字符串(如 ‘div’ 或 ‘span’),也可以是 React 组件 类型 (class 组件或函数组件),或是 React fragment 类型。

      使用 JSX 编写的代码将会被转换成使用 React.createElement() 的形式。如果使用了 JSX 方式,那么一般来说就不需要直接调用 React.createElement()。请查阅不使用 JSX 章节获得更多信息。

      cloneElement()
      React.cloneElement(
          element,
          [props],
          [...chilren]
      )
      

      以 element 元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,而来自原始元素的 key 和 ref 将被保留。

      React.cloneElement() 几乎等同于:

      <element.type {...element.props} {...props}>{children}</element.type>
      

      但是,这也保留了组件的 ref。这意味着当通过 ref 获取子节点时,你将不会意外地从你祖先节点上窃取它。相同的 ref 将添加到克隆后的新元素中。

      引入此 API 是为了替换已弃用的 React.addons.cloneWithProps()。

      createFactory()
      React.createFactory(type)
      

      返回用于生成指定类型 React 元素的函数。与 React.createElement() 相似的是,类型参数既可以是标签名字符串(像是 ‘div’ 或 ‘span’),也可以是 React 组件 类型 (class 组件或函数组件),或是 React fragment 类型。

      此辅助函数已废弃,建议使用 JSX 或直接调用 React.createElement() 来替代它。

      如果你使用 JSX,通常不会直接调用 React.createFactory()。请参阅不使用 JSX 以获得更多信息。

      isValidElement()
      React.isValidElement(object)
      

      验证对象是否为 React 元素,返回值为 true 或 false。

      React.Children

      React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

      React.Children.map

      React.Children.map(children,function[(thisArg)])
      

      在 children 里的每个直接子节点上调用一个函数,并将 this 设置为 thisArg。如果 children 是一个数组,它将被遍历并为数组中的每个子节点调用该函数。如果子节点为 null 或是 undefined,则此方法将返回 null 或是 undefined,而不会返回数组。

      注意

      如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历。

      React.Children.forEach

      React.Children.forEach(children,function[(thisArg)])
      

      与 React.Children.map() 类似,但它不会返回一个数组。
      React.Children.count

      React.Children.count(children)
      

      返回 children 中的组件总数量,等同于通过 map 或 forEach 调用回调函数的次数。

      React.Children.only

      React.Children.only(children)
      

      验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。

      注意:

      React.Children.only() 不接受 React.Children.map() 的返回值,因为它是一个数组而并不是 React 元素。

      React.Children.toArray

      React.Children.toArray(children)
      

      将 children 这个复杂的数据结构以数组的方式扁平展开并返回,并为每个子节点分配一个 key。当你想要在渲染函数中操作子节点的集合时,它会非常实用,特别是当你想要在向下传递 this.props.children 之前对内容重新排序或获取子集时。
      注意:

      React.Children.toArray() 在拉平展开子节点列表时,更改 key 值以保留嵌套数组的语义。也就是说,toArray 会为返回数组中的每个 key 添加前缀,以使得每个元素 key 的范围都限定在此函数入参数组的对象内。

      React.Fragment

      React.Fragment 组件能够在不额外创建 DOM 元素的情况下,让 render() 方法中返回多个元素。

      render(){
          return(
              <React.Fragment>
                  Some text.
                  <h2>A heading</h2>
              </React.Fragment>
          )
      }
      
      

      你也可以使用其简写语法 <></>。欲了解更多相关信息,请参阅 React v16.2.0: Fragments 支持改进。

      React.createRef

      React.createRef 创建一个能够通过 ref 属性附加到 React 元素的 ref。

      class MyComponent extends React.Component{
          constructor(props){
              super(props)
              this.inputRef = React.createRef()
          }
          render(){
              return <input type="text" ref={this.inputRef}/>
          }
          componentDidMount(){
              this.inputRef.current.focus()
          }
      }
      
      
      React.forwardRef

      React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:

      • 转发 refs 到 DOM 组件
      • 在高阶组件中转发 refs

      React.forwardRef 接受渲染函数作为参数。React 将使用 props 和 ref 作为参数来调用此函数。此函数应返回 React 节点。

      const FancyButton = React.forwardRef((props,ref)=>(
          <button ref={ref} className="FancyButton">
              {props.children}
          </button>
      ))
      //You can now get a ref directly to the DOM button
      const ref = React.createRef()
      <FancyButton ref={ref}>Click Me</FancyButton>
      

      在上述的示例中,React 会将 元素的 ref 作为第二个参数传递给 React.forwardRef 函数中的渲染函数。该渲染函数会将 ref 传递给 元素。

      因此,当 React 附加了 ref 属性之后,ref.current 将直接指向 DOM 元素实例。

      欲了解更多相关信息,请参阅 refs 转发。

      React.lazy

      React.lazy() 允许你定义一个动态加载的组件。这有助于缩减 bundle 的体积,并延迟加载在初次渲染时未用到的组件。

      你可以在代码分割文档中学习如何使用它。查阅此文章可以了解更多用法细节。

      //这个组件是动态加载的
      const SomeComponent = React.lazy(()=>import('./SomeComponent'))
      

      请注意,渲染 lazy 组件依赖该组件渲染树上层的 <React.Suspense> 组件。这是指定加载指示器(loading indicator)的方式。

      注意

      使用 React.lazy 的动态引入特性需要 JS 环境支持 Promise。在 IE11 及以下版本的浏览器中需要通过引入 polyfill 来使用该特性。

      React.Suspense

      React.Suspense 可以指定加载指示器(loading indicator),以防其组件树中的某些子组件尚未具备渲染条件。目前,懒加载组件是 <React.Suspense> 支持的唯一用例:

      const OtherComponent = React.lazy(()=> import('./OtherComponent'))
      
      function MyComponent(){
          return (
              //显示<Spinner> 组件直至OtherComponent加载而成
              <React.Suspense callback={<Spinner />}>
                 <div>
                   <OtherComponent />
                 </div>
              </React.Suspense>
          )
      }
      

      它已被收录在了我们的代码分割指南中。请注意,lazy 组件可以位于 Suspense 组件树的深处——它不必包装树中的每一个延迟加载组件。最佳实践是将 置于你想展示加载指示器(loading indicator)的位置,而 lazy() 则可被放置于任何你想要做代码分割的地方。

      虽然目前尚未支持其它特性,但未来我们计划让 Suspense 支持包括数据获取在内的更多场景。你可以在 roadmap 中了解相关信息。

      注意:

      React.lazy() 和 <React.Suspense> 尚未在 ReactDOMServer 中支持。这是已知问题,将会在未来解决。

      React.Component

      React 的组件可以定义为 class 或函数的形式。class 组件目前提供了更多的功能,这些功能将在此章节中详细介绍。如需定义 class 组件,需要继承 React.Component:

      class Welcome extends React.Component {
           render(){
               return <h1>Hello,{this.props.name}</h1>
           }
       }
      

      在 React.Component 的子类中有个必须定义的 render() 函数。本章节介绍其他方法均为可选。
      我们强烈建议你不要创建自己的组件基类。 在 React 组件中,代码重用的主要方式是组合而不是继承。

      React 并不会强制你使用 ES6 的 class 语法。如果你倾向于不使用它,你可以使用 create-react-class 模块或类似的自定义抽象来代替。

      组件的生命周期

      每个组件都包含“生命周期方法”,你可以重写这些方法,以便于在运行过程中特定的阶段执行这些方法。你可以使用此生命周期图谱作为速查表。在下述列表中,常用的生命周期方法会被加粗。其余生命周期函数的使用则相对罕见。

      挂载

      当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

      • constructor()
      • static getDerivedStateFromProps()
      • render()
      • componentDidMount()

      下述生命周期方法即将过时,在新代码中应该避免使用它们:

      UNSAFE_componentWillMount()

      更新

      当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

      • static getDerivedStateFromProps()
      • shouldComponentUpdate()
      • render()
      • getSnapshotBeforeUpdate()
      • componentDidUpdate()

      下述方法即将过时,在新代码中应该避免使用它们:

      UNSAFE_componentWillUpdate()
      UNSAFE_componentWillReceiveProps()

      卸载

      当组件从 DOM 中移除时会调用如下方法:

      componentWillUnmount()

      错误处理

      当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

      static getDerivedStateFromError()
      componentDidCatch()

      其他 APIs

      组件还提供了一些额外的 API:

      setState()
      forceUpdate()

      class 属性

      defaultProps
      displayName

      实例属性

      props
      state

      常用的生命周期方法

      本节中的方法涵盖了创建 React 组件时能遇到的绝大多数用例。想要更好了解这些方法,可以参考生命周期图谱。

      render()

      render()
      

      render() 方法是 class 组件中唯一必须实现的方法。

      当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:

      React 元素。通常通过 JSX 创建。例如,<div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件,无论是 <div /> 还是 <MyComponent /> 均为 React 元素。
      数组或 fragments。 使得 render 方法可以返回多个元素。欲了解更多详细信息,请参阅 fragments 文档。
      Portals。可以渲染子节点到不同的 DOM 子树中。欲了解更多详细信息,请参阅有关 portals 的文档
      字符串或数值类型。它们在 DOM 中会被渲染为文本节点
      布尔类型或 null。什么都不渲染。(主要用于支持返回 test && <Child /> 的模式,其中 test 为布尔类型。)
      

      render() 函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。

      如需与浏览器进行交互,请在 componentDidMount() 或其他生命周期方法中执行你的操作。保持 render() 为纯函数,可以使组件更容易思考。

      注意

      如果 shouldComponentUpdate() 返回 false,则不会调用 render()。

      constructor()

      constructor(props)
      如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

      在 React 组件挂载之前,会调用它的构造函数。在为 React.Component 子类实现构造函数时,应在其他语句之前前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug。

      通常,在 React 中,构造函数仅用于以下两种情况:

      *通过给 this.state 赋值对象来初始化内部 state。

      • 为事件处理函数绑定实例

      在 constructor() 函数中不要调用 setState() 方法。如果你的组件需要使用内部 state,请直接在构造函数中为 this.state 赋值初始 state:

      constructor(props) {
        super(props);
        // 不要在这里调用 this.setState()
        this.state = { counter: 0 };
        this.handleClick = this.handleClick.bind(this);
      }
      

      只能在构造函数中直接为 this.state 赋值。如需在其他方法中赋值,你应使用 this.setState() 替代。

      要避免在构造函数中引入任何副作用或订阅。如遇到此场景,请将对应的操作放置在 componentDidMount 中。

      避免将 props 的值复制给 state!这是一个常见的错误

      constructor(props) {
       super(props);
       // 不要这样做
       this.state = { color: props.color };
      }
      

      如此做毫无必要(你可以直接使用 this.props.color),同时还产生了 bug(更新 prop 中的 color 时,并不会影响 state)。

      只有在你刻意忽略 prop 更新的情况下使用。此时,应将 prop 重命名为 initialColor 或 defaultColor。必要时,你可以修改它的 key,以强制“重置”其内部 state。

      componentDidMount()

      componentDidMount()
      componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

      这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

      你可以在 componentDidMount() 里直接调用 setState()。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 constructor() 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modals 和 tooltips 等情况下,你可以使用此方式处理

      componentDidUpdate()
      componentDidUpdate(prevProps, prevState, snapshot)
      

      componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。

      当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。

      componentDidUpdate(prevProps) {
        // 典型用法(不要忘记比较 props):
        if (this.props.userID !== prevProps.userID) {
          this.fetchData(this.props.userID);
        }
      }
      

      你也可以在 componentDidUpdate() 中直接调用 setState(),但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。不要将 props “镜像”给 state,请考虑直接使用 props。 欲了解更多有关内容,请参阅为什么 props 复制给 state 会产生 bug。

      如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 参数传递。否则此参数将为 undefined。

      注意

      如果 shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()。

      componentWillUnmount()
      componentWillUnmount()
      

      componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。

      componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

      不常用的生命周期方法

      本节中的生命周期方法并不太常用。它们偶尔会很方便,但是大部分情况下组件可能都不需要它们。你可以在生命周期图谱中,选择“显示不常用的生命周期”复选框,即可看到下述相关方法。

      shouldComponentUpdate()
      shouldComponentUpdate(nextProps, nextState)
      

      根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。大部分情况下,你应该遵循默认行为。

      当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。

      此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。你应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()。PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。

      如果你一定要手动编写此函数,可以将 this.props 与 nextProps 以及 this.state 与nextState 进行比较,并返回 false 以告知 React 可以跳过更新。请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染。

      我们不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能。

      目前,如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate(),render() 和 componentDidUpdate()。后续版本,React 可能会将 shouldComponentUpdate 视为提示而不是严格的指令,并且,当返回 false 时,仍可能导致组件重新渲染。

      static getDerivedStateFromProps()

      static getDerivedStateFromProps(props, state)
      getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

      此方法适用于罕见的用例,即 state 的值在任何时候都取决于 props。例如,实现 组件可能很方便,该组件会比较当前组件与下一组件,以决定针对哪些组件进行转场动画。

      派生状态会导致代码冗余,并使组件难以维护。 确保你已熟悉这些简单的替代方案:

      如果你需要执行副作用(例如,数据提取或动画)以响应 props 中的更改,请改用 componentDidUpdate。
      如果只想在 prop 更改时重新计算某些数据,请使用 memoization helper 代替。
      如果你想在 prop 更改时“重置”某些 state,请考虑使组件完全受控或使用 key 使组件完全不受控 代替。

      此方法无权访问组件实例。如果你需要,可以通过提取组件 props 的纯函数及 class 之外的状态,在getDerivedStateFromProps()和其他 class 方法之间重用代码。

      请注意,不管原因是什么,都会在每次渲染前触发此方法。这与 UNSAFE_componentWillReceiveProps 形成对比,后者仅在父组件重新渲染时触发,而不是在内部调用 setState 时。

      getSnapshotBeforeUpdate()

      getSnapshotBeforeUpdate(prevProps, prevState)
      getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()。

      此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。

      应返回 snapshot 的值(或 null)。

      例如:

      class ScrollingList extends React.Component{
          constructor(props){
              super(props)
              this.listRef = React.createRef()
          }
          getSnapshotBeforeUpdate(prevProps,prevState){
              //我们是否在list中添加新的items
              //捕获滚动位置以便我们稍后调整滚动位置
              if(prevProps.list.length < this.props.list.length){
                  const list = this.listRef.current;
                  return list.scrollHeight - list.scrollTop
              }
              return null
          }
          componentDidUpdate(prevProps,prevState,snapshot){
              //如果我们snapshot,说明我们刚刚添加了新的items
              //调整滚动位置使得这些新items不会将旧的items推出视图
              //这里的snapshot是getSnapshotBeforeUpdate的返回值
              if(snapt !== null){
                  const list = this.listRef.current
                  list.scollTop = list.scollHeight - snapshot
              }
          }
          render(){
              return(
                  <div ref={this.listRef}>{/*...content..*/}</div>
              )
          }
      }
      

      在上述示例中,重点是从 getSnapshotBeforeUpdate 读取 scrollHeight 属性,因为 “render” 阶段生命周期(如 render)和 “commit” 阶段生命周期(如 getSnapshotBeforeUpdate 和 componentDidUpdate)之间可能存在延迟。

      Error boundaries

      Error boundaries 是 React 组件,它会在其子组件树中的任何位置捕获 JavaScript 错误,并记录这些错误,展示降级 UI 而不是崩溃的组件树。Error boundaries 组件会捕获在渲染期间,在生命周期方法以及其整个树的构造函数中发生的错误。

      如果 class 组件定义了生命周期方法 static getDerivedStateFromError() 或 componentDidCatch() 中的任何一个(或两者),它就成为了 Error boundaries。通过生命周期更新 state 可让组件捕获树中未处理的 JavaScript 错误并展示降级 UI。

      仅使用 Error boundaries 组件来从意外异常中恢复的情况;不要将它们用于流程控制。

      欲了解更多详细信息,请参阅 React 16 中的错误处理。

      注意

      Error boundaries 仅捕获组件树中以下组件中的错误。但它本身的错误无法捕获。

      static getDerivedStateFromError()
      static getDerivedStateFromError(error)
      

      此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state

      class ErrorBoundary extends React.Component{
          constructor(props){
              super(props)
              this.state = {hasError:false}
          }
          static getDerivedStateFromError(error){
              //更新state使下一次渲染可以显示降级UI
              return {hasError:true}
          }
      
          render(){
              if(this.state.hasError){
                  //你可以渲染任何自定义的降级 UI
                  return <h1>SomeThing went wrong</h1>
              }
              return this.props.children
          }
      }
      

      注意
      getDerivedStateFromError() 会在渲染阶段调用,因此不允许出现副作用。 如遇此类情况,请改用 componentDidCatch()。

      componentDidCatch()

      componentDidCatch(error, info)
      此生命周期在后代组件抛出错误后被调用。 它接收两个参数:

      error —— 抛出的错误。
      info —— 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息。

      componentDidCatch() 会在“提交”阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况:

      class ErrorBoundary extends React.Component{
          constructor(props){
              super(props)
              this.state = {hasError:false}
          }
          static getDerivedStateFromError(error){
              //更新 state使下一次渲染可以显示降级的UI
              return {hasError:true}
          }
          componentDidCatch(error,info){
              //组件堆栈例子:
              // in ComponentThatThrows(created by App)
              //in ErrorBoundary(created by App)
              // in div (created by App)
              // in App
              logComponentStackToMyService(info.componentStack)
          }
          render(){
              if(this.state.error){
                  //你可以渲染任何自定义的降级UI
                  return <h1>SomeThing went wrong</h1>
              }
              return this.props.children
          }
      
      
      }
      

      注意

      如果发生错误,你可以通过调用 setState 使用 componentDidCatch() 渲染降级 UI,但在未来的版本中将不推荐这样做。 可以使用静态 getDerivedStateFromError() 来处理降级渲染。

      过时的生命周期方法

      以下生命周期方法标记为“过时”。这些方法仍然有效,但不建议在新代码中使用它们。参阅此博客文章以了解更多有关迁移旧版生命周期方法的信息。

      UNSAFE_componentWillMount()

      UNSAFE_componentWillMount()
      

      注意

      此生命周期之前名为 componentWillMount。该名称将继续使用至 React 17。可以使用 rename-unsafe-lifecycles codemod 自动更新你的组件。

      UNSAFE_componentWillMount() 在挂载之前被调用。它在 render() 之前调用,因此在此方法中同步调用 setState() 不会触发额外渲染。通常,我们建议使用 constructor() 来初始化 state。

      避免在此方法中引入任何副作用或订阅。如遇此种情况,请改用 componentDidMount()。

      此方法是服务端渲染唯一会调用的生命周期函数。
      UNSAFE_componentWillReceiveProps()

      UNSAFE_componentWillReceiveProps(nextProps)
      

      注意

      此生命周期之前名为 componentWillReceiveProps。该名称将继续使用至 React 17。可以使用 rename-unsafe-lifecycles codemod 自动更新你的组件。

      注意:

      使用此生命周期方法通常会出现 bug 和不一致性:

      • 如果你需要执行副作用(例如,数据提取或动画)以响应 props 中的更改,请改用 componentDidUpdate 生命周期。
      • 如果你使用 componentWillReceiveProps 仅在 prop 更改时重新计算某些数据,请使用 memoization helper`代替。
      • 如果你使用 componentWillReceiveProps 是为了在 prop 更改时“重置”某些 state,请考虑使组件完全受控或使用 key 使组件完全不受控 代替。
        对于其他使用场景,请遵循此博客文章中有关派生状态的建议。

      UNSAFE_componentWillReceiveProps() 会在已挂载的组件接收新的 props 之前被调用。如果你需要更新状态以响应 prop 更改(例如,重置它),你可以比较 this.props 和 nextProps 并在此方法中使用 this.setState() 执行 state 转换。

      请注意,如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较。

      在挂载过程中,React 不会针对初始 props 调用 UNSAFE_componentWillReceiveProps()。组件只会在组件的 props 更新时调用此方法。调用 this.setState() 通常不会触发 UNSAFE_componentWillReceiveProps()。

      UNSAFE_componentWillUpdate()
      UNSAFE_componentWillUpdate(nextProps, nextState)
      

      注意

      此生命周期之前名为 componentWillUpdate。该名称将继续使用至 React 17。可以使用 rename-unsafe-lifecycles codemod 自动更新你的组件。

      当组件收到新的 props 或 state 时,会在渲染之前调用 UNSAFE_componentWillUpdate()。使用此作为在更新发生之前执行准备更新的机会。初始渲染不会调用此方法。

      注意,你不能此方法中调用 this.setState();在 UNSAFE_componentWillUpdate() 返回之前,你也不应该执行任何其他操作(例如,dispatch Redux 的 action)触发对 React 组件的更新

      通常,此方法可以替换为 componentDidUpdate()。如果你在此方法中读取 DOM 信息(例如,为了保存滚动位置),则可以将此逻辑移至 getSnapshotBeforeUpdate() 中。

      注意

      如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()。

      其他 API

      不同于上述生命周期方法(React 主动调用),以下方法是你可以在组件中调用的方法。

      只有两个方法:setState() 和 forceUpdate()。

      setState()
      setState(updater, [callback])
      

      setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式

      将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

      setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。如需基于之前的 state 来设置当前的 state,请阅读下述关于参数 updater 的内容。

      除非 shouldComponentUpdate() 返回 false,否则 setState() 将始终执行重新渲染操作。如果可变对象被使用,且无法在 shouldComponentUpdate() 中实现条件渲染,那么仅在新旧状态不一时调用 setState()可以避免不必要的重新渲染

      参数一为带有形式参数的 updater 函数:

      (state, props) => stateChange
      

      state 是对应用变化时组件状态的引用。当然,它不应直接被修改。你应该使用基于 state 和 props 构建的新对象来表示变化。例如,假设我们想根据 props.step 来增加 state:

      this.setState((state, props) => {
        return {counter: state.counter + props.step};
      });
      

      updater 函数中接收的 state 和 props 都保证为最新。updater 的返回值会与 state 进行浅合并。

      setState() 的第二个参数为可选的回调函数,它将在 setState 完成合并并重新渲染组件后执行。通常,我们建议使用 componentDidUpdate() 来代替此方式。

      setState() 的第一个参数除了接受函数外,还可以接受对象类型:

      setState(stateChange[, callback])
      

      stateChange 会将传入的对象浅层合并到新的 state 中,例如,调整购物车商品数:

      this.setState({quantity: 2})
      

      这种形式的 setState() 也是异步的,并且在同一周期内会对多个 setState 进行批处理。例如,如果在同一周期内多次设置商品数量增加,则相当于:

      Object.assign(
        previousState,
        {quantity: state.quantity + 1},
        {quantity: state.quantity + 1},
        ...
      )
      

      后调用的 setState() 将覆盖同一周期内先调用 setState 的值,因此商品数仅增加一次。如果后续状态取决于当前状态,我们建议使用 updater 函数的形式代替:

      this.setState((state) => {
        return {quantity: state.quantity + 1};
      });
      

      有关更多详细信息,请参阅:

      State 和生命周期指南
      深入学习:何时以及为什么 setState() 会批量执行?
      深入:为什么不直接更新 this.state?

      forceUpdate()

      component.forceUpdate(callback)
      默认情况下,当组件的 state 或 props 发生变化时,组件将重新渲染。如果 render() 方法依赖于其他数据,则可以调用 forceUpdate() 强制让组件重新渲染。

      调用 forceUpdate() 将致使组件调用 render() 方法,此操作会跳过该组件的 shouldComponentUpdate()。但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate() 方法。如果标记发生变化,React 仍将只更新 DOM。

      通常你应该避免使用 forceUpdate(),尽量在 render() 中使用 this.props 和 this.state。

      nClass 属性

      defaultProps
      defaultProps 可以为 Class 组件添加默认 props。这一般用于 props 未赋值,但又不能为 null 的情况。例如:

      class CustomButton extends React.Component{
          //...
      }
      CustomButton.defaultProps = {
          color:'blue'
      }
      

      如果未提供 props.color,则默认设置为 ‘blue’

        render() {
          return <CustomButton /> ; // props.color 将设置为 'blue'
        }
      

      如果 props.color 被设置为 null,则它将保持为 null

        render() {
          return <CustomButton color={null} /> ; // props.color 将保持是 null
        }
      
      displayName

      displayName 字符串多用于调试消息。通常,你不需要设置它,因为它可以根据函数组件或 class 组件的名称推断出来。如果调试时需要显示不同的名称或创建高阶组件,请参阅使用 displayname 轻松进行调试了解更多。

      实例属性

      props
      this.props 包括被该组件调用者定义的 props。欲了解 props 的详细介绍,请参阅组件 & Props。

      需特别注意,this.props.children 是一个特殊的 prop,通常由 JSX 表达式中的子组件组成,而非组件本身定义。

      state
      组件中的 state 包含了随时可能发生变化的数据。state 由用户自定义,它是一个普通 JavaScript 对象。

      如果某些值未用于渲染或数据流(例如,计时器 ID),则不必将其设置为 state。此类值可以在组件实例上定义。

      欲了解关于 state 的更多信息,请参阅 State & 生命周期。

      永远不要直接改变 this.state,因为后续调用的 setState() 可能会替换掉你的改变。请把 this.state 看作是不可变的。

      ReactDOM

      如果你使用一个

      react-dom 的 package 提供了可在应用顶层使用的 DOM(DOM-specific)方法,如果有需要,你可以把这些方法用于 React 模型以外的地方。不过一般情况下,大部分组件都不需要使用这个模块。

      render()
      hydrate()
      unmountComponentAtNode()
      findDOMNode()
      createPortal()
      

      浏览器支持
      React 支持所有的现代浏览器,包括 IE9 及以上版本,但是需要为旧版浏览器比如 IE9 和 IE10 引入相关的 polyfills 依赖。

      注意:

      我们不支持那些不兼容 ES5 方法的旧版浏览器,但如果你的应用包含了 polyfill,例如 es5-shim 和 es5-sham 你可能会发现你的应用仍然可以在这些浏览器中正常运行。但是如果你选择这种方法,你便需要孤军奋战了。

      render()

      ReactDOM.render(element, container[, callback])
      在提供的 container 里渲染一个 React 元素,并返回对该组件的引用(或者针对无状态组件返回 null)。

      如果 React 元素之前已经在 container 里渲染过,这将会对其执行更新操作,并仅会在必要时改变 DOM 以映射最新的 React 元素。

      如果提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行。
      注意:

      ReactDOM.render() 会控制你传入容器节点里的内容。当首次调用时,容器节点里的所有 DOM 元素都会被替换,后续的调用则会使用 React 的 DOM 差分算法(DOM diffing algorithm)进行高效的更新。

      ReactDOM.render() 不会修改容器节点(只会修改容器的子节点)。可以在不覆盖现有子节点的情况下,将组件插入已有的 DOM 节点中。

      ReactDOM.render() 目前会返回对根组件 ReactComponent 实例的引用。 但是,目前应该避免使用返回的引用,因为它是历史遗留下来的内容,而且在未来版本的 React 中,组件渲染在某些情况下可能会是异步的。 如果你真的需要获得对根组件 ReactComponent 实例的引用,那么推荐为根元素添加 callback ref。

      使用 ReactDOM.render() 对服务端渲染容器进行 hydrate 操作的方式已经被废弃,并且会在 React 17 被移除。作为替代,请使用 hydrate()。

      hydrate()

      ReactDOM.hydrate(element, container[, callback])
      与 render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。React 会尝试在已有标记上绑定事件监听器。

      React 希望服务端与客户端渲染的内容完全一致。React 可以弥补文本内容的差异,但是你需要将不匹配的地方作为 bug 进行修复。在开发者模式下,React 会对 hydration 操作过程中的不匹配进行警告。但并不能保证在不匹配的情况下,修补属性的差异。由于性能的关系,这一点非常重要,因为大多是应用中不匹配的情况很少见,并且验证所有标记的成本非常昂贵。

      如果单个元素的属性或者文本内容,在服务端和客户端之间有无法避免差异(比如:时间戳),则可以为元素添加 suppressHydrationWarning={true} 来消除警告。这种方式只在一级深度上有效,应只作为一种应急方案(escape hatch)。请不要过度使用!除非它是文本内容,否则 React 仍不会尝试修补差异,因此在未来的更新之前,仍会保持不一致。

      如果你执意要在服务端与客户端渲染不同内容,你可以采用双重(two-pass)渲染。在客户端渲染不同内容的组件可以读取类似于 this.state.isClient 的 state 变量,你可以在 componentDidMount() 里将它设置为 true。这种方式在初始渲染过程中会与服务端渲染相同的内容,从而避免不匹配的情况出现,但在 hydration 操作之后,会同步进行额外的渲染操作。注意,因为进行了两次渲染,这种方式会使得组件渲染变慢,请小心使用。

      记得保证弱网环境下的用户体验。JavaScript 代码的加载要比最初的 HTML 渲染晚的多。因此如果你只在客户端渲染不同的内容,其转换可能会不稳定。但是,如果执行顺利,那么在服务端负责渲染的 shell 会对渲染提供帮助,并且只显示客户端上额外的小组件。欲了解如何在不出现标记不匹配的情况下执行此操作,请参考上一段的解释。

      unmountComponentAtNode()

      ReactDOM.unmountComponentAtNode(container)
      从 DOM 中卸载组件,会将其事件处理器(event handlers)和 state 一并清除。如果指定容器上没有对应已挂载的组件,这个函数什么也不会做。如果组件被移除将会返回 true,如果没有组件可被移除将会返回 false。

      findDOMNode()

      注意:

      findDOMNode 是一个访问底层 DOM 节点的应急方案(escape hatch)。在大多数情况下,不推荐使用该方法,因为它会破坏组件的抽象结构。严格模式下该方法已弃用。

      ReactDOM.findDOMNode(component)
      如果组件已经被挂载到 DOM 上,此方法会返回浏览器中相应的原生 DOM 元素。此方法对于从 DOM 中读取值很有用,例如获取表单字段的值或者执行 DOM 检测(performing DOM measurements)。大多数情况下,你可以绑定一个 ref 到 DOM 节点上,可以完全避免使用 findDOMNode。

      当组件渲染的内容为 null 或 false 时,findDOMNode 也会返回 null。当组件渲染的是字符串时,findDOMNode 返回的是字符串对应的 DOM 节点。从 React 16 开始,组件可能会返回有多个子节点的 fragment,在这种情况下,findDOMNode 会返回第一个非空子节点对应的 DOM 节点。

      注意:

      findDOMNode 只在已挂载的组件上可用(即,已经放置在 DOM 中的组件)。如果你尝试调用未挂载的组件(例如在一个还未创建的组件上调用 render() 中的 findDOMNode())将会引发异常。

      findDOMNode 不能用于函数组件。

      createPortal()
      ReactDOM.createPortal(child, container)
      

      创建 portal。Portal 将提供一种将子节点渲染到 DOM 节点中的方式,该节点存在于 DOM 组件的层次结构之外。

      ReactDOMServer

      ReactDOMServer 对象允许你将组件渲染成静态标记。通常,它被使用在 Node 服务端上:

      // ES modules
      import ReactDOMServer from 'react-dom/server';
      // CommonJS
      var ReactDOMServer = require('react-dom/server');
      

      下述方法可以被使用在服务端和浏览器环境。

      renderToString()
      renderToStaticMarkup()
      下述附加方法依赖一个只能在服务端使用的 package(stream)。它们在浏览器中不起作用。

      renderToNodeStream()
      renderToStaticNodeStream()

      renderToString()
      ReactDOMServer.renderToString(element)
      

      将 React 元素渲染为初始 HTML。React 将返回一个 HTML 字符串。你可以使用此方法在服务端生成 HTML,并在首次请求时将标记下发,以加快页面加载速度,并允许搜索引擎爬取你的页面以达到 SEO 优化的目的。

      如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 将会保留该节点且只进行事件处理绑定,从而让你有一个非常高性能的首次加载体验。

      renderToStaticMarkup()

      ReactDOMServer.renderToStaticMarkup(element)
      此方法与 renderToString 相似,但此方法不会在 React 内部创建的额外 DOM 属性,例如 data-reactroot。如果你希望把 React 当作静态页面生成器来使用,此方法会非常有用,因为去除额外的属性可以节省一些字节。

      如果你计划在前端使用 React 以使得标记可交互,请不要使用此方法。你可以在服务端上使用 renderToString 或在前端上使用 ReactDOM.hydrate() 来代替此方法。

      renderToNodeStream()

      ReactDOMServer.renderToNodeStream(element)
      将一个 React 元素渲染成其初始 HTML。返回一个可输出 HTML 字符串的可读流。通过可读流输出的 HTML 完全等同于 ReactDOMServer.renderToString 返回的 HTML。你可以使用本方法在服务器上生成 HTML,并在初始请求时将标记下发,以加快页面加载速度,并允许搜索引擎抓取你的页面以达到 SEO 优化的目的。

      如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 将会保留该节点且只进行事件处理绑定,从而让你有一个非常高性能的首次加载体验。
      这个 API 仅允许在服务端使用。不允许在浏览器使用。

      通过本方法返回的流会返回一个由 utf-8 编码的字节流。如果你需要另一种编码的流,请查看像 iconv-lite 这样的项目,它为转换文本提供了转换流。

      renderToStaticNodeStream()

      ReactDOMServer.renderToStaticNodeStream(element)
      此方法与 renderToNodeStream 相似,但此方法不会在 React 内部创建的额外 DOM 属性,例如 data-reactroot。如果你希望把 React 当作静态页面生成器来使用,此方法会非常有用,因为去除额外的属性可以节省一些字节。

      通过可读流输出的 HTML,完全等同于 ReactDOMServer.renderToStaticMarkup 返回的 HTML。

      如果你计划在前端使用 React 以使得标记可交互,请不要使用此方法。你可以在服务端上使用 renderToNodeStream 或在前端上使用 ReactDOM.hydrate() 来代替此方法。

      注意:

      此 API 仅限于服务端使用,在浏览器中是不可用的。

      通过本方法返回的流会返回一个由 utf-8 编码的字节流。如果你需要另一种编码的流,请查看像 iconv-lite 这样的项目,它为转换文本提供了转换流。

      DOM 元素

      React 实现了一套独立于浏览器的 DOM 系统,兼顾了性能和跨浏览器的兼容性。我们借此机会完善了浏览器 DOM 实现的一些特殊情况。

      在 React 中,所有的 DOM 特性和属性(包括事件处理)都应该是小驼峰命名的方式。例如,与 HTML 中的 tabindex 属性对应的 React 的属性是 tabIndex。例外的情况是 aria-* 以及 data-* 属性,一律使用小写字母命名。比如, 你依然可以用 aria-label 作为 aria-label。

      属性差异

      React 与 HTML 之间有很多属性存在差异:

      checked
      当 组件的 type 类型为 checkbox 或 radio 时,组件支持 checked 属性。你可以使用它来设置组件是否被选中。这对于构建受控组件(controlled components)很有帮助。而 defaultChecked 则是非受控组件的属性,用于设置组件首次挂载时是否被选中。

      className
      className 属性用于指定 CSS 的 class,此特性适用于所有常规 DOM 节点和 SVG 元素,如

      , 及其它标签。

      如果你在 React 中使用 Web Components(这是一种不常见的使用方式),请使用 class 属性代替。

      dangerouslySetInnerHTML

      dangerouslySetInnerHTML 是 React 为浏览器 DOM 提供 innerHTML 的替换方案。通常来讲,使用代码直接设置 HTML 存在风险,因为很容易无意中使用户暴露于跨站脚本(XSS)的攻击。因此,你可以直接在 React 中设置 HTML,但当你想设置 dangerouslySetInnerHTML 时,需要向其传递包含 key 为 __html 的对象,以此来警示你。例如:

      function createMarkup(){
          return {_html:"first &middot; second"}
      }
      function MyComponent(){
          return <div dangerouslySetInnerHTML={createMarkup()}>
      }
      

      htmlFor
      由于 for 在 JavaScript 中是保留字,所以 React 元素中使用了 htmlFor 来代替。

      onChange
      onChange 事件与预期行为一致:每当表单字段变化时,该事件都会被触发。我们故意没有使用浏览器已有的默认行为,是因为 onChange 在浏览器中的行为和名称不对应,并且 React 依靠了该事件实时处理用户输入。

      selected

      组件支持 selected 属性。你可以使用该属性设置组件是否被选择。这对构建受控组件很有帮助。

      style
      注意
      在文档中,部分例子为了方便,直接使用了 style,但是通常不推荐将 style 属性作为设置元素样式的主要方式。在多数情况下,应使用 className 属性来引用外部 CSS 样式表中定义的 class。style 在 React 应用中多用于在渲染过程中添加动态计算的样式。另请参阅:FAQ:Styling 和 CSS。

      style 接受一个采用小驼峰命名属性的 JavaScript 对象,而不是 CSS 字符串。这与 DOM 中 style 的 JavaScript 属性是一致的,同时会更高效的,且能预防跨站脚本(XSS)的安全漏洞。例如:

      const divStyle = {
          color:'blue',
          backgroundImage:'url('+ imgUrl + ')'
      }
      
      function HelloWorldComponent(){
          return <div style={divStyle}>Hello,World</div>
      }
      

      注意:样式不会自动补齐前缀。如需支持旧版浏览器,请手动补充对应的样式属性:

      const divStyle = {
        WebkitTransition: 'all', // note the capital 'W' here
        msTransition: 'all' // 'ms' is the only lowercase vendor prefix
      };
      
      function ComponentWithTransition() {
        return <div style={divStyle}>This should work cross-browser</div>;
      }
      

      Style 中的 key 采用小驼峰命名是为了与 JS 访问 DOM 节点的属性保持一致(例如:node.style.backgroundImage )。浏览器引擎前缀都应以大写字母开头,除了 ms。因此,WebkitTransition 首字母为 ”W”。
      React 会自动添加 ”px” 后缀到内联样式为数字的属性后。如需使用 ”px” 以外的单位,请将此值设为数字与所需单位组成的字符串。例如:

      // Result style: '10px'
      <div style={{ height: 10 }}>
        Hello World!
      </div>
      
      // Result style: '10%'
      <div style={{ height: '10%' }}>
        Hello World!
      </div>
      

      但并非所有样式属性都转换为像素字符串。有些样式属性是没有单位的(例如 zoom,order,flex)。无单位属性的完整列表在此处

      suppressContentEditableWarning
      通常,当拥有子节点的元素被标记为 contentEditable 时,React 会发出一个警告,因为这不会生效。该属性将禁止此警告。尽量不要使用该属性,除非你要构建一个类似 Draft.js 的手动管理 contentEditable 属性的库。

      suppressHydrationWarning
      如果你使用 React 服务端渲染,通常会在当服务端与客户端渲染不同的内容时发出警告。但是,在一些极少数的情况下,很难甚至于不可能保证内容的一致性。例如,在服务端和客户端上,时间戳通常是不同的。

      如果设置 suppressHydrationWarning 为 true,React 将不会警告你属性与元素内容不一致。它只会对元素一级深度有效,并且打算作为应急方案。因此不要过度使用它。你可以在 ReactDOM.hydrate() 文档 中了解更多关于 hydration 的信息。

      value
      和 组件支持 value 属性。你可以使用它为组件设置 value。这对于构建受控组件是非常有帮助。defaultValue 属性对应的是非受控组件的属性,用于设置组件第一次挂载时的 value。

      All Supported HTML Attributes
      在 React 16 中,任何标准的或自定义的 DOM 属性都是完全支持的。

      React 为 DOM 提供了一套以 JavaScript 为中心的 API。由于 React 组件经常采用自定义或和 DOM 相关的 props 的关系,React 采用了小驼峰命名的方式,正如 DOM APIs 那样:

      <div tabIndex="-1" />      // Just like node.tabIndex DOM API
      <div className="Button" /> // Just like node.className DOM API
      <input readOnly={true} />  // Just like node.readOnly DOM API
      

      除了上述文档提到的特殊拼写方式以外,这些 props 的用法与 HTML 的属性也极为类似。

      React 支持的 DOM 属性有:

      accept acceptCharset accessKey action allowFullScreen alt async autoComplete
      autoFocus autoPlay capture cellPadding cellSpacing challenge charSet checked
      cite classID className colSpan cols content contentEditable contextMenu controls
      controlsList coords crossOrigin data dateTime default defer dir disabled
      download draggable encType form formAction formEncType formMethod formNoValidate
      formTarget frameBorder headers height hidden high href hrefLang htmlFor
      httpEquiv icon id inputMode integrity is keyParams keyType kind label lang list
      loop low manifest marginHeight marginWidth max maxLength media mediaGroup method
      min minLength multiple muted name noValidate nonce open optimum pattern
      placeholder poster preload profile radioGroup readOnly rel required reversed
      role rowSpan rows sandbox scope scoped scrolling seamless selected shape size
      sizes span spellCheck src srcDoc srcLang srcSet start step style summary
      tabIndex target title type useMap value width wmode wrap
      

      同样,所有的 SVG 属性也完全得到了支持:

      accentHeight accumulate additive alignmentBaseline allowReorder alphabetic
      amplitude arabicForm ascent attributeName attributeType autoReverse azimuth
      baseFrequency baseProfile baselineShift bbox begin bias by calcMode capHeight
      clip clipPath clipPathUnits clipRule colorInterpolation
      colorInterpolationFilters colorProfile colorRendering contentScriptType
      contentStyleType cursor cx cy d decelerate descent diffuseConstant direction
      display divisor dominantBaseline dur dx dy edgeMode elevation enableBackground
      end exponent externalResourcesRequired fill fillOpacity fillRule filter
      filterRes filterUnits floodColor floodOpacity focusable fontFamily fontSize
      fontSizeAdjust fontStretch fontStyle fontVariant fontWeight format from fx fy
      g1 g2 glyphName glyphOrientationHorizontal glyphOrientationVertical glyphRef
      gradientTransform gradientUnits hanging horizAdvX horizOriginX ideographic
      imageRendering in in2 intercept k k1 k2 k3 k4 kernelMatrix kernelUnitLength
      kerning keyPoints keySplines keyTimes lengthAdjust letterSpacing lightingColor
      limitingConeAngle local markerEnd markerHeight markerMid markerStart
      markerUnits markerWidth mask maskContentUnits maskUnits mathematical mode
      numOctaves offset opacity operator order orient orientation origin overflow
      overlinePosition overlineThickness paintOrder panose1 pathLength
      patternContentUnits patternTransform patternUnits pointerEvents points
      pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits
      r radius refX refY renderingIntent repeatCount repeatDur requiredExtensions
      requiredFeatures restart result rotate rx ry scale seed shapeRendering slope
      spacing specularConstant specularExponent speed spreadMethod startOffset
      stdDeviation stemh stemv stitchTiles stopColor stopOpacity
      strikethroughPosition strikethroughThickness string stroke strokeDasharray
      strokeDashoffset strokeLinecap strokeLinejoin strokeMiterlimit strokeOpacity
      strokeWidth surfaceScale systemLanguage tableValues targetX targetY textAnchor
      textDecoration textLength textRendering to transform u1 u2 underlinePosition
      underlineThickness unicode unicodeBidi unicodeRange unitsPerEm vAlphabetic
      vHanging vIdeographic vMathematical values vectorEffect version vertAdvY
      vertOriginX vertOriginY viewBox viewTarget visibility widths wordSpacing
      writingMode x x1 x2 xChannelSelector xHeight xlinkActuate xlinkArcrole
      xlinkHref xlinkRole xlinkShow xlinkTitle xlinkType xmlns xmlnsXlink xmlBase
      xmlLang xmlSpace y y1 y2 yChannelSelector z zoomAndPan
      

      你也可以使用自定义属性,但要注意属性名全都为小写。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React Hooks 是 React 16.8 中新增的特性,它可以让你在函数组件中使用 state、生命周期钩子等 React 特性。使用 Hooks 可以让你写出更简洁、可复用且易于测试的代码。 React Hooks 提供了一系列的 Hook 函数,包括 useState、useEffect、useContext、useReducer、useCallback、useMemo、useRef、useImperativeHandle、useLayoutEffect 和 useDebugValue。每个 Hook 都有特定的用途,可以帮助你处理不同的问题。 下面是 React Hooks 的一些常用 Hook 函数: 1. useState useState 是最常用的 Hook 之一,它可以让你在函数组件中使用 state。useState 接受一个初始状态值,并返回一个数组,数组的第一个值是当前 state 值,第二个值是更新 state 值的函数。 ``` const [count, setCount] = useState(0); ``` 2. useEffect useEffect 可以让你在组件渲染后执行一些副作用操作,比如订阅事件、异步请求数据等。useEffect 接受两个参数,第一个参数是一个回调函数,第二个参数是一个数组,用于控制 useEffect 的执行时机。 ``` useEffect(() => { // 这里可以执行副作用操作 }, [dependencies]); ``` 3. useContext useContext 可以让你在组件树中获取 context 的值。它接受一个 context 对象,并返回该 context 的当前值。 ``` const value = useContext(MyContext); ``` 4. useRef useRef 可以让你在组件之间共享一个可变的引用。它返回一个对象,该对象的 current 属性可以存储任何值,并在组件的生命周期中保持不变。 ``` const ref = useRef(initialValue); ref.current = value; ``` 5. useCallback useCallback 可以让你缓存一个函数,以避免在每次渲染时都创建一个新的函数实例。它接受一个回调函数和一个依赖数组,并返回一个 memoized 的回调函数。 ``` const memoizedCallback = useCallback(() => { // 这里是回调函数的逻辑 }, [dependencies]); ``` 6. useMemo useMemo 可以让你缓存一个计算结果,以避免在每次渲染时都重新计算。它接受一个计算函数和一个依赖数组,并返回一个 memoized 的计算结果。 ``` const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); ``` 以上就是 React Hooks 的一些常用 Hook 函数,它们可以帮助你更好地处理组件状态、副作用、上下文和性能优化等问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值