Vue2.x 响应式原理 简单实现

1.图解说明

  • 概要
    在这里插入图片描述

2.代码共享

3.代码实现

根据上图的概要逻辑,代码进行简单实现

  • 模拟Vue实例
 /**
 * Vue模拟实现
 */
class MyVue {
  constructor(options) {
    // 1. 保存数据
    this.$options = options;
    this.$data = options.data;
    this.$el = options.el;
    if (this.$el) {
      // 2. 创建Observer来监控数据的变化
      new Observer(this.$data);
      // 3. 代理 核心就是vue.data的属性 赋值给 vue
      Object.keys(this.$data).forEach(attr => {
          this._proxy(attr);
        }
      );
      // 4. 创建Compile
      new Compiler(this.$el, this);
    }
  }
  // 对vue实例中的data对象进行代理
  _proxy(attr) {
    Object.defineProperty(this, attr, {
      enumerable: true,
      configurable: true,
      set(newValue) {
        this.$data[attr] = newValue;
      },
      get() {
        return this.$data[attr];
      }
    })
  }
}


  • 创建观察者Observer来监控数据变化

/**
 * 观察者 观察,监听
 */
class Watcher {
  constructor(vm, attrValue, callback) {
    this.vm = vm;
    this.attrValue = attrValue;
    this.callback = callback;
    // 先将旧值保存起来
    this.oldValue = this.getOldVal();
  }
  getOldVal() {
    // 给Dep设置一个target属性,指向Watcher
    Dep.target = this;
    const oldValue = commonUtils.getVal(this.attrValue, this.vm);
    // 这里需要设为Null,第一次解析后,当message值改变后
    // 还会调用get方法,那时Dep.target不为null的话会,
    // 会一直向dep.addSub添加watcher
    Dep.target = null;
    return oldValue;
  }
  update() {
    const newValue = commonUtils.getVal(this.attrValue, this.vm);
    if (newValue !== this.oldValue) {
      this.callback(newValue);
    }
  }
}
/**
 *   发布者
 */
class Dep {
  constructor() {
    this.subs = [];
  }
  // 添加观察者
  addSub(watcher) {
    this.subs.push(watcher);
  }
  // 通知观察者去更新node
  notify() {
    console.log("通知观察者", this.subs);
    this.subs.forEach(watcher => {
      watcher.update();
    })
  }
}

/**
 * Observer 监视数据变化
 */
class Observer {
  constructor(data) {
    this.observe(data);
  }
  observe(data) {
    if (data && typeof data === 'object') {
      Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key]);
      })
    }
  }
  // 监视数据 Object.defineProperty
  defineReactive(obj, key, value) {
    // 递归遍历
    this.observe(value);
    // 每一个属性,对应一个Dep对象
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        if (Dep.target) {
          // Dep.target 就是Watcher
          dep.addSub(Dep.target);
        }
        return value;
      },
      set: (newValue) => {
        this.observe(newValue);
        if (newValue === value) {
          return;
        }
        value = newValue;
        dep.notify();
      }
    })
  }
}

  • 编译模板Dom
/**
 * 1.编译dom, 初始化赋值
 * 2.新建Watcher
 */
class Compiler {
  constructor(el, vm) {
    // 获取到当前传入的dom元素。例如: #app
    this.el = commonUtils.isElementNode(el) ? el : document.querySelector(el);
    // 保存当前vue实例
    this.vm = vm;
    // 1.创建文档碎片对象,放入内存中,减少页面的回流(reflow)和重绘(repaint)
    const fragment = this._createFragment();
    // 2.编译模板
    this._compile(fragment);
    // 3.将片段加入到dom中
    this.el.appendChild(fragment);
  }
  // 创建片段
  _createFragment() {
    // 能做什么: 创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点
    // 什么特点: DocumentFragment存在于内存中,并不在DOM中
    // 为什么用: 当需要添加多个dom元素时,如果先将这些元素添加到DocumentFragment中,再统一将DocumentFragment添加到页面
    //           减少浏览器对dom渲染次数,效率会明显提升
    const frag = document.createDocumentFragment();
    let child;
    while(child = this.el.firstChild) {
      // 如果添加的child是文档中存在的元素,则通过appendChild在为其添加子元素时,会从文档中删除之前存在的元素
      frag.appendChild(child);
    }
    return frag;
  }
  // 编译模板, 匹配dom和Watcher
  _compile(fragment) {
    const childNodes = fragment.childNodes;
    childNodes.forEach(childNode => {
      if (commonUtils.isElementNode(childNode)) {
        // 是否是元素节点
        this._compileElement(childNode);
      }
      if(commonUtils.isTextNode(childNode)) {
        // 是否是文本节点
        this._compileText(childNode);
      }
      // 元素节点的子元素也取出<h2>{{msg}}</h2>
      if (childNode.childNodes && childNode.childNodes.length) {
        this._compile(childNode);
      }
    })
  }
  // 编译属性节点
  _compileElement(node) {
    // <div v-text="message"></div>
    // 为了遍历, 将伪数组转为数组
    const attributeList = Array.prototype.slice.call(node.attributes);
    attributeList.forEach(attr => {
      // attr: v-text="message"
      // 判断是否以v-开头, v-text, v-html, v-model
      if (commonUtils.isStartWithVPrefix(attr.name)) {
        // 取出text, html, model等
        const attrName = attr.name.split("-")[1];
        // 数据驱动视图 value: v-text="message"中的message等
        commonUtils[attrName](node, attr.value, this.vm);
        // 删除v-属性
        node.removeAttribute("v-" + attrName);
      }
    })
  }
  // 编译文本节点
  _compileText(node) {
    // {{}}
    const content = node.textContent;
    if (commonUtils.regex.test(content)) {
      commonUtils['text'](node, content, this.vm);
    }
  }
}
  • 共通方法等
