vue3高级语法

715 阅读6分钟

一、render函数

1.1 介绍

Vue推荐在绝大数情况下使用模板来创建HTML,然后一些特殊的场景,如果真的需要JavaScript的完全编程的能力,这个时候可以使用渲染函数,它比模板更接近编译器

  • Vue在生成真实的DOM之前,会将节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM(VDOM), 事实上,编写的 template 中的HTML最终也是使用渲染函数生成对应的VNode,如果想充分的利用JavaScript的编程能力,就可以自己来编写 createVNode 函数,生成对应的VNode

  • 通过使用 h()函数:

    • h() 函数是一个用于创建 vnode 的一个函数
    • 其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数
  • 如何使用?

    • 接收3个参数 image.png

    • 注意事项:

      • 如果没有props,那么通常可以将children作为第二个参数传入
      • 如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入

1.2 h函数的基本使用

通过计数器的案例介绍

  • optionsApi使用

    <script>
    import { h } from "vue";
    
    export default {
      data() {
        return {
          counter: 0,
        };
      },
      render() {
        return h("div", { className: "app" }, [
          h("h2", null, `${this.counter}`),
          h("button", { onClick: () => this.counter++ }, "+1"),
          h("button", { onClick: () => this.counter-- }, "-1"),
        ]);
      },
    };
    </script>
    
  • compositionApi在setup中使用

    <script>
    import { h, ref } from "vue";
    
    export default {
      setup() {
        const counter = ref(0);
    
        const increment = () => {
          counter.value++;
        };
    
        const decrement = () => {
          counter.value--;
        };
    
        return () => {
          return h("div", null, [
            h("h2", null, "setup写法"),
            h("h2", { className: "num" }, `${counter.value}`),
            h("button", { onClick: increment }, "+1"),
            h("button", { onClick: decrement }, "-1"),
          ]);
        };
      },
    };
    </script>
    
    <style>
    .num {
      color: blueviolet;
    }
    </style>
    

    image.png

  • compositionApi script顶层setup

    • 这种写法仍需要template
    <template>
      <render />
    </template>
    
    <script setup>
    import { h, ref } from "vue";
    
    const counter = ref(0);
    
    const increment = () => {
      counter.value++;
    };
    
    const decrement = () => {
      counter.value--;
    };
    
    const render = () => {
      return h("div", null, [
        h("h2", null, "顶层setup写法"),
        h("h2", { className: "num" }, `${counter.value}`),
        h("button", { onClick: increment }, "+1"),
        h("button", { onClick: decrement }, "-1"),
      ]);
    };
    </script>
    
    <style>
    .num {
      color: blueviolet;
    }
    </style>
    

1.3 h函数和插槽的使用

  • 子组件接收props和插槽

    <script>
    import { h } from "vue";
    
    export default {
      props: {
        message: "",
      },
      setup(props, { attrs, slots, emit }) {
        console.log(props.message);
        const render = () => {
          return h("div", null, [
            h("h2", null, "Hello World"),
            slots.default ? slots.default({ text: props.message })
              : h("span", null, "我是插槽默认值"),
          ]);
        };
    
        return render;
      },
    };
    </script>
    
  • 在父组件传入插槽

    <script>
    import { h } from "vue";
    import HelloWorld from "./HelloWorld.vue";
    
    export default {
      setup() {
        return () => {
          return h("div", null, [
            h(
              HelloWorld,
              // 传入属性
              { message: "你好,世界!" },
              // 如果是多个插槽,此处写一个数组
              {
                default: (props) =>
                  h("span", null, `app传入到HelloWorld中的内容: ${props.text}`),
              }
            ),
          ]);
        };
      },
    };
    </script>
    

    image.png

二、jsx语法

  • 如果写了很多渲染函数,可能会觉得很痛苦,那就试一下jsx语法吧

