资讯专栏INFORMATION COLUMN

从Vue数组响应化所引发的思考

hikui / 2363人阅读

摘要:因为无法通过借用构造函数的方式创建响应式属性虽然属性可以被创建,但不具备响应式功能,因此在我们是没法继承数组的。上面整个的文章都是基于监听数组响应的一个点想到的。

前言

  首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励。

  从上一篇文章响应式数据与数据依赖基本原理开始,我就萌发了想要研究Vue源码的想法。最近看了youngwind的一篇文章如何监听一个数组的变化发现Vue早期实现监听数组的方式和我的实现稍有区别。并且在两年前作者对其中的一些代码的理解有误,在阅读完评论中@Ma63d的评论之后,感觉收益匪浅。

Vue实现数据监听的方式

  在我们的上一篇文章中,我们想尝试监听数组变化,采用的是下面的思路:

function observifyArray(array){
  //需要变异的函数名列表
  var methods = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
  var arrayProto = Object.create(Array.prototype);
  _.each(methods, function(method){
    arrayProto[method] = function(...args){
      // 劫持修改数据
      var ret = Array.prototype[method].apply(this, args);
      //可以在修改数据时触发其他的操作
      console.log("newValue: ", this);
      return ret;
    }
  });
  Object.setPrototypeOf(array, arrayProto);
}

  我们是通过为数组实例设置原型prototype来实现,新的prototype重写了原生数组原型的部分方法。因此在调用上面的几个变异方法的时候我们会得到相应的通知。但其实setPrototypeOf方法是ECMAScript 6的方法,肯定不是Vue内部可选的实现方案。我们可以大致看看Vue的实现思路

function observifyArray(array){
    var aryMethods = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
    var arrayAugmentations = Object.create(Array.prototype);
    
    aryMethods.forEach((method)=> {
    
        // 这里是原生Array的原型方法
        let original = Array.prototype[method];
       // 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
       // 注意:是属性而非原型属性
        arrayAugmentations[method] = function () {
            console.log("我被改变啦!");
            // 调用对应的原生方法并返回结果
            return original.apply(this, arguments);
        };
    });
    array.__proto__ = arrayAugmentations;
}

  __proto__是我们大家的非常熟悉的一个属性,其指向的是实例对象对应的原型对象。在ES5中,各个实例中存在一个内部属性[[Prototype]]指向实例对象对应的原型对象,但是内部属性是没法访问的。浏览器各家厂商都支持非标准属性__proto__。其实Vue的实现思路与我们的非常相似。唯一不同的是Vue使用了的非标准属性__proto__

  其实阅读过《JavaScript高级程序设计》的同学应该还记得原型式继承。其重要思路就是借助原型可以基于已有的对象创建对象。比如说:

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

  其实我们上面Vue的思路也是这样的,我们借助原型创建的基于arrayAugmentations的新实例,使得实例能够访问到我们自定义的变异方法。

  上面一篇文章的作者youngwind写文章的时候就提出了,为什么不去采用更为常见的组合式继承去实现,比如:

function FakeArray() {
    Array.apply(this,arguments);
}

FakeArray.prototype = [];
FakeArray.prototype.constructor = FakeArray;

FakeArray.prototype.push = function () {
    console.log("我被改变啦");
    return Array.prototype.push.apply(this,arguments);
};

let list = ["a","b","c"];

let fakeList = new FakeArray(list);

  结果发现fakeList并不是一个数组而是一个对象,作者当时这这样认为的:

构造函数默认返回的本来就是this对象,这是一个对象,而非数组。Array.apply(this,arguments);这个语句返回的才是数组

我们能不能将Array.apply(this,arguments);直接return出来呢?

如果我们return这个返回的数组,这个数组是由原生的Array构造出来的,所以它的push等方法依然是原生数组的方法,无法到达重写的目的。

首先我们知道采用new操作符调用构造函数会依次经历以下四个步骤:

创建新对象

将构造函数的作用域给对象(因此构造函数中的this指向这个新对象)

执行构造函数的代码

