摘要:裝飾者模式參與者裝飾者和被裝飾者共同的父類,是一個接口或者抽象類,用來定義基本行為定義具體對象,即被裝飾者抽象裝飾者,繼承自,從外類來擴展。三裝飾器高階組件可以看做是裝飾器模式在的實現。
一 裝飾者模式
優先使用對象組合而不是類繼承。 --《設計模式》
1.什么是裝飾者模式
定義:動態的給對象添加一些額外的屬性或行為。相比于使用繼承,裝飾者模式更加靈活。
2.裝飾者模式參與者
Component:裝飾者和被裝飾者共同的父類,是一個接口或者抽象類,用來定義基本行為
ConcreteComponent:定義具體對象,即被裝飾者
Decorator:抽象裝飾者,繼承自Component,從外類來擴展ConcreteComponent。對于ConcreteComponent來說,不需要知道Decorator的存在,Decorator是一個接口或抽象類
ConcreteDecorator:具體裝飾者,用于擴展ConcreteComponent
注:裝飾者和被裝飾者對象有相同的超類型,因為裝飾者和被裝飾者必須是一樣的類型,這里利用繼承是為了達到類型匹配,而不是利用繼承獲得行為。
利用繼承設計子類,只能在編譯時靜態決定,并且所有子類都會繼承相同的行為;利用組合的做法擴展對象,就可以在運行時動態的進行擴展。裝飾者模式遵循開放-關閉原則:類應該對擴展開放,對修改關閉。利用裝飾者,我們可以實現新的裝飾者增加新的行為而不用修改現有代碼,而如果單純依賴繼承,每當需要新行為時,還得修改現有的代碼。
3.javascript 如何使用裝飾者模式
javascript 動態語言的特性使得使用裝飾器模式十分的簡單,文章主要內容會介紹兩種使用裝飾者模式的實際例子。
我們都知道高階函數是什么, 高階組件其實是差不多的用法,只不過傳入的參數變成了react組件,并返回一個新的組件.
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 (Usual) } } export default simpleHoc(Usual); import React, { Component } from "react"; const simpleHoc = WrappedComponent => { console.log("simpleHoc"); return class extends Component { render() { return} } } export default simpleHoc;
組件Usual通過simpleHoc的包裝,打了一個log... 那么形如simpleHoc就是一個高階組件了,通過接收一個組件class Usual,并返回一個組件class。 其實我們可以看到,在這個函數里,我們可以做很多操作。 而且return的組件同樣有自己的生命周期,function,另外,我們看到也可以把props傳給WrappedComponent(被包裝的組件)。
實現高階組件的方法有兩種
? 屬性代理(props proxy)。高階組件通過被包裹的 React 組件來操作 props。
? 反向繼承(inheritance inversion)。高階組件繼承于被包裹的 React 組件。
屬性代理
引入里我們寫的最簡單的形式,就是屬性代理(Props Proxy)的形式。通過hoc包裝wrappedComponent,也就是例子中的Usual,本來傳給Usual的props,都在hoc中接受到了,也就是props proxy。 由此我們可以做一些操作
1.操作props
最直觀的就是接受到props,我們可以做任何讀取,編輯,刪除的很多自定義操作。包括hoc中定義的自定義事件,都可以通過props再傳下去。
import React, { Component } from "react"; const propsProxyHoc = WrappedComponent => class extends Component { handleClick() { console.log("click"); } render() { return (); } }; export default propsProxyHoc;
然后我們的Usual組件render的時候, console.log(this.props) 會得到handleClick.
2.refs獲取組件實例
當我們包裝Usual的時候,想獲取到它的實例怎么辦,可以通過引用(ref),在Usual組件掛載的時候,會執行ref的回調函數,在hoc中取到組件的實例。
import React, { Component } from "react"; const refHoc = WrappedComponent => class extends Component { componentDidMount() { console.log(this.instanceComponent, "instanceComponent"); } render() { return (this.instanceComponent = instanceComponent} />); } }; export default refHoc;
3.抽離state
這里不是通過ref獲取state, 而是通過 { props, 回調函數 } 傳遞給wrappedComponent組件,通過回調函數獲取state。這里用的比較多的就是react處理表單的時候。通常react在處理表單的時候,一般使用的是受控組件(文檔),即把input都做成受控的,改變value的時候,用onChange事件同步到state中。當然這種操作通過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; } }
在這個例子中,我們把 input 組件中對 name prop 的 onChange 方法提取到高階組件中,這樣就有效地抽象了同樣的 state 操作。
反向繼承
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 (); } }HOC Debugger Component
Props
{JSON.stringify(this.props, null, 2)}State
{JSON.stringify(this.state, null, 2)}{super.render()}
在這個例子中,顯示了 WrappedComponent 的 props 和 state,以方便我們在程序中去調試它們。
三 ES6 裝飾器高階組件可以看做是裝飾器模式(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 (Usual) } } //simple-hoc const simpleHoc = WrappedComponent => { console.log("simpleHoc"); return class extends Component { render() { return} } }
和高階組件是同樣的效果。
類的裝飾
@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 中優雅的 mixin 式繼承
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108764.html
摘要:自行車的基類如下其它方法那么我們可以先創建一個裝飾者模式基類這個基類其實沒有做什么事情,它只是接受一個實例,實現其對應的方法,并且將調用其方法返回而已。 showImg(https://segmentfault.com/img/bVbs3pt?w=650&h=651); 什么是裝飾者模式 裝飾者模式是一種為函數或類增添特性的技術,它可以讓我們在不修改原來對象的基礎上,為其增添新的能力和...
摘要:與繼承相比,裝飾者是一種更輕便靈活的做法。它只是一種模式,這種模式是由自身的組合性質必然產生的。對比原生組件增強的項可操作所有傳入的可操作組件的生命周期可操作組件的方法獲取反向繼承返回一個組件,繼承原組件,在中調用原組件的。 導讀 前端發展速度非常之快,頁面和組件變得越來越復雜,如何更好的實現狀態邏輯復用一直都是應用程序中重要的一部分,這直接關系著應用程序的質量以及維護的難易程度。 本...
摘要:所謂高階組件即使是接受一個組件作為參數返回一個新組件的函數用于提高組件的自身能力提高組件復用性普通高階組件函數將父級屬性向下傳遞并追加新屬性為添加樣式和木偶組件傳入一個組件返回一個函數式組件高階組件木偶組件我是本體是同樣還可以為增加生命周期 所謂高階組件即使是接受一個組件作為參數, 返回一個新組件的函數, 用于提高組件的自身能力, 提高組件復用性 1.普通高階組件 HOC函數將父級屬性...
閱讀 3375·2021-11-22 13:53
閱讀 3410·2021-10-11 11:11
閱讀 931·2019-08-30 14:12
閱讀 1221·2019-08-29 17:16
閱讀 639·2019-08-29 16:45
閱讀 3348·2019-08-29 12:56
閱讀 669·2019-08-28 17:55
閱讀 2065·2019-08-26 13:24