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

1.Vue2的render函数是干啥的?

terry 1天前 阅读数 18 #Vue
文章标签 Vue2;render函数

很多刚开始学Vue2的同学,一碰到render函数就犯难——明明用template写页面挺顺手,为啥还要学render?它到底在哪些场景能发挥作用?和template比起来有啥优势或不足?这篇文章就用问答的方式,把render函数的门道掰开揉碎讲清楚,帮你真正搞懂它的用法和价值。

简单说,render函数是Vue用来生成虚拟DOM(VNode)的“入口函数”,Vue组件最终要把数据转换成页面上的DOM,这个过程靠的是虚拟DOM的渲染和更新,而render函数就是用来描述“该生成什么样的虚拟DOM”。

它的核心是接收一个叫createElement的参数(大家习惯叫它h,取自hyperscript,意思是生成HTML结构的脚本),然后返回一个VNode对象,举个最基础的例子:

export default {
  render(h) {
    // h(标签名/组件, 属性对象, 子节点)
    return h('div', { class: 'hello' }, '你好,这是render渲染的内容')
  }
}

你可以把它理解成“用JS代码来描述DOM结构”,和用HTML风格的template写结构是同一个目的,但方式更偏逻辑化、编程化。

render和template有啥本质区别?

从Vue的编译流程来看,template是“半成品”——Vue会先把template转换成AST(抽象语法树),再优化、生成最终的render函数;而直接写render函数是“成品”,跳过了“template→AST→render”的编译步骤,自己手动构建VNode。

举个场景对比:如果做一个简单的静态页面,比如展示标题和一段文字,用template写<div><h1>标题</h1><p>内容</p></div>更直观;但如果要做高度动态的渲染(比如根据用户权限渲染不同组件、根据后端JSON配置生成页面),render函数的优势就体现出来了——因为它本质是JS逻辑,可以写循环、判断、调用函数,完全用JS控制渲染逻辑。

再举个实际例子:假设表格的列要根据用户权限显示,用template得写一堆v-if嵌套,代码会很臃肿;但用render函数,可以在JS里写一个循环,遍历权限列表,动态生成对应的列VNode,逻辑更集中,维护起来也方便。

怎么上手写第一个render函数?

写render函数就像“用JS拼积木”,核心是掌握h函数的用法。h函数接收三个参数:

  1. 第一个参数:要渲染的标签名(div'、'button')或者组件选项(比如导入的MyComponent);
  2. 第二个参数:数据对象,用来配置标签的属性、事件、样式等(后面会详细讲);
  3. 第三个参数:子节点,可以是字符串、数组(数组里每个元素得是VNode,也就是h函数的返回值)。

先写个最简单的组件练手:

export default {
  name: 'RenderDemo',
  render(h) {
    // 渲染一个带class和点击事件的按钮
    return h('button', {
      class: 'btn-primary',
      on: {
        click: this.handleClick
      }
    }, '点我试试')
  },
  methods: {
    handleClick() {
      alert('render函数里的点击事件触发啦~')
    }
  }
}

对比template写法,是不是感觉像“用JS代替HTML写结构”?但别慌,多写几个例子就能找到规律,比如要渲染带子元素的结构:

render(h) {
  return h('div', { class: 'card' }, [
    h('h2', '卡片标题'),
    h('p', '卡片内容,用render函数嵌套生成')
  ])
}

这里子节点是数组,每个元素都是h函数生成的VNode,和template里的子标签对应。

h函数的参数细节得注意啥?

第二个参数(数据对象)是最容易踩坑的地方,得搞清楚它的结构:

  • class:支持字符串('class-a class-b')、数组(['class-a', 'class-b'])、对象({ 'class-a': true, 'class-b': false });
  • style:必须是对象, color: 'red', fontSize: '14px' };
  • attrs:设置HTML原生属性, id: 'my-div', title: '提示文字' };
  • props:给子组件传props,比如子组件有个prop叫'user',就写{ props: { user: this.currentUser } };
  • on:绑定事件,注意区分自定义事件原生事件,如果是普通DOM元素(如button),on里的click就是原生点击;如果是自定义组件,on里的事件是组件 emits 的自定义事件;
  • nativeOn:专门给自定义组件绑定原生DOM事件(绑定在组件的根元素上),lt;MyButton>组件根元素是button,要监听原生click,就写{ nativeOn: { click: this.handleNativeClick } }。

