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

2018-06-14 admin

前言

首先欢迎大家关注我的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操作符调用构造函数会依次经历以下四个步骤:

  1. 创建新对象
  2. 将构造函数的作用域给对象(因此构造函数中的this指向这个新对象)
  3. 执行构造函数的代码
  4. 返回新对象(如果没有显式返回的情况下)

在没有显式返回的时候,返回的是新对象,因此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://segmentfault.com/a/1190000015283925

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-34014.html

文章标题:从Vue数组响应化所引发的思考

相关文章
vue+element-ui+slot-scope实现可编辑表格
1.咱开发拿到需求大多数是去网上找成型的组件,找不到再看原生的方法能否实现,大牛除外哈,大牛一般喜欢封装组件框架。 2.可编辑表格在后台管理系统还是比较常用的,因为比较流行框架element,iview都没有这个应用,所以考虑了两种方法,下...
2017-12-25
Vue获取DOM元素样式和样式更改示例
在 vue 中用 document 获取 dom 节点进行节点样式更改的时候有可能会出现 ‘style’ is not definde的错误,这时候可以在 mounted 里用 $refs 来获取样式,并进行更改: <template...
2017-03-13
从2014年的发展来展望JS的未来将会如何
<font face="寰�杞�闆呴粦, Arial, sans-serif ">2014骞达紝杞�浠惰�屼笟鍙戝睍杩呴€燂紝鍚勭�嶈��瑷€灞傚嚭涓嶇┓锛屼互婊¤冻鐢ㄦ埛涓嶆柇鍙樺寲鐨勯渶姹傘€傝繖浜涜��...
2015-11-12
12个你未必知道的CSS小知识
虽然CSS并不是一种很复杂的技术,但就算你是一个使用CSS多年的高手,仍然会有很多CSS用法/属性/属性值你从来没使用过,甚至从来没听说过。 1.CSS的color属性并非只能用于文本显示 对于CSS的color属性,相信所有Web开发人员...
2015-11-12
Vue.js组件tab实现选项卡切换
本文实例为大家分享了vue插件tab选项卡的具体代码,供大家参考,具体内容如下 效果图: 代码如下: <!DOCTYPE html> <html lang="en"> <head> ...
2017-03-13
ajax为什么令人惊异?ajax的优缺点
使用Ajax的最大优点,就是能在不更新整个页面的前提下维护数据。这使得Web应用程序更为迅捷地回应用户动作,并避免了在网络上发送那些没有改变的信息。 Ajax不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。就像DHT...
2015-11-12
破解前端面试(80% 应聘者不及格系列):从 闭包说起
不起眼的开始 招聘前端工程师,尤其是中高级前端工程师,扎实的 JS 基础绝对是必要条件,基础不扎实的工程师在面对前端开发中的各种问题时大概率会束手无策。在考察候选人 JS 基础的时候,我经常会提供下面这段代码,然后让候选人分析它实际运行的结...
2017-06-02
HTML5的5个不错的开发工具推荐
HTML5规范终于在今年正式定稿,对于从事多年HTML5开发的人员来说绝对是一个重大新闻。数字天堂董事长,DCloud CEO王安也发表了文章,从开发者和用户两个角度分析了HTML对两个人群的优势。其实,关于HTML5的开发工具,我们以往的...
2015-11-12
JavaScript教程:JS中的原型
Keith Peters 几年前发表的一篇博文,关于学习没有“new”的世界,其中解释了使用原型继承代替构造函数。两者都是纯粹的原型编码。 标准方法(The Standard Way) 一直以来,我们学习的在 JavaScript 里创建对...
2015-11-12
AJAX的浏览器支持
AJAX 的要点是 XMLHttpRequest 对象。 不同的浏览器创建 XMLHttpRequest 对象的方法是有差异的。 IE 浏览器使用 ActiveXObject,而其他的浏览器使用名为 XMLHttpRequest 的 Jav...
2015-11-12
回到顶部