Skip to content
New issue

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

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

Open
buppt opened this issue Jun 17, 2020 · 0 comments
Open

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

buppt opened this issue Jun 17, 2020 · 0 comments

Comments

@buppt
Copy link
Owner

buppt commented Jun 17, 2020

写在前面

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant