一、createElement 是干啥的?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 元素加原生属性(title
、placeholder
);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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。