We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
本篇是从零实现vue2系列第二篇,为YourVue 添加双向绑定。双向绑定大家可能都比较熟悉来,如果你能回答出下面几个问题,就可以跳过看下一篇了:
文章会最先更新在公众号:BUPPT。代码仓库:https://github.com/buppt/YourVue
上一篇我们实现了 vue 的主流程,其中先使用了 setState 函数帮助触发更新,现在我们改成直接修改 data 数据。
// main.js new 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 修改成可观测对象 }
下面来看 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(); } }); }
data 中的每个 key 都会 new 一个 dep 作为消息分发器,当有 watcher get 该数据时,会将当前 watcher 订阅到该 dep 上,当数据发生改变时(set),通过 dep 触发所有订阅 watcher 的 update 函数。
dep.js代码如下
let uid = 0 export 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函数。
dep.depend()
addDep
dep.notify()
update
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()代码如下
pushTarget(this)
popTarget()
Dep.target = null const 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 求关注~求 star~
The text was updated successfully, but these errors were encountered:
No branches or pull requests
写在前面
本篇是从零实现vue2系列第二篇,为YourVue 添加双向绑定。双向绑定大家可能都比较熟悉来,如果你能回答出下面几个问题,就可以跳过看下一篇了:
文章会最先更新在公众号:BUPPT。代码仓库:https://github.com/buppt/YourVue
正文
上一篇我们实现了 vue 的主流程,其中先使用了 setState 函数帮助触发更新,现在我们改成直接修改 data 数据。
在 YourVue 的 $mount 函数中 new 一个 watcher 实例,将 this.update 函数传入作为更新函数,并在 initData 时 observe 传入的 data 对象。下面会一点一点讲解这几行代码分别是做什么用的。
Observer
下面来看 observe 的实现,就是通过 Object.defineProperty 来修改 data 中每一个 key 的 get 和 set 函数,从而实现订阅发布。
Dep
data 中的每个 key 都会 new 一个 dep 作为消息分发器,当有 watcher get 该数据时,会将当前 watcher 订阅到该 dep 上,当数据发生改变时(set),通过 dep 触发所有订阅 watcher 的 update 函数。
dep.js代码如下
Dep 中,subs 用来存储所有订阅者。
当读取该数据时 (get),会执行
dep.depend()
,执行当前 watcher 的addDep
函数。修改其中的数据时 (set),会执行
dep.notify()
,执行所有订阅 watcher 的update
函数。Watcher
watcher 的代码也并不复杂。
其中有几个点需要注意一下。
在执行 addDep 时,会先判断是否已经订阅过该发布者,防止重复订阅。
触发更新时,会先将当前的 watcher push 到 Dep.target 中,更新结束再 pop 出栈,这是因为当前 watcher 更新过程中,可能会触发另一个 watcher 的更新,比如子组件、computed、watch 也是 watcher。
如果触发了子组件更新,子组件对应 watcher 入栈,执行完子组件的更新函数后子组件 watcher 出栈,继续父组件的更新。
pushTarget(this)
和popTarget()
代码如下那么每次更新后为什么要触发 cleanupDeps 呢?因为某一次数据更新后,可能删除了对某个数据的依赖,当前 watcher 就不需要继续订阅该数据了。
所以 watcher 中通过 deps 和 depIds 保存已经订阅的 dep,每次更新还会重新记录需要订阅的 newDeps 和 newDepIds,每次更新完成后如果当前订阅的 dep.id 不在新的 newDepIds 中,就取消订阅。
这样就可以实现文章开头那样,直接修改 data 数据触发视图更新啦!
本篇代码:https://github.com/buppt/YourVue/tree/master/oldSrc/2.mvvm 求关注~求 star~
The text was updated successfully, but these errors were encountered: