-
Notifications
You must be signed in to change notification settings - Fork 384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
vue早期源码学习系列之二:如何监听一个数组的变化 #85
Comments
ES2015的class extends语法是可以完美继承Array的,没有你提到的问题 另外, |
并没有吧?
|
@henryzp class FakeArray extends Array{
push(...args){
console.log('我被改变啦');
return super.push(...args);
}
}
var list = [1, 2, 3];
var arr = new FakeArray(...list);
console.log(arr.length)
arr.push(3);
console.log(arr) |
@renaesop ,受教。。 thx |
作者那个proto应该是原型式继承,《高程》中紧随的那一节,实际上就是Object.create |
@lingxufeng2014 刚刚翻了一下书,发现确实如此。以前看书看到最常用的组合继承之后,就忽略了其他少用到继承方式了。多谢指点! |
@youngwind 我们先不说Vue不采用继承数组来实现数组监听的问题。 function FakeArray() {
Array.call(this,arguments);
}
FakeArray.prototype = [];
FakeArray.prototype.constructor = FakeArray;
FakeArray.prototype.push = function () {
console.log('我被改变啦');
return Array.prototype.push.call(this,arguments);
};
let list = ['a','b','c'];
let fakeList = new FakeArray(list);
先说博主你说的第二个原因,这里的错误是博主搞混了call和apply。博主在: function FakeArray() {
Array.call(this,arguments);//应该用apply
} 以及: FakeArray.prototype.push = function () {
console.log('我被改变啦');
return Array.prototype.push.call(this,arguments); //应该用apply
};
...
let fakeList = new FakeArray('a','b','c');//不能用数组来作为参数,那样的话数组就被包在数组里了。 在上述两处代码中博主都犯了这个错误。 然后再回到博主说的两个原因中的第一点:
Nonono,博主你写的代码是对的,常说的组合寄生式继承就是你写的这段代码,除了刚刚说的那个apply和call的小错误,其他的一点没错。因此要想达到重写的目的的话,就用你的写法是完完全全可以的,错误的不是你的写法,而是数组这东西很特殊。那好,我们假设,我们继承的不是数组,而是一个其他正常一点的东西,比如是一个我自己写的类Father: function Father(){
}
Father.prototype.push = function(){
console.log('我是父类方法')
}
// 下面的代码是博主你的代码,我只不过fix了一下call方法和最后构造函数调用的传参
// 同时由继承Array变成了继承Father
function FakeArray() {
return Father.apply(this,arguments);
}
FakeArray.prototype = new Father;
FakeArray.prototype.constructor = FakeArray;
FakeArray.prototype.push = function () {
console.log('我被改变啦');
return Father.prototype.push.apply(this,arguments);
};
let fakeList = new FakeArray('a','b','c'); 这段简单的继承代码真心不用我说。 那为什么把Father换成Array就不行了呢? 这也是那个著名的问题的来源:ES5及以下的JS无法完美继承数组。(博主可以随意google,文章非常多,git上有大量的程序员朋友用各种奇技淫巧来实现继承数组实现队列、栈等等子类,但都不是完美的)
因为响应式的length和[[class]]我们都无法在js层面实现,因此我们无法去用任何一个对象来“仿照”一个数组,这也就导致了你要想创造一个fakeArray,你必须在fakeArray里直接用Array构造函数,不能创造一个对象然后让对象继承Array.prototype,而 ES6解决了这个问题不管是class的extends,还是setPrototypeOf,但是对于Vue,这不是解决方案。 如果有 如果有 function fakeArray(){
let a = Array.apply(null,arguments)
a.__proto__ = fakeArray.prototype
a.constructor = fakeArray
return a
}
original = Array.prototype
fakeArray.prototype = Object.create(original);
fakeArray.prototype.constructor = fakeArray
fakeArray.prototype.push = function(){
console.log('苟利国家生死已')
original.push.apply(this,arguments)
}
var words = fakeArray()
words.push('岂','因','祸','福','避','趋','之')
console.log(words.join("")) 上述代码基本就是Vue源码一个小变种,思路是一样的,Vue没有必要真正创建一个子类哈,所以Vue直接修改 当然,这种形式来监听数组意味着Vue只能监听到那8个异化方法的执行,对于修改length和直接通过下标以及Array.prototype.push.apply(this.arr,[1,2,3])这种形式的使用都无法监听(上述情况确实无解,遍历下标执行defineProperty不可取也存在巨大bug)。只能采用this.$set/$delete等方法来让被异化的数组arr的 |
@Ma63d 非常感谢指出错误并给出详尽的解释。
|
@youngwind 你的文章也让我收获很多,你探寻的东西非常广,我应该多向你学习。 |
@Ma63d 看来有些东西就得深入的研究。这篇文章看了2天,因为刚接触js不久,所以google了好长时间。虽然还有些地方不太明白,es5及以下无法完美继承array应该是明白了。 |
@mygaochunming |
@Ma63d 非常感谢您的回答,但是关于第二点,我测试了一下,结果: |
@mygaochunming 当访问没有 既然已经在原型链上做了修正constructor的操作。这个操作对所有instance应该都生效了 另外 因此从fake array的本意上来看, 如果以上的内容有不正确的地方。欢迎斧正。 |
@mygaochunming @tommytroylin |
@Ma63d @youngwind 问一个问题,就是数组也是对象,明明defineProperty可以对对象进行循环遍历来监控每一个属性,为什么就不能对数组进行监控呢。对push等方法确实不适用,但是对取下标的方法改变数组是可以监控的呀。这个里面存在什么问题和bug呢? |
let a = []
Object.defineProperty(a, '0', {get: function(){console.log('getter'); return 1;}})
a.pop(); // 报错 |
@Ma63d |
let a = [{}]
Object.defineProperty(a, '0', {get: function(){console.log('getter'); return 1;}})
a.pop()
a.push(2)
console.log(a) // 之前的getter失效 |
@Ma63d 对于对象也是这样啊,比如对 b = {'age': 24} 这个对象做监控,然后delete掉age属性,即使之后再次添加一个age属性,那么也是监控不到的呀。这也是为什么Vue针对对象新添加的属性要使用Vue.set方法的原因。 我不觉得这样是取下标的方法改变数组不能监控的原因。 |
@Zhangzirui 可是数组你想往里面添加任何数据的话用户肯定是arr.push(1,2,3,4)啊, 你难道想让他这样去手动添加元素? this.$set(arr, '0', 1)
this.$set(arr, '1', 2)
this.$set(arr, '2', 3)
this.$set(arr, '3', 4) 这也就是数组使用defineProperty的问题, 你在init阶段监控一次后, 任何时刻把元素pop/splice出去了, 你的getter就失效了. 你再push的时候你就必须得让Vue手动监控一次。一旦用户又pop/splice,你又得手动监控。 |
@Ma63d 我知道这个意思,肯定是需要通过包装这些数组方法来监听。我只是疑惑这一点: Vue 不能检测数组利用索引直接设置一个项。我纠结的是为什么不能defineProperty和这些方法一起用呢。或许我有点笨了,我该自己亲自去看看实现过程了。本来是有点害怕看不懂源码,就直接来看现成的博客的,看来还是不能偷懒。 |
@Zhangzirui 额, 不是, 哥们是我描述的不太清楚. 这样吧, 我再解释一遍. 其实你也说了 数组本质还是个对象, pop 本质是 delete, 你 delete 了当然就监听不到了. 再次添加就需要再次 set 。 但是这种需求是低频的。而数组元素增删则是极其高频的 你会去 set/delete data上的10个属性吗? 可能你100行代码里都没有一个this.$delete,但是对于数组而言, 你增/删10个、100个、1000个元素都是再平常不过的需求。几乎所有数组使用都伴随着高频的数组元素删除。 同时,数组元素增删的方法是多样化的。 举个例子, 回忆一下数组的那个"响应式" length 属性(在数组10000下标里填写元素, 数组 length 自动变成10001; 给数组length属性赋值为0, 会自动清空数组所有元素). 所以,现在的问题就出现了,你如果强行 hack , 去改造那些增、删的手段, 让用户每次增删元素, 你都能监听到, 并且在监听到之后使用 Vue.set / delete,那么因为增、删的高频性。会使得 Vue.set/ delete同样高频,但是 Vue.set / delete 会带来明显性能问题的呀。对于 data 的直接属性的 set 和 delete 会使得所有 同时,因为数组增删元素的多样性会带来代码实现上的极大复杂度,最关键的是即使代码量增加了,你也无法真正做到任何时候都能监听到他的增删操作,是的,没有方法。 所以,对每一个数组元素defineProperty带来代码本身的复杂度增加和代码执行效率的降低, 为什么不采用简单的改写数组8大方法来实现呢? |
@Ma63d 谢谢你耐心讲解,我知道了,谢啦! |
@Ma63d 关于你写的上面这段,实际换成Array是可以的,博主方式其实没问题(除了语法错误),他是直接new FakeArray(),实际添加了一个叫__proto__的属性,而且这个__proto__指向FakeArray()的prototype对象,其实和你后面直接函数调用在FakeArray里实现的一个意思。 |
@lulutia 换成Array是不行的。 |
@Ma63d ok,没注意是 构造函数 初始化的 问题,那Array是不行的,就算方法可以重写,但是初始化没法搞,值也是不对的 |
大致了解原理实现, 粘帖一段Vue源码片段,可以直接 function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
// 正常方式
let normal = [];
normal.push(2);
console.log(normal);
// 方法一
let a = [];
a.__proto__ = arrayMethods;
a.push(2);
console.log(a);
// 方法二
let b = [];
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(b, key, arrayMethods[key])
}
b.push(2)
console.log(b); |
想问一下,这个是能监听到调用数组的'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'等几个方法,但是当访问和改变数组中的数据就不能监听到,这个与上一篇遗留的问题有点不符。这个功能是不需要吗?但是如果数组中又有对象呢? |
直接继承Array,然后想修改什么就在MyArray里面覆盖掉Array的方法,不知道这样行不行 |
前言
继上一篇 #84 ,文末我们提到另一个问题如何监听数组的变化?,今天我们就来解决这个问题。我们先来看一眼vue官方说明文档?
出处:https://cn.vuejs.org/v2/guide/list.html#变异方法
出处:https://cn.vuejs.org/v2/guide/list.html#注意事项
为什么说明文档中提到只有某些特定方法才能触发视图更新呢?我们可以从vue的源码中找到答案。
奇技淫巧
这次checkout的版本更上次一样,都是这个位置。
相关的源码是这两个地方。
整体思路是什么呢? → 通过重新包装数据中数组的push、pop等常用方法。注意,这里重新包装的只是数据数组(也就是我们要监听的数组,也就是vue实例中拥有的data数据)的方法,而不是改变了js原生Array中的原型方法。
为什么不能修改原生Array的原型方法呢?这道理很显然,因为我们是在写一个框架,而非一个应用,我们不应该过多地影响全局。如果你真得采取了这种糟糕的方法,想象以下场景:”你在一个应用中使用了vue,但是你在vue实例以外定义了一些数组,你改变这些与vue无关的数组的时候,居然触发了vue的方法!!“这能忍??
代码实现
PS:如果不能理解这里的proto,请翻看《Javascript的高级程序设计》第148页,以及参看这个答案,多看几遍你就懂了。(吐槽:每次碰到js原型都不好描述.....)
======================= 分割线 ==========================
2017.3.8 更新:在下面这这一章节《作者写得有问题?》中,关于“为何这么写”的解析有误。
在此保留原文,正确的解析请参考 @Ma63d 的评论。#85 (comment)
======================= 分割线 ===========================
作者写得有问题?
ok,目前为止我们已经实现了如何监听数组的变化了。
但是,我们仔细回想一下,难道只能通过作者那样的方法来实现吗?不觉得直接重新定义proto指针有点奇怪吗?有其他实现的方法吗?
我们回到最开始的目标:
对于某些特定的数组(数据数组),他们的push等方法与原生Array的push方法不一样,但是其他的又都一样。
这不就是经典的继承问题吗? 子类和父类很像,但是呢,子类有点地方又跟父类不同
我们只需要继承父类,然后重写子类的prototype中的push方法不就可以了吗?红宝书告诉我们组合继承才是最常用的继承方法啊!(请参考红宝书第168页)难道是作者糊涂了?(想到这儿,我心里一阵窃喜,拜读了作者的代码这么久,终于让我发现一个bug了,不过好像也算不上是bug)
废话不多说,我赶紧自己用组合继承实现了一下。
结果如下图所示
虽然我成功地重新定义push方法,但是为什么fakeList是一个空对象呢?
原因是:构造函数默认返回的本来就是this对象,这是一个对象,而非数组。Array.call(this,arguments);这个语句返回的才是数组。
那么我们能不能将Array.call(this,arguments);直接return出来呢?
不能。原因有两个:
shit.....太麻烦了!看来还是没有办法通过组合继承的模式来实现一开始的目标。(写到这儿,我心里默念:还是老司机厉害啊!我还是太年轻了......)
后话
目前为止,我们已经知道如何监听对象和数组的变化了,下一步应该做什么呢?
答案是:实现一个watch库
什么是watch库?你看一下这个就知道了。
The text was updated successfully, but these errors were encountered: