React 是一个 JavaScript 库
ReactDOM.render( <h1>Hello, world</h1>, document.getElementById('root') );JSX,是一个 JavaScript 的语法扩展。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。JSX 可以生成 React “元素”。
为什么使用 JSX ? React 认为渲染逻辑本质上与其他 UI 逻辑内在耦合,React 没有采用将标记与逻辑进行分离到不同文件而是通过将二者存放在组件(松散耦合单元)之中,来实现关注点分离。React 不强制要求使用 JSX ,但在 JavaScript 中将 JSX 和 UI 放在一起,会有视觉辅助作用,还可以使 React 显示更多有用的错误和警告。
在 JSX 中嵌入表达式 在 JSX 语法中,可以在大括号内放置任何有效的 JavaScript 表达式。
const name = 'Josh Perez'; const element = <h1>Hello, {name}</h1>; ReactDOM.render( element, document.getElementById('root') ); function formatName(user) { return user.firstName + ' ' + user.lastName; } const user = { firstName: 'Harper', lastName: 'Perez' }; // 将内容包裹在括号中,可以避免遇到自动插入分号陷阱 const element = ( <h1> Hello, {formatName(user)}! </h1> ); ReactDOM.render( element, document.getElementById('root') );JSX 也是一个表达式 在编译之后,JSX 表达式会被转为普通 JavaScript 函数调用,并且对其取值后得到 JavaScript 对象。可以在 if 语句和 for 循环的代码块中使用 JSX,将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX:
function getGreeting(user) { if (user) { return <h1>Hello, {formatName(user)}!</h1>; } return <h1>Hello, Stranger.</h1>; }JSX 特定属性 可以通过使用引号,来将属性值指定为字符串字面量,也可以使用大括号,来在属性值中插入一个 JavaScript 表达式:
JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。例如,JSX 里的 class 变成了 className,而 tabindex 则变为 tabIndex。
使用 JSX 指定子元素
// JSX 可以使用 /> 来闭合标签,就像 XML 语法一样 const element = <img src={user.avatarUrl} />; // JSX 标签里能够包含很多子元素 const element = ( <div> <h1>Hello!</h1> <h2>Good to see you here.</h2> </div> );JSX 防止注入攻击 React DOM 在渲染所有输入内容之前,默认会进行转义。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。
const title = response.potentiallyMaliciousInput; // 直接使用是安全的: const element = <h1>{title}</h1>;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 以及保持随时更新。
元素是构成 React 应用的最小砖块。与浏览器的 DOM 元素不同,React 元素是创建开销极小的普通对象。React DOM 会负责更新 DOM 来与 React 元素保持一致。
const element = <h1>Hello, world</h1>将一个元素渲染为 DOM HTML 文件某处有一个 <div>:
// 该节点内的所有内容都有 React DOM 管理,仅使用 React 构建的应用通常只有单一的根节点 <div id="root"></div>将一个 React 元素渲染到根 DOM 节点,只需把它们一起传入 ReactDOM.render():
const element = <h1>Hello, world</h1>; ReactDOM.render(element, document.getElementById('root'));更新已渲染的元素 React 元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。 一个元素就像电影的单帧:它代表了某个特定时刻的 UI。 根据我们已有的知识,更新 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()。 后续提到如何将这些代码封装到有状态组件中。
React 只更新它需要更新的部分 React DOM 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 DOM 达到预期的状态。
组件 从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。
函数组件与 class 组件 函数式组件(本质上是 JavaScript 函数):
// 接收唯一带有数据的‘props’(代表属性对象)并返回一个 React 元素 function Welcome(props) { return <h1>Hello, {props.name}</h1>; }ES6 的 class 组件:
class Welcome extends React.Component { render () { return <h1>Hello, {this.props.name}</h1> } }上述两个组件在 React 中是等效的。
渲染组件 React 元素可以是 DOM 标签,也可以是用户自定义的组件,当 React 元素为自定义组件时,它会将 JSX 所接收的属性转换成名为 “props” 的对象的属性,并将 props 传递给组件,例如:
function Welcome(props) { return <h1>Hello, {props.name}</h1> } const element = <Welcome name="tqs"/>; ReactDOM.render( element, documment.getElementById('root') )组件名称必须以大写字母开头。 React 会将以小写字母开头的组件视为原生 DOM 标签。例如,<div /> 代表 HTML 的 div 标签,而 <Welcome /> 则代表一个组件,并且需在作用域内使用 Welcome。
组合组件 组件可以在其输出中引用其他组件。例如:
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') );提取组件 将组件拆分为更小的组件。例如:
function Comment(props) { return ( // ---------------------------------------------------------Comment <div className="Comment"> // ------------------------------------------------UserInfo <div className="UserInfo"> // -----------------------------------------Avatar <img className="Avatar" src={props.author.avatarUrl} alt={props.author.name} /> // -----------------------------------------Avatar <div className="UserInfo-name"> {props.author.name} </div> </div> // ------------------------------------------------UserInfo <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate(props.date)} </div> </div> // ---------------------------------------------------------Comment ); }提取组件时,建议从组件自身的角度命名 props,而不是依赖于调用组件的上下文命名。 提取后结果:
function Avatar(props) { // props 起了一个更通用的名字:user,而不是 author return (<img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} />); } function UserInfo(props) { return ( <div className="UserInfo"> <Avatar user={props.user} /> <div className="UserInfo-name"> {props.author.name} </div> </div> ); } function Comment(props) { return ( <div className="Comment"> <UserInfo user={props.author} /> <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate(props.date)} </div> </div> ); }props 的只读性 组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。React 非常灵活,但它也有一个严格的规则:所有 React 组件都必须像纯函数 (该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果)一样保护它们的 props 不被更改。
state 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。
如何封装真正可复用的 Clock 组件?它将设置自己的计时器并每秒更新一次。
function Clock(props) { return ( <div> <h1>Hello, world!</h1> <h2>It is {props.date.toLocaleTimeString()}.</h2> </div> ); } function tick() { ReactDOM.render( <Clock date={new Date()} />, document.getElementById('root') ); } setInterval(tick, 1000);将函数组件转换成 class 组件
class Clock extends React.Component { render () { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.props.date.toLocaleTimeString()}.</h2> </div> ); } }向 class 组件中添加局部的 state class 组件应该始终使用 props 参数来调用父类的构造函数。
class Clock extends React.Component { // 1.添加一个 class 构造函数,然后在该函数中为 this.state 赋初值 constructor(props) { // 通过以下方式将 props 传递到父类的构造函数中 supper(props); this.state = {date: new Date()}; } render () { return ( <div> <h1>Hello, world!</h1> // 2.把 render() 方法中的 this.props.date 替换成 this.state.date <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } }移除 <Clock /> 元素中的 date 属性
ReactDOM.render( <Clock />, document.getElementById('root') );将生命周期方法添加到 Class 中 可以为 class 组件声明一些特殊的方法,当组件挂载或卸载时就会去执行这些方法,这些方法叫做“生命周期方法”。
class Clock extends React.Component { // 1.当 <Clock /> 被传给 ReactDOM.render()的时候,React 会调用 Clock 组件的构造函数。 // 因为 Clock 需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化 this.state。我们会在之后更新 state。 constructor(props) { super(props); this.state = {date: new Date()}; } // 当 Clock 的输出被插入到 DOM 中后, React 就会调用 ComponentDidMount() 生命周期方法。 // 在这个方法中,Clock 组件向浏览器请求设置一个计时器来每秒调用一次组件的 tick() 方法。 componentDidMount() { // 尽管 this.props 和 this.state 是 React 本身设置的,且都拥有特殊的含义, // 但是其实你可以向 class 中随意添加不参与数据流(比如计时器 ID)的额外字段。 this.timerID = setInterval( () => this.tick(), 1000 ); } // 5.一旦 Clock 组件从 DOM 中被移除,React 就会调用 componentWillUnmount() 生命周期方法,这样计时器就停止了。 componentWillUnmount() { clearInterval(this.timerID); } // 4.浏览器每秒都会调用一次 tick() 方法。 在这方法之中,Clock 组件会通过调用 setState() 来计划进行一次 UI 更新。 tick() { this.setState({ date: new Date() }); } // 2.之后 React 会调用组件的 render() 方法。这就是 React 确定该在页面上展示什么的方式。 // 然后 React 更新 DOM 来匹配 Clock 渲染的输出。 // 得益于 setState() 的调用,React 能够知道 state 已经改变了,然后会重新调用 render() 方法来确定页面上该显示什么。这一次,render() 方法中的 this.state.date 就不一样了,如此以来就会渲染输出更新过的时间。React 也会相应的更新 DOM。 render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } } ReactDOM.render( <Clock />, document.getElementById('root') );正确地使用 State 构造函数是唯一可以给 state 赋值的地方
// Wrong 此代码不会重新渲染组件 this.state.comment = 'hello'; // Correct this.setState({comment: 'hello'})State 的更新可能是异步的 出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。
因为 this.props 和 this.state 可能会异步更新,所以不要依赖他们的值来更新下一个状态。 例如,此代码可能会无法更新计数器:
// Wrong this.setState({ counter: this.state.counter + this.props.increment, });要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
// Correct // 使用箭头函数/普通的函数也同样可以 this.setState((state, props) => ({ counter: state.counter + props.increment }));state 的更新会被合并 React 会把 setState() 提供的对象合并到当前的 state,这里的合并是浅合并,只添加或更新 setState() 提供的对象具有的属性
数据是向下流动的 任何 state 总是属于特定组件,而从该 state 派生的任何数据或 UI 只能影响它们的子(后代)组件,通常被叫做“自上而下”或“单向”的数据流。state 为局部的,除了拥有并设置了它的组件,其他组件都无法访问,组件可以选择把它的 state 作为 props 向下传递到它的子组件中。
React 元素的事件处理与 DOM 元素相似,但语法略有不同:React 事件的命名采用小驼峰式(camelCase),而不是纯小写;使用 JSX 语法时需要传入函数而不是字符串。另一个不同点是 React 中不能通过返回 false 的方式阻止默认行为,必须显示调用 preventDefault
function ActionLink() { // e 是一个合成事件。React 根据 W3C 规范来定义这些合成事件,不需要担心跨浏览器的兼容性问题。 function handleClick(e) { e.preventDefault(); console.log('The link was clicked.'); } return ( <a href="#" onClick={handleClick}> Click me </a> ); }使用 ES6 class 语法定义一个组件的时候,通常的做法是将事件处理函数声明为 class 中的方法
class Toggle extends React.Component { constructor(props) { super(props); this.state = {isToggleOn: true}; // 为了在回调中使用 `this`,这个绑定是必不可少的 // 将 handleClick 函数声明换成 handleClick = () => {this.setState(state => ({isToggleOn: !state.isToggleOn}));} ,可去掉此步骤 this.handleClick = this.handleClick.bind(this); } handleClick() { // 箭头函数高级语法:加括号的函数体返回对象字面表达式 this.setState(state => ({isToggleOn: !state.isToggleOn})); } render() { return ( /* 为解决 this 问题也可以使用箭头函数,问题在于每次渲染时都会创建不同的回调函数。 多数情况下,没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染*/ // <button onClick={e => this.handleClick(e)}}> {this.state.isToggleOn ? 'ON' : 'OFF'} </button> <button onClick={this.handleClick}> {this.state.isToggleOn ? 'ON' : 'OFF'} </button> ); } } ReactDOM.render( <Toggle/>, document.getElementById('root) );向事件处理程序传递参数 可以通过箭头函数或 Function.prototype.bind 来实现,例如:
<button onClick={e => this.deleteRow(id, e)}>deleteRow</button> <button onClick={this.deleteRow.bind(this, id)}>deleteRow</button>上述两种方式是等价的,在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。
React 中的条件渲染和 JavaScript 中的一样,使用 JavaScript 运算符 if 或者条件运算符去创建元素来表现当前的状态,然后让 React 根据它们来更新 UI。
元素变量 可以使用变量来储存元素。 帮助有条件地渲染组件的一部分,而其他的渲染部分并不会因此而改变。
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> ); } }与运算符 && JavaScript 中的逻辑与 (&&) 运算符,可以很方便地进行元素的条件渲染
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 方法直接返回 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') );在组件的 render 方法中返回 null 并不会影响组件的生命周期。 例如,上面这个示例中,componentDidUpdate 依然会被调用。
基础列表组件
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} />, documnet.getElementById('root') );运行这段代码,将会看到一个警告 a key should be provided for list items,意思是当你创建一个元素时,必须包括一个特殊的 key 属性。
key key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此应当给数组中的每一个元素赋予一个确定的标识。通常,使用来自数据 id 来作为元素的 key;当元素没有确定 id 的时候,万不得已可以使用元素索引 index 作为 key,如果列表项目的顺序可能会变化,会导致性能变差,还可能引起组件状态的问题。不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值
const numbers = [1, 2, 3, 4, 5]; const listItems = numbers.map((number) => // 一个元素的 key 最好是在列表中拥有的一个独一无二的字符串 <li key={number.toString()}> {number} </li> );用 key 提取组件 元素的 key 只有放在就近的数组上下文中才有意义。例如,如果提取 出一个 ListItem 组件,应该把 key 保留在数组中的这个 元素上,而不是放在 ListItem 组件中的
元素上。 function ListItem(props) { const value = props.value; return ( // 错误!你不需要在这里指定 key: <li key={value.toString()}> {value} </li> ); } function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => // 错误!元素的 key 应该在这里指定: <ListItem value={number} /> ); return ( <ul> {listItems} </ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') ); function ListItem(props) { // 正确!这里不需要指定 key: return <li>{props.value}</li>; } function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => // 正确!key 应该在数组的上下文中被指定 <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') );key 只是在兄弟节点之间必须唯一 数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的。当生成两个不同的数组时,可以使用相同的 key 值。key 会传递信息给 React ,但不会传递给自定义的组件。 如果组件中需要使用 key 属性的值,需要用其他属性名显式传递这个值
在 React 里,HTML 表单元素的工作方式和其他的 DOM 元素有些不同,这是因为表单元素通常会保持一些内部的 state。例如这个纯 HTML 表单只接受一个名称:
<form> <label> 名字: <input type="text" name="name" /> </label> <input type="submit" value="提交" /> </form>此表单具有默认的 HTML 表单行为,即在用户提交表单后浏览到新页面。在 React 中执行相同的代码,它依然有效。但大多数情况下,使用 JavaScript 函数可以很方便的处理表单的提交, 同时还可以访问用户填写的表单数据。实现这种效果的标准方式是使用“受控组件”。
受控组件 在 HTML 中,表单元素(如、 和 )之类的表单元素通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。例如:
class NameForm extends React.Component { constructor(props) { super(props); this.state = {value: ''}; } handleChange = e => this.setState({value: event.target.value}) handleSubmit = e => { e.preventDefault(); alert('提交的名字:' + this.state.value) } render() { return ( <form onSubmit={this.handleSubmit}> <label> 名字: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="提交"></input> </form> ); } }textarea 在 HTML 中, 元素通过其子元素定义其文本:
<textarea> 你好, 这是在 text area 里的文本 </textarea>而在 React 中, 使用 value 属性代替。这样,可以使得使用 的表单和使用单行 input 的表单非常类似
select 标签 在 HTML 中, 创建下拉列表标签。例如,如下 HTML 创建了水果相关的下拉列表:
<select> <option value="grapefruit">葡萄柚</option> <option value="lime">柠檬</option> <option selected value="coconut">椰子</option> <option value="mango">芒果</option> </select>由于 selected 属性的缘故,椰子选项默认被选中。React 并不会使用 selected 属性,而是在根 select 标签上使用 value 属性。这在受控组件中更便捷,只需要在根标签中更新它。例如:
class FlavorFrom extends React.Component { constructor(props) { super(props); this.state = {value: 'cocount'}; } handleChange = e => this.setState({value: e.target.value}); handleSubmit = e => { e.preventDefault(); alert('你喜欢的风味是:' + this.state.value); } render() { return ( <form onSubmit={this.handleSubmit}> <label> 选择你喜欢的风味: <select value={this.state.value} onChange={this.handleChange}> <option value="grapefruit">葡萄柚</option> <option value="lime">柠檬</option> <option value="coconut">椰子</option> <option value="mango">芒果</option> </select> </label> <input type="submit" value="提交" /> </form> ); } }可以将数组传递到 value 属性中,以支持在 select 标签中选择多个选项
文件 input 标签 在 HTML 中, 允许用户从存储设备中选择一个或多个文件,将其上传到服务器,或通过使用 JavaScript 的 File API 进行控制。
<input type="file" />因为它的 value 只读,所以它是 React 中的一个非受控组件。
处理多个输入 当需要处理多个 input 元素时,可以给每个元素添加 name 属性,并让处理函数根据 event.target.name 的值选择要执行的操作。例如:
class Reservation extends React.Component { constructor(props) { super(props); this.state = { isGoing: true, numberOfGuests: 2 }; } handleInputChange = e => { const target = event.target; const value = target.type === 'checkbox' ? target.checked : target.value; const name = target.name; // 使用了 ES6 计算属性名称的语法,等同 ES5: // var partialState = {}; // partialState[name] = value; // this.setState(partialState); this.setState({ [name]: value }); } render() { return ( <form> <label> 参与: <input name="isGoing" type="checkbox" checked={this.state.isGoing} onChange={this.handleInputChange} /> </label> <br /> <label> 来宾人数: <input name="numberOfGuests" type="number" value={this.state.numberOfGuests} onChange={this.handleInputChange} /> </label> </form> ); } }受控输入空值 在受控组件上指定 value 的 prop 可以防止用户更改输入。如果指定了 value,但输入仍可编辑,则可能是意外地将value 设置为 undefined 或 null。 下面的代码演示了这一点。(输入最初被锁定,但在短时间延迟后变为可编辑。)
ReactDOM.render(<input value="hi" />, mountNode); setTimeout(function() { ReactDOM.render(<input value={null} />, mountNode); }, 1000);受控组件的替代品 有时使用受控组件会很麻烦,需要为数据变化的每种方式都编写事件处理函数,并通过一个 React 组件传递所有的输入 state。将 React 应用程序与非 React 库集成时,这可能会令人厌烦。在这些情况下,可使用非受控组件, 这是实现输入表单的另一种方式。
成熟的解决方案 包含验证、追踪访问字段以及处理表单提交的完整解决方案,使用 Formik 是不错的选择。然而,它也是建立在受控组件和管理 state 的基础之上
通常,多个组件需要反映相同的变化数据,这时最好将共享状态提升到最近的共同父组件中去。例如:
const scaleNames = { c: 'Celsius', f: 'Fahrenheit' } function toCelsius(fahrenheit) { return (fahrenheit - 32) * 5 / 9; } function toFarenheit(celsius) { return (celsius * 9 / 5) + 32; } function tryConvert(temperature, convert) { const val = parseFloat(temperature); if (Number.isNaN(val)) { return ''; } return '' + convert(val); } function BoilingVerdict(props) { if(props.celsius >= 100) { return <p>The water would boil.</p>; } return <p>The water would not boil.</p>; } class TemperatureInput extends React.Component { constructor(props) { super(props); } handleChange = e => this.props.onTemperatureChange(e.target.value); render() { const temperature = this.props.temperature; const scale = this.props.scale; return ( <fieldset> <legend>Enter temperature in {scaleNames[scale]} :</legend> <input value={temperature} onChange={this.handleChange} /> </fieldset> } } class Calculator extends React.Component { constructor(props) { super(props); this.state = {temperature: '', scale: 'c'}; } handleCelsiusChange = temperature => this.setState({temperature: temperature, scale: 'c'}); handleFarenheitChange = temperature => this.setState({temperature: temperature, scale: 'f'}); render() { const temperature = this.state.temperature; const scale = this.state.scale; const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature; const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) :temperature; return ( <div> <TemperatureInput scale="c" temperature={celsius} onTemperatureChange={this.handleCelsiusChange} /> <TemperatureInput scale="f" temperature={fahrenheit} onTemperatureChange={this.handleFahrenheitChange} /> <BoilingVerdict celsius={parseFloat(celsius)} /> </div> ); } }在 React 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。 通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么将它提升至这些组件的最近共同父组件中。应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。
虽然提升 state 方式比双向绑定方式需要编写更多的“样板”代码,但带来的好处是,排查和隔离 bug 所需的工作量将会变少。如果某些数据可以由 props 或 state 推导得出,那么它就不应该存在于 state 中。
React 有十分强大的组合模式。推荐使用组合而非继承来实现组件间的代码重用。
包含关系 有些组件无法提前知晓它们子组件的具体内容。在 Sidebar(侧边栏)和 Dialog(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中:
function FancyBorder(props) { return ( <div className={'FancyBorder FancyBorder-' + props.color}> {props.children} </div> ); }别的组件可以通过 JSX 嵌套,将任意组件作为子组件传递给它们。
function WelcomeDialog() { return ( <FancyBorder color="blue"> <h1 className="Dialog-title"> Welcome </h1> <p className="Dialog-message"> Thank you for visiting our spacecraft! </p> </FancyBorder> ); }JSX 标签中的所有内容都会作为一个 children prop 传递给 FancyBorder 组件。因为 FancyBorder 将 {props.children} 渲染在一个
中,被传递的这些子组件最终都会出现在输出结果中。少数情况下,可能需要在一个组件中预留出几个“洞”。这种情况下,可以不使用 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 /> } /> ); }和 之类的 React 元素本质就是对象(object),可以把它们当作 props,像其他数据一样传递。类似别的库中“槽”(slot)的概念,但在 React 中没有“槽”这一概念的限制,可以将任何东西作为 props 进行传递。
特例关系 有些时候,我们会把一些组件看作是其他组件的特殊实例,比如 WelcomeDialog 可以说是 Dialog 的特殊实例。在 React 中,也可以通过组合来实现这一点。“特殊”组件可以通过 props 定制并渲染“一般”组件:
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!" /> ); }组合也同样适用于以 class 形式定义的组件。
function Dialog(props) { return ( <FancyBorder color="blue"> <h1 className="Dialog-title"> {props.title} </h1> <p className="Dialog-message"> {props.message} </p> {props.children} </FancyBorder> ); } class SignUpDialog extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.handleSignUp = this.handleSignUp.bind(this); this.state = {login: ''}; } render() { return ( <Dialog title="Mars Exploration Program" message="How should we refer to you?"> <input value={this.state.login} onChange={this.handleChange} /> <button onClick={this.handleSignUp}> Sign Me Up! </button> </Dialog> ); } handleChange(e) { this.setState({login: e.target.value}); } handleSignUp() { alert(`Welcome aboard, ${this.state.login}!`); } }关于继承 在 Facebook 成百上千个组件中使用 React 并没有发现需要使用继承来构建组件层次的情况。Props 和组合提供了清晰而安全地定制组件外观和行为的灵活方式。组件可以接受任意 props,包括基本数据类型,React 元素以及函数。如果要在组件间复用非 UI 的功能,可以将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。
第一步:将设计好的 UI 划分为组件层级 首先,需要在设计稿上用方框圈出每一个组件(包括它们的子组件),并且以合适的名称命名。设计师可能已经做过类似的工作,他们的 Photoshop 的图层名称可能最终就是 React 组件的名称!
可以将组件当作一种函数或者是对象来考虑,根据单一功能原则来判定组件的范围。也就是说,一个组件原则上只能负责一个功能。如果它需要负责更多的功能,这时候就应该考虑将它拆分成更小的组件。
在实践中,因为经常是在向用户展示 JSON 数据模型,所以如果模型设计得恰当,UI(或者说组件结构)便会与数据模型一一对应,这是因为 UI 和数据模型都会倾向于遵守相同的信息结构。只需使组件完全对应地展现数据模型的某部分即可。
第二步:用 React 创建一个静态版本 最容易的方式,是先用已有的数据模型渲染一个不包含交互功能的 UI。最好将渲染 UI 和添加交互这两个过程分开。这是因为,编写一个应用的静态版本时,往往要编写大量代码,而不需要考虑太多交互细节;添加交互功能时则要考虑大量细节,而不需要编写太多代码。所以,将这两个过程分开进行更为合适。
在构建应用的静态版本时,需要创建一些会重用其他组件的组件,然后通过 props 传入所需的数据。props 是父组件向子组件传递数据的方式。完全不应该使用 state 构建静态版本。state 代表了随时间会产生变化的数据,应当仅在实现交互时使用。
可以自上而下或者自下而上构建应用:自上而下意味着首先编写层级较高的组件(比如 FilterableProductTable),自下而上意味着从最基本的组件开始编写(比如 ProductRow)。应用比较简单时,使用自上而下的方式更方便;对于较为大型的项目来说,自下而上地构建,并同时为低层组件编写测试是更加简单的方式。
数据模型变化、调用 render() 方法、UI 相应变化,这个过程并不复杂,因此很容易看清楚 UI 是如何被更新的,以及是在哪里被更新的。React 单向数据流(也叫单向绑定)的思想使得组件模块化,易于快速开发。在 React 中,有两类“模型”数据:props 和 state。清楚地理解两者的区别是十分重要的。
第三步:确定 UI state 的最小(且完整)表示 首先需要找出应用所需的 state 的最小表示,并根据需要计算出其他所有数据。其中的关键正是 DRY: Don’t Repeat Yourself。只保留应用所需的可变 state 的最小集合,其他数据均由它们计算产生。
通过以下三个问题,逐个检查相应数据是否属于 state:
该数据是否是由父组件通过 props 传递而来的?如果是,那它应该不是 state。该数据是否随时间的推移而保持不变?如果是,那它应该也不是 state。你能否根据其他 state 或 props 计算出该数据的值?如果是,那它也不是 state。第四步:确定 state 放置的位置 React 中的数据流是单向的,并顺着组件层级从上往下传递。 可以尝试通过以下步骤来判断哪个组件应该拥有某个 state:
找到根据这个 state 进行渲染的所有组件。找到他们的共同所有者(common owner)组件(在组件层级上高于所有需要该 state 的组件)。该共同所有者组件或者比它层级更高的组件应该拥有该 state。如果找不到一个合适的位置来存放该 state,就可以直接创建一个新的组件来存放该 state,并将这一新组件置于高于共同所有者组件层级的位置。第五步:添加反向数据流 处于较低层级的表单组件更新较高层级的组件中的 state。React 通过一种比传统的双向绑定略微繁琐的方法来实现反向数据传递。尽管如此,但这种需要显式声明的方法更利于人们理解程序的运作方式。
The end 仅作为个人学习笔记之用,详情请参阅:https://zh-hans.reactjs.org/docs/