Vue源码之Object数据劫持
目录
- vue中如何侦测Object变化?
- Object.defineProperty
- 数据劫持原理
- Object 响应式中的问题
- vm.$set内部原理
- vm.$delete内部原理
- vm.$watch内部原理
- 用法
- watch内部实现原理
- deep实现原理
- 使用Object.defineProperty
- ES6的proxy(ES6浏览器支持不理想)
【Vue源码之Object数据劫持】Object可以通过Object.defineProperty将属性转换成getter/setter的形式来追踪变化。读取数据时会触发getter,修改数据时会触发setter
Object.defineProperty(obj, key, {
get() {
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal
}
}
})
这样,修改数据时,我们可以主动通知依赖进行更新,那么问题来了:
- 如何收集依赖?什么时机收集依赖?
思路:
- 任何地方,只要涉及到数据读取,就会触发get函数
- 先收集依赖,然后等属性发生变化的时候,再把之前收集好的依赖循环触发一遍就好了
- 依赖收集在哪里?
定义一个Dep类,用来存储当前key的依赖。DEP类主要负责对依赖的收集、删除、发送通知。
- 依赖是谁?
依赖就是用到数据的地方,用到这个数据的地方可能是视图中对应的坑({{name}}),也可能是开发者写的一个watch,于是抽象一个类Watcher
- 定义一个响应式数据
//数据劫持,添加依赖追踪
defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
//只有Watcher触发的getter才会收集依赖
Dep.target && dep.depend();
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
// 数据发生变更通知所有的观察者
dep.notify()
}
}
})
//递归树
this.observer(val);
}
- 定义一个Dep类,收集依赖
// Dep发布者相当于vue data 对象中的某一个属性如:name
class Dep {
constructor(vm, key) {
this.subs = [];
}depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
remove(this.subs, sub);
}notify() {
//只负责通知更新,具体是否更新以及更新操作由watcher做
this.subs.forEach(sub => sub.update())
}
}
- 定义依赖 – Watcher类
// watcher 相当于视图中对应的坑({{name}}),一个坑对应有一个观察者,监听此处的数据变化
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
//这段代码会自动将Watcher添加到Dep中
//先将Watcher赋值给Dep.target,然后读取一下值,会触发get,这时依赖就被收集了,再将Dep.target置空
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}update() {
//每个watcher所做的更新大不一样,所以将具体的更新操作放到回调里面去做
this.cb.call(this.vm, this.vm[this.key])
}
};
}
- 封装类Observer,将数据内的所有属性都转换成getter/setter形式
class Observer{
constructor(obj){
this.obj = obj
if(!Array.isArray(obj)){
this.walk(obj)
}
}
walk(obj){
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
function defineReactive(obj, key, val) {
// 递归子属性
if(typeof val === 'object'){
new Observer(val)
}
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
//如果有创建了一个观察者,那么Dep.target就会被它的实例赋值,此时就会通过访问实例的值(见Watcher 构造函数)将其add到dep
//同时这里的判断是为了防止重复添加
Dep.target && dep.depend();
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
// 数据发生变更通知所有的观察者
dep.notify()
}
}
})
}
数据劫持原理
文章图片
- Data通过Observer转换成响应式对象
- 当外界通过Watcher读取数据时,会触发getter从而将watcher添加到Dep中
- 当数据发生变化时,会触发setter,Dep像依赖(Watcher)发送通知
- Watcher收到通知后,向外界发送通知,可能会触发视图更新,或者执行回调函数。
Object 响应式中的问题 通过defineProperty方法定义obj之后, 对Obj新增属性和删除属性的操作,将无法追踪到这个变化。
原因: ES6之前,JS没有提供元编程能力
//新增属性
this.obj.name = 'xxx'
//删除属性
delete this.obj.name
为了解决这个问题,vue.js提供的两个API——vm. s e t 和 v m . set和vm. set和vm.delete
vm.$set 用法:vm.$set(target, key, value)
import {set} from './observer/index' Vue.prototype.$set = set
代码中数组的部分可以在读了《vue源码之Array》之后再了解,源码及解读:
function set (target, key, val) {
// 当target是一个数组并且key是一个有效的索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
// 如果key 已经存在于对象中,直接修改值即可
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}// 源码中,在Observer一个对象时,会给对象添加__ob__属性,标识这是一个响应式对象
var ob = (target).__ob__;
// 如果target上并没有__ob__属性,说明它本身就不是一个响应式的对象,不做处理
if (!ob) {
target[key] = val;
return val
}
//否则,将属性转换成响应式,并向依赖发出更新通知
defineReactive(ob.value, key, val);
ob.dep.notify();
return val
}
vm.$delete 用法:vm.$delete(target, key)
import {del} from './observer/index' Vue.prototype.$delete = del
源码解读:
function del (target, key) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return
}
var ob = (target).__ob__;
// 不存在此属性时,直接返回
if (!hasOwn(target, key)) {
return
}
delete target[key];
if (!ob) {
return
}
// 删除属性后,像依赖发送通知
ob.dep.notify();
}
vm.$watch内部原理 1. 用法
vm.$watch( expOrFn, callback, [options] )
用于观察一个表达式或computed函数的变化,来达到todo something的目的。
返回一个取消观察的函数,用于停止触发回调
var unwatch = vm.$watch('a.b.c', (newVal, oldVal) => {})
unwatch()
- expOrFn
- 表达式只支持以点分隔的路径
- computed函数,如 function(){ return this.name + this.age}
- options
- deep:true 深度监听对象内部值的变化,监听数组的变化不需要这么做
- immediate:true 立即以当前值触发一次回调
vm. w a t c h 实 际 上 是 对 W a t c h e r 的 一 种 封 装 。 无 论 是 视 图 中 的 多 个 依 赖 ( 如 n a m e 出 现 的 地 方 ) , 还 是 使 用 watch实际上是对Watcher的一种封装。无论是视图中的多个依赖(如{{name}}出现的地方),还是使用 watch实际上是对Watcher的一种封装。无论是视图中的多个依赖(如name出现的地方),还是使用watch监听值的变化,都可以看做是观察者。
实质:当用户使用vm.$watch()来监听一个数据变化时,实际跟我们在视图中新增一个坑
{{name}}
,所做的事情是一样的。- 实例化了一个Watcher
- 将Watcher收集到Dep中
- 当expOrFn对应的值发生变化时,会在原有的基础上新增一个依赖通知,触发cb调用
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
options = options || {};
// 创建一个依赖,并收集到Dep
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
// 依赖将自己从被收集到的所有Dep中移除,终止订阅数据变更通知
watcher.teardown();
}
};
}
- Watcher 和 Dep 多对多的关系
- expOrFn是一个表达式,则只会对应一个Dep
- expOrFn是一个函数时,当函数中使用了多个数据时,那么Watcher就要收集多个Dep
- 同时一个数据在多处运用,所以Dep对应多个Watcher
//Watcher会收集name和age两个的Dep
// 同时这两个Dep中也会收集Watcher
// 任意一个数据发生变化,Watcher都会收到通知
this.$watch(function(){
return this.name + this.age
},(newVal, oldVal) => {})
3. deep实现原理
思路:
- 无非就是收集依赖,将数据以及数据内部的子值都触发一遍依赖收集
- 在Watcher类中实现
Watcher类的修改:
Dep.target = this;
this.vm[this.key];
//收集到当前数据Dep
// 新增
if(!!options.deep){
traverse(value) // 递归收集到当前数据子值的Dep
}
Dep.target = null;
添加到子值Dep
var seenObjects = new Set();
function traverse (val) {
_traverse(val, seenObjects);
seenObjects.clear();
}function _traverse (val, seen) {
var i, keys;
if (val.__ob__) {
var depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return
}
seen.add(depId);
}
// 重点看这里
// 如果值是一个对象,则递归
keys = Object.keys(val);
i = keys.length;
while (i--) {
// 其中val[keys[i]] 会触发getter,也就是此处会进行依赖收集
_traverse(val[keys[i]], seen);
}
}
_traverse(val[keys[i]], seen)
其中val[keys[i]]会触发getter,也就是此处会进行依赖收集。
至此,所有值的变化,都会触发cb。
为了便于理解,文章中的代码都是源码的删减版
推荐阅读
- PMSJ寻平面设计师之现代(Hyundai)
- 太平之莲
- 闲杂“细雨”
- 七年之痒之后
- 深入理解Go之generate
- 由浅入深理解AOP
- vue-cli|vue-cli 3.x vue.config.js 配置
- 期刊|期刊 | 国内核心期刊之(北大核心)
- 生活随笔|好天气下的意外之喜
- 感恩之旅第75天