阅读 395

手写mini 版 vue 响应式框架

更新记录

4.27 新增 computed 属性嵌套调用

4.26 更新了界面布局 新增利用 promise 实现 $nextTick

4.25 新增了v-if 的简单实现

总体架构

image.png

完整代码

先 copy 测试 欢迎纠错 吐槽~🐰

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <style></style>
    <div
      id="app"
      style="width:100vw;border:1px solid red;position:relative"
    >
      <h1>{{msg}}</h1>
      <h2>
        <h3>我是普通模板编译:</h3>
        <h4>我的最大年龄是<span style="color:red">{{age.max}}</span></h4>
        <h4>我的最小年龄是<span style="color:red">{{age.min}}</span></h4>
      </h2>
      <h2>
        <span>我是计算属性:</span>
        <span style="color: aqua;">{{doubleAge}}</span>
      </h2>
      <h2>
        <span>我是v-if的实现:</span>
        <span v-if="alive" style="color:red">我还活着!!!....</span>
        <hr />
        <span v-if="dead" style="color:red">我挂了!!!....</span>
      </h2>
      <h2>
        <span>我是click事件的实现,点击可以修改最大年龄为 200 岁:</span>
        <button @click="beBiggerAge">修改最大年龄</button>
      </h2>
      <h2>
        <span>我是click事件的实现,点击可以修改最小年龄为 0 岁 就挂了....</span>
        <button @click="beMinnerAge">修改最小年龄</button>
      </h2>
      <h2>
        <span
          >我是v-model 的实现,可以修改最小年龄,如果输入到 0 岁
          我就挂了...</span
        >
        <input type="text" v-model="age.min" />
      </h2>
    </div>
  </body>
