React官方教程笔记(React 15 4 X)

本笔记基于React官方文档,当前React版本号为15.4.0。javascript

1. 安装

1.1 尝试

开始以前能够先去codePen尝试一下,也能够下载这份HTML文件并编辑它来尝试React。html

1.2 Creat React App工具

推荐使用React官方提供的Creat React App工具,来快速新建React单页面应用项目。前端

npm install -g create-react-app
create-react-app hello-world
cd hello-world
npm start
复制代码

1.3 推荐工做流

虽然React能够在没有任何构建工具的状况下进行使用,但在生产环境仍是应该使用成套的构建工具来将React用于你的项目。一个现代化的(前端)工做流一般由如下三部分组成:java

  • 包管理器:好比YarnNpm,可让你更方便使用第三方库而不用本身造轮子
  • 编译器:好比Babel,能翻译使用了最新语法的代码到浏览器兼容较好的版本
  • 打包器 :好比WebpackBrowserify,让你可以编写各类风格的模块化的代码,由它们打包和压缩

基于以上工做流,你能够经过Npm或者Yarn来将React安装到项目,而后使用Babel来编译JSX和ES6语法,最终用于生产环境的代码还须要通过WebpackBrowserify的打包和压缩才能使用。react

1.4 CDN服务

<!--开发环境-->
<script src="https://unpkg.com/react@15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
<!--生产环境-->
<script src="https://unpkg.com/react@15/dist/react.min.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.min.js"></script>
复制代码

2. Hello World

一个最基本的React例子:webpack

ReactDom.render(
  <h1>Hello world!</h1>,
  document.getElementById('root')
)
复制代码

你能够在CodePen上尝试修改这段代码看看效果。git

React推荐配合ES6语法使用,但仅须要了解() => {}constlet`template literals`classes这几个特性便可github

3. 初识JSX

const element = <h1>hello world</h1>
复制代码

上面这段既不是字符串又不是HTML的代码(其实主要指的是<h1>hello world</h1>)就是JSX了。官方推荐搭配使用JSX,有别于模板语言,JSX是全功能的JavaScript。JSX 用于建立“React元素”。web

3.1 JSX是表达式

跟其余JavaScript表达式同样,JSX也是表达式,被React编译后的JSX返回的是普通的JavaScript对象,这意味着你能够相似对待普通JavaScript表达式那样对待一个JSX语句:将它赋值给变量、将他做为函数参数或返回值等等:算法

function getGreating (user) {
  if (user) {
    return <h1>hello {formatName(user)}!</h1>
  }
  return <h1>hello world!</h1>
}
复制代码

稍微深刻一点,Babel会将JSX转换成对react.creatElement()的调用,因此下面两种写法彻底等价:

// JSX
const mine = (
  <h1 className="greeting"> 这是个人标题 </h1>
)

// javaScript
const yours = react.creatElement(
  'h1',
  { className: 'greeting ' },
  '这是你的标题'
)
复制代码

然而react.createElement()返回的结果是相似下面这样的一个对象:

const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: '这是谁的标题'
  }
 // ...
}
复制代码

这就不难理解JSX的用法了——像一个javaScript表达式那样去使用。

3.2 在JSX中嵌入JavaScript表达式

使用花括号{},能够在JSX中嵌入任意JavaScript表达式:

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

为了提高可读性能够对JSX使用缩进和换行,可是为了不JavaScript自动添加分号的机制给咱们带来麻烦,应该在换行的JSX外面添加一对小括号。

在JSX的元素中插入用户输入的内容是安全的,React默认会对元素内的文本进行转义以防止XSS攻击。

3.3 在JSX中声明属性

就像在HTML中声明元素属性,能够在“React元素”上直接声明某个属性。当但愿属性值是变量或引用时,则就像在在JSX中嵌入JavaScript表达式,使用花括号{}来插入“React元素”的值。

// 简单属性值
const element = <div tabIndex="0"></div>;
// 属性值为变量或引用
const element = <img src={user.avatarUrl}></img>;
复制代码

须要注意的是,JSX中元素的属性名统一使用驼峰写法(camelCase),而且在React的内置元素上,诸如classfor等属性还须要换成classNamehtmlFor来使用(自定义元素能够正常使用)。

3.4 在JSX中声明子元素

若是“React元素”的标签内没有子元素,则能够像在XML中那样使用单标签(包括React内置的HTML元素)。

const element = <img src={user.avatarUrl} />; 复制代码

若是存在子元素,则就像在HTML中那样直接包裹在父元素中便可(注意换行的JSX要加小括号()):

const element = (
  <div> <h1>Hello!</h1> <h2>Good to see you here.</h2> </div>
)
复制代码

4. 渲染元素

元素是React应用的最小组成部分。元素描绘了界面。不一样于浏览器的DOM元素,React元素是简单对象,建立它们比建立真实的DOM元素要节省太多性能,同时React DOM负责将React元素和真实DOM元素对应起来:

const ele = <h1>Hello World!</h1>
复制代码

不能将React元素和React组件搞混,React元素是React组件的组成部分,一个React组件由一个或多个React元素组成。同时也要注意区别DOM元素和React元素,DOM元素指的是HTML标准中规定的具体的某个元素,而React元素其实是用于告诉React如何渲染页面、渲染时用到哪些DOM元素的一个配置对象,它与DOM元素不是一个概念。

4.1 将React元素渲染到DOM中

先建立一个React元素,而后用ReactDOM.render()将其渲染到DOM的某个元素中(就这么简单):

const ele = <h1>Hello World!</h1>
ReactDOM.render(
  ele,
  document.getElementById('root') // 假设页面上有一个id为root的元素
)
复制代码

4.2 更新已经渲染的元素

请记住,React元素是不可变的,一旦建立,你就不能再直接改变它的属性或子元素。假如咱们要更新上面已经渲染到idroot的元素中的React元素,那么在没有其余手段的前提下就只能是像电影胶片同样一帧一帧进行刷新:

function tick() {
  const element = (
    <div> <h1>Hello World!</h1> <p>{new Date().toLocaleTimeString()}</p> </div>  
  )
  ReactDOM.render(
    ele,
    document.getElementById('root') // 假设页面上有一个id为root的元素
  )
}
setInterval(tick, 1000) // 每秒刷新
复制代码

固然正常状况下咱们不会这么作,可是这里很好的演示了另一个问题——React在渲染页面时都作了什么?答案是它只渲染了与上次渲染时DOM中不一样的部分!React会比较当前渲染与上次渲染时DOM中的不一样之处,并只刷新这些地方!

[图片上传失败...(image-89394-1535353259915)]

5. 组件和props(输入属性)

组件能让你将UI分割成独立的可复用的片断,这些片断都有各自隔离的做用域,不会互相干扰。你能够将组件理解成相似函数的概念,组件从它的props属性接受参数,而后返回React元素来描述UI。

5.1 用函数和类(class)定义组件

最简单的定义组件的方式就是写一个构造函数:

function Welcom (props) {
  return <h1>hello, {props.name}</h1>
}
复制代码

上面这个Welcom构造函数就是一个合法的React组件,由于它接受一个对象做为参数,而后返回React元素。咱们称这样的组件为“函数式”的组件由于它就是一个JavaScript构造函数。固然也可使用ES6的class特性来定义函数:

class Welcom extends React.Component {
  render () {
    return <h1>hello, {this.props.name}</h1>
  }
}
复制代码

ES6的class特性实际上是ES5的构造函数和对象继承特性的一个语法糖,上面的写法也彻底能够转换为ES5的写法。React推荐这种写法存粹是由于写起来方便,可读性也更强。但这种写法的重点是从React.Component继承一些核心的属性,后文还会细说。不过目前简单起见,咱们暂时还只是用简单函数来建立组件。

5.2 渲染组件

React元素不只仅能够用于指定须要使用的DOM元素,也能够用于指代自定义的组件:

// 指代须要使用的DOM元素
const ele1 = <div />
// 指代用户自定义的组件
const ele2 = <Welcom name="Sara">
复制代码

当React遇到像<Welcom name="Sara">这种自定义组件时,它会将JSX属性(也就是React元素属性)都放在一个对象中(这个对象就是props)并将其传递给组件的构造函数,构造函数再返回React元素用于渲染。

5.3 组件的组合

既然React元素可用于指代自定义组件,那么组件之间就能够相互嵌套使用:

function Welcom (props) {
  return <h1>Hello, {props.name}</h1>
}
function WelcomList () {
  return (
    <div>
      <Welcom name="Sara" />
      <Welcom name="Lily" />
      <Welcom name="Tom" />
    </div>
  )
}
function App () {
  return <WelcomList />
}
ReactDOM.render(
  <App />,
  document.getElementById('root')
)
复制代码

5.4 组件的提取

既然组件能够嵌套组合使用,咱们就能够将一个大的组件分割成不少小的组件。React官方鼓励对UI进行切割,分红不一样的组件来实现。基本上一组React元素是否要提取成组件,可从如下两点考虑:

  • 这组元素在别的地方也要使用
  • 这组元素内部的功能相对复杂

这部分实际上是组件化的思路,这里再也不展开。

5.5 只读的props

相似于“纯函数”的概念(不会改变任何外部的值,包括输入的参数,即与外部彻底无耦合),无论是使用构造函数仍是类来定义组件,组件都不该该修改它的props,由于这是输入到组件中的参数。在这一点上,React作了严格限定:

全部的React组件必须像“纯函数”那样永远不修改本身的props属性

6. state(私有状态)和生命周期

咱们以上文的时钟的例子来理解组件的私有状态和生命周期。

function tick() {
  const element = (
    <div> <h1>Hello World!</h1> <p>{new Date().toLocaleTimeString()}</p> </div>  
  )
  ReactDOM.render(
    ele,
    document.getElementById('root') // 假设页面上有一个id为root的元素
  )
}
setInterval(tick, 1000)
复制代码

首先咱们将时钟做为组件提取出来:

