观察者模式定义了对象间一对多的依赖关系。即被观察者状态发生变动时,所有依赖于它的观察者都会得到通知并自动更新。解决了主体对象和观察者之间功能的耦合。
Vue中基于 Observer、Dep、Watcher 三个类实现了观察者模式
- Observer类 负责数据劫持,访问数据时,调用
dep.depend()
进行依赖收集;数据变更时,调用dep.notify()
通知观察者更新视图。我们的数据就是被观察者 - Dep类 负责收集观察者 watcher,以及通知观察者 watcher 进行 update 更新操作
- Watcher类 为观察者,负责订阅 dep,并在订阅时让 dep 同步收集当前 watcher。当接收到 dep 的通知时,执行 update 重新渲染视图
dep 和 watcher 是一个多对多的关系。每个组件都对应一个渲染 watcher,每个响应式属性都有一个 dep 收集器。一个组件可以包含多个属性(一个 watcher 对应多个 dep),一个属性可以被多个组件使用(一个 dep 对应多个 watcher)
Dep
我们需要给每个属性都增加一个 dep 收集器,目的就是收集 watcher。当响应式数据发生变化时,更新收集的所有 watcher
- 定义 subs 数组,当劫持到数据访问时,执行
dep.depend()
,通知 watcher 订阅 dep,然后在 watcher内部执行dep.addSub()
,通知 dep 收集 watcher - 当劫持到数据变更时,执行
dep.notify()
,通知所有的观察者 watcher 进行 update 更新操作
Dep有一个静态属性 target,全局唯*一,Dep.target 是当前正在执行的 watcher 实例,这是一个非常巧妙的设计!因为在同一时间只能有一个全局的 watcher
注意:
渲染/更新完毕后我们会立即清空 Dep.target,保证了只有在模版渲染/更新阶段的取值操作才会进行依赖收集。之后我们手动进行数据访问时,不会触发依赖收集,因为此时 Dep.target 已经重置为 null
let id = class Dep { constructor() { this.id = id++ // 依赖收集,收集当前属性对应的观察者 watcher this.subs = [] } // 通知 watcher 收集 dep depend() { Dep.target.addDep(this) } // 让当前的 dep收集 watcher addSub(watcher) { this.subs.push(watcher) } // 通知subs 中的所有 watcher 去更新 notify() { this.subs.forEach(watcher => watcher.update()) } } // 当前渲染的 watcher,静态变量 Dep.target = null export default Dep
Watcher
不同组件有不同的 watcher。我们先只需关注渲染watcher。计算属性watcer和监听器watcher后面会单独讲!
watcher 负责订阅 dep ,并在订阅的同时执行dep.addSub()
,让 dep 也收集 watcher。当接收到 dep 发布的消息时(通过 dep.notify()
),执行 update 重新渲染
当我们初始化组件时,在 mountComponent 方法内会实例化一个渲染 watcher,其回调就是 vm._update(vm._render())
import Watcher from './observe/watcher' // 初始化元素 export function mountComponent(vm, el) { vm.$el = el const updateComponent = () => { vm._update(vm._render()) } // true用于标识是一个渲染watcher const watcher = new Watcher(vm, updateComponent, true) }
当我们实例化渲染 watcher 的时候,在构造函数中会把回调赋给this.getter
,并调用this.get()
方法。
这时!!!我们会把当前的渲染 watcher 放到 Dep.target 上,并在执行完回调渲染视图后,立即清空 Dep.target,保证了只有在模版渲染/更新阶段的取值操作才会进行依赖收集
import Dep from './dep' let id = class Watcher { constructor(vm, fn) { this.id = id++ this.getter = fn this.deps = [] // 收集当前 watcher 对应被观察者属性的 dep this.depsId = new Set() this.get() } // 收集 dep addDep(dep) { let id = dep.id // 去重,一个组件 可对应 多个属性 重复的属性不用再次记录 if (!this.depsId.has(id)) { this.deps.push(dep) this.depsId.add(id) dep.addSub(this) // watcher已经收集了去重后的 dep,同时让 dep也收集 watcher } } // 执行 watcher 回调 get() { Dep.target = this // Dep.target 是一个静态属性 this.getter() // 执行vm._render时,会劫持到数据访问,调用 dep.depend() 进行依赖收集 Dep.target = null // 渲染完毕置空,保证了只有在模版渲染阶段的取值操作才会进行依赖收集 } // 重新渲染 update() { this.get() } }
我们是如何触发依赖收集的呢?
在执行this.getter()
回调时,我们会调用vm._render()
,在_s()
方法中会去 vm 上取值,这时我们劫持到数据访问走到 getter,进而执行dep.depend()
进行依赖收集
流程:vm._render()
->vm.$options.render.call(vm)
-> with(this){ return _c('div',null,_v(_s(name))) }
-> 会去作用域链 this 上取 name
在 MDN 中是这样描述 with 的
JavaScript 查找某个未使用命名空间的变量时,会通过作用域链来查找,作用域链是跟执行代码的 context 或者包含这个变量的函数有关。'with'语句将某个对象添加到作用域链的顶部,如果在 statement 中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值
Observer
我们只会在 Observer 类 和 defineReactive 函数中实例化 dep。在 getter 方法中执行dep.depend()
依赖收集,在 setter 方法中执行dep.notity()
派发更新通知
依赖收集
依赖收集的入口就是在Object.defineProperty
的 getter 中,我们重点关注2个地方,一个是在我们实例化 dep 的时机,另一个是为什么递归依赖收集。我们先来看下代码
class Observer { constructor(data) { // 给数组/对象的实例都增加一个 dep this.dep = new Dep() // data.__ob__ = this 给数据加了一个标识 如果数据上有__ob__ 则说明这个属性被观测过了 Object.defineProperty(data, '__ob__', { value: this, enumerable: false, // 将__ob__ 变成不可枚举 }) if (Array.isArray(data)) { // 重写可以修改数组本身的方法 7个方法 data.__proto__ = newArrayProto this.observeArray(data) } else { this.walk(data) } } // 循环对象"重新定义属性",对属性依次劫持,性能差 walk(data) { Object.keys(data).forEach(key => defineReactive(data, key, data[key])) } // 观测数组 observeArray(data) { data.forEach(item => observe(item)) } } // 深层次嵌套会递归处理,递归多了性能就差 function dependArray(value) { for (let i = ; i < value.length; i++) { let current = value[i] current.__ob__ && current.__ob__.dep.depend() if (Array.isArray(current)) { dependArray(current) } } } export function defineReactive(target, key, value) { // 深度属性劫持;给所有的数组/对象的实例都增加一个 dep,childOb.dep 用来收集依赖 let childOb = observe(value) let dep = new Dep() // 每一个属性都有自己的 dep Object.defineProperty(target, key, { get() { // 保证了只有在模版渲染阶段的取值操作才会进行依赖收集 if (Dep.target) { dep.depend() // 依赖收集 if (childOb) { childOb.dep.depend() // 让数组/对象实例本身也实现依赖收集,$set原理 if (Array.isArray(value)) { // 数组需要递归处理 dependArray(value) } } } return value }, set(newValue) { ... }, }) }
实例化 dep 的时机
我们只会在 Observer 类 和 defineReactive 函数中实例化 dep
- Observer类:在 Observer 类中实例化 dep,可以给每个数组/对象的实例都增加一个 dep
- defineReactive函数:在 defineReactive 方法中实例化 dep,可以让每个被劫持的属性都拥有一个 dep,这个 dep 是被闭包读取的局部变量,会驻留到内存中且不会污染全局
我们为什么要在 Observer 类中实例化 dep?
- Vue 无法检测通过数组索引改变数组的操作,这不是 Object.defineProperty() api 的原因,而是尤大认为性能消耗与带来的用户体验不成正比。对数组进行响应式检测会带来很大的性能消耗,因为数组项可能会大,比如10000条
- Object.defineProperty() 无法监听数组的新增
如果想要在通过索引直接改变数组成员或对象新增属性后,也可以派发更新。那我们必须要给数组/对象实例本身增加 dep 收集器,这样就可以通过 xxx.__ob__.dep.notify()
手动触发 watcher 更新了
这其实就是 vm.$set 的内部原理!!!
递归依赖收集
数组中的嵌套数组/对象没办法走到 Object.defineProperty,无法在 getter 方法中执行dep.depend()
依赖收集,所以需要递归收集
举个栗子:data: {arr: ['a', 'b', ['c', 'd', 'e', ['f', 'g']], {name: 'libc'}]}
我们可以劫持 data.arr,并触发 arr 实例上的 dep 依赖收集,然后循环触发 arr 成员的 dep依赖收集。对于深层数组嵌套的['f', 'g']
,我们则需要递归触发其实例上的 dep 依赖收集
派发更新
对于对象
在 setter 方法中执行dep.notity()
,通知所有的订阅者,派发更新通知
注: 这个 dep 是在 defineReactive 函数中实例化的。 它是被闭包读取的局部变量,会驻留到内存中且不会污染全局
Object.defineProperty(target, key, { get() { ... }, set(newValue) { if (newValue === value) return // 修改后重新观测。新值为对象的话,可以劫持其数据。并给所有的数组/对象的实例都增加一个 dep observe(newValue) value = newValue // 通知 watcher 更新 dep.notify() }, })
对于数组
在数组的重写方法中执行xxx.__ob__.dep.notify()
,通知所有的订阅者,派发更新通知
注: 这个 dep 是在 Observer 类中实例化的,我们给数组/对象的实例都增加一个 dep。可以通过响应式数据的__ob__获取到实例,进而访问实例上的属性和方法
let oldArrayProto = Array.prototype // 获取数组的原型 // newArrayProto.__proto__ = oldArrayProto export let newArrayProto = Object.create(oldArrayProto) // 找到所有的变异方法 let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'] // concat slice 都不会改变原数组 methods.forEach(method => { // 这里重写了数组的方法 newArrayProto[method] = function (...args) { // args reset参数收集,args为真正数组,arguments为伪数组 const result = oldArrayProto[method].call(this, ...args) // 内部调用原来的方法,函数的劫持,切片编程 // 我们需要对新增的数据再次进行劫持 let inserted let ob = this.__ob__ switch (method) { case 'push': case 'unshift': // arr.unshift(1,2,3) inserted = args break case 'splice': // arr.splice(0,1,{a:1},{a:1}) inserted = args.slice(2) default: break } if (inserted) { // 对新增的内容再次进行观测 ob.observeArray(inserted) }