Vue 渲染函数

1. 简介

本小节我们将介绍 Vue 渲染函数包括什么是渲染函数、虚拟 DOM、如何编写渲染函数。渲染函数一个难点,通常在一些简单的项目中不会使用,在处理一些复杂的业务场景时,使用渲染函数往往可以达到事半功倍的效果

通过本小节的学习我们可以掌握渲染函数的基本用法,如果同学们学完本小节之后对此还不是特别熟悉也没有关系,我们可以先将基础知识融会贯通,然后再对该知识点多加练习,相信同学们一定可以熟练掌握。

2. 基础

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。

让我们用一个简单的例子来学习,这个例子里 render 函数很实用。假设我们要生成一些带锚点的标题标题的尺寸和锚点的地址需要通过属性传递,最终生成如下格式的 DOM 结构:

<h1>
  <a href="#hello-world">
    Hello World!
  </a>
</h1>

同学们肯定觉得这不是很简单吗,于是写下了如下示例:


<!DOCTYPE html>
<html lang="en">
<head>
  <Meta charset="UTF-8">
  <Meta name="viewport" content="width=device-width, initial-scale=1.0">
  <Meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <anchored-heading :level="1" href="#hello-wold">Hello World!</anchored-heading>
    <anchored-heading :level="2" href="#hello-wold">Hello World!</anchored-heading>
    <anchored-heading :level="3" href="#hello-wold">Hello World!</anchored-heading>
  </div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script type="text/x-template" id="anchored-heading-template">
  <h1 v-if="level === 1">
    <a :href="href">
      <slot></slot>
    </a>
  </h1>
  <h2 v-else-if="level === 2">
    <a :href="href">
      <slot></slot>
    </a>
  </h2>
  <h3 v-else-if="level === 3">
    <a :href="href">
      <slot></slot>
    </a>
  </h3>
  <h4 v-else-if="level === 4">
   <a :href="href">
      <slot></slot>
    </a>
  </h4>
  <h5 v-else-if="level === 5">
    <a :href="href">
      <slot></slot>
    </a>
  </h5>
  <h6 v-else-if="level === 6">
    <a :href="href">
      <slot></slot>
    </a>
  </h6>
</script>
<script type="text/javascript">
  Vue.component('anchored-heading', {
    template: '#anchored-heading-template',
    props: {
      level: {
        type: Number,
        required: true
      },
      href: {
        type: String,
        default: ''
      }
    }
  })
  var vm = new Vue({
    el: '#app',
    data() {
    	return {}
    }
  })
</script>
</html>

代码解释:
JS 代码第 2-33 行,我们定义了组件的模板,通过 v-if 根据传入的 level 来控制显示标签元素。
JS 代码第 34-46 行,我们定义了组件 anchored-heading,接收 level 和 href 两个属性参数。
HTML 代码第 2-4 行,使用了组件 anchored-heading

上述代码虽然实现了功能,但是代码冗长,而且在每一个级别的标题中重复书写了 <a :href="href"><slot></slot></a>。那么如何才能消除这些冗余的代码呢?我们来尝试使用 render 函数重写上面的例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <Meta charset="UTF-8">
  <Meta name="viewport" content="width=device-width, initial-scale=1.0">
  <Meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <anchored-heading :level="1" href="#hello-wold">Hello World!</anchored-heading>
    <anchored-heading :level="2" href="#hello-wold">Hello World!</anchored-heading>
    <anchored-heading :level="3" href="#hello-wold">Hello World!</anchored-heading>
  </div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>

<script type="text/javascript">
  Vue.component('anchored-heading', {
    render: function (createElement) {
      return createElement(
        'h' + this.level,   // 标签名称
        
        [createElement('a', {
          attrs: {
            href: this.href
          }
        },this.$slots.default)]
        
      )
    },
    props: {
      level: {
        type: Number,
        required: true
      },
      href: {
        type: String,
        default: ''
      }
    }
  })
  var vm = new Vue({
    el: '#app',
    data() {
    	return {}
    }
  })
</script>
</html>

通过 render 函数编写的组件看起来简单多了!同学们可能暂时还不太理解这段代码的含义,createElement 是什么? 这些参数又表示什么含义?我们先带着这些疑问继续往下学习。

3. 虚拟 DOM

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码

return createElement('h1', this.blogTitle)

createElement 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为 “虚拟节点 (virtual node)”,也常简写它为 “VNode”。“虚拟 DOM” 是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

4. createElement 参数

接下来你需要熟悉的是如何在 createElement 函数中使用模板中的那些功能。这里是 createElement 接受的参数:

createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中属性对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

也就是说,我们通过 createElement 函数来递归创建我们的节点标签

5. 深入数据对象

有一点要注意:正如 v-bind:class 和 v-bind:style 在模板语法中会被特别对待一样,它们在 VNode 数据对象中也有对应的顶层字段。该对象也允许你绑定普通的 HTML attribute,也允许绑定如 innerHTML 这样的 DOM 属性 (这会覆盖 v-html 指令)。

{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML attribute
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM 属性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 属性内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层属性
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}

现在我们再来回顾上述的例子是不是很简单?

6. 小结

本节,我们带大家学习了 Vue 渲染函数的使用方法。主要知识点有以下几点:

  • 使用渲染函数 render 代替 template 模板。
  • 虚拟 DOM 的定义,createElement 函数的使用。