首页 > 网站开发 > JavaScript >
-
JavaScript教程之React组件设计
组件分类
展示组件和容器组件
展示组件 | 容器组件 |
---|---|
关注事物的展示 | 关注事物如何工作 |
可能包含展示和容器组件,并且一般会有DOM标签和css样式 | 可能包含展示和容器组件,并且不会有DOM标签和css样式 |
常常允许通过this.props.children传递 | 提供数据和行为给容器组件或者展示组件 |
对第三方没有任何依赖,比如store 或者 flux action | 调用flux action 并且提供他们的回调给展示组件 |
不要指定数据如何加载和变化 | 作为数据源,通常采用较高阶的组件,而不是自己写,比如React Redux的connect(), Relay的createContainer(), Flux Utils的Container.create() |
仅通过属性获取数据和回调 | |
很少有自己的状态,即使有,也是自己的UI状态 | |
除非他们需要的自己的状态,生命周期,或性能优化才会被写为功能组件 |
下面是一个可能会经常写的组件,评论列表组件,数据交互和展示都放到了一个组件里面。
// 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
。
// 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>;
}
}
优势:
-
展示和容器更好的分离,更好的理解应用程序和
UI
-
重用性高,展示组件可以用于多个不同的
state
数据源 -
展示组件就是你的调色板,可以把他们放到单独的页面,在不影响应用程序的情况下,让设计师调整
UI
- 迫使你分离标签,达到更高的可用性
有状态组件和无状态组件
下面是一个最简单的无状态组件的例子:
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
组件:
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
里面拿来用就好了。
为了代码的复用性,我们应该尽量减少代码的冗余。
- 提取共享的state,如果有两个组件都需要加载同样的数据,那么他们会有相同的 componentDidMount 函数。
- 找出重复的代码,每个组件中constructor 和 componentDidMount都干着同样的事情,另外,在数据拉取时都会显示Loading... 文案,那么我们应该思考如何使用高阶组件来提取这些方法。
- 迁移重复的代码到高阶组件
- 包裹组件,并且使用props替换state
- 尽可能地简化
组件开发基本思想
单功能原则
使用react时,组件或容器的代码在根本上必须只负责一块UI功能。
让组件保持简单
-
如果组件根本不需要状态,那么就使用函数定义的无状态组件。
-
从性能上来说,函数定义的无状态组件 >
ES6 class
定义的组件 > 通过React.createClass()
定义的组件。 -
仅传递组件所需要的属性。只有当属性列表太长时,才使用
{...this.props}
进行传递。 -
如果组件里面有太多的判断逻辑(
if-else
语句)通常意味着这个组件需要被拆分成更细的组件或模块。 -
使用明确的命名能够让开发者明白它的功能,有助于组件复用。
基本准则
-
在
shouldComponentUpdate
中避免不必要的检查. -
尽量使用不可变数据类型(
Immutable
). -
编写针对产品环境的打包配置(
Production Build
). -
通过
Chrome Timeline
来记录组件所耗费的资源. -
在
componentWillMount
或者componentDidMount
里面通过setTimeOut
或者requestAnimationFram
来延迟执行那些需要大量计算的任务.
组件开发技巧
form表单里的受控组件和不受控组件
受控组件
在大多数情况下,我们推荐使用受控组件来实现表单。在受控组件中,表单数据由 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>
)
};
解决方案:
- 最佳方案: 将逻辑移到子组件内部
- 使用IIFE(Immediately-Invoked Function Expression 立即执行函数)
- 满足条件的时候使用return强制跳出函数
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>
}
setState异步性
在某些情况下,React
框架出于性能优化考虑,可能会将多次state
更新合并成一次更新。正因为如此,setState
实际上是一个异步的函数。 如果在调用setState()
函数之后尝试去访问this.state
,你得到的可能还是setState()
函数执行之前的结果。
但是,有一些行为也会阻止React
框架本身对于多次state
更新的合并,从而让state
的更新变得同步化。 比如: eventListeners
, Ajax
, setTimeout
等等。
React
框架之所以在选择在调用setState
函数之后立即更新state而不是采用框架默认的方式,即合并多次state
更新为一次更新,是因为这些函数调用(fetch
,setTimeout
等浏览器层面的API
调用)并不处于React
框架的上下文中,React
没有办法对其进行控制。React
在此时采用的策略就是及时更新,确保在这些函数执行之后的其他代码能拿到正确的数据(即更新过的state
)。
解决setState函数异步的办法?
根据React
官方文档,setState
函数实际上接收两个参数,其中第二个参数类型是一个函数,作为setState
函数执行后的回调。通过传入回调函数的方式,React
可以保证传入的回调函数一定是在setState
成功更新this.state
之后再执行。
this.setState({count: 1}, () => {
console.log(this.state.count); // 1
})
React源码中setState的实现
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