Vue组件继承与扩展

Vue组件继承与扩展

前言

与Class继承类似,在Vue中可以通过组件继承来达到复用和扩展基础组件的目的,虽然它可能会带来一些额外的性能损耗和维护成本,但其在解决一些非常规问题时有奇效。本文将通过一些非常规的功能需求来讨论其实现过程。

基础实现

进入正题之前,我们先来看一下Vue2中是如何实现组件逻辑、数据状态复用的(Vue3中推荐使用组合式API,因此不再说明)。

Props

基础组件内容如下,根据传入的type显示不同内容

<template>
  <div>
    <div v-if="type == 1">内容1</div>
    <div v-else-if="type == 2">内容2</div>
    <div v-else-if="type == 3">内容3</div>
  </div>
</template>
<script>
export default {
  props: ['type']
}
</script>

父组件使用

<base-component :type="1"></base-component>
<base-component :type="2"></base-component>

在这里插入图片描述

这种方式存在明显的问题:组件内如果存在大量条件判断,可读性和可维护性会变差

Slot

基础组件内容如下,父组件可在指定位置自定义内容

<template>
  <div>
    <slot>默认内容</slot>
    <slot name="footer"></slot>
  </div>
</template>

父组件使用

<base-component>
    替换默认内容
    <template slot="footer">
		<button>底部插入按钮</button>
    </template>
</base-component>

在这里插入图片描述

这种方式也存在一个问题:slot内元素从属于父组件的上下文,某些场景下不易拆分逻辑

Mixin

混入 (mixin) 提供了一种非常灵活的方式来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

假设我们有多个页面需要用到同一个方法,就可以将其抽离到单独文件中

export default {
  data(){
    return {
      count: 0
    }
  },
  methods: {
    increment(){
      this.count++;
    },
    decrement(){
      this.count--;
    }
  }
}

然后在需要使用的组件中混入即可

<template>
  <div>
    <div>{{ count }}</div>
    <el-button size="small" @click="increment">数量增加</el-button>
    <el-button size="small" @click="decrement">数量减少</el-button>
  </div>
</template>

<script>
import countMix from './count-mix'
export default {
   mixins: [ countMix ]
}
</script>

在这里插入图片描述

如果组件内选项与mixin冲突,一般遵循如下规则(也可以通过自定义合并策略改变默认行为,这里不做赘述)

  • 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先

  • 同名钩子函数将合并为一个数组,因此都将被调用(混入对象的钩子将在组件自身钩子之前调用)

  • 值为对象的选项(如 methods、components 和 directives)将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

当然mixin也存在一些问题:

  • 多个mixin之间命名冲突
  • 难以定位异常报错来源

SlotScope

有些情况下需要让插槽内容能够访问子组件中的数据,进行自定义展示而非固定显示,此时作用域插槽就派上用场了。

基础组件内容如下,对外暴露一些数据

<template>
  <div>
    <slot :user="user"></slot>
  </div>
</template>
<script>
  export default {
    data(){
      return{
        user:{
          name: '张三',
          age: 18,
          gender: '男'
        }
      }
    }
  }
</script>

父组件中使用

<template>
  <div>
    <base-comp>
      <template slot-scope="{user}">
        <div>姓名:{{ user.name }}</div>
        <div>年龄:{{ user.age }}</div>
        <div>性别:{{ user.gender }}</div>
      </template>
    </base-comp>
  </div>
</template>
<script>
  export default {
    components:{
      baseComp:()=>import('./base-comp.vue')
    }
  }
</script>

在这里插入图片描述

在这个例子中,基础组件仅对外提供数据,实际上是不需要定义模板的。针对这种情况,无渲染组件将会是一个很好的方式。

基础组件内容改写如下,使用render代替template,父组件使用方式相同。

<script>
export default {
  data() {
    return {
      user: {
        name: '张三',
        age: 18,
        gender: '男'
      }
    }
  },
  render() {
    return this.$scopedSlots.default({
      user: this.user
    })
  }
}
</script>

因无渲染组件与模板无关,仅提供数据,因此非常灵活,可*组合实现不同展示。但其并不像前几种方式通用,所以一般仅用于组件库开发(下面单独介绍)。