// 时钟组件
function Clock(props) {
 return (
    <div> <h1>Hello World!</h1> <p>{props.date.toLocaleTimeString()}</p> </div>  
  )
}
// 从新渲染
function tick () {
  ReactDOM.render(
    <Clock date={new Date()} />, document.getElementById('root') // 假设页面上有一个id为root的元素 ) } // 每秒刷新 setInterval(tick, 1000) 复制代码

咱们发现对于Clock组件来讲,刷新时间的功能其实彻底与外部无关,它不涉及到任何外部的变量,彻底能够由Clock组件本身来实现而不是让外部传递时间给它。此时Clock组件就须要“私有状态”来实现这个功能了。

6.1 从React.Component上继承

到目前为止,咱们使用简单的构造函数来建立React组件,无论外部输入属性仍是私有状态,都须要咱们手动建立和管理,诸如修改私有状态后刷新渲染,外部输入属性为只读这类功能,若是咱们没有在构造函数中手动实现则不会存在。

这时咱们能够从React.Component这个React内置的构造函数上继承一些有用的方法,这其中就包括对“私有状态”和“生命周期”实现。咱们可使用ES6的class特性来实现这个继承(固然这不是必须的,彻底可使用ES5的构造函数和原型的写法,但那样会繁琐不少,可读性也大大降低):

class Clock extends React.Component {
  render () { // React提供的用于渲染和刷新组件的钩子函数
    return (
      <div> <h1>Hello, world!</h1> <h2>It is {this.props.date.toLocaleTimeString()}.</h2> </div>
    )
  }
}
复制代码

6.2 定义组件私有状态

React.Component提供了propsstate来分别访问外部输入属性和内部私有状态。咱们能够在时钟组件中经过state访问私有状态,而后在其构造函数中对该私有状态进行初始化,最后将它渲染到页面上:

class Clock extends React.Component {
  constructor (props) {
    super(props) // ES6中类的constructor函数能够经过super访问其父类的构造函数
    this.state = { date: new Date() }
  } // 注意,ES6中类的方法之间不须要任何符号
  render () {
    return (
      <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
    )
  }
}
ReactDOM.render(
  <Clock />, // 外部再也不干涉Clock组件的刷新功能 document.getElementById('root) ) 复制代码

注意Clock类中的constructor构造函数中,调用了父类的构造函数,这是为了实现彻底的继承。使用class特性建立React组件时应当老是执行这一步。

6.3 添加生命周期函数

从组件被建立到组件被渲染到页面到最终被销毁,React提供了一系列的“生命周期钩子”,用于在组件的不一样阶段调用回掉函数。为了让Clock组件可以本身刷新,咱们但愿在组件被建立后当即添加一个计时器进行每秒刷新,同时在组件被销毁时一并销毁这个计时器,这样咱们就须要用到两个生命周期钩子函数:

  • componentDidMount:组件被渲染到页面后执行
  • componentWillUnmount:组件被销毁前执行
class Clock extends React.Component {
  constructor (props) {
    super(props) // ES6中类的constructor函数能够经过super访问其父类的构造函数
    this.state = { date: new Date() }
  } // 注意,ES6中类的方法之间不须要任何符号
  render () {
    return (
      <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
    )
  }
  componentDidMount () {
    this.timerID = setInterval(this.tick.bind(this), 1000)
  }
  componentWillUnmount() {
    clearInterval(this.timerID)
  }
}
ReactDOM.render(
  <Clock />, // 外部再也不干涉Clock组件的刷新功能 document.getElementById('root) ) 复制代码

注意咱们将定时器存储在了组件实例上,而不是state中,请先记住一个原则:任何没有在组件的render()函数中使用的变量,都不该该存放在state

而后再添加tick方法。在这个方法中咱们须要改变组件state中的date的值,这时须要用到方法setState(),该方法会通知React如今state已经改变了,然后React会去从新调用组件的Render()方法刷新DOM。这也是为何会有**任何没有在组件的render()函数中使用的变量,都不该该存放在state中 **一说:

class Clock extends React.Component {
  constructor (props) {
    super(props) // ES6中类的constructor函数能够经过super访问其父类的构造函数
    this.state = { date: new Date() }
  } // 注意,ES6中类的方法之间不须要任何符号
  render () {
    return (
      <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
    )
  }
  componentDidMount () {
    this.timerID = setInterval(this.tick, 1000)
  }
  componentWillUnmount() {
    clearInterval(this.timerID)
  }
  tick () {
    this.setState({ date: new Date() }) // 该方法会触发React调用实例的render方法进行重绘
  }
}
ReactDOM.render(
  <Clock />, // 外部再也不干涉Clock组件的刷新功能 document.getElementById('root) ) 复制代码

6.4 组件生命周期小结

    1. 当把组件传递给ReactDOM.render()函数后,React会调用组件的构造函数constructor,进行一些初始化
    1. 而后React会去调用Clock组件的render()方法将组件渲染出来
    1. 当组件渲染完毕后,React会调用componentDidMount()生命周期钩子函数
    1. setState()函数被调用时,React会从新调用组件的render()方法进行重绘
    1. 当组件被从DOM中移除时,React会调用componentWillUnmount()生命周期钩子函数

6.5 setState注意事项

  • 不要直接改变state 直接对组件state中的属性赋值将不会触发DOM更新,由于React并不知道state被改变了
  • state的更新多是异步的 React会一次处理多个对setState的调用以提升性能,因此调用setState()时不该当直接基于另一些来自stateprops中的属性进行计算,颇有可能当前计算的值并非最终的值,当用于计算的另外一些值再次变化后,React并不会刷新DOM(由于没有再次调用setState())。为了修正这点,React提供另外一种调用setState()函数的方式:传入一个函数,而不是对象
// 错误的用法
this.setState({
  counter: this.state.counter + this.props.increment
})
// 正确的用法
this.setState((prevState, props) => ({ // 接受一个表示前次state的参数和一个当前props的参数
  counter: prevState.counter + props.increment // 这里其实是返回了一个对象,是ES6箭头函数的简写
}))
复制代码
  • setState是对象的合并而不是替换 setState方法是将传入的参数对象或函数返回的对象与现有的state对象进行合并,很是相似于使用Object.assign(prevState, newState)的效果

6.6 单项数据流

在React组件的嵌套中,父组件经过props向子组件传递数据,无论传递进来的数据是来自于父组件的props仍是state仍是别的地方,子组件不知道也不用关心,由于它不能修改经过props传递进来的数据而只能读取它。这样,数据就能够从最外层的父组件一路向内传递下去,但反过来却不行。

这就是传说中的“单项数据流”("top-down" or "unidirectional" data flow)了:每一个组件只能修改自己和其子组件的数据,而不能修改父组件的数据。这样的好处不言而喻,数据和状态的管理会更加方便,但有时候在应用愈来愈复杂的时候,可能须要多个组件共享某些数据或状态,所以诞生了不少用于管理数据和状态的库,redux就是其中最有名的一个。

7. 事件

7.1 基本用法

在React中绑定事件跟直接在HTML中绑定事件很是类似,定义一个事件处理函数,并在JSX中绑定它:

function Greeting () {
  function sayHi(e) {
    e.preventDefault()
    console.log('Hi!')
  }
 return (
    <a onClick={sayHi}>Click me to say hi!</a>
 )
}
复制代码

全部事件绑定属性好比onClick均使用驼峰写法(camelCase),事件绑定属性的值不是字符串而是事件处理函数名称,能够带上()并传参,无参数时可省略()

7.2 使用类定义组件时事件处理函数this的指向问题

使用ES6的class特性定义组件时,一般的作法是将事件处理函数看成该类的方法写在类中。但须要注意的是方法的this指向。

定义在类中的方法的默认的this指向的是当前的类的实例,但事件处理函数由于是绑定到了具体的元素上,就会丢失定义时this的指向。若是你的处理函数中使用了this关键字来指向当前组件实例,那么你须要手动将该方法的this绑定到当前组件实例,有三种方法能够进行绑定:

1)在类的constructor中调用或在JSX中调用Function.prototype.bind()手动绑定

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};
    this.handleClick = this.handleClick.bind(this); // 手动绑定
  }

  handleClick() {
    // console.log(this)
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      // <button onClick={this.handleClick.bind(this)}> // 在这里绑定也能够
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('content')
);
复制代码

2)在JSX的事件绑定属性中的事件处理函数外层再套一个箭头函数,在其中返回处理函数调用结果

render() {
  return (
    <button onClick={(e) => this.handleClick(e)}> // 这么绑定也行 Click me </button>
  );
}
复制代码

3)Babel提供的一个ES8+的实验性质的写法

