、
vue2.0响应式原理
三个核心 Observer,Dep,Watcher
Observer
- data通过Observer转化成了getter/setter的形式来追踪变化
Dep
- 收集依赖,删除依赖和向依赖发送消息
Watcher
- Watcher就是依赖
下面来细细的说一下
- 我们在写vue代码的时候,希望那些数据将来是响应式的,会把他定义在data中
data() {
return {
active: "",
account: "",
};
},
那我们的Observer
类,就是将定义在data中的数据,转化为每个层级
都是响应式的数据。
核心API就是Object.defineProperty,将属性转化为getter/setter的形式来追踪变化。
这两句话是一个意思🤓
至此数据的劫持已经完成,如果你是单纯的console.log()去验证这个数据的响应式,是完全ok了。但是我们Vue现在要做的是数据变化,就要驱动视图变化
。那就引出了一个关键词 -> 依赖
- 何为依赖?
依赖,顾名思义,就是一种依靠关系。我们也说了vue就是数据驱动视图
,所有用到数据的地方,就称之为依赖
在vue1.x中,是细粒度依赖,用到数据的DOM
都是依赖
在Vue2.x中,是中等粒度依赖,用到数据的组件
是依赖
- 在哪里收集依赖,又在哪里触发依赖?
在getter中收集依赖,在setter中触发依赖。getter/setter只是一个操作方法。具体肯定要将这些依赖存放在一个地方,那就是我们的另一个核心Dep
- 收集谁?谁是依赖就收集谁
换句话说,就是当属性变化后,通知谁。我们用到数据的地方有很多,而且类型还不一样,这时我们就需要抽出一个能集中处理这些情况的类。=》 Watcher
核心代码实现
// 对数组的处理
import { arrayMethods } from './arr'
function observe (data) {
if (typeof data !== 'object' || data === null) return // 如果data中的数据不是对象,直接返回
new Observer(data) // 在Observer类里,将数据进行响应式
}
class Observer {
constructor(data){
data.__ob__ = this // 给每一个监控过的对象都增加一个__ob__属性,作用就是描述这个对象已经被监测
if (Array.isArray(data)) { // 如果是数组,通过代理原型的方式,进行
data.__proto__ = arrayMethods // 通过重写数组原型方法来对数组的七种方法进行拦截
this.observeArray(data)
} else {
this.walk(data) // 如果是对象,利用Object.definePrototype进行处理
}
}
walk(data){ // vue如果数据层次过多,会递归解析对象中的属性,依次增加get和set方法
// Object.keys(data) =》 [a,b,address]
// defineReactive(data, key, key[data]) } 定义响应式数据。依次将[a,b,address]变为响应式
Object.keys(data).forEach(key => defineReactive(data, key,key[data]))
}
observeArray(data){
data.forEach(i=> observe(i))
}
}
defineReactive (data,key,value){
let childOb = observe(value) // 递归实现深度检测
let dep = new Dep()
Object.definePrototype(data,key,{
get(){
// 如果现在处于依赖的收集阶段
if (Dep.target) {
dep.depend()
// 判断子元素
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set(newValue){
if (newValue === value) return
value = newValue
childOb = observe(newValue) // 继续劫持用户设置的值,因为用户可能设置的是一个对象
// 发布订阅者模式,通知dep
dep.notify() // 发布
}
})
}
let data = {
a: 1,
b: 2,
address: {
x: 'x'
},
arr: [1, 2, 3]
}
observe(data)
data.a = 33
data.address.x = 'XX'
data.address.x = { YY: "yy" }
data.arr.push([{ a: 1, b: 2 }, { c: 2 }, { d: 2 }])
arr.js 对数组观测
let oldArrayMethods = Arrar.prototype // 拿到数据的所有方法
export let arrayMethods = Object.create(oldArrayMethods) // arrayMethods通过__proto__拿到数组的方法
// Object.create()方法创建一个新对象,使现有的对象提供给新的对象的__proto__
let methods = ['push', 'shift', 'unshift', 'pop', 'splice', 'sort', 'reverse']
methods.forEach(method => {
// arrayMethods[method],只能找这七个,但是上面Object.create,可以找到数组所有的方法,两手准备。
arrayMethods[method] = function (...args) { // AOP切片编程。在不破坏封装的前提下,动态扩展功能
let rest = oldArrayMethods[method].apply(this, args) // 调用原生的方法
let inserted // 当前用户插入的数据
let obj = this.__ob__ //this代表当前数据本身,因为this代表了当前Observer实例,所以包括了Observer里面的方法,所以可以通过this.__ob__来访问observeArray,实现继续观测新增属性
switch(methods) {
case 'push',
case 'unshift'
inserted = args
break;
case 'splice'
inserted = args.slice(2)
break
}
if (inserted) obj.observeArray(inderted) // 将新增的属性继续观测
return rest
}
})
Dep
export default class Dep {
constructor() {
// 用数组存储自己的订阅者.subs存储的是watcher(订阅)的实例
this.subs = []
}
//添加订阅
addSub(sub) {
this.subs.push(sub)
}
// 添加依赖
depend() {
if (Dep.target) { // 我们指定的全局的位置
this.addSub(Dep.target) // getter函数就会从全局唯一的地方,读取正在读取数据的watcher。并把这个watcger收集到dep中
}
}
// 通知更新
notify() {
// 浅克隆一份
const subs = this.subs.slice()
subs.forEach((val => val.updata()))
}
}
- data.ob = this 次代码会导致监测过得数据,继续检测,我们做一个优化
封装def
export function def (data, key, value) {
Object.defineProperty(data, key, {
enumerable: false, // 不可枚举,循环的时候循环不到
configurable: false, // 不可配置
value: value
})
}
// data.__ob__ = this
def (data, '__ob__', this)
// 给每个响应式数据增加一个不可枚举的__ob__属性,并且指向了Observer实例。我们首先可以根据这个属性
// 防止已经被响应式观察的数据,反复被观测,其次响应式数据可以使用__ob__来获取Observer实例上相关的方法,这对数组很关键