3-2 Vue 进阶

119 阅读2分钟

原文链接(格式更好):《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())