首页 > 网站开发 > JavaScript >
-
JavaScript教程之React组件设计(2)
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>
);
}
context
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>
);
}
事件处理中的this指向问题
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() }>
-
ES7中的绑定操作符
<button onClick={ ::this._buttonClick() }>
给setState传入回调函数
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} />
};
React style
组件分类
基础组件, 布局组件, 排版组件
给无状态的纯UI组件应用样式
请保持样式远离那些离不开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}/>
)
}
样式模块(style module)
一般来说, 在组件内写死(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}/>
)
};
样式函数(Style Functions)
// 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>
);
常见小坑
state不更新?
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函数?
更干净的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