React系列(五)---|React系列(五)--- 从Mixin到HOC
系列文章
React系列(一)-- 2013起源 OSCON - React Architecture by vjeux
React系列(二)-- React基本语法实现思路
React系列(三)-- Jsx, 合成事件与Refs
React系列(四)--- virtualdom diff算法实现分析
React系列(五)--- 从Mixin到HOC
React系列(六)--- 从HOC再到HOOKS
Mixins(已废弃)
这是React初期提供的一种组合方案,通过引入一个公用组件,然后可以应用公用组件的一些生命周期操作或者定义方法,达到抽离公用代码提供不同模块使用的目的.
曾经的官方文档demo如下
var SetIntervalMixin = {
componentWillMount: function() {
this.intervals = [];
},
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount: function() {
this.intervals.map(clearInterval);
},
};
var TickTock = React.createClass({
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 React has been running for {this.state.seconds} seconds.
;
},
});
React.render( , document.getElementById('example'));
但是Mixins只能应用在
createClass
的创建方式,在后来的class写法中已经被废弃了.原因在于:- mixin引入了隐式依赖关系
- 不同mixins之间可能会有先后顺序甚至代码冲突覆盖的问题
- mixin代码会导致滚雪球式的复杂性
高阶组件(Higher-order component)
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。HOC是一种React的进阶使用方法,大概原理就是接收一个组件然后返回一个新的继承组件,继承方式分两种
属性代理(Props Proxy) 最基本的实现方式
function PropsProxyHOC(WrappedComponent) {
return class NewComponent extends React.Component {
render() {
return
}
}
}
从代码可以看出属性代理方式其实就是接受一个
WrappedComponent
组件作为参数传入,并返回一个继承了 React.Component
组件的类,且在该类的 render()
方法中返回被传入的 WrappedComponent
组件抽离state & 操作props
在高阶组件控制
state
和props
再赋值给组件import React from "react";
function PropsProxyHOC(WrappedComponent) {
return class NewComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
name: 'PropsProxyHOC',
};
}logName() {
console.log(this.name);
}render() {
const newProps = {
name: this.state.name,
logName: this.logName,
};
return ;
}
};
}class Main extends React.Component {
componentDidMount() {
this.props.logName();
}render() {
return PropsProxyHOC;
}
}export default PropsProxyHOC(Main);
有种常见的情况是用来做
双向绑定
import React, { Component } from "react";
function PropsProxyHOC(WrappedComponent) {
return class NewComponent extends React.Component {
constructor(props) {
super(props);
this.state = { fields: {} };
}// 深层更新数据
onChange(fieldName, value) {
const _s = this.state;
this.setState({
fields: {
..._s.fields,
[fieldName]: {
value: value,
onChange: _s.fields[fieldName].onChange,
},
},
});
}getField(fieldName) {
const _s = this.state;
if (!_s.fields[fieldName]) {
_s.fields[fieldName] = {
value: "",
onChange: (event) => {
this.onChange(fieldName, event.target.value);
// 重置输入框
setTimeout(() => this.onChange(fieldName, ""), 2000);
// 强行触发render
this.forceUpdate();
},
};
}return {
value: _s.fields[fieldName].value,
onChange: _s.fields[fieldName].onChange,
};
}render() {
const newProps = {
fields: this.getField.bind(this),
};
// 相当于注入value,onChange属性
return ;
}
};
}// 被获取ref实例组件
class Main extends Component {
render() {
// 相当于设置value,onChange属性
return ;
}
}export default PropsProxyHOC(Main);
获取被继承refs实例
因为这是一个被HOC包装过的新组件,所以想要在HOC里面获取新组件的ref需要用些特殊方式,但是不管哪种,都需要在组件挂载之后才能获取到.并且不能在无状态组件(函数类型组件)上使用 ref 属性,因为无状态组件没有实例。
通过父元素传递方法获取
import React, { Component } from "react";
function PropsProxyHOC(WrappedComponent) {
return class NewComponent extends React.Component {
render() {
const _p = this.props;
// 动态赋值再注入属性
const newProps = {};
// 监听到有对应方法才生成props实例
typeof _p.getInstance === "function" && (newProps.ref = _p.getInstance);
return ;
}
};
}// 被获取ref实例组件
class Main extends Component {
render() {
return Main;
}
}const HOCComponent = PropsProxyHOC(Main);
class ParentComponent extends Component {
componentWillMount() {
console.log("componentWillMount: ", this.wrappedInstance);
// componentWillMount: undefined;
}componentDidMount() {
console.log("componentDidMount: ", this.wrappedInstance);
// componentDidMount: Main实例
}// 提供给高阶组件调用生成实例
getInstance(ref) {
this.wrappedInstance = ref;
}render() {
return ;
}
}export default ParentComponent;
通过高阶组件当中间层 相比较上一方式,需要在高阶组件提供设置赋值函数,并且需要一个props属性做标记
import React, { Component } from "react";
function PropsProxyHOC(WrappedComponent) {
return class NewComponent extends React.Component {
// 暴露给组件的方法,返回ref实例
getWrappedInstance = () => {
if (this.props.withRef) {
return this.wrappedInstance;
}
};
// 暴露给组件的方法,设置ref实例
setWrappedInstance = (ref) => {
this.wrappedInstance = ref;
};
render() {
const newProps = {};
// 监听到有对应方法才赋值props实例
this.props.withRef && (newProps.ref = this.setWrappedInstance);
return ;
}
};
}// 被获取ref实例组件
class Main extends Component {
render() {
return Main;
}
}const HOCComponent = PropsProxyHOC(Main);
class ParentComponent extends Component {
componentWillMount() {
console.log("componentWillMount: ", this.refs.child);
// componentWillMount: undefined;
}componentDidMount() {
console.log("componentDidMount: ", this.refs.child.getWrappedInstance());
// componentDidMount: Main实例
}render() {
return ;
}
}export default ParentComponent;
forwardRef(16.3新增) React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:
- 转发 refs 到 DOM 组件
- 在高阶组件中转发 refs
const FancyButton = React.forwardRef((props, ref) => ( )); // You can now get a ref directly to the DOM button: const ref = React.createRef();
Click me! ;
以下是对上述示例发生情况的逐步解释:
- 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
- 我们通过指定 ref 为 JSX 属性,将其向下传递给
。 - React 传递 ref 给 fowardRef 内函数 (props, ref) => ...,作为其第二个参数。
- 我们向下转发该 ref 参数到
,将其指定为 JSX 属性。
- 当 ref 挂载完成,ref.current 将指向
DOM 节点。
最简单的例子莫过于loading组件了
import React, { Component } from "react";
function PropsProxyHOC(WrappedComponent) {
return class NewComponent extends React.Component {
render() {
// 根据状态渲染界面
return this.props.isLoading ? (
Loading...
) : (
);
}
};
}// 被获取ref实例组件
class Main extends Component {
render() {
return Main;
}
}const HOCComponent = PropsProxyHOC(Main);
class ParentComponent extends Component {
constructor() {
super();
this.state = {
isLoading: true,
};
}render() {
// 延迟出现主界面
setTimeout(() => this.setState({ isLoading: false }), 2000);
return ;
}
}export default ParentComponent;
当然也能用于布局上嵌套在其他元素输出
反向继承(Inheritance Inversion) 最简单的demo代码
function InheritanceInversionHOC(WrappedComponent) {
return class NewComponent extends WrappedComponent {
render() {
return super.render();
}
};
}
在这里
WrappedComponent
成了被继承的那一方,从而可以在高阶组件中获取到传递组件的所有相关实例获取继承组件实例
import React, { Component } from "react";
function InheritanceInversionHOC(WrappedComponent) {
return class NewComponent extends WrappedComponent {
componentDidMount() {
console.log("componentDidMount: ", this);
// componentDidMount: NewComponent实例
}render() {
return super.render();
}
};
}// 被获取ref实例组件
class Main extends Component {
constructor() {
super();
this.state = {
name: "WrappedComponent",
};
}render() {
return Main;
}
}export default InheritanceInversionHOC(Main);
cloneElement
再讲解demo之前先科普React的一个方法
React.cloneElement(
element,
[props],
[...children]
)
以
element
元素为样板克隆并返回新的 React 元素。config
中应包含新的 props
,key
或 ref
。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,如果在 config
中未出现 key
或 ref
,那么原始元素的 key
和 ref
将被保留。React.cloneElement() 几乎等同于:
{children}
但是,这也保留了组件的 ref。这意味着当通过 ref 获取子节点时,你将不会意外地从你祖先节点上窃取它。相同的 ref 将添加到克隆后的新元素中。
修改props和劫持渲染
相比属性继承来说,反向继承修改props会比较复杂一点
import React, { Component } from "react";
function InheritanceInversionHOC(WrappedComponent) {
return class NewComponent extends WrappedComponent {
constructor() {
super();
this.state = {
a: "b",
};
}render() {
// 生成实例
const wrapperTree = super.render();
// 新的属性
const newProps = {
name: "NewComponent",
};
// 以 wrapperTree 元素为样板克隆并返回新的 React 元素。
const newTree = React.cloneElement(
wrapperTree,
newProps,
// 包括组件的子元素也需要保留
wrapperTree.props.children
);
console.log("newTree: ", newTree);
/* {
type: "div"
key: null
ref: "child"
props: Object
children: "Main"
name: "NewComponent"
_owner: FiberNode
_store: Object
}*/
return newTree;
}
};
}class Main extends Component {
render() {
//原始元素的ref将被保留。
return Main;
}
}export default InheritanceInversionHOC(Main);
为什么需要用到
cloneElement
方法?
因为render
函数内实际上是调用React.creatElement
产生的React元素,尽管我们可以拿到这个方法但是无法修改它.可以用getOwnPropertyDescriptors
查看它的配置项,所以用cloneElement
创建新的元素替代相比较属性继承来说,后者只能条件性选择是否渲染
WrappedComponent
,但是前者可以更加细粒度劫持渲染元素,可以获取到 state,props,组件生命周期(component lifecycle)钩子,以及渲染方法(render),但是依旧不能保证WrappedComponent
里的子组件是否渲染,也无法劫持.注意事项 静态属性失效
// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
因为高阶组件返回的已经不是原组件了,所以原组件的静态属性方法已经无法获取,除非你主动将它们拷贝到返回组件中
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必须准确知道应该拷贝哪些方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
除了导出组件,另一个可行的方案是再额外导出这个静态方法。
// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...单独导出该方法...
export { someFunction };
// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';
渲染机制
React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。
因为高阶组件返回的是新组件,里面的唯一标志也会变化,所以不建议在render里面也调用高阶组件,这会导致其每次都重新卸载再渲染,即使它可能长得一样.
render() {
// 每次调用 render 函数都会创建一个新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
return ;
}
这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。
如果在组件之外创建 HOC,这样一来组件只会创建一次。因此,每次 render 时都会是同一个组件。一般来说,这跟你的预期表现是一致的。
所以建议高阶组件都是无副作用的纯函数,即相同输入永远都是相同输出,不允许任何有可变因素.
嵌套过深
在原组件中如果包裹层级过多会产生类似回调地狱的烦恼,难以调试,可阅读性糟糕
遵守规则
【React系列(五)---|React系列(五)--- 从Mixin到HOC】如果没有规范情况下,也可能造成代码冲突覆盖的局面,例如
- 将不相关的 props 传递给被包裹的组件
- 最大化可组合性
- 包装显示名称以便轻松调试
推荐阅读
- React系列(六)---|React系列(六)--- 从HOC再到HOOKS
- 《Python零基础入门与进阶》系列专栏完结
- Windows原理深入学习系列-信任等级检查
- Java|Java面试突击系列(十二)(数据库分库分表的面试连环炮)
- POSTMAN从入门到精通系列(二十六)(使用标签)
- 测试|jmeter系列(关联数据库)
- Arcgis|ArcGIS操作系列10- Arcmap 中矢量图层面积的计算方法
- 学习笔记(带你十天轻松搞定|学习笔记:带你十天轻松搞定 Go 微服务系列大结局(十)- 分布式事务)
- 每周知识总结|每周知识总结(五)
- React|React 组件中的state和setState()你知道多少