定义: 数据劫持,指的是在访问或者修改对象的某个属性时,经过一段代码拦截这个行为,进行额外的操做或者修改返回结果。react
简单地说,就是当咱们 触发函数的时候 动一些手脚作点咱们本身想作的事情,也就是所谓的 "劫持"操做git
Object.defineProperty(obj,prop,descriptor)
数组
参数:缓存
可供定义的特性列表:bash
在Vue中其实就是经过Object.defineProperty
来劫持对象属性的setter
和getter
操做,并“种下”一个监听器,当数据发生变化的时候发出通知,以下:app
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
这个方法,具体用在哪里而且它又解决了哪些问题,下面就简单的说一下:dom
它经过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()
的缺点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
等,它们都不会修改原始数组,而会返回一个新的数组。
使用 Object.defineProperty()
多数要配合 Object.keys()
和遍历,因而多了一层嵌套。如:
Object.keys(obj).forEach(key => {
Object.defineProperty(obj, key, {
// ...
})
})
复制代码
所谓的嵌套对象,是指相似
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
在数据劫持这个问题上,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
:initComputed
将 computed
属性转化为 watcher
实例initWatch
方法,将watch
配置转化为 watcher
实例mountComponent
方法,为 render
函数绑定 watcher
实例dep.notify()
函数,该函数再进一步触发 Watcher
对象 update
函数,执行watcher
的从新计算。对应下图:
注意,Vue 组件中的 render
函数,咱们能够单纯将其视为一种特殊的 computed
函数,在它所对应的 Watcher
对象发生变化时,触发执行render
,生成新的 virutal-dom
结构,再交由 Vue 作diff
,更新视图。
OK 本章就到此了!