什么是数据劫持?

定义: 数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。

简单地说,就是当我们 触发函数的时候 动一些手脚做点我们自己想做的事情,也就是所谓的 "劫持"操作

数据劫持的两种方案:

  • Object.defineProperty
  • Proxy

1).Object.defineProperty

  • 语法:

Object.defineProperty(obj,prop,descriptor)

  • 参数:

    • obj:目标对象
    • prop:需要定义的属性或方法的名称
    • descriptor:目标属性所拥有的特性
  • 可供定义的特性列表:

    • value:属性的值
    • writable:如果为false,属性的值就不能被重写。
    • get: 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。
    • set:一旦目标属性被赋值,就会调回此方法。
    • configurable:如果为false,则任何尝试删除目标属性或修改属性性以下特性(writable, configurable, enumerable)的行为将被无效化。
    • enumerable:是否能在for…in循环中遍历出来或在Object.keys中列举出来。

例子

在Vue中其实就是通过Object.defineProperty来劫持对象属性的settergetter操作,并“种下”一个监听器,当数据发生变化的时候发出通知,如下:

var data = {name:'test'}
Object.keys(data).forEach(function(key){Object.defineProperty(data,key,{enumerable:true,configurable:true,get:function(){console.log('get');},set:function(){console.log('监听到数据发生了变化');}})
});
data.name //控制台会打印出 “get”
data.name = 'hxx' //控制台会打印出 "监听到数据发生了变化"

上面的这个例子可以看出,我们完全可以控制对象属性的设置和读取。在Vue中,在很多地方都非常巧妙的运用了Object.defineProperty这个方法,具体用在哪里并且它又解决了哪些问题,下面就简单的说一下:

监听对象属性的变化

它通过observe每个对象的属性,添加到订阅器dep中,当数据发生变化的时候发出一个notice。 相关源代码如下:(作者采用的是ES6+flow写的,代码在src/core/observer/index.js模块里面)

export function defineReactive (obj: Object,key: string,val: any,customSetter?: Function
) {const dep = new Dep()//创建订阅对象  const property = Object.getOwnPropertyDe述  //属性的描述特性里面如果configurable为false则属性的任何修改将无效  if (property && property.configurable === false) { return }scriptor(obj, key)//获取obj对象的key属性的描// cater for pre-defined getter/setters  const getter = property && property.get  const setter = property && property.setlet childOb = observe(val)//创建一个观察者对象  Object.defineProperty(obj, key, {enumerable: true,//可枚举    configurable: true,//可修改    get: function reactiveGetter () {const value = getter ? getter.call(obj) : val//先调用默认的get方法取值      //这里就劫持了get方法,也是作者一个巧妙设计,在创建watcher实例的时候,通过调用对象的get方法往订阅器dep上添加这个创建的watcher实例      if (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend()}if (Array.isArray(value)) {dependArray(value)}}return value//返回属性值    },set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val//先取旧值      if (newVal === value) { return }//这个是用来判断生产环境的,可以无视      if (process.env.NODE_ENV !== 'production' && customSetter) {customSetter()}if (setter) {setter.call(obj, newVal)} else {val = newVal}childOb = observe(newVal)//继续监听新的属性值      dep.notify()//这个是真正劫持的目的,要对订阅者发通知了    }})
}

参考 前端进阶面试题详细解答

以上是Vue监听对象属性的变化,那么问题来了,我们经常在传递数据的时候往往不是一个对象,很有可能是一个数组,那是不是就没有办法了呢,答案显然是否则的。那么下面就看看作者是如何监听数组的变化:

监听数组的变化

看代码:

const arrayProto = Array.prototype//原生Array的原型
export const arrayMethods = Object.create(arrayProto);
['push','pop','shift','unshift','splice','sort','reverse']
.forEach(function (method) {const original = arrayProto[method]//缓存元素数组原型  //这里重写了数组的几个原型方法  def(arrayMethods, method, function mutator () {//这里备份一份参数应该是从性能方面的考虑    let i = arguments.lengthconst args = new Array(i)while (i--) {args[i] = arguments[i]}const result = original.apply(this, args)//原始方法求值    const ob = this.__ob__//这里this.__ob__指向的是数据的Observer    let insertedswitch (method) {case 'push':inserted = argsbreak      case 'unshift':inserted = argsbreak      case 'splice':inserted = args.slice(2)break    }if (inserted) ob.observeArray(inserted)// notify change    ob.dep.notify()return result})
})...//定义属性
function def (obj, key, val, enumerable) {Object.defineProperty(obj, key, {value: val,enumerable: !!enumerable,writable: true,configurable: true  });
}