class LoggingButton extends React.Component {
  handleClick = () => { // 纯粹的实验性质的写法,须要babel的支持
    console.log('this is:', this);
  }
  render() {
    return (
      <button onClick={this.handleClick}> Click me </button>
    );
  }
}
复制代码

7.3 事件对象

React的事件对象是一个彻底由React给出的事件对象,该对象对各个浏览器作了兼容,同时保留了标准事件对象的接口,详细信息能够查看React官网的参考。使用时须要关心的是如何在事件处理函数中使用事件对象。

在事件绑定的JSX中,处理函数接受一个名为event的参数来表示事件对象,能够认为event在事件绑定插值中属于React的保留字,若是须要往事件处理函数中传递更多参数,请使用其余标识符。

另外,7.2小节中不一样的事件绑定写法也对事件对象的处置略有不一样,主要体如今事件绑定JSX中:

// 无括号
<button onClick={this.handleClick}>
  Click me
</button>

// 带括号
<button onClick={this.handleClick(event)}>
  Click me
</button>

// 调用了bind()
<button onClick={this.handleClick.bind(this, event)}>
  Click me
</button>
复制代码
  • 当事件绑定插值中的处理函数省略了()时,处理函数默认接受一个表示事件对象的参数,
  • 当事件绑定插值中的处理函数未省略()时,则须要显示地使用保留字event来传入事件对象,未传入则为undefined注意,无论有没有在constructor中绑定this,直接在处理函数名后加()会致使页面初始化时该函数被当即执行一次,可能会有意想不到的错误,好比不能调用setState()方法等,因此强烈不建议用这种写法
  • 当事件绑定插值中的处理函数调用了bind()时,能够显示地使用保留字event来传入事件对象,不然React会在bind()函数参数序列的末尾默认增长一个表示事件对象的参数

最后,在React中不能经过return false来阻止默认事件,而是须要在事件处理函数中显式调用event.preventDefault()

8. 条件渲染

全部的JavaScript条件语句均可以用于React条件渲染,由于本质上JSX就是JavaScript的扩展语言。基于此有三种经常使用的条件渲染:

  • if...else...
function UserGreeting () {
  return <h1>Welcom back!</h1>
}
function GuestGreeting () {
  return <h1>Please Sign up.</h1>
}
function App (props) {
  if (!props.isLoggedIn) {
    return <GuestGreeting />
  }
  return <UserGreeting />
}

