Code前端首页关于Code前端联系我们

一、createElement 是干啥的?Vue 渲染流程里的关键角色

terry 13小时前 阅读数 10 #Vue

想学透 Vue2 的渲染逻辑,绕不开 createElement 这个核心函数,不少同学刚接触时觉得它抽象,其实把「它是干啥的、参数咋用、啥场景必须用、怎么避坑」这些问题拆明白,就能轻松掌握,下面用问答式结构,从基础到实战把 createElement 讲透~

你可以把 createElement 理解成「虚拟 DOM 工厂」——它的任务是生成 VNode(虚拟节点),而 VNode 是 Vue 渲染真实 DOM 的“蓝图”。

Vue 的渲染流程是这样的:
写在 .vue 文件里的模板(<template>),会先被编译成 抽象语法树(AST),再进一步转换成 渲染函数,渲染函数里最核心的逻辑,就是调用 createElement 生成 VNode,Vue 会通过 patch 算法,把 VNode 转换成真实 DOM 并挂载到页面上。

举个极简例子,不用模板、纯用渲染函数写 Vue 组件:

new Vue({
  el: '#app',
  // 渲染函数接收 createElement 作为参数(通常简写成 h)
  render(h) { 
    // h createElement 的别名,调用后生成 VNode
    return h('div', { class: 'hello' }, '你好,Vue2!') 
  }
})

这段代码会在页面生成一个带 class="hello"<div>是“你好,Vue2!”,能直观看到:createElement 是连接“逻辑”和“真实 DOM”的桥梁,没有它,Vue 根本不知道该怎么把 JS 逻辑变成页面元素~

createElement 的参数怎么玩明白?三个参数逐个拆

createElement(即 h 函数)接收 三个参数h(tag, data, children),每个参数都有明确分工:

第一个参数:tag(要渲染的标签或组件)

  • 可以是 字符串'div''button' 这类 HTML 标签;
  • 可以是 组件选项对象:比如自己写的 MyComponent(需要先导入);
  • 甚至可以是 异步组件() => import('./MyComponent.vue')(实现路由懒加载的思路)。

第二个参数:data(数据对象,给节点传“配置”)

这是最容易懵的部分,因为它要装 DOM 属性、组件Props、样式、事件 等一堆配置,常见字段有这些:

  • attrs:给 DOM 元素加原生属性(titleplaceholder);
  • props:给组件传 props(只有传自定义组件时有用);
  • class:设置类名,支持 字符串、数组、对象(对象形式常用于动态切换类,{ active: isActive });
  • style:设置内联样式,是个对象({ color: 'red', fontSize: '14px' });
  • on:绑定事件({ click: handleClick },注意是给组件绑自定义事件;如果给原生 DOM 绑事件,逻辑一样);
  • key:和列表渲染里的 key 作用一样,用于 diff 算法优化;
  • 还有 directives(自定义指令)、slot(插槽)等进阶用法…

第三个参数:children(子节点)

子节点支持 字符串、数组、VNode 三种形式:

  • 字符串:直接当文本内容('点击我');
  • 数组:里面可以嵌套 h 函数调用(生成子 VNode),也能混字符串;
  • VNode:由 h 函数生成的虚拟节点(比如嵌套一个子组件)。

学 createElement 只是为了替代模板?这些场景非它不可

很多同学疑惑:“模板写起来像 HTML,多直观?为啥非要学 createElement?” 其实模板是“声明式”的简化语法,而 createElement 代表的 渲染函数,是“命令式”的灵活武器,遇到这些场景,模板真搞不定:

动态组件切换(根据逻辑渲染不同组件)

比如做一个“Tab 切换”,点击不同标签显示不同组件,用模板得写一堆 v-if/v-else,但用渲染函数可以直接用 JS 逻辑控制

render(h) {
  let CurrentComponent
  if (this.activeTab === 'user') {
    CurrentComponent = UserCard // 假设 UserCard 是组件选项
  } else {
    CurrentComponent = ProductList
  }
  return h(CurrentComponent, { props: { data: this.data } })
}

模板的 <component :is="xxx"> 其实底层也是这么玩的,但自己写渲染函数能更灵活地加逻辑(比如切换时加动画、权限判断)。

复杂逻辑渲染(比如低代码/动态页面)

如果后端返回一个 JSON,描述页面该渲染哪些组件、传什么参数(类似低代码平台的玩法),模板根本“看不懂”这种动态配置,但渲染函数可以 循环解析 JSON,动态生成 VNode:

render(h) {
  // 假设 this.pageConfig 是后端返回的配置,结构像:
  // { components: [{ type: 'Button', props: { text: '点我' } }, { type: 'Input', props: { placeholder: '请输入' } }] }
  return h('div', {}, this.pageConfig.components.map(config => {
    // 根据 type 渲染不同组件
    const Component = getComponentByType(config.type) // 自己实现的“组件映射表”
    return h(Component, { props: config.props })
  }))
}

性能敏感场景(减少编译开销)

模板在运行时需要先编译成渲染函数,再生成 VNode,如果某个组件 高频更新(比如实时刷新的仪表盘),可以直接写渲染函数,跳过“模板编译”这一步,让渲染更快。

封装高阶组件(给组件加通用逻辑)

比如做一个“权限控制组件”,要求只有登录用户能看到子组件,用渲染函数可以 包装子组件,动态修改它的渲染逻辑:

