我们知道组件和自顶向下的单向数据流帮我们将大型 UI 组织成小的、独立的、可复用的部分。然而,由于逻辑是有状态的,不能提取到函数或其他组件,我们通常无法进一步分解复杂组件。
这些情况非常常见,包括动画、表单处理、异步请求数据等,以及我们希望从组件中完成的许多其他事情。 当我们试图单独使用组件来解决这些用例时,我们通常会得到:
- 难以重构和测试的大型组件(我们已经制造了很多这种组件)
- 不同组件和生命周期方法之间的重复逻辑(各种场景都很常见)
- 发明了很多复杂的模式,比如 Mixins、 Render porp 和 高阶组件(HOC)
Mixins
主要应用在ES6普及之前,在还使用React.createClass时是非常常见的一种方式,例如:
var LogMixin = {
log: function() {
console.log('log');
},
componentDidMount: function() {
console.log('in');
},
componentWillUnmount: function() {
console.log('out');
}
};
var User = React.createClass({
mixins: [LogMixin],
render: function() {
return (<div>...</div>)
}
});
var Goods = React.createClass({
mixins: [LogMixin],
render: function() {
return (<div>...</div>)
}
});
通过 Mixins,User
和 Goods
都实现了逻辑复用
- ES6 class,规范不支持 mixins。
- 不够直接,mixins 改变了 state,因此也就很难知道一些 state 是从哪里来的,尤其是当不止存在一个 mixins 时,可能还会相互依赖,相互耦合,及不利于代码维护。
- 命名冲突,不同的 mixins 中的方法可能会相互冲突、相互覆盖。
HOC
高阶组件是参数为组件,返回值为新组件的函数。
因为Mixin
带来的危害比他产生的价值还要大,所以React
全面推荐使用高阶组件来替代它。
// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
// ...并返回另一个组件...
return class extends React.Component {
this.state = {
data: selectData(DataSource, props)
};
componentDidMount() {
// ...负责订阅相关的操作...
DataSource.addChangeListener(this.handleChange);
}
this.handleChange = () => {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... 并使用新数据渲染被包装的组件!
// 请注意,我们可能还会传递其他属性
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
但是,是否解决了在使用 mixin 时遇到的问题?
- ES6 class,这里不再是问题了,ES6 class 创建的组件能够和 HOC 结合。
- 不够直接,在 mixin 中,我们不知道 state 从何而来,在 HOC 中,我们不知道 props 从何而来。
- 命名冲突,我们仍然会面临该问题。两个使用了同名 prop 的 HOC 将遭遇冲突并且彼此覆盖,并且这次问题会更加隐晦,因为 React 不会在 prop 重名是发出警告。
而且引入了更多的规则:
- 不能在 render 方法中使用 HOC
- 务必复制静态方法
- Refs 不会被传递
- 理解难度上升,开发者需要“守规矩”(不修改传入组件的原型、透传 props 等)
高阶组件主要解决的问题是代码复用,但没有解决状态的不明确性以及命名冲突,很简单的一个例子就是封装input组件的时候,需要时刻注意value不被重置。
Render Props
由于HOC也存在一定的复杂性,社区又探索出一种新的模式:
在 React 组件之间使用一个值为函数的 prop 来共享代码
具体实现类似:
// 一个类型为函数的prop
class Say extends React.Component {
static propTypes = {
render: PropTypes.func.isRequired
}
// 状态
state = { year: 2020 }
render() {
return (
<div style={{ height: '100%' }}>
{this.props.render(this.state) /* 与子组件共享状态 */}
</div>
)
}
}
// 调用
<Say render={({ year }) => (
// 传入一个可渲染的函数
<div>hi! {year}.</div>
)}/>
虽然解决了 HOC 和 Mixins 中来源不清的 state、props以及命名冲突。但是很不直观,比较反直觉,而且引入了不必要的嵌套。
并且,当我们的使用者的组件为一个React.PureComponent
的时候,由于浅比较 props 的时候总是false,所以会发生不可预料的后果,这就限制了使用者定义的组件。
Hooks
可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hooks将 React 哲学(显式数据流和组合)应用于组件内部,而不仅仅是组件之间。
与Render props 或高阶组件等模式不同,hook不会在组件树中引入不必要的嵌套。 也就没有mixins等的缺点。
假设我们设计一个监听当前窗口宽度的组件(例如,在一个可变视区上显示它的宽度)。
现在有几种方法可以编写这种代码,包括编写一个类,设置一些生命周期方法,或者想在组件之间重用它,甚至可以抽象一个 render prop 或一个高阶组件。但我认为这样会更好:
function MyResponsiveComponent() {
const width = useWindowWidth(); // 使用自定义hook,获取窗口宽高
return (
<p>Window width is {width}</p>
);
}
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
const handleResize = () => setWidth(window.innerWidth);
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return width;
}
甚至,自定义的hook我们可以实现为这样:
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
setWidth(window.innerWidth);
});
return width;
}
Hooks实现的伪代码
// by @jamiebuilds
let hooks = null;
export function useHook() {
hooks.push(hookData);
}
// react内部渲染组件的方法:
function reactInternalRenderAComponentMethod(component) {
hooks = [];
component();
let hooksForThisComponent = hooks;
hooks = null;
}
由于 Hooks 在每次渲染时的顺序都是相同的,因此我们可以为每次调用提供正确的组件状态。
React 把 Hooks 的状态保持在 React 保存类状态的同一个地方。 React 有一个内部更新队列,它是任何状态的真实来源,不论如何定义组件。
由于功能逻辑等的拆离,会让业务组件变得更好维护,下图可以直观感受下:
由此,我们只需要实现较小粒度逻辑的hooks,然后在组件中组合它们就可以完成复杂的交互逻辑。