手写JS(七)--实现VUE的双向绑定

3,297 阅读2分钟
// vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的
// 劫持数据方法: Object.defineProperty()
// view改变后更新modal很简单:事件监听就可以。比如监听input标签的input事件
// 问题的关键在于modal更新后如何通知view:用Object.defineProperty()劫持数据

const observer = (obj) => {
  if (typeof(obj) !== 'object') {
    return;
  }
  for (const key in obj) {
    let temp = obj[key];
    observer(temp); // 递归遍历子属性
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      get: function() {
        // 将订阅器Dep添加订阅者的操作设计在getter里面,这是为了让Watcher初始化时进行触发,因此需要判断是否要添加订阅者。
        console.log('监听到了get:'+key);
        dep.depend();
        return temp;
      },
      set: function(value) {
        if(value === temp) {
          // 数据没有改变,不需要通知
          return;
        }
        temp = value;
        dep.notify(); // 数据改变,通知订阅者
        console.log('新值为:' + value);
      }
    });
  }
}

// 由于订阅者有很多,所以需要一个消息订阅器,用来收集订阅者,然后在属性变化的时候执行对应订阅者的更新函数。
class Dep {
  constructor() {
    this.subscribes = [];
  }
  addSubscribe = (subscribe) => {
    this.subscribes.push(subscribe);
  }
  notify = () => {
    this.subscribes.forEach((subscribe) => {
      subscribe.update();
    })
  }
   //判断是否增加订阅者
  depend = () => {
    if (Dep.target) {
      this.addSubscribe(Dep.target)
    }
  }
}

// 这是一个全局唯一 的Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算
Dep.target = null;

class Watcher {
  constructor(vm,exp,cb) {
    this.cb = cb; // Watcher绑定的更新函数
    this.vm = vm; // 一个VUE的实例对象
    this.exp = exp; // node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name
    this.value = this.get();  // 将自己添加到订阅器的操作
  }
  update = () => {
    let value = this.vm.data[this.exp];
    let oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  }
  get = () => {
    Dep.target = this;  // 缓存自己
    // 下面这个过程中会对 vm 上的数据访问,其实就是为了触发数据对象的getter
    let value = this.vm.data[this.exp]
    Dep.target = null;  // 释放自己,当前vm的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。
    return value;
  }
}

// 用法示例
<div id="name"></div>
<input type="text">

function myVue (data, el, exp) {
  this.data = data;
  observer(data);                     //将数据变的可观测
  el.innerHTML = this.data[exp];           // 初始化模板数据的值
  new Watcher(this, exp, function (value) {
    el.innerHTML = value;
  });
  return this;
}

var ele = document.querySelector('#name');
var input = document.querySelector('input');
var myVue = new myVue({
  name: 'hello world'
}, ele, 'name');

//改变输入框内容
input.oninput = function (e) {
  myVue.data.name = e.target.value
}