Vue h函数到底是个啥?

发布于:2025-02-13 ⋅ 阅读:(108) ⋅ 点赞:(0)

h 到底是个啥?



对于了解或学习Vue高阶组件(HOC)的同学来说,h() 函数无疑是一个经常遇到的概念。
在这里插入图片描述
那么,这个h() 函数究竟如何使用呢,又在什么场景下适合使用呢?



一、h 是什么

看到这个函数你可能会有些许困惑,为什么叫h呢?代表着什么呢?

官方定义

返回一个“虚拟节点” ,通常缩写为 VNode: 一个普通对象,其中包含向 Vue 描述它应该在页面上呈现哪种节点的信息,包括对任何子节点的描述。用于手动编写render

h其实代表的是 hyperscript 。它是 HTML 的一部分,表示的是超文本标记语言,当我们正在处理一个脚本的时候,在虚拟 DOM 节点中去使用它进行替换已成为一种惯例。这个定义同时也被运用到其他的框架文档中

Hyperscript 它本身表示的是 “生成描述 HTML 结构的脚本”

二、语法

h() 函数的基本语法如下:

h(tag, props, children)
  • tag:可以是字符串或组件,表示要创建的 HTML 标签或 Vue 组件。
  • props:是一个对象,包含要传递给标签或组件的属性,如类名、样式、事件监听器等。
  • children:可以是字符串、数组或函数,表示子节点。子节点可以是文本、其他 VNode 或一个返回 VNode 的函数。

以下是一个简单的代码示例,展示了如何使用 h() 函数创建一个包含标题和段落的组件:


  <!DOCTYPE html>
  <html>
    <head>
    <meta charset="utf-8">
    <title>h()</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0-rc.4/vue.global.js"></script>
    </head>
    <body>
       <div id="app">
       </div>
       <script>
        const App = {
          render() {
            return Vue.h('h1', {}, '二川兄弟')
          }
        }
        // console 结果请在控制台查看
        console.log(Vue.h('h1', {}, '二川兄弟'))
        Vue.createApp(App).mount('#app')
      </script>

    </body>
  </html>

效果:
在这里插入图片描述

打印:
在这里插入图片描述

在这个示例中,我们创建了一个 <h1> 容器,内部插入文字 “二川兄弟”。所有这些都是通过 h() 函数来实现的,而不是使用模板语法。

三、源码解析

要深入理解 h() 函数,我们需要查看 Vue.js 的源码。在 Vue.js 的实现中,h() 函数是一个封装了 VNode 创建逻辑的工具函数。它接收标签名、属性和子节点作为参数,并返回一个包含这些信息的 VNode 对象。

export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  if (arguments.length === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // single vnode without props
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }
      // props without children
      return createVNode(type, propsOrChildren)
    } else {
      // omit props
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    if (isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

VNode 是 Vue.js 对真实 DOM 的一种轻量级表示。它包含了节点的类型、属性、子节点等信息,但不包含具体的 DOM 元素。Vue.js 会在渲染时将 VNode 转换为真实的 DOM 元素。

h() 函数的源码中,你会看到大量的边界情况处理和类型检查。例如,如果子节点是一个字符串,它会被转换为一个文本节点;如果子节点是一个数组,它会遍历数组并递归地创建子节点的 VNode;如果子节点是一个函数,它会调用该函数并传递当前上下文来创建子节点的 VNode。

_createVNode 又做了啥?

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  // 更新标志
  patchFlag: number = 0,
  // 自定义属性
  dynamicProps: string[] | null = null,
  // 是否是动态节点,(v-if v-for)
  isBlockNode = false 
): VNode {
  // type必传参数
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    if (__DEV__ && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}.`)
    }
    type = Comment
  }

  // Class 类型的type标准化
  // class component normalization.
  if (isFunction(type) && '__vccOpts' in type) {
    type = type.__vccOpts
  }

  // class & style normalization.
  if (props) {
    // props 如果是响应式,clone 一个副本
    if (isProxy(props) || InternalObjectKey in props) {
      props = extend({}, props)
    }
    let { class: klass, style } = props

    // 标准化class, 支持 string , array, object 三种形式
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }

    // 标准化style, 支持 array ,object 两种形式 
    if (isObject(style)) {
      // reactive state objects need to be cloned since they are likely to be
      // mutated
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // encode the vnode type information into a bitmap
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE
      : isTeleport(type)
        ? ShapeFlags.TELEPORT
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT
            : 0

  if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
    type = toRaw(type)
    warn(
      `Vue received a Component which was made a reactive object. This can ` +
        `lead to unnecessary performance overhead, and should be avoided by ` +
        `marking the component with \`markRaw\` or using \`shallowRef\` ` +
        `instead of \`ref\`.`,
      `\nComponent that was made reactive: `,
      type
    )
  }

  // 构造 VNode 模型
  const vnode: VNode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    children: null,
    component: null,
    suspense: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  }

  normalizeChildren(vnode, children)

  // presence of a patch flag indicates this node needs patching on updates.
  // component nodes also should always be patched, because even if the
  // component doesn't need to update, it needs to persist the instance on to
  // the next vnode so that it can be properly unmounted later.

  // patchFlag 标志存在表示节点需要更新,组件节点一直存在 patchFlag,因为即使不需要更新,它需要将实例持久化到下一个 vnode,以便以后可以正确卸载它
  if (
    shouldTrack > 0 &&
    !isBlockNode &&
    currentBlock &&
    // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    patchFlag !== PatchFlags.HYDRATE_EVENTS &&
    (patchFlag > 0 ||
      shapeFlag & ShapeFlags.SUSPENSE ||
      shapeFlag & ShapeFlags.TELEPORT ||
      shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||
      shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
  ) {
    // 压入 VNode 栈
    currentBlock.push(vnode)
  }

  return vnode
}

