【Vue】实现Vue中的双向数据绑定

Vue是通过数据劫持的方式来做数据绑定的,核心方法:Object.defineProperty(), 实现三部分:observer, watcher和compiler——指令解析器Compile,对每个元素节点的指令进行解析,根据指令模板替换数据,以及绑定相应的更新函数 ;数据监听器observer对所有vm属性进行监听,如有变动通知订阅者watcher;watcher收到更新通知后,执行相应回调函数以更新视图。

编译

最难理解的部分。首先将el根结点下所有原生节点转换为文档碎片进行解析编译,避免多次操作DOM节点效率低下。

node2Fragments: function(el) {
        var fragment = document.createDocumentFragment(), child;
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }

        return fragment;
    },

对所有节点及子节点进行解析编译,判断是元素节点还是文本节点,如果是文本节点还要判断一下是否是插值语法 {{XXX}}.

  compileElement: function(el) {
        var childNodes = el.childNodes,
            me = this;

        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;
            
            if (me.isElementNode(node)) {
                me.compile(node);

            } else if (me.isTextNode(node) && reg.test(text)) {
                console.log('node',node);
                me.compileText(node, RegExp.$1.trim());
            }

            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

对于元素节点,判断其是否是指令(模版语法,v-XXX),再判断其为事件指令(v-on:click)还是普通指令(v-bind, v-class, v-html…),调用相应函数. eg: v-on: click = “XXX”,在下面代码中,attrName = ‘v-on: click’(指令), exp = ‘XXX’(一般为方法),name = ‘on : click’.

compile(node) {
      var nodeAttrs = node.attributes, me = this;
      [].slice.call(nodeAttrs).forEach(function(attr) {
          var attrName = attr.name;
          if (me.isDirective(attrName)) {
              var exp = attr.value;
              var name = attrName.substring(2);
              if (me.isEventDirective(name)) compileUtils.eventHandler(node, me.$vm, exp, name);
              else {
                  compileUtils[name] && compileUtils[name](node, me.$vm, exp);
              } 
              node.removeAttribute(attrName);
          }
          
      })
  },

事件处理

eventHandler中,对传入的事件指令节点,提取事件类型,在vm的$options上找到该方法(val为函数名),对该节点添加事件监听器,回调函数即为该方法(fn)

eventHandler(node, vm, val, name) {
        let event = name.split(':')[1], 
            fn = vm.$options.methods && vm.$options.methods[val];
            
        if (event && fn) node.addEventListener(event, fn.bind(vm), false);
    },

数据绑定

对于普通指令,先调用相应函数进行数据渲染与绑定。v-model是双向数据绑定,当发现页面节点有输入且新值时,要将其设为vm对象的数据新值。

  text(node, vm, val) {
      this.bind(node, vm, val, 'text');
  },
  html(node, vm, val) {
      this.bind(node, vm, val, 'html');
  },
  model(node, vm, val) {
      var me = this;
      this.bind(node, vm, val, 'model');
      var data = this._getVMVal(vm, val);
      node.addEventListener('input', function (e) {
          let newData = e.target.value;
          if (data == newData) return;
          me._setVMVal(vm, val, newData);
          data = newData;
      });

  },
  class(node, vm, val) {
      this.bind(node, vm, val, 'class');
  },

更新视图

将数据渲染到页面上,并增加观察者,若数据发生变化,则更新页面(重新渲染),这一步靠创建新watcher,将回调函数设置为updateFn实现。(注意,代码中的val不是真正的数据值,而是属性名,所以要调用_getVMVal得到的返回值才是真正的值)

 bind(node, vm, val, name) {
     var updateFn = updater[name + 'Updaters'];
     updateFn && updateFn(node, this._getVMVal(vm, val));
     new Watcher(vm, val, function (value, oldValue) {
         updateFn && updateFn(node, value, oldValue);
     });
 },
 _getVMVal(vm, val) {
     var arr = val.split('.');
     var data = vm;
     arr.forEach((elem) => {
         data = data[elem];
     })
     return data;
 },
 _setVMVal(vm, val, newData) {
     var arr = val.split('.');
     var data = vm;
     arr.forEach(function (key, index) {
         if (index < arr.length - 1) {
             data = data[key];
         }
         else {
             data[key] = newData;
         }
     })
 }

updater

若是class,则要注意可能有多个class,(:class=“class1 class2 class3…”),所以要从中删去旧值加入新值

var updater = {
   textUpdaters(node, val) {
       node.textContent = typeof val == undefined ? '' : val;
   },
   htmlUpdaters(node, val) {
       node.innerHTML = typeof val == undefined ? '' : val;
   },
   classUpdaters(node, value, oldValue) {
       var className = node.className;
       className = className.replace(oldValue, '').replace(/\s$/, '');
       var space = className && String(value) ? ' ' : '';
       node.className = className + space + value;

   },
   modelUpdaters(node, value, oldValue) {
       node.value = typeof value == undefined ? '' : value;
   }

};

Observer

Observer类

成员属性是要观察的数据,对于数据对象的每个属性(递归遍历子属性),都应该利用Object.defineProperty来监听变化。

function Observer (data) {
    this.data = data;
    this.walk(data);
}

Observer.prototype = {
    constructor: Observer,
    walk(data) {
        let me = this;
        Object.keys(data).forEach(function(key) {
            me.convert(key, data[key]);
        });
    },
    convert(key, val) {
        this.defineReactive(this.data, key, val);
    },
    defineReactive(data, key, val) {
        var dep = new Dep();
        var childObj = observe(val);
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get() {
                if (Dep.target) {
                    dep.depend();
                }
                return val;
            },
            set(newVal) {
                if (newVal == val) return;
                val = newVal;
                childObj = observe(newVal);
                dep.notify();
            }
        });
    }
}

