前言
首先欢迎大家关注我的 Github 博客 https://github.com/MrErHu/blog , 也算是对我的一点鼓励, 毕竟写东西没法获得变现, 能坚持下去也是靠的是自己的热情和大家的鼓励.
从上一篇文章响应式数据与数据依赖基本原理 https://github.com/MrErHu/blog/issues/28 开始, 我就萌发了想要研究 vue 源码的想法. 最近看了 youngwind 的一篇文章如何监听一个数组的变化 https://github.com/youngwind/blog/issues/85 发现 Vue 早期实现监听数组的方式和我的实现稍有区别. 并且在两年前作者对其中的一些代码的理解有误, 在阅读完评论中 @Ma63d 的评论 https://github.com/youngwind/blog/issues/85#issuecomment-284974136 之后, 感觉收益匪浅.
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,String,Number,Regexp,Object 等等 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 []
我们可以发现调用数组子类的实例 myArray 的 slice 方法时也会返回的是 MyArray 类型的实例. 如果你喜欢尝试的话, 你会发现即使去掉了静态访问器属性
get [Symbol.species]
,myArray.slice()也会仍然返回 MyArray 的实例, 这是因为即使你不显式定义, 默认的 Symbol.species 属性也会返回 this. 当然你也将 this 改变为其他值来改变对应方法的返回的实例类型. 例如我希望实例 myArray 的 slice 方法返回的是原生数组类型 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://juejin.im/post/5b213e956fb9a01e4f47ce69