vue实现数据响应式,是通过数据劫持侦测数据变化,发布订阅模式进行依赖收集与视图更新,换句话说是Observe,Watcher以及Compile三者相互配合。
- Observe实现数据劫持,递归给对象属性,绑定setter和getter函数,属性改变时,通知订阅者
- Compile解析模板,把模板中变量换成数据,绑定更新函数,添加订阅者,收到通知就执行更新函数
- Watcher作为Observe和Compile中间的桥梁,订阅Observe属性变化的消息,触发Compile更新函数
数据劫持/代理 Observer
实现响应式的第一步就是能侦测数r据的变化,在Vue2.x是通过ES5的方法Object.defineProperty()实现对象属性的侦听,在Vue3.x中使用了ES6提供的Proxy对对象进行代理。
Object.definePropertyfunction observe(obj) {
if (!obj || typeof obj !== "object") {
return;
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
function defineReactive(obj, key, value) {
//递归子属性
observe(value);
//订阅器
const dp = new Dep();
Object.defineProperty(obj, key, {
configurable: true, //可删除
enumerable: true, //可枚举遍历
get: function () {
/* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
dp.addSub(Dep.target);
return value;
},
set: function (newValue) {
//递归新的子属性
observe(newValue);
if (value !== newValue) {
value = newValue;
/* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
dp.notify();
}
},
});
}
} Proxy实现代理let target = { name: " xiao" };
let handler = {
get(target, key) {
if (typeof target[key] === "object" && target[key] !== "null") {
return new Proxy(target[key], handler);
}
return target[key];
},
set: function (target, key, value) {
target[key] = value;
},
};
target = new Proxy(target, handler); 依赖收集Dep//Dep订阅者,依赖收集器
class Dep {
constructor() {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
addSub(sub) {
this.subs.push(sub);
}
/* 在subs中添加一个Watcher对象 */
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
//用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
//用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。 Watcher订阅者class Watcher {
constructor(obj, key, cb) {
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在observe get中会用到 */
Dep.target = this;
this.obj = obj;
this.key = key;
this.cb = cb;
//触发getter,依赖收集
this.value = obj[key];
//收集完置空Dep.target,防止重复收集
Dep.target = null;
}
update() {
//获得新值
this.value = obj[this.key];
console.log("视图更新");
}
} Compile模板编译
- 正则匹配解析vue指令、表达式
- 把变量替换成数据初始化渲染
- 创建Watcher订阅更新函数
//指令处理类
const compileUtile = {
getVal(expr,vm){
//reduce用的好啊
return expr.split('.').reduce((data,curentval)=>{
return data[curentval];
},vm.$data)
},
html(node,expr,vm){
new Watcher(vm,expr,(newVal)=>{
this.updater.htmlUpdate(node,newVal);
})
const value = this.getVal(expr,vm);
this.updater.htmlUpdate(node,value);
},
//更新函数
updater:{
htmlUpdate(node,value){
node.innerHTML= value;
},
}
}
//Compile指令解析器
class Compile{
//各种正则匹配vue指令和表达式,替换数据
} Object.defineProperty与Proxy的区别?
- Proxy可以直接监听对象,而非属性,可以监听属性的增加
- Proxy可以监听数组
- Proxy有很多Object.defineProperty不具备的拦截方法
- Proxy返回一个新对象,可以直接操作新对象达到目的,Object.defineProperty只能遍历对象属性修改
为什么要依赖收集?
数据劫持的目的是在属性变化的时候触发视图更新,依赖收集可以收集到哪些地方使用到了相关属性,属性变化时,就可以通知到所有的地方去更新视图,对于没有使用的属性,也可以避免无用的数据比对更新
Dep和Watcher的关系(多对多)
- data中一个key对应一个Dep实例, 一个Dep实例对应多个Watcher实例(一个属性在多个表达式中使用)
- 一个表达式对应一个Watcher实例,一个Watcher对用多个Dep实例(一个表达式中有多个属性)
watcher和Dep何时创建
- Dep在初始化data的属性进行数据劫持时创建的
- Watcher是在初始化时解析大括号表达式/一般指令时创建
如何实现对数组的监听
因为Object.defineProperty不能监听数组长度变化,所以Vue使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。1 // src/core/observer/array.js
2
3 // 获取数组的原型Array.prototype,上面有我们常用的数组方法
4 const arrayProto = Array.prototype
5 // 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
6 export const arrayMethods = Object.create(arrayProto)
7
8 // 列出需要重写的数组方法名
9 const methodsToPatch = [
10 'push',
11 'pop',
12 'shift',
13 'unshift',
14 'splice',
15 'sort',
16 'reverse'
17 ]
18 // 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
19 methodsToPatch.forEach(function (method) {
20 // 保存一份当前的方法名对应的数组原始方法
21 const original = arrayProto[method]
22 // 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
23 def(arrayMethods, method, function mutator (...args) {
24 // 调用数组原始方法,并传入参数args,并将执行结果赋给result
25 const result = original.apply(this, args)
26 // 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
27 const ob = this.__ob__
28 let inserted
29 switch (method) {
30 case 'push':
31 case 'unshift':
32 inserted = args
33 break
34 case 'splice':
35 inserted = args.slice(2)
36 break
37 }
38 if (inserted) ob.observeArray(inserted)
39 // 将当前数组的变更通知给其订阅者
40 ob.dep.notify()
41 // 最后返回执行结果result
42 return result
43 })
44 }) def就是通过Object.defineProperty重写value,也就是自定义的几个数组方法function def(obj,key,val,enumble){
Object.defineProperty(obj,key,{
enumble:!!enumble,
configrable:true,
writeble:true,
val:val
})
} observe方法里面加入数组的处理,
- 能获取到__proto__属性,就把__protp__属性指向重写的方法
- 获取不到__proto__属性,就把重写的方法定义到对象上实例上
// src/core/observer/index.js
export class Observer {
...
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
...
}
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
|