-
变化侦测
UI = render(state),数据驱动视图,Vue 在表达式中充当 render 这个角色,当 state 改变时,Vue 给出相应的变化到 UI 上,Vue 如何得知 state 变化了呢,即 state 变化怎么通知给 Vue -
Object.defineProperty 使对象数据变得可观测
let car = { 'brand': 'BMW', 'price': 300 } let val = 300 Object.defineProperty(car, 'price', { enumerable: true, configurable: true, get(){ console.log('price 属性读取') return val }, set(newVal){ console.log('price 属性修改') val = newVal } })
Object.defineProperty 对对象的属性进行拦截,并执行自己想要的操作,这样就可以检测对象的变化了
// 源码位置:src/core/observer/index.js // Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象 export class Observer { constructor (value) { this.value = value // 给value新增一个__ob__属性,值为该value的Observer实例 // 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作 def(value,'__ob__',this) if (Array.isArray(value)) { // 当value为数组时的逻辑 // ... } else { this.walk(value) } } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } } function defineReactive (obj,key,val) { // 如果只传了obj和key,那么val = obj[key] if (arguments.length === 2) { val = obj[key] } if(typeof val === 'object'){ new Observer(val) } Object.defineProperty(obj, key, { enumerable: true, configurable: true, get(){ console.log(`${key}属性被读取了`); return val; }, set(newVal){ if(val === newVal){ return } console.log(`${key}属性被修改了`); val = newVal; } }) }
-
依赖收集
在第一二步中我们将数据变成响应式,可观测的状态,在此过程中我们可以在 getter 中收集依赖,在 setter 中通知依赖更新;
并在此过程将依赖置于依赖收集器中export default class Dep { constructor() { this.subs = [] } addSubs(sub) { this.subs.push(sub) } removeSubs(sub) { remove(this.subs, sub) } depend() { if(window.target) { this.addSubs(window.target) } } notify() { const sub = this.subs.slice() for(let i = 0;i < sub.length;i++) { subs[i].update() } } } const remove = (arr, item) => { if(arr.length) { const index = arr.indexOf(item) if(index > -1) { return arr.splice(index, 1) } } }
在收集器中定义一个 subs 来进行依赖收集,并在这个对象中包含了对依赖的增、删、以及更新。有了收集器我们就可以将其置于 getter 中,代码如下:
function defineReactive(obj, key, val) { if(arguments.length === 2) { val = obj[key] } if(typeof val === 'object') { new Observe(val) } const dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { dep.depend() return val }, set(newVal) { if(newVal === val) return val = newVal dep.notify() } }) }
在 get 中我们进行了依赖收集, 即 dep.depend; 在 set 中我们进行了通知依赖更新,即 dep.notify;但是我们不知道通知谁,或者说我们不知道谁是依赖。
在 Vue 中定义过一个 Watch 类,在数据变化后,我们不直接通知依赖更新,而是告诉 Watch 类的实例,让实例去通知真正的试图更新export default class Watcher { constructor(vm, expOrFn, cb) { this.vm = vm this.cb = ch this.getter = parsePath(expOrFn) this.value = this.get() } get() { window.taget = this const vm = this.vm let value = this.getter.call(vm, vm) // 触发dep的依赖收集 dep.depend window.target = undefined // 依赖存放后,释放window.target return value } update() { const oldVal = this.value this.value = this.get() this.cb.call(this.vm, this.value, oldVal) // set 触发dep的notify,在notify中遍历watcher里面的依赖,并执行对应的update } } /* Parse simple path 把形如 a.b.c.d的字符串所表示的值,从真是的data对象里取出来 data = {a:{b:{c:{d:2}}}} parsePath('a.b.c.d')(data) // 2 */ const bailRE = /[^w.$]/ function parsePath(path) { if(bailRE.test(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 } }
不足:对于后续对象添加的属性或者删除已有的属性,object.definePropery 不能够监听,即无法通知依赖,无法驱动视图进行响应式更新
流程:- Data 通过 obsever 转换成了 getter/setter 的形式追踪变化
- 当外界通过 Watcher 进行数据读取时,会触发 getter 将 Watcher 添加到依赖中
- 当数据发生变化时,会触发 setter,从而向 Dep 中的依赖(Watcher)发送通知
- Watcher 收到通知后,会向外界发送通知,可能会驱动数据更新,也可能触发某些回调