一、Hello World
1. 最简易的 React 示例
ReactDOM.render('<h1>Hello World</h1>',document.querySelector('#root'))
- React Apps 的构建块就是:元素和组件(elements 和 components)
- 只要掌握了它们就可以重复使用这些块构建复杂的应用程序
二、JSX 语法
const element = <h1>Hello, world!</h1>;
JSX 是 JavaScript 的语法扩展,它看起来有点像模板语言,而其实拥有 JavaScript 的全部功能
React 认为处理逻辑与 UI 本质上是耦合的
- 如何处理事件
- 状态如何随时间变化
- 数据如何为展现做准备
与通过将标记和逻辑放在单独的文件中来认为将技术分离不同,React 通过称为“组件”的松散耦合的单元来分离关注点,它既包含标记,也包含逻辑。
React 不需要 JSX,但是在使用时将 UI 放入 JavaScript 代码能对视觉帮助很有用。它还允许帮助 React 显示更多有用的错误和警告消息。
1. JSX 中嵌入表达式
const name = 'Josh Perez'function formatName(user) {return user.firstName + ' ' + user.lastName}const element = (<h1>Hello, {name}</h1>)const element2 = (<h1>Hello, {formatName(user)}!</h1>)
- 可以把任何有效的 JavaScript 表达式放入 JSX 的大括号中
- JSX 拆分成多行时,建议加上圆括号
经过编译后,JSX 表达式变成了普通的函数调用,然后计算得到 JavaScript 对象
- 我们可以很轻松使用 if 语句、for 循环使用 JSX,它可以被赋值给变量,可以作为函数的参数,也可以作为函数的返回值
3. JSX 中指定属性
const element = (<div tabIndex="0"></div>)const element = (<img src={user.avatarUrl}></img>)
- 可以使用引号将字符串指定为属性
- 可以使用大括号将 JavaScript 表达式嵌入到属性中
- JSX 的语法更接近 JS 而不是 HTML,React DOM 使用驼峰属性的命名原则,而不是 HTML 属性名
4. JSX 中指定子元素
const element = <img src={user.avatarUrl} />const element2 = (<div><h1>Hello!</h1><h2>Good to see you here.</h2></div>)
- 如果一个标签的内容是空的,可以像 XML 一样使用
/>立刻关闭
5. JSX 防止注入攻击
const title = response.potentiallyMaliciousInput;// This is safe:const element = <h1>{title}</h1>;
- 默认情况下,React DOM 会渲染 JSX 内容之前转译所有嵌入其中的值,所以在应用里不会注入任何东西
- 所有东西在渲染之前都会转换成字符串,这能防止 XSS(cross site scripting)攻击
6. JSX 表示为对象
const element = (<h1 className="greeting">Hello, world!</h1>)const element2 = React.createElement('h1',{className: 'greeting'},'Hello, world!')
- Babel 将 JSX 编译为 React.createElement() 调用,上面两个写法是等价的
- React.createElement() 执行一些检查来帮助我们写一些没有 bug 的代码,它实际创建了这样一个对象
- 这些对象被称为“React 元素”,React 读取它们来构造 DOM, 并让 DOM 与最新 data 保持同步更新
// Note: this structure is simplifiedconst element = {type: 'h1',props: {className: 'greeting',children: 'Hello, world!'}}
三、渲染元素
const element = (<h1>Hello, world</h1>)
- 元素是 React Apps 的最小构建块,一个元素描述屏幕中我们想看到的东西
- 与浏览器 DOM 元素不同,React 元素是普通对象,创建的开销很小
- React DOM 负责更新 DOM,将它与 React 元素保持一致
- 不要将“元素”与“组件”混淆了,它们的关系是:组件是由元素组成的
1. 将元素渲染成 DOM
<div id="root"></div>
const element = <h1>Hello, world</h1>;ReactDOM.render(element, document.getElementById('root'));
- 在HTML 选择一个 root DOM,它内部的所有内容都被 React DOM 管理
- 要将 React 元素渲染成 root DOM,就把 React 元素和 root DOM 传递给 ReactDOM.render()
2. 更新渲染的元素
function tick() {const element = (<div><h1>Hello, world!</h1><h2>It is {new Date().toLocaleTimeString()}.</h2></div>);ReactDOM.render(element, document.getElementById('root'));}setInterval(tick, 1000);
- React 元素是不可变的,一旦创建了一个元素,就不能改变它的子元素和属性
- 我们可以创建一个新的元素,然后将其传递给 ReactDOM.render(),上面是一个滴答时钟的示例
3. React 只更新必要的部分
- React DOM 将元素与其子元素与先前的做比较,然后只更新 DOM 能够达到状态的所必要的部分
- 上个示例中,即使我们在每次“滴答”创建了一个新的元素来描述 UI 树,但只有文本节点的内容改变了, React DOM 才去更新它
四、组件 & Props
- 组件可以使我们将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思
- 从概念上看,组件类似于 JavaScript 函数
- 它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素
1. 函数组件和类组件
// 函数组件function Welcome(props) {return (<h1>Hello, {props.name}</h1>)}// 类组件class Welcome extends React.Component {render() {return <h1>Hello, {this.props.name}</h1>;}}
- 定义一个组件最简单的方法是编写一个 JavaScript 函数
- 示例中,它接受一个带有数据的单个“ props”(代表属性)对象参数,并返回一个 React 元素
- 我们称这些组件为“函数组件”,因为它们实际上是 JavaScript 函数
- 我们也可以使用
ES6 class定义一个组件,从 React 角度看,他们是等价的
2. 渲染一个组件
function Welcome(props) {return (<h1>Hello, {props.name}</h1>)}const element = (<Welcome name="Sara" />)ReactDOM.render(element,document.getElementById('root'))
- React 元素不仅可以代表 DOM 标签,也可以表示为用户定义的组件
- 当 React 看到一个元素表示为用户自定义组件时,它会把 JSX 的属性和子元素作为一个对象传递给它
- 这个对象被称为“props”
- 始终将组件以大写字母开头,React 会以小鞋字母开头的组件视为 DOM 标记
- 比如 表示一个 HTML div 标记
- 而
表示一个组件,并且 Welcome 需要在作用域内使用
3. 组件之间组合
function Welcome(props) {return (<h1>Hello, {props.name}</h1>)}function App() {return (<div><Welcome name="Sara" /><Welcome name="Cahal" /><Welcome name="Edite" /></div>)}ReactDOM.render(<App />,document.getElementById('root'))
- 组件可以在其输出中引用其他组件,这就可以让我们用同一组件做任意级别的抽象
- 一个按钮,一个对话框,或是一个整个的屏幕,所有这些通常都以组件组件的形式表达
- 通常,一个 React Apps 在顶层有一个
App组件 - 而如果是将 React 集成到现有的应用中,可能会从小组件,比如
Button组件开始自底向上工作到视图中
4. 提取组件
// 初始的 Comment 组件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 组件function Avatar(props) {return (<img className="Avatar"src={props.user.avatarUrl}alt={props.user.name}/>);}// 提取 UserInfo 组件function UserInfo(props) {return (<div className="UserInfo"><Avatar user={props.user} /><div className="UserInfo-name">{props.user.name}</div></div>);}// 拆分后的 Comment 组件function Comment(props) {return (<div className="Comment"><UserInfo user={props.author} /><div className="Comment-text">{props.text}</div><div className="Comment-date">{formatDate(props.date)}</div></div>);}
- 不要害怕将组件拆分成更小的组件
- 建议从组件自身的角度命名组件,而不是根据它被使用的上下文
- 提取组件一开始看起来是个繁重的工作,但在大型应用中,构建可复用组件库是值得的
5. Props 是只读的
// 这是一个纯函数,不修改输入function sum(a, b) {return a + b;}// 这个函数不纯,它改变了自己的输入function withdraw(account, amount) {account.total -= amount;}
- 当声明了一个函数组件或类组件时,它一定不能修改它的 props
- 纯函数不会尝试改变它的输入,并会为相同的输入返回相同的结果
- 所有 React 组件必须像纯函数一样使用它们的 props
- 当然,UIs 是动态的,并会随着时间推移而变化,State 允许 React 组件随时间更改其输出,以相应用户操作、网络相应和其他任何东西,而不违反此规则
五、State & 生命周期
在「更新渲染的元素」小结中我们调用 ReactDom.render() 来改变渲染出的结果,当我们有了 State,我们就可以将滴答时钟封装成可复用的组件,它将设置它自己的定时器,并在每秒更新一次
1. 将函数组件转换为类组件
class Clock extends React.Component {render() {return (<div><h1>Hello, world!</h1><h2>It is {this.props.date.toLocaleTimeString()}.</h2></div>);}}
- 把函数组件转换成类组件通常有 5 个步骤:
- 创建
ES6 class类并继承React.Component - 添加一个名叫
render()的空方法 - 将函数体移到
render()方法里 - 在
render()里的props替换成this.props - 删除剩余的空函数声明
- 创建
render()方法会在每次更新的时候被调用,一旦我们渲染<Clock />到相同的 DOM 节点时,只有一个 Clock 类的实例会被使用,这让我们可以使用额外的特性,比如局部 state 和生命周期方法
2. 给 class 添加局部 State
class Clock extends React.Component {constructor(props) {super(props);this.state = {date: new Date()};}render() {return (<div><h1>Hello, world!</h1><h2>It is {this.state.date.toLocaleTimeString()}.</h2></div>);}}ReactDOM.render(<Clock />,document.getElementById('root'));
- 添加一个初始赋值
this.state的类构造函数 - 类组件应该始终使用
props来调用构造函数
3. 给 class 添加生命周期方法
class Clock extends React.Component {constructor(props) {super(props);this.state = {date: new Date()};}componentDidMount() {this.timerID = setInterval(() => this.tick(),1000);}componentWillUnmount() {clearInterval(this.timerID);}tick() {this.setState({date: new Date()});}render() {return (<div><h1>Hello, world!</h1><h2>It is {this.state.date.toLocaleTimeString()}.</h2></div>);}}ReactDOM.render(<Clock />,document.getElementById('root'));
- 当
<Clock>第一次被渲染成 DOM 时,我们设置一个定时器。React 中将这个阶段被称为“挂载” - 在
<Clock>被移除时,我们将定时器清除。React 中将这个阶段被称为“卸载” - 我们可以在类组件上声明特殊的方法,以便在组件在挂载和卸载时运行一些代码
- 这些特殊的方法被称为“生命周期”
- componentDidMount() 方法在组件输出被渲染完毕后执行,是设置定时器的好时机
- 除了 this.props 和 this.state,我们可以自由地向勒中添加其他字段,比如 this.timeId
- 我们在 componentWillUnmount() 生命周期中清除定时器
- 最后我们实现一个
tick()方法,让Clock每秒运行一次,它使用this.setState()来安排对局部 state 的更新
4. 正确地使用 State
// 1. 不要直接修改状态。相反,我们要使用 setState()// Wrongthis.state.comment = 'Hello';// Correctthis.setState({comment: 'Hello'});// 2. state 更新可能是异步的// Wrongthis.setState({counter: this.state.counter + this.props.increment,});// Correctthis.setState((state, props) => ({counter: state.counter + props.increment}));// 3. state 更新会合并constructor(props) {super(props);this.state = {posts: [],comments: []};}componentDidMount() {fetchPosts().then(response => {this.setState({posts: response.posts});});fetchComments().then(response => {this.setState({comments: response.comments});});}
关于 setState(),我们应该要知道三件事
- 不要直接修改状态。相反,我们要使用
setState()- 直接修改状态不会重新渲染组件
- state 更新可能是异步的
- React 为了提高性能会将一批
setState()调用合并成一个 - 因此
this.state和this.props可能会异步更新,我们不能依赖的它们的值来计算下一个状态 - 如果想要正确依赖
this.state和this.props,可以使用 setState() 的第二个调用方式,它接受一个函数,第一个参数是 state,第二个参数是 props
- React 为了提高性能会将一批
- state 更新会合并
- 在调用
setState()的时,React 将提供的对象合并到当前的 state 中,而这个合并是浅合并
- 在调用
5. 数据流向下流动
- 父组件和子组件不知道一个组件是有状态的还是无状态的,也不关心它是函数组件还是类组件
- 这就是为什么 state 被称为局部和封装的原因。除了它自己,任何其他组件无法访问它
- 一个组件可以选择是否将 state 作为 props 传递给它的子组件
- 这通常称为“自顶向下”或“单向数据流”,任何 state 总是由某个特定组件拥有,并且任何数据和 UI 只能影响 tree 中“低于”它们的组件
- 在 React Apps 中,组件是否有状态被认为是组件的实现细节,这些细节可能会随着时间推移而改变
- 我们可以在有状态组件内部使用无状态组件
- 也可以在无状态组件内部使用有状态组件
六、事件处理
1. 与 DOM 事件处理做对比
<button onclick="activateLasers()">Activate Lasers</button><form onsubmit="console.log('You clicked submit.'); return false"><button type="submit">Submit</button></form>
<button onClick={activateLasers}>Activate Lasers</button>function Form() {function handleSubmit(e) {e.preventDefault();console.log('You clicked submit.');}return (<form onSubmit={handleSubmit}><button type="submit">Submit</button></form>);}
React 元素中的事件处理和在 DOM 处理事件很类似,但有些许不同
- React 的事件名使用小驼峰式,而不是小写命名
- 在 JSX 中将函数作为事件处理,而不是字符串
- 在 React 不能返回 false 来组织默认事件,必须显性地调用
preventDefault- 这里面的 e 是一个合成事件,React 根据 W3C 规范定义了这些合成事件
2. React 绑定事件的方法
class Toggle extends React.Component {constructor(props) {super(props);this.state = {isToggleOn: true};// This binding is necessary to make `this` work in the callbackthis.handleClick = this.handleClick.bind(this);}handleClick() {this.setState(prevState => ({isToggleOn: !prevState.isToggleOn}));}render() {return (<button onClick={this.handleClick}>{this.state.isToggleOn ? 'ON' : 'OFF'}</button>);}}ReactDOM.render(<Toggle />,document.getElementById('root'));// 使用公共类字段语法class LoggingButton extends React.Component {// This syntax ensures `this` is bound within handleClick.// Warning: this is *experimental* syntax.handleClick = () => {console.log('this is:', this);}render() {return (<button onClick={this.handleClick}>Click me</button>);}}// c. 事件函数使用箭头函数class LoggingButton extends React.Component {handleClick() {console.log('this is:', this);}render() {// This syntax ensures `this` is bound within handleClickreturn (<button onClick={() => this.handleClick()}>Click me</button>);}}
- 在使用 React 时,通常不需要调用
addEventListener在 DOM 元素创建后将监听器添加到该元素,相反,只需要在最初渲染元素时提供一个监听器 - 当使用
ES6类定义组件时,常见的模式是将事件处理程序作为类上的方法 - 使用时我们要时刻注意 this 问题,handleClick 中的 this 需要保证指向 Toggle 实例,有三种方法保存
- 在构造函数里使用绑定 this
- 使用公共类字段语法
- 回调函数使用箭头函数包裹
- 第三种方法的问题在于每次渲染 LoggingButton 时会创建一个不同的回调,在大多数情况下,它很好,但是如果这个回调作为一个 props 传递给下面组件,那么这些组件可能会执行额外的渲染
- 所以通常建议在构造函数中绑定或使用类字段语法,以避免此类性能问题
3. 将参数传递给事件处理器
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button><button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
- 上面两行是等价的,分别使用箭头函数和
Function.prototype.bind - 在这两种情况下,表示 React 事件的 e 将作为 ID 之后的第二个参数传递
七、条件渲染
1. 和 JavaScript 一样条件渲染
function UserGreeting(props) {return <h1>Welcome back!</h1>;}function GuestGreeting(props) {return <h1>Please sign up.</h1>;}function Greeting(props) {const isLoggedIn = props.isLoggedIn;if (isLoggedIn) {return <UserGreeting />;}return <GuestGreeting />;}ReactDOM.render(// Try changing to isLoggedIn={true}:<Greeting isLoggedIn={false} />,document.getElementById('root'));
- React 中的条件渲染与 JavaScript 中的条件渲染工作方式相同,使用
if或其他条件运算符就行了
2. 元素变量
class LoginControl extends React.Component {constructor(props) {super(props);this.handleLoginClick = this.handleLoginClick.bind(this);this.handleLogoutClick = this.handleLogoutClick.bind(this);this.state = {isLoggedIn: false};}handleLoginClick() {this.setState({isLoggedIn: true});}handleLogoutClick() {this.setState({isLoggedIn: false});}render() {const isLoggedIn = this.state.isLoggedIn;let button;if (isLoggedIn) {button = <LogoutButton onClick={this.handleLogoutClick} />;} else {button = <LoginButton onClick={this.handleLoginClick} />;}return (<div><Greeting isLoggedIn={isLoggedIn} />{button}</div>);}}ReactDOM.render(<LoginControl />,document.getElementById('root'));
- 使用变量存储元素,可以让我们有条件地渲染组件的一部分
3. 操作符 &&(内联的 if)
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('root'));// 返回的 <div>0<div>render() {const count = 0;return (<div>{ count && <h1>Messages: {count}</h1>}</div>);}
- 将表达式包装在大括号中来在 JSX 中嵌入表达式,里面使用操作符 &&
- true && expression 总是计算为 expression,而 false & expression 总是计算为 false
- 如果条件为 true,&& 右边的元素就会是输出结果,如果条件是 false,React 就会直接跳过
- 注意,如果返回的 falsy 表达式,React 会渲染出 falsy 表达式!
4. ?:表达式(内联的 if-else)
render() {const isLoggedIn = this.state.isLoggedIn;return (<div>The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.</div>);}render() {const isLoggedIn = this.state.isLoggedIn;return (<div>{isLoggedIn? <LogoutButton onClick={this.handleLogoutClick} />: <LoginButton onClick={this.handleLoginClick} />}</div>);}
- 使用 JavaScript 条件操作符
condition ? true: false - 使用哪种条件渲染的方式取决于我们认为哪个更有可读性
- 当条件变得过于复杂时,它可能是提取成组件的好时机
5. 阻止组件渲染
function WarningBanner(props) {if (!props.warn) {return null;}return (<div className="warning">Warning!</div>);}class Page extends React.Component {constructor(props) {super(props);this.state = {showWarning: true};this.handleToggleClick = this.handleToggleClick.bind(this);}handleToggleClick() {this.setState(state => ({showWarning: !state.showWarning}));}render() {return (<div><WarningBanner warn={this.state.showWarning} /><button onClick={this.handleToggleClick}>{this.state.showWarning ? 'Hide' : 'Show'}</button></div>);}}ReactDOM.render(<Page />,document.getElementById('root'));
- 在极少数的情况下,如果希望每个组件隐藏自己,而它由另一个组件渲染,可以 return null 达到目的
八、列表 && keys
1. 渲染多个组件
const numbers = [1, 2, 3, 4, 5];const listItems = numbers.map((number) =>(<li>{number}</li>));ReactDOM.render(<ul>{listItems}</ul>,document.getElementById('root'));
- 构建一个元素集合,并使用大括号 {} 将它们放入 JSX 中
2. 基本列表组件
function NumberList(props) {const numbers = props.numbers;const listItems = numbers.map((number) =><li key={number.toString()}>{number}</li>);return (<ul>{listItems}</ul>);}const numbers = [1, 2, 3, 4, 5];ReactDOM.render(<NumberList numbers={numbers} />,document.getElementById('root'));
- 通常我们会在一个组件中渲染列表
- 我们要为每个列表项分配一个 key
3. 使用 keys
const todoItems = todos.map((todo) =>(<li key={todo.id}>{todo.text}</li>));const todoItems2 = todos.map((todo, index) =>// Only do this if items have no stable IDs<li key={index}>{todo.text}</li>);
- keys 帮助 React 识别哪些项目被更改了、添加了、或被删除了
- 需要给列表中的每个元素 key,让元素具有稳定的标识
- 选择 key 的最好的方法就是使用一个在同级列表项中唯一标识它的字符串,大多数情况下是使用数据中 id
- 如果没有稳定的 id 给列表项,可以使用列表项索引作为最后的手段
- 如果项目的顺序可能发生变化,那么不建议用对键使用索引,这会对性能产生不好的影响
4. 用 keys 提取组件
function ListItem(props) {// Correct! There is no need to specify the key here:return <li>{props.value}</li>;}function NumberList(props) {const numbers = props.numbers;const listItems = numbers.map((number) =>// Correct! Key should be specified inside the array.<ListItem key={number.toString()} value={number} />);return (<ul>{listItems}</ul>);}const numbers = [1, 2, 3, 4, 5];ReactDOM.render(<NumberList numbers={numbers} />,document.getElementById('root'));
- keys 只有在周围数组的上下文才有意义
- 一个很好的经验法则是 map() 中的元素需要使用 keys
5. key 在兄弟节点之间必须唯一
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: 'Hello World', content: 'Welcome to learning React!'},{id: 2, title: 'Installation', content: 'You can install React from npm.'}];ReactDOM.render(<Blog posts={posts} />,document.getElementById('root'));
- 数组中的 key 在其兄弟数组中是唯一的,而不必是全局唯一,不同的数组可以使用相同的键
- key 用于 React 的提示,但不会传递给我们写的组件,如果想用它的值,需要显性地换个名字作为 props
6. JSX 中嵌入 map()
function NumberList(props) {const numbers = props.numbers;return (<ul>{numbers.map((number) =><ListItem key={number.toString()}value={number} />)}</ul>);}
- JSX 允许在花括号中嵌入任何表达式,所以我们可以内联
map()的结果 - 有时候这会导致更清晰的代码,但是这种风格会被滥用,如果 map() 嵌套得太多,可能是提取组件的好时机
1. 使用列表元素可以获得多倍快乐
React 元素的列表能够让我们获得多倍快乐,就像下面这样
const numbers = [1, 2, 3, 4, 5];const listItems = numbers.map((number) =><li>{number}</li>);ReactDOM.render(<ul>{listItems}</ul>,document.getElementById('root'))
2. 列表元素必须要有独一无二的 key
当运行以上代码会出现一个告警:Warning: Each child in an array or iterator should have a unique “key” prop. 意思是当创建一个元素时,必须包含一个独一无二的 key。
九、Forms
1. 原生的 form
<form><label>Name:<input type="text" name="name" /></label><input type="submit" value="Submit" /></form>
- 在React 中,form 元素与其他 DOM 元素不同,因为 form 元素通常会保存一些内部状态,例如,原生 HTML 的 form 只接受一个名字
- 当用户提交表单,此表单由默认的行为
2. 受控组件,文字输入 input 标签
class NameForm 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('A name was submitted: ' + this.state.value);event.preventDefault();}render() {return (<form onSubmit={this.handleSubmit}><label>Name:<input type="text" value={this.state.value} onChange={this.handleChange} /></label><input type="submit" value="Submit" /></form>);}}
- 在 HTML 中,像
