美文网首页
Vue高级--双向绑定原理

Vue高级--双向绑定原理

作者: autumn_3d55 | 来源:发表于2022-03-31 14:13 被阅读0次

    1.原理

    vue数据双向绑 定是通过数据劫持结合发布者-订阅者模式的方式,通过 **Object.defineProperty() ** 劫持各个属性的 setter , getter , 在数据变动时发布消息给订阅者,触发相应的监听回调。

    2. 实现思路

    要实现mvvm 的双向绑定,必须实现以下几点:

    • 1 实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如果变动,可拿到最新值并通知订阅者。
    • 2 实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
    • 3 实现一个 Watcher,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
    • 4 实现一个可以容纳订阅者的消息订阅器 Dep ,订阅器 Dep 主要负责收集订阅器,然后在属性变化的时候执行对象的 订阅者的更新函数。
    • 5 实现MVVM 入口函数,整合以上三者。

    上述流程如图所示:


    image.png

    3. 实现Observer 数据监听器

    Observer 是一个数据监听器,其核心方法就是 Object.defineProperty()。如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行 Object.defineProperty()处理,如下代码:

    • Observer.js
    // 1.实现Observer观察者
    function Observer(data) {
      this.data = data;
      this.walk(data);
    }
    
    Observer.prototype = {
      constructor: Observer,
      walk: function (data) {
        let self = this;
        Object.keys(data).forEach(key => {
          self.defineReacctive(self.data, key, data[key])
        })
      },
      defineReacctive: function (data, key, val) {
        observe(val);//监听子属性
        Object.defineProperty(data, key, {
          enumerable: true,//可枚举
          configurable: false,//不能再define
          get: function () {
            return val;
          },
          set: function (newVal) {
            if (val === newVal) return;
            console.log('监听到的值发生变化了', val, '-->', newVal);
            val = newVal;
          }
        })
      }
    }
    
    function observe(data, vm) {
      if (!data || typeof data !== 'object') {
        return;
      }
      return new Observer(data)
    }
    

    4. 实现Dep 消息订阅器

    设计过程中,需要创建一个可以容纳订阅者的消息订阅器 Dep,订阅器 Dep主要负责 收集订阅器Watcher,然后在属性变化的时候执行对象订阅者的更新函数。

    • Dep.js
    // 实现消息订阅器
    function Dep() {
      this.subs = [];
    }
    Dep.prototype = {
      addSub: function (sub) {
        this.subs.push(sub)
      },
      notify: function () {
        this.subs.forEach(function (sub) {
          sub.update();
        })
      }
    }
    Dep.target = null;
    
    • 结合Dep 和Observer

    将订阅器添加的订阅者 设计在Observer的 getter里面,这是为了让 Watcher 初始化进行触发,在 Observer的setter函数里面,如果数据变化,就会通知所有订阅者,订阅者们就会执行对象的更新函数。一个比较完整的Observer 已经实现了。

    Observer.prototype = {
      constructor: Observer,
      walk: function (data) {
        let self = this;
        Object.keys(data).forEach(key => {
          self.defineReacctive(self.data, key, data[key])
        })
      },
      defineReacctive: function (data, key, val) {
        let dep = new Dep();
        observe(val);//监听子属性
        Object.defineProperty(data, key, {
          enumerable: true,//可枚举
          configurable: false,//不能再define
          get: function () {
            //将订阅者Wather赋予给 Dep.target,每个订阅者都是不一样的
            Dep.target && dep.addSub(Dep.target);//在这里添加一个订阅器
            return val;
          },
          set: function (newVal) {
            if (val === newVal) return;
            console.log('监听到的值发生变化了', val, '-->', newVal);
            val = newVal;
            dep.notify();//通知所有订阅者
          }
        })
      }
    }
    

    5. 实现Compile 编译指令

    Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面,并将每个指定对应的节点绑定更新函数,添加监听数据的Watcher订阅者,一旦数据有变动,收到通知,更新视图,如图所示:

    image.png
    • Compile.js 代码如下:
    // 编译指令
    function Compile(el, vm) {
      this.$vm = vm;
      this.$el = this.isElementNode(el) ? el : document.querySelector(el);
      if (this.$el) {
        this.$fragment = this.nodeFragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
      }
    }
    Compile.prototype = {
      constructor: Compile,
      nodeFragment: function (el) {
        let fragment = document.createDocumentFragment();
        let child;
    
        //将原生节点拷贝到fragment
        while (child = el.firstChild) {
          fragment.appendChild(child);
        }
        return fragment;
      },
      init: function () {
        this.compileElement(this.$fragment);
      },
      compileElement: function (el) {
        let childNodes = el.childNodes;
        let self = this;
        [].slice.call(childNodes).forEach(function (node) {
          let text = node.textContent;
          let reg = /\{\{(.*)\}\}/; //匹配 {{}}
    
          if (self.isElementNode(node)) {//元素节点
            self.compile(node)
          } else if (self.isTextNode(node) && reg.test(text)) { //文本节点且 {{}}
            self.compileText(node, RegExp.$1.trim());
          }
    
          if (node.childNodes && node.childNodes.length) {//拥有孩子节点,继续递归
            self.compileElement(node)
          }
        })
      },
      compile: function (node) {
        let nodeAttrs = node.attributes;
        let self = this;
    
        [].slice.call(nodeAttrs).forEach(function (attr) {
          let attrName = attr.name;
          if (self.isDirective(attrName)) {
            let exp = attr.value;
            let dir = attrName.substring(2);
    
            if (self.isEventDirective(dir)) {//事件指令
              compileUtil.eventHandler(node, self.$vm, exp, dir);
            } else {//普通指令
              compileUtil[dir] && compileUtil[dir](node, self.$vm, exp)
            }
    
            node.removeAttribute(attrName);
          }
        })
      },
    
      compileText: function (node, exp) {
        compileUtil.text(node, this.$vm, exp);
      },
      isDirective: function (attr) {//普通指令
        return attr.indexOf('v-') == 0;
      },
      isEventDirective: function (dir) {//事件指令
        return dir.indexOf('on') === 0;
      },
      isElementNode: function (node) {//元素节点
        return node.nodeType == 1;
      },
      isTextNode: function (node) {//文本节点
        return node.nodeType == 3;
      },
    
    
    }
    //指定处理集合
    let compileUtil = {
      text: function (node, vm, exp) {
        this.bind(node, vm, exp, 'text');
      },
      html: function (node, vm, exp) {
        this.bind(node, vm, exp, 'html');
      },
      model: function (node, vm, exp) {
        this.bind(node, vm, exp, 'model');
        let self = this;
        let val = this._getVMVal(vm, exp);
        node.addEventListener('input', function (e) {
          let newVal = e.target.value;
          if (val === newVal) return;
          console.log(newVal);
          self._setVMVal(vm, exp, newVal);
          val = newVal
        })
      },
      class: function (node, vm, exp) {
        this.bind(node, vm, exp, 'class');
      },
      bind: function (node, vm, exp, dir) {
        let updateFn = updater[dir + 'Updater'];
        updateFn && updateFn(node, this._getVMVal(vm, exp));
        new Watcher(vm, exp, function (value, oldValue) {
          updateFn && updateFn(node, value, oldValue)
        })
      },
    
      //事件处理
      eventHandler: function (node, vm, exp, dir) {
        let eventType = dir.split(':')[1];
        let fn = vm.$options.methods && vm.$options.methods[exp];
        if (eventType && fn) {
          node.addEventListener(eventType, fn.bind(vm), false);
        }
      },
      _getVMVal:function(vm,exp) {
        let val = vm;
        exp = exp.split('.');
        exp.forEach(function(k) {
          val = val._data[k]
        })
        return val;
      },
      _setVMVal: function(vm,exp,value) {
        let val = vm;
        exp = exp.split('.');
        exp.forEach(function(k,i) {
          //非最后一个key,更新val的值
          if(i<exp.length -1) {
            val = val._data[k];
          } else {
            val._data[k] = value
          }
        })
      },
    }
    
    let updater = {
      textUpdater: function(node,value) {
        node.textContent = typeof value == 'undefined'? '' : value
      },
      htmlUpdater: function(node,value) {
        node.innerHtml = typeof value == 'undefined'? '' : value
      },
      classUpdater: function(node,value,oldValue) {
        let className = node.className;
        className = className.replace(oldValue,'').replace(/\s$/,'');
        let space = className && String(value)? ' ': '';
        node.className = className + space +value;
      },
      modelUpdater: function(node,value) {
        node.value = typeof value == 'undefined'? '' : value
      },
    }
    

    6. 实现Watcher 订阅者

    Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
    1.在自身实例化时往属性订阅器Dep 里面添加自己:在Dep.target上缓存订阅器,通过触发 getter方法,把自己添加到getter方法里面,添加成功后去掉Dep.target。
    2.自身必须有一个update方法。
    3.待属性变动dep.notice()通知时,能调用自身的update方法,并触发Compil中绑定的回调。

    function MVVM(options) {
      this.$options = options || {};
      let data = this._data = this.$options.data;
      let self = this;
      observe(data,self);
      this.$compile = new Compile(options.el || document.body, self)
    }
    

    从上面代码可看出监听的数据对象是options.data,每次需要更新视图,则必须通过let vm = new MVVM({data:{name: 'zs'}}); vm._data.name = 'li';,这样的方式来改变数据。
    显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:
    let vm = new MVVM({data:{name: 'zs'}}); vm.name = 'li';
    所以这里我们需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理可访问 vm._data的属性,改造后的代码如下:

    function MVVM(options) {
      this.$options = options || {};
      console.log(this);
      let data = this._data = this.$options.data;
      let self = this;
      //数据代理
      //实现 vm.xxx -> vm._data.xxx
      Object.keys(data).forEach(function (key) {
        self._proxyData(key)
      });
      this._initComputed();
      observe(data);
      this.$compile = new Compile(options.el || document.body, this)
    }
    
    MVVM.prototype = {
      constructor: MVVM,
      $watch: function (key, options, cb) {
        new Watcher(this, key, cb);
      },
      _proxyData: function (key, setter, getter) {
        let self = this;
        setter = setter ||
          Object.defineProperty(self, key, {
            enumerable: true,
            configurable: false,
            get: function proxyGetter() {
              return self._data[key]
            },
            set: function proxySetter(newVal) {
              self._data[key] = newVal;
            }
          })
      },
      _initComputed: function () {//添加计算属性
        let self = this;
        let computed = this.$options.computed;
        if (typeof computed === 'object') {
          Object.keys(computed).forEach(function (key) {
            Object.defineProperty(self, key, {
              get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
              set: function() {}
            })
          })
        }
      },
    }
    

    这里主要利用了 Object.defineProperty() 这个方法来劫持 vm实例对象的属性的读写权,使读写vm实例的属性 转成了 vm._data的属性值。

    7. html

    <body>
      <div id="app">
        <h1 id="name">{{name}}</h1>
        <input type="text" v-model="msg">
        <p>{{msg}}</p>
        <button v-on:click="clickHandle">change</button>
      </div>
    </body>
    <script src="./Dep.js"></script>
    <script src="./Observer.js"></script>
    <script src="./Watcher.js"></script>
    <script src="./Compile.js"></script>
    <script src="./MVVM.js"></script>
    <script>
      let vm = new MVVM({
        el: '#app',
        data: {
          msg: 'hello'
        },
        methods: {
          clickHandle: function() {
            this.msg = 'hi';
            console.log(this);
          }
        },
      })
    </script>
    

    效果图:


    image.png

    最后,源码都放在gitee上面了,请点击源码

    相关文章

      网友评论

          本文标题:Vue高级--双向绑定原理

          本文链接:https://www.haomeiwen.com/subject/jqwgjrtx.html