vue2数据劫持(源码剖析)

一、配置项目

初始化安装项目
  1. 初始化项目

npm init -y

  1. 安装webpack、webpack-cli、webpack-dev-server;

npm i webpack webpack-cli webpack-dev-server

  1. 安装html-webpack-plugin;

npm i html-webpack-plugin
:::

配置文件 webpck.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = {
  entry:'./src/index.js',
  output:{
    filename:'bundle.js',
    path:path.resolve(__dirname,'dist')
  },
  devtool:'source-map',
  resolve:{
    // 配置寻找文件时,先在根目录文件下寻找,根目录文件没有找到,在去node_modules下寻找
    modules:[path.resolve(__dirname,''),path.resolve(__dirname,'node_modules')]
  },
  plugins:[
    new HtmlWebpackPlugin({
      template:path.resolve(__dirname,'public/index.html')
    })
  ]
}

二、实现vue2数据劫持的基本思路

2.1 程序入口 (src/index.js)
import Vue from 'vue'
let vm = new Vue({
  el:'#app',
  data(){
    return {
      title:'水果列表',
      species:[
        {
          name:'apple',
          number:20,
          origin:'中国',
          kind:[
            {name:'red',number:15},
            {name:'green',number:5}
          ]
        },
        {
          name:'banana',
          number:60,
          origin:'泰国',
          kind:[
            {name:'yellow',number:50},
            {name:'green',number:10}
          ]
        },
      ],
      merchant:{
        address:'长征路',
        number:3,
        name:['小王水果','优乐选','百果园']
      },
      list:['香蕉','苹果'],
      info:{
        a:{
          b:1
        }
      }
    }
  }
});

Vue是构造函数,options将是我们用户提供的配置项,例如:el、data、template、methods等。_init()初始化函数将是整个程序的入口。

function Vue(options) {
  this._init(this, options)
}

init方法做了以下几件事情:

  1. 保存this指向。为什么要保存this指向呢?其实保存this指向更容易让我们明确当前this的指向,由于vm.init()的原因,所以当前init方法内部的this指向vm
  2. 保存options配置项。为什么要保存options配置项呢?其实我觉得是处于保存一份用户配置项为目的,做法也是非常的简单,将options配置项挂载到vm实例对象上即可。
  3. 保存挂载用户配置的data数据。为什么要保存data配置项呢?在原生的Vue实例对象上也挂载着_data属性,其目的我觉得在于保存用户的原始数据,不在用户的原始数据上进行操作。
Vue.prototype._init = function(vm,options){
  vm.$options = options;

  initState(vm);
}

function initState(vm){
  var options = vm.$options;
  if(options.data){
    initData(vm);
  }
}

function initData(vm){
  var data = vm.$options.data;
  // data配置可能存在函数形式与对象形式。这两种形式都可以进行配置,区别在于函数的形式最后返回独立的对象,这样就避免引用共享的问题。所以我们在处理data形式的时候,需要分别进行判断,并且设置不同的返回值。
  data = vm._data = typeof data === 'function' ? data.call(vm) : data || {};
}
2.2 难点一

