Skip to main content

Vue 的响应式原理

· 3 min read
Lex
Front End Engineer @ Baoxiaohe

解析#

Vue 使用大篇幅介绍了他的响应式原理,So let's look how it work.

首先问两个问题:

  • data 对象中的数据发生改变,Vue 是如何得知的(watch)
  • 好的,假设 Vue 知道哪些数据 change 了,那怎么通知对应的 DOM 发生更新呢

我可真会提问题 🤷‍♂️

So,应该知道的是在 JavaScript 中有监听数据变化的 API Object.defineProperty 和 ES6 的 Proxy 了,

Object.defineProperty

事实上,Vue2 就是通过前者实现的,just like

const data = {  message: "可变数据",  date: "June 27,2021",};
Object.keys(data).forEach((key) => {  let value = data[key];
  Object.defineProperty(data, key, {    set(newValue) {      value = newValue;    },    get() {      return value;    },  });});

好的,第一个问题解决了。接下来要通知谁呢,换言之就是如何收集依赖,这个过程用到了发布/订阅者模式。 为了记录谁订阅了属性的改变,我们定义一个 Dep 类来管理依赖 ✍

// 发布者// Dep类管理依赖class Dep {  constructor() {    this.subscriptions = [];  }
  addSub(watcher) {    this.subscriptions.push(watcher);  }
  notify() {    this.subscriptions.forEach((item) => {      item.update();    });  }}
// 订阅者// 中介类Watcher负责将更新通知到其他地方class Watcher {  constructor(name) {    this.name = name;  }
  update() {    console.log(this.name + "已经update");  }}
// 封装数据监测Object.defineProperty(data, key, {  set(newValue) {    value = newValue;    dep.notify();  },  get() {    return value;  },});

Vue响应式原理的解析过程 这就是 Vue 响应式的大致原理。

实现#

通过 120 行 JavaScript 代码实现 Vue 的双向绑定 🎉🎉

class Vue {  constructor(options) {    this.$options = options;    this.$data = options.data;    this.$el = options.el;
    new Observer(this.$data);    // 代理this.$data的数据    Object.keys(this.$data).forEach((key) => this._proxy(key));    // 处理el挂载 传入Vue实例    new Compiler(this.$el, this);  }
  _proxy(key) {    Object.defineProperty(this, key, {      configurable: true,      enumerable: true,      set(newValue) {        this.$data[key] = newValue;      },      get() {        return this.$data[key];      },    });  }}
class Observer {  constructor(data) {    this.data = data;
    Object.keys(data).forEach((key) =>      this.defineReactive(this.data, key, data[key])    );  }
  defineReactive(data, key, val) {    // 为每个属性key创建一个Dep对象    const dep = new Dep();
    Object.defineProperty(data, key, {      enumerable: true,      configurable: true,      get() {        if (Dep.target) {          dep.addSub(Dep.target);        }        return val;      },      set(newValue) {        if (newValue === val) {          return;        }        val = newValue;        dep.notify();      },    });  }}
class Dep {  constructor() {    this.subs = [];  }
  addSub(sub) {    this.subs.push(sub);  }
  notify() {    this.subs.forEach((sub) => sub.update());  }}
const reg = /\{\{(.+)\}\}/;class Compiler {  constructor(el, vm) {    this.el = document.querySelector(el);    this.vm = vm;
    this.frag = this._createFragment();    this.el.appendChild(this.frag);  }
  _createFragment() {    const frag = document.createDocumentFragment();    let child;    while ((child = this.el.firstChild)) {      this._compile(child);      frag.appendChild(child);    }    return frag;  }
  _compile(node) {    if (node.nodeType === 1) {      const attrs = node.attributes;      if (attrs.hasOwnProperty("v-model")) {        const name = attrs["v-model"].nodeValue;        node.addEventListener("input", (e) => (this.vm[name] = e.target.value));      }    }    if (node.nodeType === 3) {      if (reg.test(node.nodeValue)) {        const name = RegExp.$1.trim();        new Watcher(node, name, this.vm);      }    }  }}
class Watcher {  constructor(node, name, vm) {    this.node = node;    this.name = name;    this.vm = vm;    Dep.target = this;    this.update();    Dep.target = null;  }
  update() {    this.node.nodeValue = this.vm[this.name];  }}

That's all🖼