react源码片段赏析(1)

399 阅读5分钟

二话不说,今天我们就来赏析react@ 15.0.0源码中面向对象范式中父类(称之为模板类或者自定义类型都行)的写法。

它的具体写法是这样的:

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

在react@ 15.0.0源码中,这种写法的应用见诸于react几种component的模板类的定义代码。

在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】来判断某个对象是否是某个构造函数的实例的时候出错。不信?咱们来看一看。当我们采用主流的父类写法的时候,一切都正如我们所愿:

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

然而,当我们采用【覆盖原型对象】的写法,一切就不那么美好了:

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属性被覆盖了,所以导致了判断错误。

那么既然主流的模板类的写法没有什么毛病,那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.

万能的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()

全文完,谢谢阅读。