ホームページ > 記事 > ウェブフロントエンド > React高次コンポーネントとES6デコレータの応用を詳しく解説(コード付き)
この記事では、React の高位コンポーネントと ES6 デコレーターのアプリケーションについて詳しく説明しています (コード付き)。必要な方は参考にしていただければ幸いです。 。
1 デコレータ パターン
クラスの継承よりもオブジェクトの合成を優先します。 --「デザイン パターン」
1. デコレータ パターンとは
定義: 追加のプロパティまたは動作をオブジェクトに動的に追加します。継承を使用する場合と比較して、デコレータ パターンはより柔軟です。
2. デコレータ パターンの参加者
コンポーネント:デコレータとデコレータの共通の親クラスは、基本的な動作を定義するために使用されるインターフェイスまたは抽象クラスです。 ##ConcreteComponent
: 特定のオブジェクト、つまりデコレータを定義します。 Decorator:
Abstract デコレータは Component から継承され、外部クラスから ConcreteComponent を拡張します。 ConcreteComponent の場合、Decorator の存在を知る必要はありません。 Decorator はインターフェイスまたは抽象クラスです。ConcreteDecorator:
ConcreteComponent を拡張するために使用されるコンクリート デコレータです。注: デコレータと装飾されたオブジェクトには、ここでは、動作を取得するために継承を使用するのではなく、デコレータとデコレータが同じタイプである必要があるため、継承を使用して型の一致を実現します。
継承を使用してサブクラスを設計することは、コンパイル時にのみ静的に決定でき、すべてのサブクラスは同じ動作を継承します。組み合わせを使用してオブジェクトを展開すると、実行時に動的に展開できます。デコレータ パターンはオープンクローズの原則に従います。つまり、クラスは拡張のためにオープンし、変更のためにクローズする必要があります。デコレータを使用すると、既存のコードを変更せずに新しいデコレータを実装して新しい動作を追加できます。ただし、継承のみに依存すると、新しい動作が必要になるたびに既存のコードを変更する必要があります。
高次コンポーネントが実際にどのようなものであるかは誰もが知っていますが、渡されるパラメーターは同じです。
A higher-order component is a function that takes a component and returns a new component.
のような形状:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
上位コンポーネントは React アプリケーションの重要な部分であり、その最大の特徴はコンポーネント ロジックの再利用です。 。 React APIで定義された機能ではなく、Reactの構成機能から派生したデザインパターンです。
redux を使用したことがある場合は、react-redux の connect が上位コンポーネントであるため、上位コンポーネントに触れたことがあるはずです。
最も単純な上位コンポーネントから始めましょう
import React, { Component } from 'react'; import simpleHoc from './simple-hoc'; class Usual extends Component { render() { console.log(this.props, 'props'); return ( dc6dce4a544fdca2df29d5ac0ea9906b Usual 16b28748ea4df4d9c2150843fecfba68 ) } } export default simpleHoc(Usual); import React, { Component } from 'react'; const simpleHoc = WrappedComponent => { console.log('simpleHoc'); return class extends Component { render() { return 52be7657ec74da90f3cdb693591b3ded } } } export default simpleHoc;
コンポーネントUsualはsimpleHocによってパッケージ化されており、ログを持っています...次に、コンポーネントを受け取ることにより、simpleHocが上位コンポーネントになります。 class 通常、コンポーネント クラスを返します。実際、この関数では多くの操作を実行できることがわかります。また、戻りコンポーネントにも独自のライフサイクルと関数があります。さらに、props を WrappedComponent (ラップされたコンポーネント) に渡すこともできることがわかります。
上位コンポーネントを実装するには 2 つの方法があります。
プロパティ プロキシ (props プロキシ)。高次コンポーネントは、ラップされた React コンポーネントを通じて props を操作します。逆継承 (継承反転)。高次コンポーネントは、ラップされた React コンポーネントを継承します。
導入部分で書いた最も単純な形式は、Props プロキシの形式です。 WrappedComponent は、hoc (この例では Usual) を介してラップされます。最初に Usual に渡された props は、props プロキシである hoc で受信されます。これにより、いくつかの操作を行うことができます。
1. props を操作する最も直感的な方法は、props の読み取り、編集、削除などの多くのカスタム操作を実行することです。 hoc で定義されたカスタム イベントを含め、それらは props を介して渡すことができます。
import React, { Component } from 'react'; const propsProxyHoc = WrappedComponent => class extends Component { handleClick() { console.log('click'); } render() { return (<WrappedComponent {...this.props} handleClick={this.handleClick} />); } }; export default propsProxyHoc;
Usual コンポーネントがレンダリングされると、console.log(this.props) は handleClick を取得します。
Usual をラップするとき、必要に応じてそのインスタンスを取得するには、参照 (ref) を使用できます。Usual コンポーネントがマウントされている場合、ref のコールバック関数が実行されて、hoc でコンポーネントのインスタンスを取得します。
import React, { Component } from 'react'; const refHoc = WrappedComponent => class extends Component { componentDidMount() { console.log(this.instanceComponent, 'instanceComponent'); } render() { return (<WrappedComponent {...this.props} ref={instanceComponent => this.instanceComponent = instanceComponent} />); } }; export default refHoc;
3. 状態の抽出
ここでは、状態は ref を通じて取得されるのではなく、{ props, callback function } を通じて WrapComponent コンポーネントに渡され、状態はコールバック関数を通じて取得されます。ここでさらに使用されるのは、react がフォームを処理する場合です。通常、react がフォームを処理するときは、制御されたコンポーネント (ドキュメント) を使用します。つまり、入力が制御され、値が変更されると、onChange イベントを使用して状態と同期します。もちろん、この種の操作は Container コンポーネントを通じて実行することもできます。具体的な違いについては後で比較します。コードを見るだけで、何が起こっているかがわかります:
import React, { Component } from 'React'; const MyContainer = (WrappedComponent) => class extends Component { constructor(props) { super(props); this.state = { name: '', 4 }; this.onNameChange = this.onNameChange.bind(this); } onNameChange(event) { this.setState({ name: event.target.value, }) } render() { const newProps = { name: { value: this.state.name, onChange: this.onNameChange, }, } return b380ec041c5bd892a7454b55ec3a227e; } }
この例では、入力コンポーネントの name prop の onChange メソッドを上位コンポーネントに抽出し、効果的に抽象化しています。そのままの状態で動作します。
const MyContainer = (WrappedComponent) => class extends WrappedComponent { render() { return super.render(); } }
正如所见,高阶组件返回的组件继承于 WrappedComponent。因为被动地继承了 WrappedCom- ponent,所有的调用都会反向,这也是这种方法的由来。
这种方法与属性代理不太一样。它通过继承 WrappedComponent 来实现,方法可以通过 super 来顺序调用。因为依赖于继承的机制,HOC 的调用顺序和队列是一样的:
didmount→HOC didmount→(HOCs didmount)→will unmount→HOC will unmount→(HOCs will unmount)
在反向继承方法中,高阶组件可以使用 WrappedComponent 引用,这意味着它可以使用WrappedComponent 的 state、props 、生命周期和 render 方法。但它不能保证完整的子组件树被解析。
1.渲染劫持
渲染劫持指的就是高阶组件可以控制 WrappedComponent 的渲染过程,并渲染各种各样的结 果。我们可以在这个过程中在任何 React 元素输出的结果中读取、增加、修改、删除 props,或 读取或修改 React 元素树,或条件显示元素树,又或是用样式控制包裹元素树。
正如之前说到的,反向继承不能保证完整的子组件树被解析,这意味着将限制渲染劫持功能。 渲染劫持的经验法则是我们可以操控 WrappedComponent 的元素树,并输出正确的结果。但如果 元素树中包括了函数类型的 React 组件,就不能操作组件的子组件。
我们先来看条件渲染的示例:
const MyContainer = (WrappedComponent) => class extends WrappedComponent { render() { if (this.props.loggedIn) { return super.render(); } else { return null; } } }
第二个示例是我们可以对 render 的输出结果进行修改:
const MyContainer = (WrappedComponent) => class extends WrappedComponent { render() { const elementsTree = super.render(); let newProps = {}; if (elementsTree && elementsTree.type === 'input') { newProps = {value: 'may the force be with you'}; } const props = Object.assign({}, elementsTree.props, newProps); const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children); return newElementsTree; } }
在这个例子中,WrappedComponent 的渲染结果中,顶层的 input 组件的 value 被改写为 may the force be with you。因此,我们可以做各种各样的事,甚至可以反转元素树,或是改变元素 树中的 props。这也是 Radium 库构造的方法。
2.控制state
高阶组件可以读取、修改或删除 WrappedComponent 实例中的 state,如果需要的话,也可以 增加 state。但这样做,可能会让 WrappedComponent 组件内部状态变得一团糟。大部分的高阶组 件都应该限制读取或增加 state,尤其是后者,可以通过重新命名 state,以防止混淆。
我们来看一个例子:
const MyContainer = (WrappedComponent) => class extends WrappedComponent { render() { return ( e388a4556c0f65e1904146cc1a846bee c1a436a314ed609750bd7c7d319db4daHOC Debugger Component2e9b454fa8428549ca2e64dfac4625cd e388a4556c0f65e1904146cc1a846beeProps94b3e26ee717c64999d7867364b1b4a3 e03b848252eb9375d56be284e690e873{JSON.stringify(this.props, null, 2)}bc5574f69a0cba105bc93bd3dc13c4ec e388a4556c0f65e1904146cc1a846beeState94b3e26ee717c64999d7867364b1b4a3 e03b848252eb9375d56be284e690e873{JSON.stringify(this.state, null, 2)}bc5574f69a0cba105bc93bd3dc13c4ec {super.render()} 94b3e26ee717c64999d7867364b1b4a3 ); } }
在这个例子中,显示了 WrappedComponent 的 props 和 state,以方便我们在程序中去调试它们。
高阶组件可以看做是装饰器模式(Decorator Pattern)在React的实现。即允许向一个现有的对象添加新的功能,同时又不改变其结构,属于包装模式(Wrapper Pattern)的一种
ES7中添加了一个decorator的属性,使用@符表示,可以更精简的书写。那上面的例子就可以改成:
import React, { Component } from 'react'; import simpleHoc from './simple-hoc'; @simpleHoc export default class Usual extends Component { render() { return ( e388a4556c0f65e1904146cc1a846bee Usual 94b3e26ee717c64999d7867364b1b4a3 ) } } //simple-hoc const simpleHoc = WrappedComponent => { console.log('simpleHoc'); return class extends Component { render() { return 52be7657ec74da90f3cdb693591b3ded } } }
和高阶组件是同样的效果。
类的装饰
@testable class MyTestableClass { // ... } function testable(target) { target.isTestable = true; } MyTestableClass.isTestable // true
上面代码中,@testable 就是一个装饰器。它修改了 MyTestableClass这 个类的行为,为它加上了静态属性isTestable。testable 函数的参数 target 是 MyTestableClass 类本身。
如果觉得一个参数不够用,可以在装饰器外面再封装一层函数。
function testable(isTestable) { return function(target) { target.isTestable = isTestable; } } @testable(true) class MyTestableClass {} MyTestableClass.isTestable // true @testable(false) class MyClass {} MyClass.isTestable // false
上面代码中,装饰器 testable 可以接受参数,这就等于可以修改装饰器的行为。
方法的装饰
装饰器不仅可以装饰类,还可以装饰类的属性。
class Person { @readonly name() { return `${this.first} ${this.last}` } }
上面代码中,装饰器 readonly 用来装饰“类”的name方法。
装饰器函数 readonly 一共可以接受三个参数。
function readonly(target, name, descriptor){ // descriptor对象原来的值如下 // { // value: specifiedFunction, // enumerable: false, // configurable: true, // writable: true // }; descriptor.writable = false; return descriptor; } readonly(Person.prototype, 'name', descriptor); // 类似于 Object.defineProperty(Person.prototype, 'name', descriptor);
装饰器第一个参数是 类的原型对象,上例是 Person.prototype,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);
第二个参数是 所要装饰的属性名
第三个参数是 该属性的描述对象
另外,上面代码说明,装饰器(readonly)会修改属性的 描述对象(descriptor),然后被修改的描述对象再用来定义属性。
ES5 中,mixin 为 object 提供功能“混合”能力,由于 JavaScript 的原型继承机制,通过 mixin 一个或多个对象到构造器的 prototype上,能够间接提供为“类”的实例混合功能的能力。
下面是例子:
function mixin(...objs){ return objs.reduce((dest, src) => { for (var key in src) { dest[key] = src[key] } return dest; }); } function createWithPrototype(Cls){ var P = function(){}; P.prototype = Cls.prototype; return new P(); } function Person(name, age, gender){ this.name = name; this.age = age; this.gender = gender; } function Employee(name, age, gender, level, salary){ Person.call(this, name, age, gender); this.level = level; this.salary = salary; } Employee.prototype = createWithPrototype(Person); mixin(Employee.prototype, { getSalary: function(){ return this.salary; } }); function Serializable(Cls, serializer){ mixin(Cls, serializer); this.toString = function(){ return Cls.stringify(this); } } mixin(Employee.prototype, new Serializable(Employee, { parse: function(str){ var data = JSON.parse(str); return new Employee( data.name, data.age, data.gender, data.level, data.salary ); }, stringify: function(employee){ return JSON.stringify({ name: employee.name, age: employee.age, gender: employee.gender, level: employee.level, salary: employee.salary }); } }) );
从一定程度上,mixin 弥补了 JavaScript 单一原型链的缺陷,可以实现类似于多重继承的效果。在上面的例子里,我们让 Employee “继承” Person,同时也“继承” Serializable。有趣的是我们通过 mixin Serializable 让 Employee 拥有了 stringify 和 parse 两个方法,同时我们改写了 Employee 实例的 toString 方法。
我们可以如下使用上面定义的类:
var employee = new Employee("jane",25,"f",1,1000); var employee2 = Employee.parse(employee+""); //通过序列化反序列化复制对象 console.log(employee2, employee2 instanceof Employee, //true employee2 instanceof Person, //true employee == employee2); //false
ES6 中的 mixin 式继承
在 ES6 中,我们可以采用全新的基于类继承的 “mixin” 模式设计更优雅的“语义化”接口,这是因为 ES6 中的 extends 可以继承动态构造的类,这一点和其他的静态声明类的编程语言不同,在说明它的好处之前,我们先看一下 ES6 中如何更好地实现上面 ES5 代码里的 Serializable:
用继承实现 Serializable
class Serializable{ constructor(){ if(typeof this.constructor.stringify !== "function"){ throw new ReferenceError("Please define stringify method to the Class!"); } if(typeof this.constructor.parse !== "function"){ throw new ReferenceError("Please define parse method to the Class!"); } } toString(){ return this.constructor.stringify(this); } } class Person extends Serializable{ constructor(name, age, gender){ super(); Object.assign(this, {name, age, gender}); } } class Employee extends Person{ constructor(name, age, gender, level, salary){ super(name, age, gender); this.level = level; this.salary = salary; } static stringify(employee){ let {name, age, gender, level, salary} = employee; return JSON.stringify({name, age, gender, level, salary}); } static parse(str){ let {name, age, gender, level, salary} = JSON.parse(str); return new Employee(name, age, gender, level, salary); } } let employee = new Employee("jane",25,"f",1,1000); let employee2 = Employee.parse(employee+""); //通过序列化反序列化复制对象 console.log(employee2, employee2 instanceof Employee, //true employee2 instanceof Person, //true employee == employee2); //false 上面的代码,我们用 ES6 的类继承实现了 Serializable,与 ES5 的实现相比,它非常简单,首先我们设计了一个 Serializable 类: class Serializable{ constructor(){ if(typeof this.constructor.stringify !== "function"){ throw new ReferenceError("Please define stringify method to the Class!"); } if(typeof this.constructor.parse !== "function"){ throw new ReferenceError("Please define parse method to the Class!"); } } toString(){ return this.constructor.stringify(this); } }
它检查当前实例的类上是否有定义 stringify 和 parse 静态方法,如果有,使用静态方法重写 toString 方法,如果没有,则在实例化对象的时候抛出一个异常。
这么设计挺好的,但它也有不足之处,首先注意到我们将 stringify 和 parse 定义到 Employee 上,这没有什么问题,但是如果我们实例化 Person,它将报错:
let person = new Person("john", 22, "m"); //Uncaught ReferenceError: Please define stringify method to the Class!
这是因为我们没有在 Person 上定义 parse 和 stringify 方法。因为 Serializable 是一个基类,在只支持单继承的 ES6 中,如果我们不需要 Person 可序列化,只需要 Person 的子类 Employee 可序列化,靠这种继承链是做不到的。
另外,如何用 Serializable 让 JS 原生类的子类(比如 Set、Map)可序列化?
所以,我们需要考虑改变一下我们的设计模式:
用 mixin 实现 Serilizable
const Serializable = Sup => class extends Sup { constructor(...args){ super(...args); if(typeof this.constructor.stringify !== "function"){ throw new ReferenceError("Please define stringify method to the Class!"); } if(typeof this.constructor.parse !== "function"){ throw new ReferenceError("Please define parse method to the Class!"); } } toString(){ return this.constructor.stringify(this); } } class Person { constructor(name, age, gender){ Object.assign(this, {name, age, gender}); } } class Employee extends Serializable(Person){ constructor(name, age, gender, level, salary){ super(name, age, gender); this.level = level; this.salary = salary; } static stringify(employee){ let {name, age, gender, level, salary} = employee; return JSON.stringify({name, age, gender, level, salary}); } static parse(str){ let {name, age, gender, level, salary} = JSON.parse(str); return new Employee(name, age, gender, level, salary); } } let employee = new Employee("jane",25,"f",1,1000); let employee2 = Employee.parse(employee+""); //通过序列化反序列化复制对象 console.log(employee2, employee2 instanceof Employee, //true employee2 instanceof Person, //true employee == employee2); //false
在上面的代码里,我们改变了 Serializable,让它成为一个动态返回类型的函数,然后我们通过 class Employ extends Serializable(Person) 来实现可序列化,在这里我们没有可序列化 Person 本身,而将 Serializable 在语义上变成一种修饰,即 Employee 是一种可序列化的 Person。于是,我们要 new Person 就不会报错了:
let person = new Person("john", 22, "m"); //Person {name: "john", age: 22, gender: "m"}
这么做了之后,我们还可以实现对原生类的继承,例如:
继承原生的 Set 类
const Serializable = Sup => class extends Sup { constructor(...args){ super(...args); if(typeof this.constructor.stringify !== "function"){ throw new ReferenceError("Please define stringify method to the Class!"); } if(typeof this.constructor.parse !== "function"){ throw new ReferenceError("Please define parse method to the Class!"); } } toString(){ return this.constructor.stringify(this); } } class MySet extends Serializable(Set){ static stringify(s){ return JSON.stringify([...s]); } static parse(data){ return new MySet(JSON.parse(data)); } } let s1 = new MySet([1,2,3,4]); let s2 = MySet.parse(s1 + ""); console.log(s2, //Set{1,2,3,4} s1 == s2); //false
通过 MySet 继承 Serializable(Set),我们得到了一个可序列化的 Set 类!同样我们还可以实现可序列化的 Map:
class MyMap extends Serializable(Map){ ... static stringify(map){ ... } static parse(str){ ... } }
如果不用 mixin 模式而使用继承,我们就得分别定义不同的类来对应 Set 和 Map 的继承,而用了 mixin 模式,我们构造出了通用的 Serializable,它可以用来“修饰”任何对象。
我们还可以定义其他的“修饰符”,然后将它们组合使用,比如:
const Serializable = Sup => class extends Sup { constructor(...args){ super(...args); if(typeof this.constructor.stringify !== "function"){ throw new ReferenceError("Please define stringify method to the Class!"); } if(typeof this.constructor.parse !== "function"){ throw new ReferenceError("Please define parse method to the Class!"); } } toString(){ return this.constructor.stringify(this); } } const Immutable = Sup => class extends Sup { constructor(...args){ super(...args); Object.freeze(this); } } class MyArray extends Immutable(Serializable(Array)){ static stringify(arr){ return JSON.stringify({Immutable:arr}); } static parse(data){ return new MyArray(...JSON.parse(data).Immutable); } } let arr1 = new MyArray(1,2,3,4); let arr2 = MyArray.parse(arr1 + ""); console.log(arr1, arr2, arr1+"", //{"Immutable":[1,2,3,4]} arr1 == arr2); arr1.push(5); //throw Error!
上面的例子里,我们通过 Immutable 修饰符定义了一个不可变数组,同时通过 Serializable 修饰符修改了它的序列化存储方式,而这一切,通过定义 class MyArray extends Immutable(Serializable(Array)) 来实现。
以上がReact高次コンポーネントとES6デコレータの応用を詳しく解説(コード付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。