Vue 1.0 响应式

100 阅读2分钟

image.png

当把这张图看懂时,就能完全清楚的理解响应式了。

以前有过一些vue2响应式的学习,是粗浅的了解。只知道Object.defineProperty的方法能劫持属性的变化。并不了解dep、watcher与每个属性的关系。 watcher:个人理解,watcher就是我的某一个属性更新的时候,需要做的一些更新操作。例如total = price * count,当price和count更新时,必须让total = price * count这句代码执行才能更新total,watcher就是保存这句代码的。 dep:个人理解,dep是观察模式的重点,它里面需要维护一个任务队列,当某一个属性更新时,就需要把该属性相关的更新任务执行一遍。当然你可以让一个dep去维护所有属性的队列,当时那样是浪费的并不合适,所以是一个属性关联需要实例化一个dep。 example:<h1>{{ msg }}</h1><input type="text" v-model="msg" /> 在这个很简单的模版匹配和v-model例子中,当在get时进行Dep实例的数组中添加watcher,set时进行Dep的数组轮询执行watcher。这样很合理也很好理解。但是怎样在get的时候添加watcher呢?此时需要一个全局变量target,当模版匹配时(Compiler过程)中将watcher实例化,并赋值给target,再在get的时候添加到dep的数组中。watcher其实就是你需要数据变化时所需要更新模版的一些操作,例如更新文本节点或者更新input的value。

附上一些简单的代码,欢迎讨论

const utils = {
  getValue(key, vm) {
    return vm.$data[key.trim()];
  },
  setValue(key, vm, newValue) {
    vm.$data[key] = newValue;
  },
  textUpdater(node, text) {
    node.textContent = text;
  },
  modelUpdater(node, value) {
    node.value = value;
  },
  model(node, dataKey, vm) {
    const initValue = this.getValue(dataKey, vm);
    new Watcher(dataKey, vm, (newValue) => {
      this.modelUpdater(node, newValue);
    });
    node.addEventListener("input", (e) => {
      const newValue = e.target.value;
      this.setValue(dataKey, vm, newValue);
    });
    this.modelUpdater(node, initValue);
  },
  text(node, value, vm) {
    let result;
    if (value.includes("{{")) {
      result = value.replace(/\{\{(.+?)\}\}/g, (...args) => {
        new Watcher(args[1], vm, (newValue) => {
          this.textUpdater(node, newValue);
        });
        return this.getValue(args[1], vm);
      });
    } else {
      result = this.getValue(value, vm);
    }
    this.textUpdater(node, result);
  },
  on(node, eventName, vm, eventType) {
    const f = vm.$options.methods[eventName];
    node.addEventListener(eventType, f.bind(vm), false)
  },
};
class Watcher {
  constructor(expr, vm, cb) {
    this.expr = expr;
    this.vm = vm;
    this.cb = cb;
    this.oldValue = this.getOldValue();
  }
  getOldValue() {
    Dep.target = this;
    const oldValue = utils.getValue(this.expr, this.vm);
    Dep.target = null;
    return oldValue;
  }
  update() {
    const newValue = utils.getValue(this.expr, this.vm);
    if (newValue !== this.oldValue) {
      this.cb(newValue);
    }
  }
}
class Dep {
  constructor() {
    this.subsribes = [];
  }
  addWatcher(watcher) {
    this.subsribes.push(watcher);
  }
  notify() {
    this.subsribes.forEach((sub) => sub.update());
  }
}
class Compiler {
  constructor(el, vm) {
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    const fragment = this.compilerFragment(this.el);
    this.compiler(fragment);
    this.el.appendChild(fragment);
  }
  // 避免多次操作dom 用碎片来操作,最后一次dom操作达到多次操作的结果
  compilerFragment(el) {
    const frag = document.createDocumentFragment();
    let firstChild = null;
    // 循环来将原来dom插入到frag中
    while ((firstChild = el.firstChild)) {
      frag.appendChild(firstChild);
    }
    return frag;
  }
  compiler(el) {
    const childNodes = Array.from(el.childNodes);
    childNodes.forEach((childNode) => {
      if (this.isElementNode(childNode)) {
        this.compilerElement(childNode);
      }
      if (this.isTextNode(childNode)) {
        this.compilerText(childNode);
      }
      if (childNode && childNode.childNodes.length > 0) {
        // 元素有子元素 需要递归遍历
        this.compiler(childNode);
      }
    });
  }
  compilerElement(node) {
    const attrs = Array.from(node.attributes);
    attrs.forEach((attr) => {
      const { name, value } = attr;
      if (this.isDirector(name)) {
        // example: v-model v-on: v-bind
        const [, directive] = name.split("-");
        const [compilerKey, eventType] = directive.split(":");
        utils[compilerKey](node, value, this.vm, eventType);
      } else if(this.isEvent(name)) {
        // @click="handleClick" @change="handleChange" .......
        const eventType = name.slice(1)
        utils.on(node, value, this.vm, eventType);
      }
    });
  }
  compilerText(node) {
    const textContent = node.textContent;
    if (/^\{\{\s+(.+)\s+\}\}$/.test(textContent)) {
      utils.text(node, textContent, this.vm);
    }
  }
  isElementNode(el) {
    return el.nodeType === 1;
  }
  isTextNode(el) {
    return el.nodeType === 3;
  }
  isDirector(name) {
    return /v-(.+)/.test(name);
  }
  isEvent(name) {
    return /@(.+)/.test(name);
  }
}
class Observer {
  constructor(data) {
    this.observer(data);
  }
  observer(data) {
    if (data && typeof data === "object") {
      Object.keys(data).forEach((key) => {
        this.defineReactive(data, key, data[key]);
      });
    }
  }
  defineReactive(data, key, value) {
    this.observer(data[key]);
    const dep = new Dep();
    Object.defineProperty(data, key, {
      get() {
        const target = Dep.target;
        target && dep.addWatcher(target);
        return value;
      },
      set: (newVal) => {
        if (newVal !== value) {
          // this.xxxData = {} 需要遍历一下
          this.observer(newVal);
          value = newVal;
          dep.notify();
        }
      },
    });
  }
}
class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = options.el;
    this.$data = options.data;
    new Observer(this.$data);
    new Compiler(this.$el, this);
    this.proxyData(this.$data);
  }
  proxyData(data) {
    Object.keys(data).forEach((key) => {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        },
      });
    });
  }
}