全文翻译自React 16.6.0
英文文档 ,适当精简了生产环境不经常使用的内容,并对部分较为复杂的概念进行了更加翔实的解读,以及与
Vue2 进行了一些特性方面的比较。本文首先会介绍React
16 带来的一系列变化与新特性,然后解读 React 官方文档Docs 当中Quick
Start 和Advanced
Guides 的内容,最后基于项目上的使用实践,开源了一个较为完整的脚手架项目Rhino ,适合已经具备组件式 前端框架开发经验的同学快速上手。
2017 年 9 月 Facebook
释出React v16.0.x
,宣布使用对商业使用更加友好的 MIT license
开源许可,并带来了全新的render()
函数返回类型、更加健壮的错误处理机制、全新的Fragment
和Portal
特性,并完全重写了类库的核心架构,带来更为优异服务器端渲染性能的同时,有效缩小了类库代码本身的体积,更重要的意义在于杜绝了
Preact
等衍生框架对 React 社区所造成的分裂。
快速开始
如果使用 npm 作为依赖管理工具,可以通过下面命令安装 React。
1 npm install --save react react-dom
一个使用 ES6 的最简单例子是这样的:
1 2 3 4 import React from "react" ;import ReactDOM from "react-dom" ;ReactDOM .render (<h1 > Hello, React 16.6.0 !</h1 > , document .getElementById ("app" ));
当然,也可以使用独立的 React
发布包,直接在<script>
标签当中包含使用。
1 2 3 4 5 6 <script crossorigin src ="https://unpkg.com/react@16/umd/react.development.js" > </script > <script crossorigin src ="https://unpkg.com/react-dom@16/umd/react-dom.development.js" > </script > <script crossorigin src ="https://unpkg.com/react@16/umd/react.production.min.js" > </script > <script crossorigin src ="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" > </script >
JSX 语法
JSX 是一个具有JavaScript 编程特性 的类 HTML
标签 语言,目前TypeScript 和Vue2 都已经对 JSX
语法提供了良好的支持,广泛的应用于现代化前端应用开发当中。
向 JSX 中嵌入表达式
开发人员可以通过花括号语法{}
嵌入 JavaScript
表达式(例如2+2
、user.name
、auth(user)
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function auth (user ) { return user.name + " " + user.password ; } const user = { name : "hank" , password : "12345" }; const element = ( <h1 > User Info: {auth(user)}. </h1 > ); ReactDOM .render (element, document .getElementById ("app" ));
通过使用圆括号语法()
,可以方便的书写多行 JSX。
JSX 本身也是一种表达式
编译之后,JSX 表达式会被转换为标准的 JavaScript
对象 ,这意味可以在 JSX
内使用if
和for
等 JavaScript
语句、指定一个变量、接收其作为参数、甚至是从一个函数当中返回它们。
1 2 3 4 5 6 7 8 9 10 11 function getUserInfo (user ) { if (user) { return ( <h1 > User Info: {auth(user)} </h1 > ); } return <h1 > No Info.</h1 > ; }
指定 JSX 的属性
可以使用引号"
指定一个字符串字面量作为 JSX 的属性。
1 2 const element1 = <div className ="dashboard" /> ;const element2 = <div tabIndex ="0" /> ;
也可以使用花括号表达式{}
嵌入 JavaScript 表达式到 JSX
属性当中,此时无需在花括号外使用引号。
1 const element = <img src ={user.avatarUrl} /> ;
相比于静态的 HTML,由于 JSX 编程性上更加接近于 JavaScript,React DOM
使用驼峰风格(camelCase )的属性名称来代替原生 HTML
属性风格,例如:HTML 中的class
和tabindex
变为
JSX 中的className
以及tabIndex
。
使用 JSX 指定子元素
如果 JSX 标签内容为空,可以使用/>
直接进行关闭。
1 const element = <img src ={user.avatarUrl} /> ;
当然,JSX 标签可能也会包含子元素 ,如同下面这样:
1 2 3 4 5 6 const element = ( <div > <h1 > Hello!</h1 > <h2 > React 16.6.0!</h2 > </div > );
JSX 可以预防脚本注入攻击
React DOM 默认会在 JSX
渲染之前,避免任何值嵌入。因此可以确保不会被注入显式编写在应用之外的内容。为了避免
XSS 跨站脚本攻击,任何内容在渲染之前都会被转换为字符串。
1 2 3 const text = response.dangerInput ;const element = <h1 > {text}</h1 > ;
JSX 最终会被转换为对象
Babel 会将 JSX
编译为一个React.createElement()
函数调用,因此下面代码中的element1
与element2
是等效的。
1 const element1 = <h1 className ="demo" > 你好, React 16.6.0!</h1 > ;
1 const element2 = React .createElement ("h1" , { className : "demo" }, "你好, React 16.6.0!" );
虽然React.createElement()
会执行各类检查帮助你编写准确无误的代码,但是本质上其建立的对象是下面的样子:
1 2 3 4 5 6 7 8 const element = { type : "h1" , props : { className : "demo" , children : "你好, React 16.6.0!" } };
这些对象被称为React elements ,React
读取这些对象并通过它们去构建 DOM,并负责维护其状态,其名称乃至于功能都与
Vue2 模板编译所使用的createElement()
函数一致。
元素 Elements
元素 Elements 是 React
应用的最小组成部份,不同于浏览器的 DOM 元素,React
元素是一个非常易于建立的普通对象。React
的组件(Components )和元素(Elements )是非常容易混淆的两个概念,事实上React
的组件是由元素所组成的,元素是 React 的 JSX
模板的最小组成部分 。
渲染一个 React 元素到 DOM
通过ReactDOM.render()
方法渲染一个 React 元素到 HTML 的
DOM 根结点。
1 2 3 const element = <h1 > Hello, React 16.6.0 !</h1 > ;ReactDOM .render (element, document .getElementById ("app" ));
更新已经被渲染的 React 元素
React
元素是不可变的,建立后就不能修改其属性 以及子元素 。React
元素就像电影中的一个关键帧,总是在确切的时间点展现 UI。
更新 UI 总是需要去建立一个新的 React
元素,然后再通过ReactDOM.render()
渲染出来。例如下面代码,每间隔
1
秒钟使用setInterval()
回调函数执行ReactDOM.render()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function timer ( ) { const element = ( <div > <h1 > 你好, React 16.6.0 !</h1 > <h2 > 现在时间是 {new Date().toLocaleTimeString()}。 </h2 > </div > ); ReactDOM .render (element, document .getElementById ("app" )); } setInterval (timer, 1000 );
React 元素是按需更新的
React DOM 会比较当前 React 元素与其之前的状态,然后只对发生变化的 DOM
局部执行更新操作。
Components 组件
组件 Components 可以将 UI
拆分为独立且可复用的片断,React
组件接收props
作为输入参数,并在最后返回 React 元素。
函数式组件与类组件
定义 React 组件最简单的方法是通过 JavaScript 函数。
1 2 3 4 function Welcome (props ) { return <h1 > 你好, {props.name}!</h1 > ; }
当然也可以通过 ES6
的class
关键字定义一个等效组件,从而获取更多的有趣特性。
1 2 3 4 5 class Welcome extends React.Component { render ( ) { return <h1 > 你好, {this.props.name}!</h1 > ; } }
渲染一个组件
首先定义一个 React 组件,然后将组件赋值给一个 React
元素,最后再使用ReactDOM.render()
方法渲染该 React
元素到页面上。
1 2 3 4 5 6 7 8 9 10 function Welcome (props ) { return <h1 > 你好, {props.name}!</h1 > ; } const element = <Welcome name ="Hank" /> ;ReactDOM .render (element, document .getElementById ("app" ));
React 组件的名称通常约定为大写 格式。
组合使用多个组件
我们可以在一个组件返回的 JSX 当中组合引用其它的组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Welcome (props ) { return <h1 > 你好, {props.name}!</h1 > ; } function App ( ) { return ( <div > <Welcome name ="Hank" /> <Welcome name ="Jack" /> <Welcome name ="Candy" /> </div > ); } ReactDOM .render (<App /> , document .getElementById ("app" ));
组件的抽取
可以将一个较大的组件分割为更加细粒度的组件,便于复用与维护,例如下面这个函数式组件Comment
:
1 2 3 4 5 6 7 8 9 10 11 12 function Comment (props ) { return ( <div className ="Comment" > <div className ="UserInfo" > <img className ="Avatar" src ={props.author.avatarUrl} alt ={props.author.name} /> <div className ="UserInfo-name" > {props.author.name}</div > </div > <div className ="Comment-text" > {props.text}</div > <div className ="Comment-date" > {formatDate(props.date)}</div > </div > ); }
可以将其拆分为Avatar
,UserInfo
,Comment
三个具有包含关系的组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function Avatar (props ) { return <img className ="Avatar" src ={props.user.avatarUrl} alt ={props.user.name} /> ; } function UserInfo (props ) { return ( <div className ="UserInfo" > <Avatar user ={props.user} /> <div className ="UserInfo-name" > {props.user.name}</div > </div > ); } 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 > ); }
Props 是只读的
无论是以函数式还是class
类的方式声明组件,都不能对它们的props
进行修改。
1 2 3 4 5 6 7 function pure (firstname, lastname ) { return firstname + lastname; } function impure (firstname, lastname ) { firstname = "nothing" ; }
重要原则:组件外部只能通过 props
改变组件本身的行为。
State 状态
首先,我们来改写之前定时器timer
的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 function timer ( ) { const element = ( <div > <h1 > 你好, React 16.6.0 !</h1 > <h2 > 现在时间是 {new Date().toLocaleTimeString()}。 </h2 > </div > ); ReactDOM .render (element, document .getElementById ("app" )); } setInterval (timer, 1000 );
然后,将 JSX
元素element
从timer()
函数中提取出来,并抽象为一个
JSX
组件Clock
,然后通过props
向组件传递当前date
参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function Clock (props ) { return ( <div > <h1 > Hello, world!</h1 > <h2 > It is {props.date.toLocaleTimeString()}.</h2 > </div > ); } function timer ( ) { ReactDOM .render (<Clock date ={new Date ()} /> , document .getElementById ("app" )); } setInterval (timer, 1000 );
但是,我们希望Clock
组件的更新总是来自于其内部状态,而非通过组件外部的props
进行传入,如同下面代码这样:
1 ReactDOM .render (<Clock /> , document .getElementById ("app" ));
这就引出了 React
组件当中的另一个重要概念state ,state
与props
非常类似,但是其属于组件私有,只能由组件自身进行控制 。另外,前面章节有提到类组件具有比函数式组件更丰富的特性 ,而state
就是这些特性当中的一个,因为它只能由类组件进行使用。
将函数式组件转换为类组件
首先,需要建立一个继承自React.Component
ES6 的 Class
类,并添加一个render()
方法;然后将组件内容移动至该方法当中,并将函数式组件中传入的props
参数,修改为通过this.props
进行引用。
1 2 3 4 5 6 7 8 9 10 class Clock extends React.Component { render ( ) { return ( <div > <h1 > Hello, world!</h1 > <h2 > It is {this.props.date.toLocaleTimeString()}.</h2 > </div > ); } }
向类组件添加 state
首先,将render()
函数中的this.props.date
替换为this.state.date
。
1 2 3 4 5 6 7 8 9 10 class Clock extends React.Component { render ( ) { return ( <div > <h1 > Hello, world!</h1 > <h2 > It is {this.state.date.toLocaleTimeString()}.</h2 > </div > ); } }
然后,定义一个constructor()
构造方法去初始化this.state
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 > ); } }
最后,从<Clock />
元素移除作为props
的date
属性。
1 ReactDOM .render (<Clock /> , document .getElementById ("app" ));
最终结果看起来是下面这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ("app" ));
接下来,我们需要利用 React
组件提供的生命周期方法 ,每间隔 1
秒对当前显示的时间进行更新。
生命周期钩子
组件式前端框架通常都会拥有自己特定的生命周期函数,从过去的Backbone
、Ember
到更为现代化的Vue2
、Angular6
皆是如此,同样作为组件式框架的React也不例化。总体上,React的生命周期可以分为如下四个阶段:
装载 (Mounting ),组件实例被创建并插入 DOM
时会按下面顺序调用方法:
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
更新 (Updating ),修改props
或state
时会触发组件的更新,此时会依照如下顺序调用方法:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
卸载 (Unmounting ),当组件从 DOM
中被删除时调用该方法:
错误处理 (Error
Handling ),在生命周期方法、子组件构造函数、组件渲染过程中出现错误时会调用下列方法:
static getDerivedStateFromError()
componentDidCatch()
lifecycle
多组件应用程序开发当中,非常重要的一点在于:在组件被销毁的时候去释放组件占用的资源 。即当组件被渲染至
DOM 的时候,需要初始化Clock
组件中的定时器,React
生命周期中称为mounting挂载
;然后在组件被销毁时,清除组件定时器所占用的资源,React
生命周期中称为unmounting卸载
。下面详细讲解一下React
中提供的两个比较重要的生命周期钩子(lifecycle
hooks ) :componentDidMount()
和componentWillUnmount()
。
componentDidMount()钩子
React 组件被渲染到 HTML DOM
后被执行,可以用来初始化之前例子中的定时器。
1 2 3 4 5 6 7 componentDidMount ( ) { this .timerID = setInterval ( () => this .tick (), 1000 ); }
componentWillUnmount()钩子
React 组件从 HTML DOM
卸载之前得到执行,可以用来销毁定义在组件this
上的定时器。
1 2 3 componentWillUnmount ( ) { clearInterval (this .timerID ); }
抽取 timer()函数
timer()
函数会通过this.setState()
定时更新组件本身的state
,从而动态展示当前时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Clock extends React.Component { constructor (props ) { super (props); this .state = { date : new Date () }; } componentDidMount ( ) { this .timerID = setInterval (() => this .timer (), 1000 ); } componentWillUnmount ( ) { clearInterval (this .timerID ); } timer ( ) { 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 ("app" ));
深入 State
除了在类组件的构造函数之外,不允许直接修改state
,而必须通过组件提供的setState()
方法执行修改操作。
1 2 3 4 5 this .state .comment = "你好" ;this .setState ({ comment : "你好" });
React
中this.props
和this.state
的更新都是异步 的,当在同一个组件中多次应用时,不能依赖它们去计算下一个状态(可能会造成错误 ),例如下面代码可能会错误的更新计数器:
1 2 3 this .setState ({ counter : this .state .counter + this .props .increment });
要修复这个问题,setState()
可以接收一个函数作为参数,该函数第
1 个参数是之前的state
,第 2
个参数是props
。
1 2 3 this .setState ((prevState, props ) => ({ counter : prevState.counter + props.increment }));
通过setState()
对 state 的更新操作都会合并到当前的 State
状态,例如下面代码的state
中可以包含多个独立值:
1 2 3 4 5 6 7 constructor (props ) { super (props); this .state = { users : [], groups : [] }; }
然后可以在组件里,分别使用setState()
对这些值进行单独更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 componentDidMount ( ) { fetchUser ().then (response => { this .setState ({ users : response.users }); }); fetchGroup ().then (response => { this .setState ({ groups : response.groups }); }); }
单向数据流
React
当中的state
通常被认为是局部的 或者封装的 ,除了拥有并设置它的组件之外,其它任何组件都不能对其进行访问。因此,父子组件之间,可以通过将state
赋值给props
的方式,将父组件的内部状态传递给子组件。
1 2 3 4 5 <FormattedDate date={this .state .date } />; function FormattedDate (props ) { return <h2 > It is {props.date.toLocaleTimeString()}.</h2 > ; }
上面代码当中的FormattedDate
组件通过其 props
接收了父组件传递过来的状态this.state.date
。因此,可以认为
React
组件之间的数据流向是从父组件至子组件 ,即一个由上至下 的关系,通常被称为单向数据流 ,
可以将一个组件树中的props
想象成一个瀑布流,每个单独组件的state
如同一个个的独立水源,在任意时间节点加入到瀑布流当中,然后共同向下流动。
下面代码中,在一个App
组件内部渲染了多个Clock
组件,每个组件的时间都会独立进行更新,互相不受影响。
1 2 3 4 5 6 7 8 9 10 11 function App ( ) { return ( <div > <Clock /> <Clock /> <Clock /> </div > ); } ReactDOM .render (<App /> , document .getElementById ("app" ));
事件机制
React 事件机制与原生 JavaScript 事件机制语法上有以下不同:
React 事件名称使用驼峰命名 camelCase 。
JSX 可以直接使用函数作为事件处理器。
1 2 3 4 5 6 7 8 9 <button onclick="activateLasers()" > Activate Lasers </button> <button onClick ={activateLasers} > Activate Lasers </button >
(3)React
不能通过返回false
阻止事件默认行为,而必须显式调用preventDefault()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <a href="#" onclick="console.log('这个链接已经被点击!'); return false" > Click me </a>; function ActionLink ( ) { function handleClick (e ) { e.preventDefault (); console .log ("这个链接已经被点击!" ); } return ( <a href ="#" onClick ={handleClick} > 点击目标 </a > ); }
上面代码中,传入事件处理函数handleClick(e)
的参数e
是
React 遵循W3C UI
Events
事件规范 实现的合成事件 ,因此毋需担心跨浏览器兼容性问题。
使用 ES6 的 class
定义一个类组件时,通用的做法是以类方法 的形式定义事件处理函数,例如下面代码定义了一个可以点击切换【打开】和【关闭】状态的按钮。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Toggle extends React.Component { constructor (props ) { super (props); this .state = { isToggleOn : true }; this .handleClick = this .handleClick .bind (this ); } handleClick ( ) { this .setState (prevState => ({ isToggleOn : !prevState.isToggleOn })); } render ( ) { return <button onClick ={this.handleClick} > {this.state.isToggleOn ? "打开" : "关闭"}</button > ; } } ReactDOM .render (<Toggle /> , document .getElementById ("app" ));
大家注意理解上面类组件constructor()
构造函数当中,this.handleClick.bind(this)
的含义。使用bind()
是为了将类组件的this
作用域绑定至指定函数,从而方便的在该函数内部使用this
操作
React 类组件上的其它方法。
绑定组件 this 到事件处理函数
当然,如果你认为bind()
使用起来又臭又长,这里有 2
种方式可以绕开它:
(1)通过实验性的类属性转换语法(Class properties
transform )正确绑定this
到事件回调函数当中,不过需要额外安装babel-plugin-transform-class-properties
插件支持。
1 2 3 4 5 6 7 8 9 10 class Button extends React.Component { handleClick = () => { console .log ("这是:" , this ); }; render ( ) { return <button onClick ={this.handleClick} > 点击我</button > ; } }
(2)或者通过箭头函数的方式直接调用事件处理函数。
1 2 3 4 5 6 7 8 9 10 11 12 class Button extends React.Component { handleClick ( ) { console .log ("这是:" , this ); } render ( ) { return ( <button onClick ={e => this.handleClick(e)}>点击我</button > ); } }
这种方式的缺点在于每次不同的事件回调函数被建立时,都会触发 React
组件的重绘(比如下面的
Button ),如果此时事件回调传递props
到子级组件,则这些组件全部都会发生重绘,从而对页面性能造成影响。因此,React
官方更加推荐通过组件构造器调用bind()
和类属性语法 这两种方式。
向事件处理函数传递参数
通常情况下,我们都需要传递参数到事件处理函数,例如传递每一行的id
,下面使用的arrow functions
和Function.prototype.bind
两种写法都是等效的。
1 2 <button onClick={(e ) => this .deleteRow (id, e)}>删除行</button> <button onClick ={this.deleteRow.bind(this, id )}> 删除行</button >
第 1 个参数e
表示的是 React 事件对象,紧随其后的第 2
个参数即用来表示id
。
条件渲染
React 中的条件渲染类似于 JavaScript
中的条件运算,可以通过if
等条件运算符去动态展示元素、组件的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function User (props ) { return <h1 > 欢迎回来!</h1 > ; } function Guest (props ) { return <h1 > 请登录!</h1 > ; } function Greeting (props ) { const isLogged = props.isLogged ; if (isLogged) { return <User /> ; } return <Guest /> ; } ReactDOM .render ( <Greeting isLogged ={false} /> , document .getElementById ("app" ) );
元素变量
可以将 React
元素赋值给一个变量,这样可以方便的在组件内部进行条件渲染。下面例子中的<LoginControl />
组件会根据自身的状态,有条件的渲染<LoginButton />
或<LogoutButton />
以及之前例子中的<Greeting />
组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 function LoginButton (props ) { return <button onClick ={props.onClick} > 登入 </button > ; } function LogoutButton (props ) { return <button onClick ={props.onClick} > 登出 </button > ; } class LoginControl extends React.Component { constructor (props ) { super (props); this .handleLoginClick = this .handleLoginClick .bind (this ); this .handleLogoutClick = this .handleLogoutClick .bind (this ); this .state = { isLogged : false }; } handleLoginClick ( ) { this .setState ({ isLogged : true }); } handleLogoutClick ( ) { this .setState ({ isLogged : false }); } render ( ) { let button = null ; const isLogged = this .state .isLogged ; if (isLogged) { button = <LogoutButton onClick ={this.handleLogoutClick} /> ; } else { button = <LoginButton onClick ={this.handleLoginClick} /> ; } return ( <div > <Greeting isLogged ={isLogged} /> {button} </div > ); } } ReactDOM .render (<LoginControl /> , document .getElementById ("app" ));
声明一个变量和使用if
关键字是进行条件渲染非常好的方式,但有些时候可能需要使用到更简短的语法,接下来介绍几种行内的条件渲染方式:
内联条件渲染-&&运算符
将 JSX
表达式嵌入到一个花括号{}
运算符中(表达式中包含了
JavaScript 逻辑和&&
操作符 ),可以方便的将
React 元素包含到条件渲染判断语句当中。
1 2 3 4 5 6 7 8 9 10 11 12 function Mailbox (props ) { const unreadMessages = props.unreadMessages ; return ( <div > <h1 > Hello!</h1 > {unreadMessages.length > 0 && <h2 > You have {unreadMessages.length} unread messages.</h2 > } </div > ); } const messages = ["React" , "Re: React" , "Re:Re: React" ];ReactDOM .render (<Mailbox unreadMessages ={messages} /> , document .getElementById ("app" ));
JavaScript
当中true && 表达式
的结果总是表达式
,而false && 表达式
的结果总是false
。换而言之,如果条件判断结果为true
,则&&
运算符右侧的
React 元素将会出现在输出当中,如果为false
则 React
会自动跳过不进行任何渲染。
内联条件渲染-三目运算符
另外一种使用内联条件渲染的方式是通过三目运算符condition ? true : false
,下面例子中使用它对一小块文本进行了条件渲染。
1 2 3 4 5 6 7 8 render ( ) { const isLogged = this .state .isLogged ; return ( <div > 用户 <b > {isLogged ? '已经' : '没有'}</b > 登录. </div > ); }
三目运算符也可以用于进行多行的条件渲染:
1 2 3 4 5 6 7 8 9 10 11 12 render ( ) { const isLogged = this .state .isLogged ; return ( <div > {isLogged ? ( <LogoutButton onClick ={this.handleLogoutClick} /> ) : ( <LoginButton onClick ={this.handleLoginClick} /> )} </div > ); }
如同 JavaScript
一样,条件渲染的使用完全依照开发团队的习惯和实际工作的需求,但是无论如何都不要书写过于复杂的条件渲染语句,否则可以考虑将条件渲染过程抽象为一个具体的组件。
阻止组件的渲染
极少的情况下,开发人员需要将组件隐藏起来,即便它已经被其它组件渲染出来,如果需要这样做,可以让组件render()
函数返回null
而非
JSX 的内容。
下面的例子中,组件<WarningBanner />
的渲染依赖于一个名为warn
的
props 值,如果其值为false
则该组件不会渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function WarningBanner (props ) { if (!props.warn ) { return null ; } return <div className ="warning" > 警告信息!</div > ; } class Page extends React.Component { constructor (props ) { super (props); this .state = { showWarning : true }; this .handleToggleClick = this .handleToggleClick .bind (this ); } handleToggleClick ( ) { this .setState (prevState => ({ showWarning : !prevState.showWarning })); } render ( ) { return ( <div > <WarningBanner warn ={this.state.showWarning} /> <button onClick ={this.handleToggleClick} > {this.state.showWarning ? "隐藏" : "显示"}</button > </div > ); } } ReactDOM .render (<Page /> , document .getElementById ("app" ));
React
组件的render()
方法返回null
值并不会影响组件生命周期钩子函数 的触发,诸如componentWillUpdate()
和componentDidUpdate()
依然会被正常调用。
List 和 Key
通常情况下,在 JavaScript
当中我们会像下面代码这样循环一个数组列表。
1 2 3 const numbers = [1 , 2 , 3 , 4 , 5 ];const doubled = numbers.map (number => number * 2 );console .log (doubled);
React
当中循环一个组件列表的方式与上面非常相似,下面代码会渲染出一个从 1 至 5
编号的无序列表。
1 2 3 4 const numbers = [1 , 2 , 3 , 4 , 5 ];const listItems = numbers.map (number => <li > {number}</li > );ReactDOM .render (<ul > {listItems}</ul > , document .getElementById ("app" ));
列表组件
接下来,我们将上面例子中的列表循环封装到一个组件当中去,该组件将会接收一个numbers
数组作为props
。
1 2 3 4 5 6 7 8 function NumberList (props ) { const numbers = props.numbers ; const listItems = numbers.map (number => <li > {number}</li > ); return <ul > {listItems}</ul > ; } const numbers = [1 , 2 , 3 , 4 , 5 ];ReactDOM .render (<NumberList numbers ={numbers} /> , document .getElementById ("app" ));
但是,当你执行这段代码时,会得到这个警告信息:Warning: Each child in an array or iterator should have a unique "key" prop.
。这里,通过添加key={number.toString()}
可以修复该问题。
1 2 3 4 5 6 7 8 function NumberList (props ) { const numbers = props.numbers ; const listItems = numbers.map (number => ( <li key ={number.toString()} > {number}</li > )); return <ul > {listItems}</ul > ; }
列表循环的 key
key
属性用来帮助 React
鉴别具体哪一项内容发生了变化,可以给列表循环当中的每个具体项一个确切的、稳定的标识。
1 2 const numbers = [1 , 2 , 3 , 4 , 5 ];const listItems = numbers.map (number => <li key ={number.toString()} > {number}</li > );
最佳实践是使用字符串类型的键值来作为列表循环当中每项的唯一标识,通常情况下可以使用列表的id
值来作为key
。
1 const todoItems = todos.map (todo => <li key ={todo.id} > {todo.text}</li > );
如果没有稳定的id
值,可以考虑使用循环列表每项的索引值index
作为key
。
1 const todoItems = todos.map ((todo, index ) => <li key ={index} > {todo.text}</li > );
在列表项顺序可能发生变化的场景下,React
官方并不推荐使用索引作为key
,因为会带来性能方面的负面影响,并引发组件状态的问题。
key 的使用位置
属性key
只作用于数组循环上下文的内部,通常情况下是在 ES6
提供的map()
遍历方法内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ("app" ));
key 必须唯一
key
值必须保持在数组循环作用域范围内的唯一,而非全局上下文范围内的唯一,因此在不同的数组循环内使用相同key
值是被允许的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function Blog (props ) { const sidebar = ( <ul > {props.posts.map(post => ( <li key ={post.id} > {post.title} </li > ))} </ul > ); const content = props.posts .map (post => ( <div key ={post.id} > <h3 > {post.title}</h3 > <p > {post.content}</p > </div > )); return ( <div > {sidebar} <hr /> {content} </div > ); } const posts = [{ id : 1 , title : "你好" , content : "欢迎使用React 16.6.0!" }, { id : 2 , title : "安装方式" , content : "可以通过npm和yarn安装React" }];ReactDOM .render (<Blog posts ={posts} /> , document .getElementById ("app" ));
key
仅仅只是作为 React 内部的标记,并不会被渲染到组件和
DOM
当中,如果在组件内部需要使用到key
的属性值,可以考虑也同时将其传递给组件的props
。
1 2 3 4 const content = posts.map (post => ( <Post key ={post.id} id ={post.id} title ={post.title} /> ));
嵌入 map()至 JSX
JSX
允许通过花括号{}
嵌入任意表达式,因此可以将map()
嵌入至
JSX 行内使用。
1 2 3 4 5 6 7 8 9 10 function NumberList (props ) { const numbers = props.numbers ; return ( <ul > {numbers.map(number => ( <ListItem key ={number.toString()} value ={number} /> ))} </ul > ); }
某些情况下,这样的内联风格可以得到更加整洁的代码,但如果滥用也可能会影响代码的可读性,因此需要根据实际场景权衡后再使用。但是,仍然需要注意的一点:如果map()
循环体的嵌套过深,可以考虑将其抽象为组件 。
React 表单
HTML 表单与 React 表单的工作方式有些不同,因为 React
需要去保持一些内部状态。例如,下面的 HTML 表单将会接收一个 name
字段:
1 2 3 4 5 6 <form > <label > 名称:<input type ="text" name ="name" /> </label > <input type ="submit" value ="Submit" /> </form >
HTML 表单在用户点击提交请求之后会跳转到一个新的页面,React
当中虽然能够完成同样的工作,但是通常情况会使用一个 JavaScript
事件处理函数去操控表单提交行为,从而获取和控制用户在表单当中的输入行为,这种标准方式在
React 当中被称为受控组件 。
受控组件
HTML
表单元素<input>
、<textarea>
、<select>
会根据用户输入维护自己的状态,React
当中这些变化的状态会由组件的state
来维护,并只能使用setState()
进行更新。接下来,我们融合
HTML 原生表单和 React 组件state
的行为,让 React
组件在渲染表单元素的同时,也能够控制其输入状态。这种输入状态受到 React
控制的 HTML 表单就被称为受控组件(Controlled
Components ) 。
下面的代码,将会使用受控组件来重写本章开头的 HTML 表单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class InputForm extends React.Component { constructor (props ) { super (props); this .state = { value : "" }; this .handleChange = this .handleChange .bind (this ); this .handleSubmit = this .handleSubmit .bind (this ); } handleChange (event ) { this .setState ({ value : event.target .value .toUpperCase () }); } 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 ="Submit" /> </form > ); } }
上面例子中,当value
属性设置到表单元素时,其值总是this.state.value
的值,从而让
React
的state
成为表单输入的内容的单一来源 。伴随每次用户的输入handleChange
都会通过this.setState()
对this.state.value
进行更新,从而完成
HTML 表单到 React 状态的双向绑定 。
受控组件中的每个状态变化都会关联对应的事件处理函数。
textarea 标签
HTML
的<textarea>
标签,通过标签内部的字符串来定义其文本内容,如同下面这样:
1 2 3 <textarea > Hello there, this is some text in a text area </textarea >
React
当中的<textarea>
依然会通过一个value
属性来代替标签内部的字符串,其用法和上面的<input>
标签相似。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class TextareaForm extends React.Component { constructor (props ) { super (props); this .state = { value : "这是一句默认显示在输入域中的内容。" }; 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 ="Submit" /> </form > ); } }
select 标签
HTML
中的<select>
用来建立一个下拉列表,下面列表描述了一系列汽车品牌,并且通过selected
属性默认选中了奔驰 。
1 2 3 4 5 6 <select > <option value ="benz" selected > 奔驰</option > <option value ="volkswagen" > 大众</option > <option value ="peugeot" > 标致</option > <option value ="renault" > 雷诺</option > </select >
React
中使用value
属性代替了上面列表中selected
默认选中的功能,因为只需要在一个位置进行更新,所以能够更加方便的使用受控组件 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class FavoriteCarForm extends React.Component { constructor (props ) { super (props); this .state = {value : "coconut" '}; 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> 选择你最喜欢的汽车: <select value={this.state.value} onChange={this.handleChange}> <option value="benz">奔驰</option> <option value="volkswagen">大众</option> <option value="peugeot">标致</option> <option value="renault">雷诺</option> </select> </label> <input type="submit" value="确定" /> </form> ); } }
你也可以传递一个数组到value
属性当中,从而能够在<select>
标签中选择多个属性。
1 <select multiple={true } value={['B' , 'C' ]}>
总体而言,React
当中<input type="text">
、<textarea>
、<select>
的工作方式都非常类似,他们都能够接收一个value
属性。
操作多个输入域
当需要操作多个输入域的时候,你可以为这些输入域添加name
属性,然后通过事件处理函数参数所提供的event.target.name
来判断各自的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Reservation extends React.Component { constructor (props ) { super (props); this .state = { isGoing : true , numberOfGuests : 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 > Is going: <input name ="isGoing" checked ={this.state.isGoing} onChange ={this.handleInputChange} type ="checkbox" /> </label > <br /> <label > Number of guests: <input name ="numberOfGuests" value ={this.state.numberOfGuests} onChange ={this.handleInputChange} type ="number" /> </label > </form > ); } }
设置属性值为 null
将输入域控件的value
属性设置为undefined
或者null
,可以控制其编辑状态。
1 2 3 4 5 6 7 const mountedNode = document .getElementById ("app" );ReactDOM .render (<input value ="你好" /> , mountedNode);setTimeout (function ( ) { ReactDOM .render (<input value ={null} /> , mountedNode); }, 5000 );
非受控组件
通常情况下,使用受控组件是比较冗长乏味的,因为需要编写大量事件函数去处理状态的变化,并将结果传递给
React 组件进行展示,这对于旧系统向 React
的技术迁移极不友好。这种场景下,其实可以考虑使用非受控组件 (uncontrolled
components ),这是一种处理表单输入的替代技术,后面的章节将会对其进行说明。
状态提升
当多个组件需要反映相同的状态数据时,通常建议将状态提升到这些组件的共同父级组件当中。下面,通过一个沸腾水温计算器的例子来进行说明。
首先,我们建立一个BoilingVerdict
组件,该组件接收一个摄氏温度作为
props,并打印出超过 100 度的沸腾水温。
1 2 3 4 5 6 function BoilingVerdict (props ) { if (props.celsius >= 100 ) { return <p > 水将会沸腾!</p > ; } return <p > 水不会沸腾!</p > ; }
然后,再建立一个Calculator
组件,用来输入温度并将其状态保持在this.state.temperature
当中,并将这个输入值渲染到至BoilingVerdict
组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ( <fieldset > <legend > 请输入摄氏温度:</legend > <input value ={temperature} onChange ={this.handleChange} /> <BoilingVerdict celsius ={parseFloat(temperature)} /> </fieldset > ); } }
添加第 2 个输入域
接下来,需要再添加一个输入域来输入华氏温度,并保持它们的状态同步。
首先,从Calculator
组件抽象一个TemperatureInput
组件,并增加一个名称为scale
的
props(值为 c 或者 f ),
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const scaleNames = { c : "摄氏" , f : "华氏" }; class TemperatureInput 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 ; const scale = this .props .scale ; return ( <fieldset > <legend > 请输入 {scaleNames[scale]} 温度: </legend > <input value ={temperature} onChange ={this.handleChange} /> </fieldset > ); } }
然后,修改一下Calculator
组件,使其能够分别渲染scale
为c
或f
的两个TemperatureInput
组件。
1 2 3 4 5 6 7 8 9 10 class Calculator extends React.Component { render ( ) { return ( <div > <TemperatureInput scale ="c" /> <TemperatureInput scale ="f" /> </div > ); } }
进行到这一步,我们已经拥有两个输入域,但是输入其中的一个,并不会导致另一个同步更新,这并不符合本章节开头的需求。而且由于温度状态位于TemperatureInput
组件内部,Calculator
组件无法直接对其进行显示。
编写转换函数
我们的例子中,还需要两个对摄氏/华氏温度进行相互转换的函数。
1 2 3 4 5 6 7 function toCelsius (fahrenheit ) { return ((fahrenheit - 32 ) * 5 ) / 9 ; } function toFahrenheit (celsius ) { return (celsius * 9 ) / 5 + 32 ; }
以及一个对输入温度进行合法性校验的函数,不合法时返回空字符串,合法则返回值精确到小数点第
3 位。
1 2 3 4 5 6 7 8 9 10 11 12 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);
根据上面改造之后,TemperatureInput
组件已经可以独立的保持输入值在各自的state
当中。但是我们希望保持两个输入域的同步,比如输入华氏温度的时候,摄氏温度会自动展示被转换后的温度值。
完整 Demo
React
当中,多个组件之间state
的共享,需要将这些state
放置到共同的父级组件,这种方式被称为state
状态提升 。这个例子中,我们需要将TemperatureInput
组件里需要共享的state
移动到Calculator
组件内,然后通过TemperatureInput
组件上的props
属性向下分发这些共享数据,最终实现两个TemperatureInput
组件内的输入值的同步更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class TemperatureInput extends React.Component { constructor (props ) { super (props); this .handleChange = this .handleChange .bind (this ); } handleChange (e ) { this .props .onTemperatureChange (e.target .value ); } render ( ) { const temperature = this .props .temperature ; const scale = this .props .scale ; return ( <fieldset > <legend > Enter temperature in {scaleNames[scale]}:</legend > // 当输入域的值发生变化时,触发本组件内的handleChange事件处理函数 <input value ={temperature} onChange ={this.handleChange} /> </fieldset > ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Calculator extends React.Component { constructor (props ) { super (props); this .handleCelsiusChange = this .handleCelsiusChange .bind (this ); this .handleFahrenheitChange = this .handleFahrenheitChange .bind (this ); this .state = { temperature : "" , scale : "c" }; } handleCelsiusChange (temperature ) { this .setState ({ scale : "c" , temperature }); } handleFahrenheitChange (temperature ) { this .setState ({ scale : "f" , temperature }); } render ( ) { const scale = this .state .scale ; const temperature = this .state .temperature ; const celsius = scale === "f" ? tryConvert (temperature, toCelsius) : temperature; const fahrenheit = scale === "c" ? tryConvert (temperature, toFahrenheit) : temperature; return ( <div > <TemperatureInput scale ="c" temperature ={celsius} onTemperatureChange ={this.handleCelsiusChange} /> <TemperatureInput scale ="f" temperature ={fahrenheit} onTemperatureChange ={this.handleFahrenheitChange} /> <BoilingVerdict celsius ={parseFloat(celsius)} /> </div > ); } }
通常情况下,更新state
将会触发组件的重绘,如果多个组件需要共享同一个state
,可以考虑将这些state
抬升到其共同的父组件当中,然后通过至上而下 的数据流来完成state
的同步更新。
相比于 Angular、Vue2 原生提供的双向绑定机制,React
当中state
的状态提升 涉及到书写更多的样板代码,但优点在于更加容易探测到一些潜在的
bug,以及在状态变化过程中切入一些处理逻辑,比如上面例子中体现的数字精度控制和输入数据类型校验。
事实上,Vue2
的响应式更新机制是属于组件级别的,而且已经取消了组件内部的state
属性,有效避免组件间state
互相污染的问题,因此
FB 认为这是优点的说法比较牵强,否则也不会在 Redux 之后有 MobX
的出现。
组合与继承
React
组件拥有强大的组合模型,我们推荐通过组合而非继承来完成组件的复用。
内容包含
默认情况下,许多组件并不了解其子元素的情况(比如侧边栏和对话框组件 ),这样的情况推荐使用props
的children
属性将组件内部嵌套的元素内容直接渲染至组件的输出当中 ,例如下面就定义了一个使用该属性的组件:
1 2 3 function Border (props ) { return <div className ={ "border- " + props.color }> {props.children}</div > ; }
接下来,使用 JSX 语法向这个组件内放入任意内容。
1 2 3 4 5 6 7 8 function Dialog ( ) { return ( <Border color ="blue" > <h1 className ="title" > 标题</h1 > <p className ="message" > 内容</p > </Border > ); }
最后添加一个额外的样式,为组件的渲染内容呈现一个蓝色的边框。
1 2 3 .blue-border { border : 10px solid blue; }
<border />
组件内的 JSX
元素内容最终会被渲染到组件内{props.children}
所在的位置,最后的结果看起来是下面这样的:
React 当中{props.children}
的作用类似于 Vue2
当中的<slot />
元素,本质都是为了将嵌入组件的内容,在组件渲染时以合适的方式进行展示。当在需要嵌入多段内容的情况下,Vue2
通过具名插槽<slot name="">
来解决这个问题,而 React
解决该问题的方式与 Vue2 类似。
1 2 3 4 5 6 7 8 9 10 11 12 function Box (props ) { return ( <div className ="box" > <div className ="left" > {props.left} </div > <div className ="right" > {props.right} </div > </div > ); } function App ( ) { return <Box left ={ <Contacts /> } right={<Chat /> } /> ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 .box { width : 100% ; height : 100% ; } .left { float : left; width : 30% ; height : 100% ; } .right { float : left; width : 70% ; height : 100% ; }
React
组件本质是一个对象 ,因此可以将其作为props
的属性值进行传递,上面代码的执行结果如下:
特殊化
某些场景下,需要对某个组件进行特殊化处理,比如将Dialog
组件具象成为一个WelcomeDialog
组件,通常情况大家会首先想到使用继承,但是
React
当中依然可以通过使用组合解决这个问题。即在WelcomeDialog
组件内渲染Dialog
组件,并通过props
属性配置Dialog
的行为。
1 2 3 4 5 6 7 8 9 10 11 12 function Dialog (props ) { return ( <Border color ="blue" > <h1 className ="title" > {props.title} </h1 > <p className ="message" > {props.message} </p > </Border > ); } function WelcomeDialog ( ) { return <Dialog title ="欢迎" message ="感谢访问!" /> ; }
Facebook 开发团队内部已经使用 React
实现了数以千计的组件,但是并未出现需要推荐使用继承结构的用例。通过搭配使用props
与组合
,可以灵活的定制各类组件。另外需要特别注意的是,React
组件可以接受任意类型的props
,包括原生的对象或者回调函数,甚至是一个
React 组件对象本身。
而对于非 UI 相关的功能性复用,建议分离到单独的 JavaScript
模块当中,以功能函数、对象或类的方式进行实现。
React 编程思想
React 特别适用于大规模的 JavaScript 应用程序,并且已经在 Facebook 和
Instagram 相关产品上进行了实践。React
最优秀的特性来自于其提出的组件化思想,即将 DOM 页面分片断进行开发,通过
DOM
片断进行业务逻辑和功能层面的复用。组件的拆分可以遵从设计模式中的单一职责原则(single
responsibility
principle ),即一个组件理想状态下只完成一件事情,下面是 React
官网提供的一个商品表格的示例:
组件嵌套结构
1 2 3 4 5 FilterableProductTable └── SearchBar └── ProductTable └── ProductCategoryRow └── ProductRow
组件功能说明
FilterableProductTable
:橙色 ,包含所有组件。
SearchBar
:蓝色 ,接收用户输入。
ProductTable
:绿色 ,基于用户输入显示和过滤数据集合。
ProductCategoryRow
:青色 ,显示分类的标题。
ProductRow
:红色 ,显示每款商品。
完整示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 class ProductCategoryRow extends React.Component { render ( ) { const category = this .props .category ; return ( <tr > <th colSpan ="2" > {category}</th > </tr > ); } } class ProductRow extends React.Component { render ( ) { const product = this .props .product ; const name = product.stocked ? product.name : <span style ={{ color: "red " }}> {product.name} </span > ; return ( <tr > <td > {name}</td > <td > {product.price}</td > </tr > ); } } class ProductTable extends React.Component { render ( ) { const filterText = this .props .filterText ; const inStockOnly = this .props .inStockOnly ; const rows = []; let lastCategory = null ; this .props .products .forEach (product => { if (product.name .indexOf (filterText) === -1 ) { return ; } if (inStockOnly && !product.stocked ) { return ; } if (product.category !== lastCategory) { rows.push (<ProductCategoryRow category ={product.category} key ={product.category} /> ); } rows.push (<ProductRow product ={product} key ={product.name} /> ); lastCategory = product.category ; }); return ( <table > <thead > <tr > <th > Name</th > <th > Price</th > </tr > </thead > <tbody > {rows}</tbody > </table > ); } } class SearchBar extends React.Component { constructor (props ) { super (props); this .handleFilterTextChange = this .handleFilterTextChange .bind (this ); this .handleInStockChange = this .handleInStockChange .bind (this ); } handleFilterTextChange (e ) { this .props .onFilterTextChange (e.target .value ); } handleInStockChange (e ) { this .props .onInStockChange (e.target .checked ); } render ( ) { return ( <form > <input type ="text" placeholder ="Search..." value ={this.props.filterText} onChange ={this.handleFilterTextChange} /> <p > <input type ="checkbox" checked ={this.props.inStockOnly} onChange ={this.handleInStockChange} /> Only show products in stock </p > </form > ); } } class FilterableProductTable extends React.Component { constructor (props ) { super (props); this .state = { filterText : "" , inStockOnly : false }; this .handleFilterTextChange = this .handleFilterTextChange .bind (this ); this .handleInStockChange = this .handleInStockChange .bind (this ); } handleFilterTextChange (filterText ) { this .setState ({ filterText : filterText }); } handleInStockChange (inStockOnly ) { this .setState ({ inStockOnly : inStockOnly }); } render ( ) { return ( <div > <SearchBar filterText ={this.state.filterText} inStockOnly ={this.state.inStockOnly} onFilterTextChange ={this.handleFilterTextChange} onInStockChange ={this.handleInStockChange} /> <ProductTable products ={this.props.products} filterText ={this.state.filterText} inStockOnly ={this.state.inStockOnly} /> </div > ); } } const PRODUCTS = [{ 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" }];ReactDOM .render (<FilterableProductTable products ={PRODUCTS} /> , document .getElementById ("app" ));
React 拥有 2
种不同类型的模型数据 (Model ):props
和state
。
深入 JSX
本质上而言,JSX
其实是React.createElement(component, props, ...children)
函数的语法糖。
1 2 3 4 5 6 7 8 9 10 11 <MyButton color="blue" shadowSize={2 }> 点击我 </MyButton > React .createElement ( MyButton , { color : 'blue' , shadowSize : 2 }, '点击我' )
1 2 3 4 5 6 7 8 9 <div className="sidebar" /> React .createElement ( 'div' , { className : 'sidebar' }, null )
指定 React 的元素类型
React
当中,可以将组件赋值给一个变量或者常量,如果代码中使用名为<Test>
的组件,则组件对应的Test
变量必须位于当前组件的作用域内。此外,定义组件时必须显式引入React
库,即使当前组件没有直接对其进行引用。
1 2 3 4 5 6 7 import React from "react" ; import CustomButton from "./CustomButton" ;function WarningButton ( ) { return <CustomButton color ="red" /> ; }
当一个模块需要export
多个 React
组件时,可以将这些组件定义为一个对象的属性之后导出,然后 JSX
内使用时通过.
操作符进行引用。
1 2 3 4 5 6 7 8 9 10 11 12 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" /> ; }
用户自定义组件的名称首字母必须大写 ,以便于在字面上与原生的<v>
或<span>
进行有效区分。
1 2 3 4 5 6 7 8 9 10 11 12 import React from "react" ;function Hello (props ) { return <div > Hello {props.toWhat}</div > ; } function HelloWorld ( ) { return <Hello toWhat ="World" /> ; }
不能以 React 元素的方式使用 JavaScript 表达式,例如下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 import React from 'react' ;import { PhotoStory , VideoStory } from './stories' ;const components = { photo : PhotoStory , video : VideoStory }; function Story (props ) { return <components[props.storyType] story={props.story} /> ; }
解决上面问题,需要将表达式赋值给一个首字母大写的变量,参见下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 import React from "react" ;import { PhotoStory , VideoStory } from "./stories" ;const components = { photo : PhotoStory , video : VideoStory }; function Story (props ) { const SpecificStory = components[props.storyType ]; return <SpecificStory story ={props.story} /> ; }
JSX 中的 props
以 JavaScript 表达式的方式
开发人员可以通过{}
传递任意 JavaScript
表达式到prpps
。
1 2 <MyComponent foo={1 + 2 + 3 + 4 } />
if
和for
语句并不属于 JavaScript
中的表达式,因此可以直接用于 JSX。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function contextSwitching (props ) { let name; if (props.context == "internet" ) { name = <i > Uinika</i > ; } else if (props.context === "reallife" ) { name = <i > Hank</i > ; } return ( <p > 在{props.context} 里我叫 {name} </p > ); }
字符串字面量
可以向 props 传递字符串字面量,下面的两个 JSX 是等效的。
1 2 3 <MyComponent message="Hello React16!" /> <MyComponent message ={ 'Hello React16 !'} />
传递的字符串变量可以是非 HTML 转义的,因此下面的两个 JSX
表达式仍然是等效的。
1 2 3 <MyComponent message="<5" /> <MyComponent message ={ '<5 '} />
props 默认为 true
如果没有向组件的 props 传递值(声明 props
但并未进行赋值 ),则该props
的值默认为true
,下面的两行代码因此是等效的:
1 2 3 <MyTextBox autocomplete /> <MyTextBox autocomplete ={true} />
通常情况并不建议缺省 props 的值,因为这样容易与 ES6
的对象快捷声明特性,语法上发生混淆。
props 对象扩展运算
如果你的props
是一个对象 ,可以考虑使用
ES6
的对象扩展运算符 ...
,将所有的props
一次性传入组件。
1 2 3 4 5 6 7 8 function Component1 ( ) { return <Hello firstName ="Hank" lastName ="Zen" /> ; } function Component2 ( ) { const props = { firstName : "Hank" , lastName : "Zen" }; return <Hello {...props } /> ; }
你还可以让组件使用特定的props
,然后通过对象扩展运算符传递其它所有props
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const Button = props => { const { kind, ...all } = props; const className = kind === "primary" ? "btn-primary" : "btn-default" ; return <button className ={className} {...all } /> ; }; const App = ( ) => { return ( <div > <Button kind ="primary" onClick ={() => console.log("被点击了!")}> Hello React 16.2! </Button > </div > ); };
上面例子中的{ kind, ...all }
只会获取 props
中的kind
属性,然后将props
中其它属性全部赋值给...all
,
对象扩展运算符是非常有用的工具,但是容易将一些不必要的props
传递给组件,因此建议酌情根据需要进行使用。
JSX 的 children
JSX 表达式开始、结束标签内的内容会以特殊的 props
形式传递:props.children
,React
有几种不同的方式去传递这些children
。
字符串字面量
在 JSX
开始和结束标签内直接书写字符串,props.children
的值就是这段字符串内容。字符串的内容可以是非
HTML 转义的,因此编写 JSX 就像编写 HTML 一样。
1 2 3 4 5 <MyComponent >Hello React 16 !</MyComponent > <div > Hank & Github.</div >
JSX
会自动移除开始和结束行的空格,标签附近的新的行也会被同时移除,标签内部内容当中出现的空格会被缩进为一个空格,所以下面
JSX 代码的渲染结果都相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <p>Hello React 16 !</p> <p > Hello React 16! </p > <p > Hello React 16! </p > <p > Hello React 16! </p >
嵌套的 JSX
JSX
开始结束标签内依然可以使用其它标签作为子元素,从而能够以嵌套的使用各类
React 组件和 HTML 元素。
1 2 3 4 <MyContainer > <MyFirstComponent /> <MySecondComponent /> </MyContainer >
React16
带来的一个重要新特性之一是:组件可以直接返回一个数组元素 。
1 2 3 4 5 6 7 8 9 render ( ) { return [ <li key ="A" > First item</li > , <li key ="B" > Second item</li > , <li key ="C" > Third item</li > , ]; }
JavaScript 表达式作为子元素
React 可以通过{}
运算符使用任意 JavaScript 表达式作为 JSX
子元素,例如下面两个表达式就是等效的:
1 2 3 <MyComponent >foo</MyComponent > <MyComponent > {'foo'}</MyComponent >
这在渲染任意长度的 JSX 表达式列表时非常有用,请参见下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 function Item (props ) { return <li > {props.message}</li > ; } function List ( ) { const todos = ['工作' , '生活' , '运动' ,'早睡早起' ]; return ( <ul > {todos.map((message) => <Item key ={message} message ={message} /> )} </ul > ); }
JavaScript
表达式可以与其它类型子元素混用,这在为模板绑定数据的时候非常有用。
1 2 3 function Hello (props ) { return <div > Hello {props.addressee}!</div > ; }
函数作为子元素
与props
属性一样,props.children
可以传递任意类型的数据,组件会在渲染前解析props.children
中的内容。例如,可以通过props.children
向一个自定义组件传递回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 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 > ; }
上面的方法日常开发中并不常用,但是在一些需要对 JSX
功能进行扩展的的场景下还是非常有用的。
boolean、null、undefined
会被忽略
boolean
、null
、undefined
都是合法的子元素,这些类型的内容不会被渲染,因此下面例子中的
JSX 会渲染相同的结果:
1 2 3 4 5 6 7 8 9 10 11 <div /> <div > </div > <div > {false}</div > <div > {null}</div > <div > {undefined}</div > <div > {true}</div >
这对于条件运算是非常有用的,下面的 JSX
当showHeader
为true
时只会渲染出一个<Header />
。
1 2 3 4 <div> {showHeader && <Header /> } <Content /> </div>
值得注意的是,数字0 (布尔运算中通常被判断为假值 )会被
React
原样渲染,例如当下面代码中的props.messages
是一个空数组的时候,数值0
将会被展示到页面上。
1 <div>{props.messages .length && <MessageList messages ={props.messages} /> }</div>
解决这个问题,需要显式的使用布尔运算符&&
,将上面的代码修改成下面这样:
1 <div>{props.messages .length > 0 && <MessageList messages ={props.messages} /> }</div>
与此相反,如果需要将false
、true
、null
、undefined
之类的值展示到页面,需要首先将这些值转换为字符串。
1 <div>My JavaScript variable is {String (myVariable)}.</div>
PropTypes 类型检查
从伴随应用程序规模的增长,需要进行大量的类型检查工作,因此 React
内建了组件props
类型检查机制。但是从 React v15.5
开始,React.PropTypes
被迁移到单独的prop-types
包。
1 2 3 4 5 6 7 8 9 10 11 12 import React from "react" ;import PropTypes from "prop-types" ;class Component extends React.Component { render ( ) { return <div > {this.props.text}</div > ; } } Component .propTypes = { text : PropTypes .string .isRequired };
PropTypes
对象上暴露了一系列校验器,用来确保当前组件接收的数据是合法的,例如上面代码中的PropTypes.string.isRequired
,当props
的值非法时,浏览器控制台将会接收到警告信息。
出于性能方面的考量,PropTypes
类型检查只工作在开发模式 下。
PropTypes
下面是 PropTypes 上各类校验器的使用实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import PropTypes from "prop-types" ;MyComponent .propTypes = { optionalArray : PropTypes .array , optionalBool : PropTypes .bool , optionalFunc : PropTypes .func , optionalNumber : PropTypes .number , optionalObject : PropTypes .object , optionalString : PropTypes .string , optionalSymbol : PropTypes .symbol , optionalNode : PropTypes .node , optionalElement : PropTypes .element , optionalMessage : PropTypes .instanceOf (Message ), optionalEnum : PropTypes .oneOf (["News" , "Photos" ]), optionalUnion : PropTypes .oneOfType ([PropTypes .string , PropTypes .number , PropTypes .instanceOf (Message )]), optionalArrayOf : PropTypes .arrayOf (PropTypes .number ), optionalObjectOf : PropTypes .objectOf (PropTypes .number ), optionalObjectWithShape : PropTypes .shape ({ color : PropTypes .string , fontSize : PropTypes .number }), requiredFunc : PropTypes .func .isRequired , requiredAny : PropTypes .any .isRequired , customProp : function (props, propName, componentName ) { if (!/matchme/ .test (props[propName])) { return new Error ("不合法的prop `" + propName + "` 被应用到" + " `" + componentName + "`. 校验失败." ); } }, customArrayProp : PropTypes .arrayOf (function (propValue, key, componentName, location, propFullName ) { if (!/matchme/ .test (propValue[key])) { return new Error ("不合法的prop `" + propFullName + "` 被应用到" + " `" + componentName + "`. 校验失败." ); } }) };
需要单一的子元素
通过PropTypes.element
可以指定当前组件只能拥有一个单一的子元素,否则将会出现告警信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 import PropTypes from "prop-types" ;class MyComponent extends React.Component { render ( ) { const children = this .props .children ; return <div > {children}</div > ; } } MyComponent .propTypes = { children : PropTypes .element .isRequired };
默认的 props 值
可以通过 React
组件的defaultProps
属性为props
指定默认值。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Demo extends React.Component { render ( ) { return <h1 > Hello, {this.props.name}</h1 > ; } } Demo .defaultProps = { name : "React!" }; ReactDOM .render (<Demo /> , document .getElementById ("app" ));
如果你使用了 Babel 的transform-class-properties 插件,就可以方便的通过
React 组件类的静态属性来声明默认值,这个语法在 ES6
规范中还没有稳定,因此需要在 Babel
进行编译后才能在浏览器中正常工作。
1 2 3 4 5 6 7 8 9 10 class Demo extends React.Component { static defaultProps = { name : "React!" }; render ( ) { return <div > Hello, {this.props.name}</div > ; } }
上面代码中的defaultProps
属性用来确保this.props.name
总是会拥有一个缺省值,propTypes
检查发生在defaultProps
属性被解析之后,因此类型检查机制依然可以应用到defaultProps
上面 。
开发环境下,还可以通过Flow 和TypeScript 进行静态的数据类型检查,可以方便的在代码运行之前检测到数据类型方面的问题。
Refs 和 DOM
React
组件数据流当中,父组件向下与子组件沟通的唯一方式是通过props
,传入新的props
值然后子组件被重新渲染。某些场景下(管理输入聚焦、文本选择、多媒体回放,触发命令式动画,整合第
3 方 DOM 类库。 ),需要在 React
组件数据流范围之外对子元素(即可能是 React 组件,也可能是 DOM
元素 )进行修改,为此 React
提供了ref
组件属性来满足这种需求。
添加关于 DOM 元素的 ref 属性
React
提供的ref
属性可以添加到任意组件,ref
属性接收一个回调函数,该函数会在组件mounted
或unmounted
后执行。
当ref
属性应用于 HTML
元素的时候,ref
回调函数会接收到该元素对应的 DOM
对象,例如下面的代码就通过ref
存储一个 DOM 结点的引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class CustomTextInput extends React.Component { constructor (props ) { super (props); this .focusTextInput = this .focusTextInput .bind (this ); } focusTextInput ( ) { this .textInput .focus (); } render ( ) { return ( <div > <input type ="text" ref ={input => { this.textInput = input; }} /> <input type ="button" value ="Focus the text input" onClick ={this.focusTextInput} /> </div > ); } }
React
会在组件挂载的时候调用ref
上的回调函数,然后在组件卸载时将该ref
赋值为null
;因此,ref
上的回调函数先于componentDidMount
或componentDidUpdate
生命周期函数执行 。
通过ref
回调来设置类上的某个属性是 React 操作局部 DOM
的常见方式,这里推荐使用上面例子中的行内箭头函数 :ref={input => this.textInput = input}
。
将 ref 属性引用到当前类组件
当ref
属性用于自定义类组件的时候,ref
回调函数的参数将会接收到被挂载组件的实例,接下来我们为前面的CustomTextInput
组件模拟组件挂载后被点击的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class AutoFocusTextInput extends React.Component { componentDidMount ( ) { this .textInput .focusTextInput (); } render ( ) { return ( <CustomTextInput ref ={input => { this.textInput = input; }} /> ); } }
上面代码只能工作在CustomTextInput
以类组件进行声明的时候。
1 class CustomTextInput extends React.Component {
ref 与函数式组件
因为函数式组件并不拥有实例对象,因此不可以在ref
回调函数中使用this
进行赋值操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function MyFunctionalComponent ( ) { return <input /> ; } class Parent extends React.Component { render ( ) { return ( <MyFunctionalComponent ref ={input => { this.textInput = input; }} /> ); } }
但是可以在ref
回调函数中通过变量来引用当前组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function CustomTextInput (props ) { let textInput = null ; function handleClick ( ) { textInput.focus (); } return ( <div > <input type ="text" ref ={input => { textInput = input; }} /> <input type ="button" value ="Focus the text input" onClick ={handleClick} /> </div > ); }
暴露子组件的 DOM
引用到父组件
极少的情况下(触发子组件的 focus
事件以及尺寸和位置 ),开发人员需要在父组件访问子组件的 DOM
节点(虽然 React
并不推荐这么做,因为这样会破坏组件的封装性 )。
虽然你可以添加一个 ref
到子组件,但这并不是一个理想的解决方案,因为你只会获取到组件实例而非 DOM
节点,而且这样也无法用于函数类型组件。因此,这里推荐在子组件内暴露一个特殊的prop
,使子组件能够通过该prop
接收到一个任意名称的函数(例如下面函数中的
inputRef ),然后通过ref
属性将该函数关联到 DOM
节点,最终使得父组件能够通过一个中间层级组件传递其ref
回调函数至
DOM
节点,并且这种方式能够同时应用在类组件和函数组件当中,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function CustomTextInput (props ) { return ( <div > <input ref ={props.inputRef} /> </div > ); } class Parent extends React.Component { render ( ) { return <CustomTextInput inputRef ={el => (this.inputElement = el)} /> ; } }
在上面的例子当中,Parent
组件通过CustomTextInput
组件的prop.inputRef
来传递ref
回调函数,而CustomTextInput
组件又将该回调函数传递给<input>
。因此,Parent
组件中的this.inputElement
将会被设置为CustomTextInput
组件内<input>
所对应的
DOM 结点(非常重要 )。
除了可以同时更加广泛的用于函数式组件和类组件,这种模式的另一个优点在于适用于任意嵌套深度的组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function CustomTextInput (props ) { return ( <div > <input ref ={props.inputRef} /> </div > ); } function Parent (props ) { return ( <div > My input: <CustomTextInput inputRef ={props.inputRef} /> </div > ); } class Grandparent extends React.Component { render ( ) { return <Parent inputRef ={el => (this.inputElement = el)} /> ; } }
上面例子中,Grandparent
组件需要操纵CustomTextInput
组件的
DOM,只需要通过Parent
的props
进行一次赋值传递,从而让Grandparent
组件中的this.inputElement
被设置为CustomTextInput
组件当中的<input>
元素的
DOM。
出于更全面的考虑,React 官方并不建议直接暴露 DOM
节点对象,但是可以作为一种应急的处理方式。而且这种方式,需要向子组件添加一些功能代码,如果不希望对子组件造成污染,另一个选择是使用ReactDOM.findDOMNode(component)
方法。
遗留 API:字符串类型的 ref
属性
如果使用早期版本的
React,你可能会熟悉在组件上使用字符串类型的ref
属性,例如<input type="text" ref="textInput" />
元素可以通过this.refs.textInput
获取其
DOM 节点,但是目前 React
官方不建议这样做,因为存在一些悬而末决的问题,并且可能在未来 React
发布版本中被移除,所以建议通过上面回调函数的模式去使用ref
。
附加说明
如果ref
属性是通过行内函数进行定义的,那么在组件更新的时候它将会被调用两次(第
1 次值为null
,第 2 次为 DOM
元素 ),这是因为组件渲染时会建立函数对象的新实例,React
需要清除旧的ref
然后设置新的。我们可以通过将ref
回调函数定义为类组件方法避免该问题,但是大部份情况下这并不会对开发和用户体验造成影响。
非受控组件
大多数情况下,我们推荐使用受控组件 去实现表单,即表单数据由
React
组件所控制。另一种方式是使用非受控组件 ,即表单数据由
DOM 对象所控制。
使用非受控组件,可以通过一个ref
从 DOM
获取表单值,代替为组件的每次状态更新编写事件处理器,下面示例将会接收一个用户输入的字符串然后弹出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class NameForm extends React.Component { constructor (props ) { super (props); this .handleSubmit = this .handleSubmit .bind (this ); } handleSubmit (event ) { alert ("被提交的字符串:" + this .input .value ); event.preventDefault (); } render ( ) { return ( <form onSubmit ={this.handleSubmit} > <label > 输入的字符串: <input type ="text" ref ={input => (this.input = input)} /> </label > <input type ="submit" value ="提交" /> </form > ); } }
非受控组件能够更加容易的整合 React 以及非 React
代码,而且代码更加精简与小巧,言外之意 React
官方推荐通常情况应使用非受控组件。
默认值
在 React
组件的渲染生命周期中,form
元素上的value
属性将会重写
DOM 上的value
属性值。使用非受控组件的时候,通常会希望 React
指定一个能够避免后续非受控更新的初始值,这里需要使用defaultValue
来代替原生的value
属性。
1 2 3 4 5 6 7 8 9 10 render ( ) { return ( <form onSubmit ={this.handleSubmit} > <label > 名称:<input defaultValue ="Hank" ref ={(input) => this.input = input} type="text" /> </label > <input type ="submit" value ="提交" /> </form > ); }
同样的,<input type="checkbox">
和<input type="radio">
支持defaultChecked
,<select>
和<textarea>
支持defaultValue
。
文件上传
React
中的<input type="file">
总是属于非受控组件,因为其值只能被用户设置,而非编程控制。
我们可以通过 JavaScript
原生的File API
对上传文件进行操作,下面的例子体现了如何通过引用
DOM 节点的ref
,在上传事件处理函数中对文件进行操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class FileInput extends React.Component { constructor (props ) { super (props); this .handleSubmit = this .handleSubmit .bind (this ); } handleSubmit (event ) { event.preventDefault (); alert (`Selected file - ${this .fileInput.files[0 ].name} ` ); } render ( ) { return ( <form onSubmit ={this.handleSubmit} > <label > 上传文件: <input type ="file" ref ={input => { this.fileInput = input; }} /> </label > <br /> <button type ="submit" > 提交文件</button > </form > ); } } ReactDOM .render (<FileInput /> , document .getElementById ("app" ));
Fragments 片断
React
组件有时需要返回多个元素,新特性React.Fragment
可以在不增加冗余
DOM 节点的情况下,聚合一系列(多个 )子元素到 DOM 上去。
1 2 3 4 5 6 7 8 9 render ( ) { return ( <React.Fragment > <ChildA /> <ChildB /> <ChildC /> </React.Fragment > ); }
动机
当组件需要返回一个列表时,通用的处理方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Table extends React.Component { render ( ) { return ( <table > <tr > <Columns /> </tr > </table > ); } } class Columns extends React.Component { render ( ) { return ( <div > <td > Hello</td > <td > World</td > </div > ); } } <table> <tr > <div > <td > Hello</td > <td > World</td > </div > </tr > </table>;
冗余的<div>
元素嵌套在<tr>
元素下并不合乎
HTML 规范,因此 React
引入React.Fragment
新特性解决这个通点。
用法
使用<React.Fragment>
改写上面的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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>;
快捷语法
React 16
当中,我们可以使用新添加的fragment
快捷语法<></>
。
1 2 3 4 5 6 7 8 9 10 class Columns extends React.Component { render ( ) { return ( <> <td > Hello</td > <td > World</td > </> ); } }
Babel
之类的编译工具可能暂不支持fragment
快捷语法,因此未受支持的场合可以继续使用<React.Fragment>
。
带 key 属性的 fragment
<React.Fragment>
可以拥有一个key
属性,用于映射一个集合到
fragment 数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 function Production (props ) { return ( <dl > {props.items.map(item => ( // 如果没有提供key属性, React将会提示关于key的警告信息 <React.Fragment key ={item.id} > <dt > {item.name}</dt > <dd > {item.info}</dd > </React.Fragment > ))} </dl > ); }
key
是可以传入Fragment
的唯一属性,未来 React
官方可能会增加更为丰富的属性,比如对事件提供支持。
Portals 传送门
Portal([ˈpɔ:tl] 入口,门户,传送门 )用于渲染子元素到一个
DOM 节点,该 DOM 节点可以位于已存在的父元素 DOM 继承树之外。
1 ReactDOM .createPortal (child, container);
参数child
是任意可渲染的 React
子元素(element、string、fragment ),而参数container
则是一个指定的
DOM 元素。
用法
通常,当你从一个组件的 render()方法返回 HTML
元素的时候,这些元素将会被挂载到相邻父节点 DOM 下面。
1 2 3 4 5 6 7 8 render ( ) { return ( <div > {this.props.children} </div > ); }
但是,有时需要插入一个子元素到 DOM 上的不同位置。
1 2 3 4 5 6 7 render ( ) { return ReactDOM .createPortal ( this .props .children , domNode, ); }
Portals
可以应用在父组件设置overflow: hidden
或z-index
样式,子组件需要在视觉上打破其容器(即在指定位置进行层叠展示,例如:对话框、提示信息、浮动卡片 )的场景下。
事件冒泡
Portal 可以用于 DOM 树任意位置,其行为类似于普通 React
组件。无论子元素是否是一个 Protal,其上下文特性都是相同的(因为
Protal 仍然存在于 React 组件树当中,而无论其在 DOM
中的真实位置如何 ),这其中就包括了事件冒泡。
下面例子中,Portal 内触发的事件将会冒泡至 React
组件树的祖先元素,即使它们并不是 DOM 结构意义上的祖先元素:
1 2 3 4 5 6 <html> <body > <div id ="app-root" /> <div id ="modal-root" /> </body > </html>
上面的 HTML
结构当中,父组件中的#app-root
(应用根节点 )将会响应兄弟节点#modal-root
(模态框节点 )上的捕获 或者冒泡 事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 const appRoot = document .getElementById ("app-root" );const modalRoot = document .getElementById ("modal-root" );function Child ( ) { return ( <div className ="modal" > <button > 点击我!</button > </div > ); } class Modal extends React.Component { constructor (props ) { super (props); this .el = document .createElement ("div" ); } componentDidMount ( ) { modalRoot.appendChild (this .el ); } componentWillUnmount ( ) { modalRoot.removeChild (this .el ); } render ( ) { return ReactDOM .createPortal (this .props .children , this .el ); } } class Parent extends React.Component { constructor (props ) { super (props); this .state = { clicks : 0 }; this .handleClick = this .handleClick .bind (this ); } handleClick ( ) { this .setState (prevState => ({ clicks : prevState.clicks + 1 })); } render ( ) { return ( <div onClick ={this.handleClick} > <p > 当前点击次数: {this.state.clicks}</p > <Modal > <Child /> </Modal > </div > ); } } ReactDOM .render (<Parent /> , appRoot);
最终生成的 HTML DOM 结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <body > <div id ="app-root" > <div > <p > 当前点击次数: 0</p > </div > </div > <div id ="modal-root" > <div > <div class ="modal" > <button > 点击我!</button > </div > </div > </div > </body >
从父组件内的 Portal
获取事件冒泡,能够让开发更加灵活和抽象,但是这些抽象并不依赖于
Portal。例如渲染<Modal />
组件时,Parent
组件能够捕获它的事件,无论其是否通过
Portal 实现。
Web Components
React 与Web
Components 分别用来解决不同问题,Web
Components 为组件复用提供了强大的封装机制,而React 则侧重于保持数据与
DOM
的同步,两者相互补充;开发人员可以自由的对两者进行混合使用,尽管开发人员大部分情况只需要使用React ,但是不排除第三方组件使用到Web
Components 。
在 React 中使用 Web
Components
Web Components 通常需要暴露出命令式 API,例如一个
Video 作为Web
Components ,可能需要暴露play()
和pause()
两个
API,操作这些命令式 API 需要通过一个引用直接与 DOM
节点进行交互。如果你正在使用第三方提供的Web
Components ,最好的解决方式是使用 React 组件包裹Web
Components 。
Web Components 产生的事件可能不会在 React
的渲染树上正确的进行传播,开发人员将需要在 React
组件当中手动的添加事件处理函数。
1 2 3 4 5 6 7 8 9 class HelloMessage extends React.Component { render ( ) { return ( <div > Hello <x-search > {this.props.name}</x-search > ! </div > ); } }
一个比较常见的混淆是Web
Components 使用了class
去替代className
。
1 2 3 4 5 6 7 8 function BrickFlipbox ( ) { return ( <brick-flipbox class ="demo" > <div > front</div > <div > back</div > </brick-flipbox > ); }
在 Web Components 中使用
React
下面的示例代码不能工作在使用 Babel 转译的环境,你可以点击这里 查看相关
issue。也可以在加载 Web Components 之前,通过名为custom-elements-es5-adapter 的polyfill 解决这个问题。
1 2 3 4 5 6 7 8 9 10 11 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.google.com/search?q=" + encodeURIComponent (name); ReactDOM .render (<a href ={url} > {name}</a > , mountPoint); } } customElements.define ("x-search" , XSearch );
错误边界
早期 React 版本当中的 JavaScript 错误经常会破坏 React
的内部状态,从而导致整个 Web 应用程序崩溃。为了解决这一问题,新版本的
React 16
引入了一个全新的错误边界 (或译为错误分界线 )特性。
错误边界是一种用于在 React 组件当中捕捉并打印 JavaScript
错误,并显示回调 UI
界面的错误处理机制,可以广泛应用于组件渲染函数
、生命周期方法
、类组件构造器
当中 。
错误边界不能 用于事件处理函数
、异步处理代码
、服务器端渲染
、错误边界机制本身抛出的错误
一类的场景。
使用 React 16
新增的生命周期方法componentDidCatch(error, info)
即可以使一个
React 组件具备错误边界 捕获能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class ErrorBoundary extends React.Component { constructor (props ) { super (props); this .state = { hasError : false }; } componentDidCatch (error, info ) { this .setState ({ hasError : true }); logErrorToMyService (error, info); } render ( ) { if (this .state .hasError ) { return <h1 > 提示:发生错误了!</h1 > ; } return this .props .children ; } }
然后可以像 React 常规组件那样使用它。
1 2 3 <ErrorBoundary > <MyWidget /> </ErrorBoundary >
componentDidCatch()
方法类似于 JavaScript
的catch{}
语法,但是对于 React
组件而言,只有类组件能够拥有捕获错误边界的能力。不过实际开发场景下,大部分情况只需定义一个通用的错误边界组件 ,然后在
Web 应用程序其它组件内进行复用。
错误边界组件只能捕获其子组件当中发生的错误,并不能捕获错误边界组件自身产生的问题,例如其自身渲染错误提示信息失败时,此错误会传播到最近的父级错误边界组件处理,此特性与
JavaScript 的catch{}
较为相似。
componentDidCatch(error,
info)的参数
error
是抛出的错误信息;info
是一个使用componentStack
作为
key 的对象,抛出错误时该属性包含组件堆栈的相关信息。
1 2 3 4 5 6 7 8 9 10 componentDidCatch (error, info ) { logComponentStackToMyService (info.componentStack ); }
错误边界放置位置
错误边界组件的放置位置完全取决于开发人员的使用习惯,可以放置在顶级路由组件的最外层向用户展示错误信息,也可以用来包裹单独的组件,有效防止单个组件错误引发整个
Web 应用崩溃。
错误捕获的新行为
从 React16 开始,没有被任何错误边界捕获的错误将导致整个 React
组件树都被卸载。
Facebook 内部对该决定进行了讨论,在我们的经验中,离开损坏的 UI
比完全删除它的用户体验更加糟糕。例如,像 Messenger
这样的产品中,用户可以看到被破坏的
UI,这可能会导致有人向错误的人发送消息。类似地,支付应用程序显示错误的数量比不提供任何东西的用户体验更加糟糕。
这种变化意味着从老版本迁移到 React 16
时,可能会发现应用程序中存在被忽略的崩溃性错误,因此添加错误边界可以在出现问题时提供更好的用户体验。
例如,Facebook 的 Messenger
将侧边栏、信息面板、对话日志、消息输入内容封装到单独的错误边界中。如果这些
UI 区域中的某个组件崩溃,剩下的部分仍然能够正常响应用户的交互。
组件堆栈记录
React 16 可以自动打印开发时产生的错误至浏览器控制台,除了错误信息和
JavaScript
堆栈之外,还提供了组件堆栈记录,让开发人员能够更加清晰的了解组件树中发生的故障。该特性只用于开发,生产中必须禁用 。
如果 Web 应用是由create-react-app 搭建,或是手动安装了babel-plugin-transform-react-jsx-source 插件,组件堆栈记录当中还能够展示文件名 、行号 。
堆栈记录当中组件名称的展示依赖于 JavaScript
原生的Function.name
属性,如果使用 IE11
等还未支持该属性的浏览器,就需要单独安装Function.name
Polyfill 进行兼容,或者在组件定义时显式的设置displayName
属性。
使用 try/catch
try/catch
只对命令式代码有效,但是 React
组件都是声明式的,并且能够指定渲染的内容。
1 2 3 4 5 6 7 try { showButton (); } catch (error) { } <Button />;
错误边界 保留了 React
的声明特性,让代码按照预期的方式执行。例如在componentdidupdate()
生命周期方法内使用setState()
出现错误,这些错误仍将正确传播到最近的错误边界 。
使用事件处理器
错误边界 不能捕捉到事件处理函数内发生的错误,因为
React
并不需要处理事件函数内产生的错误。不同于render()
和其它组件生命周期函数,组件内的事件处理函数不会在组件渲染期间得到执行。因此当有错误被抛出的时候,React
会将其显示到屏幕上。
如果需要在事件处理函数内捕捉错误信息,建议使用 JavaScript
传统的try/catch
语句。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class MyComponent extends React.Component { constructor (props ) { super (props); this .state = { error : null }; } handleClick = () => { try { } catch (error) { this .setState ({ error }); } }; render ( ) { if (this .state .error ) { return <h1 > 捕捉到错误!</h1 > ; } return <div onClick ={this.handleClick} > 点击我!</div > ; } }
Reconciliation 调和
Reconciliation ([,rek(ə)nsɪlɪ'eɪʃ(ə)n]
n.调和 )是 React 提供的一种比较算法 ,能够让 React
组件的更新可以预测,并且提供了更优秀的 DOM 渲染性能。
使用 React 的过程中,可以通过render()
函数来创建 React
元素。当state
或props
更新时将返回不同的 React
元素树,此时 React 需要考量如何更加高效的将变化反映到 UI 上去。
对于将一棵树状数据结构同步到另外一棵树 的最小操作数算法问题,虽然有一些通用的解决方案,比如art
算法 (参见《关于树的编辑深度与相关问题的研究》 )复杂度为O(n3) ,其中n 是
React 元素树上元素的个数。如果在 React 中使用该算法,显示 1000
个元素将需要 10 亿次比较操作,性能开销极为昂贵。
因此,React 根据如下 2
个假设实现了一套启发式O(n) 算法:
不同类型的 2 个元素会产生不同的树。
通过名为key
的props
来提示哪些子元素在不同渲染过程中是稳定的。
在实践中,上述假设对于几乎所有用例都是有效的。
Diffing 算法
比较两颗树的时候,React
首先会比较其根元素,根据根元素的类型来判断行为的不同。
不同类型的元素
每当根元素类型不同时,React
都会推倒旧的树并从头构建新的树。从<a>
到<img>
、<Article>
到<Comment>
、<Button>
到<div>
,这些情况都会导致推倒重建。
React 推倒旧树意味其 DOM
节点将被销毁,组件实例执行componentWillUnmount()
方法。构建新树意味新的
DOM
节点被插入,组件实例执行componentWillMount()
以及componentDidMount()
方法,此时旧树上关联的state
将会完全消失。
下面例子当中,旧的Counter
组件会被卸载,其状态也将被销毁,然后新的Counter
组件将会被挂载至
DOM。
1 2 3 4 5 6 7 8 9 <div> <Counter /> </div> <span > <Counter /> </span >
相同类型的 DOM 元素
比较两个相同类型的 DOM 元素时,React 首先检查 2
个元素的属性,并保证相同的底层 DOM
节点,然后只更新属性发生更改的那一部分。
下面例子当中,React
只会更新className
发生改变了的组件所对应的 DOM 节点。
1 2 3 4 5 <div className="before" title="stuff" /> <div className ="after" title ="stuff" />
当更新style
属性时,React
依然会只更新style
发生改变的那部分 DOM 节点。
下面例子中,React
只会修改color
样式,而不是fontWeight
样式。
1 2 3 4 5 <div style={{color : 'red' , fontWeight : 'bold' }} /> <div style ={{color: 'green ', fontWeight: 'bold '}} />
处理 DOM 节点之后,React
将会递归的处理其它子元素 。
相同类型的 React 组件元素
当 React
组件更新的时候,组件实例保持不变,因此state
在渲染时也将被实例所维护。React
更新组件实例的props
使之匹配新的元素,并调用该组件实例上的componentWillReceiveProps()
和componentWillUpdate()
方法。最后render()
方法会被调用,比较算法将会递归的展示新的渲染结果。
递归处理子元素
默认情况下,递归 DOM 节点的子节点时,每当出现差异,React
都只遍历子元素列表。
例如,当添加一个子元素到无序列表尾部时,React
将会首先匹配两颗树的<li>first</li>
,然后是<li>second</li>
,最后插入<li>third</li>
。
1 2 3 4 5 6 7 8 9 10 11 12 <ul> <li > first</li > <li > second</li > </ul> <ul > <li > first</li > <li > second</li > <li > third</li > // 增加的项 </ul >
如果需要插入元素到无序列表<li>
子元素开头的位置,那么将会得到比较差的性能,例如需要转换下面的
2 颗 DOM 树:
1 2 3 4 5 6 7 8 9 10 11 12 <ul> <li > first</li > <li > second</li > </ul> <ul > <li > zero</li > // 增加的项 <li > first</li > <li > second</li > </ul >
React
将会改变每个子元素,并保持<li>first</li>
和<li>second</li>
不变,这样性能将是一个问题。
Keys
为了解决上面遗留的问题,React 通过旧 DOM
树上的key
属性去匹配原始 DOM
树上的元素,从而有效的区分出需要更新的部分。
现在,为上面的示例代码添加上不同的key
属性,让 React
明确的知道哪个 DOM 结点发生了更新。
1 2 3 4 5 6 7 8 9 10 11 12 <ul> <li key ="1" > first</li > <li key ="2" > second</li > </ul> <ul > <li key ="0" > zero</li > // 增加的项 <li key ="1" > first</li > <li key ="2" > second</li > </ul >
日常开发场景当中,key
属性值的 ID
在其同胞元素中必须是唯一的 (并非全局唯一 ),因此可以手动进行设置,或是使用工具生成
Hash,再或者是通过绑定的动态数据。
1 <li key={item.id }>{item.name }</li>
万不得已的时候,如果每个数据项不需要再进行排序,那么可以使用其索引值index
作为key
,但是负作用是重新排序的时候会变得非常缓慢。另外,使用数组索引作为key
,重新排序还会引发组件状态方面的问题,即移动其中一项并改变它时,会导致受控输入类组件的状态被混淆,并以不被期待的方式更新。
权衡
重新渲染当前上下文意味着调用当前所有组件的render()
方法,这并不意味
React 将会卸载或者重新挂载这些组件,React 只会按照上述规则对 DOM
结构进行局部的更新。
为了让大部分用例运行更加快速,社区经常对 React
的策略进行改进。在当前实现中,React 子树的每项 DOM
元素都只是在其兄弟元素之间移动,而非在整个页面的 DOM
结构(会造成惊人的性能开销 )。
由于 React 依赖于启发式算法,使用的时候需要注意以下两点:
该算法不会尝试匹配不同组件类型的子树,如果两个自定义组件类型具有非常相似的输出,那么可以考虑将其归为一个相同类型。
key
值应该是稳定的、可预测的、唯一的;不稳定的键(比如math.random()
生成的键 )会导致许多组件实例和
DOM
节点被不必要的重新创建,这将会导致性能的下降,并让子组件丢失状态。
Context 组件树上下文
Context
提供了一种在组件树当中传递数据的方式,而毋需手动在每层组件通过props
进行传递。
下面的例子代码当中,为了按钮组件的样式而手动传递了一个名为theme
的props
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class App extends React.Component { render ( ) { return <Toolbar theme ="dark" /> ; } } function Toolbar (props ) { return ( <div > <ThemedButton theme ={props.theme} /> </div > ); } function ThemedButton (props ) { return <Button theme ={props.theme} /> ; }
使用context
,我们可以避免向一些中间层级的组件传递props
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const ThemeContext = React .createContext ("light" );class App extends React.Component { render ( ) { return ( <ThemeContext.Provider value ="dark" > <Toolbar /> </ThemeContext.Provider > ); } } function Toolbar (props ) { return ( <div > <ThemedButton /> </div > ); } function ThemedButton (props ) { return <ThemeContext.Consumer > {theme => <Button {...props } theme ={theme} /> }</ThemeContext.Consumer > ; }
React.createContext
1 const { Provider , Consumer } = React .createContext (defaultValue);
通过createContext()
这个 API
获取{ Provider, Consumer }
对象,当 React 渲染一个 Context
的Consumer
时,它将会从闭合的Provider
当中读取当前的
Context
值。当渲染一个没有匹配Provider
的Consumer
时,defaultValue
参数用于提供一个默认值,从而有助于对组件进行独立测试。
Provider
一个 React 组件允许Consumer
去订阅 Context
的变化。value
的值会被传递到Provider
的子级Consumer
当中,一个Provider
能够连接到多个Consumer
,Provider
可以被嵌套以覆盖组件树上更深层的位置。
Consumer
1 2 3 <Consumer > {value => } </Consumer >
上面代码定义了一个订阅 Context 变化的组件。
需要一个函数作为组件的子元素,该函数会接收当前 Context 值并返回一个
React 节点。这个传入函数的参数将会等同于当前组件树上 Context 相临的
Provider 值,如果 Context 相应的 Provider
不存在,那么该参数的值将会等于传递至createContext()
的defaultValue
值。
无论 Provider 的值如何变化,所有 Consumer
都会重新进行渲染。这种变化取决于使用Object.is
类似算法所进行的新旧值比较(当传递对象作为值时,可能会导致一些问题,参见注意事项 )。
一个完整的例子
首先定义一个
Store,并将其代码放置到一个单独的文件store.js
当中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React from "react" ;export const Flux = { Store : { form : {}, table : [], modal : { add : false , auth : false , history : false } }, setStore : () => {} }; export const Context = React .createContext (Flux );
然后添加Provider
,将<SearchForm />
、<ResultTable />
两个子组件的状态全部提升至index.jsx
组件,并且引入上面定义的
Store
对象并且定义其对应的钩子函数,便于两个子组件当中的数据进行双向绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import React from "react" ;import "./style.scss" ;import SearchForm from "./search-form" ;import ResultTable from "./result-table" ;import { Context , Flux } from "./store" ;export default class Demo extends React.Component { constructor (props ) { super (props); this .state = { Store : Flux .Store , setStore : newState => { this .setState (oldState => ({ Store : { ...newState, ...oldState } })); } }; } render ( ) { return ( <div id ="demo" > <Context.Provider value ={this.state} > <SearchForm /> <ResultTable /> </Context.Provider > </div > ); } }
接下来,就可以在两个子组件内,通过Consumer
获取 Context
传入的Store
对象以及setStore()
钩子函数,完成跨组件的双向绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import React from "react" ;import ModalAuth from "./modal-auth" ;import ModalHistory from "./modal-history" ;import { Context } from "../store" ;export default class ResultTable extends React.Component { render ( ) { return ( <Context.Consumer > {({ Store, setStore }) => ( <React.Fragment > {/* 修改Store中的属性值 */} <Table onClick ={() => { setStore({ modal: { add: true } }); }} /> {/* 使用Store里的属性值 */} <h1 > {Store.modal.add}</h1 > <ModalAuth /> <ModalHistory /> </React.Fragment > )} </Context.Consumer > ); } }
虽然 React
在16.0
版本以后重写了Context API
,并移除出了官方文档中的不建议使用标识,但是受限于<Consumer>
必需在组件render()
函数内进行传值,笔者依然建议开发人员在进行跨组件通信时,选用
Reflux、Redux、Mobx 等专用的状态管理工具。
Accessibility 可访问性
Web 可访问性(Web
accessibility )也被称为a11y ,用于构建适宜所有人群访问的页面。JSX
支持所有aria-*
的 HTML 属性,这些特性在 React
当中全部采用小写:
1 <input aria-label={labelText} aria-required="true" type="text" onChange={onchangeHandler} value={inputValue} name="name" />
语义化 HTML
语义化的 HTML 是 Web 应用程序可访问性的基础。JSX
当中添加<div>
元素会破坏 DOM
的语义结构,特别是在使用了列表元素<ol>
、<ul>
、<dl>
、<table>
的情况下,此时应该使用
React 片段(Fragment )将多个元素组合到一起。
通常情况下使用<></>
语法:
1 2 3 4 5 6 7 8 function ListItem ({ item } ) { return ( <> <dt > {item.term}</dt > <dd > {item.description}</dd > > </> ); }
进行列表遍历操作时需要使用到key
属性,此时可以使用<Fragment>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import React , { Fragment } from "react" ;function Glossary (props ) { return ( <dl > {props.items.map(item => ( // Without the `key`, React will fire a key warning <Fragment key ={item.id} > <dt > {item.term}</dt > <dd > {item.description}</dd > </Fragment > ))} </dl > ); }
可访问表单的<label>
一些 HTML
表单控件(例如<input>
和<textarea>
)需要添加<label></label>
作为可访问标签,HTML
中的for
属性在 JSX
当中会写为htmlFor
,例如下面的代码:
1 2 <label htmlFor="namedInput" >Name :</label> <input id ="namedInput" type ="text" name ="name" />
输入焦点管理
React 应用在运行期间会不断对 DOM
进行修改,这可能会导致键盘焦点丢失或定位到未知元素,此时可以通过
JavaScript 代码进行修正。
首先,在类组件的 JSX 当中添加一个ref
属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class CustomTextInput extends React.Component { constructor (props ) { super (props); this .textInput = React .createRef (); } focus ( ) { this .textInput .current .focus (); } render ( ) { return <input type ="text" ref ={this.textInput} /> ; } }
有时候,父级组件需要去设置一个聚焦到子级组件的元素上,我们可以通过子级组件上的一个特殊prop
暴露
DOM
的ref
给父级组件,从而将父级的ref
传递到子级的
DOM。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function CustomTextInput (props ) { return ( <div > <input ref ={props.inputRef} /> </div > ); } class Parent extends React.Component { constructor (props ) { super (props); this .inputElement = React .createRef (); } render ( ) { return ( <CustomTextInput inputRef ={this.inputElement} /> ); } } /> this .inputElement .current .focus ();
当使用高阶组件去继承组件时,推荐通过使用 React
的forwardRef()
函数转发ref
到被包裹的组件,如果第三方高阶组件没有实现ref
转发 ,上面的模式依然可以作为一种回退。
尽管上述内容对于可访问性非常重要,但也应该审慎进行应用,总是在聚焦事件发生中断时去修复键盘的焦点。
代码切割
在与 Webpack 共同使用的场景下,伴随 Web
应用的增长,打包文件的体积也会快速的增长,因为需要引入代码拆分 特性,切分并且懒加载脚本代码,从而优化前端的用户性能与体验。
import()
引入代码拆分 最简单的方式是通过 Webpack
提供的import()
语法,Babel 上可以通过babel-plugin-syntax-dynamic-import 添加支持。
1 2 3 4 5 6 7 8 import { add } from "./math" ;console .info (add (16 , 26 ));import ("./math" ).then (math => { console .info (math.add (156 , 98 )); });
import()
语法目前还处于 ECMAScript
提案阶段,不久的将来可能会成为标准。
react-loadable
react-loadable 是一个封装良好的、能够实现动态导入的高阶组件,能够对
React 应用程序中的组件进行动态的拆分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import OtherComponent from "./OtherComponent" ;const MyComponent = ( ) => <OtherComponent /> ;import Loadable from "react-loadable" ;const LoadableOtherComponent = Loadable ({ loader : () => import ("./OtherComponent" ), loading : () => <div > Loading...</div > }); const MyComponent = ( ) => <LoadableOtherComponent /> ;
基于路由进行切割
基于路由进行代码拆分是一种相对合理的打包策略,下面示例代码中通过react-router 和react-loadable 展示了如何通过路由完成代码的切割。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import Loadable from "react-loadable" ;import { BrowserRouter as Router , Route , Switch } from "react-router-dom" ;const Loading = ( ) => <div > Loading...</div > ;const Home = Loadable ({ loader : () => import ("./routes/Home" ), loading : Loading }); const About = Loadable ({ loader : () => import ("./routes/About" ), loading : Loading }); const App = ( ) => ( <Router > <Switch > <Route exact path ="/" component ={Home} /> <Route path ="/about" component ={About} /> </Switch > </Router > );
整合 jQuery
如果需要整合 React 与 jQuery,可以在组件的 DOM
根元素上添加ref
属性,并在componentDidMount()
当中调用该ref
并将其传递给
jQuery 插件,最后在componentWillUnmount()
移除 DOM
上绑定的事件。同时,为了防止 React 组件加载之后修改 DOM
节点,需要先在render()
方法中返回一个空的<div />
,这样
React 就不会对其进行更新,封装的 jQuery 插件就可以任意修改该节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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)} /> ; } }
高阶组件
高阶组件本质是一个函数,能够接受一个组件并返回一个新的组件。
1 const EnhancedComponent = higherOrderComponent (WrappedComponent );
Render Props
Render Props 是一种将组件的 Props
设置为函数,从而通过传入参数共享数据并动态决定所需要渲染组件的模式。下面是一个动态获取当前鼠标位置的示例代码,MouseTracker
是用于渲染的根组件,Picture
实时获取鼠标的坐标位置并使组件渲染的图片与鼠标实时联动,MousePosition
用于获取鼠标的当前位置,并将状态通过render(this.state)
传递给Picture
组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Picture extends React.Component { render ( ) { const mouse = this .props .mouse ; return <img src ="/images/picture.png" style ={{ position: "absolute ", left: mouse.x , top: mouse.y }} /> ; } } class MousePosition 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: "100 %" }} onMouseMove ={this.handleMouseMove} > {/* 使用render prop动态决定需要渲染的组件,代替直接去渲染静态<Mouse > 组件。*/} {this.props.render(this.state)} </div > ); } } class MouseTracker extends React.Component { render() { return ( <div > <h1 > 移动鼠标!</h1 > {/* 为Mouse组件设置的render props是一个函数*/} <MousePosition render ={mouse => <Picture mouse ={mouse} /> } /> </div > ); } }
Render Props 本质是一种 Context 机制之外的组件间状态共享机制。
严格模式与性能优化
严格模式用于在开发模式下检查 React
应用中潜在的问题,目前能够识别的问题如下:
识别具有不安全生命周期的组件。
有关废弃的字符串 ref 用法的警告。
关于已弃用的 findDOMNode 用法的警告。
检测意外的副作用。
检测遗留的 context API。
只需在要进行严格检查的组件上添加父组件<React.StrictMode>
即可开启严格模式,具体使用请参见以下代码:
1 2 3 4 5 6 7 8 9 10 11 class StrictCheck extends React.Component { render ( ) { return ( <div > <React.StrictMode > <SomeCompenent /> </React.StrictMode > </div > ); } }
Webpack
编译打包的时候(生产环境 )可以通过添加下面代码来优化编译过程。
1 2 3 4 5 6 new webpack.DefinePlugin ({ "process.env" : { NODE_ENV : JSON .stringify ("production" ) } }), new webpack.optimize .UglifyJsPlugin ();
虽然 React 只是按需更新 DOM
节点,但是诸如多次输入事件不断触发时,会造成组件的render()
函数被不停的渲染,这里可以通过shouldComponentUpdate
避免这个问题。React
生命周期函数shouldComponentUpdate
会在组件重绘前执行,该函数默认返回true
,如果遇到组件不需要更新的情况,可以让该函数返回false
从而避免组件被重绘。
1 2 3 shouldComponentUpdate (nextProps, nextState ) { return true ; }
React Router 4
Rails、Express、Ember、Angular
使用的是静态路由 机制(Static
Routing ),即将路由作为 Web 应用初始化的一部分,React Router 4
之前的版本也采用相同的机制。
动态路由 (Dynamic Routing )是指的 Web
应用程序渲染的时候发生的路由,而非正在运行的 Web
应用程序之外的配置和约定,这意味着 React Router 当中的一切都是组件。
基本组件
React Router 拥有 3
种组件:路由组件、路由匹配的组件、导航组件,这些组件都可以通过react-router-dom
引入。
Routers
Web
应用程序的核心是路由组件,react-router-dom
提供了<BrowserRouter>
和<HashRouter>
两种路由组件,它们都会去建立一个特殊的history
对象。如果拥有一台能够响应请求的服务器,那么可以使用<BrowserRouter>
;如果使用静态文件服务器,则可以选用<HashRouter>
。
Route Matching
路由匹配组件主要包含<Route>
和<Switch>
这两个组件。
<Route>
路由的匹配是通过<Route>
组件的path
prop
与当前位置路径的比较来完成的,如果比较成功则渲染组件内容,如果失败则渲染为空。没有path
prop 的<Route>
总是会得到匹配。
开发人员可以在任意需要渲染内容的位置包含<Route>
,通常情况需要通过<Switch>
组件将一组路由放置到一起。
<Switch>
<Switch>
并非仅用来组织多个<Route>
的,其拥有更多的潜在用途。比如<Switch>
会迭代其全部子<Route>
元素,并且只渲染匹配当前地置的第一个组件,这在具有多个同名路由、路由之间的动画过渡、没有路由匹配当前地址等场景下非常有用。
Route Rendering Props
开发人员可以通过component
、render
、children
三个
props
选项,指定<Route>
如何渲染一个组件。其中component
和render
较为常用。
不能在一个传递了作用域内变量的内联函数当中使用component
,因为将会发生不必要的组件卸载和重复挂载。
Navigation
React Router 提供<Link>
组件用于在 Web
应用中建立链接,无论在哪里渲染<Link>
组件,都会在 HTML
中生成一个<a>
标签。其中<NavLink>
是一种特殊的<Link>
,访问路径匹配时可以为自身添加active
等状态。必要的情况下,也可以通过<Redirect>
强制使用其prop
进行导航。
代码分割
React
通过webpack
、babel-plugin-syntax-dynamic-import
、react-loadable
完成代码分割。webpack
已经内建了动态引入支持,如果你正在使用
Babel(用来将 JSX 转换为
JavaScript ),那么可以使用babel-plugin-syntax-dynamic-import
插件。该插件只是简单的允许
Babel 去解析动态引入,让 Webpack
能够方便的以代码分割的方式进行打包。因此,你的.babelrc
可能是这样的:
1 2 3 4 5 6 7 8 { "presets" : [ "react" ], "plugins" : [ "syntax-dynamic-import" ] }
react-loadable
是一个用来进行动态加载的高优先级组件,它能自动处理各种边界状况,让代码分割工作变得简单,下面是一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 import Loadable from "react-loadable" ;import Loading from "./Loading" ;const LoadableComponent = Loadable ({ loader : () => import ("./Dashboard" ), loading : Loading }); export default class LoadableDashboard extends React.Component { render ( ) { return <LoadableComponent /> ; } }
loader
选项是一个用来加载确切组件的函数,loading
是一个处于加载状态真实组件的占位符组件。
构建产品化的 React 应用
生产环境下,React需要结合大量的第三方包协助开发,如何基于这些第三方包来组织一个合理的项目结构,对于新接触React的开发开发人员是一个需要逐步摸索的过程。这里笔者结合自己的实践经验,分享了组织React产品化项目的一些心得,并以此作为全文的收尾章节。
项目结构
整体的项目构建上,笔者选用了Webpack + Gulp
的工具栈,并没有采用create-react-app所使用的npm script + webpack-plugin
方式,这样做的目的一方面是照顾开发团队的使用习惯,另一方面是让Webpack完成转译和代码打包的工作,而将自动化任务分离出来交给Gulp完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ├── config │ ├── base.js │ ├── common.js │ ├── develop.js │ ├── product.js │ └── style.js ├── gulpfile.js ├── package.json ├── README.md ├── server │ ├── app.js │ ├── common │ ├── dashboard │ └── login └── sources ├── app.js ├── assets ├── common ├── dashboard ├── index.html ├── layout ├── login ├── router.js └── store.js
config
目录是Webpack相关的配置,server
目录是Express构建的用于组装模拟数据的Web服务端代码,sources
目录则是React前端项目相关的代码。
程序入口点
单页面应用程序通常会拥有一个全局唯一的入口点app.js
,主要用于挂载视图DOM,以及配置路由、热加载、权限拦截、全局状态管理等。在笔者项目当中,前端路由选用了React Router 4
,UI组织库指定为ant.design
,CSS代码则使用node-sass
预处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import React from "react" ;import ReactDOM from "react-dom" ;import Router from "./router.js" ;import Auth from "./common/utils/auth.js" ;import { LocaleProvider } from "antd" ;import CN from "antd/lib/locale-provider/zh_CN" ;import "babel-polyfill" ;import { Provider } from "mobx-react" ;import Store from "./store" ;import DevTools from "mobx-react-devtools" ;import "./common/styles/base.scss" ;import "./common/styles/reset.scss" ;import "./common/styles/awesome/css/fontawesome-all.min.css" ;import "animate.css/animate.min.css" ;import "antd/dist/antd.less" ;import "./common/styles/theme.less" ;ReactDOM .render ( <LocaleProvider locale ={CN} > <Provider GlobalStore ={Store} > <Router > <DevTools /> </Router > </Provider > </LocaleProvider > , document .getElementById ("app" ) ); Auth .initializer ();Auth .interceptor ();
路由配置
笔者将前端路由的具体配置分离到了单独的router.js
文件,并且通过React Loadable
来实现基于组件的代码分割和懒加载,与此同时还配置了全局的页面加载动效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { HashRouter , Route , Link , Switch } from "react-router-dom" ;import React from "react" ;import Loadable from "react-loadable" ;import Loading from "./common/components/loading" ;import { hot } from "react-hot-loader" ;const Login = Loadable ({ loader : () => import ("./login/index.jsx" ), loading : Loading }); export default hot (module )(() => ( <HashRouter > <Switch > <Route exact path ="/" component ={Login} /> <Route exact path ="/login" component ={Login} /> <Route path ="/layout" component ={Loadable({ loader: () => import("./layout/index.jsx"), loading: Loading })} /> </Switch > </HashRouter > ));
权限认证
项目当中,权限认证相关的功能都会被封装到一个auth.js
进行集中处理,包括权限信息的初始化、HTTP权限状态的拦截、路由权限的处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import React from "react" ;import Http from "./http.js" ;import Encrypt from "./encrypt.js" ;import { Route , Redirect } from "react-router-dom" ;import { storage } from "../utils/helper" ;import queryString from "querystringify" ;export default { initializer ( ) { const searchInfo = queryString.parse (location.search ).info ; if (searchInfo) { const query = JSON .parse (Base64 .decode (searchInfo)); storage.set ("token" , query.token ); storage.set ("username" , query.username ); storage.set ("permissions" , query.permissions ); } }, interceptor ( ) { Http .fetch .interceptors .request .use ( function (config ) { const token = Encrypt .token .get (); if (token) config.headers .Authorization = token; return config; }, function (error ) { return Promise .reject (error); } ); Http .fetch .interceptors .response .use ( function (response ) { const head = response.data .head ; const body = response.data .body ; if (head && typeof head === "object" && head.hasOwnProperty ("status" )) { if (head.status === "TIMEOUT" ) { window .location .href = body.url ; storage.empty (); } } return response; }, function (error ) { return Promise .reject (error); } ); }, authRoute ({ component: Component, ...rest } ) { return ( <Route {...rest } render ={props => Encrypt.token.get() ? ( <Component {...props } /> ) : ( <Redirect to ={{ pathname: Http.url.login , state: { from: props.location } }} /> ) } /> ); } };
整合Mobx
状态管理框架方面,笔者选用了轻量好用的Mobx
方案,并且通过建立全局store
并将其分离至单独的store.js
文件便于管理和维护,下面代码仅将全局全局过渡动画的状态位纳入Mobx
管理。
1 2 3 4 5 6 7 8 9 10 11 12 import { observable, computed, action } from "mobx" ;import { Tag } from "antd" ;import React from "react" ;import Loading from "./common/components/loading" ;class Store { @observable loading = true ; } export default new Store ();
由于在app.js
当中已经完成了mobx-react
所提供的Provider
配置,因此子组件仅需注入该Store 即可通过this.props.GlobalStore
访问上面定义的全局动画状态位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import React from "react" ;import "./style.scss" ;import { Link } from "react-router-dom" ;import { observer, inject } from "mobx-react" ;@inject ("GlobalStore" ) @observer export default class GlobalLayout extends React.Component { render ( ) { return ( <h1 > {this.props.GlobalStore.loading}</h1 > ); } }
完整的脚手架项目,请参见笔者Github当中提供的开源脚手架项目Rhino 。