// 高阶组件:权限控制
function withAuth(WrappedComponent) {
  return {
    render(h) {
      if (this.isAuthenticated) { // 假设 isAuthenticated 是登录状态
        return h(WrappedComponent, { props: this.$attrs })
      } else {
        return h('div', '请登录后查看')
      }
    },
    data() { return { isAuthenticated: false } }
  }
}
// 使用时:
const AuthButton = withAuth(MyButton)

模板里的语法,在 createElement 里咋实现?

模板是渲染函数的“语法糖”,所有 v-if/v-for/:class/@click 最终都会被编译成 createElement 的调用,搞清楚对应关系,学渲染函数会更顺:

模板语法 createElement 实现逻辑
v-if 用 JS 的 if/else 判断
v-for 用数组的 map 遍历生成子节点
:class 放到 data.class(对象/数组形式)
@click 放到 data.on.click
{{ msg }} 子节点传字符串 this.msg

举个综合例子,把模板转成渲染函数,感受一一对应关系:

模板写法:

<div 
  class="box" 
  :class="{ active: isActive }" 
  @click="handleClick"
>
  <p v-if="showMsg">{{ msg }}</p>
  <ul>
    <li v-for="item in list" :key="item.id">{{ item.name }}</li>
  </ul>
</div>

渲染函数写法:

render(h) {
  return h('div', {
    // 对应 :class
    class: { box: true, active: this.isActive }, 
    // 对应 @click
    on: { click: this.handleClick } 
  }, [
    // 对应 v-if="showMsg"
    this.showMsg ? h('p', this.msg) : null, 
    // 对应 v-for
    h('ul', this.list.map(item => { 
      return h('li', { key: item.id }, item.name)
    }))
  ])
}

实际开发中,createElement 容易踩哪些坑?

学会用法后,还要避开这些“经典错误”,否则控制台报错能把人搞懵:

子节点格式错误:不是数组/文本

错误示例:

render(h) {
  return h('div', {}, { msg: '我是错的' }) // 子节点传了对象,不是数组/文本
}

报错:Invalid VNode type: [object Object] (expected string, Object, or Array)

修正:子节点必须是 字符串数组(数组里装 VNode/字符串):

render(h) {
  return h('div', {}, '我是正确文本') 
  // 或者数组形式:[h('p', '正确'), '也可以混字符串']
}

数据对象属性写错:事件/Props 传不进去

错误示例(给组件传 Props 时写错字段):

// 假设 MyComponent 有个 props: { title: String }
render(h) {
  return h(MyComponent, { attrs: { title: '错的' } }) // 应该用 props 而不是 attrs
}

结果:组件收不到 title,因为 attrs 是给 DOM 元素传原生属性的,给组件传 Props 要写在 data.props

修正:

render(h) {
  return h(MyComponent, { props: { title: '对的' } })
}

组件引用错误:传字符串当组件

错误示例(没导入组件,直接传字符串):

render(h) {
  return h('MyComponent', { props: { data: {} } }) // MyComponent 是自定义组件,不是 HTML 标签
}

报错:Unknown custom element: <MyComponent>

修正:先导入组件,再传组件选项对象

import MyComponent from './MyComponent.vue'
render(h) {
  return h(MyComponent, { props: { data: {} } })
}

列表渲染忘加 key:diff 算法出问题

错误示例(循环生成子节点没写 key):

render(h) {
  return h('ul', {}, this.list.map(item => {
    return h('li', {}, item.name) // 没加 key
  }))
}

隐患:Vue diff 时无法精准识别节点,导致更新时 DOM 操作冗余,甚至数据错乱。

修正:给每个子节点加 key(放在 data 对象里):

render(h) {
  return h('ul', {}, this.list.map(item => {
    return h('li', { key: item.id }, item.name)
  }))
}

想写得更爽?试试 JSX 和 createElement 的结合

纯用 createElement 写复杂结构时,代码会很“碎”(全是嵌套的 h 函数),Vue2 支持 JSX(需要装 babel-plugin-transform-vue-jsx 插件),写法和 React JSX 很像,可读性直接起飞!

比如用 JSX 写之前的“权限控制+列表”例子:

render() {
  return (
    <div class="box" onClick={this.handleClick}>
      {this.showMsg && <p>{this.msg}</p>}
      <ul>
        {this.list.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  )
}

对比纯 createElement 写法,JSX 更像 HTML,嵌套结构一目了然。本质上 JSX 还是会被编译成 createElement 调用,所以它是“语法糖”,让我们写得更爽~

从 Vue2 到 Vue3,createElement 有啥变化?

Vue3 里,createElement 还是叫 h 函数,但参数设计更灵活了:

  • Vue2 的 h(tag, data, children) → Vue3 的 h(type, props?, children?)
  • Vue3 把 data 里的 attrs/props/on 等字段合并到 props 里,不需要再严格区分;
  • Vue3 推荐用 组合式 API,但渲染函数的核心逻辑(生成 VNode)和 Vue2 一脉相承。

所以学透 Vue2 的 createElement,不仅能搞定当前项目,还能帮你理解 Vue 渲染原理,未来升级 Vue3 也更容易过渡~

掌握 createElement,打开 Vue 渲染的“黑箱”

createElement 是 Vue2 渲染机制的“心脏”——模板再好用,遇到动态逻辑、复杂封装、性能优化时,还是得靠它,把“参数怎么传、场景怎么选、坑怎么避”这几点吃透,再结合 JSX 写更简洁的渲染函数,你对 Vue 的理解会直接上一个台阶。

下次遇到“模板搞不定的需求”,别慌,想想 createElement 的用法,灵活用 JS 逻辑生成 VNode,你会发现:原来 Vue 的渲染还能这么玩!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门