续上文—:有关vue源码的简单实现 实现一个属于自己的min-vue
本文实现:
计算属性:依赖的值发生改变 才会重新执行用户的方法 计算属性需要维护一个dirty属性。而且在默认情况下,计算属性不会立刻执行,而是在用户取值的时候才会执行。
计算属性使用的两种方式:
computed: {
/**
* 计算属性:依赖的值发生改变 才会重新执行用户的方法 计算属性需要维护一个dirty属性
*/
// 只有get的计算属性
fullName1() {
return this.firstName + " " + this.lastName
},
// getter and setter
fullName2: {
get() {
return this.firstName + " " + this.lastName
},
set(newVal) {
[this.firstName, this.lastName] = newVal.split(" ")
}
}
}
特点:
/**
* 初始化 computed
* @param {Vue} vm 实例
*/
function initComputed(vm) {
const computed = vm.$options.computed;
const watchers = (vm._computedWatchers = {});
for (const key in computed) {
const userDef = computed[key];
// function -> get
// object -> {get(){}, set(newVal){}}
let setter;
const getter = isFunction(userDef)
? userDef
: ((setter = userDef.set), getter);
// 监控计算属性中 get的变化
// 每次data的属性发生改变 重新执行的就是这个get
// 传入额外的配置项 标明当前的函数 不需要立刻执行 只有在使用到计算属性了 才计算值
// 把属性和watcher对应起来
watchers[key] = new Watcher(vm, getter, { lazy: true });
// 劫持每一个计算属性
defineComputed(vm, key, setter);
}
}
/**
* 定义计算属性
* @param {*} target
* @param {*} key
* @param {*} setter
*/
function defineComputed(target, key, setter) {
Object.defineProperty(target, key, {
// vm.key -> vm.get key this -> vm
get: createComputedGetter(key),
set: setter,
});
}
/**
* vue2.x 的计算属性 不会收集依赖,只是让计算属性依赖的属性去收集依赖
* 创建一个懒执行(有缓存的)计算属性 判断值是否发生改变
* 检查是否需要执行这个getter
* @param {string} key
*/
function createComputedGetter(key) {
// this -> vm 因为返回值给了计算属性的 get 我们是从 vm上取计算属性的
return function lazyGetter() {
// 对应属性的watcher
const watcher = this._computedWatchers[key];
if (watcher.dirty) {
// 如果是脏的 就去执行用户传入的getter函数 watcher.get()
// 但是为了可以拿到get的执行结果 我们调用 evaluate函数
watcher.evaluate(); // dirty = false
}
// 计算属性watcher出栈后 还有渲染watcher(在视图中使用了计算属性)
// 或者说是在其他的watcher中使用了计算属性
if (Dep.target) {
// 让计算属性的watcher依赖的变量也去收集上层的watcher
watcher.depend();
}
return watcher.value;
};
}
watch选项是一个对象,每个watch的属性作为键,
如果watch的属性直接是一个函数,那么会在属性值发生改变后,给该函数传入两个参数,新值和旧值。
// 就是一个观察者
firstName(newVal, oldVal) {
console.log(newVal, oldVal)
}
watch的属性是一个数组,数组元素可以是直接定义的函数,也可以是methods中的字符串函数名
// 就是一个观察者
firstName:[
function (newVal, oldVal) {
console.log(newVal, oldVal)
},
function (newVal, oldVal) {
console.log(newVal, oldVal)
}
]
watch也可以是一个methods中的字符串函数名
vm. w a t c h , 上 面 三 种 的 定 义 方 式 , 最 终 都 是 转 为 v m . watch,上面三种的定义方式,最终都是转为vm. watch,上面三种的定义方式,最终都是转为vm.watch的形式
const unwatch = vm.$watch(()=>vm.firstName, (newVal)=>{},options)// 额外选项options
// 取消watch
unwatch()
vm.$watch(() => vm.firstName + vm.lastName, (newVal) => {
console.log("类似侦听未定义的计算属性了",newVal)
})
// 是字符串 则不需要再属性前加vm
vm.$watch("firstName", (newVal) => {
console.log(newVal)
})
在vue中,我们知道数组有七个变异方法(会修改数组自身元素的方法),vue对这七个方法实现了重写,不然正常情况下我们使用这七个方法是没有办法实现响应式更新视图的。
而且对于一个对象,如果我们修改的是对象已经在data中定义好的对象的属性,当然是可以进行响应式更新的,但是,如果我们新增一个属性,视图是没有办法实现响应式更新的。
正常情况下,只有我们让数组属性的值变为一个新数组,或者对象属性变为一个新对象,这样才能让对于没有劫持的数组元素或者对象属性给劫持下来。
// 数组数据响应式更新原理
const vm = new Vue({
data: {
arr: ["海贼王", "火影忍者", "名侦探柯南"],
obj: { name: "张三" }
},
el: "#app",
// 模板编译为虚拟dom的时候,从arr对象取值了 _v(_s(变量)) JSON.stringify()
// 所以对象会收集依赖
template: `
- {{arr[0]}}
- {{arr[1]}}
- {{arr[2]}}
- {{obj}}
`
})
setTimeout(() => {
// 这种修改方式无法监控
vm.arr[1] += 1
// 也不会刷新视图
vm.arr.length = 10;
// 7个数组的变异方法可以监控到 因为我们重写了
// 这里并没有改变 arr属性 只是改变了arr这个数组对象
// arr数组对象自身并没有改变(没有变成新数组,地址没改变)
vm.arr.push("12")
vm.obj.age = 22
console.log("1秒后更新。。。",vm.arr,vm.obj)
}, 1000)
所以我们为了能劫持修改数组自身和给对象新增属性等,也可以被Vue劫持,我们需要在数组,对象等引用类型的属性上,也让其自身具有dep,不仅仅是对象的属性,数组的元素等需要被劫持,数组,对象等自身也需要被劫持。
也就是说:不管这个属性是原始类型,还是引用类型,都让其对应一个dep,用来收集依赖。
class Observe {
constructor(data) {
// 让引用数据自身也实现依赖收集 这个dep是放在 data.__ob__ = this 上的
// 也就是说 data.__ob__.dep 并不是 data.dep 所以不会发生重复
this.dep = new Dep();
// 记录this 也是一个标识 如果对象上有了该属性 标识已经被观测
Object.defineProperty(data, "__ob__", {
value: this, // observe的实例
});
// 如果劫持的数据是数组
if (Array.isArray(data)) {
// 重写数组上的7个方法 这7个变异方法是可以修改数组本身的
Object.setPrototypeOf(data, arrayProto);
// 对于数组元素是 引用类型的,需要深度观测的
this.observeArray(data);
} else {
// Object.defineProperty 只能劫持已经存在的属性(vue提供单独的api $set $delete 为了增加新的响应式属性)
this.walk(data);
}
}
/**
* 循环对象 对属性依次劫持 重新‘定义’属性
* @param {*} data
*/
walk(data) {
Object.keys(data).forEach((key) => defineReactive(data, key, data[key]));
}
/**
* 劫持数组元素 是普通原始值不会劫持
* @param {Array} data
*/
observeArray(data) {
data.forEach((item) => observe(item));
}
}
可以看见,修改数组自身的元素,视图也能正常更新。
但是要注意,直接使用arr[index]的方式修改元素,和新增对象还不存在的元素,目前还不能进行视图更新。
也就是说目前只是修改数组自身的7个变异方法,可以劫持到,并且实现视图更新。对于使用下标修改元素和修改数组的长度等,是不能劫持到的。
对于新增属性,需要使用vm.$set()方法新增才能实现劫持。
通过上面的操作,给每个对象的观察者observe都挂上了一个dep,用来收集每个对象自身的依赖。
当我们给对象新增属性的时候,可以observe通知dep更新视图。
setTimeout(() => {
vm.obj.age = 22
vm.obj.__ob__.dep.notify()//$set原理
console.log("1秒后更新。。。",vm.arr,vm.obj)
}, 1000)
$set本质上就是这种原理实现的。
对于数组元素还是数组的这种情况,需要二次侦听。
function dependArray(arr) {
// console.log(arr);
for (let i = 0; i < arr.length; i++) {
const cur = arr[i];
// console.log(cur, cur.__ob__);
// 数组元素可能不是数组了
if (Array.isArray(cur)) {
// 收集依赖
cur.__ob__.dep.depend();
dependArray(cur);
}
}
}
把数组元素循环,对于元素还是数组的情况,让该数组自身也收集依赖。
数据劫持总结:
默认vue在初始化的时候 会对对象每一个属性都进行劫持,增加dep属性, 当取值的时候会做依赖收集
默认还会对属性值是(对象和数组的本身进行增加dep属性) 进行依赖收集
如果是属性变化 触发属性对应的dep去更新
如果是数组更新,触发数组的本身的dep 进行更新
如果取值的时候是数组还要让数组中的对象类型也进行依赖收集 (递归依赖收集)
如果数组里面放对象,默认对象里的属性是会进行依赖收集的,因为在取值时 会进行JSON.stringify操作