ReactDOM.render(
  <App isLoggedIn={false} />,
  document.getElementById('root')
)
复制代码
  • 三元运算符
function App (props) {
  return props.isLoggedIn ? <UserGreeting /> : <GuestGreeting /> } 复制代码
  • 短路
function App (props) {
  return props.isLoggedIn && <UserGreeting /> // props.isLoggedIn为true则显示UserGreeting,不然不显式 } 复制代码

若是判断逻辑比较复杂,不能用三元或者短路表达式编写,且判断后的结果须要直接用在JSX中(JSX中只能经过{}插入表达式,而不能使用语句),则可以使用if...else...语句判断并将结果保存到变量,而后再返回变量或经过{}插值到JSX中:

function UserGreeting () {
  return <h1>Welcom back!</h1>
}
function GuestGreeting () {
  return <h1>Please Sign up.</h1>
}
function Button (props) {
  return <button onClick={ props.handleToggle }>toggle me</button>
}

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      prevState: false
    }
  }
  handleClick () {
    this.setState(prevState => ({ isLoggedIn: !prevState.isLoggedIn }))
  }
  render () {
    let greeting = this.state.isLoggedIn ? <UserGreeting /> : <GuestGreeting />
    return (
      <div>
        <div><Button handleToggle={this.handleClick.bind(this)} /></div> // 注意this的重定向
        { greeting }
      </div>
    )
  }
}

ReactDOM.render(
  <App isLoggedIn={false} />,
  document.getElementById('root')
)
复制代码

另外,在组件的render函数中返回假值,会阻止组件渲染,结合条件判断,可以达到隐藏或显示组件的目的。

9. 列表和key(索引)

9.1 渲染列表

能够像下面这样渲染一个列表:

class List extends React.Component {
  constructor (props) {
    super(props)
  }
  render () {
    let list = this.props.number.map(number => ( // 拼装li
      <li>{number}</li>
    ))
    return (
      <ul>{list}</ul>
    )
  }
}

ReactDOM.render(
  <List number={[1, 2, 3, 4, 5]} />,
  document.getElementById('root')
)
复制代码

也能够将map()调用经过{}内联到JSX中:

class List extends React.Component {
  constructor (props) {
    super(props)
  }
  render () {
    return (
      <ul>{ this.props.number.map(number => ( // 内联map()方法 <li key={number}>{number}</li> )) }</ul>
    )
  }
}
复制代码

一般会使用数组的map()方法来从数组拼装列表,这与使用JavaScript拼装HTML相似。但上面的代码运行时会出现警告:

列表渲染报错

9.2 key

在渲染列表时,React的差别比较算法须要一个在列表范围内的惟一key来提升性能(一般用于获知哪一个列表项改变了)。这个惟一的key须要咱们手动提供。React官方建议使用列表数据中可用于惟一性标识的字段来做为列表项渲染时的key。若是实在没有,则可以使用数组的index勉为其难,性能上可能会打折扣。

let list = this.props.number.map(number => ( // 拼装li
      <li key={number.toString()}>{number}</li>
    ))
复制代码

key的使用须要注意一下几点:

  • 只能在数组内指定key:准确地说,只能在map()的回调函数中使用key
  • key须要在列表范围内保证惟一性:同一个数组中的key须要保证惟一性,但不一样数组中的key无所谓
  • key不会做为props传入组件:能够认为key是React在JSX中的保留字,你不能用它来向组件传递数据而应该改用其余词

10. 表单

在React中存在一个“受控组件(Controlled Component)”的概念,专门指代被React控制了的表单元素。经过onChange事件的处理函数将表单元素值的变化映射到组件的state中,而后再将组件中的这个映射好的值经过{}在JSX中插值给表单元素的value,(两者缺一不可)这就是一个被React控制了的组件也即“受控组件”了。

class Form extends React.Component {
  constructor (props) {
    super(props)
    this.state ={
      inputTextValue: ''
    }
    this.handleInputTextChange = this.handleInputTextChange.bind(this)
  }
  render () {
    return (
      <form> <input value={this.state.inputTextValue} // 从state中将值绑定到表单元素 onChange={this.handleInputTextChange}/> </form> ) } handleInputTextChange (e) { this.setState({ inputTextValue: e.target.value // 将表单元素的值的变化映射到state中 }) } } ReactDOM.render( <Form />, document.getElementById('root') ) 复制代码

基本上全部表单元素的使用都跟上例同样,经过value来“控制”元素,让state成为组件惟一的状态保存地。可是有时候在非React项目中使用React或者一些其余缘由,咱们不但愿使用受控组件时,能够选择“非受控组件”技术,这里再也不展开。

11. 共享状态提高

考虑下面的需求,页面上有两个输入框,用来输入货币数量,一个输入美圆,一个输入人民币,还有一行提示文字例如:“咱们有1美圆,也就是6.9元”;要求两个输入框随意输入一个,另外一个输入框会根据汇率自动显示转换后的货币数量,而且下方提示文字也跟随变化。

一般状况下,咱们会编写一个用于输入货币数量的组件,而后在页面上放两个这样的组件:

const exchangeRate = 6.9339
const currency = {
  '$': '美圆',
  '¥': '人民币'
}
class CurrencyInput extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      value: ''
    }
    this.changeHandler = this.changeHandler.bind(this)
  }
  render () {
    return(
      <div>
        <label>
          {currency[this.props.currency]}:
          <input value={this.state.value} onChange={this.changeHandler}/>
        </label>
      </div>
    )
  }
  changeHandler (e) {
    this.setState({
      value: e.target.value
    })
  }
}
class App extends React.Component {
  constructor (props) {
    super(props)
  }
  render () {
    return(
      <div>
        <CurrencyInput currency={'$'}/>
        <CurrencyInput currency={'¥'} />
        <p>咱们有{}美圆,也就是{}元</p>
      </div>
    )
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
)
复制代码

在上面的代码中咱们将货币种类经过props传递给输入框组件,分别显示了美圆和人名币的输入框。而后在输入框组件内部,咱们使用了上一节的“受控组件”技术,将输入框的值交由组件的state控制。但并无完成需求——两个输入框并不一样步,同时组件外部也不知道组件中到底填了什么值因此下面的提示语句也没有更新。

不少时候,若干组件须要隐射同一个变化的状态。咱们推荐将共享的状态提高至它们最近的共同的祖先上。

就像官方推荐的那样,这时咱们就须要用到共享状态提高技术:咱们要将两个货币输入框组件共享的“数量”状态,提高到它们最近的祖先组件上,也就是App组件上。

// ...省略的代码
class CurrencyInput extends React.Component {
  constructor (props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
  }
  render () {
    return(
      <div>
        <label>
          {CURRENCY[this.props.currency]}:
          <input value={this.props.value} onChange={this.handleChange}/> // 须要传递额外参数的状况下只能再包一层
        </label>
      </div>
    )
  }
  handleChange (e) {
    this.props.onValueChange(e.target.value, this.props.currency) // 父级传递进来的回调函数
  }
}

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = { // 将共享状态存放在祖先元素上
      dollar: '',
      yuan: ''
    }
    this.valueChangeHandler = this.valueChangeHandler.bind(this)
  }
  render () {
    return( // 经过props向下传递共享状态和回调函数,不少状况下子组件共享的状态父级也须要用到
      <div>
        <CurrencyInput value={this.state.dollar} currency={'$'} onValueChange={this.valueChangeHandler}/>
        <CurrencyInput value={this.state.yuan} currency={'¥'} onValueChange={this.valueChangeHandler}/>
        <p>咱们有{this.state.dollar}美圆,也就是{this.state.yuan}元</p>
      </div>
    )
  }
  valueChangeHandler (value, type) {
    this.setState({
      dollar: type === '$' ? value : this.exchange(value, type),
      yuan: type === '¥' ? value : this.exchange(value, type)
    })
  }
  exchange (value, type) {
    return value * (type === '$' ? EXCHANGERATE : 1 / EXCHANGERATE)
  }
}
// ... 省略的代码

复制代码

其实无论是美圆仍是人民币,其实背后都只有一个数量,这个数量同时表明了必定数量的美圆和必定数量的人民币,因此更好地,咱们能够也应该只存放一个状态在父组件上,而后在渲染子组件时计算子组件的状态并传递给他们:

// ... 省略的代码
function exchange (value, type) { // 将转换函数放到全局以便子组件能够访问
  return value * (type === '$' ? EXCHANGERATE : 1 / EXCHANGERATE)
}

class CurrencyInput extends React.Component {
 // ... 省略的代码
  render () {
    // 子组件在渲染时本身计算本身的状态
    let currentCurrency = this.props.currentCurrency
    let currency = this.props.currency
    let value = ''
    if (currentCurrency.value !== '' && !/^\s+$/g.test(currentCurrency.value)) {
      value = currentCurrency.type === currency ?
        currentCurrency.value : 
        exchange(currentCurrency.value, currentCurrency.type)
    }   
    return(
      <div>
        <label>
          {CURRENCY[currency]}:
          <input value={value} onChange={this.handleChange}/>
        </label>
      </div>
    )
  }
  // ... 省略的代码
}

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      currentCurrency: { // 存储一个值,这里具体作法时存储当前改变的值
        value: '',
        type: ''
      } 
    }
    this.valueChangeHandler = this.valueChangeHandler.bind(this)
  }
  render () {
    // 将共享的状态传递给组件,同时父组件须要的状态也本身计算出来
    return(
      <div>
        <CurrencyInput
          currentCurrency={this.state.currentCurrency}
          currency={'$'}
          onValueChange={this.valueChangeHandler}/>
        <CurrencyInput
          currentCurrency={this.state.currentCurrency}
          currency={'¥'}
          onValueChange={this.valueChangeHandler}/>
        <p>咱们有{exchange(this.state.currentCurrency.value, '$')}美圆,也就是{exchange(this.state.currentCurrency.value, '¥')}元</p>
      </div>
    )
  }
  valueChangeHandler (value, type) { // 这里只须要简单映射关系便可,再也不须要计算各个组件的具体状态值
    this.setState({
      currentCurrency: { value, type }
    })
  }
  
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
)
复制代码

上面的例子很好地贯彻了React官方反复强调推荐的“单项数据流”模式。虽然多写了一些代码,可是好处是能够减小由于子组件能够自行修改共享状态而引发的一些bug,毕竟咱们将共享状态提高到父级组件上之后,全部对共享状态的修改就都集中在父级组件上了。

