vue2数据响应式原理——数据劫持(数组篇)

简介: vue2数据响应式原理——数据劫持(数组篇)

在这里插入图片描述

前言

本系列查阅顺序:

  1. [vue2数据响应式原理——数据劫持(初始篇)]
  2. [vue2数据响应式原理——数据劫持(对象篇)]
  3. [vue2数据响应式原理——数据劫持(数组篇)]
  4. [vue2数据响应式原理——依赖收集和发布订阅]

通过前两篇的学习,想必你已经对Object.defineProperty()和对象的侦听有了一定的理解,现在就让我们来继续研究如何使用Object.defineProperty()来对数组进行数据劫持,以便我们能够侦听到数组的变化。

数据劫持(数组篇)

首先新建一个:

array.js

import { def } from "./utils.js";
//获取数组的原型
const arrayProto = Array.prototype;
//以Array.prototype为原型创建arrayMethods对象,并将该对象暴露出去
//之后数组调用这7个方法时,让数组调用arrayMethods对象上我们已经进行重写的这7个方法
export const arrayMethods = Object.create(arrayProto);
const methodName = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "sort",
    "reverse",
];
methodName.forEach(function(method) {
    //备份原来的方法
    const original = arrayProto[method];
    // 定义新的方法:在arrayMethods对象上进行
    def(
        arrayMethods,
        method,
        function(...args) {
            //执行原生Array.prototype上的方法,实现push等这7个方法的功能
            //不能直接original()调用,因为这样调用的话original的上下文是window对象
            //需要使用apply改变original的上下文为调用该函数的数组
            //因为push这7个方法中有些方法(例如pop,splice)含有返回值
            //所以我们需要利用result储存一下返回值,之后再把result return出去
            const result = original.apply(this, args);

            //监听到数组改变之后就可以执行一些我们需要增加的操作了:

            //数组肯定不是最高层,比如obj.c是数组,则obj一定不是数组,第一层遍历obj对象时,已经给c属性(就是这个数组)添加了__ob__属性。
            //所以我们能拿到这个数组的__ob__
            //这里的this指的就是调用该函数的数组,因为该函数绑定到了arrayMethods对象上的push等7个属性上,而arrayMethods对象被我们变成了数组的原型
            //所以我们对数组执行push等7个方法时,实际就是数组调用了它arrayMethods原型上对应得方法,也即是该函数
            //所以该函数的this指向是调用该函数的数组
            const ob = this.__ob__;
            //push,unshift,splice会向数组内增加元素,这个增加的元素我们需要调用数组身上的Observer类(即__ob__)上的observeArray方法,对新增的元素进行响应式处理
            let inserted;
            switch (method) {
                case "push":
                case "unshift":
                    inserted = args;
                    break;

                case "splice":
                    //splice格式是splice(下标,数量,插入的新项)
                    inserted = args.slice(2);
                    break;
            }
            if (inserted) {
                ob.observeArray(inserted);
            }

           
            console.log("调用了我们改写的7方法");

            return result;
        },
        false
    );
});

在数组中,我们知道能够改变数组本身的方法只有七种:

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

所以在对数组进行数据劫持,侦听数组的变化时我们只需要对这七个方法进行一定的改写。

数组的方法都是在数组的原型Array.prototype中,所以看array.js我们就能理解为什么第一步要先获取到Array.prototype

之后我们以Array.prototype为原型创建了一个arrayMethods 对象,并在该对象上利用def函数添加了那七个方法,添加这些方法时我们对原本的那七个方法进行了封装,增加了一些我们侦听到数组变化后需要执行的操作,之后我们只需让数组的原型变成我们写的arrayMethods 对象,这样在数组调用push等这七个方法时实际是在调用arrayMethods.push而非Array.prototype.push

上述代码中的 ob.observeArray实际就是Observer类上的observeArray方法,并且让数组的原型变成我们写的arrayMethods 对象都需要在Observer.js中书写,所以我们需要对Observer.js进行一些修改:

Observer.js

import { def } from "./utils.js";
import defineReactive from "./defineReactive.js";
import { arrayMethods } from "./array.js";
import observe from "./observe.js";
export default class Observer {
    constructor(value) {
            def(value, "__ob__", this, false);
            //检查它是不是数组
            if (Array.isArray(value)) {
                //如果是数组,将该数组的原型指向我们定义的arrayMethods
                //以便使用数组那7个方法时是调用arrayMethods上我们处理过的而不是Array.prototype上的
                Object.setPrototypeOf(value, arrayMethods);
                //调用数组的遍历函数
                this.observeArray(value);
            } else {
                //调用对象的遍历函数
                this.walk(value);
            }
        }
        //遍历对象
    walk(value) {
            for (let k in value) {
                defineReactive(value, k);
            }
        }
        //遍历数组
    observeArray(arg) {
        //这里不直接i < l.length,是为了防止数组在遍历过程中出现长度的变化
        for (let i = 0, l = arg.length; i < l; i++) {
            // 逐项进行observe
            observe(arg[i]);
        }
    }
}

