React中 `鎖定`this的N種‘facade pattern’模式

洛夕楓發表於2019-03-03

不管是什麼樣的歷史原因,或者是基於什麼樣的考慮。反正現在我們已經接受了JavaScript中的this的多面性,以及樂此不疲的使用this這種多面性,來編寫靈活的程式碼,比如借用其他物件的方法,改變回撥函式的呼叫者等,但有時候我們還是希望this能夠老實一點,別讓我們花費很大精力去找尋他。

快速找到

說明

由於本文是主要介紹React中鎖定this的N種方法,不會過多的介紹this多面性的原因,相信大家應該都知道詞法作用域動態作用域。並且也知道在es6之前我們依然有很多種方式,去鎖定this的指向(call, apply, bind)。接下來我們也會結合這些方式,在React中來鎖定this;不過在這之前,我們先看下之前我們採取的方式。對於本文標題facade pattern(外觀模式)指的是,這些鎖定this的方式,只是看起來不一樣,有些本質上是一樣的,有些是es5通過一些技巧實現的,有些是es6原生支援的(箭頭函式),這些看起來很不一樣的方式,有些在babel編譯以後本質是一樣的(使用閉包鎖住上下文, 通過高階函式返回新的函式)。

千好萬好ES6好(箭頭函式 () => {}好)

看下在es6之前我們是如何解決this

// 回撥裡面使用this
var Demo = {
    init() {
        this.initEvents();  
    }
    
    validateData() {
        return true;
    }
    
    initEvents() {
        var self = this;
        $(`a.submit`).click(function () {
            self.validateData();
        });
    }    
}
Demo.init();

// 借用其他物件的方法
var name = `影帝`;
var Person = {
    name: `渣渣輝`,
    sayName() {
        console.log(this.name);
    }
};
var Other = {
    name: `張家輝`
};
var sayName = Person.sayName;

sayName(); // `影帝`
sayName.call(Other); // 張家輝`
Other.sayName = sayName;
var otherSayName = Other.sayName;
otherSayName(); // `影帝`
Other.sayName.call(Person); `渣渣輝`複製程式碼

這樣看起來好奇怪呀,但是沒辦法,我們早已經習慣,自從ES2015稱為標準以來,我們已經很少看到這種程式碼了。好了言歸正題,我們開始看看Reactthis的問題。

React中this的問題

既然js中有這些問題,當然react也不能列外,使用react的我們都知道,為了保持元件的高複用,元件可以分為容器元件UI元件容器元件元件負責業務處理,UI元件負責頁面渲染,一般情況下UI元件都儘量要是純的,沒有自己的狀態,也不處理業務,但是有時候需要觸發一些事件,這樣就需要執行從父元件等傳過來的函式,同時這些函式裡面一般還會出現this,我們希望this的指向是上層元件的引用,而這個時候函式的執行者卻不是上層元件,於是this開始變臉,變得我們不認識。但是我們要避免這種情況,就需要鎖定this鎖定this的方式有很多,我們一一分析,這其中各有優劣,也有react推薦的最佳實踐。至於如何選擇,看業務場景,以及團隊編碼風格,建議最好還是遵守最佳實踐;

React中‘鎖定’this的N種方法


1.函式包裝模式

/**
 * 函式包裝模式 function wrapper pattern
 */
class Component extends React.Component {
    doSomething() {
    
    childDoSomething() {}
    
    render() {
        return (
            <div onClick={() => this.doSomething();}>
                <ChildComponent doSomething={() => this.childDoSomething();} />
            </div>
        );
    }
}
複製程式碼

建議: 不推薦 也不禁止。
優缺點:

  • 缺點:沒有明顯的缺點,只是需要多包裹一層
  • 優點:簡單,易於理解,對新手比較友好

實現原理:
這裡是通過es6箭頭函式來實現this的鎖定;

當然對應的有es5版本,其實就是我們之前熟悉的那種方式,且看程式碼