扩展方法

上面几种方式是日常开发中较为常用的实现组件复用和扩展的方式,应对绝大多数开发场景是没问题的。但在一些特殊情况下似乎就不够用了,比如移除项目中所有输入框内容的前后空格。显然我们不可能逐个页面去处理,此时需要考虑如何全局改造。

上述几种实现方式在处理自己封装的组件时非常有效,但在处理第三方组件时似乎就不太好用了,我们没办法直接修改三方组件代码来为我们的实际需求服务。

比如下面这种实现方式:二次封装输入框组件my-input,替换原有组件el-input。

<template>
  <el-input v-model.trim="newValue" @change="handleChange"></el-input>
</template>
<script>
  export default {
    name: 'my-input',
    data(){
      return {
        newValue: ''
      }
    },
    props: {
      value: {
        type: String,
        default: ''
      }
    },
    watch:{
      value(val){
        this.newValue = val;
      }
    },
    methods: {
      handleChange(val){
        this.$emit('input', val)
      }
    }
  }
</script>

看似没有问题,但我们仍然需要确认几个关键问题,比如

  • 全局替换的工作量和覆盖率
  • 做了一层封装会不会对原功能造成影响,比如一些自定义事件会不会被覆盖

显然我们不能保证其完全没问题,因此我们需要一些更加合理且精简的做法。接下来以这个输入框的需求来介绍几种常见的实现思路。

Fork+PR

拉取对应的第三方包的源代码仓库,修改源代码后发布到公服(非同名)或私服(非同版本)即可。这种方式较为常规且简单,但我们需要考虑两种情况:

如果需求点是一个稳定Bug或者通用需求,就可以提交一个PR。如果你的PR被作者接受并且合并到主线版本,那么就可以把项目中的包换回官方的包,而无需继续维护自己的版本。

而如果需求点仅仅是自己项目的定制化需求,那么提PR显然就不合理了。而单独维护自己的包又会涉及到同步官方版本等相关问题,后续处理相对麻烦。

就像上面提到移除输入框前后空格的需求,单独为了这个点而维护一个包,显然得不偿失。此时可以考虑一种较为简单的做法:源码补丁。

patch-package

即在修改node_modules中包的源码后,将修改的部分进行打补丁操作(生成对应的补丁文件),方便团队共享修改内容。简单使用方法如下

修改包源码
// node_modules/element-ui/lib/input.js
created: function created() {
     this.$on('inputSelect', this.select);
+    this.$on('change',(value)=>{
+        this.$emit('input', value.trim());
+    })
},
安装依赖
npm install patch-package --save-dev
或者
yarn add patch-package postinstall-postinstall
生成补丁
// 添加命令 package.json
"scripts": {
+    "postinstall": "patch-package"
}
// 执行命令 npx patch-package package-name
npx patch-package element-ui

在这里插入图片描述

验证

删除node_modules,重新安装依赖,此时会自动执行命名,将补丁内容更改到源码中

在这里插入图片描述
在这里插入图片描述

这种实现方式建议在依赖包版本锁定的情况下使用,否则会导致一些异常。

Coverage

如果觉得打补丁的方式还是太麻烦,还有一种简单粗暴的实现方式:通过同名组件覆盖的方法来替换掉三方库的组件。简单实现如下

复制node_module/element-ui/packages/input/src/input.vue文件到本地,根据需求修改代码

// src/components/input.vue
created: function created() {
     this.$on('inputSelect', this.select);
+    this.$on('change',(value)=>{
+        this.$emit('input', value.trim());
+    })
},

全局注册同名组件

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI)
// 注册同名组件
import Input from './components/input.vue'
Vue.component('ElInput',Input)

同样建议在依赖包版本锁定的情况下使用,否则会导致一些异常。

这种实现方式在实际开发中非常有效,比如在三方组件中多加几个插槽、改动DOM结构,如果直接改动源码,代价很大;而复制一份组件代码到本地修改,全局注册覆盖,则非常简便。当然为了避免影响到其它功能,也可以不用同名,完全当做一个新的组件处理。

Component.extends

