Vue高级--双向绑定原理

2022-03-31  本文已影响0人  autumn_3d55

1.原理

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

2. 实现思路

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

上述流程如图所示:


image.png

3. 实现Observer 数据监听器

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

// 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,然后在属性变化的时候执行对象订阅者的更新函数。

// 实现消息订阅器
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;

将订阅器添加的订阅者 设计在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
// 编译指令
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上面了,请点击源码

上一篇 下一篇

猜你喜欢

热点阅读