Skip to content

认识自定义指令

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

自定义自己的指令。

注意:在Vue中,代码的复用和抽象主要还是通过组件;

通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令;

自定义指令分为两种:

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

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

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

实现方式一:如果我们使用默认的实现方式;

实现方式二:自定义一个 v-focus 的局部指令;

实现方式三:自定义一个 v-focus 的全局指令;

实现方式一:聚焦的默认实现

我们想进入一个页面,就让这个页面的输入框自动获取焦点。可以自定义一个hook来实现,但是每个组件内使用,还是需要导入再使用,有没有更简便的方法呢?答案是使用自定义指令。

javascript
<template>
  <div class="app">
    <input type="text" ref="inputRef">
  </div>
</template>

<script setup>

// 1.方式一: 定义ref绑定到input中, 调用focus
import useInput from "./hooks/useInput"
const { inputRef } = useInput()
</script>

<style scoped>

</style>

hooks/useInput.js

javascript
import { ref, onMounted } from 'vue';

export default function useInput() {
  const inputRef = ref()

  onMounted(() => {
    inputRef.value?.focus()
  })

  return { inputRef }
}

实现方式二:局部自定义指令

实现方式二:自定义一个 v-focus 的局部指令

这个自定义指令实现非常简单,我们只需要在组件选项中使用 directives 即可;

它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加v-);

自定义指令有一个生命周期,是在组件挂载后调用的 mounted,我们可以在其中完成操作;

javascript
<template>
  <div class="app">
    <!-- <input type="text" ref="inputRef"> -->
    <input type="text" v-focus>
  </div>
</template>

<script>
  // Option API 自定义指令的写法
  export default {
    directives: {
      focus: {
        // 生命周期的函数(自定义指令)
        mounted(el) {
          // console.log("v-focus应用的元素被挂载了", el) // el就是加自定义指令的元素
          el?.focus()
        }
      }
    }
  }

</script>

<script setup>

// 1.方式一: 定义ref绑定到input中, 调用focus
// import useInput from "./hooks/useInput"
// const { inputRef } = useInput()


// 2.方式二: 自定义指令(局部指令) Composition API自定义指令的写法,必须以v开头的驼峰式写法
const vFocus = {
  // 生命周期的函数(自定义指令)
  mounted(el) {
    // console.log("v-focus应用的元素被挂载了", el)
    el?.focus()
  }
}

</script>

<style scoped>

</style>

实现方式三:自定义全局指令

自定义一个全局的v-focus指令可以让我们在任何地方直接使用

在main.js中自定义全局指令

javascript
import { createApp } from 'vue'

const app = createApp(App)
app.directive("focus", {
    // 生命周期的函数(自定义指令)
    mounted(el) {
      // console.log("v-focus应用的元素被挂载了", el)
      el?.focus()
    }
})

app.mount('#app')

但是在开发中可能自定义指令特别的多,那么如果把代码都放到main.js就不合适了,我们可以在src目录下建一个directives文件夹专门用来处理自定义指令。

directives/focus.js

javascript
export default function directiveFocus(app) {
  app.directive("focus", {
    // 生命周期的函数(自定义指令)
    mounted(el) {
      // console.log("v-focus应用的元素被挂载了", el)
      el?.focus()
    }
  })
}

有个统一暴露的出口,directives/index.js

javascript
import directiveFocus from "./focus"

export default function useDirectives(app) {
  directiveFocus(app)
}

main.js

javascript
import { createApp } from 'vue'

import useDirectives from "./01_自定义指令/directives/index"

const app = createApp(App)
useDirectives(app)
app.mount('#app')

指令的生命周期

一个指令定义的对象,Vue提供了如下的几个钩子函数:

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

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

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

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

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

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

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

javascript
<template>
  <div class="app">
    <button @click="counter++">+1</button>
    <button @click="showTitle = false">隐藏</button>
    <h2 v-if="showTitle" class="title" v-why>当前计数: {{ counter }}</h2>
  </div>
</template>

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


const counter = ref(0)
const showTitle = ref(true)

const vWhy = {
  // 页面创建的时候会调用 created beforeMount mounted
  created() {
    console.log("created")
  },
  beforeMount() {
    console.log("beforeMount")
  },
  mounted() {
    console.log("mounted")
  },
  // 点击+1按钮会触发 beforeUpdate updated
  beforeUpdate() {
    console.log("beforeUpdate")
  },
  updated() {
    console.log("updated")
  },
  // 点击隐藏按钮会触发 beforeUnmount unmounted
  beforeUnmount() {
    console.log("beforeUnmount")
  },
  unmounted() {
    console.log("unmounted")
  }
}

</script>

<style scoped>

</style>

指令的参数和修饰符

如果我们指令需要接受一些参数或者修饰符应该如何操作呢?

info是参数的名称;

aaa和bbb是修饰符的名称;

后面是传入的具体的值;

在我们的生命周期中,我们可以通过 bindings 获取到对应的内容

image-20230601051735166

javascript
<template>
  <div class="app">
    <!-- 1.参数-修饰符-值 kobe是参数名,abc和cba都是修饰符,message是传入的值-->
    <h2 v-why:kobe.abc.cba="message">哈哈哈哈</h2>
  </div>
</template>

<script setup>
    const message = '你好啊, 李银河'
    const vWhy = {
      mounted(el, bindings) {
        console.log(bindings)
        el.textContent = bindings.value // 刷新页面会将 哈哈哈哈 替换为 你好啊, 李银河
      }
    }

</script>

<style scoped>

</style>

我们来实现一个给价格加符合的自定义指令,比如100会变成¥100

directives/unit.js

javascript
export default function directiveUnit(app) {
  app.directive("unit", {
    mounted(el, bindings) {
      const defaultText = el.textContent
      let unit = bindings.value
      if (!unit) {
        unit = "¥"
      }
      el.textContent = unit + defaultText
    }
  })
}

directives/index.js

javascript
import directiveFocus from "./focus"
import directiveUnit from "./unit"

export default function useDirectives(app) {
  directiveFocus(app)
  directiveUnit(app)
}

时间格式化指令

javascript
import dayjs from 'dayjs'

export default function directiveFtime(app) {
  app.directive("ftime", {
    mounted(el, bindings) {
      // 1.获取时间, 并且转化成毫秒
      let timestamp = el.textContent
      if (timestamp.length === 10) {
        timestamp = timestamp * 1000
      }

      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
    }
  })
}
javascript
<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>

<style scoped>

</style>

认识Teleport

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

那么组件A中template的元素,会被挂载到组件B中template的某个位置;

最终我们的应用程序会形成一颗DOM树结构

但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置

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

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

Teleport是什么呢?

它是一个Vue提供的内置组件,类似于react的Portals;

teleport翻译过来是心灵传输、远距离运输的意思;

它有两个属性:

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

disabled:是否禁用 teleport 的功能;

本来hello-world是会被挂载到.content里面的,但是因为使用了teleport就会被挂载到body中

javascript
<template>
  <div class="app">
    <div class="hello">
      <p class="content">
        <teleport to="body">
          <hello-world/>
        </teleport>
      </p>
    </div>
  </div>
</template>

<script setup>

import HelloWorld from "./HelloWorld.vue"

</script>

<style scoped>

</style>
image-20230601061911645

除了可以挂载到body中,可以挂载到abc中,在index.html中加一个id为abc的div元素

index.html

javascript
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
    <style>
      #abc {
        border: 1px solid orange;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>

    <div id="abc"></div>

    <script type="module" src="/src/main.js"></script>
  </body>
</html>
javascript
<template>
  <div class="app">
    <div class="hello">
      <p class="content">
        <teleport to="#abc">
          <hello-world/>
        </teleport>
      </p>
    </div>
  </div>
</template>

<script setup>

import HelloWorld from "./HelloWorld.vue"

</script>

<style scoped>

</style>
image-20230601062157071

多个teleport

如果我们将多个teleport应用同一个目标上(to的值相同),那么这些目标会进行合并

javascript
<template>
  <div class="app">
    <div class="hello">
      <p class="content">
        <teleport to="#abc">
          <hello-world/>
        </teleport>
      </p>
    </div>

    <div class="content">
      <teleport to="#abc">
        <h2>哈哈哈哈哈</h2>
      </teleport>
    </div>
  </div>
</template>

<script setup>

import HelloWorld from "./HelloWorld.vue"

</script>

<style scoped>

</style>
image-20230601062319164

异步组件和Suspense

注意:目前(2022-08-01)Suspense显示的是一个实验性的特性,API随时可能会修改。

Suspense是一个内置的全局组件,该组件有两个插槽:

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

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

javascript
<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>

<style scoped>

</style>

认识Vue插件

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

对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;

函数类型:一个function,这个函数会在安装插件时自动执行;

插件可以完成的功能没有限制,比如下面的几种都是可以的:

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

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

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

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

router/index.js

javascript
import { createRouter } from 'vue-router'

// createRouter函数返回的对象里面有个install方法
const router = createRouter({
  // history:
})

export default router
javascript
// 安装插件
// 方式一: 传入对象的情况 vue-router导出的router本质上是一个对象,内部有个install方法
app.use({
  install: function(app) {
    console.log("传入对象的install被执行:", app)
  }
})


// 方式二: 传入函数的情况
app.use(function(app) {
  console.log("传入函数被执行:", app)
})

那么,现在就可以使用插件的方式来自定义指令,写出来的代码会更加的优雅

directives/index.js

javascript
import directiveFocus from "./focus"
import directiveUnit from "./unit"
import directiveFtime from "./ftime"

// export default function useDirectives(app) {
//   directiveFocus(app)
//   directiveUnit(app)
//   directiveFtime(app)
// }

export default function directives(app) {
  directiveFocus(app)
  directiveUnit(app)
  directiveFtime(app)
}

main.js

javascript
import { createApp } from 'vue'
import directives from "./01_自定义指令/directives/index" // directives是个函数,使用插件会自动执行

// import router from "./router"

// 自定义指令的方式一:
// const app = createApp(App)
// // useDirectives(app)
// directives(app)
// app.mount('#app')

// 自定义指令的方式二:使用插件
createApp(App).use(directives).mount("#app")

认识h函数

Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时

候你可以使用 渲染函数 ,它比模板更接近编译器

前面我们讲解过VNode和VDOM的概念:

Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM

(VDOM);

事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode;

那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode;

那么我们应该怎么来做呢?使用 h()函数:

h() 函数是一个用于创建 vnode 的一个函数;

其实更准确的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数;

image-20230601213141032

h函数 如何使用呢?

h()函数 如何使用呢?它接受三个参数:

image-20230601213257432image-20230601213310710image-20230601213322542

注意事项:

如果没有props,那么通常可以将children作为第二个参数传入;

如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入;

image-20230601213444526

h函数的基本使用

h函数可以在两个地方使用:

render函数选项中;

javascript
<script>
  import { h } from 'vue'

  export default {
    render() {
      return h("div", { className: "app" }, [
        h("h2", { className: "title" }, "我是标题"),
        h("p", { className: "content" }, "我是内容, 哈哈哈"),
      ])
    }
  }
</script>

<style scoped>

</style>

setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);

javascript
<script>
  import { h, ref } from 'vue'
  import Home from "./Home.vue"

  export default {
    setup() {
      const counter = ref(0)

      const increment = () => {
        counter.value++
      }
      const decrement = () => {
        counter.value--
      }

      return () => h("div", { className: "app" }, [
        h("h2", null, `当前计数: ${counter.value}`),
        h("button", { onClick: increment }, "+1"),
        h("button", { onClick: decrement }, "-1"),
        h(Home)
      ])
    }
  }
</script>

<style scoped>

</style>
javascript
<template>
  <render/>
</template>

<script setup>

import { ref, h } from 'vue';
import Home from './Home.vue'

const counter = ref(0)

const increment = () => {
  counter.value++
}
const decrement = () => {
  counter.value--
}

const render = () => h("div", { className: "app" }, [
  h("h2", null, `当前计数: ${counter.value}`),
  h("button", { onClick: increment }, "+1"),
  h("button", { onClick: decrement }, "-1"),
  h(Home)
])

</script>

<style scoped>

</style>

h函数计数器案例

javascript
<script>
  import { h } from 'vue'
  import Home from "./Home.vue"

  export default {
    data() {
      return {
        counter: 0
      }
    },

    render() {
      return h("div", { className: "app" }, [
        h("h2", null, `当前计数: ${this.counter}`),
        h("button", { onClick: this.increment }, "+1"),
        h("button", { onClick: this.decrement }, "-1"),
        h(Home) // 也可以渲染一个组件
      ])
    },
    methods: {
      increment() {
        this.counter++
      },
      decrement() {
        this.counter--
      }
    }
  }
</script>

<style scoped>

</style>

jsx的babel配置

如果我们希望在项目中使用jsx,那么我们需要添加对jsx的支持

jsx我们通常会通过Babel来进行转换(React编写的jsx就是通过babel转换的);

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

安装Babel支持Vue的jsx插件

javascript
npm install @vue/babel-plugin-jsx -D

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

image-20230601221225170

如果是Vite环境,需要安装插件:

javascript
npm install @vitejs/plugin-vue-jsx -D

vite.config.js

javascript
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))
    }
  }
})

jsx语法基本使用

javascript
<script lang="jsx">
  export default {
    render() {
      return (
        <div class="app">
          <h2>我是标题</h2>
          <p>我是内容, 哈哈哈</p>
        </div>
      )
    }
  }
</script>

<style lang="less" scoped>

</style>

jsx语法实现计数器

javascript
<script lang="jsx">
  import About from './About.vue'

  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>
          <About/>
        </div>
      )
    },
    methods: {
      increment() {
        this.counter++
      },
      decrement() {
        this.counter--
      }
    }
  }
</script>

<style lang="less" scoped>

</style>

在Vue3中使用jsx

javascript
<script lang="jsx">
  import { ref } from 'vue'
  import About from './About.vue'

  export default {
    setup() {
      const counter = ref(0)

      const increment = () => {
        counter.value++
      }
      const decrement = () => {
        counter.value--
      }


      return () => (
        <div class="app">
          <h2>当前计数: { counter.value }</h2>
          <button onClick={ increment }>+1</button>
          <button onClick={ decrement }>-1</button>
          <About/>
        </div>
      )
    }
  }
</script>
javascript
<template>
  <jsx/>
</template>

<script lang="jsx" setup>
import { ref } from 'vue'
import About from "./About.vue"

const counter = ref(0)

const increment = () => {
  counter.value++
}
const decrement = () => {
  counter.value--
}

const jsx = () => (
  <div class="app">
    <h2>当前计数: { counter.value }</h2>
    <button onClick={ increment }>+1</button>
    <button onClick={ decrement }>-1</button>
    <About/>
  </div>
)
</script>

<style lang="less" scoped>

</style>