拆解Vue2核心模块实现-组件支持

269 阅读3分钟

前言

在上篇 拆解Vue2核心模块实现-简易模板解析中实现了对简易<template>模板支持,不过遗留一个组件节点还未实现,这篇就对组件来一个简易的实现。

需求拆解

  • 全局组件,通过 Vue.use 方式注册
  • 局部组件,通过components属性注册

局部组件

Vue 基类首先增加 props 和 components 初始化属性处理

class Vue {
  constructor(options) {
    const { data, el, props, template, components } = options || {};
    this.$options = options || {};
    this.$props = props;
    this.$data = data;
    this.$template = template;
    this.$components = components;
    this.$el = typeof el === "string" ? document.querySelector(el) : el;
    // 增加props处理
    this._proxyProps(this.$props);
    this._proxyData(this.$data);
    new Observer(this.$data);
    new Compiler(this);
  }
  /**
   * 将props属性代理到this上,data、 computed、methods的实现类似
   * @param props
   */
  _proxyProps(props) {
    if (!isPlainObject(props)) {
      return;
    }
    Object.keys(props).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerableL: true, // 是否可枚举
        configurable: true, // 是否可遍历
        get() {
          return props[key];
        },
        set(val) {
          wranLog("禁止给props属性赋值");
        },
      });
    });
  }
  /**
   * 将data属性代理到this上
   * @param data
   */
  _proxyData(data) {
    if (!isPlainObject(data)) {
      return;
    }
    Object.keys(data).forEach((key) => {
      if (this[key]) {
        wranLog("data属性和props冲突");
        return;
      }
      Object.defineProperty(this, key, {
        enumerableL: true,
        configurable: true,
        get() {
          return data[key];
        },
        set(val) {
          if (val === data[key]) {
            return;
          }
          data[key] = val;
        },
      });
    });
  }
}

Compiler 改造

增加 compileComponent 处理组件节点


class Compiler {
  constructor(vm) {
    this.el = vm.$el;
    this.vm = vm;
    this.ast = parser(vm.$template);
    this.compile(this.el, this.ast);
  }
  /**
   * 编译模板,处理节点
   * @param el
   */
  compile(parent, ast) {
    const { children } = ast;
    if (children) {
      children.forEach((nodeData) => {
        const { tagName } = nodeData;
        let el;
        if (!tagName) {
          el = this.compileText(nodeData);
        } else if (tagName != "fragment") {
          const c = tagName.charAt(0);
          if (/[A-Z]/.test(c)) {
            el = this.compileComponent(nodeData);
          } else {
            el = this.compileElement(nodeData);
          }
        }

        if (nodeData.children && nodeData.children.length) {
          this.compile(el, nodeData);
        }
        if (el) {
          requestAnimationFrame(() => {
            parent.appendChild(el);
          });
        }
      });
    }
  }
  /**
   * 编译元素节点
   */
  compileElement(nodeData) {
    const { tagName, attributes } = nodeData;
    const node = document.createElement(tagName);

    Array.from(attributes).forEach((attr) => {
      let attrName = attr.name;
      if (isDirective(attrName)) {
        attrName = attrName.substr(2);
        const key = attr.value;
        this.update(node, key, attrName);
      }
    });
    return node;
  }

  /**
   * 编译组件
   */
  compileComponent(nodeData) {
    const { tagName, attributes } = nodeData;
    let props = {}
    const Com = this.vm.$components[tagName]
    if(!Com) {
      return
    }
    Array.from(attributes).forEach((attr) => {
      let attrName = attr.name;
      if (isDirective(attrName)) {
        // attrName = attrName.substr(2);
        // const key = attr.value;
        // this.update(C, key, attrName);
      } else {
        props[attrName] =  attr.value;
      }
    });
    new Com({
      el: this.el,
      props
    })
    return 
  }

  /**
   * 更新操作
   */
  update(node, key, attrName) {
    const updateFn = this[`${attrName}Updater`];
    updateFn && updateFn.call(this, node, this.vm[key], key);
  }
  /**
   * v-text 指令
   */
  textUpdater(node, value, key) {
    if(node.nodeType) {
      node.textContent = value;
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue;
      });
    } else {
      node.replaceProps(Object.assign(node.$props,))
    }
  }
  /**
   * v-model 指令
   */
  modelUpdater(node, value, key) {
    if(node.nodeType) {

    node.value = value;
      new Watcher(this.vm, key, (newValue) => {
        node.value = newValue;
      });
      node.addEventListener("input", (a) => {
        this.vm[key] = node.value;
      });
    } else {
      node
    }
  }
  /**
   * 文本节点,处理差值表达式
   */
  compileText(nodeData) {
    const { content } = nodeData;

    const node = document.createTextNode(content);
    // .:匹配任意字符,():提取内容
    const reg = /\{\{(.+?)\}\}/g;
    const value = content;
    if (reg.test(value)) {
      const key = RegExp.$1.trim();
      node.textContent = value.replace(reg, this.vm[key]);
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue;
      });
    }
    return node;
  }
}

测试

index.js 改动如下


class Button extends Vue {
  constructor(options) {
    options.template = `<button>{{text}}</button>` 
    super(options)
  }
}

new Vue({
  el: "#app",
  components: {Button},
  template: `
  <h1>template解析</h1>
  静态文本节点
  <div>{{msg}}</div>
  <input v-model="msg" type="text" />
  <Button text="按钮" />`,
  data: {
    msg: "msg",
  },
});

运行结果,Button 组件正确渲染出来了:

screenshot-20230116-112824.png

全局组件

  • Vue 基类增加以下方法
  static use(component) {
    if(!Vue.components) {
      Vue.components = {}
    }
    Vue.components[component.name] = component
  }
  getComponents(name) {
    return this.$components[name] || Vue.components[name] 
  }
  • Compiler的compileComponent改造如下
 compileComponent(nodeData) {
    const { tagName, attributes } = nodeData;
    let props = {}
    const Com = this.vm.getComponents(tagName)
    if(!Com) {
      return
    }
    Array.from(attributes).forEach((attr) => {
      let attrName = attr.name;
      if (isDirective(attrName)) {
      } else {
        props[attrName] =  attr.value;
      }
    });
    new Com({
      el: this.el,
      props
    })
    return 
  }

测试

index.js 改动如下


class Empty extends Vue {
  name = 'Empty'
  constructor(options) {
    options.template = `<div>Empty</div>` 
    super(options)
  }
}
class Button extends Vue {
  constructor(options) {
    options.template = `<button>{{text}}</button>` 
    super(options)
  }
}
Vue.use(Empty)
new Vue({
  el: "#app",
  components: {Button},
  template: `
  <h1>template解析</h1>
  静态文本节点
  <div>{{msg}}</div>
  <input v-model="msg" type="text" />
  <Button text="按钮" />
  <Empty />`,
  data: {
    msg: "msg",
  },
});

运行结果,全局组件 Empty 也正确渲染出来了:

screenshot-20230116-120110.png

props 优化

当前的 props 是静态的,需要支持 v-bind

增加 props 的处理

  • 增加 propsObserver
  • 增加 replaceProp 函数操作 props 改变
class Vue {
  constructor(options) {
    const { data, el, props, template, components } = options || {};
    this.$options = options || {};
    this.$props = props || {};
    this.$data = data;
    this.$template = template;
    this.$components = components;
    this.$el = typeof el === "string" ? document.querySelector(el) : el;
    this._proxyProps(this.$props);
    this._proxyData(this.$data);
    new Observer(this.$props);
    new Observer(this.$data);
    new Compiler(this);
  }
  static use(component) {
    if(!Vue.components) {
      Vue.components = {}
    }
    Vue.components[component.name] = component
  }
  getComponents(name) {
    return this.$components[name] || Vue.components[name] 
  }
  // props 改变
  replaceProp(key,value) {
    this.$props[key] = value
  }
  /**
   * 将props属性代理到this上,data、 computed、methods的实现类似
   * @param props
   */
  _proxyProps(props) {
    if (!isPlainObject(props)) {
      return;
    }

    Object.keys(props).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerableL: true, // 是否可枚举
        configurable: true, // 是否可遍历
        get() {
          return props[key];
        },
        set(val) {
          wranLog("禁止给props属性赋值");
        },
      });
    });
  }
  /**
   * 将data属性代理到this上
   * @param data
   */
  _proxyData(data) {
    if (!isPlainObject(data)) {
      return;
    }
    Object.keys(data).forEach((key) => {
      if (this[key]) {
        wranLog("data属性和props冲突");
        return;
      }
      Object.defineProperty(this, key, {
        enumerableL: true,
        configurable: true,
        get() {
          return data[key];
        },
        set(val) {
          if (val === data[key]) {
            return;
          }
          data[key] = val;
        },
      });
    });
  }
}

Compiler 增加 v-bind 和 props属性支持

主要是 compileComponentcompileText 的修改,增加 bindUpdater 处理 v-bind 指令

