React - 用于构建用户界面的 JavaScript 库
1. JSX简介 (JavaScript XML)
- JSX是JS的语法扩展
- 可以生成React元素
1.1 使用JSX的原因
-
React认为渲染逻辑本质上与其他UI逻辑内在耦合。比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。
-
React未采用将标记与逻辑进行分离到不同文件的人为分离方式,而是通过将二者共同存放在松散耦合单元 ——**
组件
**中来实现关注点分离。 -
在JS中将JSX和UI放在一起,会在视觉上有辅助作用。
-
JSX还可使React显示更多有用的错误和警告信息。
1.2 在JSX中嵌入表达式
// 声明一个name变量,在JSX中使用它,并将其包裹在大括号中
const name = 'Wenbo Yang';
// JSX
const element = <h1>Hello, {name}</h1>;
React.render(
element,
document.getElementById('root')
);
-
在JSX中,可以在大括号内放置任何有效的 JavaScript 表达式。
-
示例:
将**
formatName(user)
的结果嵌入到<h1>
**中。function formatName(user) { return user.firstName + ' ' + user.lastName; } const user = { firstName: 'Yang', lastName: 'Wenbo' }; const element = ( <h1> Hello, {formatName(user)}! </h1> ) ReactDom.render( element, document.getElementById('root') )
-
建议
为了便于阅读,React会将 JSX 拆分为多行。故建议将内容包裹在括号中,虽然这样做不是强制要求的,但是这可以避免遇到自动插入分号陷阱。
1.3 JSX自身亦为表达式
-
在编译之后,JSX会被转为普通的JavaScript函数调用,并且对其取值后得到JavaScript对象。
-
换言之,可以在if语句和for循环的代码块中使用JSX。
-
将JSX赋值给变量。
-
将JSX当作参数传入。
-
可在函数中返回JSX等。
-
示例:
function getGreeting(user) { if (user) { // 在函数中返回JSX return <h1>Hello, {formatName(user)}!</h1>; } return <h1>Hello, Stranger.</h1>; }
1.4 JSX特定属性
-
可通过使用引号,来将属性值指定为字符串字面量。
const element = <div tabIndex="0"></div>;
-
可使用大括号,在属性值中插入一个JavsScript表达式。
const element = <img src={user.avatarUrl}></img>;
-
对于同一属性不能同时使用引号或大括号。
-
JSX语法更接近JavaScript,在React DOM需使用小驼峰命名来定义属性名称,而不使用HTML属性名称的命名约定。
1.5 JSX指定子元素
-
若一个标签里没有内容时可使用
/>
来闭合标签。const element = <img src={user.avatarUrl}/>;
-
JSX标签里可包含多个子元素。
1.6 JSX防止注入攻击
-
我们可以安全地在JSX中插入用户输入内容。
const title = response.potentiallyMaliciousInput; // 可直接使用 const element = <h1>{title}</h1>
-
React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。
转义字符:
'"': '"', '\'': ''', '&': '&', '<': '<', '>': '>'
1.7 JSX表示对象
-
Babel会把JSX转译成一个名为 React.createElement() 函数调用。
const element = ( <h1 className="greeting"> Hello, world! </h1> ); // 以上代码完全等效于: const element = React.createElement( 'h1', {className: 'greeting'}, 'Hello, world!' ); // React.createElement() 会预先执行一些检查,以帮助我们编写无错代码,实际上在执行过程中它会创建以下对象: const element = { // 注意:这是简化过的结构 type: 'h1', props: { className: 'greeting', children: 'Hello, world!' } }; // 这些对象被称为React元素,其描述了我们希望在屏幕上看到的内容。 // React通过读取这些对象,然后使用它们来构建DOM以及保持随时更新。
2. 元素渲染
- 元素是构成React应用的最小砖块。
- 元素描述了我们想在屏幕上看到的内容。
- React元素是创建开销极小的普通对象。
- React DOM会负责更新DOM来与React元素保持一致。
2.1 将一个元素渲染为DOM
-
“根” DOM节点
// 假设你的HTML文件处有一个<div>: <div id="root"></div> // 该节点被称为“根”DOM节点,因为该节点内的所有内容都将由React DOM管理。
-
仅使用React构建的应用通常只有单一的根DOM节点。倘若React集成进一个已有应用时,可以在应用中包含任意多的独立根DOM节点。
-
将React元素渲染到根DOM节点,只需将其传入
ReactDOM.render()
:const element = <h1>Hello, World</h1>; ReactDOM.render( element, document.getElementById('root'); )
2.2 更新已渲染的元素
-
React元素是不可变对象。一经创建,便无法更改其子元素或属性。
-
更新UI唯一的方式是创建一个全新的元素,并将其传入
ReactDOM.render()
。 -
示例:
// 计时器 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()
2.3 React只更新它需要更新的部分
- React DOM 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使DOM达到预期的状态。
3. 组件 & Props
- 组件允许将UI拆分为独立可复用的代码片段,并对每个片段进行独立构思。
- 组件可以接受任意的入参(props),并返回用于描述页面展示内容的React元素。
3.1 函数组件与 class 组件
-
函数组件 – 定义组件最简单的方式
如果你想写的组件
只包含一个render()
方法,并且不包含state
时,使用函数组件会更简单。我们无需定义一个继承于React.Component的类,可以定义一个函数,将props作为参数,并return需要渲染的元素。function Welcome(props) { return <h1>Hello,{props.name}</h1>; }
-
class 组件
class Welcome extends React.Component { render() { return <h1>Hello,{this.props.name}</h1>; } }
3.2 渲染组件
-
React元素可以是DOM标签,也可以是用户自定义组件。
-
当React元素为用户自定义组件时,它会将JSX所接收的属性及子组件转换为单个对象传递给组件,这个对象被称之为“props”。
示例:
function Welcome(props) { return <h1>Hello, {props.name}</h1>; } const element = <Welcome name="sara"/>; ReactDOM.render( element, document.getElementById('root'); )
-
注意:
组件名称必须以大写字母开头;
React会将以小写字母开头的组件视为原生DOM标签;
3.3 组合组件
-
组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。按钮,表单,对话框,甚至整个屏幕的内容:在 React 应用程序中,这些通常都会以组件的形式表示。
示例:
// 创建一个可以多次渲染Welcome组件的App组件 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 应用程序的顶层组件都是
App
组件。但是,如果你将 React 集成到现有的应用程序中,你可能需要使用像Button
这样的小组件,并自下而上地将这类组件逐步应用到视图层的每一处。
3.4 提取组件
- 根据实际应用将组件拆分为更小的组件
3.5 Props 的只读性
-
无论是函数组件还是 class 组件,都不能修改自身的props。
示例:
function sum(a, b) { return a + b; } // 以上函数被称为纯函数,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果; // 非纯函数示例 -- 更改了自己的入参 function withdraw(account, amount) { account.total -= amount; }
-
React严格规定: 所有React组件都必须像纯函数一样保护它们的props不被更改。
4. State & 生命周期
-
State 与 Props 类似,但是 state 是私有的,并且完全受控于当前组件。
-
挂载(mount):当组件第一次被渲染到DOM时
-
卸载(unmount):当DOM中的组件被删除时
-
State不能直接修改,需使用**
setState()
** 修改State的值this.setState( {coment: 'Hello'} );
-
构造函数是唯一可以给 this.state 赋值给的地方
-
State 的更新可能是异步的
出于性能考虑,React会把多个 setState() 调用合并成一个调用。因为 this.props 和 this. state 可能会异步更新,所以不能依赖他们的值来更新下一个状态。
示例:
// 错误示例 // 因异步问题,此代码可能无法更新数据 this.setState({ counter: this.state.counter + this.props.increment, }) // 正确示例 // 可以让 setState() 接受一个函数而不是对象,此函数用上一个state作为第一个参数,将此次更新被应用时的props作为第二个参数 this.setState((state, props) => ({ counter: state.counter + props.increment }))
-
State 的更新会被合并
当调用 setState() 时,React 会将你提供的对象合并到当前的state。
示例:
// 例如,你的state包含几个独立的变量 constructor(props) { super(props); this.state = { posts: [], comments: [] } } // 然后我们可以分别调用 setState() 来单独地更新它们: componentDidMount () { fetchPosts().then(res => { this.setState({ posts: res.posts }); }); fetchComments().then(res => { this.setStae({ comments: res.comments }) }) } // 这里的合并是浅合并,所以 this.setState({comments})完整保留了this.state.posts. 但是完全替换了 this.state.comments. // 只替换了该替换的
-
数据是向下流动的
不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。
这就是为什么称 state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。
组件可以选择把它的 state 作为 props 向下传递到它的子组件中:
<FormattedDate date={this.state.date} />
FormattedDate
组件会在其 props 中接收参数date
,但是组件本身无法知道它是来自于Clock
的 state,或是Clock
的 props,还是手动输入的:function FormattedDate(props) { return <h2>It is {props.date.toLocaleTimeString()}.</h2>; }
这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。
如果你把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。
在 React 应用中,组件是有状态组件还是无状态组件属于组件实现的细节,它可能会随着时间的推移而改变。你可以在有状态的组件中使用无状态的组件,反之亦然。
5. 事件处理
-
React 事件命名采用小驼峰式。
-
使用JSX语法时需要传入一个函数作为事件处理函数,而不是一个字符串。
// 传统的HTML <button onclick="activateLasers()"> Activate Lasers </button> // React 中的事件 <button onClick={activateLasers}> Activate Lasers </button>
-
React 中不能通过返回 false 的方式阻止默认行为。必须显示的使用 preventDefault
-
使用React 时,一般不需要使用 addEventListener 为已创建的DOM 元素添加监听器。事实上,我们只需要在该元素初始渲染的时候添加监听器即可。
-
当使用 ES6 class 语法定义一个组件的时候,通常的做法是将事件处理函数声明为 class 中的方法。
示例:
// 下面的 Toggle 组件会渲染一个让用户切换开关状态的按钮: class Toggle extends React.Component { constructor(props) { super(props); this.state = {isToggleOn: true}; // 为了在回调中使用 `this`,这个绑定是必不可少的 this.handleClick = this.handleClick.bind(this); } handleClick() { this.setState(state => ({ isToggleOn: !state.isToggleOn })); } render() { return ( <button onClick={this.handleClick}> {this.state.isToggleOn ? 'ON' : 'OFF'} </button> ); } } ReactDOM.render( <Toggle />, document.getElementById('root') );
注意:
// 使用 class 的方法时必须绑定this 指向,否则将导致 this 值为 undefined。
this.handleClick = this.handleClick.bind(this);
-
向事件处理程序传递参数
-
箭头函数传参方式,事件对象必须显示的进行传递。
-
通过 bind 方式,事件对象以及更多的参数将会被隐式的进行传递。
-
以上两种方式,React 的事件对象 e 会被作为第二个参数传递。
-
示例:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button> <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
-
6. 条件渲染
条件渲染示例跳过
6.1 JSX 中内联条件渲染的方法
-
与运算符 &&
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') );
之所以能这样做是因为,在 JavaScript 中,true && expression 总是会返回 expression,而 false && expression 总是返回 false。
-
三目运算符
render() { const isLoggedIn = this.state.isLoggedIn; return ( <div> {isLoggedIn ? <LogoutButton onClick={this.handleLogoutClick} /> : <LoginButton onClick={this.handleLoginClick} /> } </div> ); }
-
阻止组件渲染
在极少数情况下,我们可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,我们可以让 render 方法直接返回 null, 而不进行任何渲染。
在组件的 render 方法中返回 null 并不会影响组件的生命周期。
示例:
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') );
7. 列表 & Key
7.1 渲染多个组件
我们可通过使用 {} 在 JXS 内构造一个元素集合。
示例:
const number = [1, 2, 3, 4, 5];
const listItems = number.map((numbers) => <li>{numbers}</li>);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<ul>{listItems}</ul>);
7.2 基础列表组件
通常我们需要在一个组件中渲染列表。
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 必须定义 Key 值
<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')
);
7.3 key
-
key 帮助 React 识别哪些元素改变了,因此需要给数组中的每一个元素赋予一个确定的标识。
-
key 必须唯一,且不建议用索引号做key。
7.4 用 key 提取组件
-
元素的 key 只有放在就近的数组上下文中才有意义。
-
一个好的经验法则是:在
map()
方法中的元素需要设置 key 属性。
7.5 key 只是在兄弟节点之间必须唯一
- 数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的 key 值。
- key 会传递信息给 React,但不会传递给组件。
7.6 在 JSX 中嵌入 map()
8. 表单
在 React 里,HTML 表单元素的工作方式和其他的 DOM 元素有些不同,因为表单元素通常会保持一些内部的 state。
8.1 受控组件
-
HTML 中,表单元素通常自己维护 state,并根据用户输入进行更新。在 React 中,可变状态 (mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState() 来更新。
-
受控组件:渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素叫做“受控组件”。
示例:
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('提交的名字: ' + 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="提交" /> </form> ); } } // 由于在表单元素上设置了value属性, 因此显示的值将始终为 this.state.value,这使得React 的 state 成为唯一数据源。由于handleChange 在每次按键时都会执行并更新React的state, 因此显示的值将随着用户输入而更新。
-
对于受控组件而言,输入的值始终由React 的 state 驱动。
8.2 textarea 标签
-
HTML 中, 元素通过其子元素定义其文本;React 中,使用 value 属性代替。
示例:
// HTML中 <textarea> 文本信息 </textarea> // React 中 constructor(props) { super(props); this.state = { value: '请撰写一篇关于你喜欢的 DOM 元素的文章.' }; } <textarea value={this.state.value} />
8.3 select 标签
-
HTML 中, 创建下拉列表标签,子选项因 selected 属性的缘故,会被默认选中。React 中并不会使用 selected 属性,而是在 根select 标签上使用 value 属性。
示例:
// HTML中 <select> <option selected value="coconut">椰子</option> <option value="mango">芒果</option> </select> // React中 constructor(props) { super(props); this.state = {value: 'coconut'}; } <select value={this.state.value} > <option value="coconut">椰子</option> <option value="mango">芒果</option> </select>
注意:
可以将数组传递到value属性中,实现 select 标签中的多选:
<select multiple={true} value={['B', 'C']}>
8.4 文件 input 标签
- HTML 中,允许用户从存储设备中选择一个或多个文件,将其上传到服务器,或通过使用 JS 的 File API 进行控制。
- 因为其 value 只读,所以它是React 中的一个非受控组件。
8.5 处理多个输入
- 当需要处理多个
input
元素时,我们可以给每个元素添加name
属性,并让处理函数根据event.target.name
的值选择要执行的操作。
8.6 受控输入空值
在受控组件上指定 value 的 prop 会阻止用户更改输入。如果指定了 value,但输入仍可编辑,则可能是意外地将 value 设置为 undefined 或 null。
下面的代码演示了这一点。(输入最初被锁定,但在短时间延迟后变为可编辑。)
ReactDOM.render(<input value="hi" />, mountNode);
setTimeout(function() {
ReactDOM.render(<input value={null} />, mountNode);
}, 1000);
8.7 受控组件的替代品
9. 状态提升
代码示例查看官网
10. 组合 vs 继承
10.1 包含关系
-
有些组件无法提前知晓他们子组件的具体内容。建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中。
这使得别的组件可以通过JSX嵌套,将任意组件作为子组件传递给它们。
示例:
function FancyBorder(props) { return ( <div className={'FancyBorder FancyBorder-' + props.color}> {props.children} </div> ); } function WelcomeDialog() { return ( <FancyBorder color="blue"> <h1 className="Dialog-title"> Welcome </h1> <p className="Dialog-message"> Thank you for visiting our spacecraft! </p> </FancyBorder> ); }
-
少数情况下,可能需要在一个组件中预留出几个”洞“。这种情况下,可以不使用 children,而是自行约定:将所需内容传入props,并使用相应的prop。
示例:
function SplitPane(props) { return ( <div className="SplitPane"> <div className="SplitPane-left"> {props.left} </div> <div className="SplitPane-right"> {props.right} </div> </div> ); } function App() { return ( <SplitPane left={ <Contacts /> } right={ <Chat /> } /> ); }