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()
复制代码

全文完,谢谢阅读。

相关文章
相关标签/搜索