返回新对象(如果没有显式返回的情况下)

  在没有显式返回的时候,返回的是新对象,因此fakeList是对象而不是数组。但是为什么不能强制返回Array.apply(this,arguments)。其实下面有人说作者这句话有问题

这个数组是由原生的Array构造出来的,所以它的push等方法依然是原生数组的方法,无法到达重写的目的。

  其实上面这句话本身确实没有错误,当我们给构造函数显式返回的时候,我们得到的fakeList就是原生的数组。因此调用push方法是没法观测到的。但是我们不能返回的Array.apply(this,arguments)更深层的原因在于我们这边调用Array.apply(this,arguments)的目的是为了借用原生的Array的构造函数将Array属性赋值到当前对象上。

举一个例子:

function Father(){
 this.name = "Father";
}

Father.prototype.sayName = function(){
 console.log("name: ", this.name);
}

function Son(){
 Father.apply(this);
 this.age = 100;
}

Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
 console.log("age: ", this.age);
}


var instance = new Son();
instance.sayName(); //name:  Father
instance.sayAge(); //age:  100

  子类Son为了继承父类Father的属性和方法两次调用Father的构造函数,Father.apply(this)就是为了创建父类的属性,而Son.prototype = new Father();目的就是为了通过原型链继承父类的方法。因此上面所说的才是为什么不能将Array.apply(this,arguments)强制返回的原因,它的目的就是借用原生的Array构造函数创建对应的属性。

  但是问题来了,为什么无法借用原生的Array构造函数创建对象呢?实际上不仅仅是Array,StringNumberRegexpObject等等JavaScript的内置类都不能通过借用构造函数的方式创建带有功能的属性(例如: length)。JavaScript数组中有一个特殊的响应式属性length,一方面如果数组数值类型下标的数据发生变化的时候会在length上体现,另一方面,修改length也会影响到数组的数值数据。因为无法通过借用构造函数的方式创建响应式length属性(虽然属性可以被创建,但不具备响应式功能),因此在E55我们是没法继承数组的。比如:

function MyArray(){
    Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
    constructor: {
        value: MyArray,
        writable: true,
        configurable: true,
        enumerable: true
    }
});

var colors = new MyArray();
colors[0] = "red"; 
console.log(colors.length); // 0

colors.length = 0;
console.log(colors[0]); //"red"

  好在我们迎来ES6的曙光,通过类class的extends,我们就可以实现继承原生的数组,例如:

class MyArray extends Array {
}

var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0

colors.length = 0;
cosole.log(colors[0]); // undefined

  为什么ES6的extends可以做到ES5所不能实现的数组继承呢?这是由于二者的继承原理不同导致的。ES5的继承方式中,先是生成派生类型的this(例如:MyArray),然后调用基类的构造函数(例如:Array.apply(this)),这也就是说this首先指向的是派生类的实例,然后指向的是基类的实例。由于原生对象(例如: Array)通过借用的方式并不能给this赋值length类似的具有功能的属性,因此我们没法实现想要的结果。

  但是ES6的extends的继承方式却是与之相反的,首先是由基类(Array)创建this的值,然后再由派生类的构造函数修改这个值,因此在上面的例子中,一开始就可以通过this创建基类的所有內建功能并接受与之相关的功能(如length),然后在此this的基础上用派生类进行扩展,因此就可以达到我们的继承原生数组的目的。

  不仅仅如此。ES6在扩展类似上面的原生对象时还提供了一个非常方便的属性: Symbol.species

Symbol.species

  Symbol.species的主要作用就是可以使得原本返回基类实例的继承方法返回派生类的实例,举个例子吧,比如Array.prototype.slice返回的就是数组的实例,但是当MyArray继承Array时,我们也希望当使用MyArray的实例调用slice时也能返回MyArray的实例。那我们该如何使用呢,其实Symbol.species是一个静态访问器属性,只要在定义派生类时定义,就可以实现我们的目的。比如:

class MyArray extends Array {
  static get [Symbol.species](){
    return this;
  }
}

