Vue数据劫持详情介绍
作者:夏日 发布时间:2024-05-13 09:38:18
前言
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
中,实例化时传入的router
和store
属性便可以通过$options
获取到。
除了设置vm.$options
,_init
中还执行了initState
方法。该方法中会判断选项中传入的属性,来分别进行props
、methods
、data
、watch
、computed
等配置项的初始化操作,这里我们主要处理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);}
push
、unshift
、splice
会为数组新增元素,对于新增的元素,也要对其进行观测。这里利用到了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
: 删除对象上的一个属性。如果对象是响应式的,确保删除触发视图更新。
来源:https://zhuanlan.zhihu.com/p/341606710
猜你喜欢
- 需求背景智慧农业里有一个很重要的功能是控制温室生长环境,让农作物生长的更好。于是,我们需要在曲线上根据不同农作物设置不同的环境数据。为了方便
- Python异步编程之Asyncio1. 协程简介1.1 协程的含义及实现方法协程(Coroutine),也可以被称为微线程,是一种用户态内
- 从百度查到在django中,使用post方法时,需要先生成随机码,以防止CSRF(Cross-site request forgery)跨站
- 主键的生成方式主要有三种: 一. 数据库自动生成 二. GUID 三. 开发创建 严格讲这三种产生方式有一定的交叉点,其定位方式将在下面进行
- 1.SQL Server2019安装包下载1.1进入官网SQL Server 20191.2下载安装包1点击Continue2.填写个人信息
- 一、下载MySQL登录MySQL官网下载MSI Installer:点击“Dnownload”点击“No thanks, just star
- def bytes_to_human(n): symbols = ('K','M','G
- 实现功能:删除当前目录下,除保留目录和文件外的所有文件和目录#!bin/env pythonimport osimport os.pathi
- 通过清晰的示例和解释,本文展示了 ChatGPT 如何简化和简化创建复杂 MySQL 查询的过程,使用户更容易与数据库交互并检索他们需要的数
- -- 建立测试表和数据create table webuser (username varchar(10));insert into web
- CategoricalDtype自定义排序当我们的透视表生成完毕后,有很多情况下需要我们对某列或某行值进行排序。排序有很多种方法。例如sor
- 前言大家都知道,Sublime Text 安装插件一般从 Package Control 中直接安装即可,当我安装 node js 插件时候
- 如果是在Oracle10g之前,删除一个表空间中的数据文件后,其文件在数据库数据字典中会仍然存在,除非你删除表空间,否则文件信息不会清除。但
- 作为EBS开发人员,开发工具用的多,部署代码类型多,管理程序麻烦,操作繁琐,一直是我最讨厌的事情。部署一次程序要使用好几个工具,
- js也是可以做出狂炫的图形的,恭请超级高手分析。给大家看个例子吧。http://www.p01.org/releases/DHTML_con
- 以前用js很少用到js的正则表达式,即使用到了,也是诸如邮件名称之类的判断,网上代码很多,很少有研究,拿来即用。最近开发遇到一些需要使用正则
- 一 使用SELECT子句进行多表查询SELECT 字段名 FROM 表1,表2 …&nbs
- 目录Python协程及asyncio基础知识定义协程函数及执行方法的演变创建协程任务的演变获取协程任务执行结果通过asyncio.gathe
- python实现银行管理系统,供大家参考,具体内容如下有的地方用的方法的比较复杂,主要是为回顾更多的知识test1用来存类和函数#test1
- 偶然在Google发现了他们的用户体验设计原则,因此翻译作一下记录。1.以人为本 —他们的生活、他们的工作和他们的梦想2.珍惜每一毫秒的时间