【手写 Vue2.x 源码】第二十六篇 - 数组依赖收集的实现

news2024/11/29 22:50:01

一,前言

上篇,主要介绍了数组依赖收集的原理

本篇,数组依赖收集的实现


二,对象依赖收集的总结

{}.dep => watcher

目前,“对象本身”和“对象中的每一个属性”都拥有一个 dep 属性,用于做依赖收集
此时,为对象新增一个不存在的新属性时,就可以找到对象上的 dep 通知对应watcher做视图更新了

之前:对象本身没有 dep,只有修改了对象中已经存在的属性才会触发更新
现在:对象本身就有 dep,新增对象属性可以通知 dep 中收集的 watcher 更新


三,数组依赖收集的位置

对象或数组类型会通过 new Observer 创建 observer 实例,
所以,Observer 中的 value 可能是数组,也可能是对象;

Observer 类中的 value,即 this 指 observer 实例,
为其添加 `__ob__` 属性,这样每个对象本身或数组就拥有了 __ob__ 属性;

因此,可在此处为 observer 实例添加 dep 属性,
这就相当于为数组或对象本身都增加了一个 dep 属性;

这样就可以在对象或数组上,通过`value.__ob__.dep` 取到 dep,
当数组数据变化时,可以通过 dep 中收集的 watcher 触发视图更新操作;

四,数组和对象本身做依赖收集

在使用 defineReactive 定义属性时,此时value值有可能是数组
对数组的取值会走 Object.defineProperty 的 get方法

而 get 方法中就会进行依赖收集,如果当前value值为数组,就进行依赖收集
所以,当取值时,会对数组和对象本身进行一次依赖收集
// src/observe/index.js

/**
 * 给对象Obj,定义属性key,值为value
 *  使用Object.defineProperty重新定义data对象中的属性
 *  由于Object.defineProperty性能低,所以vue2的性能瓶颈也在这里
 * @param {*} obj 需要定义属性的对象
 * @param {*} key 给对象定义的属性名
 * @param {*} value 给对象定义的属性值
 */
function defineReactive(obj, key, value) {
  // childOb 是数据组进行观测后返回的结果,内部 new Observe 只处理数组或对象类型
  let childOb = observe(value);
  let dep = new Dep();  // 为每个属性添加一个 dep
  Object.defineProperty(obj, key, {
    get() {
      if(Dep.target){
        // 对象属性的依赖收集
        dep.depend();
        // 数组或对象本身的依赖收集
        if(childOb){ // 如果 childOb 有值,说明数据是数组或对象类型
            // observe 方法中,会通过 new Observe 为数组或对象本身添加 dep 属性
            childOb.dep.depend();    // 让数组和对象本身的 dep 记住当前 watcher
        }
      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return
      observe(newValue);
      value = newValue;
      dep.notify(); // 对象属性的更新
    }
  })
}
默认情况下,会为对象本身或数组本添加一个 dep 属性,
当进行观测时,会拿到数组的 observer 实例,即 childOb,`childOb.dep` 就是 dep;

在页面对数组进行取值时,如{{arr}} 一定会走 get 方法
如果 childOb 有值,就让当前数组把依赖收集起来`childOb.dep.depend()`
这样就完成了数组的依赖收集

数组本身添加了 dep :

image.png

五,数组中嵌套对象(对象或数组)的递归处理

数组中有可能嵌套数组或对象:如[{}]或[[]]

当前只会对数组的外层进行依赖收集,数组中嵌套的数组不会进行依赖收集

注意:此时,数组中嵌套的对象是可以进行依赖收集的

1,数组中嵌套对象的依赖收集原理

例如:arr:[{a:1},{b:2}]

当对 arr 取值时{{arr}},默认会对 arr 进行 JSON.stringify(arr),
JSON.stringify 会取出内部所有属性进行打印输出
即 JSON.stringify 会对内部属性进行取值操作,此时会走 getter,
而 getter 中就会为对象本身和内部属性进行依赖收集

所以,这种情况默认就会进行依赖收集
<body>
  <div id=app>
    {{arr}}
  </div>
  <script src="./vue.js"></script>
  <script>
    // 测试数组的依赖收集
    let vm = new Vue({
      el: '#app',
      data() {
        return { arr: [{ a: 1 }, { b: 2 }] }
      }
    });
    vm.arr[0].a = 100;
    console.log("输出当前 vm", vm);
  </script>
</body>

页面输出:[{"a":100},{"b":2}]
对 arr 取值,内部会进行 JSON.stringify,就会为对象中的 a 属性做依赖收集;
所以,数组中的对象中的a 属性更新时,走的就是对象的更新和数组无关

当前数组中的对象中的属性是有 dep 的:
image.png

2,数组中嵌套数组的依赖收集实现

例如:arr:[[1][2]]

当对 arr 取值时{{arr}},只对外层数组进行依赖收集,内部的数组没有进行依赖收集
所以,arr[0].push直接操作内部数组,是不会触发视图更新的

需要对数组类型做递归依赖收集

数组中如果有对象[{}],也需要为对象本身做依赖收集,
因为未来有可能会为对象新增属性,对象本身做依赖收集才可以更新视图

注意:前面虽然已经对数组进行了递归观测,但用户使用数据不是递归使用的

// src/observe/index.js

function defineReactive(obj, key, value) {
  let childOb = observe(value);
  let dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      if(Dep.target){
        dep.depend();
        if(childOb){
            childOb.dep.depend();
            if(Array.isArray(value)){// 如果当前数据是数组类型
              dependArray(value)     // 可能数组中继续嵌套数组,需递归处理
            }  
        }
      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return
      observe(newValue);
      value = newValue;
      dep.notify();
    }
  })
}

/**
 * 使数组中的引用类型都进行依赖收集
 * @param {*} value 需要做递归依赖收集的数组
 */
function dependArray(value) {// 让数组里的引用类型都收集依赖
  // 数组中如果有对象:[{}]或[[]],也要做依赖收集(后续会为对象新增属性)
  for(let i = 0; i < value.length; i++){
    let current = value[i];
    // current 上如果有__ob__,说明是对象,就让 dep 收集依赖(只有对象上才有 __ob__)
    current.__ob__ && current.__ob__.dep.depend();
    // 如果内部还是数组,继续递归处理
    if(Array.isArray(current)){
      dependArray(current)
    }
  }
}

测试:

<body>
  <div id=app> {{arr}} </div>
  <script src="./vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return { arr: [[]] }
      }
    });
    console.log("输出当前 vm", vm);
  </script>
</body>

页面输出:[{"a":100},{"b":2}]

外层数组本身和内层数组都添加了 dep:

image.png

3,数组的视图更新

上边已经将数组的依赖进行了收集,目前 arr.push 还不能更新视图,因为没有调用更新方法

所以,当arr.push等操作变更数组时,还需要再触发数组的依赖更新(通过 ob 拿到 dep 调用 notify)

// src/observe/array.js

let oldArrayPrototype = Array.prototype;
export let arrayMethods = Object.create(oldArrayPrototype);
let methods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'reverse',
  'sort',
  'splice'
]

methods.forEach(method => {
  arrayMethods[method] = function (...args) {
    oldArrayPrototype[method].call(this, ...args)
    let inserted = null;
    let ob = this.__ob__; // 获取数组上的 __ob__
    switch (method) {
      case 'splice':
        inserted = args.slice(2);
      case 'push':
      case 'unshift':
        inserted = args;
        break;
    }
    if(inserted)ob.observeArray(inserted);
    ob.dep.notify();  // 通过 ob 拿到 dep,调用 notify 触发 watcher 做视图更新
  }
});

测试修改数组内部嵌套的数组:

<body>
  <div id=app> {{arr}} </div>
  <script src="./vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return { arr: [[]] }
      }
    });
    vm.arr[0].push(100);   // 修改数组中的数组
  </script>
</body>

// 页面输出:[[100]]

六,总结

响应式数据原理分为对象和数组两大类,在 Vue 初始化过程中:

  • 通过对象属性劫持,会为所有属性添加 dep
  • 还会为属性值进行依赖收集:为对象本身和数组也添加 dep
  • 如果是属性变化,将触发属性对应的 dep 去做更新;
  • 如果是数组更新,将触发数组本身的 dep 去做更新;
  • 如果取值时属性值为数组,数组中的对象类型(数组中嵌套的对象或数组)递归进行依赖收集
  • 如果数组中嵌套了对象,由于对象取值会进行 JSON.stringify,所以对象中的属性默认就会做依赖收集

七,结尾

本篇,主要介绍了数组依赖收集的实现

  • 对象依赖收集的总结;
  • 数组依赖收集的位置;
  • 数组和对象本身做依赖收集;
  • 数组中嵌套对象(对象或数组)的递归处理;

下一篇,Vue 生命周期的实现

维护日志

20210629:

  • 添加 5-3、数组的视图更新部分
  • 添加各种情况的测试 Demo、截图、部分文案调整
  • 添加 6、总结部分

20210805:

  • 更新“结尾”部分与文章摘要

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/166012.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Leetcode:669. 修剪二叉搜索树(C++)

目录 问题描述&#xff1a; 实现代码与解析&#xff1a; 递归&#xff1a; 原理思路&#xff1a; 后序递归&#xff1a; 原理思路&#xff1a; 迭代&#xff1a; 原理思路&#xff1a; 问题描述&#xff1a; 给你二叉搜索树的根节点 root &#xff0c;同时给定最小边界…

#9文献学习--基于元强化学习的边缘计算快速自适应任务卸载

文献&#xff1a;Fast Adaptive Task Offloading in Edge Computing based on Meta Reinforcement Learning 基于深度强化学习DRL的方法&#xff0c;样本效率很低&#xff0c;需要充分的再培训来学习新环境的更新策略&#xff0c;对新环境的适应性很弱。 基于元强化学习的任务…

【数据结构与算法】第十八篇:递归,尾递归,尾调用

知识概览一、递归的引入&#xff08;递归现象&#xff09;二、递归的调用过程与实例分析三、递归的基本思想小tip:链表递归的具体实例四、递归的一般使用条件五、实例分析&#xff1a;斐波那契数列1.原理剖析2.fib优化1 – 记忆化3.fib优化24.fib优化3六、实例分析&#xff1a;…

mac下ssh连接docker使用centos

配置ssh连接docker本机信息 Apple M2/ macOS Ventura 13.1完整实现如下&#xff1a;使用docker下载centos镜像docker pull centos:centos7 # centos7 指定安装版本查看本地镜像# 使用以下命令查看是否已安装了centos7➜ ~ docker images REPOSITORY TAG IMAGE ID …

c++通讯录管理系统

结构体1&#xff0c;知识点&#xff08;结构体&#xff09;&#xff0c;存放人员详情&#xff0c;名字&#xff0c;性别&#xff0c;年龄等 struct person { string m_name; int m_sex; int m_age; string m_phone; string m_addr; };结构体2&#xff0c;知识点 &#xff08;结…

狗厂的N+1+2毕业,我觉得还是挺良心的

最近又跟朋友打听到了新鲜事&#xff0c;年底的新鲜事&#xff0c;什么209万&#xff0c;就是听个乐子&#xff0c;离我太远&#xff0c;什么HR和技术人员产生矛盾&#xff0c;一巴掌眼镜都打飞了&#xff0c;好乱套&#xff0c;今天我跟朋友打听了一些不太乱套的 一、鹅肠 1.…

Quartz认知篇 - 初识分布式任务调度Quartz

定时任务的使用场景 在遇到如下几种场景可以考虑使用定时任务来解决&#xff1a; 某个时刻或者时间间隔执行任务 批量数据进行处理 对两个动作进行解耦 Quartz 介绍 Quartz 是一个特性丰富的、开源的任务调度库&#xff0c;几乎可以嵌入所有的 Java 程序&#xff0c;包括很…

基于二叉树的改进SPIHT算法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

电脑怎么设置动态壁纸?关于Windows和Mac壁纸的设置方法

为了让电脑桌面更加美观舒适&#xff0c;很多人都会给电脑的桌面设置自己喜欢的壁纸。图片壁纸很多人都会设置&#xff0c;但是电脑怎么设置动态壁纸&#xff1f;这是很多人的困扰。其实方法同样很简单&#xff0c;下面有关于Windows和Mac动态壁纸的设置方法&#xff0c;一起来…

【阶段四】Python深度学习03篇:深度学习基础知识:神经网络可调超参数:激活函数、损失函数与评估指标

