原文链接(格式更好):《3-2 Vue 进阶》
插槽
Vue2:插槽 Slots | Vue.js
Vue3:插槽 Slots | Vue.js
作用:HTML 内容的传递
// FancyButton.vue
<button class="saveBtn">
<slot></slot>
</button>
// Index.vue
<FancyButton>
保存
</FancyButton>
// 最终渲染出来的 DOM 为:
<button class="saveBtn">
保存
</button>
渲染逻辑如下:
其中的<slot>标签标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
上述代码,类比如下:
// 父元素传入插槽内容
FancyButton('保存')
// FancyButton 在自己的模板中渲染插槽内容
function FancyButton(slotContent) {
return `<button class="saveBtn">
${slotContent}
</button>`
}
插槽内容可以是任意合法的模板内容,不局限于文本,可以是 HTML、组件 等
默认内容
// FancyButton.vue
<button class="saveBtn">
<slot>提交</slot>
</button>
// Index.vue
<FancyButton>
</FancyButton>
// 最终渲染出来的 DOM 为:
<button class="saveBtn">
提交
</button>
当无插槽内容时,可以展示<slot>内的内容
具名插槽
<slot>标签可以name属性,用来标记插槽内容的展示
name 的值可以自定义,默认为default
// FancyButton.vue
<button class="saveBtn">
<slot>提交</slot> // 等价于<slot name="defalut">提交</slot>
</button>
当name="defalut"时,则称为默认插槽,用来承载父元素的所有无名插槽
// Content.vue
<div>
<header>
<slot name="header" />
</header>
<section>
<slot name="default" /> // 等价于 <slot />
</section>
<footer>
<slot name="footer" />
</footer>
</div>
// Index.vue
<Content>
<template v-slot:header> // v-slot:header 等价于 #header
<h1>我是标题</h1>
</template>
<template #default> // <template #default> 可以省略不要
<div>我是主要内容 xxxxxxx</div>
</template>
<template #footer>
<p>我是底部内容 xxxxxxx</p>
</template>
</Content>
// 最终渲染出来的 DOM 为:
<div>
<header>
<h1>我是标题</h1>
</header>
<section>
<div>我是主要内容 xxxxxxx</h1>
</section>
<footer>
<p>我是底部内容 xxxxxxx</p>
</footer>
</div>
渲染逻辑如下:
上述代码,类比如下:
Content({
default: {
// ...
},
header: {
// ...
},
footer: {
// ...
},
})
function Content(slots) {
return `
<div>
<header>
${slots.header}
</header>
<section>
${slots.default}
</section>
<footer>
${slots.footer}
</footer>
</div>
`
}
动态插槽名
支持插槽内容的名称为变量
// Content.vue
<div>
<header>
<slot name="header" />
</header>
<section>
<slot name="default" /> // 等价于 <slot />
</section>
<footer>
<slot name="footer" />
</footer>
</div>
// Index.vue
<template>
<Content>
<template v-slot:[headerName]> // v-slot:[headerName] 等价于 #[headerName]
<h1>我是标题</h1>
</template>
<div>我是主要内容 xxxxxxx</h1>
<template #[footerName]>
<p>我是底部内容 xxxxxxx</p>
</template>
</Content>
</template>
<script setup>
const headerName = "header"
const footerName = "footer"
</script>
渲染作用域
插槽内容仅能访问其定义时作用域的数据,不能访问子组件的数据
// FancyButton.vue
<template>
<button class="saveBtn">
<slot>提交</slot>
</button>
</template>
<script setup>
const count = 1
</script>
// Index.vue
<template>
<FancyButton>
{{ flag ? '提交' : '暂存' }} // '提交'
{{ count }} // 报错,count 是子组件的数据,无法在该作用域下访问
</FancyButton>
</template>
<script setup>
const flag = true
</script>
// 最终渲染出来的 DOM 为:
<button class="saveBtn">
提交
</button>
作用域插槽
如果想要插槽内容访问子组件的数据,可以通过<slot>标签回传值
传出语法:<slot :propKey1="propValue1" :propKey2="propValue2" ... />
// Content.vue
<template>
<div>
<header>
// 具名插槽传值
<slot name="header" :contentHeaderString="contentHeaderString" />
</header>
<section>
// 默认插槽传值
<slot name="default" :contentDefaultString="contentDefaultString" />
</section>
<footer>
<slot name="footer" />
</footer>
</div>
</template>
<script setup lang="ts">
const contentHeaderString = '我是 Content 组件内的 contentHeaderString 变量'
const contentDefaultString = '我是 Content 组件内的 contentDefaultString 变量'
</script>
默认插槽接受传参语法:<组件名 v-slot="slotProps"> ... </组件名>
// Index.vue
<Content v-slot="defaultSlotProps"> // 默认插槽接受传参
<div>我是主要内容 xxxxxxx:{{ defaultSlotProps.contentDefaultString }}</div>
</Content>
具名插槽接受传参语法:<template v-slot:slotName="slotProps"> ... </template>
// Index.vue
<Content>
<template v-slot:header="slotProps"> // 可简写为 #header="slotProps",具名插槽接受传参
<h1>我是标题:{{ slotProps.contentHeaderString }}</h1>
</template>
</Content>
当存在其他命名槽时,默认槽必须在自定义元素上使用“<template>”
// Index.vue
<Content>
<template v-slot:header="slotProps"> // 可简写为 #header="slotProps",具名插槽接受传参
<h1>我是标题:{{ slotProps.contentHeaderString }}</h1>
</template>
<template v-slot="defaultSlotProps"> // 默认插槽接受传参,必须使用 <template>
<div>我是主要内容 xxxxxxx:{{ defaultSlotProps.contentDefaultString }}</div>
</template>
<template #footer>
<p>我是底部内容 xxxxxxx</p>
</template>
</Content>
渲染逻辑如下:
上述代码,类比如下:
Content({
default(slotProps) {
// slotProps = { contentHeaderString }
return `<div>我是主要内容 xxxxxxx:${ slotProps.contentDefaultString }</div>`
},
header(slotProps) {
// slotProps = { contentHeaderString }
return `<div>我是标题:${ slotProps.contentHeaderString }</div>`
},
footer: {
// ...
},
})
function Content(slots) {
const contentHeaderString = '我是 Content 组件内的 contentHeaderString 变量'
const contentDefaultString = '我是 Content 组件内的 contentDefaultString 变量'
return `
<div>
<header>
${slots.header({ contentHeaderString })}
</header>
<section>
${slots.default({ contentDefaultString })}
</section>
<footer>
${slots.footer}
</footer>
</div>
`
}
过滤器
Vue3 已不支持
Vue2:过滤器 — Vue.js
在模板里面对数据的二次加工
<template>
<div>{{ message | capitalize }}</div>
<div :id="id | capitalize">xxx</div>
</template>
<script>
export default {
data() {
return {
message: 'hello',
id: 'div-1'
}
},
filters: {
capitalize(value) {
return value[0].toUpperCase() + value.slice(1)
}
}
}
</script>
// 最终渲染出来的是:
<div>Hello</div>
<div id="Div-1">xxx</div>
注意:过滤器内部的 this 不会自动绑定到 Vue 实例上,其值为undefined
原因是:过滤器本质是纯函数(对进来的数据处理然后返回结果),所以设计时就不应该跟 Vue 实例挂钩
JSX
基础:
全称:JavaScript XML,是一种在JavaScript中嵌入类似HTML语法的扩展语法
const Hello = <div>Hello, World!~</div>
看起来像 HTML,本质还是 JS 代码,最终会编译为 JS 代码(代表了对应的 DOM/VDOM),最后使用框架的能力渲染到页面上
上述代码经过编译后为(React 举例):
import { jsx as _jsx } from "react/jsx-runtime";
/*#__PURE__*/_jsx("div", {
children: "Hello, world!~"
});
Vue2 中 JSX 写法:
<script>
export default {
data() {
return {
money: 100,
};
},
render() {
const MoneyDom =
this.money > 99 ? <h1>99+ 元</h1> : <h2>{this.money} 元</h2>;
return MoneyDom;
},
};
</script>
// 最终渲染出来的是:
<h1>99+ 元</h1>
Vue3 中 JSX 写法(需要配置):
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const money = ref(100)
const MoneyDom = money.value > 99 ? <h1>99+ 元</h1> : <h2>{money.value} 元</h2>
return () => MoneyDom
}
})
语法糖实现:
v-model:事件绑定
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const inputValue = ref('')
const hnadleOnInput = (event) => {
inputValue.value = event.target.value
}
return () => (
<div>
<input @input={hnadleOnInput}/>
<p>输入的内容是:{ inputValue }</p>
</div>
)
}
})
v-if:三目运算
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const isAdd = true
return () => (
<div>
<button>{ isAdd ? '新建' : '保存' }</button>
</div>
)
}
})
v-for:循环
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const userList = [{ name: 'lisi', age: 29 }, { name: '张三', age: 39 }]
return () => (
<div>
<ul>
{
userList.map(item => {
return <li key={item.name}>
<span>姓名:{item.name}</span>
<span>年龄:{item. age}</span>
</li>
})
}
</ul>
</div>
)
}
})
好处:更加灵活
坏处:结构不够清晰
混入
Vue2:混入 — Vue.js
Vue3(不再推荐使用):混入 | Vue.js
基础
一种组合组件中的可复用功能的方式。更关注于组合
var mixin = {
data() {
return {
message: 'hello',
foo: 'abc'
}
}
}
<script>
export default {
mixins: [mixin],
data() {
return {
message: '你好',
bar: 'def'
}
},
created() {
console.log(this.$data)
// { message: "你好", foo: "abc", bar: "def" }
}
}
</script>
顺序
生命周期函数
同名时,先调用混入的,后调用组件的
类似于:
最终生命周期函数 = [
mixin1.生命周期函数,
mixin2.生命周期函数,
...
组件.生命周期函数
].map(fn => fn())
其他的
同名时,使用组件的
类似于:
最终 data = Object.assgin(
mixin1.data(),
mixin2.data(),
...
组件.$data()
)
最终 methods = Object.assgin(
mixin1.methods,
mixin2.methods,
...
组件.methods
)
// 其他等......
继承
Vue2:继承 | Vue.js
Vue3(不再推荐使用):继承 | Vue.js
一种继承组件中的可复用功能的方式。更关注于继承
写法与顺序都跟混入一样,关键词为extends: extendObj即可
当extends 与 mixins共存时,extends优先级更高
类似于:
最终生命周期函数 = [
extends.生命周期函数,
mixin1.生命周期函数,
mixin2.生命周期函数,
...
组件.生命周期函数
].map(fn => fn())