数据的双向绑定

造一个简单的vue双向绑定

参考 160实现vue的极简双向绑定 https://segmentfault.com/a/1190000015375217

之前虽然照着网络上的160行实现vue造过,但是过个一段日子就有点不清楚了,所以以这篇文章来巩固一下自己。

流程

  1. init初始化
  2. 将数据进行defineProperty的数据劫持
  3. 解析指令通知给watcher,绑定到dep
  4. 如果数据有变化就告诉watcherwatcher就去更新视图

图片

开始实现它

首先定义一下如何去调用他

new Vue({
    el: "#app", //dom
    data: {
        message: "123" //数据
    }
})

开始实现一个这样的入口

class Vue{
    constructor(option = {}){
        //获取要绑定的dom
        this.$el = document.querySelector(option.el);

        //得到数据
        this._data = option.data;

        //watcher的观察集合
        this._watcherTpl = {};
        
        //劫持数据
        this._observer(this._data);

        //编译模板 发布订阅
        this._compile(this.$el);
    }
}

watcher函数是一个数据的观察者,如果有数据发生了变动watcher就会去更新视图

/*
    el      订阅者dom
    vm      这里指vue的实例
    val     对应vue实例上data的值
    attr    要改变dom的什么属性
*/
class Watcher{
    constructor(el, vm, val, attr){
        //挂载数据
        this.el = el;
        this.vm = vm;
        this.val = val;
        this.attr = attr;

        //更新视图
        this.update();
    }
    update(){
        this.el[this.attr] = this.vm._data[this.val];
    }
}

observer数据的监听器,这里会监听劫持所有的数据,如果有数据变动就会通知给watcher让它更新视图

class Vue{
    //...

    _observer(obj){
        var _this = this;

        //遍历所有的数据,给所有数据监听
        Object.keys(obj).forEach((key) => {
            
            // 每个数据的一个订阅池,就是说如果数据发生变化就会通知给他的订阅池`_directives`,让所有的订阅者发生变动
            _this._watcherTpl[key] = {
                _directives: []
            }

            var value = obj[key];
            var watcherTpl = _this._watcherTpl[key];

            // 开始绑定数据的get、set
            Object.defineProperty(_this._data, key, {
                //可以删除或添加
                configurable: true,

                //可以枚举
                enumerable: true,

                get(){
                    // 直接把它的值放回出去
                    return value;
                },
                set(newVal){
                    // 如果新的值全等于旧值就直接返回
                    if( newVal === value )return;
                    value = newVal;

                    //给所有的订阅者更新视图
                    watcherTpl._directives.forEach(item => {
                        item.update();
                    })
                }
            })
        })
    }
}

最后是实现compile模板编译,这个函数的实现思路是深度遍历dom树,将里面的变量替换成真实的数据,将v-model添加到对应的订阅池中

class Vue{
    //...

    _compile(el){
        var _this = this,
            nodes = el.children, //得到所有的子节点
            len = nodes.length;

        // 遍历所有的节点
        for( var i = 0; i < len; i++ ){
            var node = nodes[i];
            
            // 如果这个子节点还有子节点的话就将其递归给自己
            if( node.children.length ){
                _this._compile(node)
            }

            // 为有v-model属性的input、textarea的元素绑定input事件
            if( node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA') ){
                var attrVal = node.getAttribute('v-model');

                // 为attrVal这个属性添加订阅者
                _this._watcherTpl[attrVal]._directives.push(new Watcher(
                    node,
                    _this,
                    attrVal,
                    'value'
                ))

                //监听节点的input事件,发生变动就将其中的内容赋值给vue
                node.addEventListener('input', (key => {
                    return function(){
                        _this._data[attrVal] = nodes[key].value;
                    }
                })(i))
            }

            //给dom节点绑定vue的数据
            if( node.hasAttribute('v-bind') ){
                var attrVal = node.getAttribute('v-bind');

                _this._watcherTpl[attrVal]._directives.push(new Watcher(
                    node,
                    _this,
                    attrVal,
                    'innerHTML'
                ))
            }

            var reg = /{{\s*([^}]+\S)\s*}}/g,
                txt = node.textContent;

            // 如果有匹配上面的正则的文本内容的话就将其替换成对应的vue数据
            if( reg.test(txt) ){
                // metched和placeholder分别是主匹配和附匹配,具体看exec的机制
                node.textContent = txt.replace(reg, (metched, placeholder) => {
                    var getName = _this._watcherTpl[placeholder];
                    
                    if( !getName._directives ){
                        getName._directives = [];
                    }

                    getName._directives.push(new Watcher(
                        node,
                        _this,
                        placeholder,
                        'innerHTML'
                    ))

                    /* reduce 
                        2个参数:
                            callback:4个参数
                                a: 上次调用返回的值
                                b: 这次执行时获取的值
                                c: 数组索引
                                d: 数组
                            init: 初始值
                    */
                    return placeholder.split('.').reduce((val, key) => {
                        return _this._data[key];
                    }, _this.$el)
                })
            }
        }
    }
}

到这里就实现了vue的双向绑定功能了。

这篇文章是我边看源码(160行极简代码)边写的,通过写这篇文章我对这个功能熟悉了很多。

在最后,我发现这个代码实现的功能有点不全,就是在

return placeholder.split('.').reduce((val, key) => {
    return _this._data[key];
}, _this.$el)

这一段,虽然写了分开obj.test这样的字符串的代码,但是没有创建相应的_directives,并且就算有,也没有绑定他的数据,不能进行双向绑定。

这个问题就交给下次实现更加完善的vue功能时再去实现吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值