本篇的思维导图: 神经网络可调超参数:激活函数 神经网络中的激活函数(有时也叫激励函数)。 在逻辑回归中,输入的特征通过加权、求和后,还将通过一个Sigmoid逻辑函数将线性回归值压缩至[0,1]区间,以体现分类概率值。这个逻辑函数在神经网络中被称为…

PyCharm调用远程Python解释器

PyCharm调用远程Python解释器 PyCharm中直接调用远程服务器中Python解释器&#xff1a; 本地不用搭建Python环境。既避免了本地使用Window而服务器使用Linux系统不统一情况&#xff0c;又不用担心本地调试没问题而放到服务器上就出现问题。 PyCharm中打开项目并设置Python解释…

封装chrome镜像

chrome镜像 selenium提供了一个镜像&#xff0c;但这个镜像里面包含了比较多的东西&#xff1a; 镜像地址-github supervisord java chrome webDriver 实际的使用中遇到了一些问题 chrome遇到一些比较耗费内存和cup的操作的时候&#xff0c;有的时候会kill掉java进程&a…

干货 | 大数据交易所数据安全流通体系标准化尝试

以下内容整理自清华大学《数智安全与标准化》课程大作业期末报告同学的汇报内容。第一部分&#xff1a;国内大数据交易所发展现状第二部分&#xff1a;国外大数据交易模式及法律法规欧盟的数据交易模式是基于2022年5月16日所提出的《数据治理法案》&#xff0c;其中提出了数据中…

【C++11】—— 包装器

目录 一、function包装器 1. function包装器基本介绍 2. function包装器统一类型 3. function包装器的使用场景 二、bind包装器 一、function包装器 1. function包装器基本介绍 function包装器 也叫作适配器。C中的function本质是一个类模板&#xff0c;也是一个包装器…

第四章 基本数据

在第2章中&#xff0c;我们讨论了多种导入数据到R中的方法。遗憾的是&#xff0c;将你的数据表示为矩阵或数据框这样的矩形形式仅仅是数据准备的第一步。这里可以演绎Kirk船长在《星际迷航》“末日决战的滋味”一集中的台词&#xff08;这完全验明了我的极客基因&#xff09;&a…

聚观早报|春节档新片预售总票房破千万;苹果获可折叠iPhone新专利

今日要闻&#xff1a;比亚迪据称拟在越南建汽车零部件厂&#xff1b;2023 年春节档新片预售总票房破 7000 万&#xff1b;苹果获得可折叠 iPhone 新专利&#xff1b;北京汽车获1000台EU5 PLUS约旦订单&#xff1b;娃哈哈要解决100万农户农产品出路 比亚迪据称拟在越南建汽车零部…

C 语言目标文件

前言 一个 C 语言程序经编译器和汇编器生成可重定位目标文件&#xff0c;再经链接器生成可执行目标文件。那么目标文件中存放的是什么&#xff1f;我们的源代码在经编译以后又是怎么存储的&#xff1f; 文章为 《深入理解计算机系统》的读书笔记&#xff0c;更为详细的内容可…

【数据结构】双向链表

1.双向链表的结构2.双向链表的实现首先在VS里面的源文件建立test.c和List.c,在头文件里面建立List.hList.h:#pragma once #include <stdio.h> #include <stdlib.h> #include <assert.h> typedef int LTDateType; typedef struct ListNode {LTDateType data;s…

LeetCode 329. 矩阵中的最长递增路径(C++)*

思路&#xff1a; 1.用动态规划&#xff0c;但是时间复杂度太高&#xff0c;效率太低 2.使用常规的DFS&#xff0c;时间复杂度高&#xff0c;包含了太多重复无效遍历&#xff0c;会超时 3.在DFS的基础上使用记忆化搜索&#xff0c;帮助消去重复的遍历&#xff0c;提高效率 原题…

解决: 您目前无法访问 因为此网站使用了 HSTS。网络错误和攻击通常是暂时的,因此,此网页稍后可能会恢复正常

目录 问题描述 报错信息 问题原因 如何解决 参考资料 问题描述 您目前无法访问 因为此网站使用了 HSTS。网络错误和攻击通常是暂时的&#xff0c;因此&#xff0c;此网页稍后可能会恢复正常。 报错信息 今天使用Edge浏览器在访问一个平时常用的emoji网站时&#xff0c;…