</html>
<script>
  const PENDGING = 'PENDGING';
  const FULFILLED = 'FULFILLED';
  class Vue {
    constructor(options) {
      this.$options = options;
      this.callbacks = [];
      this.status = PENDGING;
      this._init();
    }
    _init() {
      this.$data = this.initData();
      this.$methods = this.$options.methods;
      this.$computed = this.$options.computed;
      this.$watch = this.$options.watch;
      new Observer(this.$data);
      this.proxyData(this.$data);
      this.proxyData(this.$methods);
      this.proxyComputed(this.$computed);
      this._watch();
      this.$options.created.apply(this);
      if (this.el) this.$options.$mount(this.el);
      this.$options.mounted.apply(this);
    }
    //借鉴 promise 实现 nextTick 暂时没有做降级处理
    $nextTick(cb) {
      this.callbacks.push(cb);
      //保证只能执行一次回调数组
      if (this.status === PENDGING) {
        executor.call(this);
        this.status = FULFILLED;
      }
      const executor = () => {
        Promise.resolve().then(() => {
          this.status = PENDGING;
          this.callbacks.forEach((cb) => cb());
        });
      };
    }
    _watch() {
      Object.keys(this.$watch).forEach((key) => {
        new Watcher(key, this, (newValue, oldValue) => {
          this.$watch[key].call(this, newValue, oldValue);
        });
      });
    }
    //代理 computed 属性
    proxyComputed(proxy) {
      Object.keys(proxy).forEach((key) => {
        Object.defineProperty(this, key, {
          get() {
            return proxy[key].call(this);
          },
          set(newValue) {
            throw new Error('computed 属性不允许改变');
          },
        });
      });
    }
    //代理this 使得可以直接访问 this.data this.method
    proxyData(proxy) {
      Object.keys(proxy).forEach((key) => {
        Object.defineProperty(this, key, {
          get() {
            return proxy[key];
          },
          set(newValue) {
            if (newValue !== proxy[key]) proxy[key] = newValue;
          },
        });
      });
    }
    //初始化 data
    initData() {
      const type = typeof this.$options.data;
      return type === 'function' ? this.$options.data() : this.$options.data;
    }
    //挂载 element
    $mount(el) {
      if (typeof el === 'string') this.$el = document.querySelector(el);
      else if (el.nodeType === 1) this.$el = el;
      else throw new Error('节点错误');
      new Compiler(this.$el, this);
    }
  }
  class Compiler {
    constructor(el, vm) {
      this.$vm = vm;
      this.$el = el;
      let fragment = this.vNodeFragment(el);
      this.compilerFragment(fragment);
      this.$el.appendChild(fragment);
    }
    vNodeFragment(el) {
      let fragment = document.createDocumentFragment(); //创建文档碎片
      while (el.firstChild) fragment.appendChild(el.firstChild);
      return fragment;
    }
    compilerFragment(fragment) {
      const childNodes = fragment.childNodes;
      childNodes.forEach((node) => {
        if (node.nodeType === 1) {
          this.compileElement(node);
          this.compilerFragment(node);
        } else {
          this.compileText(node);
        }
      });
    }
    compileText(node) {
      let reg = /\{\{(.+?)\}\}/g;
      let text = node.textContent;

      if (reg.test(text)) {
        let variable = CompilerUtils.getTextVariable(text);
        const isComputed = this.$vm.$computed[variable];
        //编辑 computed 属性
        if (isComputed) {
          CompilerUtils.computed(variable, node, this.$vm);
          // console.log(isComputed, variable)
        } else {
          CompilerUtils.text(variable, node, this.$vm);
        }
      }
    }
    compileElement(node) {
      const attrs = [...node.attributes];
      attrs.forEach((attr) => {
        const name = attr.name;
        if (name.includes('v-')) {
          //处理指令
          const value = attr.value;
          const [, type] = name.split('v-');
          CompilerUtils[type](value, node, this.$vm);
        } else if (name.includes('@')) {
          //处理事件
          const [, event] = name.split('@');
          const method = attr.value;
          // console.log(method, event, node)
          CompilerUtils['addEvent'](method, event, node, this.$vm);
        }
      });
    }
  }
  //计算属性
  class ComputedWathcer {
    constructor(variable, vm, cb) {
      this.variable = variable;
      this.vm = vm;
      this.cb = cb;
      this.value = this.getValue();
    }
    getValue() {
      Dep.target = this;
      let value = this.vm.$computed[this.variable].call(this.vm);
      Dep.target = null;
      return value;
    }
    update() {
      let newValue = this.vm.$computed[this.variable].call(this.vm);
      if (newValue !== this.value) {
        this.value = newValue;
        this.cb(newValue, this.value);
      }
    }
  }
  //模板编译工具
  const CompilerUtils = {
    domUpdater(node, { nextNode, newNode, parentNode }, exist, fromWatch) {
      if (exist) {
        fromWatch && parentNode.insertBefore(newNode, nextNode);
      } else {
        node.remove();
      }
    },
    //暂时只实现了计算属性
    if(variable, node, vm) {
      //此时还没有模板编译完 依赖于 parentNode 所以需要执行一个异步
      setTimeout(() => {
        const fn = this.domUpdater;
        const nextNode = node.nextElementSibling;
        const newNode = node.cloneNode(true);
        const parentNode = node.parentElement;
        let otherNode = { nextNode, newNode, parentNode };
        let computedIns = new ComputedWathcer(variable, vm, (nv, ov) => {
          fn && fn(node, otherNode, nv, true);
          if (nv) node = otherNode.newNode; //需要手动更新 node
        });
        fn && fn(node, otherNode, computedIns.value);
      });
    },
    addEvent(method, event, node, vm) {
      node.addEventListener(event, (...args) => {
        vm[method].apply(vm, args);
      });
    },
    textUpdater(node, value) {
      node.textContent = value;
    },
    computed(variable, node, vm) {
      let fn = this.textUpdater;
      let computedIns = new ComputedWathcer(variable, vm, (nv, ov) => {
        fn && fn(node, nv);
      });
      fn && fn(node, computedIns.value);
    },
    text(variable, node, vm) {
      let fn = this.textUpdater;
      let value = this.getValue(variable, vm);
      new Watcher(variable, vm, (newValue) => {
        fn && fn(node, newValue);
      });
      fn && fn(node, value);
    },
    getTextVariable(variable) {
      let reg = /\{\{(.+?)\}\}/g;
      let res = variable.replace(reg, ($0, $1) => $1);
      return res;
    },
    getValue(variable, vm) {
      return variable.split('.').reduce((prev, next) => {
        return prev[next];
      }, vm.$data);
    },
    setValue(variable, vm, newValue) {
      const keys = variable.split('.');
      const len = keys.length;
      keys.reduce((prev, next, index) => {
        if (index === len - 1) prev[next] = newValue;
        return prev[next];
      }, vm.$data);
    },
    inputUpdater(node, value) {
      node.value = value;
    },
    model(variable, node, vm) {
      const value = this.getValue(variable, vm);
      const fn = this.inputUpdater;
      fn && fn(node, value);
      node.addEventListener('input', (event) => {
        const newValue = event.target.value;
        if (newValue !== value) this.setValue(variable, vm, newValue);
      });
      new Watcher(variable, vm, (newValue) => {
        fn && fn(node, newValue);
      });
    },
  };
  //劫持数据 双向绑定
  class Observer {
    constructor(data) {
      this.observe(data);
    }
    observe(data) {
      if (!data || typeof data !== 'object') return;
      Object.keys(data).forEach((key) => {
        this.defineReactive(key, data[key], data);
        if (typeof data[key] === 'object') this.observe(data[key]);
      });
    }
    defineReactive(key, value, data) {
      let dep = new Dep();
      let _this = this;
      Object.defineProperty(data, key, {
        get() {
          Dep.target && dep.addSub(Dep.target);
          return value;
        },
        set(newValue) {
          if (newValue !== value) {
            value = newValue;
            _this.observe(newValue);
            dep.notify();
          }
        },
      });
    }
  }
  class Dep {
    constructor() {
      this.subs = [];
    }
    addSub(wathcer) {
      this.subs.push(wathcer);
    }
    notify() {
      this.subs.forEach((w) => w.update());
    }
  }
  class Watcher {
    constructor(variable, vm, cb) {
      this.variable = variable;
      this.vm = vm;
      this.cb = cb;
      this.value = this.get();
    }
    get() {
      Dep.target = this;
      const value = CompilerUtils.getValue(this.variable, this.vm);
      Dep.target = null;
      return value;
    }
    update() {
      let newValue = CompilerUtils.getValue(this.variable, this.vm);
      let oldValue = this.value;
      if (newValue !== oldValue) {
        this.value = newValue;
        this.cb(newValue, oldValue);
      }
    }
  }

  new Vue({
    data() {
      return {
        msg: '第一次测试',
        name: 'mike',
        age: {
          max: 100,
          min: 10,
        },
      };
    },
    watch: {
      msg(newV, oldV) {
        console.log(newV, oldV);
      },
    },
    computed: {
      dead() {
        return !this.alive;
      },
      alive() {
        return +this.age.min > 0;
      },
      doubleAge() {
        return `你好我是mike,具备双倍年龄, 今年${this.age.max * 2}岁。`;
      },
    },
    methods: {
      beMinnerAge() {
        this.age.min = 0;
      },
      beBiggerAge() {
        this.age.max = 200;
      },
    },
    created() {
      this.msg = 'created 已创建';
      console.log('created');
    },
    mounted() {
      console.log('mounted');
    },
  }).$mount('#app');
</script>
复制代码

效果图

vue.gif

文章分类
前端
文章标签