举个综合例子,渲染一个带样式、属性、事件的自定义组件:

import MyInput from './MyInput.vue'
<p>export default {
render(h) {
return h(MyInput, {
class: ['input-wrapper', { 'is-focus': this.isFocus }],
style: { marginTop: '10px' },
attrs: { placeholder: '请输入内容' },
props: { value: this.inputValue },
on: {
input: this.handleInput, // MyInput emits的input事件
customEvent: this.handleCustom // 自定义事件
},
nativeOn: {
click: this.handleRootClick // 绑定MyInput根元素的原生click
}
})
},
// ...数据和方法省略
}

子节点如果是循环生成的,一定要加key!和template里的v-for加key一样重要,否则Vue的diff算法无法正确识别节点,会导致更新异常,比如渲染列表:

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

render里怎么处理组件复用和动态组件?

动态切换组件是很常见的需求,比如根据用户选择显示不同的表单、不同的页面模块,用render函数做这件事,比template的<component :is="xxx">更灵活,因为能在JS里写任意逻辑。

举个例子:做一个多类型表单切换组件,用户选“登录”显示LoginForm,选“注册”显示RegisterForm,代码可以这样写:

import LoginForm from './LoginForm.vue'
import RegisterForm from './RegisterForm.vue'
<p>export default {
data() {
return {
formType: 'login', // 切换标识:login/register
formData: {} // 表单数据
}
},
render(h) {
let CurrentForm
if (this.formType === 'login') {
CurrentForm = LoginForm
} else {
CurrentForm = RegisterForm
}
// 给子组件传props,监听事件
return h(CurrentForm, {
props: { data: this.formData },
on: {
submit: this.handleSubmit
}
})
},
methods: {
handleSubmit(data) {
console.log('表单提交数据:', data)
}
}
}

这种方式的好处是,组件的选择逻辑完全在JS里,可以加更复杂的判断(比如结合用户权限、后端配置),而不用在template里写一堆v-if/v-show。

如果是更复杂的“动态组件列表”,比如后端返回一个组件名数组,前端根据名字渲染对应的组件,也可以用对象映射的方式:

import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
import ComponentC from './ComponentC.vue'
<p>const componentMap = {
A: ComponentA,
B: ComponentB,
C: ComponentC
}</p>
<p>export default {
data() { return { componentList: ['A', 'B', 'C'] } },
render(h) {
return h('div', {}, this.componentList.map(name => {
const Comp = componentMap[name]
return h(Comp)
}))
}
}

render函数能结合JSX用吗?Vue2里怎么配置JSX?

能!而且用JSX写render函数,会比纯h函数调用更直观,特别适合习惯React开发的同学,Vue2要支持JSX,需要装一个Babel插件:babel-plugin-transform-vue-jsx(现在也可以用@vue/babel-plugin-jsx,但Vue2更推荐前者)。

配置好后,render函数可以写成JSX风格:

