react源碼片斷賞析(1)

二話不說,今天咱們就來賞析react@ 15.0.0源碼中面向對象範式中父類(稱之爲模板類或者自定義類型都行)的寫法。javascript

它的具體寫法是這樣的:java

function Constructor(){}
Object.assign(
    Constructor.prototype,
    literal-object1,
    literal-object2
)
module.exports = Constructor;
複製代碼

在react@ 15.0.0源碼中,這種寫法的應用見諸於react幾種component的模板類的定義代碼。react

在src/renderers/dom/shared/ReactDOMTextComponent.js 中:

var ReactDOMTextComponent = function(text) {
  // TODO: This is really a ReactText (ReactNode), not a ReactElement
  this._currentElement = text;
  this._stringText = '' + text;
  // other properties
  // .........
};

Object.assign(ReactDOMTextComponent.prototype, {
  mountComponent: function(){ //.......},
  receiveComponent: function(){ //.......}
  // other methods
  // .......
});

module.exports = ReactDOMTextComponent;
複製代碼

在src/renderers/dom/shared/ReactDOMComponent.js中:

function ReactDOMComponent(element) {
  this._currentElement = element;
  this._tag = tag.toLowerCase();
 // other properties
 // .........
}

ReactDOMComponent.Mixin = {
    mountComponent: function(){ //.......},
    _createOpenTagMarkupAndPutListeners:function(){ //.......},
    // other methods
    // .........
}

Object.assign(
  ReactDOMComponent.prototype,
  ReactDOMComponent.Mixin,
  ReactMultiChild.Mixin
);

module.exports = ReactDOMComponent;
複製代碼

在src/renderers/shared/reconciler/instantiateReactComponent.js中:

var ReactCompositeComponentWrapper = function(element) {
  this.construct(element);
};
Object.assign(
  ReactCompositeComponentWrapper.prototype,
  ReactCompositeComponent.Mixin,
  {
    _instantiateReactComponent: instantiateReactComponent,
  }
);
// other code.......

instance = new ReactCompositeComponentWrapper(element);

複製代碼

在這裏,咱們不妨比較一下這種在js中實現模板類的寫法與主流寫法的異同。 首先,咱們先回憶一下,在js犀牛書中,它爲咱們推薦的,也是當前業界主流的寫法是怎樣呢?對,是這樣的:編程

function SomeConstructor(){
    this.property1='xxxx';
    this.property2="xxxx";
    // ...... other properties
}
SomeConstructor.prototype.method1=function(){}
SomeConstructor.prototype.method1=function(){}
// ......other methods

複製代碼

對,這是家常便飯的模式了。書中還告訴咱們,模板類編寫的最佳實踐是不要採用【覆蓋原型對象】的寫法。爲何呢?那是由於一旦這麼作了,原型對象的constructor屬性就會被覆蓋掉,這會致使其餘人在使用【someInstance.constructor === SomeConstructor】來判斷某個對象是不是某個構造函數的實例的時候出錯。不信?我們來看一看。當咱們採用主流的父類寫法的時候,一切都正如咱們所願:bash

function Foo(name){
    this.name = name || 'you got no name';
}

Foo.prototype.sayHello = function(){
    console.log(`hello from ${this.name}`)
}

const sam = new Foo('sam');
console.log(sam instanceof Foo) // true
console.log(sam.constructor === Foo) // true
複製代碼

然而,當咱們採用【覆蓋原型對象】的寫法,一切就不那麼美好了:app

function Foo(name){
    this.name = name || 'you got no name';
}

Foo.prototype = {
    sayHello:function(){
        console.log(`hello from ${this.name}`)
    }
}

const sam = new Foo('sam');
console.log(sam instanceof Foo) // true
console.log(sam.constructor === Foo) // false,這顯然不是咱們想要的結果
複製代碼

正如上面所說的Foo.prototype對象上的constructor屬性被覆蓋了,因此致使了判斷錯誤。dom

那麼既然主流的模板類的寫法沒有什麼毛病,那react的源碼中,爲何不採用這種寫法而是採用Object.assign這種寫法呢?在回答這個問題以前,咱們不妨探索一下Object.assign這種API有什麼特性。函數

The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.性能

Properties in the target object will be overwritten by properties in the sources if they have the same key. Later sources' properties will similarly overwrite earlier ones.ui

萬能的MDN如是說。上面所說的target object指的是咱們最終獲得的對象,source objects是指咱們將某些對象合併到這個target object去的【那些對象】。 從上面,咱們能夠獲得兩點信息。

  1. 使用Object.assign時,會將source objects的自有屬性(也稱爲實例屬性),而且是可枚舉屬性拷貝到target object中。這個過程是不考慮source objects的原型屬性的。
  2. 使用Object.assign來合併對象,原則是「有,則論優先級;否,則添加」。什麼意思呢?意思就是當target object沒有這個屬性的時候,就往它身上添加;而當多個source objects都具備相同的一個屬性時,那麼越是後面的source objects的優先級越高。

咱們回顧一下react源碼中的寫法,它都是將一個字面量對象(命名爲mixin)合併到構造函數的原型對象上的。雖然字面量對象可以訪問「constructor」屬性,可是這個屬性是原型屬性。因此,在合併對象的時候,構造函數的原型對象上的constructor屬性並不會被覆蓋掉。不信?我們來驗證一下:

const sam = { nickname: 'littlepoolshark'};

console.log(sam.constructor) // ƒ Object() { [native code] };字面量對象能訪問「constructor」屬性
console.log(abc.constructor === Object) // true
for(let key in sam){
    console.log(key); // 只打印了一個「nickname」,並無「constructor」
}

console.log(sam.hasOwnProperty('constructor'))

複製代碼

從上面的代碼能夠看出,字面量對象上能訪問的「constructor」屬性既不是實例屬性,也不是可枚舉屬性。可是它能夠訪問,那它只能是原型鏈上的屬性了,也就是原型屬性。因此,react的這種寫法符合【不要覆蓋原型對象的constructor屬性】的這個最佳實踐的要求。

也許你會問,那主流的寫法也能夠達到這種效果啊?爲何react源碼不這麼寫呢?帶着這個疑問,咱們繼續往下探索。不知道,你有沒有去看看源碼,正如上面所羅列的幾個模板類那樣,在react源碼中模板類的方法通常都是不少的。若是採用主流的寫法,一個方法一個方法地往原型對象上添加,那麼就顯得重複和笨拙了。就像下面那樣:

function ReactDOMComponent(element) {
  this._currentElement = element;
  this._tag = tag.toLowerCase();
 // other properties
 // .........
}

ReactDOMComponent.prototype.method1= function(){}
ReactDOMComponent.prototype.method2= function(){}
ReactDOMComponent.prototype.method3= function(){}
ReactDOMComponent.prototype.method4= function(){}
ReactDOMComponent.prototype.method5= function(){}
ReactDOMComponent.prototype.method6= function(){}
.............

複製代碼

這麼寫法還有一個問題,那就是鑑於javascript這門語言的動態性再加上原型鏈的冗長,屬性查找是相對耗時的。在方法數量很大的狀況下,那麼這種重複的屬性查找所帶來的性能消耗必然是很大的。我想這就是react源碼不採用這種寫法的緣由。還有一點是,採用把方法都放在字面量對象裏面,而後結合Object.assign來擴展構造函數的原型對象,能帶來兩點好處:

  • 起到批量添加的效果。
  • 能獲得相似於切面編程所帶來的代碼解耦和複用的效果。

綜上所述,咱們能夠把react源碼中採用這種寫法的動機推測以下:

  1. 繼續遵循【不要覆蓋原型對象的constructor屬性】的最佳實踐。
  2. 可以往原型對象上【批量】添加方法。
  3. 只訪問一次原型對象,保證【屬性查找】過程當中較低的性能消耗。
  4. 用字面量對象來容納方法,可以獲得相似於切面編程所帶來的【代碼解耦和複用】的好處。

好,到這裏咱們已經賞析完畢了。

附加題

最後咱們來一下發散思惟。若是,咱們在合併的過程當中,也想把source object的原型屬性也一併合併過來呢,應該怎麼實現呢?下面是個人答案:

// 原型對象屬性 + 對象自身屬性 = 全部屬性
// 注意點:targetProto要成爲Object.assign()的第二個參數
function clone(targetObj){
    const targetProto = Object.getPrototypeOf(targetObj);
    return Object.assign(targetObj, targetProto);
}

// 加強原生的Object.assign()方法
function assign(target,...sources){
    const cloneSources = sources.map(source =>  clone(source));
    return Object.assign(target,...cloneSources);
}

function SomeConstructor(){
    
}

assign(
    SomeConstructor.prototype,
    mixinObj1,
    mixinObj2,
    ......
    )
    
// 實例化
const inst = new SomeConstructor()
複製代碼

全文完,謝謝閱讀。

相關文章
相關標籤/搜索