如果你觉得上面几种方式还是太复杂,不够优雅,那么我们可以用到官方提供的一个组件扩展方式:extends。

extends允许一个组件扩展另一个组件,继承其组件选项,从实际效果上看几乎与mixins一致,但两者的含义完全不同,mixins主要用于组合功能块,而extends主要用于继承(合并策略一致)。一般使用形式如下

const CompA = { ... }

const CompB = {
  extends: CompA,
  ...
}

由此上述需求就可以改写为

import ElementUI from 'element-ui'
// 扩展
Vue.component('el-input', {
    extends: ElementUI.Input,
    created(){
        this.$on('change', (value) => {
            this.$emit('input', value.trim())
        })
    }
})

是不是相当简便?没有额外的操作,几行代码就可以替换上面几种方式的所有操作。

利用这种方式可以处理很多特殊场景,如下拉框因为某条数据过长导致宽度很大,影响样式美观

在这里插入图片描述

我们希望下拉那部分的宽度要和上方保持一致,就可以这么处理

// 扩展el-select,设置下拉宽度
Vue.component('el-select', {
    extends: ElementUI.Select,
    mounted(){
        // 设置下拉宽度与上方输入框一致
        this.$refs.popper.$el.style.width = `${this.$el.offsetWidth}px`;
    }
})
// 扩展el-option,设置超长tip
Vue.component('el-option', {
    extends: ElementUI.Option,
    mounted(){
        // 设置超长的title
        this.$el.setAttribute('title',this.currentLabel||'')
    }
})

在这里插入图片描述

这种实现方式可以应对除修改template外的几乎所有需求,且非常高效。

Render

如果觉得上面几种实现还是不够灵活的话,那么render将会是一个终极解决方案(这一节仅作基础知识点说明,实际应用在后面两节中体现)。

在绝大多数情况下Vue 推荐使用模板来创建HTML,但在一些场景中需要JavaScript的完全编程能力,这时可以用渲染函数,它比模板更接近编译器。

简单来说,在Vue中我们一般使用模板语法构建页面,使用render函数可以让我们通过JavaScript来构建DOM,这样可以免去转译的过程,灵活且高效。

这部分内容较多,大家可以直接查阅官方文档:https://v2.cn.vuejs.org/v2/guide/render-function.html,下面列出一些关键点

基础

假设有这么一个组件:根据传入的值动态生成h1-h4标题

<template>
  <div>
    <h1 v-if="level === 1">
      <slot></slot>
    </h1>
    <h2 v-else-if="level === 2">
      <slot></slot>
    </h2>
    <h3 v-else-if="level === 3">
      <slot></slot>
    </h3>
    <h4 v-else-if="level === 4">
      <slot></slot>
    </h4>
  </div>
</template>

<script>
export default {
  props: {
    level: {
      type: Number,
      default: 1,
      required: true
    }
  }
}
</script>

显然这个场景下使用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了slot。于是我们可以尝试使用 render函数重写上面的例子:

<script>
export default {
  props: {
    level: {
      type: Number,
      default: 1,
      required: true
    }
  },
  render: function (h) {
    return h(
      'h' + this.level, // 标签名称
      this.$slots.default // 子节点
    )
  },
}
</script>

在了解渲染函数之前,需要先了解浏览器的工作原理。示例如下

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>

当浏览器读到这些代码时,会建立一个“DOM 节点”树来保持追踪所有内容。如下图所示

在这里插入图片描述

每个元素都是一个节点,每段文字也是一个节点,甚至注释也都是节点,一个节点就是页面的一个部分。

高效地更新所有节点会是比较困难的,好在Vue已经帮我们处理了这个复杂的过程。我们仅需告知Vue页面上的HTML是什么即可,它可以在一个模板里:

<h1>{{ title }}</h1>

也可以在一个渲染函数中

render: function (createElement) {
  return createElement('h1', this.title)
}

这两种写法,Vue都会自动保持页面的更新,即便 title 发生了改变。

虚拟DOM

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实DOM,如下代码

return createElement('h1', this.title)

createElement返回的内容并不是一个真实的DOM元素,而是节点的相关信息,因此它更应该被叫做createNodeDescription。它所包含的信息会告诉Vue页面上需要渲染什么样的节点,包括及其子节点的描述信息。这样的节点描述称之为“虚拟节点 (virtual node)”,简写为“VNode”。“虚拟 DOM”是对由 Vue 组件树建立起来的整个 VNode 树的统称。

createElement

标准用法如下

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
    // 内容较多,见官方文档:https://v2.cn.vuejs.org/v2/guide/render-function.html#深入数据对象
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

插槽

可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:

render: function (createElement) {
  // `<div><slot></slot></div>`
  return createElement('div', this.$slots.default)
}

也可以通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数:

props: ['message'],
render: function (createElement) {
  // `<div><slot :text="message"></slot></div>`
  return createElement('div', [
    this.$scopedSlots.default({
      text: this.message
    })
  ])
}

如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots 字段:

render: function (createElement) {
  // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
  return createElement('div', [
    createElement('child', {
      // 在数据对象中传递 `scopedSlots`
      // 格式为 { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return createElement('span', props.text)
        }
      }
    })
  ])
}

JSX和函数式组件

假设有这么一段简单的模板代码

<anchored-heading :level="1">
  <span>Hello</span> world!
</anchored-heading>

如果使用渲染函数,会发现非常复杂

createElement(
  'anchored-heading',
  {
    props: {
      level: 1
    }
  },
  [
    createElement('span', 'Hello'),
    ' world!'
  ]
)

因此通过Babel 插件在 Vue 中使用 JSX 语法,可以让我们的书写行为更加贴合模板语法

render: function (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
}

函数式组件也非常重要,限于篇幅此处不做详述,可查阅官方文档。

操作模板

上述内容都是继承一个组件并做功能的修改或扩展,这里我们思考最后一个场景:怎么实现继承并修改template模板?

问题

Vue的继承、合并策略中其实是不包含template的,若是在继承的组件中定义模板内容又会覆盖原有模板(所以下面不讨论直接重写模板的情况)。

实现方式

假设我们有一个基础组件,它只有一个简单模板内容,示例如下

<template>
  <div>
    基础组件,插槽:
    <slot name="menu">
      <el-button size=small>默认按钮</el-button>
    </slot>
  </div>
</template>

此时我们要实现两个操作:一是在原模板的基础上追加自定义内容,二是替换原插槽中的默认内容。(下面两种方式仅作参考)

追加内容

既然没办法直接修改模板,那么可以尝试通过render更改渲染逻辑

<script>
import baseComp from './baseComp.vue'
export default {
  extends: baseComp,
  render() {
    // 基础组件render
    var parentRenderer = baseComp.render.apply(this, arguments);
    // 创建自定义内容
    var prefix = <span>扩展前置内容</span>
    var sufix = <span>扩展后置内容</span>
    // 在原组件基础上添加自定义内容
    return <div>{prefix}{parentRenderer}{sufix}</div>
  }
}
</script>

效果如下,在原组件前后各添加一些内容

在这里插入图片描述

替换插槽

上面的方法可以很轻松的在原组件的前后添加自定义内容,而如果我们要把原组件插槽中的按钮换成自定义内容,又该如何处理?

<script>
import baseComp from './baseComp.vue'
export default {
  extends: baseComp,
  render() {
    // 创建自定义内容
    var prefix = <span>扩展前置内容</span>
    var sufix = <span>扩展后置内容</span>
    // 创建插槽内容
    this.$slots.menu = [
      h('el-button', {
        attrs: {
          type: 'primary'
        },
        on: {
          click: () => { console.log('自定义按钮1') },
        },
      }, '新按钮1'),
      h('el-button', {
        attrs: {
          type: 'danger'
        },
        on: {
          click: () => { console.log('自定义按钮2') },
        },
      }, '新按钮2'),
      h('el-button', {
        attrs: {
          type: 'plain'
        },
        on: {
          click: () => { console.log('自定义按钮3') },
        },
      }, '新按钮3'),
    ];

    // 在原组件基础上替换插槽内容
    var parentRenderer = baseComp.render.apply(this, h);
    // 或 baseComp.render.apply(this, [this.$slots.menu]);
    return <div>{prefix}{parentRenderer}{sufix}</div>
  }
}
</script>