2.1 安装插件(vue-cli3默认是支持jsx格式

  • 如果希望在项目中使用jsx,就需要添加对jsx的支持:

    • 通常会通过Babel来进行转换

    • 对于Vue来说,只需要在Babel中配置对应的插件即可

    • 安装Babel支持Vue的jsx插件:npm install @vue/babel-plugin-jsx -D

    • 在babel.config.js配置文件中配置插件:

      image.png

  • vite 环境

    • 安装插件:npm install @vitejs/plugin-vue-jsx - D
    • 配置
    import { fileURLToPath, URL } from 'node:url'
    
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import jsx from '@vitejs/plugin-vue-jsx'
    
    // https://vitejs.dev/config/
    export default defineConfig({
      plugins: [
        vue(),
        jsx()
      ],
      resolve: {
        alias: {
          '@': fileURLToPath(new URL('./src', import.meta.url))
        }
      }
    })
    

2.2 基本使用

计数器案例

  • optionsApi

    <script lang="jsx">
    export default {
      data() {
        return {
          counter: 0,
        };
      },
      render() {
        return (
          <div class="app">
            <h2>{this.counter}</h2>
            <button onClick={this.increment}>+1</button>
            <button onClick={this.decrement}>-1</button>
          </div>
        );
      },
      methods: {
        increment() {
          this.counter++;
        },
        decrement() {
          this.counter--;
        },
      },
    };
    </script>
    
  • compositionApi

    <template>
      <jsx />
    </template>
    
    <script lang="jsx" setup>
    import { ref } from "vue";
    import Home from "./Home.vue";
    
    const counter = ref(0);
    
    const increment = () => {
      counter.value++;
    };
    
    const decrement = () => {
      counter.value--;
    };
    
    const jsx = () => {
      return (
        <div class="app">
          <h2>{counter.value}</h2>
          <button onClick={increment}>+1</button>
          <button onClick={decrement}>-1</button>
          <!-- 使用子组件 -->
          <Home />
        </div>
      );
    };
    </script>
    

三、自定义指令

3.1 介绍

  • 在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。

    • 注意:在Vue中,代码的复用和抽象主要还是通过组件
    • 通常在某些情况下,需要对DOM元素进行底层操作,这个时候就会用到自定义指令
  • 自定义指令分为两种:

    • 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用

    • 自定义全局指令:app的 directive 方法,可以在任意组件中被使用

3.2 基本使用

此处做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点

  • 默认的实现方式

    <template>
      <div>
        <input type="text" ref="inputRef" />
      </div>
    </template>
    
    <script setup>
    import { ref, onMounted } from "vue";
    
    const inputRef = ref();
    onMounted(() => {
      inputRef.value?.focus();
    });
    </script>
    
    • 封装hooks
    import { ref, onMounted } from 'vue';
    
    export default function useInput() {
      const inputRef = ref()
    
      onMounted(() => {
        inputRef.value?.focus()
      })
    
      return { inputRef }
    }
    
  • 自定义一个 v-focus 的局部指令

    • 需要在组件选项中使用 directives 即可
    • 它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加v-)
    • 自定义指令有一个生命周期,是在组件挂载后调用的 mounted,可以在其中完成操作
    <template>
      <div>
        <input type="text" v-focus />
      </div>
    </template>
    
    <script>
    export default {
      directives: {
        focus: {
          mounted(el) {
            el.focus();
          },
        },
      },
    };
    </script>
    
    • setup顶层语法中使用
    <script setup>
    const vFocus = {
      mounted(el) {
        el.focus();
      },
    };
    </script>
    
  • 自定义一个 v-focus 的全局指令

    const app = createApp(App);
    
    app.directive("focus", {
      mounted(el) {
        el.focus();
      },
    });
    

3.3 指令的生命周期

  • created:在绑定元素的 attribute 或事件监听器被应用之前调用

  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用

  • mounted:在绑定元素的父组件被挂载后调用

  • beforeUpdate:在更新包含组件的 VNode 之前调用

  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用

  • beforeUnmount:在卸载绑定元素的父组件之前调用

  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次

    <template>
      <div class="app">
        <button @click="counter++">+1</button>
        <button @click="showTitle = false">隐藏</button>
        <h2 v-if="showTitle" class="title" v-test>当前计数: {{ counter }}</h2>
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    
    const counter = ref(0)
    const showTitle = ref(true)
    
    const vTest = {
      created() {
        console.log("created")
      },
      beforeMount() {
        console.log("beforeMount")
      },
      mounted() {
        console.log("mounted")
      },
      beforeUpdate() {
        console.log("beforeUpdate")
      },
      updated() {
        console.log("updated")
      },
      beforeUnmount() {
        console.log("beforeUnmount")
      },
      unmounted() {
        console.log("unmounted")
      }
    }
    </script>
    

3.4 指令的参数和修饰符

  • 接受一些参数或者修饰符
    • ddd是参数的名称
    • abc,cba 是修饰符的名称
    • 后面是传入的具体的值;
  • 可以通过 bindings 获取到对应的内容
<template>
  <div class="app">
    <h2 class="title" v-test:ddd.abc.cba="message">哈哈哈</h2>
  </div>
</template>

<script setup>
import { ref } from "vue";

const message = ref("Hello World");

const vTest = {
  mounted(el, bindings) {
    console.log(bindings);
    el.textContent = bindings.value;
  },
};
</script>

image.png

3.5 时间格式化指令(将时间戳进行格式化)

  • 时间格式化指令

    import dayjs from "dayjs";
    
    export default function (app) {
      app.directive("ftime", {
        mounted(el, bindings) {
          // 1.获取时间, 并且转化成毫秒
          let timestamp = el.textContent;
          if (timestamp.length === 10) {
            timestamp = timestamp * 1000;
          }
          
          // 转为数字类型(如果不是10位数)
          timestamp = Number(timestamp)
    
          // 2.获取传入的参数
          let value = bindings.value;
          if (!value) {
            value = "YYYY-MM-DD HH:mm:ss";
          }
    
          // 3.对时间进行格式化
          const formatTime = dayjs(timestamp).format(value);
          el.textContent = formatTime;
        },
      });
    }
    
  • 使用

    <template>
      <div class="app">
        <h2 v-ftime="'YYYY/MM/DD'">{{ timestamp }}</h2>
        <h2 v-ftime>{{ 1551111166666 }}</h2>
      </div>
    </template>
    
    <script setup>
    const timestamp = 1231355453;
    </script>
    

四、安装插件

4.1 认识插件

  • 我们肯定用到过vue-router、vuex或者pinia,这些都属于vue的插件

  • 通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:

    • 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行
    • 函数类型:一个function,这个函数会在安装插件时自动执行
  • 插件可以完成的功能没有限制,比如下面的几种都是可以的:

    • 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现

    • 添加全局资源:指令/过滤器/过渡等

    • 通过全局 mixin 来添加一些组件选项

    • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能

4.2 使用

  • 对象语法

    // 全局添加属性
    export default {
      install(app) {
        app.config.globalProperties.$name = "test"
      }
    }
    
    • 引入,在main.js中使用app.use(),就能全局访问
    <template>
      <div>hhh</div>
    </template>
    
    <script setup>
    // export default {
    //   mounted() {
    //     console.log(this.$name);
    //   },
    // };
    
    import { getCurrentInstance } from "vue";
    
    const instance = getCurrentInstance();
    console.log(instance.appContext.config.globalProperties.$name); // test
    </script>
    
  • 函数语法

    export default function (app) {
      // 添加组件或混入
      // app.component()
      // app.mixin()
    
      console.log(app);
    }
    
  • main.js中 app.use()会自动执行对象插件的install方法或者直接执行函数

五、内置组件

5.1 teleport

  • 在组件化开发中,我们封装一个组件A,在另外一个组件B中使用:

    • 组件A中template的元素,会被挂载到组件B中template的某个位置,应用程序会形成一颗DOM树结构
  • 某些情况下,我们会希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置:

    • 比如移动到body元素上,或者我们有其他的div#app之外的元素上

    • 这个时候我们就可以通过teleport来完成

  • Teleport是什么呢?

    • 一个Vue提供的内置组件,teleport翻译过来是心灵传输、远距离运输的意思

    • 它有两个属性:

      • to:指定将其中的内容移动到的目标元素,可以使用选择器

      • disabled:是否禁用 teleport 的功能

  • 基本使用

    <template>
      <div class="app">
        <div class="hello">
          <p class="content">
            <!-- 挂在到body元素 -->
            <teleport to="body">
              <hello-world />
            </teleport>
          </p>
        </div>
      </div>
    </template>
    
    <script setup>
    import HelloWorld from "./HelloWorld.vue";
    </script>
    

    image.png

  • 挂在到某个id,多个teleport使用

    • 将多个teleport应用到同一个目标上(to的值相同),那么这些目标会进行合并
    <template>
      <div class="app">
        <div class="hello">
          <p class="content">
            <teleport to="#abc">
              <hello-world />
            </teleport>
          </p>
        </div>
    
        <p class="content">
          <teleport to="#abc">
            <h2>App</h2>
          </teleport>
        </p>
      </div>
    </template>
    
    <script setup>
    import HelloWorld from "./HelloWorld.vue";
    </script>
    

    image.png

5.2 suspence(慎用)

Suspense显示的是一个实验性的特性,API随时可能会修改

  • Suspense是一个内置的全局组件(一般用在异步组件中),该组件有两个插槽:

    • default:如果default可以显示,那么显示default的内容

    • fallback:如果default无法显示,那么会显示fallback插槽的内容

  • 使用

    • 通常用于异步组件,在组件未加载时显式fallback插槽的内容,加载完成后则显示异步组件
    <template>
      <div class="app">
        <suspense>
          <template #default>
            <async-home />
          </template>
          <template #fallback>
            <h2>loading</h2>
          </template>
        </suspense>
      </div>
    </template>
    
    <script setup>
    import { defineAsyncComponent } from "vue";
    
    const AsyncHome = defineAsyncComponent(() => import("./AsyncHome.vue"));
    </script>