React查缺补漏之二——《React模式》
/ / 点击 / 阅读耗时 18 分钟通过给一个通用函数传入参数定制特定函数的用法
_onFieldChange
函数是一个通用实例方法,通过给这个函数传入不同的参数来实现返回结果的不同。在构造函数中,进行绑定(没有想过这种用法)。
1. `this._onNameChanged = this._onFieldChange.bind(this, 'name');`
2. `this._onPasswordChanged =this._onFieldChange.bind(this, 'password');`
**注意点击的回调事件要写在事件处理函数参数的最后**,上述在绑定的过程中传入的参数相当于第一个参数。
这是因为:
1. 使用箭头函数,事件对象e要显式传递;
2. 使用bind方法,事件对象e会隐式传递,e要放在参数的最后。
这里使用的是bind方法,事件对象会隐士传递,所以可以直接在`input`的`onChange`属性中单纯传入回调函数并且回调事件要写在回调函数参数的最后。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Form extends React.Component {
constructor(props) {
super(props);
this._onNameChanged = this._onFieldChange.bind(this, 'name');
this._onPasswordChanged =
this._onFieldChange.bind(this, 'password');
}
render() {
return (
<form>
<input onChange={ this._onNameChanged } />
<input onChange={ this._onPasswordChanged } />
</form>
);
}
_onFieldChange(field, event) {
console.log(`${ field } changed to ${ event.target.value }`);
}
};
解绑强耦合组件的讨论——组合
作者认为组合是React的最强大的优势之一。
书中的组合部分涉及
组件作为属性传递
,高阶组件
,自己现在在写代码的时候会有意的将每个组件都写成单一功能的组件,自己感觉主要的好处还是视觉上的,不会出现看起来庞大数量巨大到恐怖的组件,改bug比较好定位。现在写的多数都是一次性组件,也没有写过组件测试,所以代码重用的优点感受的不明显,降低测试难度完全没有体会。children
属性这个属性之前没有好好关注过,如今看来是个相当好用的属性。书里说这个属性一大用途是用来解绑父组件和子组件的强耦合。
将组件作为参数传入另一个组件
其实上面的
children
属性也相当与将组件作为参数传入另一个组件。之所以现在不太用这种写法是因为自己还没有写过通用组件,比如项目里的各种modal。写的全都是一次性的组件,写成强耦合的组件或许更直观,代码量也能减少,但如果涉及到组件测试就完逼了,最近写的组件业务逻辑都比较复杂了,组件的测试也肯定会相当复杂。(之前自己从没写过测试组件,现在要练习下。)高阶组件
高阶组件的调用要在render()之前,生成一个唯一的组件,如果在render()中调用,每次生成的都不一样,会产生性能问题,见作者观点。
下面是一个典型的高阶组件,在组件强化函数中,
Component
组件的参数title和remoteTitle的数据来源一个是配置文件,一个是ajax请求,即能控制原始组建的输入,同时还能传入高阶组件的props,书里说这是高阶组件的最大优点(控制数据来源)。感觉甚至可以把ajax请求作为高阶组件的参数传入,这样即保证组件的功能单一可复用,又能让数据和对应的组件组合在一起,还能避免原先的在上层的父组件中写入和父组件完全不相关的数据请求函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28var config = require('path/to/configuration');
var enhanceComponent = (Component) =>
class Enhance extends React.Component {
constructor(props) {
super(props);
this.state = { remoteTitle: null };
}
componentDidMount() {
fetchRemoteData('path/to/endpoint').then(data => {
this.setState({ remoteTitle: data.title });
});
}
render() {
return (
<Component
{...this.props}
title={ config.appTitle }
remoteTitle={ this.state.remoteTitle }
/>
)
}
};
var OriginalTitle = ({ title, remoteTitle }) =>
<h1>{ title }{ remoteTitle }</h1>;
var EnhancedTitle = enhanceComponent(OriginalTitle);
4. 将函数作为children和render props
下面这个例子里,App组件中不仅有全部数据,而且还有如何显示数据的方法。TodoList组件单纯只是一个显示数据的容器。使用时,将数据和操作显示数据的方法传入TodoList组件。感觉这个模式和自己之前的想法不太一样。之前自己写代码只是考虑组件功能的单一性,单从这个角度我肯定会把显示数据的方法写在TodoList组件里面,这样做完全没有考虑TodoList组件的复用,显然也没有让TodoList组件成为纯粹的样式渲染组件,明显`将函数作为children`模式以及`render props`模式更胜一筹。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function TodoList({ todos, children }) {
return (
<section className='main-section'>
<ul className='todo-list'>{
todos.map((todo, i) => (
<li key={ i }>{ children(todo) }</li>
))
}</ul>
</section>
);
}
function App() {
const todos = [
{ label: 'Write tests', status: 'done' },
{ label: 'Sent report', status: 'progress' },
{ label: 'Answer emails', status: 'done' }
];
const isCompleted = todo => todo.status === 'done';
return (
<TodoList todos={ todos }>
{
todo => isCompleted(todo) ?
<b>{ todo.label }</b> :
todo.label
}
</TodoList>
);
}
这里作者举了他们项目的一个例子:
>最近,我们在工作中使用这种模式,我们必须将某些界面限制只对具有 read:products 权限的用户开放。我们使用的是 render prop 模式。
1
2
3
<Authorize
permissionsInclude={[ 'read:products' ]}
render={ () => <ProductsList /> } />
这样写组件语义真的好清晰啊,上面的这三行代码不言自明:`Authorize`组件**验证**是否是有(`permissionsInclude`)权限(`[ 'read:products' ]`),如果有,则渲染(`render`)产品列表(`<ProductsList />`)。**这个组件仅靠组件名称、属性名称就完整描述了这个组件的功能,数据和渲染规则来自父组件,具体渲染细节则被封装进单纯的显示组件`Authorize`中。**
哇擦!有些激动!这真的变成代码写的文章了,而不是冗繁的字码,tsubarashi,真的是代码的艺术啊!
**所以今后写组件,不要盲目的用单一性拆分代码,从语义的角度考虑明显更优雅。**
展示型组件vs容器型组件
以现在写的项目为例,配置选型的创建功能使用了三个组件:
base
,first_step
,second_step
。其中后两个组件为函数组件,遵循了展示型组件的原则,最大限度的不去管理数据。base
组件则为一个混合组件,有几乎全部的业务逻辑,还有一部分组合first_step
和second_step
两个组件的显示代码,这一部分可以拆出来作为一个独立的组件,这样就会变成一个纯粹的容器型组件。在组件中更新组件外部数据
下面的代码中,
Store.set.bind(Store)
中的bind(Store)
是关键。将函数的执行环境和Store对象绑定。1
2
3
4
5
6
7
8
9
10
11
12
13var Store = {
_flag: false,
set: function(value) {
this._flag = value;
},
get: function() {
return this._flag;
}
};
function App() {
return <Switcher onChange={ Store.set.bind(Store) } />;
};双向绑定
之前写的代码里,有几处用到了下面这种写法:
父组件向子组件传递了一个能setState的函数,当子组件满足某个要求时执行这个函数,修改了父组件中的数据,同时子组件的数据完全来源于父组件,当数据有变化时,子组件再次渲染。
但上述写法感觉不是双向绑定,双向绑定是相同数据状态存在多处,此刻再进行类似上述操作,数据状态彼此影响,感觉这个是双向绑定。
单项数据流
将数据状态存储在全局的一个
store
对象中,这里作为整个应用数据属性更新的唯一地方。感觉这个是理解Redux的关键啊。下面是书中的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46//数据状态存储和更新的唯一地点
var Store = {
_handlers: [],
_flag: '',
subscribe: function(handler) {
this._handlers.push(handler);
},
set: function(value) {
this._flag = value;
this._handlers.forEach(handler => handler(value))
},
get: function() {
return this._flag;
}
};
//状态组件的数据来源依然是store
//这个组件初始化的时候注册了一个本组件修改状态的函数
//然后将最终修改函数Store.set传给子组件,当子组件满足条件时发动
class App extends React.Component {
constructor(props) {
super(props);
this.state = { value: Store.get() };
Store.subscribe(value => this.setState({ value }));
}
render() {
return (
<div>
<Switcher
value={ this.state.value }
onChange={ Store.set.bind(Store) } />
</div>
);
}
};
function Switcher({ value, onChange }) {
return (
<button onClick={ e => onChange(!value) }>
{ value ? 'lights on' : 'lights off' }
</button>
);
};感觉这本书里讲redux虽然没有很深入但是很好理解,译文点击此处。
React context
之前没有用过这个功能,只在redux中有见过
Provider
,但这个组件来自react-redux
。下面这段是文章中对context功能的使用说明:
createContext 返回的对象具有 Provider 和 Consumer 属性。它们实际上是有效的 React 类。Provider 以 value 属性的形式接收 context 。Consumer 用来访问 context 并从中读取数据。
1
2
3
4
5
6import { createContext } from 'react';
const Context = createContext({});
export const Provider = Context.Provider;
export const Consumer = Context.Consumer;使用:
传入context
1
2
3
4
5
6
7
8
9
10
11
12
13import { Provider } from './context';
const context = { title: 'React In Patterns' };
class App extends React.Component {
render() {
return (
<Provider value={ context }>
<Header />
</Provider>
);
}
};在子组件中使用
Comsumer
Consumer 类使用函数作为嵌套子元素 ( render prop 模式) 来传递 context 。
1
2
3
4
5
6
7
8
9import { Consumer } from './context';
function Title() {
return (
<Consumer>{
({ title }) => <h1>Title: { title }</h1>
}</Consumer>
);
}
使用模块系统
技术:模块创建全局对象+高阶组件
描述:之前的项目中,创建数据缓存和router都使用了类似的技术。
方法:在创建三个函数和一个存储对象
存储对象
1
var dependencies = {};
注册函数用来获取属性
1
2export function register(key, dependency) {
dependencies[key] = dependency;获取函数用来拿到需要的值
1
2
3export function fetch(key) {
if (dependencies[key]) return dependencies[key];
throw new Error(`${ key } is not registered as dependency.`);高阶函数用来包装原始组件,获取属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17export function wire(Component, deps, mapper) {
return class Injector extends React.Component {
constructor(props) {
super(props);
this._resolvedDependencies = mapper(...deps.map(fetch));
}
render() {
return (
<Component
{...this.state}//这里表示的是Injector这个组件维护的状态,但感觉不太有用的到的机会,这种通用函数因该很少有这样的状态吧
{...this.props}//父组件传入的属性
{...this._resolvedDependencies}
/>
);
}
};
}