美文网首页
手撸 Vue2

手撸 Vue2

作者: 李霖弢 | 来源:发表于2021-05-06 14:08 被阅读0次

概念说明

hijackData

劫持数据让 vm.key 代理 vm.$data.key

hijackData2 + Observer

初始化时对data下每个属性(深层)进行 defineProperty,改为 get/set 模式,并为产生一个new Dep()

  • 对象
    defineProperty 只能针对具体的某个属性,因此实际使用中如直接为data中某个对象添加不存在的属性,该属性不会自动加入响应式。Vue3 改为 Proxy 即无此问题。
  • 数组
    对 Array 原型上几个改变数组自身的内容的方法(如 push)做了拦截,因此通过调用方法修改数组可触发响应式。而直接对数组中不存在的某个index赋值则无法触发。
  • Dep
    依赖,在调用该属性get函数时,通过Dep.target.addDep(dep)被Watcher收集,在调用该属性set函数时,通过dep.notify()通知变更,遍历该dep关联的所有watcher实例并调用其接收的Updater方法;
compile

开始编译,先将dom节点都转移到内存中,深层遍历并通过正则处理其中的vue表达式。每个vue表达式根据其指令形式调用相应的Updater函数更新dom节点,并为产生一个new Watcher(),该Updater函数也会带着参数作为回调加入watcher实例

  • Watcher
    观察对象,用来接收Dep的通知。
    该实例接收vue表达式中涉及的响应式属性和Updater函数,初始化时实例指向Dep.target,然后调用响应式属性的get方法触发Dep.target.addDep(dep)(即该Watcher实例自身收集到dep),最后再将Dep.target重置
    最终Watcher和Dep实例会形成多对多的关系
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>手撸vue</title>
</head>

<body>
  <div id="app">
    name:<div>{{name}}</div>
    name:<div v-text="name"></div>
    obj.age:<div>{{obj.age}}</div>
    obj.age+obj.age2:<div>{{obj.age+obj.age2}}</div>
    html:<div v-html="html"></div>
    text:<input type="text" v-model="text">
  </div>