至此我们就实现了对数组的变化侦测:

let obj = {
    c: [1, 2, 3, { p: "我是p呀!!!" }],
};
observe(obj);

console.log(obj.c[3].p);
obj.c[3].p = "哇哈哈";
console.log("-------------");
obj.c.push(9);
console.log("-------------");
const a = obj.c.splice(1, 2, 888);
console.log("splice的返回值", a);

在这里插入图片描述

可以看到每当我们调用能够改变数组的方法时都会输出《调用了我们改写的7方法》这句话,说明我们已经能够侦听到数组的变化,并且数组内有对象时,当我们改变该对象的值时也能够被侦测到。

相关文章
|
JSON JavaScript 前端开发
❤Nodejs 第九章(token身份认证和express-jwt的安装认识)
【4月更文挑战第9天】Node.js第九章介绍了Token身份认证,特别是JWT(JSON Web Token)作为跨域认证的解决方案。JWT由Header、Payload和Signature三部分组成,用于在客户端和服务器间安全传输用户信息。前端收到JWT后存储在localStorage或sessionStorage中,并在请求头中发送。Express-JWT是一个中间件,用于解析JWT。基本用法包括设置secret和algorithms。注意安全问题,避免混合使用不同算法以防止降级攻击。
589 0
|
Ubuntu 网络协议 Linux
如何在无公网IP环境使用Windows远程桌面Ubuntu
如何在无公网IP环境使用Windows远程桌面Ubuntu
990 0
|
JavaScript Windows 容器
Flutter Web:根据浏览器窗口改变布局大小
之前我们通过flutter开发web应用,然后用electron打包成可执行文件在pc端使用,因为electron可以设置最小宽高,所以布局不会越界,但是如果直接在浏览器中打开,因为浏览器的大小无法控制,如果用户缩小浏览器会导致布局越界。根据大部分网站的经验来看,当窗口缩小到一定程度后,布局就不会再改变,反而是增加了滚动,这样就保证了布局的正确性,所以我们也打算这么做。
1888 0
|
缓存 Dart JavaScript
flutter版本控制工具 `FVM`
文章目录 FVM 切换VSCode 的Flutter版本 在windows上安装fvm 方法一:先安装 choco 检查安装是否成功 常用指令 接下来安装fvm 方法二:pub方式安装 fvm VSCode配置 安卓studio Flutter版本切换 项目 缓存目录 列出配置 设置缓存路径 配置 常用命令 安装 删除 列出 releases doctor 路由 配置全局版本 项目多个选项 针对不用flavor版本 切换flavor版本 查看flavor 常见问题 运行 FVM 时内核二进制文件无效或 sdk 哈希无效 PATH 中 Windows 的环境变量顺序 找不到命令“pub”
1874 1
flutter版本控制工具 `FVM`
|
Web App开发 JavaScript
Vue 项目中使用 debugger 在 chrome 谷歌浏览器中失效以及 console.log 指向去了 vue.js 代码
Vue 项目中使用 debugger 在 chrome 谷歌浏览器中失效以及 console.log 指向去了 vue.js 代码
2096 0
|
SQL 关系型数据库 MySQL
MySQL如何排查和删除重复数据
该文章介绍了在MySQL中如何排查和删除重复数据的方法,包括通过组合字段生成唯一标识符以及使用子查询和聚合函数来定位并删除重复记录的具体步骤。
1489 2
|
JavaScript 前端开发 API
纯js轻量级页面顶部Loading进度条插件
纯js轻量级页面顶部Loading进度条插件
|
存储 资源调度 JavaScript
一文带你了解PNPM以及 npm,yarn,pnpm区别
一文带你了解PNPM以及 npm,yarn,pnpm区别
1457 9
|
前端开发 JavaScript 搜索推荐
前端懒加载:提升页面性能的关键技术
前端懒加载是一种优化网页加载速度的技术,通过延迟加载非首屏内容,减少初始加载时间,提高用户访问体验和页面性能。
|
C++
VS Code 如何设置大小写转换快捷键
VS Code 如何设置大小写转换快捷键
2628 0
VS Code 如何设置大小写转换快捷键