13-Vue2 渲染函数与 JSX Vue2 渲染函数与 JSXVue 的模板语法在绝大多数场景下足够使用但在某些复杂场景下渲染函数Render Functions提供了更灵活的编程能力。配合 JSX 语法可以像写 React 一样编写 Vue 组件。一、前言Vue 推荐在绝大多数情况下使用模板来构建 HTML。然而在某些场景下你需要完全利用 JavaScript 的编程能力需要大量条件判断和循环的动态组件结构需要精细控制子组件的渲染逻辑封装高度抽象的组件库Vue 模板编译模板渲染函数JSX编译为渲染函数直接操作 VNode编译为渲染函数VNode 树DOM 更新二、虚拟 DOM 与 VNode2.1 什么是虚拟 DOM虚拟 DOMVirtual DOM是真实 DOM 的 JavaScript 对象表示。Vue 通过对比新旧 VNode 树Diff 算法最小化地更新真实 DOM。模板/渲染函数创建 VNodeDiff 对比生成补丁更新真实 DOM2.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:spanHTML/span},on:{click:this.handleClick}},[// 子节点数组h(h1,标题),h(p,内容),this.items.map(itemh(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 | ArrayVNode }scopedSlots:{default:propsh(span,props.text)},// 插槽名称slot:name-of-slot,// 其他顶级属性key:myKey,ref:myRef},// {String | Array} 子节点[先写一些文字,h(h1,一则头条)]);四、模板 vs 渲染函数4.1 模板写法templatedivclasslist-containerh2v-iftitle{{ title }}/h2ulliv-foritem in items:keyitem.id:class{ active: item.active }clickselect(item){{ item.name }}/li/ul/div/template4.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(itemh(li,{key:item.id,class:{active:item.active},on:{click:()this.select(item)}},item.name)))]);}};五、JSX 语法5.1 JSX 配置需要配置 Babel 插件支持 JSXnpminstallvue/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(divclasscontainerh1{this.msg}/h1p使用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{ethis.inputValuee.target.value}/{/* v-on - onXxx */}button onClick{this.handleClick}点击/button{/* 事件修饰符 - 原生处理 */}button onClick_stop{this.handleClick}阻止冒泡/buttonbutton onClick_prevent{this.handleClick}阻止默认/button{/* 插槽 */}MyComponentdiv slotheader头部/divdiv默认内容/div/MyComponent{/* 作用域插槽 */}MyComponent scopedSlots{{default:({text})span{text}/span}}//div);}};六、函数式组件6.1 什么是函数式组件函数式组件是无状态没有响应式数据且无实例没有 this 上下文的组件。它们只接收 props 并返回 VNode// 函数式组件JSXexportdefault{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.clickcontext.listeners.click(...args);}};returnh(Component,{...context.data,on:listeners},context.children);}};}七、插槽与渲染函数7.1 访问插槽exportdefault{render(h){// 访问默认插槽constdefaultSlotthis.$slots.default;// 访问命名插槽constheaderSlotthis.$slots.header;// 访问作用域插槽constscopedthis.$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(colh(th,col.title)))]),h(tbody,this.data.map(rowh(tr,this.columns.map(colh(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:valthis.$emit(change,field.key,val)}});}));}};九、总结方式适用场景优点缺点模板绝大多数场景直观、声明式、易维护复杂逻辑受限渲染函数高度动态内容完全编程控制可读性较差JSX熟悉 React 的开发者接近 JavaScript 语法需要额外配置函数式组件纯展示组件性能最优无状态、无生命周期建议优先使用模板遇到模板难以表达的场景再考虑渲染函数或 JSX。下一章我们将学习 Vue 项目的构建与工程化掌握 Vue CLI 和 Webpack 配置。十、练习使用渲染函数重写一个v-ifv-for组合的复杂组件用 JSX 实现一个可复用的List组件支持自定义渲染项创建一个函数式组件Heading根据 level 渲染 h1-h6实现一个动态表单组件根据 JSON 配置渲染不同类型的表单项