目前为止,我们已经搭建了基本的架构,现在需要处理我们遇到的第一个问题,我们先梳理一下需要处理的问题:
现状:我们现在想通过vm实例对象去获取data属性下某个值,我们只能通过vm._data.xxx的形式进行获取。因为在initData方法中,我们已经将配置后的data属性挂在到vm实例对象上去了(vm._data = data = _data)
差异:但是vm._data.xxx的形式并不是我们需要的,因为在原生的Vue中,我们可以在其它配置项内部直接通过this.xxx形式取值,例如:在methods配置中获取title属性值console.log(this.title)
预期:我们现在也想要能够通过this.xxx||vm.xxx的形式进行取值操作。
解决方法:代理数据。什么是代理数据呢?其实我理解的很简单,也就是说,你现在想将vm._data.title取值的形式转换为vm.title的形式。那么实际上当你进行vm.title取值操作时,你获取的值实际上就是vm._data.title的值。当你进行vm.title = xxx赋值操作时,你赋值的形式就是vm._data.title = xxx。换句话说,这种取值赋值的行为相当于对vm._data.title包裹了一层操作,通过这层操作才能对vm._data.title起到效果,所以vm.title就起到这层包裹的效果。
思路:所谓的代理数据,其实本质上目的也很明确,就是在你需要的对象上定义属性(如果用Object.defineProperty())方法。那么我们现在的思路就是要搞清楚,怎么代理数据?谁是代理对象?数据代理给谁?数据从那里来?这些问题?
回过头来看我们的需求:vm._data.title => vm.title,我们需要将vm._data.title的取值操作转换为vm.title的形式。首先我们vm实例对象上要有title属性,不然怎么去进行取值操作,title属性是哪里来的呢?很显然titleoptions配置项中data数据中的属性,之前我们将options.data保存备份在vm实例对象中。那么vm.title取值取的是谁的值呢?也很简单,vm.title => vm._data.title,明白了吗?实际上vm.title取的就是vm._data.title值。
结合上述的分析,我们看下面方法的思路:
proxyData(vm, target, key)方法是核心方法,思路也和我们上述分析的一致:在vm实例对象上定义属性key,取key值的操作get(){}方法实质是在取vm._data[key]的值。而赋值set(newValue){}操作,其实本质上是在对vm._data[key]进行赋值。其中,set函数做了优化,当新赋予的值与旧值相同时,则不进行赋值操作。

Vue.prototype._init = function(vm,options){
  vm.$options = options;

  initState(vm);
}

function initState(vm){
  var options = vm.$options;
  if(options.data){
    initData(vm);
  }
}

function initData(vm){
  var data = vm.$options.data;
  // data配置可能存在函数形式与对象形式。这两种形式都可以进行配置,区别在于函数的形式最后返回独立的对象,这样就避免引用共享的问题。所以我们在处理data形式的时候,需要分别进行判断,并且设置不同的返回值。
  data = vm._data = typeof data === 'function' ? data.call(vm) : data || {};
  for (const key in data) {
   proxyData(vm, '_data', key);
  }
}
function proxyData(vm,target,key){
  Object.defineProperty(vm,key,{
    get(){
      // vm._data[key]
      // console.log('proxyData',vm[target][key]);
      return vm[target][key];
    },
    set(newValue){
      // vm.title = xxx   vm._data.title = xxx
      if (newValue === vm[target][key]) return;
      vm[target][key] = newValue;
    }
  })
}

到目前为止的话,我们成功完成基本的数据代理,现在你尝试vm.title的形式去进行取值操作,就达到了我们预期。

2.3 难点二

现在我们需要引入一个新的概念:观察模式observe。什么是观察模式呢?为什么要引入观察模式呢?在哪里去引入观察模式呢?
现状:结合原生的Vue来说,当我们尝试修改完成data中的数据时,视图也会有所随之更新。那么就需要我们想一想,为什么只是单纯的修改数据就能够影响到视图更新呢?我们目前能够实现这种效果吗?
差异:我们现在并不能做到数据变化之后,视图随之进行更新的效果。但是原生Vue可以做到,那不妨我们大胆的猜测一下:也就是说,Vue在数据变化的时候,它不仅仅只是让数据单纯的变化(取值与赋值操作),它还做了一系列的其他操作。这样的话,当数据修改的同时还会进行其他的操作。
预期:我们也想实现类似原生Vue的这种效果。
解决方法:数据劫持正好符合我们的概念。所以,当我们不想让数据只是单纯的进行取值赋值操作时,我们可以使用数据劫持的概念,让数据单纯的取值赋值操作具备其他的逻辑能力。例如:当我数据修改的同时,我想让视图进行更新、我想收集依赖、我想执行其他的逻辑操作。那么观察者模式observe和数据劫持有什么关系?从整体看下来,observe观察者模式更加抽象,而数据劫持更加具体。怎么说呢?观察者更像是一种模式,而数据劫持是一种解决方法。数据劫持去操作每个数据的取值赋值行为,当某种行为触发的时候,将在取值和赋值的基础上添加其他逻辑。而所有的数据都会被这种模式监听和观察,如果说数据劫持像一台机器的话,那么观察者模式就是一个工厂。某个数据进入到我的工厂里面,工厂把该数据投入到对应的机器中进行加工,当数据发生对应的行为时,就会产生对应的响应行为。所有的数据都遵循着这种模式,你只需要把数据交给我的工厂,我就会给你输出相同的产品。

