架构爱好者
学习交流中心

实现vue的数据双向绑定

1、页面结构

<div id="app">
    <h3>数据的双向绑定</h3>
    <div class="cell">
        <div class="text" v-text="myText"></div>
        <input class="input" type="text" v-model="myText" >
    </div>
</div>

2、创建类

class myVue{
    constructor (options){
        // 传入的配置参数
        this.options = options;
        // 根元素
        this.$el = document.querySelector(options.el);
        // 数据域
        this.$data = options.data;

        // 保存数据model与view相关的指令,当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
        this._directives = {};
        // 数据劫持,重新定义数据的 set 和 get 方法
        this._obverse(this.$data);
        // 解析器,解析模板指令,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
        this._compile(this.$el);
    }
}

这里我们定义了 myVue 构造函数,并在构造方法中进行了一些初始化操作, 上面做了注释,这里我就不在赘述, 主要来看里面关键的两个方法 _obverse 和 _compile。 首先是 _observe 方法,他的作用就是处理传入的 data , 并重新定义 data 的 set 和 get 方法, 保证我们在 data 发生变化的时候能跟踪到,并发布通知, 主要用到了 Object.defineProperty() 这个方法。

3、_observe

//_obverse 函数,对data进行处理,重写data的set和get函数
_obverse(data){
    let val ;
    //遍历数据
    for( let key in data ){
        // 判断是不是属于自己本身的属性
        if( data.hasOwnProperty(key) ){
            this._directives[key] = [];
        }

        val = data[key];
        //递归遍历
        if ( typeof val === 'object' ) {
            //递归遍历
            this._obverse(val);
        }

        // 初始当前数据的执行队列
        let _dir = this._directives[key];

        //重新定义数据的 get 和 set 方法
        Object.defineProperty(this.$data,key,{
            enumerable: true,
            configurable: true,
            get: function () {
                return val;
            },
            set: function (newVal) {
                if ( val !== newVal ) {
                    val = newVal;
                    // 当 myText 改变时,触发 _directives 中的绑定的Watcher类的更新
                    _dir.forEach(function (item) {
                        //调用自身指令的更新操作
                        item._update();
                    })
                }
            }
        })
    }
}

 

上面的代码也很简单,注释也都很清楚,不过有个问题就是, 我在递归遍历数据的时候,偷了个小懒 –, 这里我只涉及到了一些简单的数据结构,复杂的例如循环引用的这种我没有考虑进入, 大家可以自行补充一下哈~

接着我们来看看 _compile 这个方法,它实际上是一个解析器, 其功能就是解析模板指令,并将每个指令对应的节点绑定更新函数, 添加监听数据的订阅者,一旦数据有变动,就收到通知, 然后去更新视图变化,具体实现如下:

4、_compile

_compile(el){
    //子元素
    let nodes = el.children;
    for( let i = 0 ;  i < nodes.length ; i++ ){
    	let node = nodes[i];
    	// 递归对所有元素进行遍历,并进行处理
    	if( node.children.length ){
    		this._compile(node);
    	}

        //如果有 v-text 指令 , 监控 node的值 并及时更新
        if( node.hasAttribute('v-text')){
            let attrValue = node.getAttribute('v-text');
            //将指令对应的执行方法放入指令集
            this._directives[attrValue].push(new Watcher('text',node,this,attrValue,'innerHTML'))
        }

    	//如果有 v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
        if( node.hasAttribute('v-model') && ( node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')){
            let _this = this;
            //添加input时间
            node.addEventListener('input',(function(){
            	let attrValue = node.getAttribute('v-model');
            	//初始化赋值
            	_this._directives[attrValue].push(new Watcher('input',node,_this,attrValue,'value'));
                return function () {
                    //后面每次都会更新
                    _this.$data[attrValue] = node.value;
            	}
            })())
        }
    }
}

上面的代码也很清晰,我们从根元素 #app 开始递归遍历每个节点, 并判断每个节点是否有对应的指令,这里我们只针对 v-text 和 v-model, 我们对 v-text 进行了一次 new Watcher(), 并把它放到了 myText 的指令集里面,对 v-model 也进行了解析, 对其所在的 input 绑定了 input 事件,并将其通过 new Watcher() 与 myText 关联起来, 那么我们就应该来看看这个 Watcher 到底是什么?

Watcher 其实就是订阅者,是 _observer 和 _compile 之间通信的桥梁用来绑定更新函数, 实现对 DOM 元素的更新。

5、Watcher

class Watcher{
    /*
    * name  指令名称,例如文本节点,该值设为"text"
    * el    指令对应的DOM元素
    * vm    指令所属myVue实例
    * exp   指令对应的值,本例如"myText"
    * attr  绑定的属性值,本例为"innerHTML"
    * */
    constructor (name, el, vm, exp, attr){
        this.name = name;
        this.el = el;
        this.vm = vm;
        this.exp = exp;
        this.attr = attr;

        //更新操作
        this._update();
    }

    _update(){
    	this.el[this.attr] = this.vm.$data[this.exp];
    }
}

每次创建 Watcher 的实例,都会传入相应的参数, 也会进行一次 _update 操作,上述的 _compile 中, 我们创建了两个 Watcher 实例,不过这两个对应的 _update 操作不同而已, 对于 div.text 的操作其实相当于 div.innerHTML=h3.innerHTML = this.data.myText , 对于 input 相当于 input.value=this.data.myText, 这样每次数据 set 的时候,我们会触发两个 _update 操作, 分别更新 div 和 input 中的内容~

6、使用

废话不多说,赶紧测试一下吧~

先初始化一下~

//创建vue实例
const app = new myVue({
    el : '#app' ,
    data : {
        myText : 'hello world'
    }
})

我们顺利的实现了一个简单的双向绑定,棒棒哒 ~

参考资料

未经允许不得转载:技术杂烩 » 实现vue的数据双向绑定