另外,再次强调一个原则:任何能够由stateprops计算出来的状态,都不该该放在state。就像上例那样,应该直接在render()函数中直接计算后使用。

12. 聚合而不是继承

React官方推荐使用聚合而不是继承来在组件之间复用代码。一般有两种服用的状况,一种是组件的部分结构或内容不肯定,须要由外部传入,这时组件就至关于一个容器;另外一种是从更为抽象的组件建立一个较为具体的组件,好比“弹层”和“登录弹层”。

12.1 容器

当组件内有部份内容不肯定须要外部传入时,可使用一个特殊的props属性children来传入。在组件内部访问props.children能够获取使用组件时写在组件开始和结束标签内的内容:

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

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

当组件有多个部份内容不肯定都须要外部传入时,单靠props.children就不能知足需求了。但时不要忘记React组件的props能够接受任意类型的参数,因此其实组件的内容也彻底能够直接使用props来传递到组件内部:

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

function App() { // JSX中使用{}插入另外一个JSX,由于JSX也是表达式
  return <SplitPane left={ <Contacts /> } right={ <Chat /> } /> } 复制代码

12.2 具象化

有时咱们但愿一个组件是另外一个较为抽象的组件的特例(更为具象),官方推荐的作法是将抽象组件包裹在具象组件中,并使用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!" /> ); } 复制代码

至于继承...忘掉它吧。

13. Think in React (这部分直接翻译的原文)

在React官方看来,React是构建大型、高性能web应用的不二之选。它在Fb和Ins表现得很是好。React最棒呆的地方在于它让你在构建应用时如何看待你的应用。下面给咱们经过编写一个搜索列表,来带你体验这个思惟过程。

13.1 从效果图开始

假设咱们已经有了一个JSON接口,并有了设计师给咱们的效果图:

效果图

JSON接口返回的数据格式以下:

[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
]
复制代码

13.2 第一步:将UI按照组件层级进行分解

要作的第一件事就是在效果图上的组件(和子组件)周围画框框,并命名组件。若是这是你的设计师同事给你的,那么这部分工做他可能已经作完了,去和他唠唠。他的PSD图层名颇有可能能够做为你的组件名。

但具体怎么划分组件呢?答案是跟你建立一个函数或对象同样。这其中的一个原则是“单一职责原则”,具体来讲就是一个组件应该只作一件事,不然,它应该被拆分红更多的子组件。

若是你常常将JSON数据展示给你的用户,那你应该知道若是你建立了正确的数据模型,你的UI(和组件)将会规划组织的很是好。由于你的UI和数据模型是同一个信息结构,这也意味着划分组件是一件比较繁琐的事情。就将你的组件按照JSON返回的数据结构拆分为就行了。

组件拆分

(未完待续,这是React官方基础教程的最后一章,有空我再继续翻译吧)

(2018年7月27日更新......天道好轮回,苍天饶过谁......最终仍是要写react,时隔快2年了,我本身也得回来看文章复习一波...索性把坑填上吧!)

你会看到这个简单的应用中有5个组件。咱们将每一个组件所表明的数据用斜体表示。

  1. FilterableProductTable(橙色):示例的所有内容
  2. SearchBar(蓝色):接受 用户输入
  3. ProductTable(绿色):基于用户输入显示过滤好的 产品列表
  4. ProductCategoryRow(青色):为每一个 类目 显示一个标题
  5. ProductRow(红色):为每一个 产品 显示一行

ProductTable的表头(包含NamePrice标签)不是一个单独的组件。这是个偏好问题,无论哪一种方式都还存在争议。这个例子中,咱们将其留在了ProductTable中,由于ProductTable的任务是渲染 产品列表 ,而表头也算列表的一部分。然而,当这个表头变得复杂了(好比当咱们添加了排序功能),就能够瓜熟蒂落地将其抽出来成为ProductTableHeader组件。

既然如今咱们已经将组件都识别出来了,那就把他们结构化一下吧。这很easy。效果图中出如今另外一个组件内部的组件,在结构上应该做为前者的子组件:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

13.3 第二步:用React构建一个静态版本

codepen查看示例代码

如今你有了组件结构,是时候开始实现你的应用了。最容易的方式是构建一个带着数据并渲染了UI,可是没有交互的版本。最好是将这两步分开作,由于构建静态版本只须要无脑编写,而添加交互则须要大量思考和少许编写。后面咱们会看到这么作的缘由。

为了构建你的应用的静态版本,你会想要复用其它组件来构建新的组件,并利用props来传递数据。props是从父组件传递数据到子组件的一种方式。若是你熟悉state,那么构建这个静态版本就彻底不要使用statestate是专为交互性预留的,数据会随时间改变。目前这仍是个静态版本,你还不须要它。

你能够自上而下或者自下而上进行构建。也就是说,你能够从较高层的组件开始构建(好比从FilterableProductTable开始),也能够从较底层的组件开始(ProductRow)。在简单的示例中,一般自上而下更容易,而在更大型的项目中,自下而上地构建组件并同时为其编写测试会来的更容易。