   /**
    * function wrapper pattern es5
    */
   class Component extends React.Component {
       doSomething() {
       
       childDoSomething() {}
       
       render() {
           const self = this;
           return (
               <div onClick={function () { self.doSomething();}>
                   <ChildComponent doSomething={function () { self.childDoSomething();} />
               </div>
           );
       }
   }
複製程式碼

建議: 不推薦, 最好不要這樣寫。
優缺點:

  • 缺點:需要對this的指向進行儲存,導致程式碼沒有箭頭函式來的簡潔, 優雅(其實也就是箭頭函式的優點)
  • 優點:對熟悉es5老式寫法的比較友好

實現原理:
使用變數先保持對this的引用,使用的時候是這個變數,也就是此函式外部的this;

2.渲染繫結模式

/**
 * 渲染繫結模式 render bind pattern
 * 或者叫
 * 懶繫結模式 lazy bind pattern
 */
class Component extends React.Component {
    doSomething() {}
    
    childDoSomething() {}
    
    render() {
        return (
            <div onClick={this.doSomething.bind(this)}>
                <ChildComponent doSomething={this.childDoSomething.bind(this);} />
            </div>
        );
    }
}
複製程式碼

建議: 禁止採取這種模式
優缺點:

  • 缺點:有效能隱患,每次render都會重新繫結
  • 優點:好像也只有看起來稍微好看,不用像在constructor裡面一樣重新輔助

實現原理:
就是使用bing鎖定,關於bind的使用以及原理可以參考mdn或者網上其他文章或者教程,當然你也可以實現自己的bind;

3.覆寫繫結模式

/**
 * 覆寫繫結模式rewrite bind pattern
 * 或者叫 
 * 預繫結模式 prepare bind pattern
 */
class Component extends React.Component {
    
    constructor() {
        this.doSomething = this.doSomething.bind(this);
        this.childDoSomething = this.childDoSomething.bind(this);
    }
    
    doSomething() {}
    
    childDoSomething() {}
    
    render() {
        return (
            <div onClick={this.doSomething}>
                <ChildComponent doSomething={this.childDoSomething} />
            </div>
        );
    }
}
複製程式碼

建議: 建議採用這種方式,也是react最佳實踐推薦的寫法。
優缺點:

  • 缺點:需要在建構函式裡面重寫需要繫結this的方法,如果這類方法比較多了,就不是那麼的優雅了,不過尚可以接受。
  • 優點:react最佳實踐推薦,也是最常見的方式,效能較好,只會繫結一次

實現原理:
和渲染時繫結模式實現原理一樣,只是在這種方式下是提前繫結好。

對比:
這種模式和上一種在render時繫結實現原理是一樣的,這種方式只會繫結一次,效能是好於在render裡面的繫結;對比下來在寫法上面也有些區別,一個是在constructor提前繫結,一個是在準備要用的時候懶繫結。

4.屬性賦值模式

/**
 * 屬性賦值模式 attribute assignment pattern
 */
class Component extends React.Component {
    
    doSomething = () => {}
    
    childDoSomething = () => {}
    
    render() {
        return (
            <div onClick={this.doSomething}>
                <ChildComponent doSomething={this.childDoSomething} />
            </div>
        );
    }
}
複製程式碼

建議: 可以採用,react最佳實踐也有推薦的這種寫法。
優缺點:

  • 缺點:不被標準所支援(babel以後是沒有問題的),寫法怪怪的, 要是有很多這種寫法, 不夠優雅。
  • 優點:寫法很簡單(雖然很怪),不用顯式的bind

實現原理:
借用箭頭函式在定義的時候就繫結好了this

5.高階函式渲染繫結模式

/**
 * 高階函式渲染繫結模式 higher-order function render bind pattern
 * 或者叫 高階函式懶繫結模式 higher-order function lazy bind pattern
 */
class Component extends React.Component {
    
    doSomething(data) {
        return () => {
            // 使用this, data
        }
    }
    
    childDoSomething(data) {
        return () => {
            // 使用this, data
        }
    }
    
    render() {
        return (
            <div onClick={this.doSomething()}>
                <ChildComponent doSomething={this.childDoSomething()} />
            </div>
        );
    }
}
複製程式碼

建議: 可以採用,嘗試函式式寫法。
優缺點:

  • 缺點:不熟悉高階函式(或者函式式),接受起來有難度,需要呼叫一次, 每次都產生新的函式。
  • 優點:優雅,高階函式, 可以提前儲存一些變數。

實現原理:
利用高階函式返回箭頭函式, 實現this的鎖定。

當然這種方法有對應的es5版本

/**
 * higher-order function es5 pattern
 */
class Component extends React.Component {
    
    doSomething(data) {
        cosnt self = this;
        return function() {
            // self, data
        }
    }
    
    childDoSomething(data) {
        cosnt self = this;
        return function() {
            // self, data
        }
    }
    
    render() {
        return (
            <div onClick={this.doSomething()}>
                <ChildComponent doSomething={this.childDoSomething()} />
            </div>
        );
    }
}
複製程式碼

注意:
這種模式被我稱為懶模式,和在render裡面使用bind的方式很像,在準備要使用的時候才繫結,每次都產生一個新的函式,可能會帶來效能問題。當然又這種懶模式,我們也有提前繫結模式。

6.高階函式覆寫繫結模式

/**
 * 高階函式覆寫繫結模式 higher-order function rewrite bind pattern 
 * 或者叫 
 * 高階函式預繫結模式 higher-order function prepare bind pattern
 */
class Component extends React.Component {
    
    constructor() {
        this.doSomething = this.doSomething();
        this.childDoSomething = this.childDoSomething();
    }
    
    doSomething(data) {
        return () => {
            // 使用this, data
        }
    }
    
    childDoSomething(data) {
        return () => {
            // 使用this, data
        }
    }
    
    render() {
        return (
            <div onClick={this.doSomething}>
                <ChildComponent doSomething={this.childDoSomething} />
            </div>
        );
    }
}
複製程式碼

建議: 可以採用,嘗試函式式寫法。
優缺點:

  • 缺點:不熟悉高階函式(或者函式式),接受起來有難度,需要呼叫一次。
  • 優點:沒有顯式繫結,在某些場景下可以提前儲存一些變數, 對比上一種模式效能較好。

實現原理:
和上一個高階函式渲染繫結模式一樣利用高階函式返回箭頭函式, 實現this的鎖定。不同的是這個模式是在建構函式裡面提前呼叫。繫結後函式只會產生一次。

當然這種方法有對應的es5版本, 和上個模式的es5版本很像,也是通過變數快取this, 不同就是在constructor裡面呼叫一次函式,而不是在render裡面。


我是分割線,到了最後一種方式了


7.屬性getter渲染繫結模式

/**
 * 屬性getter渲染繫結模式 attribute getter render bind pattern
 * 或者叫 
 * 屬性getter懶繫結模式 attribute getter lazy bind pattern
 */
class Component extends React.Component {
    
    get doSomething() {
        // 這裡也可以使用this, 做一些屬性的計算, 比如 this.xxx + this.yyyy
        return () => {
            // 使用this
        }
    }
    
    get childDoSomething() {
        // 這裡也可以使用this, 做一些屬性的計算, 比如 this.xxx + this.yyyy
        return () => {
            // 使用this
        }
    }
    
    render() {
        return (
            <div onClick={this.doSomething}>
                <ChildComponent doSomething={this.childDoSomething} />
            </div>
        );
    }
}
複製程式碼

建議: 可以採用,嘗試新的寫法。
優缺點:

  • 缺點:接受起來需要成本, 每次產生新的函式。
  • 優點:被標準所支援,沒有顯式繫結,沒有顯式呼叫,比較簡潔優雅,可以提前做一些屬性的聚合或者計算。

實現原理:
借用屬性的getter, 返回一個箭頭函式繫結this;

說明:
這種模式和高階函式很像,都是返回一個新的函式,這種模式在特定情況下很強大,簡潔的同時,可以對當前物件的一些屬性做一些計算(是不是很像Vue的計算屬性), 這種模式下每次getter後返回的都是一個新的函式,可能會有效能問題,但是如果對其他屬性進行了聚合計算,或者說是依賴其他屬性的最新值,就需要在render裡面getter,以保證依賴的屬性都是乾淨的值(最新的值);

當然大家知道里面返回的是箭頭函式,肯定也有es5版本,其實和其他模式的es5版本都很像,在這裡就不寫了。既然這種模式下有可能產生效能問題,對比其他模式,我們可定也有預繫結模式。請往下看

8.屬性getter賦值繫結模式

/**
 * 屬性getter賦值繫結模式attribute getter assignment bind pattern
 * 或者叫 
 * 屬性getter預繫結模式 attribute getter prepare bind pattern
 */
class Component extends React.Component {
    constructor() {
        this.doSomethingBind = this.doSomething;
        this.childDoSomethingBind = this.childDoSomething
    }
    
    get doSomething() {
        // 這裡也可以使用this, 做一些屬性的計算, 比如 this.xxx + this.yyyy
        return () => {
            // 使用this
        }
    }
    
    get childDoSomething() {
        // 這裡也可以使用this, 做一些屬性的計算, 比如 this.xxx + this.yyyy
        return () => {
            // 使用this
        }
    }
    
    render() {
        return (
            <div onClick={this.doSomethingBind}>
                <ChildComponent doSomething={this.childDoSomethingBind} />
            </div>
        );
    }
}
複製程式碼

建議: 可以採用,嘗試新的寫法。
優缺點:

  • 缺點:接受起來需要成本,賦值的函式需要另外一個名字 。
  • 優點:被標準所支援,沒有顯式繫結,只產生一次函式,比較簡潔優雅,可以提前做一些屬性的聚合或者計算。

實現原理:
借用屬性的getter, 返回一個箭頭函式繫結this;賦值給物件的另外一個屬性,呼叫的是另外一個方法。

說明:
這種模式和上一種模式區別在於,提前繫結,只會產生一次函式。但是要注意不是重寫函式,而是賦值給另外一個不同的方法名,可能大家覺得這種換名字不夠好,但是換個角度考慮一下,系物件多了一個方法,同時又持有之前的getter,這樣可以更加的靈活,可以選擇性的使用這兩種函式。

同其他一些返回箭頭的模式一樣,這種模式依然有es5版本,寫法同上,不在贅述。

總結

上面列舉的這些模式,不一定是全部寫法,不過足以應對工作中的大多數場景,同時有些模式還可以讓我們去接觸另外的實現方式。列舉了這麼多種,每一種都有優劣,工作中可以選擇性的去使用,看場景和團隊風格。

展望

既然JavaScriptthis的問題一直困擾著我們,那麼有沒有一種方式可以不使用this,就可以實現我們想要的所有功能,答案是肯定的。React 16.7.0-alpha版本加入了特別神奇的hooks(好像Vue 3.0裡面也已經加入了相似的特性),可以讓我們徹底擺脫this的困擾(當然this依然是js裡面一個神奇的存在),同時讓我們的程式碼更加函式式,更大程度的複用處理邏輯,當然這個特性還在等待成為事實標準,我們希望這一天很快到來,不過我們仍然可以現在就是使用它。

相關連結

1.可以讓我們不用關心this繫結問題(react hooks 官方文件)
2.關於hooks 的一些問題(react hooks 官方文件)

相關文章