Vue3学习笔记
Vue3学习笔记
-
setup = data + methods + computeds
-
teleport组件:将其内部内容转移到目标元素中
-
suspense组件:加载异步组件
-
createApp():创建新实例
-
不要再选项property或回调上使用箭头函数,因其没有this,导致报错
-
v-once:数据改变时,插值处的内容不会更新
-
<span v-once>这个将不会改变: {{ msg }}</span>
-
-
v-html:双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用v-html指令:
-
const RenderHtmlApp = { data() { return { rawHtml: '<span style="color: red">This should be red.</span>' } } } Vue.createApp(RenderHtmlApp).mount('#example1')
-
<div id="example1" class="demo"> <p>Using mustaches: {{ rawHtml }}</p> <p>Using v-html directive: <span v-html="rawHtml"></span></p> </div>
-
在你的站点上动态渲染任意的 HTML 是非常危险的,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要将用户提供的内容作为插值。
-
这个
span
的内容将会被替换成为 property 值rawHtml
,直接作为 HTML——会忽略解析 property 值中的数据绑定。注意,你不能使用v-html
来复合局部模板,因为 Vue 不是基于字符串的模板引擎。反之,对于用户界面 (UI),组件更适合作为可重用和可组合的基本单位。
-
-
动态参数:
-
<a v-bind:[attributeName]="url"> ... </a> <a v-on:[eventName]="doSomething"> ... </a>
-
动态参数预期会求出一个字符串,异常情况下值为
null
。这个特殊的null
值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。 -
动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。
-
<!-- 这会触发一个编译警告 --> <a v-bind:['foo' + bar]="value"> ... </a>
-
-
在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:
-
<!-- 在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。 除非在实例中有一个名为“someattr”的 property,否则代码不会工作。 --> <a v-bind:[someAttr]="value"> ... </a>
-
-
-
修饰符:
- .prevent:事件调用event.preventDefault();
-
data:
-
const app = Vue.createApp({ data() { return { count: 4 } } }) const vm = app.mount('#app') console.log(vm.$data.count) // => 4 console.log(vm.count) // => 4 // 修改 vm.count 的值也会更新 $data.count vm.count = 5 console.log(vm.$data.count) // => 5 // 反之亦然 vm.$data.count = 6 console.log(vm.count) // => 6
-
直接将不包含在
data
中的新 property 添加到组件实例是可行的。但由于该 property 不在背后的响应式$data
对象内,所以 Vue 的响应性系统不会自动跟踪它。
-
-
在定义
methods
时应避免使用箭头函数,因为这会阻止 Vue 绑定恰当的this
指向。 -
防抖和节流:Lodash
-
为了使组件实例彼此独立,可以在生命周期钩子的
created
里添加该防抖函数: -
app.component('save-button', { created() { // 用 Lodash 的防抖函数 this.debouncedClick = _.debounce(this.click, 500) }, unmounted() { // 移除组件时,取消定时器 this.debouncedClick.cancel() }, methods: { click() { // ... 响应点击 ... } }, template: ` <button @click="debouncedClick"> Save </button> ` })
-
-
计算属性:模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。
对于任何包含响应式数据的复杂逻辑,都应该使用计算属性。
基本例子:
<div id="computed-basics"> <p>Has published books:</p> <span>{{ publishedBooksMessage }}</span> </div> <script> Vue.createApp({ data() { return { author: { name: 'John Doe', books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] } } }, computed: { // 计算属性的 getter publishedBooksMessage() { // `this` points to the vm instance return this.author.books.length > 0 ? 'Yes' : 'No' } } }).mount('#computed-basics') </script>
你可以像普通属性一样将数据绑定到模板中的计算属性。
Vue 知道
vm.publishedBookMessage
依赖于vm.author.books
,因此当vm.author.books
发生改变时,所有依赖vm.publishedBookMessage
绑定也会更新。而且最妙的是我们已经声明的方式创建了这个依赖关系:计算属性的 getter 函数没有副作用,这使得更易于测试和理解。
计算属性是基于它们的反应依赖关系缓存的
计算属性只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要
author.books
还没有发生改变,多次访问publishedBookMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数。计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:
// ... computed: { fullName: { // getter get() { return this.firstName + ' ' + this.lastName }, // setter set(newValue) { const names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } } // ...
-
侦听器:当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
-
<div id="watch-example"> <p> Ask a yes/no question: <input v-model="question" /> </p> <p>{{ answer }}</p> </div> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script> <script> const watchExampleVM = Vue.createApp({ data() { return { question: '', answer: 'Questions usually contain a question mark. ;-)' } }, watch: { // whenever question changes, this function will run question(newQuestion, oldQuestion) { if (newQuestion.indexOf('?') > -1) { this.getAnswer() } } }, methods: { getAnswer() { this.answer = 'Thinking...' axios .get('https://yesno.wtf/api') .then(response => { this.answer = response.data.answer }) .catch(error => { this.answer = 'Error! Could not reach the API. ' + error }) } } }).mount('#watch-example') </script>
-
在这个示例中,使用
watch
选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
-
-
class绑定一个返回对象的计算属性:
-
<div :class="classObject"></div> <script> data() { return { isActive: true, error: null } }, computed: { classObject() { return { active: this.isActive && !this.error, 'text-danger': this.error && this.error.type === 'fatal' } } } </script>
-
将数组传给:class:
-
<div :class="[activeClass, errorClass]"></div> <div :class="[{ active: isActive }, errorClass]"></div> <script> data() { return { activeClass: 'active', errorClass: 'text-danger' } } </script>
-
三元表达式:
-
<div :class="[isActive ? activeClass : '', errorClass]"></div>
-
-
如果你的组件有多个根元素,你需要定义哪些部分将接收这个类。可以使用
$attrs
组件属性执行此操作:-
<div id="app"> <my-component class="baz"></my-component> </div>
const app = Vue.createApp({}) app.component('my-component', { template: ` <p :class="$attrs.class">Hi!</p> <span>This is a child component</span> ` })
-
-
:style
-
<div :style="styleObject"></div>
data() { return { styleObject: { color: 'red', fontSize: '13px' } } }
对象语法常常结合返回对象的计算属性使用。
Vue 将自动侦测并添加相应的浏览器前缀
-
-
v-if
-
<div v-if="type === 'A'"> A </div> <div v-else-if="type === 'B'"> B </div> <div v-else-if="type === 'C'"> C </div> <div v-else> Not A/B/C </div>
-
-
v-show
不支持
元素,也不支持
v-else -
v-if VS v-show
-
v-if
是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。相比之下,
v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。一般来说,
v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用
v-show
较好;如果在运行时条件很少改变,则使用v-if
较好。
-
-
不推荐同时使用
v-if
和v-for
。当v-if
与v-for
一起使用时,v-if
具有比v-for
更高的优先级。 -
你也可以用
of
替代in
作为v-for的分隔符,因为它更接近 JavaScript 迭代器的语法 -
当 Vue 正在更新使用
v-for
渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
-
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。
-
Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。
example1.items = example1.items.filter(item => item.message.match(/Foo/))
-
有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际变更或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。
例如:
<li v-for="n in evenNumbers">{{ n }}</li>
data() { return { numbers: [ 1, 2, 3, 4, 5 ] } }, computed: { evenNumbers() { return this.numbers.filter(number => number % 2 === 0) } }
-
v-for
也可以接受整数。在这种情况下,它会把模板重复对应次数。<div id="range" class="demo"> <span v-for="n in 10">{{ n }} </span> </div>
-
任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,我们要使用 props;
不自动将
item
注入到组件里的原因是,这会使得组件与v-for
的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。 -
在methods中访问原始的 DOM 事件。可以用特殊变量
$event
把它传入方法:<button @click="warn('Form cannot be submitted yet.', $event)"> Submit </button>
// ... methods: { warn(message, event) { // now we have access to the native event if (event) { event.preventDefault() } alert(message) } }
-
事件处理程序中可以有多个方法,这些方法由逗号运算符分隔:
<!-- 这两个 one() 和 two() 将执行按钮点击事件 --> <button @click="one($event), two($event)"> Submit </button>
// ... methods: { one(event) { // first handler logic... }, two(event) { // second handler logic... } }
-
方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
-
事件修饰符:
-
<!-- 阻止冒泡 --> <a @click.stop="doThis"></a> <!-- 提交事件不再重载页面 --> <form @submit.prevent="onSubmit"></form> <!-- 修饰符可以串联 --> <a @click.stop.prevent="doThat"></a> <!-- 只有修饰符 --> <form @submit.prevent></form> <!-- 添加事件监听器时使用事件捕获模式 --> <!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 --> <div @click.capture="doThis">...</div> <!-- 只当在 event.target 是当前元素自身时触发处理函数 --> <!-- 即事件不是从内部元素触发的 --> <div @click.self="doThat">...</div> <!-- 点击事件将只会触发一次 --> <a @click.once="doThis"></a> <!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --> <!-- 而不会等待 `onScroll` 完成 --> <!-- 这其中包含 `event.preventDefault()` 的情况 --> <div @scroll.passive="onScroll">...</div>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用
v-on:click.prevent.self
会阻止所有的点击,而v-on:click.self.prevent
只会阻止对元素自身的点击。不要把
.passive
和.prevent
一起使用,因为.prevent
将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive
会告诉浏览器你不想阻止事件的默认行为。
-
-
按键修饰符:
-
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` --> <input @keyup.enter="submit" /> <!-- 处理函数只会在 $event.key 等于 'PageDown' 时被调用。--> <input @keyup.page-down="onPageDown" /> <!-- Alt + Enter --> <input @keyup.alt.enter="clear" /> <!-- Ctrl + Click --> <div @click.ctrl="doSomething">Do something</div> <!-- 即使 Alt 或 Shift 被一同按下时也会触发 --> <button @click.ctrl="onClick">A</button> <!-- 有且只有 Ctrl 被按下的时候才触发 --> <button @click.ctrl.exact="onCtrlClick">A</button> <!-- 没有任何系统修饰符被按下的时候才触发 --> <button @click.exact="onClick">A</button>
-
.enter
-
.tab
-
.delete
(捕获“删除”和“退格”键) -
.esc
-
.space
-
.up
-
.down
-
.left
-
.right
-
.ctrl
-
.alt
-
.shift
-
.meta
-
在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。在 Sun 操作系统键盘上,meta 对应实心宝石键 (◆)。在其他特定键盘上,尤其在 MIT 和 Lisp 机器的键盘、以及其后继产品,比如 Knight 键盘、space-cadet 键盘,meta 被标记为“META”。在 Symbolics 键盘上,meta 被标记为“META”或者“Meta”。
-
.exact
修饰符允许你控制由精确的系统修饰符组合触发的事件。 -
鼠标修饰符:
.left
.right
.middle
-
-
因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,所以它不会导致任何维护上的困难。
-
v-model
会忽略所有表单元素的value
、checked
、selected
attribute 的初始值而总是将当前活动实例的数据作为数据来源。你应该通过 JavaScript 在组件的data
选项中声明初始值。 -
v-model会根据空间类型自动选取正确的方法来更新元素。
v-model
在内部为不同的输入元素使用不同的 property 并抛出不同的事件:- text 和 textarea 元素使用
value
property 和input
事件; - checkbox 和 radio 使用
checked
property 和change
事件; - select 字段将
value
作为 prop 并将change
作为事件。
对于需要使用输入法 (如中文、日文、韩文等) 的语言,你会发现
v-model
不会在输入法组织文字过程中得到更新。如果你也想处理这个过程,请使用input
事件。 - text 和 textarea 元素使用
-
修饰符:
-
.lazy:
-
<!-- 在“change”时而非“input”时更新 --> <input v-model.lazy="msg" />
-
-
.number:自动将用户的输入值转为数值类型
-
<input v-model.number="age" type="number" />
-
-
.trim:自动过滤用户输入的首尾空白字符
-
<input v-model.trim="msg" />
-
-
-
app.component('blog-post', { props: ['title'], emits: ['enlarge-text'] })
-
$event
-
动态组件:
-
<!-- 组件会在 `currentTabComponent` 改变时改变 --> <component :is="currentTabComponent"></component> <scirpt> computed: { currentTabComponent() { return 'tab-' + this.currentTab.toLowerCase() } } </scirpt>
-
-
HTML 属性名不区分大小写,因此浏览器将把所有大写字符解释为小写。
-
v-is
-
有些 HTML 元素,诸如
<ul>
、<ol>
、<table>
和<select>
,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如<li>
、<tr>
和<option>
,只能出现在其它某些特定的元素内部。这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:
<table> <blog-post-row></blog-post-row> </table>
这个自定义组件
<blog-post-row>
会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的v-is
attribute 给了我们一个变通的办法:<table> <tr v-is="'blog-post-row'"></tr> </table> <!-- 错误的,这样不会渲染任何东西 v-is 值应为 JavaScript 字符串文本 --> <tr v-is="blog-post-row"></tr> <!-- 正确的 --> <tr v-is="'blog-post-row'"></tr>
-
-
在对象中放一个类似
ComponentA
的变量名其实是ComponentA
:ComponentA
的缩写:-
import ComponentA from './ComponentA.vue' export default { components: { ComponentA } // ... }
-
-
props:
-
props: { // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证) propA: Number, // 多个可能的类型 propB: [String, Number], // 必填的字符串 propC: { type: String, required: true }, // 带有默认值的数字 propD: { type: Number, default: 100 }, // 带有默认值的对象 propE: { type: Object, // 对象或数组默认值必须从一个工厂函数获取 default: function() { return { message: 'hello' } } }, // 自定义验证函数 propF: { validator: function(value) { // 这个值必须匹配下列字符串中的一个 return ['success', 'warning', 'danger'].indexOf(value) !== -1 } }, // 具有默认值的函数 propG: { type: Function, // 与对象或数组默认值不同,这不是一个工厂函数 —— 这是一个用作默认值的函数 default: function() { return 'Default function' } } } // 当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。
-
-
每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
如果子组件接下来希望将其作为一个本地的 prop 数据来使用,则
props: ['initialCounter'], data() { return { counter: this.initialCounter } } //这个 prop 以一种原始的值传入且需要进行转换 props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如
data
、computed
等) 在default
或validator
函数中是不可用的。 -
–> 非Prop 的Attribute
-
添加TS支持:vue add typescript
-
要让 TypeScript 正确推断 Vue 组件选项中的类型,需要使用
defineComponent
全局方法定义组件:import { defineComponent } from 'vue' const Component = defineComponent({ // 已启用类型推断 })
-
如果你有一个复杂的类型或接口,你可以使用 type assertion 对其进行强制转换:
interface Book { title: string author: string year: number } const Component = defineComponent({ data() { return { book: { title: 'Vue 3 Guide', author: 'Vue Team', year: 2020 } as Book } } })
-
由于 Vue 声明文件的循环特性,TypeScript 可能难以推断 computed 的类型。因此,你可能需要注释返回类型的计算属性。
import { defineComponent } from 'vue' const Component = defineComponent({ data() { return { message: 'Hello!' } }, computed: { // 需要注释 greeting(): string { return this.message + '!' } // 在使用setter进行计算时,需要对getter进行注释 greetingUppercased: { get(): string { return this.greeting.toUpperCase(); }, set(newValue: string) { this.message = newValue.toUpperCase(); }, }, } })
-
Vue 对定义了
type
的 prop 执行运行时验证。要将这些类型提供给 TypeScript,我们需要使用PropType
强制转换构造函数:import { defineComponent, PropType } from 'vue' interface ComplexMessage { title: string okMessage: string cancelMessage: string } const Component = defineComponent({ props: { name: String, success: { type: String }, callback: { type: Function as PropType<() => void> }, message: { type: Object as PropType<ComplexMessage>, required: true, validator(message: ComplexMessage) { return !!message.title } } } })
如果你发现验证器没有得到类型推断或者成员完成不起作用,那么用期望的类型注释参数可能有助于解决这些问题。
-
Refs 根据初始值推断类型:
import { defineComponent, ref } from 'vue' const Component = defineComponent({ setup() { const year = ref(2020) const result = year.value.split('') // => Property 'split' does not exist on type 'number' } })
有时我们可能需要为 ref 的内部值指定复杂类型。我们可以在调用 ref 重写默认推理时简单地传递一个泛型参数:
const year = ref<string | number>('2020') // year's type: Ref<string | number> year.value = 2020 // ok!
-
当声明类型
reactive
property,我们可以使用接口:import { defineComponent, reactive } from 'vue' interface Book { title: string year?: number } export default defineComponent({ name: 'HelloWorld', setup() { const book = reactive<Book>({ title: 'Vue 3 Guide' }) // or const book: Book = reactive({ title: 'Vue 3 Guide' }) // or const book = reactive({ title: 'Vue 3 Guide' }) as Book } })
-
如果泛型的类型未知,建议将
ref
转换为Ref<T>
。 -
计算值将根据返回值自动推断类型
import { defineComponent, ref, computed } from 'vue' export default defineComponent({ name: 'CounterButton', setup() { let count = ref(0) // 只读 const doubleCount = computed(() => count.value * 2) const result = doubleCount.value.split('') // => Property 'split' does not exist on type 'number' } })
-
setup
-
两个参数:
-
props
-
如需解构prop,可通过toRefs来完成:
-
import { toRefs } from 'vue' setup(props) { const { title } = toRefs(props) console.log(title.value) }
-
-
-
context:上下文
-
export default { setup(props, context) { // Attribute (非响应式对象) console.log(context.attrs) // 插槽 (非响应式对象) console.log(context.slots) // 触发事件 (方法) console.log(context.emit) } }
-
它不是响应式的,这意味着你可以安全地对
context
使用 ES6 解构。// MyBook.vue export default { setup(props, { attrs, slots, emit }) { ... } }
-
attrs
和slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以attrs.x
或slots.x
的方式引用 property。请注意,与props
不同,attrs
和slots
是非响应式的。如果你打算根据attrs
或slots
更改应用副作用,那么应该在onUpdated
生命周期钩子中执行此操作。
-
-
执行
setup
时,组件实例尚未被创建。因此,你只能访问以下 property:props
attrs
slots
emit
换句话说,你将无法访问以下组件选项:
data
computed
methods
-
在
setup()
内部,this
不会是该活跃实例的引用,因为setup()
是在解析其它组件选项之前被调用的,所以setup()
内部的this
的行为与其它选项中的this
完全不同。这在和其它选项式 API 一起使用setup()
时可能会导致混淆。 -
setup函数是处于 围绕
beforeCreate
和created
生命周期钩子运行 -
setup函数是 Composition API(组合API)的入口
-
在setup函数中定义的变量和方法最后都是需要 return 出去的 不然无法再模板中使用
-
由于我们不能在 setup函数中使用 data 和 methods,所以 Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了 undefined
-
setup函数只能是同步的不能是异步的
-
-
-
研究:
-
<script lang="ts"> import { Options, Vue } from 'vue-class-component'; import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src @Options({ components: { HelloWorld, }, }) export default class Home extends Vue {} </script>
-
-
h
-
返回一个”虚拟节点“,通常缩写为 VNode:一个普通对象,其中包含向 Vue 描述它应在页面上渲染哪种节点的信息,包括所有子节点的描述。它的目的是用于手动编写的渲染函数:
render() { return Vue.h('h1', {}, 'Some title')//三个参数(type,props,children) }
子代 VNode,使用
h()
生成,或者使用字符串来获取“文本 VNode”,或带有插槽的对象。可选。h('div', {}, [ 'Some text comes first.', h('h1', 'A headline'), h(MyComponent, { someProp: 'foobar' }) ])
-
-
nextTick
-
将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。
import { createApp, nextTick } from 'vue' const app = createApp({ setup() { const message = ref('Hello!') const changeMessage = async newMessage => { message.value = newMessage await nextTick() console.log('Now DOM is updated') } } })
-
-
在setup函数中,可以使用ref函数,用于创建一个响应式数据,当数据发生改变时,Vue会自动更新UI
- ref函数仅能监听基本类型的变化,不能监听复杂类型的变化(比如对象、数组)
-
组件中name的作用:
- 当项目使用keep-alive时,可搭配组件name进行缓存过滤
- 方便递归调用组件
- vue-tools插件调试需要name
下一篇: 计算两个日期之间的工作日