理解&实现(一):Vue2 响应式原理

685 阅读3分钟

Vue2 中,响应式实现的核心就是 ES5 的 Object.defineProperty(obj, prop, descriptor)。通过 Object.defineProperty() 劫持 data 和 props 各个属性的 getter 和 setter , getter 做依赖收集, setter 派发更新。整体来说是一个 数据劫持 + 发布-订阅者模式。

  • vue 初始化阶段( beforeCreate 之后 create 之前),遍历 data/props,调用 Object.defineProperty 给每个属性加上 getter、setter。
  • 每个组件、每个 computed 都会实例化一个 watcher (当然也包括每个自定义 watcher ),订阅渲染/计算所用到的所用 data/props/computed
  • 一旦数据发生变化, setter 被调用,会通知渲染 watcher 重新计算、更新组件。

响应式原理简单实现,设计五个类:

  • Vue:处理 options 传入的参数,把 data 中的成员转成 getter/setter
  • Observer:递归处理 data 所有属性,劫持数据 get/set 添加依赖/更新视图
  • Dep:添加 watcher ,数据变化通知所有 watcher
  • Watcher:实例化时往 dep 中添加自己,数据变化 dep 通知 watcher 更新视图
  • Compiler:解析指令/插值表达式,遇到模板依赖数据,添加 watcher

Vue

功能:

  • 记录传入的选项,设置 data,data, el
  • 把 data 中的属性注入到 Vue 实例,转换成 getter/setter
  • 调用 observer 监听 data 中所有属性的变化
  • 调用 compiler 解析指令 / 插值表达式

类属性方法:

  • $options
  • $el
  • $data
  • _proxyData()
class Vue {
  constructor(options) {
    this.$options = options || {};
    this.$data = options.data || {};
    this.$el =
      typeof options.el === "string"
        ? document.querySelector(options.el)
        : options.el;
    this._proxyData(this.$data);
    this.initMethods(options.methods || {});
    new Observer(this.$data);
    new Compiler(this);
  }

  _proxyData(data) {
    Object.keys(data).forEach((key) => {
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get() {
          return data[key];
        },
        set(newValue) {
          if (newValue === data[key]) return;
          console.log("set -> newValue", newValue);
          data[key] = newValue;
        },
      });
    });
  }

  initMethods(methods) {
    Object.keys(methods).forEach((key) => {
      this[key] = methods[key];
    });
  }
}

Observer

功能:

  • 数据劫持
    • 负责把 data 中的成员转换成 getter/setter
    • 负责把多层属性转换成 getter/setter
    • 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
  • 添加 Dep 和 Watcher 的依赖关系
  • 数据变化发送通知

类属性方法:

  • walk(data) 遍历 data
  • defineReactive(data, key, value) 响应式处理
class Observer {
  constructor(data) {
    this.walk(data);
  }

  walk(data) {
    if (typeof data !== "object" || data === null) {
      return;
    }
    Object.keys(data).forEach((key) => {
      this.defineReactive(data, key, data[key]);
    });
  }

  defineReactive(obj, key, val) {
    let self = this;
    const dep = new Dep();
    this.walk(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set(newValue) {
        if (newValue === val) return;
        val = newValue;
        self.walk(newValue);
        dep.notify();
      },
    });
  }
}

Dep

功能:

  • 收集依赖,添加观察者 (watcher)
  • 通知所有观察者

类属性方法:

  • subs 存放所有观察者
  • addSub() 添加观察者
  • notify() 事件发生时,调用所有观察者的 update 方法
class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }

  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

Watcher

功能:

  • 当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
  • 自身实例化的时候往 dep 对象中添加自己

类属性方法:

  • vm vue 实例
  • key data 中的属性名称
  • cb
  • oldValue
  • update()
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    Dep.target = this;
    this.oldValue = vm[key];
    Dep.target = null;
  }
  update() {
    const newValue = this.vm[this.key];
    if (newValue === this.oldValue) {
      return;
    }
    this.cb(newValue);
  }
}

Compiler

功能:

  • 负责编译模板,解析指令 / 插值表达式
  • 负责页面的首次渲染
  • 当数据变化后重新渲染视图

类属性方法:

  • el
  • vm
  • compile(el) 编译原生的指令
  • compileElement(node)
  • compileText(node) 编译差值表达式
  • isDirective(attrName) 判断是否是以 v - 开头的指令
  • isTextNode(node)
  • isElementNode(node)
class Compiler {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    this.compile(this.el);
  }
  compile(el) {
    let childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      if (this.isTextNode(node)) {
        this.compileText(node);
      } else if (this.isElementNode(node)) {
        this.compileElement(node);
      }
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  }
  isTextNode(node) {
    return node.nodeType === 3;
  }
  isElementNode(node) {
    return node.nodeType === 1;
  }
  compileText(node) {
    let reg = /\{\{(.+)\}\}/;
    let value = node.textContent;
    if (reg.test(value)) {
      let key = RegExp.$1.trim();
      console.log("compileText -> key", key);
      node.textContent = value.replace(reg, this.vm[key]);

      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue;
      });
    }
  }
  compileElement(node) {
    Array.from(node.attributes).forEach((attr) => {
      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        attrName = attrName.substr(2);
        if (attrName.includes("on:")) {
          const tmp = attrName.split(":");
          const name = tmp[1];
          this.onHandler(node, attr.value, name);
        } else {
          this.update(node, attr.value, attrName);
        }
      }
    });
  }
  isDirective(attrName) {
    return attrName.startsWith("v-");
  }
  update(node, key, attrName) {
    let fn = this[attrName + "Updater"];
    fn && fn.call(this, node, this.vm[key], key);
  }
  textUpdater(node, value, key) {
    node.textContent = value;
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue;
    });
  }
  modelUpdater(node, value, key) {
    node.value = value;
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue;
    });
    node.addEventListener("input", () => {
      this.vm[key] = node.value;
    });
  }
  htmlUpdater(node, value, key) {
    node.innerHTML = value;
    new Watcher(this.vm, key, (newValue) => {
      node.innerHTML = newValue;
    });
  }
  onHandler(node, value, name) {
    let modifier = "";
    if (name.includes(".")) {
      const tmp = name.split(".");
      name = tmp[0];
      modifier = tmp[1].trim();
    }

    // 动态时间处理:v-on:[event]="doThis"
    if (name.startsWith("[")) {
      name = name.slice(1, -1);
      name = this.vm[name];
    }

    let third_params = false;
    if (modifier === "capture") {
      third_params = true;
    } else if (modifier === "passive") {
      third_params = { passive: true };
    }

    const cb = (e) => {
      if (modifier === "stop") {
        e.stopPropagation();
      }
      if (modifier === "prevent") {
        e.preventDefault();
      }
      let methodName = value;
      let args = [e];
      // 处理内联语句 传递额外参数
      if (value.endsWith(")")) {
        const tmp = value.split("(");
        methodName = tmp[0];
        args = tmp[1]
          .slice(0, -1)
          .split(",")
          .map((item) => {
            item = item.trim();
            console.log("onHandler -> item", item, typeof item);
            if (item === "$event") {
              return e;
            }
            if (item.startsWith('"') || item.startsWith("'")) {
              console.log("onHandler -> item", item);
              return item.slice(1, -1);
            }
            return this.vm[item];
          });
      }
      this.vm[methodName](...args);
      if (modifier === "once") {
        node.removeEventListener(name, cb, third_params);
      }
    };
    node.addEventListener(name, cb, third_params);
  }
}

码云地址