文章目录
- template 选项
- Vue 生命周期
- 生命周期图示
- 生命周期钩子
- beforeCreate
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeDestroy
- destroyed
- 组件基础
- 组件是什么?
- 组件注册
- 全局组件
- 局部组件
- 组件名
- 组件复用
- 自闭合组件
- 组件的 data 选项
- 单个根元素
- 组件\_Prop
- 注册自定义特性
- Prop 的大小写
- 传递静态或动态 Prop
- 传递一个对象的所有属性
- 组件\_Prop 验证
- 组件\_单向数据流
- 组件\_非 Prop 特性
- 替换/合并已有的特性
- 禁用特性继承
- 组件\_监听组件事件
- 使用事件抛出一个值
- 事件名
- 将原生事件绑定到组件
- 在组件上使用 v-model
- .sync 修饰符
- v-model VS .sync
- 组件\_插槽
- 插槽内容
- 编译作用域
- 后备内容
- 具名插槽
- 作用域插槽
- 独占默认插槽的缩写语法
- 解构插槽 Prop
- 动态插槽名
- 具名插槽的缩写
- 废弃了的语法
- 带有 slot 特性的具名插槽
- 带有 slot-scope 特性的作用域插槽
template 选项
关于 el
提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标。可以是 CSS 选择器,也可以是一个 HTML 元素 实例。
如果在实例化时存在这个选项,实例将立即进入编译过程,否则,需要显式调用 vm.$mount() 手动开启编译。
template
一个字符串模板作为 Vue 实例的标识使用。模板将会 替换 挂载的元素,挂载元素的内容都将被忽略。
<div id="app"></div>
const vm = new Vue({
el: "#app",
template: `
<div id="ceshi">xxx</div>
`,
});
Vue 初始化到挂载的流程
Vue 生命周期
每个 Vue 实例在被创建时都要经过一系列的初始化过程,例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
生命周期图示
生命周期钩子
所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对属性和方法进行运算
beforeCreate
在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
<div id="app">
<div @click="handleClick">点击事件</div>
</div>
const vm = new Vue({
el: "#app",
data: {
msg: "hellow world",
},
beforeCreate() {
console.log(this.msg); // undefined
console.log(this.handleClick); // undefined
console.log("-----beforeCreate-----");
},
methods: {
handleClick() {
console.log(handleClick);
},
},
watch: {
msg: {
handler() {
console.log("侦听msg的值");
},
immediate: true,
},
},
});
打印顺序:
undefined;
undefined;
-----beforeCreate---- - 侦听msg的值;
created
在实例创建完成后被立即调用。
在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。
如果要在第一时间调用 methods 中的方法,或者操作 data 中的数据,可在此钩子中进行操作。
需要注意的是,执行此钩子时,挂载阶段还未开始,$el 属性目前不可见。
此时,可以进行数据请求,将请求回来的值赋值给 data 中的数据。
<div id="app">
<div @click="handleClick">点击事件</div>
</div>
const vm = new Vue({
el: "#app",
data: {
msg: "hellow world",
},
created() {
console.log(this.msg); // hello world
console.log(this.handleClick); // function () {...}
console.log(this.$el); // undefined
console.log("----------created-------");
},
methods: {
handleClick() {
console.log(handleClick);
},
},
watch: {
msg: {
handler() {
console.log("侦听msg的值");
},
immediate: true,
},
},
});
打印顺序:
侦听msg的值
hellow world
ƒ handleClick () { console.log(handleClick); }
undefined
----------created-------
beforeMount
在挂载开始之前被调用,此时模板已经编译完成,只是未将生成的模板替换 el 对应的元素。
在此钩子函数中,可以获取到模板最初始的状态。
此时,可以拿到 vm.$el,只不过为旧模板
const vm = new Vue({
el: "#app",
beforeMount() {
console.log(this.$el);
},
});
mounted
el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
在该钩子函数中的 vm.$el 为新模板。
执行完该钩子函数后,代表实例已经被完全创建好。
如果要在第一时间,操作页面上的 dom 节点时,可以在此钩子函数中操作
const vm = new Vue({
el: "#app",
mounted() {
console.log(this.$el);
},
});
beforeUpdate
数据更新时调用,发生在虚拟 DOM 打补丁之前。此时数据已经更新,但是 DOM 还未更新
<div id="app">{{ msg }}</div>
const vm = new Vue({
el: "#app",
data: {
msg: "hellow world",
},
beforeUpdate() {
console.log(this.msg);
console.log(this.$el);
},
methods: {
handleClick() {
console.log("handleClick");
},
},
});
this.msg = "xxx";
updated
数据更改导致 DOM 重新渲染后,会执行该钩子函数。
此时数据和 dom 同步。
beforeDestroy
实例销毁之前调用。在这一步,实例仍然完全可用。
可以在该钩子函数中,清除定时器。
<div id="app">{{ msg }}</div>
const vm = new Vue({
el: "#app",
data: {
msg: "hellow world",
timer: 0,
},
created() {
this.timer = setInterval(() => {
console.log("xxx");
}, 500);
},
beforeDestroy() {
clearInterval(this.timer);
},
});
destroyed
Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除。
组件基础
组件是什么?
组件是可复用的 Vue 实例,且带有一个名字,例如名字为 shanshan-cmp,那么我们则可以在一个通过 new Vue 创建的根实例中,把这个组件作为自定义元素来使用:
<div id="app">
<shanshan-cmp></shanshan-cmp>
</div>
const vm = new Vue({
el: "#app",
});
因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。
组件注册
全局组件
Vue.component
利用 Vue.component 创建的组件组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中。
参数:
- {string}
- {Function | Object} [definition]
用法:
注册或获取全局组件。注册还会自动使用给定的 id 设置组件的名称。
示例:
<div id="app">
<button-counter></button-counter>
</div>
Vue.component("button-counter", {
data() {
return {
count: 0,
};
},
template: `
<button @click="count ++">你按了我{{ count }}次</button>
`,
});
const vm = new Vue({
el: "#app",
});
局部组件
在 components 选项中定义要使用的组件。
对于 components 对象中的每一个属性来说,其属性名就是自定义元素的名字,其属性值就是这个组件的选项对象。
示例:
<div id="#app">
<button-counter></button-counter>
</div>
const buttonCounter = {
data() {
return {
count: 0,
};
},
template: `
<button @click="count ++">你按了我{{ count }}次</button>
`,
};
const vm = new Vue({
el: "#app",
components: {
"button-counter": buttonCounter,
},
});
组件名
在注册一个组件的时候,我们始终需要给它一个名字。你给予组件的名字可能依赖于你打算拿它来做什么,所以命名要语义化。
组件名大小写
定义组件名的方式有两种:
使用 kebab-case (横短线分隔命名)
Vue.component("my-component", {
/***/
});
当使用 kebab-case 定义一个组件时,你必须在引用这个自定义元素时使用 kebab-case,例如:<my-component></my-component>
。
使用 PascalCase (大驼峰命名)
Vue.component("MyComponent", {
/***/
});
当使用 PascalCase 定义一个组件时,你在引用这个自定义元素时两种命名法都可以。也就是说<my-component-name>
和 <MyComponentName>
都是可接受的。注意,尽管如此,直接在 DOM (即字符串模板或单文件组件) 中使用时只有 kebab-case 是有效的。
另:我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。
组件复用
可以将组件进行任意次数的复用:
<div id="#app">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
自闭合组件
在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。
自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。
不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。
组件的 data 选项
当我们定义一个组件时,它的 data 并不是像这样直接提供一个对象:
data: {
count: 0;
}
取而代之的是,一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:
data () {
return {
count: 0
}
}
如果 Vue 没有这条规则,点击一个按钮就可能会像下面一样影响到其它所有实例:
单个根元素
每个组件必须只有一个根元素,当模板的元素大于 1 时,可以将模板的内容包裹在一个父元素内。
组件_Prop
注册自定义特性
组件默认只是写好结构、样式和行为,使用的数据应由外界传递给组件。
如何传递?注册需要接收的 prop,将数据作为一个自定义特性传递给组件。
如:
<div id="app">
<video-item
title="羊村摇"
poster="https://developer.duyiedu.com/bz/video/955bac93ccb7f240d25a79b2ff6a9fdbda9537bc.jpg@320w_200h.webp"
play="638000"
rank="1207"
></video-item>
</div>
Vue.component("video-item", {
props: ["title", "poster", "play", "rank"],
});
在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问 data 中的值一样:
<div id="app">
<video-item
title="羊村摇"
poster="https://developer.duyiedu.com/bz/video/955bac93ccb7f240d25a79b2ff6a9fdbda9537bc.jpg@320w_200h.webp"
play="638000"
rank="1207"
></video-item>
</div>
Vue.component("video-item", {
props: ["title", "poster", "play", "rank"],
template: `<div>{{ title }}</div>`,
});
Prop 的大小写
HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。故:当 传递的 prop 为 短横线分隔命名时,组件内 的 props 应为 驼峰命名 。
如:
<div id="app">
<!-- 在 HTML 中是 kebab-case 的 -->
<video-item sub-title="hello!"></video-item>
</div>
Vue.component("video-item", {
// 在 JavaScript 中是 camelCase 的
props: ["subTitle"],
template: "<h3>{{ postTitle }}</h3>",
});
要注意的是:如果使用的是字符串模板,那么这个限制就不存在了。
传递静态或动态 Prop
像这样,我们已经知道了可以给 prop 传入一个静态的值:
<video-item title="羊村摇"></video-item>
若想要传递一个动态的值,可以配合 v-bind 指令进行传递,如:
<video-item :title="title"></video-item>
传递一个对象的所有属性
如果你想要将一个对象的所有属性都作为 prop 传入,你可以使用不带参数的 v-bind 。例如,对于一个给定的对象 person:
person: {
name: 'shanshan',
age: 18
}
传递全部属性:
<my-component v-bind="person"></my-component>
上述代码等价于:
<my-component :name="person.name" :age="person.age"></my-component>
组件_Prop 验证
我们可以为组件的 prop 指定验证要求,例如你可以要求一个 prop 的类型为什么。如果说需求没有被满足的话,那么 Vue 会在浏览器控制台中进行警告,这在开发一个会被别人用到的组件时非常的有帮助。
为了定制 prop 的验证方式,你可以为 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:
Vue.component("my-component", {
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise,
},
});
上述代码中,对 prop 进行了基础的类型检查,类型值可以为下列原生构造函数中的一种:String
、Number
、Boolean
、Array
、Object
、Date
、Function
、Symbol
、任何自定义构造函数、或上述内容组成的数组。
需要注意的是null
和 undefined
会通过任何类型验证。
除基础类型检查外,我们还可以配置高级选项,对 prop 进行其他验证,如:类型检测、自定义验证和设置默认值。
如:
Vue.component("my-component", {
props: {
title: {
type: String, // 检查 prop 是否为给定的类型
default: "杉杉最美", // 为该 prop 指定一个默认值,对象或数组的默认值必须从一个工厂函数返回,如:default () { return {a: 1, b: 10} },
required: true, // 定义该 prop 是否是必填项
validator(prop) {
// 自定义验证函数,该prop的值回作为唯一的参数代入,若函数返回一个falsy的值,那么就代表验证失败
return prop.length < 140;
},
},
},
});
为了更好的团队合作,在提交的代码中,prop 的定义应该尽量详细,至少需要指定其类型。
组件_单向数据流
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
这里有两种常见的试图改变一个 prop 的情形:
- 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用,在后续操作中,会将这个值进行改变。在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
- 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
组件_非 Prop 特性
非 Prop 特性指的是,一个未被组件注册的特性。当组件接收了一个非 Prop 特性时,该特性会被添加到这个组件的根元素上。
替换/合并已有的特性
想象一下 <my-cmp>
的模板是这样的:
<input type="date" class="b" />
为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名:
<my-cmp class="my-cmp"></my-cmp>
在这种情况下,我们定义了两个不同的 class 的值:
- my-cmp,这是在组件的模板内设置好的
- b,这是从组件的父级传入的
对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入 type=“text” 就会替换掉 type=“date” 并把它破坏!庆幸的是,class 和 style 特性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:my-cmp b。
禁用特性继承
如果不希望组件的根元素继承特性,那么可以在组件选项中设置 inheritAttrs: false
。如:
Vue.component("my-cmp", {
inheritAttrs: false,
// ...
});
在这种情况下,非常适合去配合实例的 $attrs 属性使用,这个属性是一个对象,键名为传递的特性名,键值为传递特性值。
{
required: true,
placeholder: 'Enter your username'
}
使用 inheritAttrs: false
和 $attrs
相互配合,我们就可以手动决定这些特性会被赋予哪个元素。如:
Vue.component("base-input", {
inheritAttrs: false,
props: ["label", "value"],
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
`,
});
注意:inheritAttrs: false 选项不会影响 style 和 class 的绑定。
组件_监听组件事件
首先,我们来写一个博文组件,如:
Vue.component("blog-post", {
props: {
post: {
type: Object,
},
},
template: `
<div class="blog-post">
<h3>{{ post.title }}</h3>
<button>放大字号</button>
<div>{{ post.content }}</div>
</div>
`,
});
<div id="app">
<div :style="{fontSize: postFontSize + 'em'}">
<blog-post v-for="post in posts" :key="post.id" :post="post"> </blog-post>
</div>
</div>
const vm = new Vue({
el: "#app",
data: {
posts: [
{ title: "标题1", content: "正文内容", id: 0 },
{ title: "标题2", content: "正文内容", id: 1 },
{ title: "标题3", content: "正文内容", id: 2 },
],
postFontSize: 1,
},
});
可以看到每一个博文组件中,都有一个按钮,可以去放大页面中字体的字号,也就是说,当点击这个按钮时,我们要告诉父组件改变postFontSize
数据去放大所有博文的文本。碰见这样的情况,该如何做呢?
Vue 实例提供了一个自定义事件来解决这个问题。父组件可以像处理原生 DOM 元素一样,通过 v-on
指令,监听子组件实例的任意事件,如:
<div id="app">
<div :style="{fontSize: postFontSize + 'em'}">
<blog-post ... @enlarge-text="postFontSize += 0.1"> </blog-post>
</div>
</div>
那么,怎么样能够去监听到一个 enlarge-text
这么奇怪的事件呢?这就需要在组件内,去主动触发一个自定义事件了。
如何触发?
通过调用 $emit 方法 并传入事件名称来触发一个事件,如:
Vue.component('blog-post', {
props: {
...
},
template: `
<div class="blog-post">
...
<button @click="$emit('enlarge-text')">放大字号</button>
...
</div>
`,
})
这样,父组件就可以接收该事件,更新数据 pageFontSize
的值了。
使用事件抛出一个值
在有些情况下,我们可能想让 <blog-post>
组件决定它的文本要放大多少。这是可以使用 $emit 的第二个参数来提供这个值,如:
Vue.component('blog-post', {
props: {
...
},
template: `
<div class="blog-post">
...
<button @click="$emit('enlarge-text', 0.2)">放大字号</button>
...
</div>
`,
})
在父组件监听这个事件时,可以通过 $event 访问到被抛出的这个值:
<div id="app">
<div :style="{fontSize: postFontSize + 'em'}">
<blog-post ... @enlarge-text="postFontSize += $event"> </blog-post>
</div>
</div>
或者,将这个事件处理函数写成一个方法:
<div id="app">
<div :style="{fontSize: postFontSize + 'em'}">
<blog-post ... @enlarge-text="onEnlargeText"> </blog-post>
</div>
</div>
那么,这个值将会作为第一个参数,传入这个方法:
methods: {
onEnlargeText: function (enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
事件名
不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所有的名称。如果触发一个 camelCase 名字的事件:
this.$emit("myEvent");
则监听这个名字的 kabab-case 版本是不会有任何效果的。
<!-- 没有效果 -->
<my-component v-on:my-event="doSomething"></my-component>
与组件和 prop 不同的是,事件名不会被当作一个 JS 变量名或者属性名,所以就没有理由使用 camelCase 或 PascalCase 了。
并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写,所以 @myEvent 将会变成 @myevent,导致 myEvent 不可能被监听到。
因此,推荐始终使用 kebab-case 的事件名。
将原生事件绑定到组件
在组件上去监听事件时,我们监听的是组件的自动触发的自定义事件,但是在一些情況下,我们可能想要在一个组件的根元素上直接监听一个原生事件。这是,可以使用 v-on 指令的 .native 修饰符,如:
<base-input @focus.native="onFocus" @blur.native="onBlur"></base-input>
Vue.component("base-input", {
template: `
<input type="text" />
`,
});
这样处理,在有些时候是很有用的,不过在尝试监听一个类似<input>
元素时,这并不是一个好主意,例如<base-input>
组件可能做了重构,如:
<label>
姓名:
<input type="text" />
</label>
可以看到,此时组件的根元素实际上是一个
为了解决这个问题,Vue 提供了一个$listeners 属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:
{
focus: function (event) { /* ... */ }
blur: function (event) { /* ... */ },
}
有了这个 $listeners 属性,我们可以配合 v-on=“$listeners” 将所有的事件监听器指向这个组件的某个特定的子元素,如:
Vue.component("base-input", {
template: `
<label>
名字:
<input v-on="$listeners" />
</label>
`,
});
在组件上使用 v-model
由于自定义事件的出现,在组件上也可以使用 v-model 指令。
在 input 元素上使用 v-mode 指令时,相当于绑定了 value 特性以及监听了 input 事件:
<input v-model="searchText" />
等价于:
<input :value="searchText" @input="searchText = $event.target.value" />
当把 v-model 指令用在组件上时:
<base-input v-model="searchText" />
则等价于:
<base-input :value="searchText" @input="searchText = $event" />
同 input 元素一样,在组件上使用 v-model 指令,也是绑定了 value 特性,监听了 input 事件。
所以,为了让 v-model 指令正常工作,这个组件内的<input>
必须:
- 将其 value 特性绑定到一个叫 value 的 prop 上
- 在其 input 事件被触发时,将新的值通过自定义的 input 事件抛出
如:
Vue.component("base-input", {
props: ["value"],
template: `
<input
:value="value"
@input="$emit('input', $event.target.value)"
/>
`,
});
这样操作后,v-model 就可以在这个组件上工作起来了。
通过上面的学习,我们知道了,一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value 特性用于不同的目的。碰到这样的情况,我们可以利用 model 选项来避免冲突:
Vue.component("base-checkbox", {
model: {
prop: "checked",
event: "change",
},
props: {
checked: Boolean,
},
template: `
<input
type="checkbox"
:checked="checked"
@change="$emit('change', $event.target.checked)"
>
`,
});
使用组件:
<base-checkbox v-model="lovingVue"></base-checkbox>
这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的属性将会被更新。
.sync 修饰符
除了使用 v-model 指令实现组件与外部数据的双向绑定外,我们还可以用 v-bind 指令的修饰符 .sync 来实现。
那么,该如何实现呢?
先回忆一下,不利用 v-model 指令来实现组件的双向数据绑定:
<base-input :value="searchText" @input="searchText = $event"></base-input>
Vue.component("base-input", {
props: ["value"],
template: `
<input
:value="value"
@input="$emit('input', $event.target.value)"
/>
`,
});
那么,我们也可以试着,将监听的事件名进行更改,如:
<base-input
:value="searchText"
@update:value="searchText = $event"
></base-input>
Vue.component("base-input", {
props: ["value"],
template: `
<input
:value="value"
@input="$emit('update:value', $event.target.value)"
/>
`,
});
这样也是可以实现双向数据绑定的,那么和 .sync 修饰符 有什么关系呢?
此时,我们对代码进行修改:
<base-input :value.sync="searchText"></base-input>
Vue.component("base-input", {
props: ["value"],
template: `
<input
:value="value"
@input="$emit('update:value', $event.target.value)"
/>
`,
});
所以,.sync 修饰符 本质上也是一个语法糖,在组件上使用:
<base-input :value.sync="searchText"></base-input>
等价于:
<base-input :value="searchText" @update:value="searchText = $event" />
当我们用一个对象同时设置多个 prop 时,也可以将.sync 修饰符和 v-bind 配合使用:
<base-input v-bind.sync="obj"></base-input>
注意:
- 带有.sync 修饰符的 v-bind 指令,只能提供想要绑定的属性名,不能和表达式一起使用,如:
:title.sync="1+1"
,这样操作是无效的 - 将
v-bind.sync
用在 一个字面量对象上,如v-bind.sync="{ title: 'haha' }"
,是无法工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
v-model VS .sync
先明确一件事情,在 vue 1.x 时,就已经支持 .sync 语法,但是此时的 .sync 可以完全在子组件中修改父组件的状态,造成整个状态的变换很难追溯,所以官方在 2.0 时移除了这个特性。然后在 vue2.3 时,.sync 又回归了,跟以往不同的是,现在的.sync 完完全全就是一个语法糖的作用,跟 v-model 的实现原理是一样的,也不容易破环院有的数据模型,所以使用上更安全也更方便。
- 两者都是用于实现双向数据传递的,实现方式都是语法糖,最终通过
prop
+事件
来达成目的。 - vue 1.x 的 .sync 和 v-model 是完全两个东西,vue 2.3 之后可以理解为一类特性,使用场景略微有区别
- 当一个组件对外只暴露一个受控的状态,切都符合统一标准的时候,我们会使用 v-model 来处理。.sync 则更为灵活,凡是需要双向数据传递时,都可以去使用。
组件_插槽
和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样:
<my-cmp> Something bad happened. </my-cmp>
如果有这样的需求,我们就可以通过插槽来做。
插槽内容
通过插槽,我们可以这样合成组件:
<my-cmp> 写在组件标签结构中的内容 </my-cmp>
组件模板中可以写成:
<div>
<slot></slot>
</div>
当组件渲染时,<slot></slot>
将会被替换为“写在组件标签结构中的内容”。
插槽内可以包含任何模板代码,包括 HTML 和其他组件。
如果<my-cmp>
没有包含<slot>
元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。
编译作用域
当在插槽中使用数据时:
<my-cmp> 这是插槽中使用的数据:{{ user }} </my-cmp>
该插槽跟模板的其他地方一样可以访问相同的实例属性,也就是相同的“作用域”,而不能访问<my-cmp>
的作用域。
请记住:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
后备内容
我们可以设置默认插槽,它会在没有提供内容时被渲染,如,在<my-cmp>
组件中:
Vue.compopnent("my-cmp", {
template: `
<button type="submit">
<slot></slot>
</button>
`,
});
我们希望这个<button>
内绝大多数情况下都渲染文本“Submit”,此时就可以将“Submit”作为后备内容,如:
Vue.compopnent("my-cmp", {
template: `
<button type="submit">
<slot>Submit</slot>
</button>
`,
});
当使用组件未提供插槽时,后备内容将会被渲染。如果提供插槽,则后备内容将会被取代。
具名插槽
有时我们需要多个插槽,如<my-cmp>
组件:
Vue.compopnent("my-cmp", {
template: `
<div class="container">
<header>
<!-- 页头 -->
</header>
<main>
<!-- 主要内容 -->
</main>
<footer>
<!-- 页脚 -->
</footer>
</div>
`,
});
此时,可以在<slot>
元素上使用一个特殊的特性:name。利用这个特性定义额外的插槽:
Vue.compopnent("my-cmp", {
template: `
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
`,
});
一个不带 name 的 <slot>
出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
<my-cmp>
<template v-slot:header>
<h1>头部</h1>
</template>
<p>内容</p>
<p>内容</p>
<template v-slot:footer>
<p>底部</p>
</template>
</my-cmp>
现在<template>
元素中的所有内容都会被传入相应的插槽。任何没有被包裹在带有v-slot
的<template>
中的内容都会被视为默认插槽的内容。
为了模板更清晰,也可以写成以下这样:
<my-cmp>
<template v-slot:header>
<h1>头部</h1>
</template>
<template v-slot:default>
<p>内容</p>
<p>内容</p>
</template>
<template v-slot:footer>
<p>底部</p>
</template>
</my-cmp>
注意:v-slot 只能添加在<template>
上,只有一种例外情况。
作用域插槽
为了能够让插槽内容访问子组件的数据,我们可以将子组件的数据作为<slot>
元素的一个特性绑定上去:
Vue.component("my-cmp", {
data() {
return {
user: {
name: "杉杉",
age: 18,
},
};
},
template: `
<span>
<slot v-bind:user="user"></slot>
</span>
`,
});
绑定在 <slot>
元素上的特性被称为插槽 prop。
那么在父级作用域中,我们可以给v-slot
带一个值来定义我们提供的插槽 prop 的名字:
<div id="app">
<my-cmp>
<template v-slot:default="slotProps"> {{ slotProps.user.name }} </template>
</my-cmp>
</div>
独占默认插槽的缩写语法
当被提供的内容只有默认插槽时,组件的标签可以被当作插槽的模板来使用,此时,可以将v-slot
直接用在组件上:
<my-cmp v-slot:default="slotProps"> {{ slotProps.user.name }} </my-cmp>
也可以更简单:
<my-cmp v-slot="slotProps"> {{ slotProps.user.name }} </my-cmp>
注意:默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确。
<!-- 无效,会导致警告 -->
<my-cmp v-slot="slotProps">
{{ slotProps.user.name }}
<template v-slot:other="otherSlotProps">
slotProps 在这里是不合法的
</template>
</my-cmp>
只要出现多个插槽,就需要为所有的插槽使用完整的基于<template>
的语法。
解构插槽 Prop
我们可以使用解构传入具体的插槽 prop,如:
<my-cmp v-slot="{ user }"> {{ user.name }} </my-cmp>
这样模板会更简洁,尤其是在为插槽提供了多个 prop 时。
此外还可以有其他可能,如 prop 重命名:
<my-cmp v-slot="{ user: person }"> {{ person.name }} </my-cmp>
以及自定义后备内容,当插槽 prop 是 undefined 时生效:
<my-cmp v-slot="{ user = { name: 'Guest' } }"> {{ user.name }} </my-cmp>
动态插槽名
Vue 2.6.0 新增
<my-cmp>
<template v-slot:[dynamicSlotName]> ... </template>
</my-cmp>
具名插槽的缩写
Vue 2.6.0 新增
跟v-on
和v-bind
一样,v-slot
也有缩写,将v-slot:
替换为#
。
<my-cmp>
<template #header>
<h1>头部</h1>
</template>
<template #default>
<p>内容</p>
<p>内容</p>
</template>
<template #footer>
<p>底部</p>
</template>
</my-cmp>
当然,和其它指令一样,该缩写只在其有参数的时候才可用。
废弃了的语法
带有 slot 特性的具名插槽
自 2.6.0 起被废弃
<my-cmp>
<template slot="header">
<h1>头部</h1>
</template>
<template>
<p>内容</p>
<p>内容</p>
</template>
<template slot="footer">
<p>底部</p>
</template>
</my-cmp>
带有 slot-scope 特性的作用域插槽
自 2.6.0 起被废弃
<my-cmp>
<template slot="default" slot-scope="slotProps">
{{ slotProps.user.name }}
</template>
</my-cmp>