Object.defineProperty
var arr = [1,2,3];
arr.forEach((item, index) => {
Object.defineProperty(arr, index, {
get() {
console.log('数组被getter拦截')
return item
},
set(value) {
console.log('数组被setter拦截')
return item = value
}
})
})
arr[1] = 4;
console.log(arr)
// 结果
数组被setter拦截
数组被getter拦截
4
已知长度的数组是可以通过索引属性来设置属性的访问器属性的,但数据的 add 无法拦截,因为数组所添加的索引值并没有预先加入数据拦截中。 所以 Vue 在响应式系统中对数组的方法进行了重写。
Proxy
和 Object.defineProperty 一样,Proxy 可以修改某些操作的默认行为,但是不同的是,Proxy 针对目标对象会创建一个新的实例对象,并将目标对象代理到新的实例对象上。 本质的区别是后者会创建一个新的对象对原对象做代理,外界对原对象的访问,都必须先通过这层代理进行拦截处理。而拦截的结果是我们只要通过操作新的实例对象就能间接的操作真正的目标对象了。
var arr = [1, 2, 3]
let obj = new Proxy(arr, {
get: function (target, key, receiver) {
// console.log("获取数组元素" + key);
return Reflect.get(target, key, receiver);
},
set: function (target, key, receiver) {
console.log('设置数组');
return Reflect.set(target, key, receiver);
}
})
// 1. 改变已存在索引的数据
obj[2] = 3
// result: 设置数组
// 2. push,unshift添加数据
obj.push(4)
// result: 设置数组 * 2 (索引和length属性都会触发setter)
// // 3. 直接通过索引添加数组
obj[5] = 5
// result: 设置数组 * 2
// // 4. 删除数组元素
obj.splice(1, 1)
数据初始化
数据初始化的过程就是进行响应式设计的过程
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
// 初始化props
if (opts.props) { initProps(vm, opts.props); }
// 初始化methods
if (opts.methods) { initMethods(vm, opts.methods); }
// 初始化data
if (opts.data) {
initData(vm);
} else {
// 如果没有定义data,则创建一个空对象,并设置为响应式
observe(vm._data = {}, true /* asRootData */);
}
// 初始化computed
if (opts.computed) { initComputed(vm, opts.computed); }
// 初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
initProps
function initProps (vm, propsOptions) {
var propsData = vm.$options.propsData || {};
var loop = function(key) {
···
defineReactive(props,key,value,cb);
if (!(key in vm)) {
proxy(vm, "_props", key);
}
}
// 遍历props,执行loop设置为响应式数据。
for (var key in propsOptions) loop( key );
}
响应式构建
- Observer 类:实例化一个Observer类会通过Object.defineProperty对数据的getter,setter方法进行改写,在getter阶段进行依赖的收集,在数据发生更新阶段,触发setter方法进行依赖的更新
- Watcher 类:实例化watcher类相当于创建一个依赖,简单的理解是数据在哪里被使用就需要产生了一个依赖。当数据发生改变时,会通知到每个依赖进行更新
Dep 类:收集依赖和派发更新依赖 ```javascript // 使一个对象响应化 function observify(val) { if (!isObject(val)) {
return;
}
if (Array.isArray(val)) {
observifyArray(val);
val.forEach(item => {
observify(item);
})
} else {
Object.keys(val).forEach(key => {
defineReactive(val, key, val[key]); // 遍历每个键使其响应化
})
}
}
在 defineReactive 中实例化一个 Dep 类,为每个数据都创建一个依赖的管理。
```javascript
// 为对象的一个键应用 Object.defineProperty
function defineReactive(obj, key, value) {
//为每个键都创建一个 dep
let dep = new Dep();
observify(value); // 递归
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if(Dep.target) {
dep.depend();
}
return value;
},
set: (newVal) => {
if (newVal === value) {
return;
}
value = newValue;
observify(newValue);
// 通知该数据收集的watcher依赖,遍历每个watcher进行数据更新,
dep.notify(newVal, oldVal);
}
})
}
// Dep 类,保存数据源的所有订阅,并在接收到数据源的变动通知后,触发所有订阅
let uid = 1;
class Dep {
constructor() {
this.id = uid++; // 为每个 dep 标记一个 uid
this.subs = [];
}
// 添加订阅
addSub(sub) {
this.subs.push(sub);
}
// 依赖收集函数,Dep.target为当前执行的watcher
// 在 getter 中执行,在 Dep.target 上找到当前 watcher,并添加依赖
depend() {
Dep.target && Dep.target.addDep(this);
}
notify(newVal, oldVal) {
this.subs.forEach(sub => {
sub.update(newVal, oldVal); // 触发订阅
})
}
}
// Dep.target 用来暂存正在收集依赖的当前 watcher
Dep.target = null;
// Watcher 类,每个 Watcher 为一个订阅源
class Watcher {
/**
*
* @param {*} expFn 依赖收集函数
* @param {*} cb 回调
* @param {*} options 附加配置
*/
constructor(expFn, cb, options = {}) {
this.context = options.context
this.expFn = expFn
this.depIds = new Set() //标记当前 watcher 已经加入到了哪些 dep
this.cb = cb
this.value = this.subAndGetValue()
this.clonedOldValue = _.cloneDeep(this.value)
}
// 执行回调
update() {
let value = this.subAndGetValue() //获取 newValue
if(!_.isEqual(value, this.clonedOldValue)) { // 比对前后两次值是否相等时借助一下 lodash 中的 isEqual 函数进行比较
this.value = value
this.cb.call(this.context, value, this.clonedOldValue)
this.clonedOldValue = _.cloneDeep(value) // 缓存本次结果,会成为下次的 oldValue, 对于对象使用深拷贝
}
}
//执行依赖收集函数,订阅依赖!
subAndGetValue() {
Dep.target = this // 把当前 watcher 放到 Dep.target 上,这样 getter 就知道应该把哪个 watcher 加入 dep 中了。
let value = this.expFn.call(this.context)
Dep.target = null // 订阅完置回空。
return value
}
// 在 dep 上添加订阅
// 为每个数据依赖收集器添加需要被监听的watcher
addDep(dep) {
if(!this.depIds.has(dep.id)) { //防止重复订阅,防止在一个 dep 中订阅两次
this.depIds.add(dep.id)
dep.addSub(this)
}
}
}
//简单封装下 new Watcher
function watch(expFn, cb, context) {
return new Watcher(expFn, cb, context)
}