Skip to content

渲染函数

渲染函数是组成完整的Vue响应式渲染系统的另一半。

Vue会将template模板编译成渲染函数,然后通过调用渲染函数生成虚拟DOM(VNode),最后将虚拟DOM渲染成真实DOM。

Vue会监听状态的变化,当状态发生变化时,会重新调用渲染函数生成新的虚拟DOM,然后通过对比新旧虚拟DOM的差异,最终只更新需要更新的真实DOM节点,从而实现高效的DOM更新。

虚拟DOM

所谓的虚拟DOM,其实本质是一个JavaScript对象,用来描述真实DOM的结构。 例如:

js
const VNode = {
  tag: 'div',
  props: {
    class: 'container'
  },
  children: [
    {
      tag: 'h1',
      props: {
        class: 'title'
      },
      children: 'Hello World'
    }
  ]
}

该虚拟DOM描述了一个div元素,其中包含一个h1元素,h1元素中包含文本内容"Hello World"。 相当于:

html
<div class="container">
  <h1 class="title">Hello World</h1>
</div>

为什么要用虚拟DOM?

js
// 真实DOM
document.createElement('div')
// [Object HTMLDivElement]

// 虚拟DOM
vm.$createElement('div')
// { tag: 'div', data: { attrs: {}, ... }, children: [] }

我们知道一个DOM元素有很多属性,比如class、style、id、data-*等,如果直接操作DOM,那么就需要逐个设置这些属性,非常繁琐,且浪费资源。而虚拟DOM则将这些属性封装成一个JavaScript对象,只需要操作JavaScript对象,就可以实现DOM的更新,大大简化了操作。

注意:常见的误解是虚拟DOM让框架变快了,其实虚拟DOM只是让框架变得更简单,更高效,让开发者更方便地操作DOM,从而提高开发效率。 其实虚拟DOM只是解决了原始DOM局限性的一个方法,它提供了以声明方式去构成你想要的DOM结构。 虚拟DOM的另一个好处,它把渲染逻辑从真实DOM抽离出来,我们先计算差异,然后将这些更新应用到DOM上,这样就可以避免频繁操作DOM,提高性能。

响应性和渲染函数

image-20250610110940213

组件更新原理

我们结合前面学习的响应性和渲染函数,来理解组件更新的过程。

  1. template模版被编译成渲染函数
  2. 数据被转化成响应式数据,它的getter/setter方法被重写
  3. 每个组件都会有一个Watcher用于收集依赖、清理依赖、通知修改等;当数据发生变化时,setter方法被触发,通知依赖该数据的组件进行更新
  4. 组件的渲染函数被重新调用,生成新的虚拟DOM
  5. Vue将新旧虚拟DOM进行对比,找出差异
  6. Vue将差异应用到真实DOM上,完成组件的更新

渲染函数的使用

js
export default {
  render(h) {
    return h('div', {}, [])
  }
}

hyperscript函数(h函数)是Vue渲染函数的核心,它接收三个参数:

  1. 第一个参数是标签名,可以是字符串或组件对象
  2. 第二个参数是数据对象,可选的,里面可以嵌套propsattrsstyleclass、DOM属性、事件等
  3. 第三个参数是子节点,可以是字符串、数组(可包含更多的h函数或字符串)

例如:

js
h('div', 'some text')

h('div', { class: 'foo' }, 'some text')

h('div', { ... }, [
  'some text',
  h('span', 'bar')
])

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

练习1

使用render函数,动态渲染标签。 Example组件如下:

html
<div>
  <h1>0</h1>
  <h2>1</h2>
  <h3>2</h3>
</div>

编写:

html
<script src="./node_modules/vue/dist/vue.min.js"></script>

<div id="app">
  <Example :tags="['h1', 'h2', 'h3']"></Example>
</div>

<script>
  Vue.component('Example', {
    // TODO:补充完整
  })
  const vm = new Vue({
    el: '#app'
  })
</script>

答案:

html
<script src="./node_modules/vue/dist/vue.min.js"></script>

<div id="app">
  <Example :tags="['h1', 'h2', 'h3']"></Example>
</div>

<script>
  Vue.component('Example', {
    props: ['tags'],
    render(h) {
      return h('div', this.tags.map((tag, index) => h(tag, index)))
    }
  })
  const vm = new Vue({
    el: '#app'
  })
</script>

练习2

动态渲染不同的组件

html
<script src="./node_modules/vue/dist/vue.min.js"></script>

<div id="app">
  <Example :ok="ok"></Example>
<div>

<script>
  const Foo = {
    // TODO:编写完整
  }
  const Bar = {
    // TODO:编写完整
  }

  Vue.component('Example', {
    // TODO:编写完整
  })
  const vm = new Vue({
    el: '#app'
  })
</script>

答案:

html

<script src="./node_modules/vue/dist/vue.min.js"></script>

<div id="app">
  <Example :ok="ok"></Example>
  <button @click="ok = !ok">toggle</button>
<div>

<script>
  const Foo = {
    render(h) {
      return h('div', 'foo')
    }
  }
  const Bar = {
    render(h) {
      return h('div', 'bar')
    }
  }

  Vue.component('Example', {
    props: ['ok'],
    render(h) {
      return this.ok ? h(Foo) : h(Bar)
    }
  })
  const vm = new Vue({
    el: '#app',
    data: {
      ok: true
    }
  })
</script>

接下来,我们进入下一个话题:插件编写