美文网首页
Composition API(三)

Composition API(三)

作者: Imkata | 来源:发表于2022-05-09 16:10 被阅读0次

认识h函数

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

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

那么我们应该怎么来做呢?使用 h() 函数:
h() 函数是一个用于创建 vnode 的一个函数,其实更准确的命名是createVNode() 函数,但是为了简便在Vue中将之简化为 h() 函数。

可能你会懵逼了,render()函数、h()函数,createVNode()函数什么关系?
render()函数是我们在组件中编写的一个选项,它要求我们返回一个VNode对象,我们利用h()函数来返回一个VNode对象,而h()函数更准确的命名是createVNode()函数。

h()函数如何使用呢?

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

注意:如果没有props,最好将null作为第二个参数传入,将children作为第三个参数传入,以防产生歧义。

h函数的基本使用

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

  • render函数选项中;
  • setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);
<script>
  // 先导入h函数
  import { h } from 'vue';

  export default {
    //使用render函数后,上面的template就可以删掉了
    render() {
      // 参数一:h2标签  参数二:带有title的class  参数三:内容是Hello Render
      return h("h2", {class: "title"}, "Hello Render")
    }
  }
</script>

<style scoped>
</style>

h函数计数器案例

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

  export default {
    data() {
      return {
        counter: 0
      }
    },
    render() {
      return h("div", {class: "app"}, [
        h("h2", null, `当前计数: ${this.counter}`),
        h("button", {
          onClick: () => this.counter++
        }, "+1"),
        h("button", {
          onClick: () => this.counter--
        }, "-1"),
      ])
    }
  }
</script>

<style scoped>
</style>

如果使用setup,代码如下:

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

  export default {
    setup() {
      // setup函数可以代替data选项
      const counter = ref(0);
      
      // setup函数还可以代替render函数
      return () => {
        return h("div", {class: "app"}, [
          h("h2", null, `当前计数: ${counter.value}`),
          h("button", {
            onClick: () => counter.value++
          }, "+1"),
          h("button", {
            onClick: () => counter.value--
          }, "-1"),
        ])
      }
    }
  }
</script>

<style scoped>
</style>

h函数中组件和插槽的使用

前面我们使用h函数第一个参数传的是元素标签的名字,其实还可以传组件。

App.vue组件代码:

<script>
  import { h } from 'vue';
  import HelloWorld from './HelloWorld.vue';

  export default {
    render() {
      return h("div", null, [
        h(HelloWorld, null, {
          default: props => h("span", null, `app传入到HelloWorld中的内容: ${props.name}`)
        })
      ])
    }
  }
</script>

<style scoped>
</style>

HelloWorld.vue组件代码:

<script>
  import { h } from "vue";

  export default {
    render() {
      // HelloWorld组件也是通过render函数创建出来的
      return h("div", null, [
        h("h2", null, "Hello World"),
        this.$slots.default ? this.$slots.default({name: "coderwhy"}): h("span", null, "我是HelloWorld的插槽默认值")
      ])
    }
  }
</script>

<style lang="scss" scoped>
</style>

jsx的babel配置

上面的代码显然难以阅读,如果我们又想充分利用JavaScript的编程能力又不想写上面那些代码,我们可以使用jsx,jsx和React很像。

如果我们希望在项目中使用jsx,那么我们需要添加对jsx的支持。jsx我们通常会通过Babel来进行转换(React编写的jsx就是通过babel转换的),对于Vue来说,我们只需要在Babel中配置对应的插件即可。

安装Babel支持Vue的jsx插件:

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

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

编写jsx代码:

jsx计数器案例

<script>
  import HelloWorld from './HelloWorld.vue';

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

    render() {
      const increment = () => this.counter++;
      const decrement = () => this.counter--;

      return (
        <div>
          <h2>当前计数: {this.counter}</h2>
          <button onClick={increment}>+1</button>
          <button onClick={decrement}>-1</button>
          // 还可以使用组件
          <HelloWorld>
          </HelloWorld>
        </div>
      )
    }
  } 
</script>

<style lang="scss" scoped>
</style>

jsx组件的使用

认识自定义指令

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

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

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

比如我们来做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点。
方式一:如果我们使用默认的实现方式;
方式二:自定义一个 v-focus 的局部指令;
方式三:自定义一个 v-focus 的全局指令;

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

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

自定义一个 v-focus 的局部指令,这个自定义指令实现非常简单,我们只需要在组件选项中使用 directives 即可,它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加v-),自定义指令有一个生命周期,是在组件挂载后调用 mounted,我们可以在其中完成操作。

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

<script>
  export default {
    // 局部指令
    directives: {
      focus: {
        // 参数一:当前el元素
        // 参数二:绑定的一个对象,传入的参数和修饰符就在这里面
        // 参数三:虚拟节点
        // 参数四:上一个虚拟节点
        mounted(el, bindings, vnode, preVnode) {
          //console.log("focus mounted");
          el.focus();
        }
      }
    }
  }
</script>

<style scoped>
</style>

方式三:自定义全局指令

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

import { createApp } from 'vue'

const app = createApp(App);

app.directive("focus", {
  mounted(el, bindings, vnode, preVnode) {
    //console.log("focus mounted");
    el.focus();
  }
})

app.mount('#app');

指令的生命周期、参数、修饰符

一个指令定义的对象,Vue提供了如下几个钩子函数,在每个钩子函数中都有四个参数:el, bindings, vnode, preVnode 详情如下:

created:在绑定元素的 attribute 或事件监听器被应用之前调用;
beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
mounted:在绑定元素的父组件被挂载后调用;
beforeUpdate:在更新包含组件的 VNode 之前调用;
updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
beforeUnmount:在卸载绑定元素的父组件之前调用;
unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;

<template>
  <div>
    <button v-if="counter < 2" v-why.aaaa.bbbb="'coderwhy'" @click="increment">当前计数: {{counter}}</button>
  </div>
</template>

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

  export default {
    // 指令生命周期
    directives: {
      why: {
        // 参数一:当前el元素
        // 参数二:绑定的一个对象,传入的参数和修饰符就在这里面
        // 参数三:虚拟节点
        // 参数四:上一个虚拟节点
        created(el, bindings, vnode, preVnode) {
          console.log("why created", el, bindings, vnode, preVnode);
          //获取传递的参数,也就是'coderwhy'
          console.log(bindings.value);
          //获取修饰符,modifiers是个对象 {aaa: true, bbb: true}
          console.log(bindings.modifiers);
        },
        beforeMount() {
          console.log("why beforeMount");
        },
        mounted() {
          console.log("why mounted");
        },
        beforeUpdate() {
          console.log("why beforeUpdate");
        },
        updated() {
          console.log("why updated");
        },
        beforeUnmount() {
          console.log("why beforeUnmount");
        },
        unmounted() {
          console.log("why unmounted");
        }
      }
    },
    setup() {
      const counter = ref(0);
      const increment = () => counter.value++;

      return {
        counter,
        increment
      }
    }
  }
</script>

<style scoped>
</style>

自定义指令练习:时间戳的显示需求

在开发中,大多数情况下从服务器获取到的都是时间戳,我们需要将时间戳转换成具体格式化的时间来展示。在Vue2中我们可以通过过滤器来完成,Vue3中删除了过滤器,在Vue3中我们可以通过计算属性(computed)或者自定义一个方法(methods)来完成,其实我们还可以通过一个自定义的指令来完成。

我们来实现一个可以自动对时间格式化的指令v-format-time,这里我封装了一个函数,在首页中我们只需要调用这个函数并且传入app即可。

我们创建directives文件夹,format-time.js是每个指令的具体代码,index.js是注册每个指令,这样我们在main.js中直接导入,然后传入app即可,文件目录如下:

format-time.js代码如下:

// 需要通过npm install dayjs
import dayjs from 'dayjs';

export default function(app) {
  app.directive("format-time", {
    // 1. 先设置时间格式参数
    created(el, bindings) {
      // 设置默认的时间格式
      // 这里我们把值设置到bindings上,目的就是为了在bindings里面能取到
      // 如果我们不设置到bindings上就没法传给mounted了
      bindings.formatString = "YYYY-MM-DD HH:mm:ss";
      //如果自定义指令传递了时间格式参数,就使用传递的参数
      if (bindings.value) {
        bindings.formatString = bindings.value;
      }
    },
    // 2. 再转换
    mounted(el, bindings) {
      //获取显示的文字
      const textContent = el.textContent;
      //转成int
      let timestamp = parseInt(textContent);
      //如果10位就是秒,转成毫秒
      if (textContent.length === 10) {
        timestamp = timestamp * 1000
      }
      //format后面的参数使用传递的参数
      el.textContent = dayjs(timestamp).format(bindings.formatString);
    }
  })
}

index.js文件代码如下:

import registerFormatTime from './format-time';

export default function registerDirectives(app) {
  registerFormatTime(app);
}

main.js文件代码如下:

import { createApp } from 'vue'
import App from './03_自定义指令/App.vue'
import registerDirectives from './directives'

const app = createApp(App);

registerDirectives(app);

app.mount('#app');

这样我们就将main.js里面注册全局组件的代码抽出来了,下次如果需要注册新的全局组件,只需在index.js里面添加一行代码即可。

认识Teleport

在组件化开发中,我们封装一个组件A,在另外一个组件B中使用,那么组件A中template的元素,会被挂载到组件B中template的某个位置,最终我们的应用程序会形成一颗DOM树结构。但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置,比如移动到body元素上,或者我们有其他的div#app之外的元素上,这个时候我们就可以通过teleport来完成。

Teleport是什么呢?
它是一个Vue提供的内置组件,类似于react的Portals,teleport翻译过来是心灵传输、远距离运输的意思,它有两个属性:

  • to:指定将其中的内容移动到的目标元素,可以使用选择器;
  • disabled:是否禁用 teleport 的功能;

我们来看下面代码的效果:

和组件结合使用

当然,teleport也可以和组件结合一起来使用,我们可以在 teleport 中使用组件,并且也可以给他传入一些数据。

多个teleport

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

实现效果如下:

认识Vue插件

通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
① 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;
② 函数类型:一个function,这个函数会在安装插件时自动执行;

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

  • 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现;
  • 添加全局资源:指令/过滤器/过渡等;
  • 通过全局 mixin 来添加一些组件选项;
  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能;

插件的编写方式

对象类型的写法,plugins_object.js文件代码:

export default {
  install(app) {
    // 全局配置添加一个name
    app.config.globalProperties.$name = "coderwhy"
  }
}

函数类型的写法,plugins_function.js文件代码:

export default function(app) {
  console.log(app);
}

安装插件:

import { createApp } from 'vue'
import App from './03_自定义指令/App.vue'

const app = createApp(App);

//安装插件,其实内部就是调用install方法
app.use(pluginObject);
app.use(pluginFunction);

app.mount('#app');

使用插件:

如果在Vue2中,直接使用this.$xxx就可以拿到全局配置的属性了:

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

如果在Vue3中,因为在setup中我们不能使用this,所以我们可以通过如下方式获取this.$xxx:

import { getCurrentInstance } from "vue";

setup() {
  const instance = getCurrentInstance();
  console.log(instance.appContext.config.globalProperties.$name);
},

相关文章

网友评论

      本文标题:Composition API(三)

      本文链接:https://www.haomeiwen.com/subject/qwyhurtx.html