“React初学者教程”的版本间的差异
(→用 React Router 创建单页应用) |
(→用 React Router 创建单页应用) |
||
第1,181行: | 第1,181行: | ||
− | 这是一个简单的 React 应用,该应用用 React Router 来提供所有的导航和视图加载。点击不同的链接,可以加载相关的内容,随便 | + | 这是一个简单的 React 应用,该应用用 React Router 来提供所有的导航和视图加载。点击不同的链接,可以加载相关的内容,随便[在浏览器窗口中打开这个页面 https://www.kirupa.com/react/examples/react_router_final.htm],用后退和前进按钮来看看。 |
2017年11月2日 (四) 02:37的版本
目录
React 简介
老式的多页设计
如果你几年前做过这种应用程序,可能会采用包含多个单页面的方式。
在多页设计方式下,对于大多数改变页面显示的行为,Web 应用会导航到一个完全不同的页面。用户会看到原页面被销毁,然后出来一个新页面,这种用户体验很不尽人意。
新派的单页应用
现代的应用程序趋向于采用一种称为单页应用(SPA)的模式。这种模式下,我们不需要导航到不同的页面,甚至不需要重新加载一个页面。应用的不同视图被加载和卸载到同一页面上。
用来创建真正可组合 UI 的 API
React 鼓励我们将视觉元素分为更小的组件,而不是一整大块。
http://www.w3cplus.com/sites/default/files/blogs/2016/1611/react_components_composable_72.png
完全在 JavaScript 中定义 UI
虽然这听起来有点不可理喻,(我们知道,在 Web 标准年代,崇尚的是结构、表现形式和行为分离,也就是 UI 的结构、表现形式和行为部分分别分离到 HTML、CSS 和 JavaScript 三个文件中。完全在 JavaScript 中定义 UI,岂不是跟 Web 标准背道而驰么?)。但是且听我说完。如果像过去一样采用 HTML 模板的方式定义 UI,除了古怪的语法外,还有另一个主要问题。在模板中,除了只是显示数据,我们被限制做很多事情。例如,如果你想根据特定条件,选择显示哪一块 UI,就不得不在应用中到处写 JavaScript,或者用一些古怪的框架特有的模板语法,才能让它起作用。
而 React 实现的方式就很优雅。UI 完全在 JavaScript 中定义,我们可以利用 JavaScript 提供的强大功能在模板内做各种事情。我们受到的限制只是 JavaScript 支持不支持,而不是模板框架的限制。
创建第一个 React 应用
React 的学习曲线相当陡峭,里面大大小小的障碍不少。
处理 JSX
除了标准的 HTML、CSS 和 JavaScript 外,很多 React 代码都会用 JSX 编写。JSX 是一门可以让我们很容易混合 JavaScript 和 类似 HTML 的标记,来定义用户界面元素以及其功能的语言。这听起来很酷,但是问题是:浏览器是不知道如何处理 JSX的。
要用 React 创建 Web 应用,我们需要一种方式采用 JSX,并将它转换为浏览器可以理解的标准 JavaScript,如果不这么做,React 应用就无法运行。目前将 JSX 转换为 JavaScript 有两种解决方案:
- 围绕 Node 以及一些构建工具(比如 Webpack)来设置开发环境。
- 让浏览器在运行时自动将 JSX 转换为 JavaScript。
在 React 入门阶段,我们打算用第二种方案。那么为什么我们不一直用第二种方案呢?原因是,浏览器每次要花时间把 JSX 翻译为 JS,这对性能是有影响的。在学习如何使用 React 时,这是完全可以接受的,但是在部署应用程序实际使用时,这肯定是完全不能接受的。所以后面我们会在已经熟悉了 React 后,再用第一解决方案,即设置开发环境。
上手 React
首先,我们需要一个空白 HTML 页面作为起点。
<!DOCTYPE html> <html> <head><title>React! React! React!</title> <script src="https://unpkg.com/react@15.3.2/dist/react.js"></script> <script src="https://unpkg.com/react-dom@15.3.2/dist/react-dom.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script> </head> <body> <script></script> </body> </html>
第一行引入 核心 React 库,第二行引入 React DOM 库。如果没有这两个库,我们是没法创建 React 应用的。然后,我们还需要在两个 script 标记下添加对 Babel JavaScript 编译器的引用。
现在如果马上预览页面,我们会注意到页面是空的,什么都看不到。没关系,下面我们就要让它显示点什么。
显示你的姓名
你要做的第一件事情就是在屏幕上显示你的姓名。
ReactDOM.render( <h1>Sherlock Holmes</h1>, document.body );
此时,不要担心这几行代码到底是什么意思。我们的目标是先在屏幕上显示点什么,不久之后我们就会搞清楚这几行代码到底在做什么。现在,在预览页面看看发生了什么之前,我们需要给这个 script 块标注一下,这样 babel 才能可以发挥其魔力。实现方法是将 script 标记的 type 属性设置为 text/babel。
之后,我们就可以在浏览器预览了。我们会看到单词 'Sherlock Holmes' 在屏幕上用很大的字符打印出来。恭喜!你刚用 React 创建一个 APP。
改变输出的目标
我们要做的第一件事情是改变 JSX 输出的目标。用 JavaScript 将输出直接放在 body 元素上肯定不是一个好主意。有时候会出错,特别是如果你将 React 与其它 JS 库和框架混合在一起用时。推荐的路径是专门创建一个元素作为新的根元素,让这个新元素作为 render 方法要用的目标。OK,我们回到 HTML,添加一个 id 值为 container 的 div 元素。
var destination = document.querySelector("#container"); ReactDOM.render( <h1>Sherlock Holmes</h1>, destination );
完整示例
你的 React 应用的看起来应该是这个样子,最终结果依然 100% 的 HTML、CSS 和 JavaScript:
<!DOCTYPE html> <html> <head><title>React! React! React!</title> <script src="https://unpkg.com/react@15.3.2/dist/react.js"></script> <script src="https://unpkg.com/react-dom@15.3.2/dist/react-dom.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script> </head> <body> <div id="container"></div> <script type="text/babel"> var destination = document.querySelector("#container"); ReactDOM.render( <h1>Sherlock Holmes</h1>, destination ); </script> </body> </html>
我们可以看到,这里连一点 React 类似代码的痕迹都看不到。
React 中的组件
组件是让 React 变得美好的事情之一,它是定义人们在使用应用程序时所看到的视觉和交互的主要方式之一。
JSX 问题:输出多个元素
现在,我们让应用打印出几个超级英雄的姓名:
var destination = document.querySelector("#container"); ReactDOM.render( <div> <h1>Batman</h1> <h1>Iron Man</h1> <h1>Nicolas Cage</h1> <h1>Mega Man</h1> </div>, destination );
这里要呼出一个重要的 JSX 细节。包含 h1 元素的 div 应该去掉,是因为这看起来像个好主意。但是在 JSX 和 JavaScript 之间的邪恶联盟眼里,它是无效的。这貌似是一个可怕的限制,但是应变方式很简单。虽然我们只能输入一个元素,但是一个元素可以有很多子节点。
创建一个 “Hello, World!” 组件
var HelloWorld = React.createClass({ render: function () { return ( <p>Hello, world!</p> ); } }); ReactDOM.render( <div> <HelloWorld/> <HelloWorld/> <HelloWorld/> <HelloWorld/> <HelloWorld/> <HelloWorld/> </div>, document.querySelector("#container") );
指定属性
var HelloWorld = React.createClass({ render: function () { return ( <p>Hello, {this.props.greetTarget}!</p> ); } }); ReactDOM.render( <div> <HelloWorld greetTarget="Batman"/> <HelloWorld greetTarget="Iron Man"/> <HelloWorld greetTarget="Nicolas Cage"/> <HelloWorld greetTarget="Mega Man"/> <HelloWorld greetTarget="Bono"/> <HelloWorld greetTarget="Catwoman"/> </div>, document.querySelector("#container") );
处理子元素
var button = document.querySelector("#button"); var Buttonify = React.createClass({ render: function () { return ( <div> <button type={this.props.behavior}>{this.props.children}</button> </div> ); } }); ReactDOM.render( <div> <Buttonify behavior="Submit">SEND DATA</Buttonify> </div>, button );
总结
如果你想用 React 创建应用,如果不用组件你不会走的太远。不用组件来创建 React 应用,就像不用函数来创建基于 JavaScript 的应用程序一样。
在 React 中设置样式
如何格式化 HTML 元素?样式应该放在哪里?你可能猜到我们会在哪里处理这点。如果把样式放在另一个地方,我们就没法有一个自包含的 UI 块。这就是为什么 React 鼓励我们把样式与 HTML 和 JavaScript 放在一起的原因。在本教程中,我们将学习这种神秘的,可能是骇人听闻的格式化内容的方法。
var destination = document.querySelector("#container"); var Letter = React.createClass({ render: function () { var letterStyle = { padding: 10, margin: 10, backgroundColor: this.props.bgcolor, color: "#333", display: "inline-block", fontFamily: "monospace", fontSize: "32", textAlign: "center" }; return ( <div style={letterStyle}> {this.props.children} </div> ); } }); ReactDOM.render( <div> <Letter bgcolor="#58B3FF">A</Letter> <Letter bgcolor="#FF605F">E</Letter> <Letter bgcolor="#FFD52E">I</Letter> <Letter bgcolor="#49DD8E">O</Letter> <Letter bgcolor="#AE99FF">U</Letter> </div>, destination );
我们刚才所做的,是用普通 CSS 很难实现的。后面我们在学习那些内容会根据状态或者用户交互而改变的组件时,我们会看到更多的这样用 React 方式格式化内容的示例有更多好的优点。
创建复杂的组件
从界面到组件
迄今为止我们所看到的几个例子是很基础的。对于突出技术概念来说,这几个例子还不错,但是对于要为真实世界做准备来说,它们就一般般了。
在真实世界中,你要用 React 实现的肯定不会是像一个姓名列表,一个彩色元音字母块这么简单了。我们面对的往往是一些复杂用户界面的视觉。
现在,我们要做一个简单的调色板卡。
1. 识别主要的视觉元素
第一个步骤是识别我们要处理的所有视觉元素。即使是最小的视觉元素也不能省略。
2. 识别组件
我们需要搞清楚我们识别出来的视觉元素中哪些需要变成组件,哪些不需要。通用的规则是我们的组件应该只做一件事情。如果你发现你可能的组件将会做太多事情,可能就要将组件拆分成多个组件。另一方面,如果潜在的组件做的事情太少,可能就要完全略过让这个视觉元素成为一个组件。
创建组件
我们就要开始定义我们的三个组件了。这三个组件的名字将是 Card、Label 和 Square。
var destination = document.querySelector("#container"); var Card = React.createClass({ render: function () { var cardStyle = { height: 200, width: 150, padding: 0, backgroundColor: "#FFF", WebkitFilter: "drop-shadow(0px 0px 5px #666)", filter: "drop-shadow(0px 0px 5px #666)" }; return ( <div style={cardStyle}> <Square/> <Label/> </div> ); } }); var Square = React.createClass({ render: function () { var squareStyle = { height: 150, backgroundColor: "#FF6663" }; return ( <div style={squareStyle}> </div> ); } }); var Label = React.createClass({ render: function () { var labelStyle = { fontFamily: "sans-serif", fontWeight: "bold", padding: 13, margin: 0 }; return ( <p style={labelStyle}>#FF6663</p> ); } }); ReactDOM.render( <div> <Card/> </div>, destination );
再次传递属性!
在当前示例中,我们把 Squar 和 Label 组件用的颜色值硬编码了。这是件奇怪的事情,可能会也可能不会故意地这样做以得到戏剧性的效果,但是修复它是很简单的。只需要指定一个属性名,并且通过this.props 方法该属性名就可以了。我们在前面已经这样做过了。不同的是这次我们将不得不多次这样做。
var destination = document.querySelector("#container"); var Card = React.createClass({ render: function () { var cardStyle = { height: 200, width: 150, padding: 0, backgroundColor: "#FFF", WebkitFilter: "drop-shadow(0px 0px 5px #666)", filter: "drop-shadow(0px 0px 5px #666)" }; return ( <div style={cardStyle}> <Square color={this.props.color}/> <Label color={this.props.color}/> </div> ); } }); var Square = React.createClass({ render: function () { var squareStyle = { height: 150, backgroundColor: this.props.color }; return ( <div style={squareStyle}> </div> ); } }); var Label = React.createClass({ render: function () { var labelStyle = { fontFamily: "sans-serif", fontWeight: "bold", padding: 13, margin: 0 }; return ( <p style={labelStyle}>{this.props.color}</p> ); } }); ReactDOM.render( <div> <Card color="#FFA737"/> </div>, destination );
尽管 color 属性只被 Square 和 Label 组件所用,但是父组件 Card 负责传递该属性给它们。对于更深层次的嵌套,你会需要更多的中间组件负责传递属性。这就变得更糟糕。当你有多个属性想沿着多级组件传递时,你所做的打字(或者复制/粘贴)数量也会增加很多。有方法可以缓解,我们会在后面的教程中更详细地了解这些缓解措施。
传递属性
处理属性有令人沮丧的一面,在前一个教程中我们已经看到了一点。在只处理一层组件时,将属性从一个组件传递到另一个很简单。但是如果你想将一个属性在多层组件之间传递,事情就开始变得复杂了。
事情变得复杂从来不是一件好事情,所以在本教程中,我们来看看我们怎么做才能让在多层组件之间传递属性变得容易。
问题概述
React 强制一个命令链,在链中,属性必须从父组件向下流动到直接的子组件。也就是说,在传递一个属性时,你不能跳过子层。这还意味着你的子组件不能把一个属性传回到父组件。所有的通讯是从父到子单向的。
如果我们想发送三个属性又该怎么办?或者四个呢?
我们很快会看到这种方法既没有可扩展性,也没有可维护性。对于需要通讯的每个附加的属性,我们将不得不为它添加一个入口作为每个组件声明的一部分。如果在某个时间我们决定重新命名属性,就不得不确保该属性的每个实例也被重新命名。如果删除一个属性,我们需要从依赖该属性的每个组件中删除该属性。总的来说,这是我们在编写代码时,应该努力避免的一种情况。那么我们可以怎么做呢?
遇见扩展运算符
所有这些问题的解决方案在于一个 JavaScript 新概念:扩展运算符(Spread Operator)。如果没有上下文,要解释扩展运算符有点难度,所以这里我们先看一个示例,然后再看扩展运算符的定义。
printStuff(...items);
扩展运算符是在 items 数组前的 '…' 字符,使用 '…items' 等于分别调用 items[0], item[1], item[2]。
正确传递属性
var destination = document.querySelector("#container"); var Display = React.createClass({ render: function () { return ( <div> <p>{this.props.color}</p> <p>{this.props.num}</p> <p>{this.props.size}</p> </div> ); } }); var Label = React.createClass({ render: function () { return ( <Display {...this.props}/> ); } }); var Shirt = React.createClass({ render: function () { return ( <div> <Label {...this.props}/> </div> ); } }); ReactDOM.render( <div> <Shirt color="steelblue" num="3.145" size="medium"/> </div>, destination );
总结
ES6/ES2015 委员会设计扩展运算符时,只设计把它用在数组上。它能在对象字面量(例如我们的 props 对象)上使用,归功于 React 扩展了标准。到目前为止,还没有浏览器支持在对象字面量上使用扩展运算符。我们的示例能够运行是因为有 Babel。除了将所有 JSX 转变为浏览器能理解的语言外, Babel 还会将前沿以及实验功能转变成跨浏览器友好的东西。
深入JSX
一、JSX背后发生了什么?
return React.createElement( "div", {style: cardStyle}, React.createElement(Square, {color: this.props.color}), React.createElement(Label, {color: this.props.color}) );
由Babel完全在浏览器中执行 JSX => JS 的转换,被转变成JavaScript。
二、要记住的 JSX 怪癖
1. 只能返回一个根节点
如果是有多个根元素,就会破坏函数返回值以及 createElement的机制。
2. 不能指定 inline CSS
3. 保留关键字和 className
ReactDOM.render( <div className="slideIn"> <p className="emphasis">Gabagool!</p> <Label/> </div>, document.querySelector("#container") );
4. 注释
如果你指定一个注释作为一个标记的子节点,那么你必须用 {和 } 把注释包起来,以确保它被解析为一个表达式:
ReactDOM.render( <div class="slideIn"> <p class="emphasis">Gabagool!</p> {/* I am a child comment */} <Label/> </div>, document.querySelector("#container") );
如果完全在一个标记内部指定注释,那么就不需要用大括号把单行或者多行注释括起来:
ReactDOM.render( <div class="slideIn"> <p class="emphasis">Gabagool!</p> <Label /* This comment goes across multiple lines */ className="colorCard" // end of line /> </div>, document.querySelector("#container") );
5. 大小写、HTML 元素和组件
大小写很重要。如果大小写出错,React 将不会正确渲染内容。
要表示 HTML 元素,必须确保 HTML 标记是小写字母。当表示组件时,组件的名称必须是大写。
6. JSX 可以出现在任何地方
在很多情况下,JSX 并不是像我们前面看到的示例一样,整齐地排列在一个 render 或者 return 函数内。看看如下示例:
var swatchComponent = <Swatch color="#2F004F"></Swatch>; ReactDOM.render( <div> {swatchComponent} </div>, document.querySelector("#container") );
我们有一个 swatchComponent 变量,该变量被初始化为一行 JSX。当 swatchComponent 变量放在 render 函数内时,Swatch 组件就被初始化。这一切都是有效的,并且在将来当我们学习如何用 JavaScript 生成和操作 JSX 时,我们将会做更多这种事情。
处理状态
到目前为止,我们已经创建的组件都是无状态的(stateless)。它们有从它们的父组件传递进来的属性(亦称 props),一旦属性被设置了,就被当作是不可修变的(immutable)。对于很多交互场景来说,你肯定不想这样子。在一些用户交互发生,或者一些数据从服务器或者其它地方返回过来时,你想能改变组件的外观。
我们所要的是另一种超越属性的在组件上存储数据的方式,即存储可以修改的数据。我们所需要的是称为状态(state)的东西。
我们要创建一个简单的雷击计数示例:
https://www.w3cplus.com/sites/default/files/blogs/2016/1611/lightningCounter_144.png
var LightningCounter = React.createClass({ getInitialState: function () { return {strikes: 0}; }, render: function () { return ( <h1>{this.state.strikes}</h1> ); } }); var LightningCounterDisplay = React.createClass({ render: function () { var divStyle = { width: 250, textAlign: "center", backgroundColor: "black", padding: 40, fontFamily: "sans-serif", color: "#999", borderRadius: 10 }; return ( <div style={divStyle}> <LightningCounter/> </div> ); } }); ReactDOM.render( <LightningCounterDisplay/>, document.querySelector("#container") );
让计数器跑起来
现在我们已经完成了基础代码,我们可以开始规划下一步了。 counter 工作的方式很简单。我们打算用 setInterval 函数每 1000ms(即 1s)调用一段代码。这段代码每被调用一次,就让一个值增 100。看起来很简单,对吧?
要让这些都可以工作起来,我们打算依赖于 React 组件暴露的三个 API:
- componentDidMount:该方法在组件渲染后被调用(or mounted as React calls it).。
- getInitialState:该方法在组件挂载之前运行,并允许你修改组件的 state 对象。
- setState:该方法允许你修改 state 对象的值。
设置初始状态值
我们要做的是在 getInitialState 方法内初始化 strikes 。
启动定时器并设置状态
下一步是让定时器运行起来,增加 strikes 属性。像我们前面提到的一样,我们将用 setInterval 函数每秒钟增加 strikes 属性 100。我们打算在组件被渲染后用内置的 componentDidMount 方法立即做所有这事。
启动定时器的代码如下:
var LightningCounter = React.createClass({ getInitialState: function() { return { strikes: 0 }; }, timerTick: function () { this.setState({strikes: this.state.strikes + 100}); }, componentDidMount: function() { setInterval(this.timerTick, 1000); }, render: function() { return ( <h1>{this.state.strikes}</h1> ); } });
一旦组件被渲染了,componentDidMount 方法就被调用,在该方法内,setInterval 方法每 1000ms调用 timerTick 函数一次。
渲染状态改变
如果在浏览器中预览,你会看到 strikes 值每秒钟增100。这种更新是因为 React 的机制:只要你调用 setState 并更新了 state 对象中一些东西,那么组件的 render 方法就会自动被调用。
完整的代码
var LightningCounter = React.createClass({ getInitialState: function () { return { strikes: 0 }; }, timerTick: function () { this.setState({ strikes: this.state.strikes + 100 }); }, componentDidMount: function () { setInterval(this.timerTick, 1000); }, render: function () { var counterStyle = { color: "#66FFFF", fontSize: 50 }; var count = this.state.strikes.toLocaleString(); return ( <h1 style={counterStyle}>{count}</h1> ); } }); var LightningCounterDisplay = React.createClass({ render: function () { var commonStyle = { margin: 0, padding: 0 }; var divStyle = { width: 250, textAlign: "center", backgroundColor: "#020202", padding: 40, fontFamily: "sans-serif", color: "#999999", borderRadius: 10 }; var textStyles = { emphasis: { fontSize: 38, ...commonStyle }, smallEmphasis: { ...commonStyle }, small: { fontSize: 17, opacity: 0.5, ...commonStyle } }; return ( <div style={divStyle}> <LightningCounter/> <h2 style={textStyles.smallEmphasis}>LIGHTNING STRIKES</h2> <h2 style={textStyles.emphasis}>WORLDWIDE</h2> <p style={textStyles.small}>(since you loaded this example)</p> </div> ); } }); ReactDOM.render( <LightningCounterDisplay/>, document.querySelector("#container") );
总结
我们只是肤浅地研究了如何创建有状态的组件。虽然用定时器来更新 state 对象很酷,但是实际行为是在我们开始将用户交互与状态组合时才发生的。迄今为止,我们回避了大量的鼠标、触摸、键盘以及组件将会接触到的其它相关事情。在下一个教程中,我们将解决这个问题。沿着这条路,你会看到我们把我们已经看到的有关状态的技术带到一个全新的层次!如果这还不能让你激动,那么我不知道什么可以 :p
从数据到 UI
示例
var Circle = React.createClass({ render: function () { var circleStyle = { padding: 10, margin: 20, display: "inline-block", backgroundColor: this.props.bgColor, borderRadius: "50%", width: 100, height: 100, }; return ( <div style={circleStyle}></div> ); } }); var destination = document.querySelector("#container"); ReactDOM.render( <div> <Circle bgcolor="#F9C240"/> </div>, destination );
https://www.w3cplus.com/sites/default/files/blogs/2016/1611/one_yellow_circle.png
JSX 可以在任何地方
function showCircle() { var colors = ["#393E41", "#E94F37", "#1C89BF", "#A1D363"]; var ran = Math.floor(Math.random() * colors.length); return <Circle bgColor={colors[ran]}/>; } ReactDOM.render( <div> {showCircle()} </div>, destination );
处理数组
var colors = ["#393E41", "#E94F37", "#1C89BF", "#A1D363", "#85FFC7", "#297373", "#FF8552", "#A40E4C"]; var renderData = []; for (var i = 0; i < colors.length; i++) { renderData.push(<Circle bgColor={colors[i]}/>); } ReactDOM.render( <div> {renderData} </div>, destination );
总结
在本文中你所看到的所有诀窍都可能是由一个东西造成:JSX 就是 Javascript。这就是为什么 JavaScript 出现的地方都可以用 JSX 的原因。对于我们来说,下面这样的代码看起来是相当怪异的:
for (var i = 0; i < colors.length; i++) { var color = colors[i]; renderData.push(<Circle key={i + color} bgColor={color}/>); }
尽管我们把一些 JSX 判断压到一个数组中,像变戏法一样,当 renderData 在 render 方法内被求值时,所有代码都能正常运行。我讨厌听起来像坏唱片一样,但是这是因为我们的浏览器最终看起来像这样子:
for (var i = 0; i < colors.length; i++) { var color = colors[i]; renderData.push(React.createElement(Circle, { key: i + color, bgColor: color })); }
当 JSX 被转换为纯 JS 时,一切又变得有意义。这就是为什么我们可以将 JSX 和 数据一起放在各种不舒服的条件下,但是依然可以得到我们想要的最终结果的原因。因为,最终,都是 JavaScript。
React 中的事件
迄今为止,我们所有的示例都是只在页面加载时执行。你可能会猜到,这是不正常的。在很多应用中,特别是重 UI 类型的应用中,应用程序要做的很多事情只是对某种事情的响应。这里,某种事情可能是被鼠标点击、按键、窗口缩放、或者其它手势操作以及交互。而让所有这一切变得可能的粘合剂是事件。
React 处理事件的方式有点不同,如果没有密切关注的话,这些不同之处会以不同的方式让你吃惊。
监听和响应事件
学习 React 事件最简单的方式就是用它。这里我们先做一个简单的示例,每次点击按钮后,计数器增一:
https://www.w3cplus.com/sites/default/files/blogs/2016/1611/event_ex_final.png
每次点击 + 按钮,计数器就增加 1。点击几次按钮后,就是这样子的。
起点
var destination = document.querySelector("#container"); var Counter = React.createClass({ render: function () { var textStyle = { fontSize: 72, fontFamily: "sans-serif", color: "#333", fontWeight: "bold" }; return ( <div style={textStyle}> {this.props.display} </div> ); } }); var CounterParent = React.createClass({ getInitialState: function () { return { count: 0 }; }, render: function () { var backgroundStyle = { padding: 50, backgroundColor: "#FFC53A", width: 250, height: 100, borderRadius: 10, textAlign: "center" }; var buttonStyle = { fontSize: "1em", width: 30, height: 30, fontFamily: "sans-serif", color: "#333", fontWeight: "bold", lineHeight: "3px" }; return ( <div style={backgroundStyle}> <Counter display={this.state.count}/> <button style={buttonStyle}>+</button> </div> ); } }); ReactDOM.render( <div><CounterParent/></div>, destination );
完成这段代码后,在浏览器中预览一下,看看代码是否正确。然后,花几分钟看看这些代码是干嘛的。这些代码都是我们前面学习过的,所有代码看起来都不应该是陌生的。当然,唯一古怪的事情是点击按钮什么都不会发生,下面我们马上来解决这个问题。
让按钮点击做点事情
每次点击加号按钮,我们想让计数器的值增一。我们要做的事情大致是这样的:
- 监听按钮上的点击事件。
- 当点击事件被监听到时,指定处理该事件的事件处理器。
- 实现事件处理器,增加 this.state.count 属性的值。
increase: function (e) { this.setState({ count: this.state.count + 1 }); }, render: function () { ... return ( <div style={backgroundStyle}> <Counter display={this.state.count}/> <button onClick={this.increase} style={buttonStyle}>+</button> </div> ); }
合成事件
在 React 中,如果像在 onClick 中所做的那样在 JSX 中指定事件时,是不能直接处理常规的 DOM 事件的,而是处理 React 特定的事件类型 SyntheticEvent。你的事件处理器不能得到原生的事件参数类型 MouseEvent、KeyboardEvent 等等,而是总是得到封装了浏览器的原生事件的事件参数类型 SyntheticEvent。那么这对我们代码有什么影响呢?令人吃惊的是完全没有影响。
当使用合成事件及其属性时,不要参考传统 DOM 事件文档。因为 SyntheticEvent 包含了原生 DOM 事件,事件及其属性并不一定是一对一映射的。有些 DOM 事件在 React 甚至是不存在的。要避免遇到任何问题,如果你想知道一个合成事件或者任何其属性,应该参考 React 事件系统文档。https://reactjs.org/docs/events.html
用事件属性干活
现在,你可能已经看到比你可能喜欢的更多的有关 DOM 和合成事件。我们开始写一些代码,将新发现的知识放在一起很好的使用。现在,我们的计数器通过每次点击加号按钮加一。我们想做的是当用鼠标点击加号按钮同时按着 Shift 键,让计数器加十。
我们打算在用鼠标时,用 SyntheticEvent 的 shiftKey 属性完成这件事情。
increase: function (e) { var currentCount = this.state.count; if (e.shiftKey) { currentCount += 10; } else { currentCount += 1; } this.setState({ count: currentCount }); },
不能直接在组件上监听事件
原因是,组件是 DOM 元素的包装器。要监听一个组件上的事件到底是什么意思?一旦组件被展开到 DOM 元素中,你要监听的事件是放到外层 HTML 元素上吗?它是一些其它元素吗?在监听一个事件与声明你要监听的一个 prop 之间你如何区分?
这些问题都没有清晰的答案。过份点说,解决方案是要么就不监听组件上的事件。幸运的是,有一个变通方案,就是将事件处理器当作是一个 prop,并将它传递给组件。然后在组件内,我们可以把事件赋值给一个 DOM 元素,并将事件处理器设置为我们刚传进来的 prop 的值。我们来看一个示例。
var CounterParent = React.createClass({ . . . render: function() { return ( <div> <Counter display={this.state.count}/> <PlusButton clickHandler={this.increase}/> </div> ); } });
在本例中,我们创建一个属性 clickHandler,该属性的值是 increase 事件处理器。然后,在 PlusButton 组件内,我们可以像这样做:
var PlusButton = React.createClass({ render: function() { return ( <button onClick={this.props.clickHandler}> + </button> ); } });
在 button 元素上,我们指定了 onClick 事件,并将它的值设置为 clickHandler prop。在运行时,这个 prop 被求值为 increase 函数,并且点击加号按钮确保 increase 函数被调用。这就解决了我们的问题,同时还让组件全程参与了。
事件处理器中 this 的含义
通常来说,事件处理器中 this 的值引用的是触发该事件的元素。而在 React 世界中,事件处理器中的 this 引用的总是事件处理器所处的组件。
React...为什么?为什么?!
就今天就到这儿之前,我们来谈谈为什么 React 决定背离我们过去处理事件的方式。有两个原因:
1. 浏览器兼容性
事件处理在现代浏览器中是可以一致工作的事情之一,但是如果回到旧的浏览器版本,事情就会马上变得很糟糕。通过将所有本地事件封装为一个 SyntheticEvent 类型的对象,React 让我们从处理最终不得不处理的事件处理怪癖中解脱出来。
2. 提升性能
在复杂的 UI 中,事件处理器越多,应用程序占用的内存就越多。虽然手动处理这并不难,但是这有点枯燥,因为你得设法将事件组织到一个共同的父之下。有时,这是不可能的。有时,麻烦超过了好处。而 React 就做的很聪明。
React 从不会将事件处理器直接绑定到 DOM 元素。它在文档的根部使用一个事件处理器,来负责监听所有事件,并按需调用合适的事件处理器。
组件生命周期
为帮助做这些事情,React 给我们提供了一些生命周期方法。生命周期方法是组件正常运转时自动调用的特殊方法。它们告知我们组件生命中重要的里程碑,我们可以用这些通知来只是注意,或者改变组件要做的事情。
初识生命周期方法
生命周期方法不是很复杂。我们可以把它们当作是在组件生命中的不同的点被调用的美化的事件处理器,并且就像事件处理器一样,我们可以在这些不同的点编写代码做一些事情。在深入之前,我们先来快速了解一下生命周期方法。
- componentWillMount
- componentDidMount
- componentWillUnmount
- componentWillUpdate
- componentDidUpdate
- shouldComponentUpdate
- componentWillReceiveProps
还有三个方法严格上讲并非生命周期方法,但是我们仍要把它们跟生命周期方法混在一起。
- getInitialState
- getDefaultProps
- render
这些方法中,有些名称可能看起来是熟悉,有些可能是第一次看到。不要着急。在本教程结束后,你对所有这些方法都很熟悉。我们打算要做的是从不同角度看看这些生命周期方法。我们还是以代码开始!
看看生命周期方法起作用
var destination = document.querySelector("#container"); var Counter = React.createClass({ render: function () { var textStyle = { fontSize: 72, fontFamily: "sans-serif", color: "#333", fontWeight: "bold" }; return ( <div style={textStyle}> {this.props.display} </div> ); } }); var CounterParent = React.createClass({ getDefaultProps: function () { console.log("getDefaultProps: Default prop time!"); return {}; }, getInitialState: function () { console.log("getInitialState: Default state time!"); return {count: 0}; }, increase: function () { this.setState({count: this.state.count + 1}); }, componentWillUpdate: function (newProps, newState) { console.log("componentWillUpdate: Component is about to update!"); }, componentDidUpdate: function (currentProps, currentState) { console.log("componentDidUpdate: Component just updated!"); }, componentWillMount: function () { console.log("componentWillMount: Component is about to mount!"); }, componentDidMount: function () { console.log("componentDidMount: Component just mounted!"); }, componentWillUnmount: function () { console.log("componentWillUnmount: Component is about to be removed from the DOM!"); }, shouldComponentUpdate: function (newProps, newState) { console.log("shouldComponentUpdate: Should component update?"); if (newState.count < 5) { console.log("shouldComponentUpdate: Component should update!"); return true; } else { ReactDOM.unmountComponentAtNode(destination); console.log("shouldComponentUpdate: Component should not update!"); return false; } }, componentWillReceiveProps: function (newProps) { console.log("componentWillReceiveProps: Component will get new props!"); }, render: function () { var backgroundStyle = { padding: 50, border: "#333 2px dotted", width: 250, height: 100, borderRadius: 10, textAlign: "center" }; return ( <div style={backgroundStyle}> <Counter display={this.state.count}/> <button onClick={this.increase}> +</button> </div> ); } }); ReactDOM.render( <div><CounterParent/></div>, destination );
初始渲染阶段
https://www.w3cplus.com/sites/default/files/blogs/2016/1611/render_144.png
除了 render 方法外,所有这些生命周期方法只能触发一次。这与我们下面要看到的方法有很大不同。
更新阶段
https://www.w3cplus.com/sites/default/files/blogs/2016/1611/statechange_144.png
shouldComponentUpdate 有时,当一个状态发生改变时,你不想让组件更新。这个方法可以让我们控制更新行为。如果你使用该方法,并返回一个 true 值,组件就会被更新。如果返回一个 false 值,该组件就不会更新。
如下是一个简单的示例:
shouldComponentUpdate: function(newProps, newState) { if (newState.id <= 2) { console.log("Component should update!"); return true; } else { console.log("Component should not update!"); return false; } }
该方法带有两个参数:newProps 和 newState。
处理 prop 改变
https://www.w3cplus.com/sites/default/files/blogs/2016/1611/propschange_144.png
这里唯一的新方法是 componentWillReceiveProps。该方法只返回一个参数,并且该参数包含将要赋值的新 prop 值。
剩下的生命周期方法我们在前面查看状态改变时已经看过了,所以这里就不在重复了。在处理 prop 修改时,它们的行为是一致的。
卸载阶段
https://www.w3cplus.com/sites/default/files/blogs/2016/1611/unmount_144.png
这里唯一的生命周期方法是 componentWillUnmount。我们可以在这个方法中执行清理相关的任务,比如移除事件监听器、停止计数器等等。在该方法被调用后,组件就从 DOM 中移除,你可以对它说拜拜了。
总结
React 不断地在每次有兴趣的事情发生时候监视和通知组件。这都是通过生命周期方法实现的。现在,我想安慰你的是,知道每个生命周期方法做什么以及什么时候它被调用,是总有一天会派上用场的。你所学的一切并非仅是繁琐的知识,如果你能凭记忆描述所有生命周期方法,你的朋友们会被打动的。
用 React Router 创建单页应用
这是一个简单的 React 应用,该应用用 React Router 来提供所有的导航和视图加载。点击不同的链接,可以加载相关的内容,随便[在浏览器窗口中打开这个页面 https://www.kirupa.com/react/examples/react_router_final.htm],用后退和前进按钮来看看。
https://www.w3cplus.com/sites/default/files/blogs/2016/1611/router_highlight_link.png