共计 7554 个字符,预计需要花费 19 分钟才能阅读完成。
什么是数据劫持?
定义: 数据劫持,指的是在拜访或者批改对象的某个属性时,通过一段代码拦挡这个行为,进行额定的操作或者批改返回后果。
简略地说,就是当咱们 触发函数的时候 动一些手脚做点咱们本人想做的事件,也就是所谓的 “ 劫持 ” 操作
数据劫持的两种计划:
- 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
来劫持对象属性的 setter
和getter
操作,并“种下”一个监听器,当数据发生变化的时候发出通知, 如下:
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.set | |
let 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.length | |
const args = new Array(i) | |
while (i--) {args[i] = arguments[i] | |
} | |
const result = original.apply(this, args)// 原始办法求值 const ob = this.__ob__// 这里 this.__ob__指向的是数据的 Observer | |
let inserted | |
switch (method) { | |
case 'push': | |
inserted = args | |
break | |
case 'unshift': | |
inserted = args | |
break | |
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.length | |
let 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()
中的 get
和set
办法劫持实现的。
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: push
、pop
、shift
、unshift
、splice
、sort
、reverse
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
的拦挡处理器除了 get
、set
外还反对多种拦挡形式。
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. 调用initComputed
将computed
属性转化为watcher
实例
2. 调用initWatch
办法,将watch
配置转化为watcher
实例
3. 调用mountComponent
办法,为render
函数绑定watcher
实例 - 第三步,状态变更后,触发
dep.notify()
函数,该函数再进一步触发Watcher
对象update
函数,执行watcher
的从新计算。
对应下图:
留神,Vue 组件中的 render
函数,咱们能够单纯将其视为一种非凡的 computed
函数,在它所对应的 Watcher
对象发生变化时,触发执行render
,生成新的 virutal-dom
构造,再交由 Vue 做diff
,更新视图。
OK 本章就到此了