效果如下,替换原组件的默认插槽内容

在这里插入图片描述

问题

通过上述方法可以实现简单的“模板合并”,当然在实际开发中并不推荐使用这种方式实现此类需求。修改或替换模板内容可能会导致某些异常,如

  • 样式丢失:原样式依赖DOM层级结构
  • 功能丢失:原组件依赖特定元素
  • 事件丢失:原组件定义事件监听、委托依赖某些元素
  • 性能问题:重写或改变模板可能会导致额外的DOM操作
  • 维护问题:修改原模板会导致组件强耦合

实战应用

假设有这么一个功能:有封装好的列表组件,如Avue这种,只需要传入列配置和数据源即可展示列表。现在要求列表的操作最多显示3个按钮,超出部分收起放入下拉中,类似这样的效果

!在这里插入图片描述

针对已有项目该如何处理(项目有几百个页面,难道要逐个去手动判断?如果这些按钮是带权限的,总不能写死哪些按钮是收起来的吧)。

于是基本原则如下:最好底层适配改造,不要让每个研发去逐个页面修改,否则工作量和稳定性无法保证。

尝试几个方案:

  • 添加额外插槽,同时给dropdown添加一个参数,控制在没有子元素的情况下隐藏
  • 获取插槽的VNode节点,手动渲染成button或者dropdown-item
  • 直接操作DOM,将超出3个的按钮移入到dropdown中
  • 逐个页面修改,将原有插槽写法改为functionList,由底层再进行一次处理

这几种方式虽然能实现功能,但不够优雅。其中甚至有直接操作DOM的做法,显然是不能接受的。

经过思考和实践,最后在第二种方式的基础上,结合render的相关方法,形成了最终方案,下面介绍实现过程。

基础封装

便于大家理解,先做一个简单的table封装,模拟crud组件

<template>
  <el-table :data="data">
    <!-- 循环列 -->
    <el-table-column v-for="col in columns" :key="col.prop" :label="col.label" :prop="col.prop"></el-table-column>
    <!-- 固定操作列 -->
    <el-table-column fixed="right" label="操作">
      <!-- 按钮插槽 -->
      <template slot-scope="{row,$index}">
        <slot name="menu"
          :row="row"
          :index="$index">
        </slot>
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
export default {
  props: {
    columns: {
      type: Array,
      default: () => []
    },
    data: {
      type: Array,
      default: () => []
    }
  }
}
</script>

使用及实现效果如下

<template>
  <my-table :data="tableData" :columns="columns">
    <template slot="menu" slot-scope="scope">
      <el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
      <el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
    </template>
  </my-table>
</template>
<script>
  export default {
    components: {
      myTable: () => import('./myTable')
    },
    data(){
      return {
        columns: [
          { prop: 'name', label: '姓名' },
          { prop: 'age', label: '年龄' }
        ],
        tableData: []
      }
    },
    methods: {
      // 省略相关数据方法
    }
  }
</script>

在这里插入图片描述

改写渲染逻辑

考虑到直接使用插槽会被默认渲染,那么我们就要移除原有插槽,改为手动渲染

<el-table-column fixed="right" label="操作">
    <!-- 按钮插槽 -->
    <template slot-scope="{row,$index}">
        <!-- <slot name="menu"
        :row="row"
        :index="$index">
        </slot> -->
		<render-button :row="row" :index="$index"></render-button>
    </template>
</el-table-column>

封装一个替换组件,实现超出3个按钮放入下拉中

<!-- renderButton.vue -->
<template>
  <div class="column-flex-btn">
    <!-- 左侧按钮渲染区域 -->
    <!-- leftButtons -->
    <el-dropdown size="small" v-if="rightButtons.length">
      <el-button icon="el-icon-more" type="text" round size="small"></el-button>
      <el-dropdown-menu slot="dropdown">
        <!-- 右侧按钮渲染区域 -->
        <!-- rightButtons -->
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script>
export default {
  props: ['row','index'],
  computed: {
    leftButtons() {
      return []
    },
    rightButtons() {
      return []
    }
  }
}
</script>

获取插槽内容

