写在前面
本篇是从零实现 vue2 系列第二篇,为 YourVue 添加双向绑定。双向绑定大家可能都比较熟悉来,如果你能回答出下面几个问题,就可以跳过看下一篇了:
1.vue2 通过 Object.defineProperty 修改 get 和 set 方法,实现订阅发布。2.为什么要用栈结构的 Dep.target 来存储当前 watcher ?3.为什么 watcher 每次更新后要 cleanupDeps,以及是如何 cleanupDeps 的?
文章会最先更新在公众号:BUPPT。代码仓库:https://github.com/buppt/YourVue
正文
上一篇我们实现了 vue 的主流程,其中先使用了 setState 函数帮助触发更新,现在我们改成直接修改 data 数据。
// main.jsnew YourVue({..., methods:{ addCount(){ this.count += 1 }, decCount(){ this.count -= 1 } }})
在 YourVue 的 $mount 函数中 new 一个 watcher 实例,将 this.update 函数传入作为更新函数,并在 initData 时 observe 传入的 data 对象。下面会一点一点讲解这几行代码分别是做什么用的。
class YourVue{..., $mount(){ const vm = this new Watcher(vm, vm.update.bind(vm), noop) }}function initData(vm){ let data = vm.$options.data vm._data = data data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {} Object.keys(data).forEach(key => { proxy(vm, '_data', key) }) observe(data) //将 data 修改成可观测对象}
Observer
下面来看 observe 的实现,就是通过 Object.defineProperty 来修改 data 中每一个 key 的 get 和 set 函数,从而实现订阅发布。
class Observer{ constructor(data) { this.data = data; this.walk(data); } walk(data) { Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); }}function observe(value) { if (!value || typeof value !== 'object') { return; } return new Observer(value);}
function defineReactive(data, key, val) { const dep = new Dep(); let childOb = observe(val); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend() } } return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; dep.notify(); } });}
Dep
data 中的每个 key 都会 new 一个 dep 作为消息分发器,当有 watcher get 该数据时,会将当前 watcher 订阅到该 dep 上,当数据发生改变时(set),通过 dep 触发所有订阅 watcher 的 update 函数。
dep.js代码如下
let uid = 0export class Dep { constructor(){ this.id = uid++ this.subs = [] } addSub (sub){ this.subs.push(sub); } notify() { this.subs.forEach(sub => sub.update()); } depend () { if (Dep.target) { Dep.target.addDep(this) } }}
Dep 中,subs 用来存储所有订阅者。当读取该数据时 (get),会执行dep.depend(),执行当前 watcher 的addDep函数。修改其中的数据时 (set),会执行dep.notify(),执行所有订阅 watcher 的update函数。
Watcher
watcher 的代码也并不复杂。
export class Watcher{ constructor(vm, expOrFn, cb){ this.cb = cb; this.vm = vm; this.getter = expOrFn this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.value = this.get(); } update(){ this.run(); } run(){ const value = this.get() if (value !== this.value) { const oldValue = this.value this.value = value; this.cb.call(this.vm, value, oldValue); } } get(){ pushTarget(this) const vm = this.vm const value = this.getter.call(vm, vm) popTarget() this.cleanupDeps() return value; } addDep (dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }}
其中有几个点需要注意一下。
•在执行 addDep 时,会先判断是否已经订阅过该发布者,防止重复订阅。•触发更新时,会先将当前的 watcher push 到 Dep.target 中,更新结束再 pop 出栈,这是因为当前 watcher 更新过程中,可能会触发另一个 watcher 的更新,比如子组件、computed、watch 也是 watcher。•如果触发了子组件更新,子组件对应 watcher 入栈,执行完子组件的更新函数后子组件 watcher 出栈,继续父组件的更新。
pushTarget(this)和popTarget()代码如下
Dep.target = nullconst targetStack = []
export function pushTarget (_target) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target}
export function popTarget () { Dep.target = targetStack.pop()}
那么每次更新后为什么要触发 cleanupDeps 呢?因为某一次数据更新后,可能删除了对某个数据的依赖,当前 watcher 就不需要继续订阅该数据了。
所以 watcher 中通过 deps 和 depIds 保存已经订阅的 dep,每次更新还会重新记录需要订阅的 newDeps 和 newDepIds,每次更新完成后如果当前订阅的 dep.id 不在新的 newDepIds 中,就取消订阅。
这样就可以实现文章开头那样,直接修改 data 数据触发视图更新啦!
本篇代码:https://github.com/buppt/YourVue/tree/master/oldSrc/2.mvvm
领取专属 10元无门槛券
私享最新 技术干货