IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> vue1历史源码解析:数组更新检测 -> 正文阅读

[JavaScript知识库]vue1历史源码解析:数组更新检测

前言

我们先来看看vue对应数组更新检测的文档:数组更新检测

1、Vue.js 包装了被观察数组的变异方法,故它们能触发视图更新。被包装的方法有:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

2、Vue.js 不能检测到下面数组变化:

直接用索引设置元素,如 vm.items[0] = {};
修改数据的长度,如 vm.items.length = 0

解析

Q:数组更新检测的整体思路是什么呢?

A:通过重新包装数据中数组的push、pop等常用方法。

注意,这里重新包装的只是数据数组(也就是我们要监听的数组,也就是vue实例中拥有的data数据)的方法,而不是改变了js原生Array中的原型方法。否则,当与vue无关的数组更新的时候,居然触发了vue的方法那就晕菜了

代码实现

const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];

aryMethods.forEach((method)=> {

    // 这里是原生Array的原型方法
    let original = Array.prototype[method];

   // 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
   // 注意:是属性而非原型属性
    arrayAugmentations[method] = function () {
        console.log('我被改变啦!');

        // 调用对应的原生方法并返回结果
        return original.apply(this, arguments);
    };

});

let list = ['a', 'b', 'c'];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 别忘了这个空数组的属性上定义了我们封装好的push等方法
list.__proto__ = arrayAugmentations;
list.push('d');  // 我被改变啦! 4

// 这里的list2没有被重新定义原型指针,所以就正常输出
let list2 = ['a', 'b', 'c'];
list2.push('d');  // 4

PS:如果不能理解这里的__proto__,可以前往 proto 和 prototype 以及 constructor进行了解

当然还有Vue先判断了一下是否能使用__proto__,不能的话最后采用直接给实例数组上挂异化后的push等方法的形式来完成。

深入思考

ok,目前为止我们已经实现了如何监听数组的变化了。

但是,我们仔细回想一下,疑惑Vue为什么不采用 继承数组 来实现数组监听的问题呢?

首先,我们来实现一段ES5继承的代码(感兴趣的可以点击看ES5写继承的思路):

function Parent() {}
Parent.prototype.push = function () {
    console.log('我是父类方法');
};
function FakeArray() {
    Parent.apply(this,arguments);
}
FakeArray.prototype = Object.create(Parent.prototype);
FakeArray.prototype.constructor = FakeArray;
FakeArray.prototype.push = function () {//重写子类的方法
    console.log('我被改变啦');
    return Parent.prototype.push.apply(this,arguments);
};
let fakeList = new FakeArray(0,1,2);
fakeList.push(3); //输出 我被改变啦!

那么,把上面的Parent 换成 Array是否就能 继承数组呢?

答案是不能,因为Array,String,Number,Regexp,Object 等JS的内置类 执行时不会对传进去的 this 做任何处理

例如 Object.apply({a:'1'}) 跟你执行 Object() 得到的对象一模一样,而我们自己写的构造函数却不会。

那么,我们换个思路,是否可以照着数组“仿照”一个构造方法,例如把上面的Parent构造函数模拟成Array,答案也是不行的,这也是那个著名的问题的来源:

ES5及以下的JS无法完美继承数组。(这是一个著名的问题,大伙们可以随意google,文章非常多,git上有大量的程序员朋友用各种奇技淫巧来实现继承数组实现队列、栈等等子类,但都不是完美的),主要有以下两个原因:

(1)因为数组有个响应式的length:他会自动根据你填入元素的下标进行增长,同时你把他改小的话,他一次删除把中间的元素给删除。

a = [1];a[10]=1;a.length===11以及a = [1,2,3,4];a.length=1//此时元素2,3,4被删除了

(2)数组内部的[[class]]属性,这个属性就是我们用Array.isArray(someArray)Object.prototype.toString.call(someArray)来判断 someArray是否是数组的根源 ,这是引擎内部实现,用任何JS方法无法改变。