// 共通属性, 方法定义
const commonUtils = {
  // 匹配mustache
  regex: /\{\{(.+?)\}\}/g,
  // 判断是dom中元素对象还是字符串
  isElementNode(node) {
    return node.nodeType === 1;
  },
  // 判断是dom中文本对象还是字符串
  isTextNode(node) {
    return node.nodeType === 3;
  },
  // 判断是否是v-前缀开头
  isStartWithVPrefix(attrName) {
    return attrName.startsWith("v-");
  },
  // 取出vue data中定义的属性值
  getVal(attrValue, vm) {
    return attrValue.split(".").reduce(function(result, currentValue) {
      return result[currentValue];
    },vm);
  },
  // 取出vue data中定义的属性值
  setVal(attrValue, vm, inputVal) {
    let length = attrValue.split(".").length;
    return attrValue.split(".").reduce(function (result, currentValue, currentIndex) {
      // person.name中的最后的name进行赋值
      if (length - 1 === currentIndex) {
        result[currentValue] = inputVal;
      }
      return result[currentValue];
    }, vm)
  },
  // 为了替换 {{person.name}} -- {{person.age}}
  getContentVal(attrValue, vm) {
    return attrValue.replace(this.regex, (...args) =>{
      return this.getVal(args[1], vm);
    })
  },
  // 取出文本属性值
  text(node, attrValue, vm) {
    let value;
    if (attrValue.indexOf("{{") !== -1) {
      // {{person.name}}--{{person.age}}
      let temp = this;
      value = attrValue.replace(this.regex, function (target, m1) {
        // 绑定观察者, 将来数据发生变化时会去调用
        new Watcher(vm, m1, () => {
          temp.updater.textUpdater(node, temp.getContentVal(attrValue, vm));
        });
        return temp.getVal(m1, vm);
      });
    } else {
      // v-text="person.age"中的person.age
      value = this.getVal(attrValue, vm);
      // 绑定观察者, 将来数据发生变化时会去调用
      new Watcher(vm, attrValue, (newValue) => {
        this.updater.textUpdater(node, newValue);
      });
    }
    this.updater.textUpdater(node, value);
  },
  // 取出html属性值
  html(node, attrValue, vm) {
    const value = this.getVal(attrValue, vm);
    // 绑定观察者, 将来数据发生变化时会去调用
    new Watcher(vm, attrValue, (newValue) => {
      this.updater.htmlUpdater(node, newValue);
    });
    this.updater.htmlUpdater(node, value);
  },
  // 取出model属性值
  model(node, attrValue, vm) {
    const value = this.getVal(attrValue, vm);
    // 绑定观察者, 将来数据发生变化时会去调用 数据 -> 视图
    new Watcher(vm, attrValue, (newValue) => {
      this.updater.modelUpdater(node, newValue);
    });
    //  视图 -> 数据 -> 视图
    node.addEventListener("input", e => {
      // 设置值
      this.setVal(attrValue, vm, e.target.value);
    })
    this.updater.modelUpdater(node, value);
  },
  updater: {
    textUpdater(node, value) {
      // <div v-text="message">value</div>
      node.textContent = value;
    },
    htmlUpdater(node, value) {
      // <div v-html="message">value</div>
      node.innerHTML = value;
    },
    modelUpdater(node, value) {
      // <input type="text" v-model="message">
      node.value = value;
    }
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值