vue3学习 --- 组件高级补充

982 阅读6分钟

render函数

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

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

我们编写的 template 中的HTML 最终也是使用**渲染函数(render函数)**生成对应的VNode

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

也就是自己编写render函数,直接使用JavaScript的方式编写模板。

此时因为render函数需要返回的是vnode,所以我们需要借助另一个函数h() 函数,

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

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

h函数接收三个参数:

  1. 元素或组件
  2. 元素或组件上的属性 --- 可选的
  3. 子元素,子组件,数组(多个子元素的时候) --- 可选的

注意事项:

  • 如果没有props,那么通常可以将children作为第二个参数传入但是不推荐
  • 容易产生歧义,推荐将null{}作为第二个参数传入,将children作为第三个参数传入
<script>
  import { h } from 'vue'

  export default {
    // template中的代码会被编译为render函数
    // 而render其实是options api中的一个选项
    // 如果我们自己编写了render函数,那么我们就不需要在编写template模板
    // 如果同时存在template和render函数,那么vue编译器会去读取template 忽略render函数
    render() {
      // render函数需要返回vnode
      // 我们无法直接编写vnode, 所以存在h函数(createVNode)函数
      // 来为我们生成对应的vnode
      return h('h2', { title: 'title' }, 'Hello Vue')
    }
  }
</script>

<!-- 
	=> <h2 title="title">Hello Vue</h2>
-->
<script>
  import { h } from 'vue'
  import Cpn from './components/Cpn.vue'

  export default {
    render() {
      return h(Cpn, { title: 'title' })
    }
  }
</script>
<script>
  import { h } from 'vue'
  import Cpn from './components/Cpn.vue'

  export default {
    // render函数是和data,methods,setup同级的一个选项
    render() {
      return h('div', { title: 'title' }, [
        'Hello Vue',
        h('p', {}, 'Hello Render'),
        Cpn.render()
      ])
    }
  }
</script>

计数器案例

写法1

<script>
import { h } from 'vue'

export default {
  name: 'App',

  data() {
    return {
      count: 0
    }
  },

  render() {
    return h('div', {}, [
      h('p', {}, `count:${this.count}`),

      h('button', {
        onClick: () => this.count++
      }, '+1'),

      h('button', {
        onClick: () => this.count--
      }, '-1')
    ])
  }
}
</script>

写法2

<script>
import { ref, h } from 'vue'

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

    return {
      count
    }
  },

  render() {
    // 在render函数中,this有被正常绑定,所以可以使用this
    // 在这里使用ref对象的时候,会自动解包
    return h('div', {}, [
      h('p', {}, `count:${this.count}`),

      h('button', {
        onClick: () => this.count++
      }, '+1'),

      h('button', {
        onClick: () => this.count--
      }, '-1')
    ])
  }
}
</script>

写法3

<script>
import { ref, h } from 'vue'

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

    // return 可以返回一个render函数
    // 这里使用ref对象的时候,并不会自动解包
    return () => {
      return h('div', {}, [
        h('p', {}, `count:${count.value}`),

        h('button', {
          onClick: () => count.value++
        }, '+1'),

        h('button', {
          onClick: () => count.value--
        }, '-1')
      ])
    }
  }
}
</script>

插槽的使用

父组件

<script>
import { h } from 'vue'
import Cpn from './components/Cpn.vue'

export default {
  setup() {
    return () => h(Cpn, {}, {
      default(props) {
        return h('h2', {}, props)
      }
    })
  }
}
</script>

子组件

<script>
import { h } from 'vue'

export default {
  setup(props, { slots }) {
    return () => h('div', {}, slots.default?.('something in Cpn') || h('h2', {}, 'default value'))
  }
}
</script>

父组件

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

export default {
  setup() {
    return () => {
      // 如果是组件,第一个传递组件对象即可
      return h(Home, null, {
        default(props) {
          return `my name is ${props}`
        }
    	})
    }
  }
}
</script>

子组件

<script>
export default {
  //  不使用的变量,可以使用下划线做占位符
  setup(_, { slots }) {
    const name = 'Klaus'

    return () => {
      return slots.default ? slots.default(name) : 'default value'
    }
  }
}
</script>

JSX

render函数使用h函数的可读性是非常差的,所以在实际编写的时候,我们使用的一般是JSX代码,

然后通过babel来将我们的jsx代码转换为h函数的形式

计数器

<!-- 
	如果是vite环境 需要安装@vitejs/plugin-vue-jsx并进行对应的配置
	并为script添加属性 lang="jsx"或lang="tsx"
-->

<script>
import { ref } from 'vue'

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

    return () => {
      return (
        <>
	        {/*  JSX不会自动包裹fragment,所以需要手动添加  */}
          <h2>count: {count.value}</h2>
          <button onClick={ () => count.value++ }>+1</button>
          <button onClick={ () => count.value-- }>-1</button>
        </>
      )
    }
  }
}
</script>

使用组件

<script>
import Cpn from './components/Cpn.vue'

export default {
  setup() {
    return () => {
      return (
        <>
         <Cpn />
        </>
      )
    }
  }
}
</script>

传递props

传递者

<script>
import Cpn from './components/Cpn.vue'

export default {
  setup() {
    return () => {
      return (
        <>
         <Cpn msg="msg" />
        </>
      )
    }
  }
}
</script>

调用者

<script>
export default {
  // 传入的props依旧需要在这里进行声明,以便于区分props和no-props attribute
  props: ['msg'],

  setup(props) {
    console.log(props.msg) // => 'msg'

    return () => {
      return <h2>default value</h2>
    }
  }
}
</script>

使用插槽

插槽使用者

<script>
import Cpn from './components/Cpn.vue'

export default {
  setup() {
    return () => {
      return (
        <>
         <Cpn>
        	{/* 插槽以对象的形式进行传递 */}
          {
            {
              default(props) {
                return <h2>{ props }</h2>
              }
            }
          }
         </Cpn>
        </>
      )
    }
  }
}
</script>

插槽提供者

<script>
export default {
  setup(props, { slots }) {
    return () => {
      return slots.default?.('Hello World') || <h2>default value</h2>
    }
  }
}
</script>

自定义指令

在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等

除了使用这些指令之外,Vue 也允许我们来自定义自己的指令

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

自定义指令分为两种:

  • 自定义局部指令: 组件中通过 directives 选项,只能在当前组件中使用
  • 自定义全局指令: app的 directive 方法,可以在任意组件中被使用

局部指令

<template>
  <div>
    <input type="text" v-focus>
  </div>
</template>

<script>
export default {
  name: 'App',

  // directives是一个对象,可以挂载多个指令
  directives: {
    // key是指令的名称, 不需要添加v-前缀
    // value是一个配置对象,其中的值主要都是一些指令对应的生命周期函数
    focus: {
      mounted(el) {
        el.focus()
      }
    }
  }
}
</script>

全局指令

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.directive('focus', {
  mounted(el) {
    el.focus()
  }
}) 

app.mount('#app')

script setup中的自定义指令

<template>
  <div>
    <input type="text" v-focus>
  </div>
</template>

<script setup>
  // 在vue script setup中 自定义局部指令只需要定义一个变量
  // 该变量使用v开头的小驼峰命名 以区分普通变量即可
  const vFocus = {
    mounted(el) {
      el.focus()
    }
  }
</script>

生命周期

函数说明
created在绑定元素的 attribute 或事件监听器被应用之前调用
元素创建完毕,属性和事件没有绑定上之前
一般用于进行初始化操作
beforeMount当指令第一次绑定到元素并且在挂载父组件之前调用
mounted在绑定元素的父组件被挂载后调用
beforeUpdate在更新包含组件的 VNode 之前调用
updated在包含组件的 VNode 及其子组件的 VNode 更新后调用
beforeUnmount在卸载绑定元素的父组件之前调用
unmounted当指令与元素解除绑定且父组件已卸载时,只调用一次

每一个生命周期钩子都有下面四个参数

名称说明
el绑定的元素
bindings绑定信息,存储了修饰符和参数
vnode当前的vnode
preVnode更新之前的vnode

参数和修饰符

<template>
  <div>
    <!--
      .once --- modifier --- 修饰符
      param --- 参数
     -->
    <input type="text" v-focus.once="'param'">
  </div>
</template>

<script>
export default {
  name: 'App',

  directives: {
    focus: {
      mounted(el, bindings) {
        el.focus()

        console.log(bindings.value) // => param
        console.log(bindings.modifiers) // => { once: true }
      }
    }
  }
}
</script>

案例

格式化时间

定义 --- src/driectives/formatTime.js

import dayjs from 'dayjs'

// 注册指令所需要使用的app由外部传入
export default function(app) { 
  // 指令名使用 format-time 和 formatTime 皆可
  // 推荐使用format-time
  app.directive('format-time', {
    created (el, bindings) {
      // 将formatStr在这里初始化是为了避免多次调用执行的时候
      // 可以都得到正确的初始化
      // 如果在函数外部定义的话,指令在多次调用的时候
      // 初始化值可能会被污染
      bindings.formatStr = 'YYYY-MM-DD HH:mm:ss'
      
      if (bindings.value) {
         bindings.formatStr = bindings.value
      }
    },

    mounted (el) {
      // textContent的类型是string
      const timeStamp = Number(el.textContent)

      el.textContent = timeStamp.length === 10
        ? dayjs.unix(timeStamp).format( bindings.formatStr) 
      	: dayjs(timeStamp).format( bindings.formatStr)
    }
  })
}

统一暴露 --- src/driectives/index.js

import registerFormatTime from './formatTime'

// 统一暴露出一个函数,目的是为了获取注册所必须的app对象
export default function(app) {
  registerFormatTime(app)
}

全局注册 --- main.js

import { createApp } from 'vue'
import App from './App.vue'

import registerDriectives from './driectives'

const app = createApp(App)

// 全局注册指令
registerDriectives(app)

app.mount('#app')

使用

<!--
	指令中的value默认会被解析为变量,
  所以如果需要传递字符串,需要手动添加引号
-->
<p v-format-time="'YYYY/MM/DD'">1629561162</p>

Teleport

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

  • 那么组件A中template的元素,会被挂载到组件B中template的某个位置
  • 最终我们的应用程序会形成一颗DOM树结构

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

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

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

Teleport是什么

  • 它是一个Vue提供的内置组件,类似于react的Portals
  • teleport翻译过来是心灵传输、远距离运输的意思
<template>
  <div>
    <!-- 
			这里的元素将不在被挂载到div#app上,而是会被挂载到div#foo上 
			使用to重新定义挂载点
		-->
    <teleport to="#foo">
      <h2>Hello Teleport</h2>
    </teleport>
  </div>
</template>
<template>
  <div>
    <teleport to="#foo">
      <h2>Hello Teleport</h2>
    </teleport>

    <!--
      多个teleport同时存在的时候
      并不会产生覆盖,而是会进行合并
    -->
    <teleport to="#foo">
      <Cpn />
    </teleport>
  </div>
</template>

插件

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

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

插件可以完成的功能没有限制,

  • 可以是添加全局方法或者 property
  • 可以是 添加全局资源,如指令/过滤器/过渡等
  • 通过全局 mixin 来添加一些组件选项
  • 将某一个库中的API挂载到全局上进行使用

定义 --- plugins/addGlobalVariable.js

对象方式

export default {
  // 在调用函数的时候,会将app对象作为参数传入
  install(app) {
    // 全局的方法或变量一般以$开头,以区分全局变量或局部变量
    app.config.globalProperties.$name = 'Klaus'
  }
}

函数方式

export default function(app) {
  app.config.globalProperties.$name = 'Klaus'
}

注册插件

import { createApp } from 'vue'
import App from './App.vue'

import addGlobalVariable from './plugins/addGlobalVariable'

const app = createApp(App)

// 注册插件
app.use(addGlobalVariable)

app.mount('#app')

使用 --- vue2

mounted() {
  console.log(this.$name)
}

使用 --- vue3

import { getCurrentInstance } from 'vue'

setup() {
  //  getCurrentInstance().appContext ==> 获取到的就是app实例对象
  const name = getCurrentInstance().appContext.config.globalProperties.$name
  console.log(name)
}

案例

格式化时间

实现方式一 --- 挂载到全局属性

实现

import dayjs from 'dayjs'

export default {
  install(app) {
    app.config.globalProperties.$formatTime =
    (timeStamp = Date.now() , formatStr = 'YYYY-MM-DD HH:mm:ss') =>
      timeStamp.toString().length === 10 ?
        dayjs.unix(timeStamp).format(formatStr)
        : dayjs(timeStamp).format(formatStr)
  }
}

挂载

import { createApp } from 'vue'
import App from './App.vue'

import formatTime from './plugins/formatTime'

const app = createApp(App)

app.use(formatTime)

app.mount('#app')

使用

import { getCurrentInstance } from 'vue'

setup() {
  const { $formatTime } = getCurrentInstance().appContext.config.globalProperties
  console.log($formatTime(1629564731))
}

实现方式二 -- 使用provide全局传递

实现

import dayjs from 'dayjs'

export default {
  install(app) {
    app.provide('$formatTime', (timeStamp = Date.now() , formatStr = 'YYYY-MM-DD HH:mm:ss') =>
    timeStamp.toString().length === 10 ?
      dayjs.unix(timeStamp).format(formatStr)
      : dayjs(timeStamp).format(formatStr))
  }
}

挂载

import { createApp } from 'vue'
import App from './App.vue'

import formatTime from './plugins/formatTime'

const app = createApp(App)

app.use(formatTime)

app.mount('#app')

使用

import { inject } from 'vue'

export default {
  name: 'App',

  setup() {
    const $formatTime = inject('$formatTime')
    console.log($formatTime(1629564731))
  }
}

实现方式三 -- 使用全局mixin

定义

import dayjs from 'dayjs'

export default {
  install(app) {
    app.mixin({
      methods: {
        formatTime(timeStamp = Date.now() , formatStr = 'YYYY-MM-DD HH:mm:ss') {
          return timeStamp.toString().length === 10 ?
                dayjs.unix(timeStamp).format(formatStr)
                : dayjs(timeStamp).format(formatStr)
        }
      }
    })
  }
}

使用

<template>
  <div>

  </div>
</template>

<script>
import { getCurrentInstance, onMounted } from 'vue'
export default {
  name: 'App',

  setup() {
  // 只有在mounted方法中,定义在data和methods中的属性才会被挂载
   onMounted(() => {
      const instance = getCurrentInstance()
      // 在组件实例的ctx上有一个方法可以获取到methods中定义的方法
      console.log(instance.ctx.formatTime(1629564731))
      // 也可以在data中获取在data中定义的数据
      // console.log(instance.data.name)
   })
  }
}
</script>