复用
基于框架开发可复用的组件,代码片段,才能让我们的日常开发提效。
React在复用上的探索
var SetIntervalMixin = {
componentWillMount: function() {
this.intervals = [];
},
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount: function() {
this.intervals.forEach(clearInterval);
}
};
Mixin (混入)是最早的复用方式,它是一个对象,可以包含自定义的方法和 React 生命周期函数,用来扩充 React 组件的能力,通常把可复用的逻辑放到 mixin 中,在需要使用到的地方引入。
var createReactClass = require('create-react-class');
var TickTock = createReactClass({
mixins: [SetIntervalMixin], // Use the mixin
getInitialState: function() {
return {seconds: 0};
},
componentDidMount: function() {
this.setInterval(this.tick, 1000); // Call a method on the mixin
},
tick: function() {
this.setState({seconds: this.state.seconds + 1});
},
render: function() {
return (
<p>
React has been running for {this.state.seconds} seconds.
</p>
);
}
});
通过 mixins 属性引入需要的 mixin,就可以直接在组件中使用混入的方法。
Mixin的缺点主要有以下几点:
1. 数据源来源不清晰。当一个组件使用了多个 mixin 时,很难判断哪个方法/变量来自哪里。
2. 命名空间冲突。不同开发者开发的 mixin 无法保证命名不会和其他的冲突,为了减少冲突几率可能会在命名时添加&,_等特殊字符,影响可读性。
3. 复杂度会滚雪球般上升。当开发者想基于现有 mixin 扩展新 mixin 或者对 mixin 中逻辑增加新功能时,代码会变得难以理解,并且耦合严重。
4. ES6 不支持 mixin 所以使用 ES6 Class 语法的 React 组件无法使用 mixin。
在函数式编程中,如果一个函数 接受一个或多个函数作为参数或者返回一个函数 就可称之为 高阶函数。
如果一个函数 接受一个或多个组件作为参数并且返回一个组件 就可称之为 高阶组件。
function StoreMixin(...stores) {
var Mixin = {
getInitialState() {
return this.getStateFromStores(this.props);
},
componentDidMount() {
stores.forEach(store =>
store.addChangeListener(this.handleStoresChanged)
);
this.setState(this.getStateFromStores(this.props));
},
componentWillUnmount() {
stores.forEach(store =>
store.removeChangeListener(this.handleStoresChanged)
);
},
handleStoresChanged() {
if (this.isMounted()) {
this.setState(this.getStateFromStores(this.props));
}
}
};
return Mixin;
}
var UserProfilePage = React.createClass({
mixins: [StoreMixin(UserStore)],
propTypes: {
userId: PropTypes.number.isRequired
},
getStateFromStores(props) {
return {
user: UserStore.get(props.userId);
}
}
render() {
var { user } = this.state;
return <div>{user ? user.name : 'Loading'}</div>;
}
});
假设有一个 mixin,订阅了特定的 Flux Stores,并且可触发组件状态改变。它可能长这样:
function connectToStores(Component, stores, getStateFromStores) {
const StoreConnection = React.createClass({
getInitialState() {
return getStateFromStores(this.props);
},
componentDidMount() {
stores.forEach(store =>
store.addChangeListener(this.handleStoresChanged)
);
},
componentWillUnmount() {
stores.forEach(store =>
store.removeChangeListener(this.handleStoresChanged)
);
},
handleStoresChanged() {
if (this.isMounted()) {
this.setState(getStateFromStores(this.props));
}
},
render() {
return <Component {...this.props} {...this.state} />;
}
});
return StoreConnection;
};
var ProfilePage = React.createClass({
propTypes: {
userId: PropTypes.number.isRequired,
user: PropTypes.object // note that user is now a prop
},
render() {
var { user } = this.props; // get user from props
return <div>{user ? user.name : 'Loading'}</div>;
}
});
// Now wrap ProfilePage using a higher-order component:
ProfilePage = connectToStores(ProfilePage, [UserStore], props => ({
user: UserStore.get(props.userId)
});
起来和 mixin 非常类似,但是它包装了组件并传递 state 给这个被包装的组件,而不是直接操作被包装组件的 state。通过简单的组件嵌套,包装组件的生命周期,无需任何特殊的合并行为就可以发挥作用。
HOC 很棒,它的本质是以函数式的方式将组件中的关注点分离。
不能在 ES6 Class 中使用。✅
数据源来源不清晰。当一个组件使用了多个 mixin 时,很难清除哪个方法/变量来自哪里。❎
命名空间冲突。不同开发者开发的 mixin 无法保证命名不会和其他的冲突,为了减少冲突几率肯能会在命名时添加&,_等特殊字符,影响可读性。❎
复杂度会滚雪球般上升。当开发者想基于现有 mixin 扩展新 mixin 或者对mixin 中逻辑增加新功能时,代码会变得难以理解,并且耦合严重。❎
过多的组件包裹产生 wrapper hell
// Function as Child Components
class Wrap extends React.Component {
state = {
name: 'hello world'
}
render() {
return (
<div>
{this.props.children(name)}
</div>
);
}
}
<Wrap>
{name => (
<h2>{name}</h2>
)}
</Wrap>
// render props
class Wrap extends React.Component {
render() {
return (
<div>
{this.props.render('hello world')}
</div>
);
}
}
<Wrap render={name => (
<h2>{name}</h2>
)}
/>
render props 是一种在 React 组件之间使用一个值为函数的 prop 共享代码的技术
render props
mixin && HOC
不能在 ES6 Class 中使用。
数据源来源不清晰。当一个组件使用了多个 mixin 时,很难清除哪个方法/变量来自哪里。
命名空间冲突。不同开发者开发的 mixin 无法保证命名不会和其他的冲突,为了减少冲突几率肯能会在命名时添加&,_等特殊字符,影响可读性。
复杂度会滚雪球般上升。当开发者想基于现有 mixin 扩展新 mixin 或者对mixin 中逻辑增加新功能时,代码会变得难以理解,并且耦合严重。
本质是利用了闭包,会引起 callback hell 的问题。
React Conf 2018 上,React 团队 leader 指出 React 仍然存在的问题:
必须调用 super(props),很烦
方法要手动 bind this
通过生命周期组织代码,强制把相关的代码穿插在不同的生命周期之间
难以优化,Class 语法在 Babel 编译后代码量增加很多
React 的方向从 Class Component 转向 Function Component
// 一个简单的函数组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
不支持state
不支持生命周期
React 缺少一种方便的,在函数组件中处理 state,生命周期 的机制。
Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.
顾名思义,useState 让函数组件可以使用 state。入参是 initialState,返回一个数组,第一值是 state,第二个值是改变 state 的函数。
import React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
如果 initialState 的提高需要消耗大量的计算力,同时不期望这些计算阻塞后面要干的事情的话。initialState 可以是个函数,会在 render 前调用达到 Lazy Calc 的效果。
同时为了避免不必要的性能开销,在设置 State 的时候如果两个值是相等的,则也不会触发 rerender。(判断两个值相等使用的是 Object.is)
Effect hooks 用来处理函数组件中的副作用。useEffect 相当于 componentDidMount, componentDidUpdate, componentWillUnmount 的组合。在组件挂载、更新、卸载的时候都会执行 effect 里面的函数。可以认为 Effect hooks 的执行时机是 “after render”
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
Class Component 一般在 componentWillUnmount 中做取消订阅的操作。
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
需要注意的是这里的 subscribe 和 unsubscribe每次after render都会触发,可以减少bug(比如prosps.friend.id突然变化)
useEffect 的返回值会作为 cleanup 函数,在组件更新后或卸载前执行。
如果 effects 确实不需要每次 after render 都执行,可以向useEffect传入第二个参数跳过不必要的effects。第二个数组中的变量表示是这个 effect 的依赖,仅当依赖发生变化时才重新渲染。如果传入空数组,effect 只执行一次(didMount阶段)。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
// 类似于
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
我们可以基于 useState,useEffect 封装自定义 hooks
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
// 使用
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
return isOnline === null ? 'Loading...' : isOnline ? 'Online' : 'Offline';
}
对比 Class Component
hooks