有了渲染容器之后,我们需要拿到用户自定义的按钮信息,显然此时只能通过插槽来获取,那么如何获得插槽内容?输出$scopedSlots会发现menu插槽是一个函数

在这里插入图片描述

直接执行,发现其内容为传入按钮对应的VNode数组

this.$scopedSlots.menu();

在这里插入图片描述

按照需求拆分左右两部分数据

computed: {
    menuNodes(){
      let slot = this.table.$scopedSlots.menu;
      let nodes = [];
      if(slot){
        // 排除一些换行、空格等特殊情况,也可以直接过滤button-Tag
        nodes = slot().filter(t=>t.tag);
      }
      return nodes
    },
    leftButtons() {
      // 截取左侧VNodes
      return this.menuNodes.slice(0, 3)
    },
    rightButtons() {
      // 截取右侧VNodes
      return this.menuNodes.slice(3)
    }
}

渲染子节点

根据Render一节的内容,渲染一个button元素可以通过无模板方式创建,示例如下

render: function (createElement) {
  // `<el-button>按钮名称</el-button>`
  return createElement('el-button','按钮名称')
}

createElement返回的是对应的VNode节点,这刚好就是我们执行插槽得到的结果。因此,我们封装一个单纯的render组件,接收VNode,通过render直接返回结果即可

// renderNode.vue
export default {
  props: ['node'],
  render() {
    return this.node
  }
}

效果如下,实现基本布局

在这里插入图片描述

设置插槽作用域

此时我们点击按钮,发现无法获得行数据

在这里插入图片描述

还是根据Render一节中插槽的相关内容,设置作用域数据只需要在创建或执行插槽时传入对应数据即可

menuNodes(){
    let slot = this.table.$scopedSlots.menu;
    let nodes = [];
    if(slot){
        nodes = slot({
            row: this.row,
            index: this.index
        }).filter(t=>t.tag);
    }
}

查看效果,数据获取正常。到此这个需求完美解决。

在这里插入图片描述

插件式组件

梳理上述内容,会发现还剩一大类场景没有覆盖:

  • 现有组件都是固定模板,动态渲染组件如何处理?
  • 现有组件都是在app容器中渲染,如何实现全局(body)组件?
  • js调用组件

因此我们需要一个高*的组件创建方式,它至少要满足以下两点:

  • 能被随时创建
  • 可以任意指定其渲染位置

这个时候就要考虑使用Vue.extend了。

基础用法

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

Vue.extend(options)

示例如下,首先定义一个容器(挂载位置)

<div id="mount-node"></div>

创建构造器,实例化并挂载(这里的模板、数据、挂载位置都可以动态处理)

// 创建构造器
var extConstructor = Vue.extend({
    template: '<div><h1>{{title}}</h1><h3>{{content}}</h3></div>',
    data: function () {
        return {
            title: '自定义标题',
            content: '自定义文本内容',
        }
    }
})
// 创建实例,并挂载到一个元素上。
new extConstructor().$mount("#mount-node")
// 或
new extConstructor({el:"#mount-node"})

在这里插入图片描述

在示例中,extConstructor是构造器,而非普通组件,因此需要实例化后使用

在这里插入图片描述

需要注意的是,挂载完成后(无论是否指定挂载容器),都可以通过$el获取到对应的DOM,这一点非常重要。因为拿到对应的DOM后,你可以通过任意方式去处理成你想要的结果。

在这里插入图片描述

实现原理

  • extend:主要实现Vue的继承并添加一些方法到子类
  • _init():是Vue实例初始化过程中的核心方法,它完成了组件实例的初始化、状态的初始化、事件的初始化、渲染相关的属性和方法等工作。

内容较多,这里不做赘述,感兴趣的可查看源码:

extend:vue/src/core/global-api/extend.ts

_init():vue/src/core/instancei/init.ts

全局Toast实现

下面来看一个全局toast提示框的简单实现过程。

定义模板
<!--toast.vue-->
<template>
  <div class="my-toast" :class="type" v-if="showToast"
上一篇:「字符串」Z函数(扩展KMP|exKMP)/ LeetCode 2223(C++)


下一篇:Git 工作区、暂存区和仓库