export default {
  data() { return { list: [{ id: 1, name: '张三' }, { id: 2, name: '李四' }] } },
  render() {
    return (
      <div class="jsx-container">
        <h1>这是JSX写的标题</h1>
        <button onClick={this.handleClick}>点我</button>
        <ul>
          {this.list.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      </div>
    )
  },
  methods: {
    handleClick() {
      alert('JSX里的点击事件~')
    }
  }
}

JSX本质是h函数的语法糖,写出来的代码和HTML结构几乎一样,大大降低了render函数的学习成本,但要注意:Vue的JSX和React的JSX有细节差异(比如事件绑定是onClick还是@click?不,Vue JSX里是onClick,因为最终转成h函数的on参数;而Vue模板里是@click),所以得习惯Vue JSX的规则。

哪些真实项目场景必须用render函数?

不是所有项目都要用到render,但碰到这些场景,render几乎是“最优解”甚至“唯一解”:

(1)通用组件库开发

比如Element UI、Ant Design Vue这些组件库,很多复杂组件(如Table、Select、Tree)的自定义渲染逻辑都是用render实现的,以Table组件为例,用户需要自定义列的内容(比如某一列显示头像+用户名),组件库会暴露render函数的插槽(slot-scope),让用户能在JS里控制渲染。

(2)低代码/无代码平台

这类平台的核心逻辑是“根据后端返回的JSON配置生成页面”,比如后端给一个组件数组:[{ type: 'button', text: '提交', events: { click: 'handleSubmit' } }, { type: 'input', placeholder: '用户名' }],前端就需要用render函数循环解析这个数组,动态生成对应的VNode。

(3)复杂条件渲染优化

如果template里的v-if、v-for嵌套了三四层,逻辑绕来绕去,维护起来像“迷宫”,这时把渲染逻辑抽到render函数里,用JS的if、for、函数调用等方式组织代码,可读性和可维护性会大幅提升。

(4)精细控制虚拟DOM

虽然不常见,但某些性能优化场景下,需要在渲染前修改VNode的结构(比如批量修改属性、动态注入样式),这时直接操作render返回的VNode,比在template里折腾指令更高效。

用render容易踩哪些坑?怎么避?

写render函数时,这些“雷区”要特别注意:

(1)响应式依赖丢失

Vue的响应式是靠“依赖收集”实现的,如果在render函数里用了某个数据,但这个数据没在data里声明,或者是后来动态赋值的,Vue就无法追踪变化,导致数据更新了页面却不刷新。

避坑方法:所有在render里用的数据,都要在data里预先声明,或者用计算属性。

// 错误写法:data里没声明,后来赋值的
export default {
  render(h) {
    return h('div', {}, this.msg) // this.msg是后来赋值的,非响应式
  },
  mounted() {
    this.msg = '新内容' // 页面不会更新
  }
}
<p>// 正确写法:data里声明msg
export default {
data() { return { msg: '' } },
render(h) {
return h('div', {}, this.msg)
},
mounted() {
this.msg = '新内容' // 页面会更新
}
}

(2)子节点key缺失

和template的v-for一样,render里循环生成子节点时,必须给每个子节点加key,否则Vue的diff算法无法正确识别节点,导致更新时重复渲染、事件丢失等问题。

避坑方法:循环生成子节点时,一定要在数据对象里加key

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

(3)事件绑定混淆

自定义组件的事件、原生DOM事件容易搞混。

  • 普通DOM元素(div、button等):用on: { click: ... }绑定原生事件;
  • 自定义组件:用on: { 自定义事件名: ... }绑定组件 emits 的事件;如果要绑定组件根元素的原生事件,用nativeOn: { click: ... }

举个错误例子:给自定义组件<MyButton>绑定原生click,却用了on:

// 错误:MyButton是自定义组件,on里的click会被当作自定义事件,而不是原生事件
h(MyButton, { on: { click: this.handleClick } })
<p>// 正确:用nativeOn绑定根元素的原生click
h(MyButton, { nativeOn: { click: this.handleClick } })

(4)样式格式错误

class和style的格式不对,会导致样式不生效,class可以是字符串、数组、对象;style必须是对象,属性名用驼峰(如fontSize)或短横线(如'font-size',但更推荐驼峰)。

错误例子:style用了字符串:

// 错误:style必须是对象
h('div', { style: 'color: red;' })
<p>// 正确
h('div', { style: { color: 'red', fontSize: '14px' } })

render函数和Vue的编译过程有啥关系?

Vue的模板编译

版权声明

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

发表评论:

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

热门