Vue3官网-可复用&组合式API(十四)实例 property(\$slots,\$attrs)、渲染函数render(虚拟节点VNode,h() 参数,使用JavaScript代替模板功能),插件

  • Post author:
  • Post category:java

Vue3官网-可复用&组合式API(十四)实例 property($slots,$attrs)、渲染函数render(虚拟节点VNode,h() 参数,使用JavaScript代替模板功能),插件(编写,创建插件)

总结:

  • 补充

    • Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。

    • truthy(真值):在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 假值(即除 false0""nullundefinedNaN 以外皆为真值)。括号内都是假值falsy。

    • .prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault()

    • ==Event.preventDefault方法取消浏览器对当前事件的默认行为。==比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;再比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了。该方法生效的前提是,事件对象的cancelable属性为true,如果为false,调用该方法没有任何效果。

    • vue:用组件(app.component)构建一个模板(template),并反复使用模板

    • 父组件、子组件

      • const app = Vue.createApp({
          components: {
            'component-a': ComponentA,
            'component-b': ComponentB
          }
        })
        
      • 上面代码中app为父组件,ComponentA和ComponentB为子组件

      • context.emit

        • 父组件通过 :data="data" 传递数据
        • 父组件通过 @fun="fun" 传递方法
        • 子组件通过 props 接收父组件传递的值
        • 子组件通过 $emit 调用父组件的方法并传递数据
    • Vue中美元$符号的意思

      • 除了数据属性,Vue 实例还暴露了一些有用的实例属性与方法。它们都有前缀 $,以便与用户定义的属性区分开来。
      • vue中$refs的作用?
        • 当我们需要获取一个dom元素进行操作的时候 可以利用原生js的getElementByXxx 而在vue中可以设置refs来获取这个dom元素
    • 第三方网站

      • greensock API(GSAP):是一套用于所有主流浏览器中制作高性能html5动画的工具。他们有一个很棒的 ease visualizer,帮助你建立精心制作的画架。
        • GSAP的全名是==GreenSock Animation Platform,这个名字是有些怪异(官网还在各种安利你加入它的Club),但它的确是一个从flash时代一直发展到今天的专业动画库。==参照CSDN:https://blog.csdn.net/weixin_39939012/article/details/80833073
      • animate.css :集成第三方 CSS 动画库
      • CSS Triggers :来查看哪些属性会在动画时触发重绘。这个网站是用来告诉开发者不同内核浏览器对css属性修改的重绘/回流情况,开发者知道了这些细节可以提高页面性能。
    • JavaScript钩子

      • 添加 :css="false",也会让 Vue 会跳过 CSS 的检测,除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响。
    • 解构(Destructuring)

      • ES6允许按照一定模式从数组和对象中提取值,然后对变量进行复制,这被称为解构(Destructuring)
    • API参考

      • https://v3.cn.vuejs.org/api/refs-api.html#ref
    • 自定义指令

      • 注意,在 Vue 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
    • 渲染函数

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

    • $el
      • 组件实例正在使用的根 DOM 元素。
    • $parent
      • 父实例,如果当前实例有的话。
    • $slots
      • 用来以编程方式访问通过插槽分发的内容。每个具名插槽都有其相应的 property (例如:v-slot:foo 中的内容将会在 this.$slots.foo() 中被找到)。default property 包括了所有没有被包含在具名插槽中的节点,或 v-slot:default 的内容。
    • $refs
      • 一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。
    • $attrs
      • 包含了父作用域中不作为组件 props自定义事件的 attribute 绑定和事件。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs" 传入内部组件——这在创建高阶的组件时会非常有用。
  • 渲染函数(render)

    • 概述

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

      • app.component('anchored-heading', {
          render() {
            return h(
              'h' + this.level, // 标签名
              {}, // prop 或 attribute
              this.$slots.default() // 包含其子节点的数组
            )
          },
          props: {
            level: {
              type: Number,
              required: true
            }
          }
        
    • 虚拟节点VNode

    • h() 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

    • h() 参数

      • h() 函数是一个用于创建 VNode 的实用程序。也许可以更准确地将其命名为 createVNode(),但由于频繁使用和简洁,它被称为 h()

      • 三个参数(最重要

        • {String | Object | Function} tag 一个 HTML 标签名、一个组件、一个异步组件、或一个函数式组件。必需的
        • {Object} props与 attribute、prop 和事件相对应的对象。这会在模板中用到。 可选的,通常没有时直接跳过显示第三个参数,也可以用Null表示
        • {String | Array | Object} children子 VNodes, 使用 h() 构建,或使用字符串获取 “文本 VNode” 或者有插槽的对象。可选的,当没有第二个参数时,就显示在第二位
      • 举例如下,h()的第一个参数就是VNode的组件名

         [
            'Some text comes first.',
            h('h1', 'A headline'),
            h(MyComponent, {
              someProp: 'foobar'
            })
          ]
        
    • 创建VNode

      • 传递给 h 的第一个参数应该是组件本身。

      • 解析组件

        • render 函数通常只需要对全局注册的组件使用 resolveComponent

          render() {
            return h(resolveComponent('ButtonCounter'))
          }
          
        • 而对于局部注册的却可以跳过,请看下面的例子:

          render() {
            return h(ButtonCounter)
          }
          
    • 使用JavaScript代替模板功能

      • v-ifv-for

      • 在渲染函数中用 JavaScript 的 if/elsemap() 来重写

      • v-model 指令

        • 扩展为 modelValueonUpdate:modelValue 在模板编译过程中,我们必须自己提供这些 prop
      • v-on

        • 例如,要处理 click 事件,prop 名称应该是 onClick
      • 事件修饰符

        • .passive.capture.once 事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:
        render() {
          return h('input', {
            onClickCapture: this.doThisInCapturingMode,
            onKeyupOnce: this.doThisOnce,
            onMouseoverOnceCapture: this.doThisOnceInCapturingMode
          })
        }
        
        • 修饰符 处理函数中的等价操作
          .stop event.stopPropagation()
          .prevent event.preventDefault()
          .self if (event.target !== event.currentTarget) return
          按键: .enter, .13 if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码
          修饰键: .ctrl, .alt, .shift, .meta if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey, shiftKey, 或 metaKey)
      • 插槽

        • 通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组

        • 要使用渲染函数将插槽传递给子组件:

          const { h, resolveComponent } = Vue
          
          render() {
            // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
            return h('div', [
              h(
                resolveComponent('child'),
                {},
                // 将 `slots` 以 { name: props => VNode | Array<VNode> } 的形式传递给子对象。
                {
                  default: (props) => Vue.h('span', props.text)
                }
              )
            ])
          }
          
      • is attribute

        • 模板使用 resolveDynamicComponent 来实现 is attribute。
      • 自定义指令

    • React JSX

      • React 使用 JSX 来替代常规的 JavaScript。

        JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。

        我们不需要一定使用 JSX,但它有以下优点:

        • JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化。
        • 它是类型安全的,在编译过程中就能发现错误。
        • 使用 JSX 编写模板更加简单快速。
      • 我们先看下以下代码:

        const element = <h1>Hello, world!</h1>;
        

        这种看起来可能有些奇怪的标签语法既不是字符串也不是 HTML。

        它被称为 JSX, 一种 JavaScript 的语法扩展。 我们推荐在 React 中使用 JSX 来描述用户界面。

        JSX 是在 JavaScript 内部实现的。

        我们知道元素是构成 React 应用的最小单位,JSX 就是用来声明 React 当中的元素。

        与浏览器的 DOM 元素不同,React 当中的元素事实上是普通的对象,React DOM 可以确保 浏览器 DOM 的数据内容与 React 元素保持一致。

    • 函数是组件 FunctionalComponent

      • Vue 会把 props 当作第一个参数传入
      • 第二个参数 context 包含三个 property:attrsemitslots。它们分别相当于实例的 $attrs$emit$slots 这几个 property。

1. 实例 property

$data

  • 类型:Object

  • 详细:

    组件实例正在侦听的数据对象。组件实例代理了对其 data 对象 property 的访问。

  • 参考:选项 / 数据 – data

$props

  • 类型:Object

  • 详细:

    当前组件接收到的 props 对象。组件实例代理了对其 props 对象 property 的访问。

$el

  • 类型:any

  • 仅可读

  • 详细:

    组件实例正在使用的根 DOM 元素。

    对于使用了片段的组件,$el 是占位 DOM 节点,Vue 使用它来跟踪组件在 DOM 中的位置。建议使用模板引用来直接访问 DOM 元素,而不是依赖于 $el

$options

  • 类型:Object

  • 仅可读

  • 详细:

    用于当前组件实例的初始化选项。当你需要在选项中包含自定义 property 时会有用处:

    const app = createApp({
      customOption: 'foo',
      created() {
        console.log(this.$options.customOption) // => 'foo'
      }
    })
    

$parent

  • 类型:Vue instance

  • 仅可读

  • 详细:

    父实例,如果当前实例有的话。

$root

  • 类型:Vue instance

  • 仅可读

  • 详细:

    当前组件树的根组件实例。如果当前实例没有父实例,此实例将会是其自己。

$slots

  • 类型:{ [name: string]: (...args: any[]) => Array<VNode> | undefined }

  • 仅可读

  • 详细:

    用来以编程方式访问通过插槽分发的内容。每个具名插槽都有其相应的 property (例如:v-slot:foo 中的内容将会在 this.$slots.foo() 中被找到)。default property 包括了所有没有被包含在具名插槽中的节点,或 v-slot:default 的内容。

    在使用渲染函数编写一个组件时,访问 this.$slots 会很有帮助。

  • 示例:

    <blog-post>
      <template v-slot:header>
        <h1>About Me</h1>
      </template>
    
      <template v-slot:default>
        <p>
          Here's some page content, which will be included in $slots.default.
        </p>
      </template>
    
      <template v-slot:footer>
        <p>Copyright 2020 Evan You</p>
      </template>
    </blog-post>
    
    const app = createApp({})
    
    app.component('blog-post', {
      render() {
        return h('div', [
          h('header', this.$slots.header()),
          h('main', this.$slots.default()),
          h('footer', this.$slots.footer())
        ])
      }
    })
    
  • 参考:

$refs

  • 类型:Object
  • 仅可读
  • 详细:

一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。

$attrs

  • 类型:Object
  • 仅可读
  • 详细:

包含了父作用域中不作为组件 props自定义事件的 attribute 绑定和事件。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs" 传入内部组件——这在创建高阶的组件时会非常有用。

2. 渲染函数

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

让我们深入一个简单的例子,这个例子里 render 函数很实用。假设我们要生成一些带锚点的标题:

<h1>
  <a name="hello-world" href="#hello-world">
    Hello world!
  </a>
</h1>

锚点标题的使用非常频繁,我们应该创建一个组件:

<anchored-heading :level="1">Hello world!</anchored-heading>

当开始写一个只能通过 level prop 动态生成标题 (heading) 的组件时,我们很快就可以得出这样的结论:

const { createApp } = Vue

const app = createApp({})

app.component('anchored-heading', {
  template: `
    <h1 v-if="level === 1">
      <slot></slot>
    </h1>
    <h2 v-else-if="level === 2">
      <slot></slot>
    </h2>
    <h3 v-else-if="level === 3">
      <slot></slot>
    </h3>
    <h4 v-else-if="level === 4">
      <slot></slot>
    </h4>
    <h5 v-else-if="level === 5">
      <slot></slot>
    </h5>
    <h6 v-else-if="level === 6">
      <slot></slot>
    </h6>
  `,
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

这个模板感觉不太好。它不仅冗长,而且我们为每个级别标题重复书写了 <slot></slot>。当我们添加锚元素时,我们必须在每个 v-if/v-else-if 分支中再次重复它。

虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render 函数重写上面的例子:

const { createApp, h } = Vue

const app = createApp({})

app.component('anchored-heading', {
  render() {
    return h(
      'h' + this.level, // 标签名
      {}, // prop 或 attribute
      this.$slots.default() // 包含其子节点的数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

render() 函数的实现要精简得多,但是需要非常熟悉组件的实例 property。在这个例子中,你需要知道,向组件中传递不带 v-slot 指令的子节点时,比如 anchored-heading 中的 Hello world! ,这些子节点被存储在组件实例中的 $slots.default 中。如果你还不了解,在深入渲染函数之前推荐阅读实例 property API

DOM 树

在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。以下面这段 HTML 为例:

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>

当浏览器读到这些代码时,它会建立一个 ”DOM 节点“ 树 来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。

上述 HTML 对应的 DOM 节点树如下图所示

DOM Tree Visualization

每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。

高效地更新所有这些节点会是比较困难的,不过所幸你不必手动完成这个工作。你只需要告诉 Vue 你希望页面上的 HTML 是什么,这可以是在一个模板里:

<h1>{{ blogTitle }}</h1>

或者一个渲染函数里:

render() {
  return h('h1', {}, this.blogTitle)
}

在这两种情况下,Vue 都会自动保持页面的更新,即便 blogTitle 发生了改变。

虚拟 DOM 树

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:

return h('h1', {}, this.blogTitle)

h() 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode=。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

h() 参数

h() 函数是一个用于创建 VNode 的实用程序。也许可以更准确地将其命名为 createVNode(),但由于频繁使用和简洁,它被称为 h() 。它接受三个参数:

// @returns {VNode}
h(
  // {String | Object | Function} tag
  // 一个 HTML 标签名、一个组件、一个异步组件、或
  // 一个函数式组件。
  //
  // 必需的。
  'div',

  // {Object} props
  // 与 attribute、prop 和事件相对应的对象。
  // 这会在模板中用到。
  //
  // 可选的。
  {},

  // {String | Array | Object} children
  // 子 VNodes, 使用 `h()` 构建,
  // 或使用字符串获取 "文本 VNode" 或者
  // 有插槽的对象。
  //
  // 可选的。
  [
    'Some text comes first.',
    h('h1', 'A headline'),
    h(MyComponent, {
      someProp: 'foobar'
    })
  ]
)

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

完整实例

有了这些知识,我们现在可以完成我们最开始想实现的组件:

const { createApp, h } = Vue

const app = createApp({})

/** 递归地从子节点获取文本 */
function getChildrenTextContent(children) {
  return children
    .map(node => {
      return typeof node.children === 'string'
        ? node.children
        : Array.isArray(node.children)
        ? getChildrenTextContent(node.children)
        : ''
    })
    .join('')
}

app.component('anchored-heading', {
  render() {
    // 从 children 的文本内容中创建短横线分隔 (kebab-case) id。
    const headingId = getChildrenTextContent(this.$slots.default())
      .toLowerCase()
      .replace(/\W+/g, '-') // 用短横线替换非单词字符
      .replace(/(^-|-$)/g, '') // 删除前后短横线

    return h('h' + this.level, [
      h(
        'a',
        {
          name: headingId,
          href: '#' + headingId
        },
        this.$slots.default()
      )
    ])
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

约束

VNodes 必须唯一

组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:

render() {
  const myParagraphVNode = h('p', 'hi')
  return h('div', [
    // 错误 - 重复的 Vnode!
    myParagraphVNode, myParagraphVNode
  ])
}

如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:

render() {
  return h('div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

创建组件 VNode

要为某个组件创建一个 VNode,传递给 h 的第一个参数应该是组件本身

render() {
  return h(ButtonCounter)
}

如果我们需要通过名称来解析一个组件,那么我们可以调用 resolveComponent

const { h, resolveComponent } = Vue

// ...

render() {
  const ButtonCounter = resolveComponent('ButtonCounter')
  return h(ButtonCounter)
}

resolveComponent 是模板内部用来解析组件名称的同一个函数。

render 函数通常只需要对全局注册的组件使用 resolveComponent。而对于局部注册的却可以跳过,请看下面的例子:

// 此写法可以简化
components: {
  ButtonCounter
},
render() {
  return h(resolveComponent('ButtonCounter'))
}

我们可以直接使用它,而不是通过名称注册一个组件,然后再查找:

render() {
  return h(ButtonCounter)
}

使用 JavaScript 代替模板功能

v-ifv-for

只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-ifv-for

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

这些都可以在渲染函数中用 JavaScript 的 if/elsemap() 来重写:

props: ['items'],
render() {
  if (this.items.length) {
    return h('ul', this.items.map((item) => {
      return h('li', item.name)
    }))
  } else {
    return h('p', 'No items found.')
  }
}

v-model

v-model 指令扩展为 modelValueonUpdate:modelValue 在模板编译过程中,我们必须自己提供这些 prop:

props: ['modelValue'],
emits: ['update:modelValue'],
render() {
  return h(SomeComponent, {
    modelValue: this.modelValue,
    'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
  })
}

v-on

我们必须为事件处理程序提供一个正确的 prop 名称,例如,要处理 click 事件,prop 名称应该是 onClick

render() {
  return h('div', {
    onClick: $event => console.log('clicked', $event.target)
  })
}
事件修饰符

对于 .passive.capture.once 事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:(链接:https://blog.csdn.net/qq_43456781/article/details/120347514?spm=1001.2014.3001.5501)

实例:

render() {
  return h('input', {
    onClickCapture: this.doThisInCapturingMode,
    onKeyupOnce: this.doThisOnce,
    onMouseoverOnceCapture: this.doThisOnceInCapturingMode
  })
}

对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:

修饰符 处理函数中的等价操作
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
按键: .enter, .13 if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码
修饰键: .ctrl, .alt, .shift, .meta if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey, shiftKey, 或 metaKey)

这里是一个使用所有修饰符的例子:

render() {
  return h('input', {
    onKeyUp: event => {
      // 如果触发事件的元素不是事件绑定的元素
      // 则返回
      if (event.target !== event.currentTarget) return
      // 如果向上键不是回车键,则终止
      // 没有同时按下按键 (13) 和 shift 键
      if (!event.shiftKey || event.keyCode !== 13) return
      // 停止事件传播
      event.stopPropagation()
      // 阻止该元素默认的 keyup 事件
      event.preventDefault()
      // ...
    }
  })
}

插槽

你可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:

render() {
  // `<div><slot></slot></div>`
  return h('div', {}, this.$slots.default())
}
props: ['message'],
render() {
  // `<div><slot :text="message"></slot></div>`
  return h('div', {}, this.$slots.default({
    text: this.message
  }))
}

要使用渲染函数将插槽传递给子组件,请执行以下操作:

const { h, resolveComponent } = Vue

render() {
  // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
  	//每个插槽都是一个 VNode 数组,所以第二项是一个数组
    return h('div', [
    h(
      resolveComponent('child'),
      {},
      // 将 `slots` 以 { name: props => VNode | Array<VNode> } 的形式传递给子对象。
      {
        default: (props) => Vue.h('span', props.text)
      }
    )
  ])
}

插槽以函数的形式传递,允许子组件控制每个插槽内容的创建。任何响应式数据都应该在插槽函数内访问,以确保它被注册为子组件的依赖关系,而不是父组件。相反,对 resolveComponent 的调用应该在插槽函数之外进行,否则它们会相对于错误的组件进行解析。

// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>`
render() {
  // 应该是在插槽函数外面调用 resolveComponent。
  const Button = resolveComponent('MyButton')
  const Icon = resolveComponent('MyIcon')

  return h(
    Button,
    null,
    {
      // 使用箭头函数保存 `this` 的值
      default: (props) => {
        // 响应式 property 应该在插槽函数内部读取,
        // 这样它们就会成为 children 渲染的依赖。
        return [
          h(Icon, { name: this.icon }),
          this.text
        ]
      }
    }
  )
}

如果一个组件从它的父组件中接收到插槽,它们可以直接传递给子组件。

render() {
  return h(Panel, null, this.$slots)
}

也可以根据情况单独传递或包裹住。

render() {
  return h(
    Panel,
    null,
    {
      // 如果我们想传递一个槽函数,我们可以通过
      header: this.$slots.header,

      // 如果我们需要以某种方式对插槽进行操作,
      // 那么我们需要用一个新的函数来包裹它
      default: (props) => {
        const children = this.$slots.default ? this.$slots.default(props) : []

        return children.concat(h('div', 'Extra child'))
      }
    }
  )
}

<component>is

参考:动态组件

在底层实现里,模板使用 resolveDynamicComponent 来实现 is attribute。如果我们在 render 函数中需要 is 提供的所有灵活性,我们可以使用同样的函数:

const { h, resolveDynamicComponent } = Vue

// ...

// `<component :is="name"></component>`
render() {
  const Component = resolveDynamicComponent(this.name)
  return h(Component)
}

就像 is, resolveDynamicComponent 支持传递一个组件名称、一个 HTML 元素名称或一个组件选项对象。

通常这种程度的灵活性是不需要的。通常 resolveDynamicComponent 可以被换做一个更直接的替代方案。

例如,如果我们只需要支持组件名称,那么可以使用 resolveComponent 来代替。

如果 VNode 始终是一个 HTML 元素,那么我们可以直接把它的名字传递给 h

// `<component :is="bold ? 'strong' : 'em'"></component>`
render() {
  return h(this.bold ? 'strong' : 'em')
}

同样,如果传递给 is 的值是一个组件选项对象,那么不需要解析什么,可以直接作为 h 的第一个参数传递。

<template> 标签一样,<component> 标签仅在模板中作为语法占位符需要,当迁移到 render 函数时,应被丢弃。

自定义指令

可以使用 withDirectives 将自定义指令应用于 VNode:

const { h, resolveDirective, withDirectives } = Vue

// ...

// <div v-pin:top.animate="200"></div>
render () {
  const pin = resolveDirective('pin')

  return withDirectives(h('div'), [
    [pin, 200, 'top', { animate: true }]
  ])
}

resolveDirective 是模板内部用来解析指令名称的同一个函数。只有当你还没有直接访问指令的定义对象时,才需要这样做。

内置组件

诸如 <keep-alive><transition><transition-group><teleport>内置组件默认并没有被全局注册。这使得打包工具可以 tree-shake,因此这些组件只会在被用到的时候被引入构建。不过这也意味着我们无法通过 resolveComponentresolveDynamicComponent 访问它们。

在模板中这些组件会被特殊处理,即在它们被用到的时候自动导入。当我们编写自己的 render 函数时,需要自行导入它们:

const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue
// ...
render () {
  return h(Transition, { mode: 'out-in' }, /* ... */)
}

渲染函数的返回值

在我们目前看过的所有示例中,render 函数返回的是单个根 VNode。但其实也有别的选项

返回一个字符串时会创建一个文本 VNode,而不被包裹任何元素:

render() {
  return 'Hello world!'
}

我们也可以返回一个子元素数组,而不把它们包裹在一个根结点里。这会创建一个片段 (fragment):

// 相当于模板 `Hello<br>world!`
render() {
  return [
    'Hello',
    h('br'),
    'world!'
  ]
}

可能是因为数据依然在加载中的关系,组件不需要渲染,这时它可以返回 null。这样我们在 DOM 中会渲染一个注释节点。

JSX

如果你写了很多渲染函数,可能会觉得下面这样的代码写起来很痛苦:

h(
  'anchored-heading',
  {
    level: 1
  },
  {
    default: () => [h('span', 'Hello'), ' world!']
  }
)

特别是对应的模板如此简单的情况下:

<anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>

这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。

import AnchoredHeading from './AnchoredHeading.vue'

const app = createApp({
  render() {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

app.mount('#demo')

有关 JSX 如何映射到 JavaScript 的更多信息,请参阅使用文档

函数式组件

函数式组件是自身没有任何状态的组件的另一种形式。它们在渲染过程中不会创建组件实例,并跳过常规的组件生命周期。

我们使用的是一个简单函数,而不是一个选项对象,来创建函数式组件。该函数实际上就是该组件的 render 函数。而因为函数式组件里没有 this 引用,Vue 会把 props 当作第一个参数传入:

const FunctionalComponent = (props, context) => {
  // ...
}

第二个参数 context 包含三个 property:attrsemitslots。它们分别相当于实例的 $attrs$emit$slots 这几个 property。

大多数常规组件的配置选项在函数式组件中都不可用。然而我们还是可以把 propsemits 作为 property 加入,以达到定义它们的目的:

FunctionalComponent.props = ['value']
FunctionalComponent.emits = ['click']

如果这个 props 选项没有被定义,那么被传入函数的 props 对象就会像 attrs 一样会包含所有 attribute。而如果 props 选项没有被定制,每个 prop 的名字都会基于驼峰命名法被一般化处理。

函数式组件可以像普通组件一样被注册和消费。如果你将一个函数作为第一个参数传入 h,它将会被当作一个函数式组件来对待。

模板编译

你可能会有兴趣知道,Vue 的模板实际上被编译成了渲染函数。这是一个实现细节,通常不需要关心。但如果你想看看模板的功能具体是怎样被编译的,可能会发现会非常有意思。下面是一个使用 Vue.compile 来实时编译模板字符串的简单示例:

在这里插入图片描述

3. 插件

插件是自包含的代码,通常向 Vue 添加全局级功能。它可以是公开 install() 方法的 object,也可以是 function

插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者 property。如:vue-custom-element
  2. 添加全局资源:指令/过渡等。如:vue-touch
  3. 通过全局 mixin 来添加一些组件选项。(如vue-router)
  4. 添加全局实例方法,通过把它们添加到 config.globalProperties 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

编写插件

为了更好地理解如何创建自己的 Vue.js 版插件,我们将创建一个非常简化的插件版本,它显示 i18n 准备好的字符串。

每当这个插件被添加到应用程序中时,如果它是一个对象,就会调用 install 方法。如果它是一个 function,则函数本身将被调用。在这两种情况下——它都会收到两个参数:由 Vue 的 createApp 生成的 app 对象和用户传入的选项。

让我们从设置插件对象开始。建议在单独的文件中创建它并将其导出,如下所示,以保持包含的逻辑和分离的逻辑。

// plugins/i18n.js
export default {
  install: (app, options) => {
    // Plugin code goes here
  }
}

我们想要一个函数来翻译整个应用程序可用的键,因此我们将使用 app.config.globalProperties 暴露它。

该函数将接收一个 key 字符串,我们将使用它在用户提供的选项中查找转换后的字符串。

// plugins/i18n.js
export default {
  install: (app, options) => {
    app.config.globalProperties.$translate = key => {
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
  }
}

我们假设用户使用插件时,将在 options 参数中传递一个包含翻译后的键的对象。我们的 $translate 函数将使用诸如 greetings.hello 之类的字符串,查看用户提供的配置内部并返回转换后的值-在这种情况下为 Bonjour!

例如:

greetings: {
  hello: 'Bonjour!'
}

插件还允许我们使用 inject 为插件的用户提供功能或 attribute。例如,我们可以允许应用程序访问 options 参数以能够使用翻译对象。

// plugins/i18n.js
export default {
  install: (app, options) => {
    app.config.globalProperties.$translate = key => {
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }

    app.provide('i18n', options)
  }
}

插件用户现在可以将 inject[i18n] 注入到他们的组件并访问该对象。

另外,由于我们可以访问 app 对象,因此插件可以使用所有其他功能,例如使用 mixindirective。要了解有关 createApp 和应用程序实例的更多信息,请查看 Application API 文档

// plugins/i18n.js
export default {
  install: (app, options) => {
    app.config.globalProperties.$translate = (key) => {
      return key.split('.')
        .reduce((o, i) => { if (o) return o[i] }, options)
    }

    app.provide('i18n', options)

    app.directive('my-directive', {
      mounted (el, binding, vnode, oldVnode) {
        // some logic ...
      }
      ...
    })

    app.mixin({
      created() {
        // some logic ...
      }
      ...
    })
  }
}

使用插件

在使用 createApp() 初始化 Vue 应用程序后,你可以通过调用 use() 方法将插件添加到你的应用程序中。

我们将使用在编写插件部分中创建的 i18nPlugin 进行演示。

use() 方法有两个参数。第一个是要安装的插件,在这种情况下为 i18nPlugin

它还会自动阻止你多次使用同一插件,因此在同一插件上多次调用只会安装一次该插件。

第二个参数是可选的,并且取决于每个特定的插件。在演示 i18nPlugin 的情况下,它是带有转换后的字符串的对象。

INFO

如果你使用的是第三方插件 (例如 VuexVue Router),请始终查看文档以了解特定插件期望作为第二个参数接收的内容。

import { createApp } from 'vue'
import Root from './App.vue'
import i18nPlugin from './plugins/i18n'

const app = createApp(Root)
const i18nStrings = {
  greetings: {
    hi: 'Hallo!'
  }
}

app.use(i18nPlugin, i18nStrings)
app.mount('#app')

awesome-vue 集合了大量由社区贡献的插件和库。


版权声明:本文为qq_43456781原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。