var myArray = new MyArray(); // MyArray[]
myArray.slice(); // MyArray []

  我们可以发现调用数组子类的实例myArrayslice方法时也会返回的是MyArray类型的实例。如果你喜欢尝试的话,你会发现即使去掉了静态访问器属性get [Symbol.species]myArray.slice()也会仍然返回MyArray的实例,这是因为即使你不显式定义,默认的Symbol.species属性也会返回this。当然你也将this改变为其他值来改变对应方法的返回的实例类型。例如我希望实例myArrayslice方法返回的是原生数组类型Array,就可以采用如下的定义:

class MyArray extends Array {
  static get [Symbol.species](){
    return Array;
  }
}

var myArray = new MyArray(); // []
myArray.slice(); // []

  当然了,如果在上面的例子中,如果你希望在自定义的函数中返回的实例类型与Symbol.species的类型保持一致的话,可以如下定义:

class MyArray extends Array {
  static get [Symbol.species](){
    return Array;
  }
  
  constructor(value){
    super();
    this.value = value;
  }
  
  clone(){
    return new this.constructor[Symbol.species](this.value)
  }
}

var myArray = new MyArray();
myArray.clone(); //[]

  通过上面的代码我们可以了解到,在实例方法中通过调用this.constructor[Symbol.species]我们就可以获取到Symbol.species继而可以创造对应类型的实例。

  上面整个的文章都是基于监听数组响应的一个点想到的。这里仅仅是起到抛砖引玉的作用,希望能对大家有所帮助。如有不正确的地方,欢迎大家指出,愿共同学习。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95443.html

相关文章

  • Vue响应式数据: Observer模块实现

    摘要:响应式数据是在模块中实现的我们可以看看是如何实现的。早期代码使用是进行单元测试,是事件模型的单元测试文件。模块实际上采用采用组合继承借用构造函数原型继承方式继承了其目的就是继承的,等方法。 前言   首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励。接下来的日子我应该会着力写一系列关于Vue与React内部原...

    shinezejian 评论0 收藏0
  • vue 子组件修改props引用类型参数引发思考

    摘要:但是当传递的参数为对象或者数组的时候,是通过引用传入的,所以对于一个引用类型的来说,在子组件中改变这个参数本身将会影响到父组件的数据状态。 问题 父级组件与子组件的通信一般都是通过props来实现的,因为数据流向的单一才能保证数据变化的可追踪性,在vue中props遵循的是单向数据流,原则上子组件修改props是不被允许的。但是当props传递的参数为对象或者数组的时候,是通过引用传入...

    周国辉 评论0 收藏0
  • 一次线上问题排查所引发思考

    摘要:直到有一天你会碰到线上奇奇怪怪的问题,如线程执行一个任务迟迟没有返回,应用假死。正好这次借助之前的一次生产问题来聊聊如何排查和解决问题。本地模拟上文介绍的是线程相关问题,现在来分析下内存的问题。尽可能的减少多线程竞争锁。 showImg(https://segmentfault.com/img/remote/1460000015568421?w=2048&h=1150); 前言 之前或...

    levy9527 评论0 收藏0
  • 记一次思否问答问题思考Vue为什么不能检测数组变动

    摘要:这里加了个简单判断,只看数组元素的,然后写了一个简单案例,主要测试使用改变数组元素能不能被监测到,并响应式的渲染页面运行页面可以看到,运行了次,我们数组长度为,也就是说数组被遍历了两遍。 问题来源:https://segmentfault.com/q/10... 问题描述:Vue检测数据的变动是通过Object.defineProperty实现的,所以无法监听数组的添加操作是可以理解的...

    raoyi 评论0 收藏0
  • 一道JS试题引发思考

    摘要:也给当初出入迷宫的我不小考验,一道题目可以引发许多思考,今天写下的只是今时今日的想法,到未来也许还有别样的看法。对于回调函数,可以对其传入三个参数分别是当前元素,元素索引,调用的数组。 [1,2,3].map(parseInt) 这道JS题目,相信大家并不会陌生。也给当初出入JS迷宫的我不小考验,一道题目可以引发许多思考,今天写下的只是今时今日的想法,到未来也许还有别样的看法。...

    xiao7cn 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<