React学习笔记
你可以基于React创建一个新的工程,或者为现有工程添加React支持。
要开始一个新的、单页面应用,Create React App是很好的起点。该模块能够创建一个没有构建配置的React应用:
1 2 3 4 5 6 7 |
# 安装此模块 npm install -g create-react-app # 创建一个React应用 create-react-app ReactStudy cd ReactStudy/ # 在3000端口启动一个Web服务,并自动打开浏览器 npm start |
命令create-react-app不会处理后端逻辑或者数据库,它仅仅创建一个由webpack、Babel、ESLint构成的前端构建管道(frontend build pipeline)。
如果要为现有的工程添加React支持,可以执行以下步骤:
可以使用包管理器Yarn或者npm,将React添加为当前工程的依赖:
1 2 |
yarn add react react-dom npm install --save react react-dom |
yarn和npm都从npm仓库(Registry)下载JavaScript包。
为了能够在代码中使用ES6和JSX,推荐使用编译器Babel。你需要添加babel-preset-react、babel-preset-es2015这两个预设。具体步骤参考使用Babel进行JS转码
最好使用webpack或者Browserify这样的打包器(bundler),为模块化代码提供支持、打包为最小化的脚本以降低客户端加载时间。
默认的,React包含了很多辅助开发的警告信息,这些警告让React变的缓慢、庞大,因此在生产环境下,必须使用React的Production版本。
如果你使用了create-react-app,则 npm run build 自动会在build目录中创建一个为生产环境优化的构建。
如果使用Webpack,应该添加DefinePlugin、UglifyJsPlugin。
如果使用Browserify,可以在环境变量NODE_ENV=production的情况下运行Browserify,并且使用UglifyJS作为构建的最后一步,以保证development-only代码被去除。
考虑下面的变量声明:
1 |
const element = <h1>Hello, world!</h1>; |
这种语法显然不是合法的JavaScript,它混合了一段HTML字符串,而字符串并没有用引号限定其范围。
这段代码叫做JSX,是对JavaScript的语法扩展。React推荐使用JSX来描述组件的UI。JSX不是简单的模板语言,它具有JavaScript的所有功能。JSX代码编译后产生React元素,你可以把这些元素看成页面内容的描述,React会将这些元素转换为DOM元素并插入到DOM树中。
JSX本质上是函数 React.createElement(component, props, ...children) 的语法糖。例如下面的JSX:
1 2 3 |
<MyButton color="blue" shadowSize={2}> Click Me </MyButton> |
会编译成以下纯JS代码:
1 2 3 4 5 |
React.createElement( MyButton, { color: 'blue', shadowSize: 2 }, 'Click Me' ); |
使用花括号包围后,可以把任意JavaScript表达式嵌入到JSX中:
1 2 3 4 5 6 7 8 |
const user = { firstName: 'Alex', lastName: 'Wong' }; // 这里使用括号,避免JavaScript的自动分号插入 const element = ( <h1> Hello, {((u)=>{return u.firstName + ' ' + u.lastName})(user)}! </h1> ); |
这个例子中,我们在JSX中嵌入了一个复杂的箭头函数调用表达式。在编译后,上述代码会变成合法的JavaScript代码。
嵌入的表达式也可以作为HTML标签的属性,注意此时不需要引号:
1 2 3 |
const element = <div tabIndex="0"></div>; // 属性值不需要引号包围 const element = <img src={user.avatarUrl}></img>; |
需要注意:
- JSX使用驼峰式大小写的HTML属性名,而不是HTML中的全小写风格。此外部分属性名有变动。例如class属性在JSX中对应className;tabindex则对应tabIndex
- JSX自动防止注入(XSS)攻击,也就是说你不需要自己处理HTML特殊字符的转义
在编译后,JSX会变成一个普通的JavaScript对象——React元素对象,这意味着你可以像使用任何对象一样使用它。
在使用Babel编译后,JSX会变成 React.createElement() 调用,下面的两段代码是等价的:
1 2 3 4 5 6 7 8 9 |
const element = ( <h1 className="greeting">Hello, world!</h1> ); const element = React.createElement( 'h1', { className: 'greeting' }, 'Hello, world!' ); |
JSX标签的名称,决定了React元素的类型。大写字母开头的名称意味着JSX标签对应了React组件,这种情况下,标签名直接作为组件类名称使用,这意味着在目标组件必须位于当前Scope中;小写子母开头的名称则被当作HTML标签名看待,不会解析为组件类。由于JSX代码被编译为React.createElement调用,所以React库也必须位于当前Scope中。
JSX标签名可以使用点号导航语法来引用位于任何名字空间中的组件。
如果React组件没有按照首字母大写的风格命名,你可以将其赋值给任意变量,然后以此变量名作为标签名:
1 2 3 4 |
function Story( props ) { const SpecificStory = components[ props.storyType ]; return <SpecificStory story={props.story}/>; } |
只要以 { } 包围,任何JavaScript表达式都可以作为JSX元素属性。但是分支、循环等控制语句不属于表达式的范畴,不能直接作为JSX元素属性。
如果传入字符串直接量作为属性,则该字符串中HTML特殊字符会被自动转义:
1 2 3 |
<MyComponent message="<3" /> // 等价于: <MyComponent message={'<3'} /> |
如果不为属性提供值,则其默认值是true:
1 2 3 |
<MyTextBox autocomplete /> // 等价于: <MyTextBox autocomplete={true} /> |
可以使用展开操作符,把对象中的属性都设置给组件:
1 |
<Greeting {...props} /> |
应当避免使用展开操作符,这会让你倾向于把很多无关的属性设置给组件。
可以为JSX元素提供子节点,不同类型的子节点——文本节点、HTML元素、React组件可以混合使用。
编写容器类组件时,你可以让客户端决定子元素(集合)是什么,客户端只需要提供 props.children 属性即可。
任何JavaScript表达式都可以作为子节点,例如:
1 2 3 4 5 6 |
// 普通表达式: <MyComponent>{'foo'}</MyComponent> // 函数调用: <ul> {todos.map((message) => <Item key={message} message={message} />)} </ul> |
甚至,作为组件的编写者,你可以提供一个回调函数,此回调函数会应用到props.children的每一个成员:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function ListOfTenThings() { return ( <Repeat numTimes={10}> // 提供一个回调函数 {( index ) => <div key={index}>This is item {index} in the list</div>} </Repeat> ); } function Repeat( props ) { let items = []; for ( let i = 0; i < props.numTimes; i++ ) { items.push( props.children( i ) ); } // 传入被回调函数处理的children return <div>{items}</div>; } |
false、null、undefined、true是合法的子节点,但是不会做任何渲染。
元素是React应用的最小组成块(building blocks),它描述你能够在屏幕上看见的东西。
与浏览器的DOM元素不同,React元素是简单对象,其创建/修改的成本很低。React负责监控元素的变动,并将其同步到DOM树中。
你需要提供一个渲染React元素的容器:
1 |
<div id="root"></div> |
通常,上述元素作为body的唯一子元素。该元素被称为根DOM节点,因为其内部的一切,都将被React DOM管理。
要把React元素渲染到上述节点中,需要调用react-dom模块提供的render函数:
1 2 3 4 5 6 |
const element = <h1>Hello, world</h1>; // 第一个参数是被渲染的React元素,第二个参数是容器DOM元素 ReactDOM.render( element, document.getElementById('root') ); |
React元素是不可变(immutable)对象,一旦被创建,其子元素、属性都不能再改变。一个元素就像电影的一帧,它描述了UI在某个瞬间的样子。根据现有的知识,为了更新UI,唯一的办法是创建一个新元素,然后再次调用ReactDOM.render()。
这样做好像效率很低下?并非如此,我们前面提到过React元素是轻量的,而React再重新渲染时,会比较现有元素和即将渲染的新元素,仅仅其中不同的部分,对应浏览器DOM才会改变。
在React的哲学看来,思考UI在任意时刻应该长什么样子,而不是思考如何随着时间去修改UI,可以减少很多BUG。
注意:在实践中,ReactDOM.render()通常只会调用一次。后续章节你会知道其原因。
1 |
ReactDOM.unmountComponentAtNode( document.getElementById('root') ); |
属性(props)、状态(state)、引用(refs)是React组件的三大核心特质。
使用组件,你可以把UI分割到多个相互独立的、可重用的片断中。就像JavaScript函数一样,组件也接收一系列输入——所谓属性(props),其“返回值”则是描述UI的React元素集。
React支持以两种风格来定义组件,首先是函数式:
1 2 3 |
function Welcome( props ) { return <h1>Hello, {props.name}</h1>; } |
上述函数可以作为合法的React组件,因为它:1、接受单个属性对象作为参数;2、返回React元素。
使用TypeScript的时候,可以使用如下方式声明函数式组件:
1 2 3 4 5 6 7 8 9 10 |
import {FC} from "react"; interface IProps { name: string; age?: int; } export FC<IProps> = ({name, age}) => { return </> } |
下面则是类式风格的组件,它使用ES6的class语法:
1 2 3 4 5 |
class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } } |
类式组件具有一些额外的特性,例如状态(State),后续讲述。
注意:组件名应该总是以大写子母开头。
在前面,我们了解了代表了一个DOM标签的React元素:
1 |
const element = <div />; |
实际上,React元素也可以代表一个用户定义的组件,此时,组件的名称作为元素的表签名:
1 |
const element = <Welcome name="Alex" />; |
一旦React遇到代表用户组件的元素,就会把JSX的属性(Attributes)组合成一个对象,作为props传递给目标组件。
既然组件是可重用的UI片断,很自然的React支持将多个组件(实例)组合到一起,形成更大的组件。这样,你可以使用组件来抽象任意级别的UI,从按钮、输入框到对话框、登录页。
组件可以在其输出(渲染结果)中包含其它的组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function App() { return ( <div> <Welcome name="Alex"/> <Welcome name="Meng"/> <Welcome name="Cai"/> </div> ); } ReactDOM.render( <App />, document.getElementById( 'root' ) ); |
注意:组件总是必须返回单个元素,尽管其内部可以包含任意层级的其它元素。
不要害怕把你的大组件重构为多个小组件的组合,事实上,React鼓励这样的分解,以提高组件的可重用性。
考虑下面这个评论组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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> ); } |
这个组件包含了三个部分:用户信息、评论文本、评论日期。其中用户信息由头像、名称两部分内容。
要修改此组件是比较困难的,因为所有东西都嵌套在一起。而且也不易于重用。实际上,头像部分的代码是很适宜重用的:
1 2 3 4 5 6 7 8 |
function Avatar( props ) { return ( <img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} /> ); } |
上面把评论组件的头像部分抽取出来,因为头像不一定非要在评论中才被用到。更进一步,我们把用户信息也抽取为独立的组件:
1 2 3 4 5 6 7 8 9 10 |
function UserInfo( props ) { return ( <div className="UserInfo"> <Avatar user={props.user}/> <div className="UserInfo-name"> {props.user.name} </div> </div> ); } |
重构完毕后的评论组件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function Comment( props ) { return ( <div className="Comment"> <UserInfo user={props.author}/> <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate( props.date )} </div> </div> ); } |
当UI会被使用多次,或者一个UI本身足够复杂,就应该考虑重构并抽取子组件。越是大型的应用越能够从中获益。
当一个函数不去修改其输入,并且相同的输入总是返回相同的值时,称为纯(pure)函数。
不管你以函数还是类的形式声明组件,永远不要尝试修改其props。React要求组件的行为类似于纯函数。那么这是不是意味着组件一经创建就无法改变?如果是的话岂不是毫无灵活性。
实际上,React引入状态这一概念来解决上述问题。状态(State)允许React组件响应用户操作、网络响应或者定时器,然后改变自己的输出,与此同时,不违反纯函数的规则。
考虑一个JavaScript时钟的例子:
1 2 3 4 5 6 7 |
function Clock(props) { return ( <div> <h1>{props.date.toLocaleTimeString()}</h1> </div> ); } |
以我们现在掌握的知识,要让其每秒刷新,只能定期重新渲染组件:
1 |
setInterval( () => ReactDOM.render( <Clock date={new Date()}/>, document.getElementById( 'root' ) ), 100 ); |
这种实现方式在职责划分上有问题——定期刷新UI以更新时间,应该是时钟这个组件自己的职责,而不是其用户的。
要把定时器逻辑封装到组件内部,需要为Clock组件添加状态。状态类似于props,但是状态是私有的,完全由组件自己去控制。只有类式组件才能支持状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
export default function Demo() { // useState返回一个数组,只有两个元素 // 元素1 为状态,元素2 为更新状态的方法 // 第一次调用时会将count进行存储,重复渲染不会重置count数据 const [count, setCount] = React.useState(0); // 初始值赋为 0 const [name, setName] = React.useState("Tom"); function add(){ // 进行状态赋值 // setCount(count + 1); // 写法1,直接将原来的状态值覆盖 setCount(count=> count+1); // 写法2,参数为函数,接受原本的状态值,返回新的状态值,覆盖原来的状态 } return ( <div> <h3>名字:{name}</h3> <h2>当前求和:{count}</h2> <button onClick={add}>点击加一</button> </div> ) } |
首先需要把函数式组件改为类式的:
- 创建同名的ES6类
- 将函数体转移到render()方法中
- 将嵌入表达式中的props改为this.props
修改结果如下:
1 2 3 4 5 6 7 8 9 |
class Clock extends React.Component { render() { return ( <div> <h1>{this.props.date.toLocaleTimeString()}</h1> </div> ); } } |
下一步,添加状态字段,因为时间的变化应该由组件内部管理,因此它应该作为状态的一部分(私有):
1 |
<h1>{this.state.date.toLocaleTimeString()}</h1> |
然后,添加一个构造函数,设置一个初始状态:
1 2 3 4 |
constructor( props ) { super( props ); // 所有组件必须如此调用构造函数 this.state = { date: new Date() }; } |
最后,客户端代码不需要提供时间:
1 |
ReactDOM.render( <Clock />, document.getElementById( 'root' ) ); |
在由很多组件构成的大型应用程序中,销毁组件的同时释放它们占用的资源特别重要。在时钟例子中:
- 每当Clock组件被渲染到DOM中,我们都需要创建一个定时器对象,在React的术语中,称组件渲染为挂载(mounting)
- 每当Clock组件被销毁时,我们则需要将定时器对象清除,在React的术语中,称组件销毁为卸载(unmounting)
如果不在组件卸载的同时,正确的进行资源清理(例如时钟里的定时器就是个资源),将会导致内存泄漏。好在,组件提供了一系列的生命周期钩子(lifecycle hooks),我们可以提供这些钩子,以便在组件生命周期的对应阶段执行特定的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 组件的输出被渲染到DOM中后调用 componentDidMount() { // 除了props、state,你可以自由的给组件添加任何的字段 this.timerID = setInterval( () => this.tick(), 100 ); } // 在组件从DOM移除之前调用 componentWillUnmount() { // 销毁资源 clearInterval( this.timerID ); } |
下面,实现tick方法:
1 2 3 4 5 |
tick() { this.setState( { date: new Date() } ); } |
现在,打开浏览器页面,你可以看到时钟可以正常刷新了。
这里了解一下时钟应用的工作流程:
- 调用ReactDOM.render()时,传递 <Clock /> ,React因而创建一个Clock组件。在构造器中初始化了组件的状态
- React调用Clock组件的render方法,然后React更新DOM结构,使其匹配该方法的输出
- 当DOM结构更新后,生命周期钩子componentDidMount被调用,因而定时器启动
- 每隔100ms,浏览器会调用组件的tick方法,后者调用setState改变了组件的状态
- 一旦setState()被调用,React就知道组件的状态发生了改变,需要更新UI了
- React再次调用组件的render方法,并检查DOM的哪个部分修改改变,最终修改了h1元素的文本区域
- 如果Clock组件被移除,则生命周期钩子componentWillUnmount被调用,定时器被取消
关于组件状态,你需要知道:
- 不要直接读写state字段,应该调用setState()方法。你仅仅需要在构造器中写入state字段,来建立初始状态
- 状态更新可以是异步的,出于性能的考虑,React可能把多个setState()调用合并为单个状态更新。由于this.props、this.state的值可能异步的更新,因此你不能依赖于它们的值来计算下一个状态。下面是一个错误的示例:
1234this.setState({// 依赖了当前值counter: this.state.counter + this.props.increment,});要解决这种依赖于“前一个”值的场景,你需要另外一种形式的调用:
1234// 有效的“前一个”状态,作为入参,自动传递给你提供的回调函数this.setState( ( prevState, props ) => ({counter: prevState.counter + props.increment}) ); - 新状态的属性是合并到老状态(使用浅覆盖的方式)中的,而不是替换为新对象:
12345678910111213componentDidMount() {fetchPosts().then(response => {this.setState({posts: response.posts});});fetchComments().then(response => {this.setState({comments: response.comments});});}上面的例子,两个调用分别替换了组件状态的posts、comments字段
不管是组件C的父组件,还是子组件,都不知道C是无状态的还是有状态的。也不应该关心C是函数式还是类式的。状态属于组件实现的内部细节,因此也叫本地(local)状态或者封装(encapsulated)状态。
你可以向子组件的props传递当前组件的状态:
1 2 |
<FormattedDate date={this.state.date} /> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> |
但是:
- 子组件不知道其属性是来自父组件的状态,还是其它地方
- 子组件绝不能向父组件传递自己的状态,React没有这种机制
这种状态/数据的流向,就是React著名的自上而下、单向数据流—— 状态总是被某个特定组件拥有,而基于此状态的数据或者UI只能影响更下层的组件。整个组件树就好像一条瀑布,而每个组件的状态就像是瀑布中途加入的水流一样。
调用setState()后,React会自动调用组件的render()。
还有一种修改状态的方法,就是 forceUpdate,意思是强制更新,即强制更新状态,其入参是更新状态完成后的回调函数:
1 |
this.forceUpdate([callback]); |
在React中处理事件,与处理DOM事件很相似,但是需要注意一些语法区别:
- React的事件类型,使用驼峰式大小写,且首字母小写
- 使用JSX时,传递函数(给元素属性)作为事件处理器,而不是字符串:
123456789// 寻找当前组件的方法、或者上下文中的变量作为事件处理器<button onClick={activateLasers}>Activate Lasers</button>// 寻找window对象的activateLasers属性作为事件处理器<button onclick="activateLasers()">Activate Lasers</button> - 在React中你不能通过
return false 来阻止浏览器默认行为,你必须显式的调用preventDefault:
123456789function ActionLink() {function handleClick( e ) {// 显式调用e.preventDefault();}return (<a href="#" onClick={handleClick}>Link</a>);} - 事件处理器的参数是一个合成(synthetic)的事件对象,而不是浏览器Native事件对象
当你使用ES6类语法时,通常都把事件处理器作为组件的方法来声明。例如下面这个按钮组件:
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 Toggle extends React.Component { constructor( props ) { super( props ); this.state = { isToggleOn: true }; // this指向问题,方式一:确保handleClick方法中的this总是指向当前组件 this.handleClick = this.handleClick.bind( this ); } handleClick() { // 用户点击后,改变组件状态,自然导致UI更新 this.setState( prevState => ({ isToggleOn: !prevState.isToggleOn }) ); } // this指向问题,方式二:利用箭头函数改变this指向(总是指向定义箭头函数所在地方的this),推荐 handleClick = ()=> { // 用户点击后,改变组件状态,自然导致UI更新 this.setState( prevState => ({ isToggleOn: !prevState.isToggleOn }) ); } render() { return ( // 如果不bind,handleClick被调用时,this将为undefined <button onClick={this.handleClick}> {this.state.isToggleOn ? 'ON' : 'OFF'} </button> ); } } |
你可能觉得每次都要bind太麻烦,Babel提供了一个试验特性,添加好Babel插件后,你可以这样写:
1 2 3 4 5 6 |
// 属性初始化语法,确保this总是绑定到当前组件 handleClick = () => { this.setState( prevState => ({ isToggleOn: !prevState.isToggleOn }) ); } |
不过这种语法目前不能被IDE、ESLint等很好的支持。 create-react-app默认支持该语法。
你也可以在JSX中使用箭头函数,箭头函数天然绑定变量到定义它的地方:
1 |
<button onClick={(e) => this.handleClick(e)}> |
这种方式的缺点是,组件每次被Render时,处理器回调都被重新创建一次。当前组件作为子组件(例如Grid组件的Row)且使用父组件的方法时(通过props传递),这可能是个问题。
- 通过 onXxx属性指定事件处理函数(注意大小写)
- React使用的是自定义(合成)事件,而不是原生的 DOM事件(为了更好的兼容性)
- React的事件是通过事件委托方式处理的(委托给组件最外层的元素)(为了更加的高效)
- 可以通过事件的 event.target获取发生事件的DOM元素对象,可以尽量减少 refs的使用
- 在绑定事件的时候不要加括号,这会被 JSX识别为执行函数
通过前面的学习,我们知道在React中你可以创建独特的组件,并在其中封装你需要的行为。
React支持所谓条件渲染,就像JavaScript的分支控制语句那样,对组件状态进行if判断,并且决定输出内容。考虑下面这个登录组件:
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 |
class LoginControl extends React.Component { constructor( props ) { super( props ); this.handleLoginClick = this.handleLoginClick.bind( this ); this.handleLogoutClick = this.handleLogoutClick.bind( this ); this.state = { isLoggedIn: false }; } handleLoginClick() { this.setState( { isLoggedIn: true } ); } handleLogoutClick() { this.setState( { isLoggedIn: false } ); } render() { const isLoggedIn = this.state.isLoggedIn; let button = null; // 条件渲染,根据状态的不同: if ( isLoggedIn ) { button = <LogoutButton onClick={this.handleLogoutClick}/>; } else { button = <LoginButton onClick={this.handleLoginClick}/>; } return ( <div> <Greeting isLoggedIn={isLoggedIn}/> // 任意变量可以插入到JSX中,这里插入React元素 {button} </div> ); } } ReactDOM.render( <LoginControl />, root ); |
利用条件渲染,这个组件同时承担了登录按钮、注销按钮的功能。
你可以在JSX中嵌入任意JavaScript表达式,甚至是JSX本身,有些时候这会带来便利:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function Mailbox( props ) { const unreadMessages = props.unreadMessages; return ( <div> <h1>Hello!</h1> // 使用短路机制 {unreadMessages.length > 0 && <h2> You have {unreadMessages.length} unread messages. </h2> } </div> ); } |
使用过于复杂的条件渲染,往往是需要重构的指征。
某些特殊的条件下,你可能期望组件隐藏自己,即使该组件是由其它组件来引用的。此时,你只需要让render()函数或者函数式组件本身 return null 即可。
你可以创建元素的集合,然后通过花括号,将其包含在JSX中,就像普通的表达式那样。例如下面这个例子:
1 2 3 4 5 6 7 |
// 这个是数据集 const numbers = [ 1, 2, 3, 4, 5 ]; const listItems = numbers.map( ( number ) => // 这个是模板 <li>{number}</li> ); // 结果是元素集 |
你只需要将上面的列表条目集合,添加到一个列表元素中,即可完成渲染:
1 2 3 4 |
ReactDOM.render( <ul>{listItems}</ul>, document.getElementById( 'root' ) ); |
将上面的列表渲染逻辑封装为组件也很简单:
1 2 3 4 5 6 7 8 9 10 11 |
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( 'root' ) ); |
在开发模式下运行上述组件,会得到如下提示:
1 |
Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `NumberList` |
React要求列表条目具有唯一的key属性,所谓key是一个字符串属性,你应该提供它以标识集合中的条目:
1 |
const listItems = numbers.map( ( number ) => <li key={number.toString()}>{number}</li> ); |
React依赖于Key来识别列表的哪一个条目被改变、添加、或者删除。集合元素总是应该具有Key属性,并且在兄弟节点之间具有唯一性。 最常见的做法,是将来自数据模型的ID作为Key。如果这样的ID,可以使用元素的索引值:
1 2 3 |
const todoItems = todos.map((todo, index) => <li key={index}>{todo.text}</li> ); |
如果列表元素的顺序可能变化,则最好不要使用索引值作为Key,可能会导致性能问题。
键只有在集合的上下文中指定,才能生效。 如果你有列表、条目两个组件,则必须在列表组件迭代输出条目的时候指定key属性,在条目组件内指定key毫无意义:
1 2 3 4 |
function Item( props ) { // 无效写法 return <li key={props.id}></li> } |
正确的写法应该是:
1 2 3 4 5 6 7 |
function List( props ) { let items = props.items.map( // 正确写法 ( item, index ) => <Item key={item.id}/> ); return <ul>{items}</ul>; } |
在React中,HTML表单元素与其它DOM元素的行为有所不同,这是因为表单元素天然的维持了一些内部状态(并且在状态变化时自动更新UI)。考虑下面的HTML片断:
1 2 3 4 |
<form> <label>Name:<input type="text" name="name"/></label> <input type="submit" value="Submit"/> </form> |
这个表单具有这样的默认行为:当用户点击提交时,导航到一个新的页面。
你当然可以在React中使用此默认行为,但是在更多的情况下,使用JavaScript函数还处理表单提交、访问表单字段更加便利。 实现此基于JS的处理的标准途径是所谓受控组件(controlled components)。
在HTML中,各种表单元素维持内部状态并且在输入变化时自动更新UI。在React中,可变化的状态一般都是放置在私有的状态字段中的,而UI更新则由setState()触发。
我们可以联合这两种UI更新机制,并且把React状态变化作为UI更新的唯一原因。渲染表单的React组件控制用户输入改变时表单会发生什么。值被React控制的表单元素,称为受控组件。
将前文的表单改写为React组件并使用受控组件:
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 |
class NameForm extends React.Component { // 事件处理器将用户输入转变为React组件的状态变化 handleChange = ( event ) => this.setState( { value: event.target.value } ); handleSubmit = ( event ) => { // 处理表单元素的值 console.log( this.input.value ); event.preventDefault(); } constructor( props ) { super( props ); this.state = { value: '' }; } render() { return ( // 通过onSubmit事件处理器受控 <form onSubmit={this.handleSubmit}> <label> // 通过onChange事件处理器受控 // 表单元素的值绑定到React组件状态 Name:<input type="text" value={this.state.value} onChange={this.handleChange}/> </label> <input type="submit" value="Submit"/> </form> ); } } |
在用户输入改变时, 上述组件基于事件监听器自动的更新组件状态,而组件状态的更新会导致UI刷新。从效果上看,好像是组件状态变化是UI刷新的(唯一)根源似的。
使用受控组件后,表单元素的值(对应组件的一个状态字段)总是关联一个事件处理器,因此在事件处理器中验证或者修改用户输入是很自然的:
1 2 3 4 |
handleChange(event) { // 修改用户输入为大写 this.setState({value: event.target.value.toUpperCase()}); } |
基于受控组件机制处理表单元素时,总是绑定容器元素的value属性到组件状态的特定字段:
1 2 3 4 5 6 7 8 9 10 |
// 文本区,绑定value属性 <textarea value={this.state.value} onChange={this.handleChange} /> // 下拉列表,绑定父元素的value属性 <select value={this.state.value} onChange={this.handleChange}> <option value="grapefruit">Grapefruit</option> <option value="lime">Lime</option> <option value="coconut">Coconut</option> <option value="mango">Mango</option> </select> |
某些情况下使用受控组件可能导致啰嗦的代码,因为你需要为每个表单元素编写事件处理器并将其关联到状态字段,在把既有的代码转换为React组件时这种情况可能特别严重。此时你可以考虑使用非受控组件(uncontrolled components)。
React推荐在大部分情况下通过受控组件来实现表单。在受控组件中,表单数据由React组件来处理,对于每个表单元素的值变更,都需要注册事件处理器并转换为组件的状态变更。
另外一个备选的方案是非受控组件,这种组件通过ref从DOM中获得表单字段的取值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class NameForm extends React.Component { handleSubmit = ( event ) => { // 处理表单元素的值 console.log( this.input.value ); event.preventDefault(); } constructor( props ) { super( props ); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Name: <input type="text" ref={( input ) => this.input = input}/></label> <input type="submit" value="Submit"/> </form> ); } } |
非受控组件将DOM作为数据的源头,某些时候易于将React与非React代码进行集成。非受控组件的代码编写起来可能快但是违反React的理念。
非受控组件必须依赖于(主要是提交时的)pull操作,让数据和UI手工同步。而受控组件基于事件机制,将表单元素的值变化push到组件,这样就可以保持数据(组件状态)和UI(表单元素)的同步状态。这种自动同步,会让很多逻辑实现起来更加简单:
- 立即的反馈,例如数据验证
- 禁用某些按钮,除非表单数据合法
- 方便进行强制的输入限制,例如信用卡号
在典型的React组件生命周期中,表单元素的value属性的值,会覆盖通过DOM指定的值。当使用非受控组件时,你通常需要指定一个初始的默认值,并将后续的值更新交给非受控组件自己管理:
1 2 3 4 |
<input defaultValue="Bob" type="text" /> <input defaultChecked="true" type="checkbox" /> <input defaultChecked="true" type="radio" /> <select defaultValue="Bob"></select> |
有时候,几个组件可能需要反映同一个数据项(状态字段),此时最好将此状态字段提升到最近的共同祖先组件中。考虑下面这个温度计算器的例子,它能够计算在特定的温度下,水能否烧开:
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 |
const scaleNames = { c: 'Celsius', f: 'Fahrenheit' }; function toCelsius( fahrenheit ) { return (fahrenheit - 32) * 5 / 9; } function toFahrenheit( celsius ) { return (celsius * 9 / 5) + 32; } function tryConvert( value, convert ) { const input = parseFloat( value ); if ( Number.isNaN( input ) ) { return ''; } const output = convert( input ); const rounded = Math.round( output * 1000 ) / 1000; return rounded.toString(); } // 温度输入控件 class TemperatureInput extends React.Component { constructor( props ) { super( props ); } // 关键:将事件处理函数委托给父组件提供的回调 handleChange = ( e ) => this.props.onChange( e.target.value ); render() { // 关键:这个状态字段用父组件提供 const value = this.props.value; const scale = this.props.scale; return ( <fieldset> <legend>Enter temperature in {scaleNames[ scale ]}:</legend> <input value={value} onChange={this.handleChange}/> </fieldset> ); } } // 沸腾判断组件 function BoilingVerdict( props ) { if ( props.celsius >= 100 ) { return <p>The water would boil.</p>; } return <p>The water would not boil.</p>; } // 父组件 class Calculator extends React.Component { handleCelsiusChange = ( value ) => this.setState( { scale: 'c', value } ); handleFahrenheitChange = ( value ) => this.setState( { scale: 'f', value } ); constructor( props ) { super( props ); this.state = { value: '', scale: 'c' }; } render() { const scale = this.state.scale; const value = this.state.value; // 每次渲染时,都重新计算两个输入框的值,以便更新子组件 const celsius = scale === 'f' ? tryConvert( value, toCelsius ) : value; const fahrenheit = scale === 'c' ? tryConvert( value, toFahrenheit ) : value; return ( // 两个温度输入框组件的事件处理器委托到父组件的方法,这是因为单向数据流下,子组件不知道父组件的信息 // 这两个回调函数,都会导致父组件的状态改变,进而导致重新渲染 <div> // 这里的onChange仅仅是传递props,和DOM事件处理没有关系 <TemperatureInput scale="c" value={celsius} onChange={this.handleCelsiusChange}/> <TemperatureInput scale="f" value={fahrenheit} onChange={this.handleFahrenheitChange}/> <BoilingVerdict celsius={parseFloat( celsius )}/> </div> ); } } ReactDOM.render( <Calculator/>, root ); |
可以看到,为了方便的共享状态,TemperatureInput把两个状态字段scale、value交由父组件管理。同时,为了让子组件中发生的用户输入能够触发父组件的状态改变,则需要父组件提供合适的事件处理器。
在React应用程序中,任何变化了的数据都应该只有一个来源(source of truth)。通常,状态字段被加入到组件中,如何渲染依赖于此字段。如果其它组件也需要同一字段,就应该将字段提升到两个组件的共同祖先。你应该依赖于React自上而下的数据流,而不是手工在多个组件之间同步数据。
比起AngularJS、ExtJS 5等支持双向数据绑定的框架,React的状态提升会让你编写一些乏味的样码(boilerplate)代码 :
- 子组件需要将事件处理函数代理给父组件提供的回调
- 父组件需要将共享状态通过props传递给子组件
但是,状态提升可以让你更容易发现并定位BUG,因为UI的变化都使用状态变化导致的,每个状态只属于一个组件,只有该组件才能改变它,因此只需要定位到状态、组件就找到BUG分析的入口点了。
只要某个变量可以由其它prop或者state推导出来,它就不应该属于状态。上例中,我们没有让两个输入框分别持有celsiusValue、fahrenheitValue这两个状态,这是因为它们可以相互推导——你输入一个,另外一个值应该自动改变。
React推荐尽可能使用强大的组合,而不是使用继承。本节内容引入一些场景,在这些场景下React新手往往采用继承而不是组合解决问题。
某些类型的组件不能提前知道其内部需要容纳什么其它组件,典型的例子是通用容器——对话框、侧边栏等。
对于这类容器,React推荐它们接受一个名为 children 的特殊属性,允许容器的使用者传入任意的其它组件,而容器则直接将其写入到输出中:
1 2 3 4 5 6 7 |
function FancyBorder( props ) { return ( <div className={'FancyBorder FancyBorder-' + props.color}> {props.children} </div> ); } |
该组件的使用者可以这样直白的传入children:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function WelcomeDialog() { return ( <FancyBorder color="blue"> // 这两个React元素作为children <h1 className="Dialog-title"> Welcome </h1> <p className="Dialog-message"> Thank you for visiting our spacecraft! </p> </FancyBorder> ); } |
之前我们没有这样做过——调用组件时,为其添加子元素。其实这种写法就是给children这个特殊属性赋值 。就好像{props.children}是FancyBorder组件上的一个“洞”一样,允许使用者将其填充。
虽然不多见,但是组件上可以有多个洞。此时你就需要自定义洞属性的名字了:
1 2 3 4 5 6 7 8 |
function SplitPane( props ) { return ( <div className="SplitPane"> <div className="SplitPane-left">{props.left}</div> <div className="SplitPane-right">{props.right}</div> </div> ); } |
使用者可以这样填充每个洞:
1 2 3 4 5 6 7 8 9 10 11 12 |
function App() { return ( <SplitPane left={ <Contacts /> } right={ <Chat /> } /> ); } |
某些时候,我们认为一个组件是其它组件的特殊情况。例如我们可以认为WelcomeDialog是Dialog的特例。这种特例也不必非要使用继承来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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!"/> ); } |
在典型的React数据流中,props是父子组件之间进行通信的唯一办法。为了修改子组件,你必须基于新的props来重新渲染它。
特殊情况下,你需要在React数据流之外,强制的修改某个React组件或者DOM元素。React提供支持这种修改的API。
早期方式,不再推荐使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Demo extends React.Component{ showData(){ // 通过自己设定的 refs名称获取对应的元素 const {myInput} = this.refs; // 返回该元素 alert(myInput.value) } render(){ return ( <div> { /* 通过 ref属性绑定input元素到this.refs.myInput */ } <input type="text" ref="myInput" placeholder="search something" /> </div> ) } } |
React支持一个可以附加到任何元素的特殊属性ref,该属性的值是一个回调函数,当组件挂载、卸载后,此回调会立即执行。
当ref用在HTML元素上时,回调的入参是目标DOM元素,此时你可以把目标DOM元素设置为组件的属性。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class CustomTextInput extends React.Component { constructor( props ) { super( props ); this.focus = this.focus.bind( this ); } focus = () => { // 使用DOM API来明确的获得焦点 this.textInput.focus(); } render() { return ( <div> // 当组件挂载时,以input元素作为入参;当组件卸载时,以null作为入参 // 设置当前组件的属性为DOM元素 <input type="text" ref={( input ) => { this.textInput = input; } }/> <input type="button" value="Focus the text input" onClick={this.focus} /> </div> ); } } |
当ref用在自定义React组件上时,其入参是目标(被挂载的)组件。例如:
1 2 3 4 5 6 7 8 9 10 11 |
class AutoFocusTextInput extends React.Component { componentDidMount() { // 父组件挂载后,调用CustomTextInput的focus()方法 this.textInput.focus(); } render() { return ( <CustomTextInput ref={(input) => { this.textInput = input; }} /> ); } } |
在函数式组件中,可以这样使用ref属性:
1 2 3 4 5 6 7 8 9 10 11 |
function CustomTextInput(props) { let textInput = null; function handleClick() { textInput.focus(); } return ( <div> <input type="text" ref={(input) => { textInput = input; }} /> </div> ); } |
这是目前推荐的方式,解决回调方式的一些问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
export default class BPMNViewer extends React.Component { constructor(props) { this.containerRef = React.createRef(); } componentDidMount() { const container = this.containerRef.current; } render() { return ( <div className="bpmn-viewer-container" ref={this.containerRef}></div> ); } } |
1 2 3 4 5 6 7 8 9 10 11 |
export default function WorkflowViewerExample() { let bpmnViewer = React.useRef(); function shade() { let viewer = bpmnViewer.current.viewer; bpmnViewer.current.props.width=100; } return ( <BPMNViewer ref={bpmnViewer} /> ) } |
使用React可以很方便通过组件来跟踪数据流,你创建新组件的时候可以看到传递的props,而状态更新也往往由用户交互来触发并在作为组件实例方法的回调中处理。
某些情况下,你期望在整个组件树中使用数据结构,而不是手工的逐层传递,此时可以使用React的上下文API。
尽管提供了上下文API,大部分应用程序并不应该使用它。这是因为上下文倾向于破坏应用程序的稳定性,并且可能在未来版本的React中失效。
Redux或者MobX之类的状态管理库的React绑定可能更加适合管理与多个组件相关的状态。
下面是一个消息列表组件,使用上下文来为每个按钮设置背景色:
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 |
// 类式组件与上下文 class Button extends React.Component { render() { return ( // 从上下文读取背景色 <button style={{ background: this.context.color }}>{this.props.children}</button> ); } } // 函数式组件与上下文 const Button = ( { children }, context ) => { <button style={{ background: context.color }}> {children} </button>; } // 任何在contextTypes中定义了数据项的子组件,都可以访问祖先组件传递来的上下文 // 如果不指定contextTypes,则this.context是空对象 Button.contextTypes = { color: React.PropTypes.string }; class Message extends React.Component { render() { return ( <div>{this.props.text} <Button>Delete</Button></div> ); } } class MessageList extends React.Component { // 为子树提供上下文 getChildContext() { return { color: "purple" }; } render() { const children = this.props.messages.map( ( message ) => <Message text={message.text}/> ); return <div>{children}</div>; } } // 为子树提供上下文 MessageList.childContextTypes = { color: React.PropTypes.string }; |
getChildContext()方法会在当前组件状态/属性变化后被调用,以生成新的上下文。
如果组件定义了静态属性contextTypes,则以下生命周期方法会接受context作为额外入参:
1 2 3 4 5 |
constructor(props, context) componentWillReceiveProps(nextProps, nextContext) shouldComponentUpdate(nextProps, nextState, nextContext) componentWillUpdate(nextProps, nextState, nextContext) componentDidUpdate(prevProps, prevState, prevContext) |
整体生命周期:
- 自身组件挂载
- constructor: 构造函数(初始化)
- componentWillMount: 组件挂载前
- render: 组件挂载中
- componentDidMount: 组件挂载完成后
- 拥有父组件且父组件状态进行更新(setState):
- 父组件 shouldComponentUpdate: 父组件是否进行状态更新:具有布尔类型返回值,而且默认为 true(不写该钩子时);具有入参nextProps,nextState分别表示尚未应用到组件的新的props和state
- 父组件 componentWillUpdate: 父组件状态更新前
- 父组件 render: 父组件更新挂载中
- 子组件 componentWillReceiveProps: 子组件将收到新的Props
- 子组件 shouldComponentUpdate: 子组件是否进行状态更新
- 子组件 componentWillUpdate: 子组件状态更新前
- 子组件 render: 子组件更新挂载中
- 子组件 componentDidMount: 子组件挂载完成
- 父组件 componentDidMount: 父组件挂载完成
- 当组件要进行卸载时:
- componentWillUnmount: 组件卸载前
setState时会经过生命周期 shouldComponentUpdate,但是使用 forceUpdate时则不会,会直接到 componentWillUpdate生命周期。
变化的地方:
在新的生命周期中,舍弃了(即将舍弃)三个生命周期函数:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
新增了两个生命周期函数:
- getDerivedStateFromProps
- getSnapshotBeforeUpdate
整体生命周期如下:
- 自身组件挂载:
- constructor: 构造函数(初始化)
- getDerivedStateFromProps: 从props中获取派生的state
- render: 组件挂载中
- componentDidMount: 组件完成挂载
- 父组件更新时
- 父组件 getDerivedStateFromProps: 从 props中获取派生的state
- 父组件 shouldComponentUpdate: 判断是否进行状态更新
- 父组件 render: 父组件挂载中
- 子组件 getDerivedStateFromProps: 从 props中获取派生的state
- 子组件 shouldComponentUpdate: 判断是否进行状态更新
- 子组件 render: 子组件挂载中
- 子组件 getSnapshotBeforeUpdate: 子组件获取状态更新前的快照
- 子组件 componentDidUpdate: 子组件完成更新
- 父组件 getSnapshotBeforeUpdate: 父组件获取状态更新前的快照
- 父组件 componentDidUpdate: 父组件完成更新
- 组件卸载时
- componentWillUnmount: 组件卸载前
要在React中实现组件挂载/卸载时的动画效果,可以使用ReactCSSTransitionGroup。下面是一个示例:
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 |
import ReactCSSTransitionGroup from 'react-addons-css-transition-group' class TodoList extends React.Component { constructor( props ) { super( props ); this.state = { items: [ 'hello', 'world', 'click', 'me' ] }; } handleAdd = () => { const newItems = this.state.items.concat( [ prompt( 'Enter some text' ) ] ); this.setState( { items: newItems } ); } handleRemove( i ) { let newItems = this.state.items.slice(); newItems.splice( i, 1 ); this.setState( { items: newItems } ); } render() { // 使用箭头函数以便传递i // 你必须为ReactCSSTransitionGroup的每个元素提供key,即使在只渲染单个元素的情况下 const items = this.state.items.map( ( item, i ) => <div key={item} onClick={() => this.handleRemove( i )}>{item}</div> ); return ( <div> <button onClick={this.handleAdd}>Add item</button> // 使用动画 <ReactCSSTransitionGroup transitionName="example" transitionEnterTimeout={500} transitionLeaveTimeout={300}> {items} </ReactCSSTransitionGroup> </div> ); } } ReactDOM.render( <TodoList/>, document.getElementById( 'root' ) ); |
在上例中,所有条目被添加到ReactCSSTransitionGroup元素的内部。这些条目会自动获得名为example的CSS变换:
- 当条目被加入时,样式类example-enter被应用,紧接着example-enter-active则被应用
- 当条目被移除时,样式类example-leave被应用,紧接着example-leave-active则被应用
样式定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
.example-enter { opacity: 0.01; } /* 500ms完成淡入 */ .example-enter.example-enter-active { opacity: 1; transition: opacity 500ms ease-in; } .example-leave { opacity: 1; } /* 500毫秒完成淡出 */ .example-leave.example-leave-active { opacity: 0.01; transition: opacity 300ms ease-in; } |
你会注意到,在CSS和JSX中同时定义了时间。这是因为两者的作用不同:前者用于控制动画效果,后者用于通知React何时从元素上移除动画样式类并从DOM中删除元素(仅leaving)。
除了上例中用到的transitionEnter、transitionLeave,你还可以提供额外(但是较少使用)的属性transitionAppear,以便在组件初始挂载时( initial mount)添加额外的CSS变换阶段:
1 2 3 4 5 6 7 8 9 10 11 12 |
render() { return ( <ReactCSSTransitionGroup transitionName="example" transitionAppear={true} transitionAppearTimeout={500} transitionEnter={false} transitionLeave={false}> <h1>Fading at Initial Mount</h1> </ReactCSSTransitionGroup> ); } |
样式类遵循类似的命名规则:
1 2 3 4 5 6 7 8 |
.example-appear { opacity: 0.01; } .example-appear.example-appear-active { opacity: 1; transition: opacity .5s ease-in; } |
在ReactCSSTransitionGroup初始挂载时,所有子元素将应用appear而不是enter动画,然后后续添加的组件则应用enter而不是appear动画。
React允许你自定义样式类的名称,不必遵循默认命名规则:
1 2 3 4 5 6 7 8 9 10 11 |
<ReactCSSTransitionGroup transitionName={ { enter: 'enter', enterActive: 'enterActive', leave: 'leave', leaveActive: 'leaveActive', appear: 'appear', appearActive: 'appearActive' } }> {item} </ReactCSSTransitionGroup> |
要给子元素添加CSS变换动画,你必须:
- 将ReactCSSTransitionGroup提前挂载到DOM结构中
- 或者,使用appear阶段
下面的例子无法工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
render() { const items = this.state.items.map((item, i) => ( <div key={item} onClick={() => this.handleRemove(i)}> <ReactCSSTransitionGroup transitionName="example">{item}</ReactCSSTransitionGroup> </div> )); return ( <div> <button onClick={this.handleAdd}>Add Item</button> {items} </div> ); } |
因为ReactCSSTransitionGroup总是和它唯一的子元素一起挂载。
ReactCSSTransitionGroup支持单个子元素,甚至没有子元素。 这样规定的目的是:
- 允许对单个元素的Entering/Levaing进行动画
- 在使用新元素替换当前元素时启用动画
示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
function ImageCarousel(props) { return ( <div> <ReactCSSTransitionGroup transitionName="carousel" transitionEnterTimeout={300} transitionLeaveTimeout={300}> <img src={props.imageSrc} key={props.imageSrc} /> </ReactCSSTransitionGroup> </div> ); } |
前面提到过,即使只有单个元素,也要提供key属性。在这里,key属性用来分辨是否同一张图片。只有图片不同时,才会替换为新的img元素进而触发Levaing/Entering动画。
某些情况下你需要禁用掉某些动画阶段,例如仅需要Entering动画而不需要Leaving动画。此时可以设置ReactCSSTransitionGroup元素的属性:
1 |
<ReactCSSTransitionGroup transitionEnter={true} transitionLeave={true} transitionAppear={false} /> |
注意:出于兼容性考虑,transitionAppear的默认值是false,另外两个默认值是true。
ReactTransitionGroup是React的加载项(add-on),它提供了低级别的动画API。要使用此API,需要导入:
1 |
import ReactTransitionGroup from 'react-addons-transition-group' |
当添加、删除ReactTransitionGroup的子元素时,子元素的下列生命周期回调会被依次调用:
1 2 3 4 5 6 7 8 |
// 添加时依次调用 componentWillAppear() componentDidAppear() componentWillEnter() // 删除时依次调用 componentDidEnter() componentWillLeave() componentDidLeave() |
ReactTransitionGroup组件本身会渲染为一个span标签,你可以指定为任何其它HTML标签或者React组件:
1 2 3 |
<ReactTransitionGroup component="ul"> {/* ... */} </ReactTransitionGroup> |
钩子函数(Hooks)是16.8引入的特性,允许你在编写函数式组件的同时,使用状态等React属性。
1 2 3 |
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]; const [state, setState] = useState(initialState); |
返回一个可变的有状态值,以及用于修改它的函数。在组件最初渲染时,状态为传入的 initialState。
函数用于修改状态,接受状态的新值,入队一个组件重渲染请求。该函数还可以接受一个回调,这样你可以依赖状态前值来计算新值:
1 2 3 |
const [count, setCount] = useState(initialCount); setCount(prevCount => prevCount - 1)}; |
要将初始状态计算,推迟到最初渲染时刻,可以传递回调给useState:
1 2 3 4 |
const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState; }); |
如果新旧状态相同(使用Object.is进行比较)则React不会触发子组件的渲染,或者触发effect。但是当前组件仍然会被重新渲染,由于React仅仅渲染变化部分的DOM的特性,这个操作成本不高。
1 |
function useEffect(effect: EffectCallback, deps?: DependencyList): void; |
接受一个命令式的、可能有副作用(effectful)的回调函数。
在函数式组件的主函数体中,不能具有数据修改、事件订阅、日志记录、定时器等存在副作用的代码,否则会导致难以定位的缺陷、UI渲染中的不一致现象。
要在这里进行带有副作用的命令式编程,可以使用useEffect,这种钩子在render已经提交给屏幕渲染之后,才会执行。
默认情况下,每次完成render之后,都会执行effect钩子,使用deps,则仅仅当依赖发生变化后,才执行。
通常effect钩子可能创建了一些资源(例如事件订阅、定时器ID),这些资源需要在组件销毁的时候清理掉。传递给useEffect的回调,可以返回一个清理函数,这个函数会在组件从UI中移除之前调用:
1 2 3 4 5 6 7 |
useEffect(() => { const subscription = props.source.subscribe(); return () => { // Clean up the subscription subscription.unsubscribe(); }; }); |
和componentDidMount、componentDidUpdate不同,传递给useEffect的函数,是在页面布局、绘制之后,在一个延迟的事件中执行的。这个延迟时刻非常适合进行具有副作用的操作,例如注册事件处理器,这些具有副作用的工作不应该阻塞浏览器的渲染。
但是,并非所有工作都能够延迟进行,例如对DOM的修改,必须在下一次绘制之前,同步的发起,否则影响用户体验。对于这类工作,可以使用useLayoutEffect钩子,此钩子和useEffect签名一致。
1 2 3 |
function useContext<T>(context: Context<T>): T; const value = useContext(XxxContext); |
该函数接受一个上下文对象,并且返回上下文的当前值。当前上下文的值,取决于最近(向上追溯)的 <XxxContext.Provider>。
该函数接受的对象,必须由React.createContext创建:
1 2 3 4 |
import React from 'react'; export default React.createContext<{ ... }>(null!); |
当最近的Provider更新后,useContext钩子会使用最新上下文的值进行重新渲染,这个渲染总是会发生。
完整的例子:
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 |
const themes = { light: { foreground: "#000000", background: "#eeeeee" }, dark: { foreground: "#ffffff", background: "#222222" } }; // 定义上下文 const ThemeContext = React.createContext(themes.light); function App() { return ( // 提供者 <ThemeContext.Provider value={themes.dark}> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } function ThemedButton() { const theme = useContext(ThemeContext); return ( // 当提供者的值变了,这里重新渲染 <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> ); } |
1 2 3 4 5 6 7 8 |
function useReducer<R extends ReducerWithoutAction<any>, I>( reducer: R, initializerArg: I, initializer: (arg: I) => ReducerStateWithoutAction<R> ): [ReducerStateWithoutAction<R>, DispatchWithoutAction]; const [state, dispatch] = useReducer(reducer, initialArg, init); |
在具有复杂状态逻辑的情况下,用于替换useState,其中reducer回调的格式为: (state, action) => newState。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); } |
1 2 3 4 5 6 7 8 |
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T; const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], ); |
返回一个 memoized版本的回调,这个回调仅仅在依赖发生变化时,才真正执行。
1 2 3 |
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T; const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); |
类似上面,但是返回的是一个值,这个值仅仅在依赖变化时才重新计算。入参是创建值的工厂函数。
1 2 3 4 5 6 7 |
function useRef<T>(initialValue: T): MutableRefObject<T>; interface MutableRefObject<T> { current: T; } const refContainer = useRef(initialValue); |
返回一个可变的引用对象,其current属性,初始化为initialValue。此钩子返回的对象,在组件的整个生命周期中保持一致。
useRef的一个重要用法是,命令式的访问一个子组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` points to the mounted text input element inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); } |
useRef在组件每次渲染时,返回的是同一个对象(但是current属性可能变了)。current属性的变化不会触发重新渲染。
React中的事件处理函数会接收到一个SyntheticEvent的实例作为入参。这是一个跨浏览器的、原生浏览器事件的包装器,提供统一的访问接口。
通过SyntheticEvent的 nativeEvent 属性,可以访问到原生浏览器事件对象。
从React v0.14开始,从事件处理函数返回false,不能阻止事件的冒泡。你必须显式调用 e.stopPropagation() 或者 e.preventDefault()
SyntheticEvent是被池化的(pooled),出于性能的考虑,其实例会被重用——置空所有属性并作为新事件使用。因此你不能异步的使用SyntheticEvent。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
boolean bubbles boolean cancelable DOMEventTarget currentTarget boolean defaultPrevented number eventPhase boolean isTrusted DOMEvent nativeEvent void preventDefault() boolean isDefaultPrevented() void stopPropagation() boolean isPropagationStopped() DOMEventTarget target number timeStamp string type |
SyntheticEvent支持以下事件类型:
分类 | 事件 |
剪切板事件 |
事件名称:onCopy onCut onPaste 额外属性: |
键盘事件 |
事件名称:onKeyDown onKeyPress onKeyUp 额外属性: |
焦点事件 |
事件名称:onFocus onBlur 注意:支持任何React元素,不仅仅是表单元素 |
表单事件 | 事件名称:onChange onInput onSubmit |
鼠标事件 |
事件名称:onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit 额外属性: |
选区事件 | 事件名称:onSelect |
触屏事件 |
事件名称:onTouchCancel onTouchEnd onTouchMove onTouchStart 额外属性: |
滚轮事件 |
事件名称:onWheel 额外属性: |
图片事件 |
事件名称:onLoad onError |
动画事件 |
事件名称:onAnimationStart onAnimationEnd onAnimationIteration 额外属性: string animationName |
CSS变换事件 |
事件名称:onTransitionEnd 额外属性: |
UI事件 |
事件名称:onScroll 额外属性: |
ReactDOMServer可以用于在服务器端完成React渲染:
1 |
import ReactDOMServer from 'react-dom/server' |
你可以使用以下静态方法:
ReactDOMServer.renderToString(element) 将React元素渲染为其初始的HTML,该方法应该仅仅在服务器端使用。可以用于加速客户端最初的页面加载速度或者利于搜索引擎爬虫的访问 |
|
ReactDOMServer.renderToStaticMarkup(element) 与上面的方法类似,但是不会渲染data-reactid之类的React内部使用的额外属性。如果你把React作为页面生成器使用,可以调用该方法 |
在评估React应用性能之前,确保使用最小化的产品构建(minified production build)。
如果你基于create-react-app创建应用,你需要执行 npm run build 来触发产品构建。
如果使用Webpack,你需要添加下面的内容到Webpack配置文件中:
1 2 3 4 5 6 |
new webpack.DefinePlugin( { 'process.env': { NODE_ENV: JSON.stringify( 'production' ) } } ), new webpack.optimize.UglifyJsPlugin() |
React提供声明式的API,你只需要在render函数中说明特定状态下组件长什么样子。至于操控DOM进行UI更新这样的繁琐低效的工作,由React框架本身的重渲染(Reconciliation)机制负责。React使用了一套差异比较算法(diffing algorithm)来判断状态变化前后render()函数输出的React元素子树是否存在变化,并在变化时执行重渲染。
基于下面的假设,React的差异比较算法提供O(n)复杂度的算法:
- 两个类型不同的元素类型,总是产生不同的DOM子树
- 开发者应当使用key属性对子元素进行标识
在比较两棵子树时,React首先比较的是根元素。如果根元素类型发生变化,则整个子树都被重新创建:
- 旧的DOM结构会被销毁,而旧组件的 componentWillUnmount() 钩子会被触发
- 新的DOM结构被插入,新组件的 componentWillMount() 、 componentDidMount() 钩子依次被触发
- 任何嵌套的子DOM结构/组件也会被销毁或者创建
如果根元素类型没有变化,则根元素对应的DOM节点被保留,仅仅变化了的属性会被更新。在更新内联样式时,仅仅发生变化的样式会被更新。
在不使用key属性的情况下,React会逐个比较子节点,每发现不一样的子节点,就认为其已变化。例如:
1 2 3 4 5 6 7 8 9 10 |
<ul> <li>Duke</li> <li>Villanova</li> </ul> <ul> <li>Connecticut</li> <li>Duke</li> <li>Villanova</li> </ul> |
会被认为是删除了两个子元素,添加了三个新子元素。而不是认为在列表最前面插入一个子元素。这种算法会造成性能问题,特别是子元素个数特别大的情况下。
要解决上述性能问题,可以使用key属性来标识子元素。具有相同key值的子元素将被作为同一子元素处理,而不管它在子元素集合中的索引如何变化。
React创建并维护UI的内部表达,其中包含了组件返回的React元素。该表达被称为虚拟(virtual ) DOM,React使用它避免对DOM节点的不必要访问,因为DOM节点的访问比起JavaScript对象操控,是相对缓慢的。
当组件的props或者state发生变化后,React判断是否需要对真实DOM进行更新。判断的依据是对当前render()返回的元素与先前渲染的元素进行比对,如果不相同,则执行更新。
你可以覆盖生命周期函数shouldComponentUpdate来提高渲染的效率。该函数在重新渲染(re-rendering)处理开始时被调用,默认的实现是返回true,表示需要重新渲染:
1 2 3 |
shouldComponentUpdate(nextProps, nextState) { return true; } |
某些情况下没有必要进行重新渲染,此时你可以返回一个false。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class CounterButton extends React.Component { constructor( props ) { super( props ); this.state = { count: 1 }; } shouldComponentUpdate( nextProps, nextState ) { // 仅当和UI有关的属性、状态发生变化了,才需要更新UI if ( this.props.color !== nextProps.color || this.state.count !== nextState.count ) return true; else return false; } render() { <button color={this.props.color} onClick={() => this.setState( state => ({ count: state.count + 1 }) )}> Count: {this.state.count} </button> } } |
在上面这个例子中,和UI有关的变量只有props.color和state.count,因此仅当它们发生变化时才有必要重新渲染。
如果组件的props/state结构比较复杂,你可以简单使用浅比较(shallow comparison)——仅比较props/state的直接属性——来判断是否发生“变化”。React对这种比较逻辑内置了支持,你只需要继承PureComponent即可:
1 2 3 |
class CounterButton extends React.PureComponent { // PureComponent基于浅比较覆盖了shouldComponentUpdate方法 } |
基于浅比较来判断“变化”与否,尤其固有的缺点,对于状态:
1 |
this.state = { users : [] } |
在 this.state.users.push( 'Alex' ) 调用前后,users是“相等”的,而实际上数组的元素发生变化了。
要避免类似这样无法识别状态变化的情况,最简单的方式就是使用不变对象,即对象一旦创建就不得改变,你只能给引用对象的变量重新赋值。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
this.setState( prevState => ({ words: prevState.users.concat( [ new User() ] ) // concat会返回一个新数组 }) ); this.setState( prevState => ({ colors: Object.assign({}, prevState.colors, {blue: '0000FF'}) // assign会返回一个新对象 }) ); // ES6展开操作符语法: this.setState( prevState => ({ words: [ ...prevState.users, new User() ], }) ); this.setState( prevState => ({ words: [ ...prevState.colors, blue: '0000FF' ], }) ); |
某些JavaScript库在API上对不变模式提供了支持,例如Immutable.js。该库对不可变、持久性对象提供支持:
- 不可变性:对象一旦创建就不可以被修改
- 持久性:可以基于现有的集合、可变数据结构(例如Set)来创建不可对象。在新对象创建之后,原有对象仍然可用
- 结构共享:基于现有集合、可变数据结构创建新对象后,新老对象最大可能的共享数据结构,减少拷贝操作并提升性能
在Webstorm中创建React工程,需要create-react-app包的支持。File ⇨ New ⇨ Project,在弹出的对话框中,参考下图设置,完毕后点击Create按钮:
Webstorm对React的支持包括:
- 能够基于扩展名.jsx识别JSX,提供语法高亮、代码分析功能
- 在JSX标签中,对React特有的属性(例如className、classID)进行自动完成,对于className,可以自动提示定义在工程CSS文件中的样式类
- Emmet优化,例如 div.my-class 会展开为 <div className=”my-class"></div> 而不是普通HTML中的 <div class=”my-class"></div>
- 支持花括号语法中的JS表达式的自动完成
- 支持对React方法进行代码自动完成和导航
- 可以为定义在JavaScript方法、其它组件中的组件名称进行自动完成
- 对以识别导入的基于ES6语法的组件,并提供自动完成
要获得代码自动完成功能,你需要:
- 确保react.js在工程目录下的任意位置
- 或者,将react.js配置为外部JavaScript库
Webstorm支持查看React方法参数的类型信息,执行以下步骤启用:
- Settings ⇨ Languages&Frameworks ⇨ JavaScript ⇨ Libraries,点击右侧的Download按钮
- 在弹出的对话框中选择react,点击Download and Install,如下图:
- 下载react.d.ts并添加到当前工程中。如果以扩展名.tsx结尾,则Webstorm指定将其识别为TypeScript,该文件用于支持增强自动完成
Webstorm会在键入HTML属性名 = 后,自动添加双引号,该特性在编写JSX时可能更多的是多此一举,按照如下步骤取消:
Settings ⇨ Editor ⇨ General ⇨ Smart Keys,在XML/HTML一段,取消勾选Add quote for attribute value on typing '=' and attribute completion
1 |
import React, {Component} from 'react'; |
按照ES6规范, import React from 'react'; 意味着将react模块的默认导出(default export)导入到当前模块。但是React并非基于ES6语言编写,因此不适用默认导出。
实际上,此语法是依赖于Babel的支持,效果上相当于把react模块的 module.exports = React; 直接赋值给当前模块的React变量。下面这种写法是等价的:
1 |
import * as React from 'react'; |
撒花,这应该是我目前看到最好的React中文笔记了。
过奖了:)