</body>
<script>
  //每一个依赖都对应一个Watcher,确保了每个依赖在数据改变时都能得到更新,用于实现数据响应化
  class Watcher {
    callback
    constructor(vm, exp, callback) {
      this.callback = callback;
      Dep.target = this; //把Watcher赋值给Dep.target
      new Function("vm", `with(vm){return ${exp}}`)(vm);//触发get
      Dep.target = null;
    }
    update() { //dep.notify()通知更新最终入口
      //内存 --> 视图,也是双向数据绑定中的另一方向
      this.callback(); //更新显示
    }
    //收集依赖
    addDep(dep) {//此处省略了判断是否已被收集 如已收集则不重复添加
      dep.depWatchers.push(this);
    }
  }

  class Vue {
    constructor(options) {
      this.$el = document.querySelector(options.el);
      if (!this.checkDom(this.$el)) throw "不存在该元素"

      this.$options = options;
      this.$data = options.data;
      //劫持数据让vm.key代理vm.$data.key
      this.hijackData(this.$data);

      //实现Observer观察
      this.hijackData2(this.$data);
      //编译模版
      this.compile(this.$el);

    }
    hijackData(data) {
      Object.keys(data).forEach(key => {
        this.proxyData(key);
      });
    }
    //对data中所有属性深层实现一个观察者
    hijackData2(data) {
      if (!data || typeof data !== 'object') return
      Object.keys(data).forEach(key => {
        this.hijackData2(data[key]);
        Observer(data, key);
      });
    }
    proxyData(key) {//数据代理
      Object.defineProperty(this, key, {
        enumerable: true, //可枚举
        configurable: false, //不能再配置
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
    compile(el) {
      //将this.$el中的子节点转移到内存中,document --> fragment(内存)
      this.$fragment = this.node2Fragment(this.$el);
      //编译
      this.compileElements(this.$fragment);
      //内存 --> document
      this.$el.appendChild(this.$fragment);
    }
    node2Fragment(node) {
      let child = null;
      let fragment = document.createDocumentFragment();
      //createDocumentFragment创建的虚拟节点需要插入子元素
      while (child = node.firstChild) {
        //节点有且只有一个父节点,所以是转移,不是复制,不会出现两份,
        //因此下一次while循环时child=node.firstChild为null,循环结束
        fragment.appendChild(child);
      }
      return fragment;
    }

    compileElements(vNode) {
      let text = '';
      //正则表达式,用于匹配大括号表达式
      const reg = /\{\{(.*)\}\}/;
      //转为真数组并遍历所有节点
      Array.from(vNode.childNodes).forEach(node => {
        text = node.textContent;
        if (this.isElementNode(node)) { //元素节点,解析所有的指令属性
          let exp = ''; //表达式
          let dir = ''; //指令
          let attrName = '';
          let attrs = node.attributes;
          //取出所有属性,转为数组并遍历
          Array.from(attrs).forEach(attr => {
            exp = attr.value;
            attrName = attr.name;
            // 普通指令v-text,v-html,v-model等
            if (this.isDirective(attrName)) {
              dir = attrName.substring(2);
              this.update(node, exp, dir);
              node.removeAttribute(attrName);
              //事件指令@click等
            } else if (this.isEvent(attrName)) {
              dir = attrName.substring(1);
              this.eventHandler(node, exp, dir);
              node.removeAttribute(attrName);
            }
          });
        } else if (this.isTextNode(node) && reg.test(text)) { //文本节点,大括号表达式
          this.update(node, RegExp.$1.trim(), 'text');//其实RegExp这个对象会在我们调用了正则表达式的方法后, 自动将最近一次的结果保存在里面, 所以如果我们在使用正则表达式时, 有用到分组, 那么就可以直接在调用完以后直接使用RegExp.$xx来使用捕获到的分组内容
        }
        //递归遍历所有层次的节点
        if (node.hasChildNodes()) {
          this.compileElements(node);
        }
      });
    }
    //判断是否为元素节点
    checkDom(dom) {
      if (typeof HTMLElement === 'object') {
        return dom instanceof HTMLElement;
      } else {//nodeType 1元素 2属性 3元素/属性中的字符串
        return dom && typeof dom === 'object' && dom.nodeType === 1
      }
    }
    //事件处理器
    eventHandler(node, exp, eType) {
      const callback = this.$options.methods && this.$options.methods[exp];
      callback && node.addEventListener(eType, callback.bind(this));
    }
    //更新视图,依赖的统一入口,每个引用过data中数据的依赖都会进来这里
    update(node, exp, dir) {
      //拿到相对应的更新函数
      const fn = this[dir + 'Updater'];
      //初始化更新显示,这里需要指定调用的实例this,即vue实例
      fn && fn.call(this, this, node, exp);
      //变化更新显示
      new Watcher(this, exp, () => {
        fn && fn.call(this, this, node, exp);
      });
    }
    textUpdater(vm, node, exp) {
      node.textContent = new Function("vm", `with(vm){return ${exp}}`)(vm);
    }
    htmlUpdater(vm, node, exp) {
      node.innerHTML = new Function("vm", `with(vm){return ${exp}}`)(vm);

    }
    modelUpdater(vm, node, exp) {
      node.value = new Function("vm", `with(vm){return ${exp}}`)(vm);
      //添加input事件监听
      //v-model双向数据绑定的其中一个方向,即:视图 --> 内存
      node.addEventListener('input', e => {
        // vm[exp] = e.target.value;
        eval(`vm.${exp} = e.target.value`);
      });
    }
    //判断是否为v-指令
    isDirective(node) {
      return node.startsWith("v-");
    }
    //判断是否为@事件
    isEvent(node) {
      return node.startsWith("@")
    }
    //判断是否为元素节点
    isElementNode(node) {
      return (node.nodeType === 1);
    }
    //判断是否为文本节点
    isTextNode(node) {
      return (node.nodeType === 3);
    }
  }

  //通过Observer将data下所有属性深层遍历改为get/set模式而非value模式
  function Observer(data, key) {
    let val = data[key];
    //每一个数据对应一个Dep容器,存放所有依赖于该数据的依赖项
    const dep = new Dep();
    Object.defineProperty(data, key, {
      enumerable: true, //可枚举
      configurable: false,//不能再配置
      get() {
        dep.depend();//收集依赖
        return val;
      },
      set(newVal) {
        if (newVal === val) return
        val = newVal;
        dep.notify(); //当数据发生变化时,通知所有的依赖进行更新显示
      }
    });
  }

  class Dep {
    static target//当前正在计算的watcher
    constructor() {
      this.depWatchers = []; //所有的依赖将存放在该数组中
    }
    //收集依赖
    depend() {
      if (Dep.target) {//Dep.target存放具体的依赖,在编译阶段检测到依赖后被赋值
        Dep.target.addDep(this);
      }
    }
    //通知更新
    notify() {
      this.depWatchers.forEach(depWatcher => {
        depWatcher.update();
      });
    }
  }
  var vm = new Vue({
    el: "#app",
    data: {
      name: "hello",
      obj: {
        age: 18,
        age2: 20
      },
      html: `<p>this is p</p>`,
      text: "hello"
    }
  });
  vm.obj.age = 30;
  vm.name = "Vue";
  vm.text+=" world";
  ;
</script>
</html>

一些未实现的功能

  • computed
    直接在{{}}中执行的方法/表达式每当依赖发生变化都会重新计算
    而computed的值在依赖变化时只是记录脏值,每次获取时进行脏检查,如dirty为true则触发computed的回调并将结果加入缓存,否则从缓存中读取
  • watch
    并非单纯监听set,如被监听的值在一段同步脚本中多次改变,只有最后一次改变会触发watch的回调,且在所有同步任务执行完后才触发

相关文章

网友评论

      本文标题:手撸 Vue2

      本文链接:https://www.haomeiwen.com/subject/kafllltx.html