咱们都知道在业务开发的过程当中,若是彻底不一样的组件有类似的功能,这就会产生横切关注点(cross-cutting concerns)
问题。javascript
在React中,存在一些最佳实践去处理横切关注点的问题,能够帮助咱们更好地进行代码的逻辑复用。html
针对这个问题,在使用createReactClass
建立 React 组件的时候,引入 mixins 功能会是一个很好的解决方案。java
为了在初始阶段更加容易地适应和学习React,官方在 React 中包含了一些急救方案。mixin 系统是其中之一。react
因此咱们能够将通用共享的方法包装成Mixins方法,而后注入各个组件进行逻辑复用的实现。编程
const mixin = function(obj, mixins) {
const newObj = obj;
newObj.prototype = Object.create(obj.prototype);
for (let prop in mixins) {
if (mixins.hasOwnProperty(prop)) {
newObj.prototype[prop] = mixins[prop];
}
}
return newObj;
}
复制代码
上述代码就实现了一个简单的mixin函数,其实质就是将mixins中的方法遍历赋值给newObj.prototype
,从而实现mixin返回的函数建立的对象都有mixins中的方法,也就是把额外的功能都混入进去。设计模式
在咱们大体明白了mixin做用后,让咱们来看看如何在React使用mixin。数组
var RowMixin = {
renderHeader: function() {
return (
<div className='row-header'> <h1> {this.getHeaderText()} </h1> </div>
);
}
};
var UserRow = React.createClass({
mixins: [RowMixin], // 混入renderHeader方法
getHeaderText: function() {
return this.props.user.fullName;
},
render: function() {
return (
<div> {this.renderHeader()} <h2>{this.props.user.biography}</h2> </div>
)
}
});
复制代码
使用React.createClass
,官方提供了mixins的接入口。须要复用的代码逻辑从这里混入就能够。app
这是ES5的写法,实际上React16版本后就已经废弃了。ide
ES6 自己是不包含任何 mixin 支持。所以,当你在 React 中使用 ES6 class 时,将不支持 mixins 。函数式编程
官方也发现了不少使用 mixins 而后出现了问题的代码库。而且不建议在新代码中使用它们。
Mixins 引入了隐式的依赖关系(Mixins introduce implicit dependencies)
Mixins 引发名称冲突(Mixins cause name clashes)
Mixins 致使滚雪球式的复杂性(Mixins cause snowballing complexity)
引自官方博客: reactjs.org/blog/2016/0…
官方博客里面有一篇文章详细描述了弃用的缘由。里面列举了三条罪状,如上所述。
在实际开发的过程当中,咱们没法预知别人往代码里mixin了什么属性和状态。若是想要mixin本身的功能,可能会发生冲突,甚至须要去解耦以前的代码。
这样的方式同时也破坏了组件的封装性,代码之间的依赖是不可见的,给重构代码也带来了必定的难度。若是对组件进行修改,极可能会致使mixin方法错误或者失效。
在日后的开发维护过程当中,就致使了滚雪球式的复杂性。
组件中含有多个mixin——
不一样的mixin中含有相同名字的非生命周期函数,React会抛出异常(不是后面的函数覆盖前面的函>数)。
不一样的mixin中含有相同名字的生命周期函数,不会抛出异常,mixin中的相同的生命周期函数(除render方法)会按照createClass中传入的mixins数组顺序依次调用,所有调用结束后再调用组件内部的相同的声明周期函数。
不一样的mixin中默认props或初始state中存在相同的key值时,React会抛出异常。
mixin里面对不一样状况名称冲突的处理,只有当相同名称的生命周期函数,才会按照声明的顺序调用,最后调用组件内部的同名函数。其余状况下都会抛出异常。
mixin这种混入模式,会给组件不断增长新的方法和属性,组件自己不只能够感知,甚至须要作相关的处理(例如命名冲突、状态维护),一旦混入的模块变多时,整个组件就变的难以维护,也就是为何如此多的React库都采用高阶组件的方式进行开发。
在mixin废弃后,不少开源组件库都是使用的高阶组件写法。
高阶组件属于函数式编程(functional programming)思想。
对于被包裹的组件时不会感知到高阶组件的存在,而高阶组件返回的组件会在原来的组件之上具备功能加强的效果。
说到高阶组件,先要说一下高阶函数的定义。
在数学和计算机科学中,高阶函数是至少知足下列一个条件的函数:
接受一个或多个函数做为输入
输出一个函数
简单地来讲,高阶函数就是接受函数做为输入或者输出的函数。
const add = (x,y,f) => f(x)+f(y);
add(-5, 6, Math.abs);
复制代码
A higher-order component is a function that takes a component and returns a new component.
高阶组件是一个接受组件而且返回新组件的函数,注意虽然名字叫高阶组件但它自身是一个函数,它能够加强它所包裹的组件功能,或者说赋予了它所包裹的组件一个新的功能。
它不是React API的一部分,源自于React生态,是官方推崇的复用组合的一种方式。它对应着设计模式中的装饰者模式。
高阶组件,主要有两种方式处理包裹组件的方式,分别是属性代理和反向继承。
实质上是经过包裹原来的组件来操做props
操做props
得到refs引用
抽象state
用其余元素包裹组件
export default function withHeader(WrappedComponent) {
return class HOC extends Component {
render() {
const newProps = {
test:'hoc'
}
// 透传props,而且传递新的newProps
return <div> <WrappedComponent {...this.props} {...newProps}/> </div> } } } 复制代码
属性代理,其实是经过包裹原来的组件,来注入一些额外的props或者state。
为了加强可维护性,有一些固有的约定,好比命名高阶组件的时候须要使用withSomething
的格式。
对于传入的props最好直接透传,不要破坏组件自己的属性和状态。
渲染劫持
操做props和state
export default function (WrappedComponent) {
return class Inheritance extends WrappedComponent {
componentDidMount() {
// 能够方便地获得state,作一些更深刻的修改。
console.log(this.state);
}
render() {
return super.render();
}
}
}
复制代码
反向继承能够经过super
关键字获取到父类原型对象上的全部方法(父类实例上的属性或方法则没法获取)。在这种方式中,它们的关系看上去被反转(inverse)了。
反向继承能够劫持渲染,能够进行延迟渲染/条件渲染等操做。
约定:将不相关的 props 传递给被包裹的组件
约定:包装显示名称以便轻松调试
约定:最大化可组合性
// 而不是这样...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// ... 你能够编写组合工具函数
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
// 这些都是单参数的 HOC
withRouter,
connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
复制代码
compose能够帮助咱们组合任意个(包括0个)高阶函数,例如compose(a,b,c)返回一个新的函数d,函数d依然接受一个函数做为入参,只不过在内部会依次调用c,b,a,从表现层对使用者保持透明。 基于这个特性,咱们即可以很是便捷地为某个组件加强或减弱其特征,只须要去变动compose函数里的参数个数便可。
模块复用
页面鉴权
日志及性能打点
…
export const withTimer = (interval) => (wrappedComponent) => {
return class extends wrappedComponent {
constructor(props) {
super(props);
}
// 传入endTime 计算剩余时间戳
endTimeStamp = DateUtils.parseDate(this.props.endTime).getTime();
componentWillMount() {
// 未过时则手动调用计时器 开始倒计时
if (Date.now() < this.endTimeStamp) {
this.onTimeChange();
this.setState({expired: false});
this.__timer = setInterval(this.onTimeChange, interval);
}
}
componentWillUnmount() {
// 清理计时器
clearInterval(this.__timer);
}
onTimeChange = () => {
const now = Date.now();
// 根据剩余时间戳计算出 时、分、秒注入到目标组件
const ret = Helper.calc(now, this.endTimeStamp);
if (ret) {
this.setState(ret);
} else {
clearInterval(this.__timer);
this.setState({expired: true});
}
}
render() {
// 反向继承
return super.render();
}
};
};
复制代码
@withTimer()
export class Card extends React.PureComponent {
render() {
const {data, endTime} = this.props;
// 直接取用hoc注入的状态
const {expired, minute, second} = this.state;
// 略去render逻辑
return (...);
}
}
复制代码
需求是须要进行定时器倒计时,不少组件都须要注入倒计时功能。那么咱们把它提取为一个高阶组件。
这是一个反向继承的方式,能够拿到组件自己的属性和状态,而后把时分秒等状态注入到了组件中。
原组件使用了ES7的装饰器语法,就能够增强它的功能。
组件自己只须要有一个endTime
的属性,而后高阶组件就能够计算出时分秒而且进行倒计时。
也就是说,高阶组件赋予了原组件倒计时的功能。
在使用高阶组件写法时,也有一些注意事项。
render() {
// 每次调用 render 函数都会建立一个新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 这将致使子树每次渲染都会进行卸载,和从新挂载的操做!
return <EnhancedComponent />; } 复制代码
若是在render函数中建立,每次都会从新渲染一个新的组件。这不只仅是性能问题,每次重置该组件的状态,也可能会引发代码逻辑错误。
// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 如今使用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 加强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
复制代码
当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。
你可使用hoist-non-react-statics
自动拷贝全部非 React 静态方法:
通常来讲,高阶组件能够传递全部的props属性给包裹的组件,可是不能传递 refs 引用。由于并非像 key 同样,refs 是一个伪属性,React 对它进行了特殊处理。
若是你向一个由高级组件建立的组件的元素添加 ref 应用,那么 ref 指向的是最外层容器组件实例的,而不是包裹组件。
在不编写class的状况下使用state以及其余的React特性。
Hook是一些可让你在函数组件hook react state及生命周期等特性的函数。它不能在class组件中使用。
在组件之间复用状态逻辑
render props
任何被用于告知组件须要渲染什么内容的函数props在技术上均可以被成为称为render prop
若是在render方法里建立匿名函数,那么使用render prop会抵消使用React.PureComponent带来的优点。 须要把render方法建立为实例函数,或者做为全局变量传入。
hoc
providers
consumers
这些抽象层组成的组件会造成嵌套地狱,所以React须要为共享状态逻辑提供更好的原生途径。
加强代码可维护性
class难以理解
React社区接受了React hooks的提案,这将减小编写 React 应用时须要考虑的概念数量。
Hooks 可使得你始终使用函数,而没必要在函数、类、高阶组件和 reader props之间不断切换。
基础 Hook
useState
useEffect
启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。
useContext
额外的 Hook
useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue
自定义Hook useSomething
自定义Hook是一种重用状态逻辑的机制,全部的state和反作用都是彻底隔离的。
官方已经弃用了一些生命周期,useEffect
至关于componentDidMount
,componentDidUpdate
和 componentWillUnmount
。
除了官方提供的Hook API之外,你可使用自定义Hook。
自定义 Hook 不须要具备特殊的标识。咱们能够自由的决定它的参数是什么,以及它应该返回什么(若是须要的话)。
换句话说,它就像一个正常的函数。可是它的名字应该始终以 use
开头,这样能够一眼看出其符合 Hook 的规则。
动画、订阅声明、计时器是自定义Hook的一些经常使用操做。
接下来,咱们来用React Hook改写一下以前的高阶组件demo。
export function useTimer(endTime, interval, callback) {
interval = interval || 1000;
// 使用useState Hook get/set状态
const [expired, setExpired] = useState(true);
const endTimeStamp = DateUtils.parseDate(endTime).getTime();
function _onTimeChange () {
const now = Date.now();
// 计算时分秒
const ret = Helper.calc(now, endTimeStamp);
if (ret) {
// 回调传出所需的状态
callback({...ret, expired});
} else {
clearInterval(this.__timer);
setExpired(true);
callback({expired});
}
}
// 使用useEffect代替生命周期的调用
useEffect(() => {
if (Date.now() < endTimeStamp) {
_onTimeChange();
setExpired(false);
this.__timer = setInterval(_onTimeChange, interval);
}
return () => {
// 清除计时器
clearInterval(this.__timer);
}
})
}
复制代码
export function Card (props) {
const {data, endTime} = props;
const [expired, setExpired] = useState(true);
const [minute, setMinute] = useState(0);
const [second, setSecond] = useState(0);
useTimer(endTime, 1000, ({expired, minute, second}) => {
setExpired(expired);
setMinute(minute);
setSecond(second);
});
return (...);
复制代码
自定义Hook除了命名须要遵循规则,参数传入和返回结果均可以根据具体状况来定。
这里,我在定时器每秒返回后传出了一个callback,把时分秒等参数传出。
除此以外能够看到没有class的生命周期,使用useEffect
来完成反作用的操做。
使用一个eslint-plugin-react-hooks
ESLint插件来强制执行这些规则
不要在循环
,条件
或嵌套函数
中调用 Hook, 确保老是在React 函数的最顶层调用他们。
由于React是根据你声明的顺序去调用hooks的,若是不在最顶层调用,那么不能保证每次渲染的顺序都是相同的。
遵照规则,React 才可以在屡次的 useState
和 useEffect
调用之间保持 hook 状态的正确。
只在 React 函数中调用 Hook
在 React 的函数组件中调用 Hook
在自定义 Hook 中调用其余 Hook