Vue2 渲染函数与 JSX
Vue 的模板语法在绝大多数场景下足够使用,但在某些复杂场景下,渲染函数(Render Functions)提供了更灵活的编程能力。配合 JSX 语法,可以像写 React 一样编写 Vue 组件。
一、前言
Vue 推荐在绝大多数情况下使用模板来构建 HTML。然而在某些场景下,你需要完全利用 JavaScript 的编程能力:
- 需要大量条件判断和循环的动态组件结构
- 需要精细控制子组件的渲染逻辑
- 封装高度抽象的组件库
二、虚拟 DOM 与 VNode
2.1 什么是虚拟 DOM
虚拟 DOM(Virtual DOM)是真实 DOM 的 JavaScript 对象表示。Vue 通过对比新旧 VNode 树(Diff 算法),最小化地更新真实 DOM。
2.2 VNode 的结构
// 简化的 VNode 结构{tag:'div',data:{class:'container',attrs:{id:'app'}},children:[{tag:'h1',text:'标题'},{tag:'p',text:'段落'}],text:undefined,elm:undefined// 对应的真实 DOM 元素}三、渲染函数基础
3.1 createElement 参数
渲染函数接收createElement(通常简写为h)作为参数:
exportdefault{render(h){returnh('div',{// 数据对象class:{active:this.isActive},style:{color:'red'},attrs:{id:'foo'},domProps:{innerHTML:'<span>HTML</span>'},on:{click:this.handleClick}},[// 子节点数组h('h1','标题'),h('p','内容'),this.items.map(item=>h('span',item.name))]);}};3.2 createElement 参数详解
h(// {String | Object | Function}// HTML 标签、组件选项或异步组件函数'div',// {Object} 可选,数据对象{// 与 `v-bind:class` 相同class:{active:true,'text-danger':false},// 与 `v-bind:style` 相同style:{fontSize:'14px',color:'red'},// 普通 HTML 属性attrs:{id:'foo',href:'#'},// 组件 propsprops:{myProp:'bar'},// DOM 属性domProps:{innerHTML:'baz'},// 事件监听器,支持 ~ 和 ! 修饰符on:{click:this.clickHandler,'~keyup':this.keyupHandler,'!click':this.captureClick},// 仅用于组件,原生事件nativeOn:{click:this.nativeClickHandler},// 自定义指令directives:[{name:'my-directive',value:'2',expression:'1 + 1'}],// 作用域插槽格式:{ name: props => VNode | Array<VNode> }scopedSlots:{default:props=>h('span',props.text)},// 插槽名称slot:'name-of-slot',// 其他顶级属性key:'myKey',ref:'myRef'},// {String | Array} 子节点['先写一些文字',h('h1','一则头条')]);四、模板 vs 渲染函数
4.1 模板写法
<template><divclass="list-container"><h2v-if="title">{{ title }}</h2><ul><liv-for="item in items":key="item.id":class="{ active: item.active }"@click="select(item)">{{ item.name }}</li></ul></div></template>4.2 等价渲染函数
exportdefault{props:['title','items'],methods:{select(item){this.$emit('select',item);}},render(h){returnh('div',{class:'list-container'},[this.title?h('h2',this.title):null,h('ul',this.items.map(item=>h('li',{key:item.id,class:{active:item.active},on:{click:()=>this.select(item)}},item.name)))]);}};五、JSX 语法
5.1 JSX 配置
需要配置 Babel 插件支持 JSX:
npminstall@vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props-D// babel.config.jsmodule.exports={presets:['@vue/babel-preset-jsx']};5.2 JSX 基本用法
exportdefault{data(){return{msg:'Hello JSX'};},render(){return(<divclass="container"><h1>{this.msg}</h1><p>使用JSX编写 Vue 组件</p></div>);}};5.3 JSX 中的指令
exportdefault{render(){constitems=['a','b','c'];return(<div>{/* v-if -> 三元表达式 */}{this.show?<span>显示</span>:null}{/* v-for -> Array.map */}<ul>{items.map((item,index)=>(<li key={index}>{item}</li>))}</ul>{/* v-model -> value + onInput */}<input value={this.inputValue}onInput={e=>this.inputValue=e.target.value}/>{/* v-on -> onXxx */}<button onClick={this.handleClick}>点击</button>{/* 事件修饰符 -> 原生处理 */}<button onClick_stop={this.handleClick}>阻止冒泡</button><button onClick_prevent={this.handleClick}>阻止默认</button>{/* 插槽 */}<MyComponent><div slot="header">头部</div><div>默认内容</div></MyComponent>{/* 作用域插槽 */}<MyComponent scopedSlots={{default:({text})=><span>{text}</span>}}/></div>);}};六、函数式组件
6.1 什么是函数式组件
函数式组件是无状态(没有响应式数据)且无实例(没有 this 上下文)的组件。它们只接收 props 并返回 VNode:
// 函数式组件(JSX)exportdefault{functional:true,props:['level','title'],render(h,context){const{props,slots,listeners}=context;returnh(`h${props.level}`,{on:listeners},[props.title,slots().default]);}};6.2 函数式组件优势
- 渲染开销低(无响应式系统)
- 适合纯展示组件
- 可作为高阶组件包装器
// 高阶组件示例:添加点击跟踪exportdefaultfunctionwithTracking(Component){return{functional:true,render(h,context){constlisteners={...context.listeners,click:(...args)=>{console.log('组件被点击');context.listeners.click&&context.listeners.click(...args);}};returnh(Component,{...context.data,on:listeners},context.children);}};}七、插槽与渲染函数
7.1 访问插槽
exportdefault{render(h){// 访问默认插槽constdefaultSlot=this.$slots.default;// 访问命名插槽constheaderSlot=this.$slots.header;// 访问作用域插槽constscoped=this.$scopedSlots.default;returnh('div',[h('header',headerSlot),h('main',defaultSlot),scoped?scoped({text:'作用域数据'}):null]);}};八、常见场景
8.1 动态表格列
exportdefault{props:['columns','data'],render(h){returnh('table',[h('thead',[h('tr',this.columns.map(col=>h('th',col.title)))]),h('tbody',this.data.map(row=>h('tr',this.columns.map(col=>h('td',{domProps:{innerHTML:col.render?col.render(h,row[col.key],row):row[col.key]}})))))]);}};8.2 动态组件工厂
// 根据配置动态生成表单组件exportdefault{props:['config'],render(h){returnh('form',this.config.fields.map(field=>{constcomponentMap={input:'el-input',select:'el-select',date:'el-date-picker'};returnh(componentMap[field.type]||'input',{props:{value:field.value},on:{input:val=>this.$emit('change',field.key,val)}});}));}};九、总结
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 模板 | 绝大多数场景 | 直观、声明式、易维护 | 复杂逻辑受限 |
| 渲染函数 | 高度动态内容 | 完全编程控制 | 可读性较差 |
| JSX | 熟悉 React 的开发者 | 接近 JavaScript 语法 | 需要额外配置 |
| 函数式组件 | 纯展示组件 | 性能最优 | 无状态、无生命周期 |
建议:优先使用模板,遇到模板难以表达的场景再考虑渲染函数或 JSX。
下一章我们将学习 Vue 项目的构建与工程化,掌握 Vue CLI 和 Webpack 配置。
十、练习
- 使用渲染函数重写一个
v-if+v-for组合的复杂组件 - 用 JSX 实现一个可复用的
List组件,支持自定义渲染项 - 创建一个函数式组件
Heading,根据 level 渲染 h1-h6 - 实现一个动态表单组件,根据 JSON 配置渲染不同类型的表单项