Vue最大的特点之一就是数据驱动视图,数据变化引起视图变化(MVVM)
- 在Angular中是通过脏值检查流程来实现变化侦测;
- 在React是通过对比虚拟DOM来实现变化侦测;
- vue中通过js提供的Object.defineProperty来实现变化侦测;
侦测对象
1. Compile
- 模板解析器,即模拟Vue模板是如何被解析的,奔例会简单的解析 v-model 指令和 {{}}指令
2.Observer
- 数据监听器,能够对数据对象进行监听,即当读取或者修改数据属性的时候,能够追踪到变化。
3.Watcher
- 订阅者,简单来说,它就是一个依赖,能够监听到其绑定的数据属性发生变化(读取/修改),然后做出改变,所以它通常会是一个模板指令,computed属性等, 后续会介绍。
4.Dep
- 消息订阅器,简单来说,它就是一个收集器,它主要负责(1)用来收集依赖(2)当数据属性发生变化时,来通知相关依赖
源码解读
Observer
- Observer类会附加到每一个被侦测的object上。一旦被附加上,Observer会将object的所有属性转换为getter/setter的形式。来收集属性的依赖,并且当属性发生变化时会通知这些依赖
import Dep from './Dep';
export class Observer {
constructor(value) {
this.value = value;
if (!Array.isArray(value)) {
this.walk(value);
}
}
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function defineReactive(data, key, val) {
if (typeof val === 'object') {
new Observer(val);
}
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
dep.depend();//收集依赖
return val
},
set(newVal) {
if (val === newVal) {
return
}
val = newVal;
dep.notify();//触发依赖
}
})
}
Dep
- 它用来收集依赖、删除依赖和向依赖发送消息等。
import { Watcher } from "./Watcher";
export class Dep {
target; //target: ?Watcher;
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
remove(this.subs, sub);
}
depend(){
if(this.target instanceof Watcher){
this.addSub(this.target);
}
}
notify(){
const subs=this.subs.slice();
for (let i = 0; i < subs.length; i++) {
subs[i].update();
}
}
}
function remove(arr, item) {
if (arr.length) {
const index = arr.findIndex(item);
if (index > -1) {
this.subs.splice(index, 1);
}
}
}
Watcher
- Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
import { Dep } from "./Dep";
export class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;// vm指当前的Vue实例
this.getter = parsePath(expOrFn);
this.cb = cb;
this.value = this.get();// 读取vm.$data中的值,同时会触发属性上的getter
}
get() {
// Watcher把自己设置到全局唯一的指定位置,在这里就是Dep.target
Dep.target = this;
//读取数据,触发这个数据的getter。因此Observer会收集依赖,将这个Watcher收集到Dep,也就是依赖收集。
let value = this.getter.call(this.vm, this.vm);
//收集结束,清除Dep.target的内容
Dep.target = null;
//返回读取到的数据值
return value
}
update() {
//数据改变之后,Dep会依次循环向依赖发通知,这里接到通知之后,先获取之前的旧数据
const oldValue = this.value;
//然后获取最新的值
this.value = this.get();
//将新旧值传给回调函数
this.cb.call(this.vm, this.value, oldValue);
}
}
const bailRE = /[^\w.$]/
export function parsePath(path) {
if (bailRE.tetx(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) { return; }
obj = obj[segments[i]]
}
return obj;
}
}
逻辑脑图
vue变化侦测对象.png
- 每一个data中的变量都有一个observer,还有一个dep负责存放和这一个变量有关的watcher。
- 当data中的数据发生变化是就会通过observer的set函数通知dep数组,dep数组开始循环他里边的watcher调用watcher的updata方法更新视图。
对象侦测不足
- 当我们向object数据里添加一对新的key/value或删除一对已有的key/value时,它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete。
this.$set(obj, key, value)/vue.set(obj, key, value)
Vue.delete(obj,key)
侦测数组
- 而针对数组的变化侦测会稍微麻烦一点,因为数组的变化侦测是针对它原型的方法,如push、pop、shift、unshift、splice、sort、reverse,针对数组的变化侦测,有一个类似拦截器的家伙来拦截访问数组的方法。
源码解读
拦截器
// 源码位置:/src/core/observer/array.js
const arrayProto = Array.prototype
// 创建一个对象作为拦截器
export const arrayMethods = Object.create(arrayProto)
// 改变数组自身内容的7个方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
const original = arrayProto[method] // 缓存原生方法
Object.defineProperty(arrayMethods, method, {
enumerable: false,
configurable: true,
writable: true,
value:function mutator(...args){
const result = original.apply(this, args)
return result
}
})
})
逻辑脑图
vue变化侦测数组.png
数组侦测不足
- 对于数组变化侦测是通过拦截器实现的,也就是说只要是通过数组原型上的方法对数组进行操作就都可以侦测到,如果使用下标是无法侦测到的。 为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete。
this.$set([], 下标, 值)/vue.set([], 下标, 值)
Vue.delete([],下标)