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

写在前面

本篇是从零实现 vue2 系列第二篇,为 YourVue 添加双向绑定。双向绑定大家可能都比较熟悉来,如果你能回答出下面几个问题,就可以跳过看下一篇了:

  1. vue2 通过 Object.defineProperty 修改 get 和 set 方法,实现订阅发布。
  2. 为什么要用栈结构的 Dep.target 来存储当前 watcher ?
  3. 为什么 watcher 每次更新后要 cleanupDeps,以及是如何 cleanupDeps 的?

代码仓库: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 修改成可观测对象
}

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 = 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函数。

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 = 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~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值