class Compiler {
  constructor(vm) {
    this.el = vm.$el;
    this.vm = vm;
    this.ast = parser(vm.$template);
    this.compile(this.el, this.ast);
  }
  /**
   * 编译模板,处理节点
   * @param el
   */
  compile(parent, ast) {
    const { children } = ast;
    if (children) {
      children.forEach((nodeData) => {
        const { tagName } = nodeData;
        let el;
        if (!tagName) {
          el = this.compileText(nodeData);
        } else if (tagName != "fragment") {
          const c = tagName.charAt(0);
          if (/^[A-Z]/.test(c)) {
            el = this.compileComponent(parent,nodeData);
          } else {
            el = this.compileElement(nodeData);
          }
        }

        if (nodeData.children && nodeData.children.length) {
          this.compile(el, nodeData);
        }
        if (el) {
          requestAnimationFrame(() => {
            parent.appendChild(el);
          });
        }
      });
    }
  }
  /**
   * 编译元素节点
   */
  compileElement(nodeData) {
    const { tagName, attributes } = nodeData;
    const node = document.createElement(tagName);

    Array.from(attributes).forEach((attr) => {
      let attrName = attr.name;
      if (isDirective(attrName)) {
        attrName = attrName.substr(2);
        const key = attr.value;
        this.update(node, key, attrName);
      }
    });
    return node;
  }

  /**
   * 编译组件
   */
  compileComponent(parent,nodeData) {
    const { tagName, attributes } = nodeData;
    const Com = this.vm.getComponents(tagName)
    if(!Com) {
      return
    }
    const C = new Com({
      el:parent
    })
    Array.from(attributes).forEach((attr) => {
      let attrName = attr.name;
      if (isBinding(attrName)) {
        if (isDirective(attrName)) {
          attrName = attrName.substr(2);
        } else {
          attrName = attrName.substr(1);
        }
        const key = attr.value;
        this.bindUpdater(C, key,attrName);
      } else {
        
        C.replaceProp(attrName, attr.value)
      }
    });
    return 
  }

  /**
   * 更新操作
   */
  update(node, key, attrName) {
    const updateFn = this[`${attrName}Updater`];
    updateFn && updateFn.call(this, node, this.vm[key], key);
  }
  /**
   * 组件动态props
   */
  bindUpdater(Com,key,propsKey) {
      let that = this.vm
      if(inObject(this.vm.$props,key)) {
       
        that = this.vm.$props
      }
      new Watcher(that, key, (newValue) => {
        Com.replaceProp(propsKey,newValue)
      });
      Com.replaceProp(propsKey,that[key])

    
  }
  /**
   * v-text 指令
   */
  textUpdater(node, value, key) {
    node.textContent = value;
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue;
    });
  }
  /**
   * v-model 指令
   */
  modelUpdater(node, value, key) {
    node.value = value;
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue;
    });
    node.addEventListener("input", (a) => {
      this.vm[key] = node.value;
    });
  }
  /**
   * 文本节点,处理差值表达式
   */
  compileText(nodeData) {
    const { content } = nodeData;

    const node = document.createTextNode(content);
    // .:匹配任意字符,():提取内容
    const reg = /\{\{(.+?)\}\}/g;
    const value = content;
    if (reg.test(value)) {
      const key = RegExp.$1.trim();
      node.textContent = value.replace(reg, this.vm[key]);
      let that = this.vm
      if(inObject(this.vm.$props,key)) {
        that = this.vm.$props
      }
      new Watcher(that, key, (newValue) => {
        node.textContent = newValue;
      });
    }
    return node;
  }
}

最终效果

index.js 改为:


class Empty extends Vue {
  name = 'Empty'
  constructor(options) {
    options.props = {text: '空'}
    options.template = `<div>{{text}}</div>` 
    super(options)
  }
}
class Button extends Vue {
  constructor(options) {
    options.props = {text: ''}
    options.template = `<button>{{text}}</button>` 
    super(options)
  }
}
Vue.use(Empty)
new Vue({
  el: "#app",
  components: {Button},
  template: `
  <h1>template解析</h1>
  静态文本节点
  动态节点<div>{{msg}}</div>
  v-model: <input v-model="msg" type="text" />
  <div>
    v-bind:<Button :text="msg" />
  </div>
  组件静态props:<Empty text="暂无数据" />`,
  data: {
    msg: "",
  },
});

运行测试如图: screenshot-20230116-211421.png 本篇相关改动代码

总结

本篇简单实现了组件支持,包括全局和局部注册,为了支持父传子,增加了 props 对应处理

上一篇:拆解Vue2核心模块实现-简易模板解析