Bootstrap

Proxy 实现简易 toy-vue

一、主要功能

  • 实现响应式
  • 处理 {{ }}
  • 实现 v-model
  • 实现 v-bind
  • 实现 v-on

二、实现思路

实现响应式

通过使用 Proxy 实现数据劫持 ,通过 effect 函数实现数据响应,从而更新页面内容.

  • get 阶段
    • get 中保存当前依赖到 effects 上,由于 effectsMap 结构,于是可以将当前被访问对象当成对应 key 进行保存,其对应的 vlaue 初始为 Array,并根据 get 触发时将 当前的 currentEffect 存存储在 effects 中与当前对应的 effects[key] 中.
  • set 阶段
    • 当对被劫持的数据对象进行赋值更新时,会触发 set, 在 set 中会取出 effects 中与当前对应的依赖数组遍历执行,这一步是为了将更新后的数据同步到视图进行更新.

处理 tempalte 模板

当进行 new ToyVue 时,会通过模板中的 el 获取到真实的 dom,这样是为了便于在 effect 中最直接的更新视图.

  • 获取到真实 dom 后,可以获取 dom 元素的文本节点,将文本节点中被 {{}} 包裹的内容替换成 data 中的值
  • 基于 Dom Api 可以获取到对应的 dom 属性,包括自定义指令 v-bind v-on v-model
    • 对于 v-bind 的处理,这里直接将 bind 的名称和数据内容,设置成为 dom 上的一个属性名和属性值
    • 对于 v-on 的处理,通过 addEventListenerdom 实现事件注册,同时要保证 methods 中所有方法的 this 指向问题,这里默认让 this 指向了 this.data
    • 对于 v-model 的处理,首先为 dom 设置 value 属性并指向 v-model 中对应 data 的值,同时为 dom 注册对应的事件,这里默认注册 input 事件,并在 input 事件中更新 data 中的值
  • 遍历处理所有子节点,进行上述处理

三、具体实现

html

<div id="app" style="text-align: center;">
  <h1>v-model</h1>
  <p>{{ msg }}</p>
  <input type="text" v-model="msg" />
  <hr />
  <h1>v-bind</h1>
  <p v-bind:title="title">{{ title }}</p>
  <hr />
  <h1>v-on</h1>
  <p>{{ reverseMessage }}</p>
  <p v-on:click="reverseMessage">反转</p>
</div>
<script type="module">
  import { ToyVue as Vue } from './toy-vue.js'
  var app = new Vue({
    el: '#app',
    data: {
      title: 'this is a title !!!',
      msg: 'this is a message !!!',
      reverseMessage:'this is reverseMessage !!!'
    },
    methods: {
      reverseMessage(){
        this.reverseMessage = this.reverseMessage.split('').reverse().join('');
      }
    }
  });
</script>

toy-vue

export class ToyVue {
  constructor(config) {
    // 1. 模板 -> 真实 dom
    this.tempalte = document.querySelector(config.el);
    // 2. data -> 响应式 data
    this.data = reactive(config.data);
    // 3. 处理 methods
    this.handleMethods(config.methods);
    // 4. 处理 template
    this.traversal(this.tempalte);
  }

  handleMethods(methods) {
    for (let name in methods) {
      // 保证方法中的 this 指向为 this.data
      this[name] = methods[name].bind(this.data);
    }
  }

  traversal(node) {
    // 1. 处理文本节点,处理 {{}}
    if (node.nodeType === Node.TEXT_NODE) {
      console.log("textContent = ", node.textContent);
      if (node.textContent.trim().match(/^{{([\s\S]+)}}$/)) {
        let name = RegExp.$1.trim();
        // 1.1 替换 {{ msg }} 为 data.msg
        effect(() => {
          node.textContent = this.data[name];
        });
      }
    }
    // 2. 处理 dom 元素 attributes 属性, 处理指令
    if (node.nodeType === Node.ELEMENT_NODE) {
      let attributes = node.attributes;
      for (let attribute of attributes) {
        console.log("attribute = ", attribute);
        // 2.1 处理 v-model
        if (attribute.name === "v-model") {
          let name = attribute.value;
          // 2.1.1 更新 dom 元素 value
          effect(() => {
            node.value = this.data[name];
          });
          // 2.1.2 给 dom 元素注册事件,根据不同表单类型注册不同事件
          node.addEventListener("input", () => {
            this.data[name] = node.value;
          });
        }
        // 2.2 处理 v-bind
        if (attribute.name.match(/^v\-bind:([\s\S]+)$/)) {
          let attrName = RegExp.$1;
          let name = attribute.value;
          console.log("v-bind = ", attrName, name);
          // 2.2.1 为 dom 元素设置对应属性和属性内容
          effect(() => {
            node.setAttribute(attrName, this.data[name]);
          });
        }
        // 2.3 处理 v-on
        if (attribute.name.match(/^v\-on:([\s\S]+)$/)) {
          let attrName = RegExp.$1;
          let name = attribute.value;
          console.log("v-on = ", attrName, name);
          // 2.2.1 为 dom 元素注册事件
          effect(() => {
            node.addEventListener(attrName, this[name]);
          });
        }
      }
    }
    // 3. 递归处理子节点
    if (node.childNodes && node.childNodes.length) {
      for (let child of node.childNodes) {
        this.traversal(child);
      }
    }
  }
}

let effects = new Map();
let currentEffect = null;

// 收集依赖
function effect(fn) {
  currentEffect = fn;
  fn(); // 初始化执行,目的是为了自动收集依赖 depends
  currentEffect = null;
}

// 响应式
function reactive(traget) {
  let observe = new Proxy(traget, {
    get(object, prop) {
      console.log("get = ", object, prop);
      // 1. 当前 currentEffect 存在,证明当前 effect 中依赖了当前响应式数据中的属性,此时要收集依赖
      if (currentEffect) {
        // 1.1 当前 effects 不存在和 object 对应属性, 收集依赖
        if (!effects.has(object)) {
          effects.set(object, new Map());
        }
        // 1.2 从 effects 中获到对应依赖项,判断当前访问 key 是否已存在对应依赖中,不存在则给定初始值为 array ,方便之后添加和删除
        if (!effects.get(object).has(prop)) {
          effects.get(object).set(prop, new Array());
        }
        // 1.3 将当前 currentEffect 存储在对应的 effects[object][prop] 中
        effects.get(object).get(prop).push(currentEffect);
      }

      // 2. 返回当前访问属性对应值
      return object[prop];
    },
    set(object, prop, value) {
      console.log("set = ", object, prop, value);
      // 1. 更新当前属性对应值
      object[prop] = value;

      // 2. 根据收集的依赖,执行 effect
      if (effects.has(object) && effects.get(object).has(prop)) {
        for (let effect of effects.get(object).get(prop)) {
          effect();
        }
      }

      return true;
    },
  });

  return observe;
}

;