function observe (value, vm) {
    if (!value || typeof value !== 'object') {
        return;
    }
    new Observer(value);
}

消息订阅器

observer监听到属性变化后,通知消息订阅器。故我们还要实现一个消息订阅器,也就是上面代码中的dep。由于有很多不同的数据对象需要监听,所以订阅器要分配一个uid,并维护一个subs列表存储订阅者。在observer代码中可以看到,当get被调用,说明该属性被订阅了,调用dep.depend为这个属性添加订阅器。

uid = 0;
function Dep () {
    this.id = uid++;
    this.subs = [];
}

Dep.prototype = {
    depend() {
        Dep.target.addDep(this);
    },
    addSub(sub) {
        this.subs.push(sub);
    },
    notify() {
        this.subs.forEach(function(sub) {
            sub.update();
        })
    }
}
Dep.target = null;

Watcher

Watcher中传入vm对象,观察的属性(函数或表达式),以及变化后的回调;维护一个订阅器列表(订阅多个属性)。首先处理所观察的属性,如果是表达式(XX.XX.XX),需要调用parseGetter逐步得到该子属性,函数/属性存放在this.getter中。watcher中将原本属性旧值存放在this.val。

function Watcher (vm, expOrFn, cb) {
    this.cb = cb;
    this.vm = vm;
    this.expOrFn = expOrFn;
    this.depsId = {};
    this.getter = typeof expOrFn == 'function'? expOrFn : this.parseGetter(expOrFn.trim()); 
    this.value = this.get();
}

Watcher.prototype = {
    get: function() {
        Dep.target = this;
        var value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    },

    parseGetter(expOrFn) {
        if (/[^\w.$]/.test(expOrFn)) return;
        let exps = expOrFn.split('.');
        
        return function(obj) {
            for (var i = 0, len = exps.length; i < len; i++) {
                if (!obj) return;
                obj = obj[exps[i]];
            }
            return obj;
        }

    }
}

通过addDep将自己加入订阅器中,同时自己的depIds属性中也加入该订阅器。

addDep(dep) {
        if (!this.depsId.hasOwnProperty(dep.id)){
            dep.addSub(this);
            this.depsId[dep.id] = dep;
        }
    },

当observer观察到变化并通过dep通知到watcher,watcher执行update,比较新旧值,若不同则调用回调。

Watcher.prototype = {
    ...
    update() {
        this.run();
    },
    run() {
        var newValue = this.get();
        var oldValue = this.value;
        if (newValue !== oldValue){
            this.value = newValue;
            console.log('change');
            this.cb.call(this.vm, newValue, oldValue)
        }
        
     },
   }

MVVM

MVVM是数据绑定的入口,也就是在模仿Vue类,对传入的对象参数解析出data,method和computed,并为data和computed做数据代理, 用observe监听vm数据变化,compile编译el根节点解析模版,利用watcher实现视图与数据双向绑定。

function MVVM(options) {
    this.$options = options || {};
    var data = this._data = this.$options.data;
    var me = this;
    Object.keys(data).forEach(function(key) {
        me._proxyData(key);
    });
    this._initComputed();
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this);
}
MVVM.prototype = {
    constructor: MVVM,
    $watch: function(key, cb, options) {
        new Watcher(this, key, cb);
    },
    _proxyData(key) {
        var me = this;
        Object.defineProperty(me, key, {
            configurable:false,
            enumerable: true,
            get() {
                return me._data[key];
            },
            set(newVal) {
                me._data[key] = newVal;
            }
        })
    },
    _initComputed() {
        var me = this;
        var computed = this.$options.computed;
        if (typeof computed === 'object') {
            Object.keys(computed).forEach(function(key) {
                Object.defineProperty(me, key, {
                    configurable: false,
                    enumerable: true,
                    get: typeof computed[key] === 'function' 
                            ? computed[key] 
                            : computed[key].get,
                    set: function() {}
                });
                console.log(computed[key]);
            });
        }
        
    }
}
  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值