JSX 有许多不同方式来表达条 件逻辑,理解每种方式的益处及其存在的问题对于编写可读且可维护的代码非常重要。
// 下述做法可行,但可读性不好,组件合条件很多时会更差
let button;
if (isLoggedIn) {
button = <LogoutButton />;
}
return <div>{button}</div>;
优化
// JSX 可以利用行内条件来判断:
<div>{isLoggedIn && <LoginButton />}</div>
let button;
if (isLoggedIn) {
button = <LogoutButton />;
} else {
button = <LoginButton />;
}
return <div>{button}</div>;
优化
<div>{isLoggedIn ? <LogoutButton /> : <LoginButton />}</div>
// 需要多个变量才能判断是否要渲染组件:
<div>{dataIsReady && (isAdmin || userHasPermissions) && <SecretData />}</div>
优化
// 上述示例中的行内条件语句的写法很好,
// 但可读性受到了很大影响。此时可以在组件内编写 一个辅助函数来检验 JSX 的条件语句:
canShowSecretData() {
const { dataIsReady, isAdmin, userHasPermissions } = this.props
return dataIsReady && (isAdmin || userHasPermissions)
}
<div>
{this.canShowSecretData() && <SecretData />}
</div>
在优化(使用 getter 方法来取代函数)
get canShowSecretData() {
const { dataIsReady, isAdmin, userHasPermissions } = this.props
return dataIsReady && (isAdmin || userHasPermissions)
}
<div>
{this.canShowSecretData && <SecretData />}
</div>
// 纯粹的函数
const add = (x, y) => x + y;
// 非纯粹的函数
let x = 0;
const add = (y) => (x = x + y);
// 以下代码不遵循不可变性
const add3 = (arr) => arr.push(3);
const myArr = [1, 2];
add3(myArr); // [1, 2, 3]
add3(myArr); // [1, 2, 3, 3]
// 可以改用concat满足不可变性
const add3 = (arr) => arr.concat(3);
const myArr = [1, 2];
const result1 = add3(myArr); // [1, 2, 3]
const result2 = add3(myArr); // [1, 2, 3]
// 原先的写法
const add = (x, y) => x + y;
// 改成柯里化:
// 这种函数写法相当方便,因为传入第一个参数后,
// 第一个值被保留起来,返回的第二个函数 可以多次复用。
const add = (x) => (y) => x + y;
const add = (x, y) => x + y;
const square = (x) => x * x;
const addAndSquare = (x, y) => square(add(x, y));
// 不推荐
const Button = React.createClass({ render() {
return <button />
})
// 推荐使用继承React.Component
class Button extends React.Component {
render() {
return <button />
}
}
// 以下的简单示例
const Button = React.createClass({
handleClick() {
console.log(this);
},
render() {
return <button onClick={this.handleClick} />;
},
});
// 如果使用继承的方法(this结果会是null)
class Button extends React.Component {
handleClick() {
console.log(this);
}
render() {
return <button onClick={this.handleClick} />;
}
}
// 解决:
// 1)箭头函数:自动将当前的 this 绑定到函数体
class Button extends React.Component {
handleClick() {
console.log(this);
}
render() {
return <button onClick={() => this.handleClick()} />;
}
}
注意: 这样做符合预期,也不会带来什么特殊问题。唯一的缺点在于,如果在意性能,那么就需要理解代码的本质。
实际上,在渲染方法中绑定函数会带来无法预料的副作用,因为每次渲染组件(应用在生命 周期内会多次渲染组件)时都会触发箭头函数。虽然在渲染方法内多次触发某个函数不太理想,但本身并没有什么问题。 问题在于,如果这个函数传递给子组件,那么子组件在每次更新过程中都会接收新的 prop。这可能会导致低效的渲染,进而引发问题,对于纯粹组件而言尤其如此。
解决函数绑定问题最佳方案是在构造器内进行绑定操作,这样即使多次渲染组件,它也不会发任何改变。
class Button extends React.Component {
constructor(props) {
super(props);
// 就是这样,问题解决了!
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this);
}
render() {
return <button onClick={this.handleClick} />;
}
}
() => <button />;
// 无状态函数式组件可以接收 props 对 象作为参数:
props => <button>{props.text}</button>
// 更简洁的 ES2015 解构语法:
({ text }) => <button>{text}</button>
// 无状态函数就可以通过 propTypes 属性来接收 props
const Button = ({ text }) => <button>{text}</button>
Button.propTypes = {
text: React.PropTypes.string,
}
// 无状态函数式组件也接收表示上下文的第二个参数。
(props, context) => (
<button>
{context.currency}
{props.value}
</button>
)
关键词 this && 状态 && 生命周期
无状态函数式组件与状态组件的一项区别在于,this 在无状态函数式组件的执行过程中不指向组件本身。
由于这个原因,与组件实例相关的 setState 等方法以及生命周期方法都无法使用。
新版的 React 出了 hooks 之后这个问题将不存在
ref 与事件处理器
// 因为无状态函数式组件不能访问组件实例,
// 所以如果要使用 ref 或者事件处理器,需要按以下方式来定义。
() => {
let input;
const onClick = () => input.focus();
return (
<div>
<input ref={(el) => (input = el)} />
<button onClick={onClick}>Focus</button>
</div>
);
};
this.setState({ clicked: true }, () => {
console.log("the state is now", this.state);
});
// 在事件处理器中触发了 setState 后,尝试将当前状态值打印到控制台中,那 么获得的是旧状态值:
handleClick() {
this.setState({
clicked: true,
})
console.log('the state is now', this.state) // the state is now null
}
render() {
return <button onClick={this.handleClick}>Click me!</button>
}
// 稍微修改一下代码:
handleClick() {
setTimeout(() => {
this.setState({
clicked: true,
})
console.log('the state is now', this.state)
// 结果: the state is now Object {clicked: true}
})
}
示例使用 setTimeout 只是为了展示 React 的行为,你永远不要这样编写事件监听器。
class Price extends React.Component {
constructor(props) {
super(props)
this.state = {
price: `${props.currency}${props.value}`, // 不推荐
}
}
render() {
return <div>{this.state.price}</div>
}
}
// 如果在父组件中按照以下方式创建,那么这种做法是可行的:
<Price currency="£" value="100" />
// 问题在于,如果货币单位或价格在 Price 组件的生命周期内发生改变,
// 则永远不会重新计算状态(因为只会调用构造器一次),应用就会显示错误的价格。
// 因此,只要可以,就应该用 props 来计算值:
getPrice() {
return `${this.props.currency}${this.props.value}`
}
// 基于高阶函数封装请求
const withData = (url) => (Component) =>
class extends React.Component {
constructor(props) {
super(props);
this.state = { data: [] };
}
componentDidMount() {
// 模拟虚拟请求
setTimeout(() => {
this.setState({
data: [
{ id: "001", title: "小明", excerpt: "开发工程师" },
{ id: "002", title: "老王", excerpt: "设计师" },
],
});
}, 1000);
}
render() {
return <Component {...this.props} {...this.state} />;
}
};
const withGists = withData("https://api.github.com/users/gaearon/gists");
const List = (props) => {
console.log("List -> gists", props);
return (
<ul>
{props.data.map((gist) => (
<li key={gist.id}>{gist.title}</li>
))}
</ul>
);
};
List.propTypes = {
data: PropTypes.array,
};
const ListWithGists = withGists(List);
import { connect as refetchConnect } from "react-refetch";
const List = ({ gists }) => {
if (gists.pending) {
return <div>loading...</div>;
} else if (gists.rejected) {
return <div>{gists.reason}</div>;
} else if (gists.fulfilled) {
return (
gists.fulfilled && (
<ul>
{gists.value.map((gist) => (
<li key={gist.id}>{gist.description}</li>
))}
</ul>
)
);
}
};
const ListWithGists = refetchConnect(() => ({
gists: `https://api.github.com/users/gaearon/gists`,
}))(List);
const style = {
color: "palevioletred",
backgroundColor: "papayawhip",
};
const Button = () => <button style={style}>Click me!</button>;
// 这个方法就是 shouldComponentUpdate,如果它返回 false,那么在父组件的更新过程中,组件及其全部子元素的渲染方法不会被调用。
shouldComponentUpdate() {
return false
}