第一章:响应式原理
一、观察者模式
- 观察者(订阅者) -- Watcher
update():当事件发生时,具体要做的事情 - 目标(发布者) -- Dep
subs 数组:存储所有的观察者
addSub():添加观察者
notify():当事件发生,调用所有观察者的 update() 方法 - 没有事件中心
// 目标(发布者)
// Dependency
class Dep {
constructor() {
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 通知所有观察者
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 观察者(订阅者)
class Watcher {
update() {
console.log('update')
}
}
// 测试
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
- 总结
观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。
二、响应式原理
- watcher在第一次视图绑定变量的时候(插值表达式、指令处理时)创建,每个key有自己的一个watcher。
- 创建watcher的时候应该加到对应的发布者里去,所以在watcher构造函数处调用
dep.addSub(watcher)
。 - 发布者当数据变更时需要通知观察者
dep.notify()
,所以发布者的实例创建即new Dep()
应该是在数据变更处,即Observer的walk里。 - 而watcher构造函数里为了能调用到Observer的walk里defineProperty的dep实例,可以借用
this.oldValue = vm[key]
去操作。所以可以把dep.addSub(watcher)
写在getter里,并通过Dep.target控制只有第一次才addSub。
// 处理插值表达式
compileText(node) {
const regExp = /\{\{(.+?)\}\}/
const value = node.textContent
if (regExp.test(value)) {
const key = RegExp.$1.trim()
node.textContent = value.replace(regExp, this.vm[key])
// 为当前的key创建watcher,当key变化时更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
class Watcher {
// 每个数据变化都要触发更新,所以每个key对应一个watcher
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// 创建watcher的时候应该加到对应的发布者里去,发布者当数据变更时需要通知观察者,所以发布者应该是在数据变更处,那么添加watcher也可以放在对应地方即Observer的walk里
// 此处跟Observer里walk相关的也就获取旧数据,为了在walk里面触发添加watcher,可以把添加写在getter里,并通过Dep.target控制只有第一次才addSub
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this
// 触发get方法,在get方法中会调用addSub
this.oldValue = vm[key]
Dep.target = null
}
// 当数据变化的时候更新视图
update() {
let newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
const that = this
const val = data[key] // 避免循环先存下来
const dep = new Dep() // 负责收集依赖,当数据变化的时候通知观察者
this.walk(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// 只有创建watcher时Dep.target才存在,所以只有创建watcher的时候获取数据才会触发添加watcher
Dep.target && dep.addSub(Dep.target)
return val
},
set(newVal) {
if (newVal === val) {
return
}
that.walk(newVal)
val = newVal // val被闭包缓存了,此处修改的是当前作用域下的val,get返回的也是同一个val
dep.notify() // 通知观察者调用update
}
})
})
}
}
一、对象监测
object.defineProperty只能观测到取值和改值,如果是增加或删除key是监测不到的,还得通过Vue.set
和Vue.delete
解决。
二、数组监测
数组的读取和整个替换是可以通过object.defineProperty监测到的,但数据内部元素的增删改查无法监测到。可以自行实现数组的增删改查方法,在里面实现监测和调用watcher。但数组下标的修改无法监测。
Array原型中可以改变数组自身内容的方法有7个,分别是: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'
]
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
}
})
})
做好数组方法拦截器之后,还要让用户的数组实例默认都用这些修改后的方法。
- 因为数组实例本身没有push之类的方法,都是用原型上的,所以我们可以直接把实例的原型proto改成修改后的原型。
- 若不支持proto,则直接往实例上加修改后的方法。
// 源码位置:/src/core/observer/index.js
export class Observer {
constructor (value) {
this.value = value
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}
// 能力检测:判断__proto__是否可用,因为有的浏览器不支持该属性
export const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
function protoAugment (target, src: Object, keys: any) {
target.__proto__ = src
}
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])
}
}