在这一步骤的结尾,你讲会有一个可复用的组件库来渲染你的数据。因为目前你的应用仍是静态版本,组件们将只有render()方法。组件结构最顶层的组件(FilterableProductTable)会将你的数据做为一个"prop"进行接收。若是你修改了基础数据并再次调用ReactDOM.render(),UI会跟着更新。很容易就能观察到你UI的更新和变化的地方,由于到目前为止尚未什么复杂的事情发生。React 的单向数据流(又被称为单向绑定)使得一切有序而迅捷。

执行这一步时若是须要能够连接到React文档寻找帮助。

小插曲:Props与State

在React中有两种数据模型:propsstate。理解两种模型的差异很是重要,若是你不了解其中差别,能够跳转到React官方文档进行查看。

13.4 第三步:标识出最小(但完整的)UI状态

为了让你的UI可交互,你须要可以触发变化到你的基础数据上。React用**state**将这件事变得很简单。

为了正确构建出你的应用,首先你须要思考你的应用须要的最小可变state集合。这里的关键是DRY: Don’t Repeat Yourself。搞清楚你的应用须要的最小的state表示,而后按须要计算出其它的一切。好比,你要构建一个TODO LIST,只须要维护一个TODO项数组便可;不须要单独为数量保存一个state变量。相反,当你想要渲染TODO的数量时,只须要简单地读取TODO项数组的长度便可。

考虑一下咱们示例应用中的全部数据:

  • 原始产品列表
  • 用户输入的搜索文字
  • 复选框的值
  • 过滤后的产品列表

咱们一个一个过一遍来搞清楚哪一个才是state。只要简单地在每一个数据上问三个问题:

  1. 它是从父组件经过props传递进来的吗?若是是,那它不是state
  2. 它一直不会变化吗?若是是,那它不是state
  3. 你能基于组建的其它stateprops计算出它来吗?若是是,那它不是state

原始产品列表是从props传递进来的,因此它不是state。用户输入的搜索文字和复选框的值好像是state,由于他们会变化,而且不能基于其它数据计算出来。最后,过滤后的产品列表不是state,由于他可以基于原始产品列表、用户输入的搜索文字和复选框的值计算出来。

因此最终,咱们的state是:

  • 用户输入的搜索文字
  • 复选框的值

13.5 第四步:肯定你的state的位置

codepen查看示例代码

因此咱们如今已经get到了应用须要的最小state集合,接下来咱们须要搞清楚这些状态应该放到哪些组件里。

记住:React的一切都是关于沿着组件层次结构乡下的单项数据流。可能一开始你无法立刻搞清楚哪一个组件应该拥有哪些状态。这一般对于新手来讲是最难的部分了,但你能够按下面的步骤来搞定:

对于你应用里的每个state来讲:

  • 肯定基于该state渲染的全部组件
  • 找到这些组件的一个共同的全部者组件(在层级结构中找到一个包含全部须要该state的组件的单一组件)
  • 要么是共同的全部者组件,要么是更高层的祖先组件应该拥有该state
  • 若是你没办法找到一个合适的组件来控制这个state,那就在共同的全部者组件之上再为其建立一个父组件来控制这个state

让咱们按照这个策略来看一下咱们的应用:

  • ProductTable须要基于state来过滤产品列表,SearchBar须要展现搜索文字和复选框状态
  • 他们共同的全部者组件是FilterableProductTable
  • 那么理论上把过滤文字和复选框的值放到FilterableProductTable上是比较合适的

好,咱们已经决定将咱们的state放在FilterableProductTable上了。首先,在FilterableProductTableconstructor中添加一个实例属性this.state = { filterText: '', inStockOnly: false }来初始化你的应用的state。而后将filterTextinStockOnly做为props传递给ProductTableSearchBar。最后,用这俩props来过滤ProductTable的行,以及设置SearchBar的值。

你已经能够看到你的应用的一些表现了:将filterText设置为ball并刷新你的应用,你将看到数据表格正确地更新了。

13.6 第五步:添加反向数据流

codepen查看示例代码

目前咱们已经构建了一个应用,propsstate沿着层级结构自上而下流转并正确渲染出来。如今是时候支持另外一种方式的数据流了:层级结构深处的的表单组件须要更新FilterableProductTable上的state

React将这种数据流作的很是直白,让你能更好理解地你的程序是如何运转的。但相较于传统的双向数据绑定,它的确会须要多写一点代码。

若是你尝试在当前这个版本的示例中输入或点击复选框,React将会忽略你的输入。这是故意的,由于咱们已经将inputvalue设置为老是等于从FilterableProductTable传递进来的state

让咱们思考一下咱们想作什么。咱们但愿不管什么时候用户修改了表单,都要更新state来反映用户输入。由于组件应该只更新它们本身的stateFilterableProductTable会给SearchBar传递回调函数,当state须要更新时执行该回调函数便可。咱们可使用inputonChange事件来通知其更新。FilterableProductTable传递的回调函数中会调用setState(),应用就会更新了。

虽然这听起来很复杂,但实际上却只有几行代码而已。而且你的数据如何在应用中流转已经很是明确了。

13.7 终于搞定了

但愿这篇文章能给你一些如何用 React思考构建组件和应用的启发。虽然这可能比你原来的代码多一些,但请记住,读代码的次数要远远多于写代码的次数,并且读这种模块化的、直白的代码很是容易。当你开始构建大型组件库时,你会感激这种明晰性和模块化,而且随着代码的重用,你的代码会越写越少。:)

相关文章
相关标签/搜索