手写Vue(理解双向绑定,指令解析,Watcher,Dep)

121 阅读1分钟

学习原理的总结

TODO: 下一篇把vue中的虚拟dom算法手写学习下

<div id="app">
  <input type="text" v-model="name" />
  <div style="margin: 20px 0 20px 0" v-html="name"></div>
  <button v-html="name" @click="changeit"></button>
  <p>{{name}}</p>
</div>

<script>
  //负责收集更新函数,属于发布订阅模型中的调度中心
  class Dep {
    constructor() {
      this._listeners = [];
    }

    add(watcher) {
      this._listeners.push(watcher);
    }

    notify() {
      this._listeners.forEach(watcher=> watcher.update());
    }
  }

  //观察者,直接跟DOM交互,负责将更新函数传递给Dep
  class Watcher {
    constructor(vm,key,updateFunction) {
      this.vm = vm
      this.key = key
      this.updateFn = updateFunction

      Dep.target = this // 这就是相当于一个标记
      this.vm[this.key] // 这一步会跳转到observerData中的get部分,触发dep.add()方法
      Dep.target = null //设置为null,在Dep.notify方法执行循环的时候,触发observerData的get时不会再次add
    }

    update(val){
      this.updateFn.call(this.vm,this.vm[this.key],val)
    }
  }

  class Vue {
    constructor(options) {
      this.$options = options;
      this.$data = options.data();
      this.observerRoot(); //劫持根实例
      this.observerData(this.$data); //劫持$data
      this.createFragment(); //创建虚拟元素,避免频繁操作真实DOM
      this.compile(); //编译元素,解析指令、事件、方法
    }

    compile() {
      // 1、解析
      this._compileElement(this.$fragment); //解析元素
      // 2、重新append到this.$el下
      this.$el.appendChild(this.$fragment);
    }

    _compileElement(ele) {
      Array.from(ele.childNodes).forEach((node) => {
        //解析节点
        if (node.childNodes) this._compileElement(node);
        if (node.nodeType === 1) {
          this._compileNode(node);
        } else if (this.isInter(node)) {
          this.compileText(node);
        }
      });
    }

    // 编译文本
    compileText(node) {
      this.update(node, RegExp.$1, "text");
    }

    // 判断插值表达式
    isInter(node) {
      return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }

    //{{}}
    text(node, exp) {
      this.update(node, exp, "text");
    }

    textUpdater(node, val) {
      node.textContent = val;
    }

    //v-html
    html(node, exp) {
      this.update(node, exp, "html");
    }

    htmlUpdater(node, val) {
      node.innerHTML = val;
    }
    //v-model
    model(node, exp) {
      this.update(node, exp, "model");
    }
	// 面试也会考到这里关于v-model
    modelUpdater(node, val, exp) {
      node.oninput = (e) => {
        this.$data[exp] = e.target.value || "";
      };
      node.value = val;
    }

    update(node, exp, dir) {
      const fn = this[dir + "Updater"];
      fn && fn.call(this, node, this.$data[exp], exp);
      //观察者,
      new Watcher(this,exp,function(val){
        fn&& fn.call(this,node,val,exp)
      }); 
    }

    //解析节点
    _compileNode(node) {
      //1、解析节点包含的指令、事件、等等...
      let res = this._checkHasBind(node);
      //2、处理解析结果
      this._resolveBind(node, res);
    }

    //解析单个节点的指令、事件
    _checkHasBind(node) {
      let attributes = node.attributes; //节点上的所有属性
      //指定正则
      let dir_reg = /^v\-\w*$/;
      //事件正则
      let event_reg = /^\@\w/;

      let result = {
        directives: [], //指令数组
        events: [], //事件数组
      };

      //循环节点属性,分成指令和事件两部分
      if (attributes)
        Array.from(attributes).forEach((item) => {
          if (dir_reg.test(item.name))
            result.directives.push({ name: item.name, value: item.value });
          if (event_reg.test(item.name))
            result.events.push({ name: item.name, value: item.value });
        });
      return result;
    }

    // 处理单个节点解析的结果
    _resolveBind(node, res) {
      let _this = this;
      let data = this.$data;
      let { directives, events } = res;

      //解析绑定事件
      events.length &&
        events.forEach((item) => {
          let method_name = item.value;
          let target_event = item.name.slice(1, item.name.length);
          node.addEventListener(target_event, () => {
            this.$options.methods[method_name].call(this);
          });
        });

      //解析指令
      directives.length &&
        directives.forEach((item) => {
          const dir = item.name.substring(2);
          let update = () => {
            this[dir] && this[dir](node, item.value);
          };
          update();
        });
    }

    createFragment() {
      this.$el = document.querySelector(this.$options.el); //根节点
      this.$fragment = document.createDocumentFragment(); //虚拟节点
      /**
       * 如果根节点拥有第一个子节点
       * 就把这个子节点放到fragment里
       * fragment.appendChild具有移动性
       * 慢慢抽取$el中的几点存放在fragment中
       * 这样就不用动真实的节点而在fragment中操作
       * 性能会更好一点
       */
      while (this.$el.firstChild) {
        this.$fragment.appendChild(this.$el.firstChild);
      }
    }

    //劫持根实例,代理this
    //有人问过个问题:为什么不直接this.$data.xx这样取变量而要使用this.xx
    //框架给使用的人减少负担,使用的人还问框架为什么你要帮我提高效率
    //换成你是框架,你会放下手上那40厘米的大砍刀么
    observerRoot() {
      Object.keys(this.$data).forEach((item) => {
        Object.defineProperty(this, item, {
          enumerable: true, //是否可以遍历
          configurable: false, //是否可以再次调用defineProperty
          get() {
            return this.$data[item];
          },
          set(newVal) {
            this.$data[item] = newVal;
          },
        });
      });
    }

    //劫持$data
    observerData(obj) {
      if (!obj || typeof obj !== "object") return;
      Object.keys(obj).forEach((item) => {
        let val = obj[item]; // 内存地址指向,重复get,内存溢出
        if (typeof val === "object") {
          //是对象则递归调用遍历自身属性
          this.observerData(val);
        } else {
          let dep = new Dep();
          Object.defineProperty(obj, item, {
            enumerable: true, //是否可以遍历
            configurable: false, //是否可以再次调用defineProperty
            get() {
              Dep.target && dep.add(Dep.target);

              return val;
            },
            set(newVal) {
              val = newVal; // 引用类型指针,修改了val其实等于修改了堆里的同一个值,而且不会重复触发set
              dep.notify();
            },
          });
        }
      });
    }
  }
</script>

<script>
  let app = new Vue({
    el: "#app",
    data() {
      return {
        name: "xiaoming",
      };
    },
    methods: {
      changeit() {
        this.name = Math.random();
      },
    },
  });
</script>