是时候重学一遍Vue3中的 render 渲染函数了 I 附带实例

在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力,这时渲染函数就派上用场了。

一、创建 VNode

VNode 是 Vue 对页面 DOM 节点的描述, 其是一个 Object 类型

Vue 提供了一个 h() 函数用于创建 VNode:

js 复制代码
import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)

h()hyperscript 的简称------意思是"能生成 HTML (超文本标记语言) 的 JavaScript"。一个更准确的名称应该是 createVnode(),但当你需要多次使用渲染函数时,一个简短的名字会更省力。

h() 函数的使用方式非常的灵活:

js 复制代码
// 除了类型必填以外,其他的参数都是可选的
h('div')
h('div', { id: 'foo' })

// attribute 和 property 都能在 prop 中书写
// Vue 会自动将它们分配到正确的位置
h('div', { class: 'bar', innerHTML: 'hello' })

// 像 `.prop` 和 `.attr` 这样的的属性修饰符
// 可以分别通过 `.` 和 `^` 前缀来添加
h('div', { '.name': 'some-name', '^width': '100' })

// 类与样式可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })

// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')

// 没有 props 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])

// children 数组可以同时包含 vnodes 与字符串
h('div', ['hello', h('span', 'hello')])

h() 函数接收三个参数

js 复制代码
// @returns {VNode}
h(
  // {String | Object | Function | null} tag
  // 一个 HTML 标签名、一个组件、一个异步组件,或者 null。
  // 使用 null 将会渲染一个注释。
  //
  // 必需的。
  'div',

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

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

二、声明渲染函数

当组合式 API 与模板一起使用时,setup() 钩子的返回值是用于暴露数据给模板。然而当我们使用渲染函数时,可以直接把渲染函数返回:

js 复制代码
import { ref, h } from 'vue'

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const count = ref(1)

    // 返回渲染函数
    return () => h('div', props.msg + count.value)
  }
}

setup() 内部声明的渲染函数天生能够访问在同一范围内声明的 props 和许多响应式状态。

除了返回一个 vnode,你还可以返回字符串或数组:

js 复制代码
export default {
  setup() {
    return () => 'hello world!'
  }
}
js 复制代码
import { h } from 'vue'

export default {
  setup() {
    // 使用数组返回多个根节点
    return () => [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}

下面来看一个简单的例子:

js 复制代码
<script>
import { defineComponent, h } from "vue";
export default defineComponent({
  render() {
    const props = { style: { color: "red" } };
    const small = h("small", "副标题");
    return h("h2", props, ["123456789", small]);
  },
});
</script>

注意:组件树中的 vnodes 必须是唯一的,下面是错误示范:

js 复制代码
function render() {
  const p = h('p', 'hi')
  return h('div', [
    // 啊哦,重复的 vnodes 是无效的
    p,
    p
  ])
}

如果想在页面上渲染多个重复的元素或者组件,可以使用一个工厂函数来做这件事

js 复制代码
function render() {
  return h(
    'div',
    Array.from({ length: 20 }).map(() => {
      return h('p', 'hi')
    })
  )
}

三、渲染组件并实现双向绑定

h() 函数还可以渲染 "组件", 假设我们有一个 switch 组件, 其支持 <switch v-model:checked="checked"/>.

js 复制代码
<script>
import { ref, h } from 'vue'
import ASwitch from "../components/ASwitch.vue";

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const checked = ref(false)

    // 返回渲染函数
    return () => h(ASwitch)
  }
}
</script>

此时会发现点击切换不了,这是因为没有像在模板中那样使用 v-model,需要在 "h" 中通过第2 个参数传入 checked 属性和 onUpdate:checked 事件实现 v-model 的等同操作

js 复制代码
<script>
import { ref, h } from 'vue'
import ASwitch from "../components/ASwitch.vue";

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const checked = ref(false)

    // 返回渲染函数
    return () => h(ASwitch, {
      checked: checked.value,
      ["onUpdate:checked"]: (checked) => {
        checked.value = checked;
      },
    });
  },)
  }
}
</script>

四、渲染函数官网案例

1. v-if

js 复制代码
<div>
  <div v-if="ok">yes</div>
  <span v-else>no</span>
</div>

// 等同于
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])

2. v-for

js 复制代码
<ul>
  <li v-for="{ id, text } in items" :key="id">
    {{ text }}
  </li>
</ul>

// 等同于
h(
  'ul',
  // assuming `items` is a ref with array value
  items.value.map(({ id, text }) => {
    return h('li', { key: id }, text)
  })
)

3. v-on

on 开头,并跟着大写字母的 props 会被当作事件监听器。比如,onClick 与模板中的 @click 等价。

js 复制代码
h(
  'button',
  {
    onClick(event) {
      /* ... */
    }
  },
  'click me'
)

4. 渲染插槽

js 复制代码
export default {
  props: ['message'],
  setup(props, { slots }) {
    return () => [
      // 默认插槽:
      // <div><slot /></div>
      h('div', slots.default()),

      // 具名插槽:
      // <div><slot name="footer" :text="message" /></div>
      h(
        'div',
        slots.footer({
          text: props.message
        })
      )
    ]
  }
}

5. 传递插槽

js 复制代码
// 单个默认插槽
h(MyComponent, () => 'hello')

// 具名插槽
// 注意 `null` 是必需的
// 以避免 slot 对象被当成 prop 处理
h(MyComponent, null, {
    default: () => 'default slot',
    foo: () => h('div', 'foo'),
    bar: () => [h('span', 'one'), h('span', 'two')]
})

五、实例:Modal.confirm 中使用渲染函数

下面来看一下我在公司的管理后台项目中遇到的一个问题。首先在 ant-design-vue 组件库中的对话框组件有使用到 createVNode,代码如下:

js 复制代码
<template>
  <div>
    <a-button @click="showConfirm">Confirm</a-button>
  </div>
</template>
<script lang="ts">
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { createVNode, defineComponent } from 'vue';
import { Modal } from 'ant-design-vue';
export default defineComponent({
  setup() {
    const showConfirm = () => {
      Modal.confirm({
        title: () => 'Do you Want to delete these items?',
        icon: () => createVNode(ExclamationCircleOutlined),
        content: () => createVNode('div', { style: 'color:red;' }, 'Some descriptions'),
        onOk() {
          console.log('OK');
        },
        onCancel() {
          console.log('Cancel');
        },
        class: 'test',
      });
    };
    return {
      showConfirm
    };
  },
});
</script>

下面我们公司新增了一个需求,需要在确认框中放入一个 select 下拉框,其实我们可以改造下确认框,直接使用 a-modal 即可,这样会方便许多;那么在这里我将使用 createVNode 来完成插入下拉框的需求,代码上会复杂许多,但是能够提高对渲染函数的理解。

js 复制代码
Modal.confirm({
    title: () => '修改所有用户的属性',
    closable: true, 
    centered: true,
    content: (h) => {
        let Option = []
        for (let i in self.addOption) {
            Option.push(h('a-select-option', {
                props: {
                    value: self.addOption[i].type
                }
            }, self.addOption[i].title))
        }
        let Select = h('div', {style: {padding: '10px 0'}}, [h('a-select', {
            props: {
                value: self.form.addType,
            },
            style: {
                width: '100%'
            },
            on: {
                input: (val) => {
                    self.form.addType = val
                },
                change: (v) => {
                    self.form.addType = v
                }
            }
        }, Option)])
        
        let Input = h('div', {style: {padding: '10px 0'}}, [h('a-input', {
            props: {
                value: self.form.editValue,
                placeholder: '请输入值'
            },
            style: {
                width: '100%'
            },
            on: {
                input: (val) => {
                    self.form.editValue = val
                },
                change: (v) => {
                    self.form.editValue = v
                }
            }
        })])
        return h('div', {style: {padding: '10px 0'}}, [Select, Input])
    },
    onOk() {
        console.log('OK');
    },
    onCancel() {
        console.log('Cancel');
    },
    class: 'test',
});

最终效果如下:

相关推荐
alikami3 分钟前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
吃杠碰小鸡37 分钟前
lodash常用函数
前端·javascript
emoji1111111 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼1 小时前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_748250031 小时前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O1 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235951 小时前
web复习(三)
前端
迷糊的『迷』1 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
web135085886351 小时前
uniapp小程序使用webview 嵌套 vue 项目
vue.js·小程序·uni-app
AiFlutter1 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter