Vue数据劫持详情介绍

目录
  • 前言
  • 创建Vue实例
  • 数据观测存在的问题
  • 结语

前言

Vue会对我们在data中传入的数据进行拦截:

  • 对象:递归的为对象的每个属性都设置get/set方法
  • 数组:修改数组的原型方法,对于会修改原数组的方法进行了重写

在用户为data中的对象设置值、修改值以及调用修改原数组的方法时,都可以添加一些逻辑来进行处理,实现数据更新页面也同时更新。

Vue中的响应式(reactive): 对对象属性或数组方法进行了拦截,在属性或数组更新时可以同时自动地更新视图。在代码中被观测过的数据具有响应性

创建Vue实例

我们先让代码实现下面的功能:

<body>
<script>
  const vm = new Vue({
    el: '#app',
    data () {
      return {
        age: 18
      };
    }
  });
  // 会触发age属性对应的set方法
  vm.age = 20;
  // 会触发age属性对应的get方法
  console.log(vm.age);
</script>
</body>

src/index.js中,定义Vue的构造函数。用户用到的Vue就是在这里导出的Vue:

import initMixin from './init';

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

// 进行原型方法扩展
initMixin(Vue);
export default Vue;

init中,会定义原型上的_init方法,并进行状态的初始化:

import initState from './state';

function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    const vm = this;
    // 将用户传入的选项放到vm.$options上,之后可以很方便的通过实例vm来访问所有实例化时传入的选项
    vm.$options = options;
    initState(vm);
  };
}
export default initMixin;

_init方法中,所有的options被放到了vm.$options中,这不仅让之后代码中可以更方便的来获取用户传入的配置项,也可以让用户通过这个api来获取实例化时传入的一些自定义选选项。比如在Vuex 和Vue-Router中,实例化时传入的routerstore属性便可以通过$options获取到。

除了设置vm.$options_init中还执行了initState方法。该方法中会判断选项中传入的属性,来分别进行propsmethodsdatawatchcomputed 等配置项的初始化操作,这里我们主要处理data选项:

import { observe } from './observer';
import { proxy } from './shared/utils';

function initState (vm) {
  const options = vm.$options;
  if (options.props) {
    initProps(vm);
  }
  if (options.methods) {
    initMethods(vm);
  }
  if (options.data) {
    initData(vm);
  }
  if (options.computed) {
    initComputed(vm)
  }
  if (options.watch) {
    initWatch(vm)
  }
}

function initData (vm) {
  let data = vm.$options.data;
  vm._data = data = typeof data === 'function' ? data.call(vm) : data;
  // 对data中的数据进行拦截
  observe(data);
  // 将data中的属性代理到vm上
  for (const key in data) {
    if (data.hasOwnProperty(key)) {
      // 为vm代理所有data中的属性,可以直接通过vm.xxx来进行获取
      proxy(vm, key, data);
    }
  }
}
export default initState;

initData中进行了如下操作:

  • data可能是对象或函数,这里将data统一处理为对象
  • 观测data中的数据,为所有对象属性添加set/get方法,重写数组的原型链方法
  • data中的属性代理到vm上,方便用户直接通过实例vm来访问对应的值,而不是通过vm._data来访问

新建src/observer/index.js,在这里书写observe函数的逻辑:

function observe (data) {
  // 如果是对象,会遍历对象中的每一个元素
  if (typeof data === 'object' && data !== null) {
    // 已经观测过的值不再处理
    if (data.__ob__) {
      return;
    }
    new Observer(data);
  }
}
export { observe };

observe函数中会过滤data中的数据,只对对象和数组进行处理,真正的处理逻辑在Observer中:

/**
 * 为data中的所有对象设置`set/get`方法
 */
class Observer {
  constructor (value) {
    this.value = value;
    // 为data中的每一个对象和数组都添加__ob__属性,方便直接可以通过data中的属性来直接调用Observer实例上的属性和方法
    defineProperty(this.value, '__ob__', this);
    // 这里会对数组和对象进行单独处理,因为为数组中的每一个索引都设置get/set方法性能消耗比较大
    if (Array.isArray(value)) {
      Object.setPrototypeOf(value, arrayProtoCopy);
      this.observeArray(value);
    } else {
      this.walk();
    }
  }

  walk () {
    for (const key in this.value) {
      if (this.value.hasOwnProperty(key)) {
        defineReactive(this.value, key);
      }
    }
  }

  observeArray (value) {
    for (let i = 0; i < value.length; i++) {
      observe(value[i]);
    }
  }
}

需要注意的是,__ob__属性要设置为不可枚举,否则之后在对象遍历时可能会引发死循环

Observer类中会为对象和数组都添加__ob__属性,之后便可以直接通过data中的对象和数组vm.value.__ob__来获取到Observer实例。

当传入的value为数组时,由于观测数组的每一个索引会耗费比较大的性能,并且在实际使用中,我们可能只会操作数组的第一项和最后一项,即arr[0],arr[arr.length-1],很少会写出arr[23] = xxx的代码。

