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数据劫持源码剖析

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

️不倒翁

你的鼓励就是我前进的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值