vue的h函数(在 Vue 2中也称为 createElement)理解

2,501 阅读5分钟

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

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

官方定义

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

h函数格式说明及使用

h 接收三个参数

  • type: 必需,表示要创建的元素类型或组件类型。它可以是一个字符串(HTML标签名),一个组件选项对象、异步组件函数或者一个函数式组件。
  • props: 可选的对象,包含了传递给元素或组件的所有属性(attributes)和 props。例如:可以包含类名、样式、事件监听器等。
  • children: 可选,代表子节点,它可以是字符串(文本内容)、数组(包含多个子节点,每个子节点可以是字符串或其他由 h 创建的虚拟节点)或者其他合法的虚拟DOM节点。
  • 示例1:简单创建一个VNode(vue3)

import { h } from 'vue'

const vnode = h(
  'div', // 类型为 div 标签
  { id: 'app', class: 'container' }, // props 对象
  [ // 子节点数组
    h('p', { class: 'text' }, 'Hello, World!'), // 子节点1:一个 p 标签
    h('button', { onClick: handleClick ,
    style: { color: '#fbbf24', fontSize: '22px' } //样式
    }, 'Click me') // 子节点2:一个绑定了点击事件handleClick的按钮
  ]
)

function handleClick() {
  console.log('Button was clicked!')
}

通过h函数的方式构建虚拟DOM树,Vue可以在内部高效地比较新旧虚拟DOM的不同,并最小化地更新实际DOM,从而提高页面渲染性能。

示例2:vue2中h函数用法

import Vue from 'vue' // 引入Vue和h函数

// 定义一个组件
var MyComponent = {
  props: ['message'],
  render: function (createElement) {
    return createElement( // createElement实际上就是h函数
      'div', // 元素类型
      { 		 // 属性对象
        class: ['my-class'], 
        style: { color: 'red' },
        on: {
          click: this.handleClick,
        },
      },
      [ // 子元素数组
        this.message, // 文本内容
        createElement('span', {}, '这是一个子元素'),
      ]
    )
  },
  methods: {
    handleClick: function (event) {
      console.log('Clicked!')
    }
  }
}

// 注册并使用该组件
new Vue({
  el: '#app',
  components: {
    MyComponent
  },
  template: `
    <div id="app">
      <my-component message="Hello from custom render function!" />
    </div>
  `
})

示例3:vue3中h函数的用法

import { h, ref, defineComponent } from 'vue';

// 定义一个使用h函数的组件
const MyComponent = defineComponent({
  props: {
    message: String,
  },
  setup(props) {
    const count = ref(0);

    function handleClick() {
      count.value++;
    }

    return () => h('div', { // 元素类型
      class: ['my-class'], 
      style: { color: 'red' },
      onClick: handleClick, // 事件监听器
    }, [
      props.message, // 文本内容
      h('span', {}, '这是一个子元素'),
      `Clicked ${count.value} times`, // 动态文本内容
    ]);
  },
});

// 使用该组件
new Vue({
  render: (h) => h(MyComponent, { props: { message: "Hello from custom render function!" } }),
}).$mount('#app');

h函数实现原理

官方实现原理

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)
  }
}

_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函数常用场景

1、自定义渲染逻辑:当模板语法不足以处理复杂的动态内容或需要根据运行时条件动态生成DOM结构时,可以使用 h 函数在组件的 render 函数中编写更灵活的渲染逻辑。

2、高级组件库开发:组件库开发者通常会依赖 h 函数来创建可复用、功能丰富的组件。通过编程方式构建虚拟节点,可以实现对DOM元素精细控制和优化,如高级表格组件、树形控件、拖拽排序等复杂交互场景。

3、性能优化:在某些性能关键路径上,通过手动编写 h 函数来精确控制DOM更新,避免不必要的子组件渲染,从而提升应用性能。

4、无模板组件:如果不希望或者无法使用 .vue 单文件组件中的 <template> 标签,可以完全基于 h 函数来编写组件的渲染内容。

5、与JSX结合:Vue 3支持 JSX 语法,而 JSX 在编译阶段会被转换为 h 函数调用,因此对于喜欢 React 风格 JSX 开发的开发者来说,h 函数是底层支持的基础。