所以我们选择对数组的方法进行重写,将数组的原型指向继承Array.prototype新创建的对象arrayProtoCopy,对数组中的每一项继续进行观测。

创建data中数组原型的逻辑在src/observer/array.js中:

// if (Array.isArray(value)) {
//    Object.setPrototypeOf(value, arrayProtoCopy);
//    this.observeArray();
// }
const arrayProto = Array.prototype;
export const arrayProtoCopy = Object.create(arrayProto);

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

methods.forEach(method => {
  arrayProtoCopy[method] = function (...args) {
    const result = arrayProto[method].apply(this, args);
    console.log('change array value');
    // data中的数组会调用这里定义的方法,this指向该数组
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice': // splice(index,deleteCount,item1,item2)
        inserted = args.slice(2);
        break;
    }
    if (inserted) {ob.observeArray(inserted);}
    return result;
  };
});

通过Object.create方法,可以创建一个原型为Array.prototype的新对象arrayProtoCopy。修改原数组的7个方法会设置为新对象的私有属性,并且在执行时会调用arrayProto 上对应的方法。

在这样处理之后,便可以在arrayProto中的方法执行前后添加自己的逻辑,而除了这7个方法外的其它方法,会根据原型链,使用arrayProto上的对应方法,并不会有任何额外的处理。

在修改原数组的方法中,添加了如下的额外逻辑:

const ob = this.__ob__;
let inserted;
switch (method) {
  case 'push':
  case 'unshift':
    inserted = args;
    break;
  case 'splice': // splice(index,deleteCount,item1,item2)
    inserted = args.slice(2);
    break;
}
if (inserted) {ob.observeArray(inserted);}

pushunshiftsplice会为数组新增元素,对于新增的元素,也要对其进行观测。这里利用到了Observer中为数组添加的__ob__属性,来直接调用ob.observeArray ,对数组中新增的元素继续进行观测。

对于对象,要遍历对象的每一个属性,来为其添加set/get方法。如果对象的属性依旧是对象,会对其进行递归处理

function defineReactive (target, key) {
  let value = target[key];
  // 继续对value进行监听,如果value还是对象的话,会继续new Observer,执行defineProperty来为其设置get/set方法
  // 否则会在observe方法中什么都不做
  observe(value);
  Object.defineProperty(target, key, {
    get () {
      console.log('get value');
      return value;
    },
    set (newValue) {
      if (newValue !== value) {
        // 新加的元素也可能是对象,继续为新加对象的属性设置get/set方法
        observe(newValue);
        // 这样写会新将value指向一个新的值,而不会影响target[key]
        console.log('set value');
        value = newValue;
      }
    }
  });
}

class Observer {
  constructor (value) {
    // some code ...
    if (Array.isArray(value)) {
      // some code ...
    } else {
      this.walk();
    }
  }

  walk () {
    for (const key in this.value) {
      if (this.value.hasOwnProperty(key)) {
        defineReactive(this.value, key);
      }
    }
  }

  // some code ...
}

数据观测存在的问题

检测变化的注意事项

我们先创建一个简单的例子:

const mv = new Vue({
  data () {
    return {
      arr: [1, 2, 3],
      person: {
        name: 'zs',
        age: 20
      }
    }
  }
})

对于对象,我们只是拦截了它的取值和赋值操作,添加值和删除值并不会进行拦截:

vm.person.school = '北大'
delete vm.person.age

而对于数组,用索引修改值以及修改数组长度不会被观测到:

vm.arr[0] = 0
vm.arr.length--

为了能处理上述的情况,Vue为用户提供了$set$delete方法:

  • $set: 为响应式对象添加一个属性,确保新属性也是响应式的,因此会触发视图更新
  • $delete: 删除对象上的一个属性。如果对象是响应式的,确保删除触发视图更新。

结语

通过实现Vue的数据劫持,将会对Vue的数据初始化和响应式有更深的认识。

在工作中,我们可能总是会疑惑,为什么我更新了值,但是页面没有发生变化?现在我们可以从源码的角度进行理解,从而更清楚的知道代码中存在的问题以及如何解决和避免这些问题。

源代码: 传送门