function initData(vm){
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function' ? data.call(vm) : data || {};
  for (const key in data) {
   proxyData(vm, '_data', key);
  }
  // 加入观察者模式
  observe(vm._data)
}

observe()观察者模式需要接收参数是对象的形式,当接受的是非对象的形式,将自动返回,因为原始值无需放入观察者模式中进行观察。

function observe(data){
  if(typeof data !== 'object' || data == null) return;
  return new Observer(data);
}

Observer函数就是观察者模式的核心方法,需要我们注意的是,观察到对象不仅仅只是对象的形式,还有可能是数组的形式。对于不同的类型,我们需要分别处理,相比较数组的处理方法,对象的处理方法比较简单:
对象形式的处理思路:我们通过循环枚举对象属性,利用Object.defineProperty()方法对该对象重新定义属性,利用数据劫持的概念去重新定义该属性取值与赋值的行为。需要我们注意的是:在进行数据劫持的时候,可能对象是深层次的,那么我们就需要递归深层次的进行数据劫持。

function Observer(data){
  if(Array.isArray(data)){
   // 数组形式
  }else{
    // 对象形式
    this.walk(data);
  }
}

Observer.prototype.walk = function(data){
  // 获取该对象键值
  var keys = Object.keys(data);
  // console.log(keys,'keys');
  // 循环枚举对象属性
  for (let i = 0; i < keys.length; i++) {
    var key = keys[i],
        value = data[key];
    // 数据劫持重新定义属性行为
    defineReactiveData(data,key,value);
  }
}

function defineReactiveData(data,key,value){
  // 如果value是对象,继续递归深层次数据劫持
  observe(value)
  // 重新定义属性的取值与赋值行为
  Object.defineProperty(data,key,{ 
    get(){
      console.log('响应式数据获取',value);
      return value;
    },
    set(newValue){
      console.log('响应式数据设置',newValue);
      if(newValue === value) return;
      // 如果设置的值是对象,递归深层次数据劫持
      observe(newValue)
      value = newValue;
    }
  })
}

数组形式问题出现的原因:虽然我们现在对数组本身进行了数据劫持,但是并未对数组内部的元素进行处理,这样做将会引发一个问题,比如说:我现在对 list 数据进行赋值的操作(vm.list = []),现在我们是可以数据劫持到的。但是如果是vm.list.push(1)的形式,那么现在我们就无法劫持到数据,因为Object.defineProperty方法本身就是给对象定义属性了来用的,对于处理数组问题,它还不是随心所欲。换句话来说,我们目前无法劫持到对原数组本身操作的方法。
数组形式的解决方法:我们对那些操作原数组的方法进行重构,也就是说,我们不改变数组的原有方法,让数组的原有方法去处理关于对数组对操作。我们只需关注自己需要做的逻辑,比如说,push方法添加的元素是否是对象或者数组形式,如果是的话,我们还需要将新增的数据放入到观察者模式。

// 定义哪些数组方法是操作的原数组
var ARR_METHODS = ['push','pop','shift','unshift','splice','sort','reverse']var originArrMethods = Array.prototype, // 拿到所有数组的方法
    arrMethods = Object.create(originArrMethods); // 创建一个原型链指向数组的对象

ARR_METHODS.map(function(m){
  // 遍历所有需要重新定义的数组方法
  arrMethods[m] = function(){
    // // 将实际参数列表转换数组形式
    var args = Array.prototype.slice.call(arguments),
      // 执行原数组的方法
    rt = originArrMethods[m].apply(this, args);
    console.log('数组新方法执行',args);
    // 视图更新的逻辑......
    var newArr;
    switch(m){
      case 'push':
      case 'unshift':
        newArr = args;
        break;
      case 'splice':
        newArr = args.slice(2);
        break;
      default:
        break;
    }
    newArr && observeArr(newArr);

    return rt;
  }
})

// 对数组新增的元素进行观察,因为新增的可能是新的数组或者对象 
function observeArr(arr){
  for (let i = 0; i < arr.length; i++) {
    observe(arr[i])
  }
}

三、源码链接

vue2数据劫持源码剖析

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

aosp13/14命令行进入分屏相关实战

背景&#xff1a; 分屏一般在手机上都是都是从桌面的最近任务卡片进入的&#xff0c;一般来说手机用户都是这样操作的&#xff0c;但是有一些场景或者情况就不一定可以顺利用上这个桌面的多任务卡片进入。 比如以下场景&#xff1a; 1、可能不是桌面的多任务的场景&#xff0c…

【Altium Designer 20 笔记】PCB铺铜过程

PCB铺铜步骤 切换到Keep-Out Layer&#xff08;禁止布线层&#xff09; 使用shifts键切换单层显示 画禁止布线范围&#xff08;防止铺铜过大&#xff09; 切换到需要铺铜的层 选择铺铜网络&#xff0c;通常是地&#xff08;GND&#xff09;或某个电源网络 隐藏覆铜&#xff1a;…

一.吊打面试官系列-数据库优化-认识MySql索引

1.什么是索引 索引&#xff08;Index&#xff09;是帮助DBMS&#xff08;数据库&#xff09;高效获取数据的数据结构&#xff0c;索引是为了加速对表中数据行的检索而创建的一种分散的存储结构。如果数据库没有索引就会走表进行全表扫描&#xff0c;一旦数据量上来&#xff0c…

如何基于香橙派AIpro对视频/图像数据进行预处理

背景介绍 受网络结构和训练方式等因素的影响&#xff0c;绝大多数神经网络模型对输入数据都有格式上的限制。在计算机视觉领域&#xff0c;这个限制大多体现在图像的尺寸、色域、归一化参数等。如果源图或视频的尺寸、格式等与网络模型的要求不一致时&#xff0c;我们需要对其…

【中间件】ElasticSearch简介和基本操作

一、简介 Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎&#xff0c;支持各种数据类型&#xff0c;包括文本、数字、地理、结构化、非结构化 ,可以让你存储所有类型的数据&#xff0c;能够解决不断涌现出的各种用例。其构成如下&#xff1a; 说明&#xff1…

递归、搜索与回溯算法——递归

T04BF &#x1f44b;专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 小比特 大梦想 此篇文章与大家分享递归,搜索与回溯算法关于递归的专题 如果有不足的或者错误的请您指出! 目录 1.什么时候使用递归2.汉诺塔2.1解析2.2题解 3.合并两个有序链表3.1解析3.2题解 4.翻转链表4.1解析4…

Spring Boot 统一功能处理(二)

本篇主要介绍Spring Boot统一功能处理中的统一数据返回格式。 目录 一、定义统一的返回类 二、配置统一数据格式 三、测试配置效果 四、统一格式返回的优点 五、源码角度解析String问题 一、定义统一的返回类 在我们的接口在处理请求时&#xff0c;返回的结果可以说是参…

判断位数、按位输出、倒序输出(C语言)

一、运行结果&#xff1b; 二、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h>int main() {//初始化变量值&#xff1b;int number 0;int i 1;int m 0;int z 0;int z1 0, z2 0, z3 0, z4 0;//提示用户&#xff1b;printf("请输…

编程新手必看,Python3中函数知识点及语法学习总结(18)

介绍&#xff1a; Python3中的函数是组织好的、可重复使用的代码段&#xff0c;用于实现单一或相关联的功能。 以下是Python3中函数的一些基本介绍&#xff1a; 函数定义&#xff1a;在Python中&#xff0c;可以通过def关键字来定义一个函数。函数定义后&#xff0c;可以多次调…