而为什么一定要用这两种方法来判断是否是数组大家应该都看过相关文章把,比如someArray instanceof Array就无法正确判断,在 someArray是来自一个iframe而不是当前window的情况下(因为instanceof原型链上逐个比对)。


因为上面的两个原因导致你根本没办法继承数组,ES6倒是解决了这个问题,不管是classextends,还是setPrototypeOf,但是对于Vue,这都不是解决的方案。

除非是 __proto__ 这个实现最广的非标准属性(除了部分安卓机型没有实现改属性)才可以得以解决,下面我们来看看代码实现:

function FakeArray() {
    const arr = Array.apply(null,arguments)
    arr.__proto__ = FakeArray.prototype
    return arr
}
FakeArray.prototype = Object.create(Array.prototype);
FakeArray.prototype.constructor = FakeArray;
FakeArray.prototype.push = function () {//重写子类的方法
    console.log('我被改变啦');
    return Array.prototype.push.apply(this,arguments);
};
let fakeList = new FakeArray(0,1,2);
fakeList.push(3); //输出 我被改变啦!
console.log(fakeList)

到此,我们用 __proto__ 这个非标准属性解决了继承数组的问题。然而,Vue没有必要真正创建一个子类,所以Vue直接修改__proto__为一个继承自数组的对象即可

当然,这种形式来监听数组意味着 Vue 只能监听到那 7个异化方法 的执行,对于修改 length直接通过下标 以及 Array.prototype.push.apply(this.arr,[1,2,3]) 这种形式的使用都无法监听(上述情况确实无解,遍历下标执行 defineProperty 不可取也存在巨大bug)。只能采用this.$set/$delete等方法来让被异化的数组arr的arr.__ob__.dep属性上存放的dep实例收到数组修改事件,从而让所有订阅到这个数组的watcher都得到通知,当然,他们收到的通知是这个数组修改了,至于是哪个元素修改了并不知道。 所以才会有启发式diff算法的介入

数组为什么不能通过defineProperty的方式呢

首先 Vue 是反对你用 Vue.set/delete的。 但是增删属性的需求确实是有的,并且不少,所以他俩存在的意义是保证这种需求至少可以得到满足。

但是这种需求是低频的。而数组元素增删则是极其高频的

你会去 set/delete data上的10个属性吗? 可能你100行代码里都没有一个this.$delete,但是对于数组而言, 你增/删10个、100个、1000个元素都是再平常不过的需求。几乎所有数组使用都伴随着高频的数组元素删除。

同时,数组元素增删的方法是多样化的。

举个例子, 回忆一下数组的那个"响应式" length 属性(在数组10000下标里填写元素, 数组 length 自动变成10001; 给数组length属性赋值为0, 会自动清空数组所有元素).
这里单单举了length的例子, 但是光是使用length删除属性这种方法你就没法监听:如果你强行用defineProperty去强行改造length,不用多想,必然造成其响应式特性的丢失。

所以,现在的问题就出现了,你如果强行 hack , 去改造那些增、删的手段, 让用户每次增删元素, 你都能监听到, 并且在监听到之后使用 Vue.set / delete,那么因为增、删的高频性。会使得 Vue.set/ delete同样高频,但是 Vue.set / delete 会带来明显性能问题的呀。对于 data 的直接属性的 set 和 delete 会使得所有
watcher 重新计算 value,退订、重订依赖( _digest()方法 )。 每次 set / delete 都是 O(number of watcher) 级别。对于 n个数组元素的增删就是 O(n * number of watcher)。

同时,因为数组增删元素的多样性会带来代码实现上的极大复杂度,最关键的是即使代码量增加了,你也无法真正做到任何时候都能监听到他的增删操作,是的,没有方法。

所以,对每一个数组元素defineProperty带来代码本身的复杂度增加和代码执行效率的降低, 为什么不采用简单的改写数组7大方法来实现呢?

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-07 22:34:53  更:2022-04-07 22:35:40 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:12:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码