到此这篇关于Vue数据劫持详情介绍的文章就介绍到这了,更多相关Vue数据劫持内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解vue的数据劫持以及操作数组的坑

    TL;DR 给data添加新属性的时候vm.$set(vm.info,'newKey','newValue') data上面属性值是数组的时候,需要用数组的方法操作数组,而不能通过index或者length属性去操作数组,因为监听不到属性操作的动作. 安装和初使用vue vue是构建用户界面的渐进式框架.所谓的渐进式:vue + components + vue-router + vuex + vue-cli可以根据需要选择相应的功能. 来串命令mkdir vue-apply;cd vue-ap

  • 深入浅出 Vue 系列 -- 数据劫持实现原理

    一.前言 数据双向绑定作为 Vue 核心功能之一,其实现原理主要分为两部分: 数据劫持 发布订阅模式 本篇文章主要介绍 Vue 实现数据劫持的思路,下一篇则会介绍发布订阅模式的设计. 二.针对 Object 类型的劫持 对于 Object 类型,主要劫持其属性的读取与设置操作.在 JavaScript 中对象的属性主要由一个字符串类型的"名称"以及一个"属性描述符"组成,属性描述符包括以下选项: value: 该属性的值: writable: 仅当值为 true 时

  • 手写Vue源码之数据劫持示例详解

    源代码: 传送门 Vue会对我们在data中传入的数据进行拦截: 对象:递归的为对象的每个属性都设置get/set方法 数组:修改数组的原型方法,对于会修改原数组的方法进行了重写 在用户为data中的对象设置值.修改值以及调用修改原数组的方法时,都可以添加一些逻辑来进行处理,实现数据更新页面也同时更新. Vue中的响应式(reactive): 对对象属性或数组方法进行了拦截,在属性或数组更新时可以同时自动地更新视图.在代码中被观测过的数据具有响应性 创建Vue实例 我们先让代码实现下面的功能:

  • 3分钟了解vue数据劫持的原理实现

    目的: 了解Object.defineProperty如何实现数据劫持 大致原理是这样的: 定义一个监听函数,对对象的每一个属性进行监听 通过Object.defineProperty对监听的每一个属性设置get 和 set 方法. 对对象实行监听 对对象内嵌对象进行处理 对数组对象进行处理 1. 先定义一个对象 let obj = { name: 'jw' } 2. 定义一个监听函数 /** * 判断监听的是否是对象 * 如果是对象,就遍历,并且对每个属性进行定义get 和 set */ fu

  • 手写Vue2.0 数据劫持的示例

    一:搭建webpack 简单的搭建一下webpack的配置.新建一个文件夹,然后init一下.之后新建一个webpack.config.js文件,这是webpack的配置文件.安装一下简单的依赖. npm install webpack webpack-cli webpack-dev-server -D 在同级目录下新建一个public/index.html和src/index.js,作为出口文件和入口文件. j简单配置一下webpack, 在webpack.config.js文件中: cons

  • vue MVVM双向绑定实例详解(数据劫持+发布者-订阅者模式)

    目录 实现过程 1.实现一个Observer 2.实现Watcher 3.实现Compile 总结 参考文献:https://www.jb51.net/article/160654.htm https://www.jb51.net/article/239554.htm MVVM拆开来即为Model-View-ViewModel,有View,ViewModel,Model三部分组成.View层代表的是视图.模版,负责将数据模型转化为UI展现出来.Model层代表的是模型.数据,可以在Model层中

  • Vue数据劫持详情介绍

    目录 前言 创建Vue实例 数据观测存在的问题 结语 前言 Vue会对我们在data中传入的数据进行拦截: 对象:递归的为对象的每个属性都设置get/set方法 数组:修改数组的原型方法,对于会修改原数组的方法进行了重写 在用户为data中的对象设置值.修改值以及调用修改原数组的方法时,都可以添加一些逻辑来进行处理,实现数据更新页面也同时更新. Vue中的响应式(reactive): 对对象属性或数组方法进行了拦截,在属性或数组更新时可以同时自动地更新视图.在代码中被观测过的数据具有响应性 创建

  • 微信小程序数据劫持代理的实现

    index.html index.js // Vue数据劫持代理 //模拟Vue中data选项 let data = { username:'小镭', age:3 } // 模拟组件的实例 let _this={} // 利用object.defineProperty() for(let item in data){ console.log(item,data[item]); Object.defineProperty(_this,item,{ // get作用:用来获取扩展属性值,当获取该属性

  • Vue数据增删改查与表单验证的实现流程介绍

    目录 1. 准备工作 2. 弹出窗口 3. 新增更新功能 4. 删除功能 5. 表单验证 6. 接口文档 1. 准备工作 后台服务接口,对书本的增删改查操作 2. 弹出窗口 进入ElementUi官网, 找到Dialog对话框,可以参考“嵌套表单的dialog”实现. 该步骤先实现弹出窗口的前端逻辑,并不会调用后台接口服务进行实际的业务操作. BookList.vue <!-- 弹出窗口:增加和修改书本信息共用一个弹出窗口,需要根据用户的选择动态的设置弹出窗口的标题 :tile 通过绑定值的方式

  • Vue数据变化后页面更新详细介绍

    首先会通过module.hot.accept监听文件变化,并传入该文件的渲染函数: module.hot.accept(/*! ./App.vue?vue&type=template&id=472cff63&scoped=true& */ "./App.vue?vue&type=template&id=472cff63&scoped=true&", __WEBPACK_OUTDATED_DEPENDENCIES__ =&g

随机推荐