VUE2系列: 实现双向绑定

262 阅读3分钟

思路

思想就是:双向绑定 就是 数据 更改会响应到视图上,视图更改会响应回数据

具体到代码上就是:

  1. 通过 Object.defineProperty 监听到数据 value 的变化。
  2. 再 get 中 使Dep收集对应的wather并放入缓存列表。
  3. 再 set 中传递新值并触发Dep遍历缓存列表,将新值传递到wather更新函数并执行。

其中核心就是观察者模式的运用、递归的运用、模版正则匹配,除此之外都是为了迎合业务进行的代码。

如果你此时没有深入了解 观察者模式,不懂用递归, 建议先学学这两个,之后你再看就会觉得特别简单。

代码实现myVue

点击下面 — 查看简略版代码

点击查看 简略版代码

class myVue {
    constructor(options) {
        this.$data = options.data();
        Observe(this.$data);
        Compile(options.el, this);
    }
}
// 数据劫持
function Observe(obj) {
  if (!obj || typeof obj !== "object") return;


Object.keys(obj).forEach((key) => {
const dep = new Dep(); // 创建发布者
let value = obj[key];
Observe(value);// 递归掉用(深层调用)



// 数据劫持
Object.defineProperty(obj, key, {
  configurable: true,
  enumerable: true,
  set(newValue) {
    Observe(newValue); // 递归掉用 (当重新赋值是新对象的时候)
    value = newValue;
    dep.notify();  // 发布通知
  },
  get() {
    console.log("数据劫持 get:" + key, value);
    Dep.target && dep.addSub(Dep.target); // 收集订阅者
    return value;
  },
});




});
}




// 因为模版编译其实就是业务上处理 重点是遍历所有节点 和 正则匹配
// 我们想的简单一点 假设是对<div id='app'> {{ aaa }} </div>这个特定的就一个div 来进行响应式
function Compile(option) {
let html_ = document.querySelector(option.el)
// todo: 模版解析
html_.innerText  = option.data.aaa
new watcher(option, "aaa", (data) => {
html_.innerText = data // 将更新方法传递
})
}




// ==== 观察者 订阅者 ===
class Dep {
constructor() {
this.list = []
}
notify(newValue) {
this.list.forEach(wather => {
wather.update(newValue)
})
}
push(watcher){
this.list.push(watcher)
}
}




class watcher {
constructor(option, key, updateFunc) {
Dep.tag = this;
option.data[key]; // 进行取值操作 来触发 get 使Dep来收集watcher
this.updateFunc = updateFunc;
Dep.tag = null
}




update(v){
this.updateFunc(v);
console.log('执行更新函数');
}
}


下面的详细的代码

class MyVue {
  constructor(options) {
    this.$data = options.data();
    console.log("this.$data: ", this.$data);
    // console.log("dep: ", dep);

    // 数据劫持
    Observe(this.$data);

    // 属性代理 将vue上定义的属性之间设置到data上
    Object.keys(this.$data).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        set(newValue) {
          this.$data[key] = newValue;
        },
        get() {
          return this.$data[key];
        },
      });
    });

    // 对html进行模板编译
    Compile(options.el, this);
  }
}

// 处理指令操作
function Compile(el, vm) {
  // 获取对应的文档结构
  vm.$el = document.querySelector(el);
  // 创建文档碎片(创建一块内存,放到内存中,页面上没有,防止重排重绘)
  const fragment = document.createDocumentFragment();

  while ((children = vm.$el.firstChild)) {
    fragment.appendChild(children);
  }

  replace(fragment);
  vm.$el.appendChild(fragment);

  // 文档编译
  function replace(node) {
    // 匹配{{}}
    const regMustaChe = /\{\{\s*(\S+)\s*\}\}/;

    // 是文本节点
    if (node.nodeType == 3) {
      const text = node.textContent;
      let execResult = regMustaChe.exec(text);
      // console.log("text: ", text);
      // console.log("execResult: ", execResult);
      if (execResult) {
        let value = execResult[1]
          .split(".")
          .reduce((newObj, k) => newObj[k], vm);
        node.textContent = text.replace(regMustaChe, value);

        // 创建watcher实例
        new Watcher(vm, execResult[1], (newValue) => {
          node.textContent = text.replace(regMustaChe, newValue);
        });
      }
      return;
    }
    // input v-model 属性
    if (node.nodeType == 1 && node.tagName.toUpperCase() == "INPUT") {
      // 得到当前元素所有属性节点
      const attr = Array.from(node.attributes);
      let isHasVModel = attr.find((k) => k.name == "v-model");
      if (isHasVModel) {
        let newValue = isHasVModel.nodeValue
          .split(".")
          .reduce((newObj, k) => newObj[k], vm);
        node.value = newValue;

        new Watcher(vm, isHasVModel.nodeValue, (newValue) => {
          node.value = newValue;
        });

        // 监听数据改变
        node.addEventListener("input", (e) => {
          let newValue = e.target.value;
          let arr = isHasVModel.nodeValue.split(".");
          let obj = arr
            .slice(0, arr.length - 1)
            .reduce((newObj, k) => newObj[k], vm);
          obj[arr[arr.length - 1]] = newValue;
        });
      }
    }
    node.childNodes.forEach((e) => replace(e));
  }
}

function Observe(obj) {
  if (!obj || typeof obj !== "object") return;

  Object.keys(obj).forEach((key) => {
    const dep = new Dep();

    let value = obj[key];
    // 递归掉用(深层调用)
    Observe(value);

    // 数据劫持
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      set(newValue) {
        // 递归掉用 (当重新赋值时新对象的时候)
        Observe(newValue);
        console.log("数据劫持 set: " + key, newValue);
        value = newValue;
        dep.notify();
      },
      get() {
        console.log("数据劫持 get:" + key, value);
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
    });
  });
}
// 订阅者
class Dep {
  constructor() {
    this.sub = [];
  }

  addSub(watcher) {
    this.sub.push(watcher);
  }
  // 通告
  notify() {
    console.log("Dep notify 发布订阅");
    this.sub.forEach((watcher) => {
      watcher.upData();
    });
  }
}

// 发布者

class Watcher {
  /**
   *
   * @param {*} vm vue实例
   * @param {*} key 属性
   * @param {*} cb 更新自己
   */
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;

    // 存入
    Dep.target = this;
    key.split(".").reduce((newObj, k) => newObj[k], vm);
    Dep.target = null;
  }

  // 修改dom
  upData() {
    let newValue = this.key
      .split(".")
      .reduce((newObj, k) => newObj[k], this.vm);
    console.log("newValue: ", newValue);
    this.cb(newValue);
  }
}

收获

个人感觉死记这个实现原理没有啥用,还是推荐学设计模式,总之对我来说收获是:

    1. 它巧用wather 来触发get进而 Dep收集自己; 这种思维很好;
    1. 对于字符串 "user.name" 可以使用 key.split(".").reduce((newObj, k) => newObj[k], vm) > 按照常规方法是不是 split之后通过 forEach 来取值,但也可以通过累加方法来取值 这样更高级好看,还是要熟悉数组的常用方法。
    1. 这个递归有意思

这个递归有意思

vm.$el = document.querySelector(el);
const fragment = document.createDocumentFragment();

while ((children = vm.$el.firstChild)) {
    fragment.appendChild(children);
}