根据vue源码手动实现双向绑定

其实写双向绑定的文章也挺多了,不过都没有仔细讲发布者-订阅者模式在其中的使用,本文尽量讲解清楚所有代码,代码结构、函数等完全按照[email protected]源码思路实现(除了新建vue类的时候)。github地址在这里。

根据vue源码手动实现双向绑定_第1张图片
图中,input的value、h1、h2都与data.name绑定,可以通过input改变data.namedata.name改变后(发布者)与之绑定的h1、h2(两个订阅者)都会改变,即实现了双向绑定。

本文目的是学习双向绑定,没有实现模版解析,所以并不能像vue一样通过{{name}}就将这个位置的内容与data.name绑定,需要手动将element和data绑定。不过这不重要。

<h1 id="name">h1>
<p id="num">p>
<h2 id="name2">h2>
<input id="input" type="text"/>
<script>
var name1 = document.getElementById('name');
var num = document.getElementById('num');
var name2 = document.getElementById('name2');
var input = document.getElementById('input');

var selfVue = new Vue({
    name: 'hello world',
    a:1,
});

selfVue.bindData(name1,'name')
selfVue.bindData(num,'a')
selfVue.bindData(name2,'name')
selfVue.bindData(input,'name')
script>

新建的Vue类如下

class Vue {
    constructor(data) {
        this.data = data;
        observe(data);
    }
    bindData(elm,name){
        var self = this;
        if(elm.tagName=='INPUT'){
            elm.addEventListener('input', function(e) {
                var newValue = e.target.value;
                var val = self.data[name]
                if (val === newValue) {
                    return;
                }
                self.data[name] = newValue;
            });
        }else{
            elm.innerHTML = this.data[name]
        }
        new Watcher(this, name, function (val) {
            elm.innerHTML = val;
        });

    }
}

new一个vue之后,会将data中的数据进行observe,data中每一个数据都是一个发布者,每有一个元素与数据绑定,就会新建一个watcher(订阅者),观察着数据是否会发生变化,然后执行变化后的操作。

observer的代码如下:

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中的每一个数据会new一个Dep发布者,当读取该数据时(get),会执行dep.depend(),修改其中的数据时(set),会执行dep.notify()

dep.js代码如下

class Dep {
    constructor(){
        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(),现在可以知道,depend函数执行的是watcher的addDep函数。
修改其中的数据时(set),会执行dep.notify(),现在知道notify函数执行的是watcher的update函数。

然后是watcher的代码。


class Watcher{
    constructor(vm, expression, cb){
        this.cb = cb;
        this.vm = vm;
        this.expression = expression;
        this.value = this.get();  
    }
    update(){
        this.run();
    }
    run(){
        const value = this.get()
        var oldVal = this.value;
        this.value = value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    }
    get(){
        pushTarget(this)
        var value = this.vm.data[this.expression] 
        popTarget()
        return value;
    }
    addDep (dep) {
        dep.addSub(this)
    }
}

前面知道每有一个元素与数据绑定,就会新建一个watcher(订阅者)。
watcher构造函数中执行了this.get(),该函数中用到了pushTarget(this)popTarget(),代码如下

function pushTarget (_target) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}
function popTarget () {
  Dep.target = targetStack.pop()
}

pushTarget(this)先把Dep.target设置为自己,然后获取data中数据的时候就可以触发之前设置的,if (Dep.target) dep.depend(); 然后就触发了自己watcher中的addDep (dep),如果这个watcher没有订阅该发布者dep的话,就触发dep.addSub(this),将这个watcher加入到发布者dep的订阅者subs中。
然后把之前pushTarget(this)的Dep.targets删掉。

这样,当data中的数据发生变化之后,就会执行set中的dep.notify();,然后就会执行该发布者的所有订阅者的update函数this.subs.forEach(sub => sub.update());

即每一个订阅者根据需要改变自己的dom或textContent。(这里就是虚拟dom的工作了,下一篇文章会介绍虚拟dom的源码和实现)。

欢迎star~

你可能感兴趣的:(vue)