四、使用场景

h() 函数在 Vue.js 中有多种使用场景,以下是一些常见的场景:

  1. 渲染函数

    • 当需要完全控制组件的渲染逻辑时,可以使用渲染函数,并在其中使用 h() 函数来创建 VNode。这种方式提供了比模板语法更高的灵活性和控制力。

    以下是一个使用渲染函数的示例,展示了如何根据条件动态渲染不同的内容:

    import { h } from 'vue';
    
    export default {
      props: ['isLoggedIn'],
      render() {
        return h('div', 
          {}, 
          this.isLoggedIn 
            ? h('p', {}, 'Welcome back!') 
            : h('p', {}, 'Please log in.')
        );
      }
    };
    
  2. 高阶组件(HOC)

    • 高阶组件是一种模式,它接收一个组件作为参数,并返回一个新的组件。在创建新组件的过程中,h() 函数用于定制或扩展原始组件的渲染逻辑。

    以下是一个高阶组件的示例,展示了如何为组件添加额外的类名:

    function withClassName(WrappedComponent, className) {
      return {
        props: WrappedComponent.props,
        render() {
          return h(WrappedComponent, { ...this.$props, class: className });
        }
      };
    }
    
    // 使用高阶组件
    const MyComponentWithClass = withClassName(MyComponent, 'my-custom-class');
    
  3. 动态组件

    • 在需要根据条件动态渲染不同组件时,h() 函数可以方便地根据条件创建不同的 VNode。

    以下是一个动态组件的示例,展示了如何根据条件渲染不同的组件:

    import { h, defineComponent } from 'vue';
    import ComponentA from './ComponentA.vue';
    import ComponentB from './ComponentB.vue';
    
    export default defineComponent({
      props: ['condition'],
      render() {
        return this.condition 
          ? h(ComponentA, { ...this.$props }) 
          : h(ComponentB, { ...this.$props });
      }
    });
    
  4. 手动创建复杂结构

    • 在某些情况下,可能需要手动创建复杂的组件结构,而不是使用模板语法。这时,h() 函数就显得非常有用。

    以下是一个手动创建复杂结构的示例,展示了如何创建一个带有嵌套子组件的表格:

    import { h } from 'vue';
    import TableRow from './TableRow.vue';
    
    export default {
      props: ['data'],
      render() {
        return h('table', 
          {}, 
          this.data.map(row => 
            h(TableRow, { key: row.id, row: row })
          )
        );
      }
    };
    

五、其他比较

与模板语法相比,使用 h() 函数提供了更高的灵活性和控制力。模板语法更适合于简单的组件和静态内容,而 h() 函数则更适合于复杂的组件和动态内容。

此外,与 JSX 相比,h() 函数更加简洁和直接。JSX 是一种语法糖,它允许在 JavaScript 中编写类似 HTML 的代码。然而,JSX 需要额外的编译步骤,并且可能会增加代码的复杂性。而 h() 函数则是 Vue.js 内置的工具函数,无需额外的编译步骤,并且更加符合 Vue.js 的设计哲学。

六、最佳实践

在使用 h() 函数时,以下是一些最佳实践:

  1. 保持简洁

    • 尽量避免在渲染函数中编写过多的逻辑。可以将复杂的逻辑拆分成多个函数或组件,以提高代码的可读性和可维护性。
  2. 使用辅助函数

    • 可以编写一些辅助函数来简化 h() 函数的使用。例如,可以编写一个函数来创建带有特定样式的节点,或者一个函数来创建带有事件监听器的节点。
  3. 避免过度使用

    • 在大多数情况下,模板语法已经足够满足需求。只有在需要更高的灵活性和控制力时,才应该考虑使用 h() 函数。
  4. 与模板语法结合使用

    • 可以将 h() 函数与模板语法结合使用。例如,可以在模板中使用 <script setup> 语法来定义渲染函数,并在其中使用 h() 函数来创建复杂的节点结构。

七、总结

在这里插入图片描述

h() 函数是 Vue.js 框架中一个强大且灵活的工具,用于在渲染函数中创建虚拟 DOM 节点。通过深入理解 h() 函数的语法、源码和使用场景,开发者可以更好地掌握 Vue.js 的渲染机制,并在实际开发中灵活运用这一工具来创建高效、可维护的组件。无论是编写渲染函数、实现高阶组件,还是处理动态组件和复杂结构,h() 函数都是不可或缺的一部分。


网站公告

今日签到

点亮在社区的每一天
去签到