首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

从零写一个 Vue(二)双向绑定

写在前面

本篇是从零实现 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

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20200616A0KJCE00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券