上面的代码主要是继承了Array本身的原型方法,然后又做了劫持修改,可以发出通知。Vue在observer数据阶段会判断如果是数组的话,则修改数组的原型,这样的话,后面对数组的任何操作都可以在劫持的过程中控制。结合Vue的思想,简单的写个小demo方便更好的理解:

let arrayMethod = Object.create(Array.prototype);
['push','shift'].forEach(function(method){Object.defineProperty(arrayMethod,method,{value:function(){let i = arguments.lengthlet args = new Array(i)while (i--) {args[i] = arguments[i]}let original = Array.prototype[method];let result = original.apply(this,args);console.log("已经控制了,哈哈");return result;},enumerable: true,writable: true,configurable: true    })
})
let bar = [1,2];
bar.__proto__ = arrayMethod;
bar.push(3);//控制台会打印出 “已经控制了,哈哈”;并且bar里面已经成功的添加了成员 ‘3’

整个过程看起来好像没有什么问题,似乎Vue已经做到了完美,其实不然,Vue还是不能检测到数据项和数组长度改变的变化,例如下面的调用:

vm.items[index] = "xxx";
vm.items.length = 100;

所以我们尽量避免这样的调用方式,如果确实需要,作者也帮我们实现了一个$set操作,下去自己了解

实现对象属性代理

正常情况下我们是这样实例化一个Vue对象:

var VM = new Vue({ data:{ name:'lhl' }, el:'#id'})

按理说我们操作数据的时候应该是VM.data.name = ‘hxx’才对,但是作者觉得这样不够简洁,所以又通过代理的方式实现了VM.name = ‘hxx’的可能。 相关代码如下:

function proxy (vm, key) {if (!isReserved(key)) {Object.defineProperty(vm, key, {configurable: true,enumerable: true,get: function proxyGetter () {return vm._data[key]},set: function proxySetter (val) {vm._data[key] = val;}});}
}

表面上看起来我们是在操作VM.name,实际上还是通过Object.defineProperty()中的getset方法劫持实现的。

Object.defineProperty()的缺点

1).不能监听数组的变化

let arr = [1,2,3]
let obj = {}
Object.defineProperty(obj, 'arr', {get () {console.log('get arr')return arr},set (newVal) {console.log('set', newVal)arr = newVal}
})
obj.arr.push(4) // 只会打印 get arr, 不会打印 set
obj.arr = [1,2,3,4] // 这个能正常 set

数组的以下几个方法不会触发 set: pushpopshiftunshiftsplicesortreverse

Vue 把这些方法定义为变异方法 (mutation method),指的是会修改原来数组的方法。与之对应则是非变异方法 (non-mutating method),例如 filter, concat, slice 等,它们都不会修改原始数组,而会返回一个新的数组。

2).必须遍历对象的每个属性

使用 Object.defineProperty() 多数要配合 Object.keys() 和遍历,于是多了一层嵌套。如:

Object.keys(obj).forEach(key => {Object.defineProperty(obj, key, {// ...})
})

3).必须深层遍历嵌套的对象

所谓的嵌套对象,是指类似

let obj = {info: {name: 'eason'}
}

如果是这一类嵌套对象,那就必须逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。

给出完整版的数据劫持代码:

const arrayProto = Array.prototype;// 得到原型上的方法
const proto = Object.create(arrayProto)  // 复制一份原型上的方法;['push', 'shift', 'pop', 'splice'].forEach(method => {// console.log(method)// 重写'push','shift','pop','splice',当然也可以多加几个方法,想加什么就加什么proto[method] = function (...args) {// console.log(this)  // [ 1, 2, 3, { age: [Getter/Setter] } ]updateView();arrayProto[method].call(this, ...args)}})
function updateView() {console.log("更新视图成功了...")
}
function observer(obj) {if (typeof obj !== "object" || obj == null) {return obj}if (Array.isArray(obj)) {// 如果是一个数组要重写数组上原型上的方法 Object.setPrototypeOf(obj, proto)for (let i = 0; i < obj.length; i++) {let item = obj[i];observer(item)}} else {for (let key in obj) {definedReactive(obj, key, obj[key])}}
}function definedReactive(obj, key, value) {observer(value)Object.defineProperty(obj, key, {get() {console.log("获取数据成功了...")return value;},set(newValue) {if (value !== newValue) {observer(newValue)value = newValue;updateView();}}})
}let data = { name: [1, 2, 3, { age: 888 }] }
observer(data)
// 数据改变了
// data.name[3].age = 666;
// push shift unshift pop 也能改变数组中的数组
data.name.push({ address: "xxx" })  // 目的是:更新视图// 思路:重写Push方法  这些方法在Array的原型上
//      不要把Array原型上的方法直接重写了
//      先把原型上的方法copy一份,去重写(加上视图更新的操作)
//      再去调用最原始的push方法

接下来说一下Object.defineProperty()的升级版 Proxy

2).Proxy数据代理

在数据劫持这个问题上,Proxy 可以被认为是 Object.defineProperty() 的升级版。外界对某个对象的访问,都必须经过这层拦截。因此它是针对 整个对象,而不是 对象的某个属性。

proxy即代理的意思。个人理解,建立一个proxy代理对象(Proxy的实例),接受你要监听的对象和监听它的handle两个参数。当你要监听的对象发生任何改变,都会被proxy代理拦截来满足需求。

var arr = [1,2,3]
var handle = {//target目标对象 key属性名 receiver实际接受的对象get(target,key,receiver) {console.log(`get ${key}`)// Reflect相当于映射到目标对象上return Reflect.get(target,key,receiver)},set(target,key,value,receiver) {console.log(`set ${key}`)return Reflect.set(target,key,value,receiver)}
}
//arr要拦截的对象,handle定义拦截行为
var proxy = new Proxy(arr,handle)
proxy.push(4) //可以翻到控制台测试一下会打印出什么

优点:
1.使用proxy可以解决defineProperty不能监听数组的问题,避免重写数组方法;
2.不需要再遍历key
3.Proxy handle的拦截处理器除了getset外还支持多种拦截方式。
4.嵌套查询。实际上proxy get()也是不支持嵌套查询的。解决方法:

let handler = {get (target, key, receiver) {// 递归创建并返回if (typeof target[key] === 'object' && target[key] !== null) {return new Proxy(target[key], handler)}return Reflect.get(target, key, receiver)}
}

依赖管理方案

说完了上面的,简单说一下 依赖管理方案

Object.defineProperty 只是解决了状态变更后,如何触发通知的问题,那要通知谁呢?谁会关心那些属性发生了变化呢?在 Vue 中,使用 Dep 解耦了依赖者与被依赖者之间关系的确定过程。简单来说:

  • 第一步,通过 Observer 提供的接口,遍历状态对象,给对象的每个属性、子属性都绑定了一个专用的 Dep 对象。这里的状态对象主要指组件当中的data属性。
  • 第二步,创建三中类型的watcher
    1.调用 initComputedcomputed 属性转化为 watcher 实例
    2.调用 initWatch 方法,将 watch 配置转化为 watcher 实例
    3.调用 mountComponent 方法,为 render 函数绑定 watcher 实例
  • 第三步,状态变更后,触发 dep.notify() 函数,该函数再进一步触发 Watcher 对象 update 函数,执行watcher的重新计算。

对应下图:

注意,Vue 组件中的 render 函数,我们可以单纯将其视为一种特殊的 computed 函数,在它所对应的 Watcher 对象发生变化时,触发执行render,生成新的 virutal-dom 结构,再交由 Vue 做diff,更新视图。

OK 本章就到此了

每日一题之Vue数据劫持原理是什么?相关推荐

  1. vue 数据劫持详解

    2019独角兽企业重金招聘Python工程师标准>>> 首先 Object.defineProperty(obj,prop,descriptor) 用法介绍: 参数 obj:目标对象 ...

  2. JS每日一题:new Vue()中发生了什么?

    20190214问 new Vue()中发生了什么? 先从语法上分析,new关键字在js语言中代表实例化一个对象, 而Vue实际上是一个类, 我们简单看一下源码 源码地址 https://github ...

  3. 探讨Vue 数据监测原理-第四节-Vue.Set() API 介绍应用

    文章目录 探讨一下Vue 数据监测的原理 本次探讨共的第四个章节 第四节:Vue.Set() API 介绍应用 1. 案例 2. 需求1 给学生添加性别 2.1 获取对象添加性别 2.2. 添加响应式 ...

  4. 探讨Vue 数据监测原理-第五节-展开介绍 Vue中监测-【数组】数据的原理

    文章目录 探讨一下Vue 数据监测的原理 本次探讨共的第五个章节 第五节:展开介绍 Vue中监测-[数组]数据的原理 1. 案例 1.1. 页面案例 2. 数组形式|对象形式爱好的区别 2.1. 页面 ...

  5. 【重学Vue】数据响应原理真的是双向绑定吗?

    最近 Ant Design Vue 作者 - 唐金州,在某平台开课了,在整个课程中系统的讲述了Vue的开发实战.在第八讲中介绍了Vue双向绑定的问题,这里我整理一些资料客观的分析一下 Vue数据响应原 ...

  6. java -- 每日一题

    Hashtable源码解析 Java面试每日一题 -- 动态代理两种方式及比较 equals与==的区别 Java面试每日一题 -- Object有哪些公用方法? Java面试每日一题 -- Java ...

  7. 微信小程序——数据劫持代理

    index.html index.js // Vue数据劫持代理//模拟Vue中data选项 let data = {username:'小镭',age:3 }// 模拟组件的实例 let _this ...

  8. 前端面试题 HTML5 CSS3(盒子模型、盒子水平垂直居中、经典布局) JS(闭包、深浅克隆、数据劫持和拦截) 算法(排序、去重、数组扁平化) Vue(双向数据绑定原理、通信方式)

    前端面试题 HTML5 相关面试题 CSS3 相关面试题 盒子模型 盒子水平垂直居中的方案 经典布局方案 圣杯布局 双飞翼布局 flex布局 定位方式布局 css实现三角形 JS 相关面试题 8种数据 ...

  9. this指向-作用域、作用域链-预解析 变量提升-Vue组件传值 父子 子父 非父子-Vue数据双向绑定原理

    目录 this指向 作用域.作用域链 预解析 变量提升 Vue组件传值 父子 子父 非父子 Vue数据双向绑定原理 1.this指向 函数的this指向 看调用.不看声明 (1)普通函数调用 ①函数名 ...

最新文章

  1. hdu 2602 Bone Collector 01背包
  2. React Native 红屏之Could not connect to development server.
  3. 如何识别一个指针式的时种的时间?
  4. 云中计算将软件服务外包带入3.0时代
  5. 艾伟:WCF从理论到实践(2):决战紫禁之巅
  6. 【报告分享】2019中国社交电商白皮书.pdf
  7. 百度成立互联网医院;钉钉招小学生产品体验师;iOS 13.4 上线 | 极客头条
  8. EIGRP特性试验笔记
  9. adminlte java_AdminLTE Button小结
  10. IDEA插件-Translation提示更新TKK失败,配置有道翻译解决
  11. sniffer超级详细介绍
  12. 机房合作—SVN skipped remains conflicted
  13. JDBC学习总结及复习笔记(附MySQL驱动和JDK-API中文版)
  14. 循环、推导式、多重循环
  15. ivms虚拟服务器,ivms监控服务器地址
  16. 2010水瓶座年运势
  17. Java并发编程系列18:多线程之生产者和消费者模式_信号灯法(wait/notify通知机制)
  18. 计算机网络智能小区综合分布线系统的总结,智能住宅小区综合布线系统的构建...
  19. 2018中国互联网企业100强,有你想进的吗?
  20. 书呆子rico_Excel书呆子的夏季赠品

热门文章

  1. 校招产品经理面经篇四
  2. python正则匹配括号内任意字符,python 正则匹配 获取括号内字符
  3. 用原生JS写一个网页版的2048小游戏(兼容移动端)
  4. 中国建设银行宣布:国内第一家无人银行,在上海正式开业!
  5. 想搞机器学习,不会特征工程?你TM逗我那!
  6. 成都java培训一般需要多久
  7. 基于javaweb+jsp的家庭理财系统(java+SSM+JSP+Tomcat8+Mysql)
  8. 《银翼杀手》画面布局赏析
  9. [5 算法] 31. 了解各种排序选择(partition,stable_partition,nth_element,partial_sort,sort,stable_sort)
  10. 手机网页调用手机QQ QQ在线客服