展现组件 | 容器组件 |
---|---|
关注事物的展现 | 关注事物如何工做 |
可能包含展现和容器组件,而且通常会有DOM标签和css样式 | 可能包含展现和容器组件,而且不会有DOM标签和css样式 |
经常容许经过this.props.children传递 | 提供数据和行为给容器组件或者展现组件 |
对第三方没有任何依赖,好比store 或者 flux action | 调用flux action 而且提供他们的回调给展现组件 |
不要指定数据如何加载和变化 | 做为数据源,一般采用较高阶的组件,而不是本身写,好比React Redux的connect(), Relay的createContainer(), Flux Utils的Container.create() |
仅经过属性获取数据和回调 | |
不多有本身的状态,即便有,也是本身的UI状态 | |
除非他们须要的本身的状态,生命周期,或性能优化才会被写为功能组件 |
下面是一个可能会常常写的组件,评论列表组件,数据交互和展现都放到了一个组件里面。css
// CommentList.js class CommentList extends React.Component { constructor() { super(); this.state = { comments: [] } } componentDidMount() { $.ajax({ url: "/my-comments.json", dataType: 'json', success: function(comments) { this.setState({comments: comments}); }.bind(this) }); } render() { return <ul> {this.state.comments.map(renderComment)} </ul>; } renderComment({body, author}) { return <li>{body}—{author}</li>; } }
咱们对上面的组件进行拆分,把他拆分红容器组件 CommentListContainer.js
和展现组件 CommentList
。html
// CommentListContainer.js class CommentListContainer extends React.Component { constructor() { super(); this.state = { comments: [] } } componentDidMount() { $.ajax({ url: "/my-comments.json", dataType: 'json', success: function(comments) { this.setState({comments: comments}); }.bind(this) }); } render() { return <CommentList comments={this.state.comments} />; } } // CommentList.js class CommentList extends React.Component { constructor(props) { super(props); } render() { return <ul> {this.props.comments.map(renderComment)} </ul>; } renderComment({body, author}) { return <li>{body}—{author}</li>; } }
优点:react
UI
state
数据源UI
下面是一个最简单的无状态组件的例子:ajax
function HelloComponent(props, /* context */) { return <div>Hello {props.name}</div> } ReactDOM.render(<HelloComponent name="Sebastian" />, mountNode)
能够看到,本来须要写“类”定义(React.createClass
或者 class YourComponent extends React.Component
)来建立本身组件的定义(有状态组件),如今被精简成了只写一个 render
函数。更值得一提的是,因为仅仅是一个无状态函数,React
在渲染的时候也省掉了将“组件类” 实例化的过程。算法
结合 ES6
的解构赋值,可让代码更精简。例以下面这个 Input
组件:json
function Input({ label, name, value, ...props }, { defaultTheme }) { const { theme, autoFocus, ...rootProps } = props return ( <label htmlFor={name} children={label || defaultLabel} {...rootProps} > <input name={name} type="text" value={value || ''} theme={theme || defaultTheme} {...props} /> )} Input.contextTypes = {defaultTheme: React.PropTypes.object};
无状态组件不像上述两种方法在调用时会建立新实例,它建立时始终保持了一个实例,避免了没必要要的检查和内存分配,作到了内部优化。数组
无状态组件不支持 "ref"浏览器
高阶组件经过函数和闭包,改变已有组件的行为,本质上就是 Decorator
模式在 React
的一种实现。性能优化
当写着写着无状态组件的时候,有一天突然发现须要状态处理了,那么无需完全返工:)
每每咱们须要状态的时候,这个需求是能够重用的。闭包
高阶组件加无状态组件,则大大加强了整个代码的可测试性和可维护性。同时不断“诱使”咱们写出组合性更好的代码。
function welcome() { let username = localStorage.getItem('username'); console.log('welcome ' + username); } function goodbey() { let username = localStorage.getItem('username'); console.log('goodbey ' + username); } welcome(); goodbey();
咱们发现两个函数有一句代码是同样的,这叫冗余唉。(平时可能会有一大段代码的冗余)。
下面咱们要写一个中间函数,读取username,他来负责把username传递给两个函数。
function welcome(username) { console.log('welcome ' + username); } function goodbey(username) { console.log('goodbey ' + username); } function wrapWithUsername(wrappedFunc) { let newFunc = () => { let username = localStorage.getItem('username'); wrappedFunc(username); }; return newFunc; } welcome = wrapWithUsername(welcome); goodbey = wrapWithUsername(goodbey); welcome(); goodbey();
好了,咱们里面的 wrapWithUsername
函数就是一个“高阶函数”。
他作了什么?他帮咱们处理了 username
,传递给目标函数。咱们调用最终的函数 welcome
的时候,根本不用关心 username
是怎么来的。
下面是两个冗余的组件。
import React, {Component} from 'react' class Welcome extends Component { constructor(props) { super(props); this.state = { username: '' } } componentWillMount() { let username = localStorage.getItem('username'); this.setState({ username: username }) } render() { return ( <div>welcome {this.state.username}</div> ) } } export default Welcome;
import React, {Component} from 'react' class Goodbye extends Component { constructor(props) { super(props); this.state = { username: '' } } componentWillMount() { let username = localStorage.getItem('username'); this.setState({ username: username }) } render() { return ( <div>goodbye {this.state.username}</div> ) } } export default Goodbye;
咱们能够经过刚刚高阶函数的思想来建立一个中间组件,也就是咱们说的高阶组件。
import React, {Component} from 'react' export default (WrappedComponent) => { class NewComponent extends Component { constructor() { super(); this.state = { username: '' } } componentWillMount() { let username = localStorage.getItem('username'); this.setState({ username: username }) } render() { return <WrappedComponent username={this.state.username}/> } } return NewComponent }
import React, {Component} from 'react'; import wrapWithUsername from 'wrapWithUsername'; class Welcome extends Component { render() { return ( <div>welcome {this.props.username}</div> ) } } Welcome = wrapWithUsername(Welcome); export default Welcome;
import React, {Component} from 'react'; import wrapWithUsername from 'wrapWithUsername'; class Goodbye extends Component { render() { return ( <div>goodbye {this.props.username}</div> ) } } Goodbye = wrapWithUsername(Goodbye); export default Goodbye;
看到没有,高阶组件就是把 username
经过 props
传递给目标组件了。目标组件只管从 props
里面拿来用就行了。
为了代码的复用性,咱们应该尽可能减小代码的冗余。
使用react时,组件或容器的代码在根本上必须只负责一块UI功能。
若是组件根本不须要状态,那么就使用函数定义的无状态组件。
从性能上来讲,函数定义的无状态组件 > ES6 class
定义的组件 > 经过 React.createClass()
定义的组件。
仅传递组件所须要的属性。只有当属性列表太长时,才使用{...this.props}
进行传递。
若是组件里面有太多的判断逻辑(if-else
语句)一般意味着这个组件须要被拆分红更细的组件或模块。
使用明确的命名可以让开发者明白它的功能,有助于组件复用。
在shouldComponentUpdate
中避免没必要要的检查.
尽可能使用不可变数据类型(Immutable
).
编写针对产品环境的打包配置(Production Build
).
经过Chrome Timeline
来记录组件所耗费的资源.
在componentWillMount
或者componentDidMount
里面经过setTimeOut
或者requestAnimationFram
来延迟执行那些须要大量计算的任务.
在大多数状况下,咱们推荐使用受控组件来实现表单。在受控组件中,表单数据由 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('A name was submitted: ' + 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="Submit" /> </form> ); } }
设置表单元素的value
属性以后,其显示值将由this.state.value
决定,以知足React
状态的同一数据理念。每次键盘敲击以后会执行handleChange
方法以更新React
状态,显示值也将随着用户的输入改变。
对于受控组件来讲,每一次 state
(状态)变化都会伴有相关联的处理函数。这使得能够直接修改或验证用户的输入和提交表单。
由于不受控组件的数据来源是 DOM 元素,当使用不受控组件时很容易实现 React 代码与非 React 代码的集成。若是你但愿的是快速开发、不要求代码质量,不受控组件能够必定程度上减小代码量。不然。你应该使用受控组件。
通常状况下不受控组件咱们使用ref
来获取DOM
元素进行操做。
class NameForm extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); } handleSubmit(event) { alert('A name was submitted: ' + this.input.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Name: <input type="text" ref={(input) => this.input = input} /> </label> <input type="submit" value="Submit" /> </form> ); } }
const sampleComponent = () => { return isTrue ? <p>True!</p> : <p>false!</p> };
const sampleComponent = () => { return isTrue ? <p>True!</p> : <none/> };
const sampleComponent = () => { return isTrue && <p>True!</p> };
须要注意的是若是isTrue
为 0 ,其实会转换成 false
,可是在页面中显示的时候,&&
仍是会返回0
显示到页面中。
// 问题代码 const sampleComponent = () => { return ( <div> {flag && flag2 && !flag3 ? flag4 ? <p>Blah</p> : flag5 ? <p>Meh</p> : <p>Herp</p> : <p>Derp</p> } </div> ) };
解决方案:
const sampleComponent = () => { const basicCondition = flag && flag2 && !flag3; if (!basicCondition) return <p>Derp</p>; if (flag4) return <p>Blah</p>; if (flag5) return <p>Meh</p>; return <p>Herp</p> }
在某些状况下,React
框架出于性能优化考虑,可能会将屡次state
更新合并成一次更新。正由于如此,setState
其实是一个异步的函数。 若是在调用setState()
函数以后尝试去访问this.state
,你获得的可能仍是setState()
函数执行以前的结果。
可是,有一些行为也会阻止React
框架自己对于屡次state
更新的合并,从而让state
的更新变得同步化。 好比: eventListeners
, Ajax
, setTimeout
等等。
React
框架之因此在选择在调用setState
函数以后当即更新state而不是采用框架默认的方式,即合并屡次state
更新为一次更新,是由于这些函数调用(fetch
,setTimeout
等浏览器层面的API
调用)并不处于React
框架的上下文中,React
没有办法对其进行控制。React
在此时采用的策略就是及时更新,确保在这些函数执行以后的其余代码能拿到正确的数据(即更新过的state
)。
根据React
官方文档,setState
函数实际上接收两个参数,其中第二个参数类型是一个函数,做为setState
函数执行后的回调。经过传入回调函数的方式,React
能够保证传入的回调函数必定是在setState
成功更新this.state
以后再执行。
this.setState({count: 1}, () => { console.log(this.state.count); // 1 })
ReactComponent.prototype.setState = function(partialState, callback) { invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.' ); this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); } };
updater
的这两个方法,和React
底层的Virtual Dom
(虚拟DOM树)的diff
算法有紧密的关系,因此真正决定同步仍是异步的实际上是Virtual DOM
的diff
算法。
在React
中,想作依赖注入(Dependency Injection
)其实至关简单。能够经过props
来进行传递。可是,若是组件数量不少,而且组件嵌套层次很深的话,这种方式就不太合适。
// inject.jsx var title = 'React Dependency Injection'; export default function inject(Component) { return class Injector extends React.Component { render() { return ( <Component {...this.state} {...this.props} title={ title } /> ) } }; }
// Title.jsx export default function Title(props) { return <h1>{ props.title }</h1>; }
// Header.jsx import inject from './inject.jsx'; import Title from './Title.jsx'; var EnhancedTitle = inject(Title); export default function Header() { return ( <header> <EnhancedTitle /> </header> ); }
React v16.3.0
以前的 Context
:
var context = { title: 'React in patterns' }; class App extends React.Component { getChildContext() { return context; } // ... } App.childContextTypes = { title: PropTypes.string };
class Inject extends React.Component { render() { var title = this.context.title; // ... } } Inject.contextTypes = { title: PropTypes.string };
以前的 Context
做为一个实验性质的 API
,直到 React v16.3.0
版本前都一直不被官方所提倡去使用,其主要缘由就是由于在子组件中使用 Context
会破坏 React
应用的分型架构。
这里的分形架构指的是从理想的 React
应用的根组件树中抽取的任意一部分都还是一个能够直接运行的子组件树。在这个子组件树之上再包一层,就能够将它无缝地移植到任意一个其余的根组件树中。
但若是根组件树中有任意一个组件使用了支持透传的 Context
API
,那么若是把包含了这个组件的子组件树单独拿出来,由于缺乏了提供 Context
值的根组件树,这时的这个子组件树是没法直接运行的。
而且他有一个致命缺陷:任何一个中间传递的组件shouldComponentUpdate
函数返回false
,组件都不会获得更新。
新的Context Api
新的Context Api
采用声明式的写法,而且能够透过shouldComponentUpdate
函数返回false
的组件继续向下传播,以保证目标组件必定能够接收到顶层组件 Context
值的更新,一举解决了现有 Context API
的两大弊端,也终于成为了 React
中的第一级(first-class) API
。
新的 Context API 分为三个组成部分:
React.createContext
用于初始化一个 Context
。
XXXContext.Provider
做为顶层组件接收一个名为 value
的 prop
,能够接收任意须要被放入 Context
中的字符串,数字,甚至是函数。
XXXContext.Consumer
做为目标组件能够出如今组件树的任意位置(在 Provider
以后),接收 children prop
,这里的 children
必须是一个函数(context => ()
)用来接收从顶层传来的 Context
。
const ThemeContext = React.createContext('light'); class App extends React.Component { render() { return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } function ThemedButton(props) { return ( <ThemeContext.Consumer> {theme => <Button {...props} theme={theme} />} </ThemeContext.Consumer> ); }
class Switcher extends React.Component { constructor(props) { super(props); this.state = { name: 'React in patterns' }; } render() { return ( <button onClick={ this._handleButtonClick }> click me </button> ); } _handleButtonClick() { console.log(`Button is clicked inside ${ this.state.name }`); // 将致使 // Uncaught TypeError: Cannot read property 'state' of null } }
咱们能够经过下面三种方式简单实现this指向的绑定:
constructor
中事先绑定 this._buttonClick = this._handleButtonClick.bind(this);
<button onClick={ () => this._buttonClick() }>
<button onClick={ ::this._buttonClick() }>
setState() 不只能接受一个对象,还能接受一个函数做为参数呢,该函数接受该组件前一刻的 state 以及当前的 props 做为参数,计算和返回下一刻的 state。
// assuming this.state.count === 0 this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); // this.state.count === 1, not 3 this.setState((prevState, props) => ({ count: prevState.count + props.increment }));
// Passing object this.setState({ expanded: !this.state.expanded }); // Passing function this.setState(prevState => ({ expanded: !prevState.expanded }));
import HomePage from './HomePage.jsx'; import AboutPage from './AboutPage.jsx'; import UserPage from './UserPage.jsx'; import FourOhFourPage from './FourOhFourPage.jsx'; const PAGES = { home: HomePage, about: AboutPage, user: UserPage }; const Page = (props) => { const Handler = PAGES[props.page] || FourOhFourPage; return <Handler {...props} /> };
基础组件, 布局组件, 排版组件
请保持样式远离那些离不开state的组件. 好比路由, 视图, 容器, 表单, 布局等等不该该有任何的样式或者css class出如今组件上. 相反, 这些复杂的业务组件应该有一些带有基本功能的无状态UI组件组成.
class SampleComponent extends Component { render() { return ( <form onSubmit={this.handleSubmit}> <Heading children='Sign In'/> <Input name='username' value={username} onChange={this.handleChange}/> <Input type='password' name='password' value={password} onChange={this.handleChange}/> <Button type='submit' children='Sign In'/> </form> ) } } // 表达组件(带样式) const Button = ({ ...props }) => { const sx = { fontFamily: 'inherit', fontSize: 'inherit', fontWeight: 'bold', textDecoration: 'none', display: 'inline-block', margin: 0, paddingTop: 8, paddingBottom: 8, paddingLeft: 16, paddingRight: 16, border: 0, color: 'white', backgroundColor: 'blue', WebkitAppearance: 'none', MozAppearance: 'none' } return ( <button {...props} style={sx}/> ) }
通常来讲, 在组件内写死(hard code)样式应该是要被避免的. 这些有可能被不一样的UI组件分享的样式应该被分开放入对应的模块中.
// 样式模块 export const white = '#fff'; export const black = '#111'; export const blue = '#07c'; export const colors = { white, black, blue }; export const space = [ 0, 8, 16, 32, 64 ]; const styles = { bold: 600, space, colors }; export default styles
// button.jsx import React from 'react' import { bold, space, colors } from './styles' const Button = ({ ...props }) => { const sx = { fontFamily: 'inherit', fontSize: 'inherit', fontWeight: bold, textDecoration: 'none', display: 'inline-block', margin: 0, paddingTop: space[1], paddingBottom: space[1], paddingLeft: space[2], paddingRight: space[2], border: 0, color: colors.white, backgroundColor: colors.blue, WebkitAppearance: 'none', MozAppearance: 'none' }; return ( <button {...props} style={sx}/> ) };
// Modular powers of two scale const scale = [ 0, 8, 16, 32, 64 ]; // 经过这个函数去取得一部分的样式 const createScaledPropertyGetter = (scale) => (prop) => (x) => { return (typeof x === 'number' && typeof scale[x] === 'number') ? {[prop]: scale[x]} : null }; const getScaledProperty = createScaledPropertyGetter(scale); export const getMargin = getScaledProperty('margin'); export const getPadding = getScaledProperty('padding'); // 样式函数的用法 const Box = ({ m, p, ...props }) => { const sx = { ...getMargin(m), ...getPadding(p) }; return <div {...props} style={sx}/> }; // 组件用法. const Box = () => ( <div> <Box m={2} p={3}> A box with 16px margin and 32px padding </Box> </div> );
class SampleComponent extends Component { // constructor function (or getInitialState) constructor(props) { super(props); this.state = { flag: false, inputVal: props.inputValue }; } render() { return <div>{this.state.inputVal && <AnotherComponent/>}</div> } }
这样作的危险在于, 有可能组件的props
发生了改变可是组件却没有被更新. 新的props
的值不会被React
认为是更新的数据由于构造器constructor
或者getInitialState
方法在组件建立以后不会再次被调用了,所以组件的state
再也不会被更新。 要记住, State
的初始化只会在组件第一次初始化的时候发生。
class SampleComponent extends Component { // constructor function (or getInitialState) constructor(props) { super(props); this.state = { flag: false }; } render() { return <div>{this.props.inputValue && <AnotherComponent/>}</div> } }
更干净的render
函数? 这个概念可能会有点让人疑惑.
其实在这里干净是指咱们在shouldComponentUpdate
这个生命周期函数里面去作浅比较, 从而避免没必要要的渲染.
class Table extends PureComponent { render() { return ( <div> {this.props.items.map(i => <Cell data={i} options={this.props.options || []}/> )} </div> ); } }
这种写法的问题在于{this.props.options || []}
这种写法会致使全部的Cell
都被从新渲染即便只有一个cell
发生了改变. 为何会发生这种事呢?
仔细观察你会发现, options
这个数组被传到了Cell
这个组件上, 通常状况下, 这不会致使什么问题. 由于若是有其余的Cell
组件, 组件会在有props
发生改变的时候浅对比props
而且跳过渲染(由于对于其余Cell
组件, props
并无发生改变). 可是在这个例子里面, 当options
为null
时, 一个默认的空数组就会被当成Props
传到组件里面去. 事实上每次传入的[]
都至关于建立了新的Array
实例. 在JavaScript
里面, 不一样的实例是有不一样的实体的, 因此浅比较在这种状况下老是会返回false
, 而后组件就会被从新渲染. 由于两个实体不是同一个实体. 这就彻底破坏了React
对于咱们组件渲染的优化.
const defaultval = []; // <--- 也可使用defaultProps class Table extends PureComponent { render() { return ( <div> {this.props.items.map(i => <Cell data={i} options={this.props.options || defaultval}/> )} </div> ); } }
class App extends PureComponent { render() { return <MyInput onChange={e => this.props.update(e.target.value)}/>; } }
class App extends PureComponent { update(e) { this.props.update(e.target.value); } render() { return <MyInput onChange={this.update.bind(this)}/>; } }
在上面的两个坏实践中, 每次咱们都会去建立一个新的函数实体. 和第一个例子相似, 新的函数实体会让咱们的浅比较返回false
, 致使组件被从新渲染. 因此咱们须要在更早的时候去bind
咱们的函数.
class App extends PureComponent { constructor(props) { super(props); this.update = this.update.bind(this); } update(e) { this.props.update(e.target.value); } render() { return <MyInput onChange={this.update}/>; } }
React模块名使用帕斯卡命名,实例使用骆驼式命名
// bad import reservationCard from './ReservationCard'; // good import ReservationCard from './ReservationCard'; // bad const ReservationItem = <ReservationCard />; // good const reservationItem = <ReservationCard />;
// bad export default function withFoo(WrappedComponent) { return function WithFoo(props) { return <WrappedComponent {...props} foo />; } } // good export default function withFoo(WrappedComponent) { function WithFoo(props) { return <WrappedComponent {...props} foo />; } const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; WithFoo.displayName = `withFoo(${wrappedComponentName})`; return WithFoo; }
避免使用DOM相关的属性来用做其余的用途。
// bad <MyComponent style="fancy" /> // good <MyComponent variant="fancy" />
在React模块中,不要给所谓的私有函数添加 _ 前缀,本质上它并非私有的。
为何?_
下划线前缀在某些语言中一般被用来表示私有变量或者函数。可是不像其余的一些语言,在JS中没有原生支持所谓的私有变量,全部的变量函数都是共有的。尽管你的意图是使它私有化,在以前加上下划线并不会使这些变量私有化,而且全部的属性(包括有下划线前缀及没有前缀的)都应该被视为是共有的。
class extends React.Component 的生命周期函数:
可选的 static 方法
点击回调或者事件处理器 如 onClickSubmit()
或 onChangeDescription()
render
里的 getter
方法 如 getSelectReason()
或 getFooterContent()
可选的 render
方法 如 renderNavigation()
或 renderProfilePicture()
render
render()
方法
如何定义 propTypes
, defaultProps
, contextTypes
, 等等其余属性...
import React from 'react'; import PropTypes from 'prop-types'; const propTypes = { id: PropTypes.number.isRequired, url: PropTypes.string.isRequired, text: PropTypes.string, }; const defaultProps = { text: 'Hello World', }; class Link extends React.Component { static methodsAreOk() { return true; } render() { return <a href={this.props.url} data-id={this.props.id}>{this.props.text}</a>; } } Link.propTypes = propTypes; Link.defaultProps = defaultProps; export default Link;