ADB的基本语法及常用命令

学习网址 ADB命令的基本语法如下&#xff1a; adb [-d|-e|-s <serialNumber>] <command> 如果有多个设备/模拟器连接&#xff0c;则需要为命令指定目标设备。 参数及含义如下&#xff1a; 常用命令如下&#xff1a; 1. 启动ADB服务 adb start-server 2. 停止…

【ROS2笔记六】ROS2中自定义接口

6.ROS2中自定义接口 文章目录 6.ROS2中自定义接口6.1接口常用的CLI6.2标准的接口形式6.3接口的数据类型6.4自定义接口Reference 在ROS2中接口interface是一种定义消息、服务或动作的规范&#xff0c;用于描述数据结构、字段和数据类型。ROS2中的接口可以分为以下的几种消息类型…

腾讯云优惠券领取及使用教程详解

腾讯云作为国内领先的云服务提供商&#xff0c;以其稳定可靠、性能卓越的服务赢得了广大用户的青睐。为了回馈用户&#xff0c;腾讯云经常推出各种优惠活动&#xff0c;其中优惠券就是非常受欢迎的一种。本文将详细介绍腾讯云优惠券的领取和使用方法&#xff0c;帮助大家更好地…

【c语言】结构体的访问

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;C语言 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步&…

记录 OpenHarmony 使用 request.uploadFile 时踩的坑

​ 开发环境 设备环境&#xff1a;OpenHarmony 4.1.x SDK 版本&#xff1a;API 10 开发模型&#xff1a;Stage 模型 IDLE: Dev Eco 4.1 官方文档 踩坑一&#xff1a;后台服务地址 上传文件依赖后台服务器&#xff0c;如果使用本地搭建的服务&#xff0c;是无法访问的&…

两部电话机怎样能实现对讲?直接连接能互相通话吗?门卫门房传达室岗亭电话怎么搞?

目录 两部电话机能直接连接吗&#xff1f;用三通头分出来一条电话线两部电话机用一根电话线直接连接能互相通话吗&#xff1f; 什么电话机可以直接连接两部IP电话机&#xff08;网络电话机&#xff09;可以直接连接两部普通电话机之间通过一个电话交换机也可以连接跨区域的两部…

Avalonia中嵌入网页程序(CefNet)

Avalonia中嵌入网页程序cefNet 1. 引入CefNetNuget包2. 下载 cef 基础环境3. 将cef基础环境放入程序运行目录下4. 代码中初始化cef5. 添加Webview控件6. 在窗口关闭的时候释放Cef7. 项目结构图CefNet 开源的作者已经停止维护并删除了原始的代码库:GetHub:CefNet,Nuget上还有发…

【简单介绍下单片机】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

Python编程之旅:深入探索强大的容器——列表

在Python编程的世界中&#xff0c;容器&#xff08;Containers&#xff09;是一种用于存储多个项目的数据结构。其中&#xff0c;列表&#xff08;List&#xff09;是最常用且功能强大的容器之一。无论是初学者还是资深开发者&#xff0c;掌握列表的使用方法和技巧都是提升Pyth…

引导和服务(2)

服务 1.systemd服务的简要介绍 &#xff08;1&#xff09;对比5 6 可以解决依赖关系并行启动 &#xff08;2&#xff09;按需启动 &#xff08;3&#xff09;自动解决依赖关系 负责在系统启动或运行时&#xff0c;激活系统资源&#xff0c;服务器进程和其它进程 2.System…

Python 处理地理空间异常值:基于 MAD 的简单方法

就像任何其他数据一样,在处理地理空间数据时,识别和纠正异常值是数据准备中的关键步骤,可确保任何后续分析的准确性。异常值可能会严重扭曲空间分析的结果,从而导致错误的结论。虽然还有其他方法可以解决此问题,但处理这些异常值的一种直接有效的方法是使用中值绝对偏差 (…
最新文章