# Vue2.x 的 render

前言

介绍 Vue2.x render 函数及其应用场景

# 1.案例

刷新
全屏/自适应

# 2.约束

所有的组件树中,如果 vNode 是组件或含有组件的 slot,那么 vNode 必须是唯一。以下两个示例是错误的。









 



const Child = {
  render: (h) => {
    return h("p", "text")
  },
}
export default {
  render: (h) => {
    const ChildNode = h(Child)
    return h("div", [ChildNode, ChildNode])
  },
}
1
2
3
4
5
6
7
8
9
10
11


 



{
  render: (h) => {
    return h("div", [this.$slots.default, this.$slots.default])
  }
}
1
2
3
4
5

重复渲染多个组件或元素,可以通过一个循环和工厂函数来解决:

const Child = {
  render: (h) => {
    return h("p", "text")
  },
}

export default {
  render: (h) => {
    const children = Array.apply(null, {
      length: 5,
    }).map(() => {
      return h(Child)
    })
    return h("div", children)
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

对于函数有组件的 slot,复用比较复杂,需要将 slot 的每个子节点克隆一份,例如:

{
  render: (h) => {
    function cloneVNode(vnode) {
      // 递归遍历所有子节点,并克隆
      const clonedChildren =
        vnode.children && vnode.children.map((vnode) => cloneVNode(vnode))
      const cloned = h(vnode.tag, vnode.data, clonedChildren)
      cloned.text = vnode.text
      cloned.isComment = vnode.isComment
      cloned.componentOptions = vnode.componentOptions
      cloned.elm = vnode.elm
      cloned.context = vnode.context
      cloned.ns = vnode.ns
      cloned.isStatic = vnode.isStatic
      cloned.key = vnode.key

      return cloned
    }

    const vNodes = this.$slots.default === undefined ? [] : this.$slots.default
    const clonedVNodes =
      this.$slots.default === undefined
        ? []
        : vNodes.map((vnode) => cloneVNode(vnode))

    return h("div", [vNodes, clonedVNodes])
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

在 Render 函数里创建一个 cloneVNode 的工厂函数,通过递归将 slot 所有子节点都克隆一份,并对 VNode 的关键属性也进行了复制。

深度克隆 slot 并非 Vue.js 内置方法,也没有得到推荐,属于黑科技,在一些特殊的场景才会使用到,正常业务几乎是用不到的。比如 iView 组件库的穿梭框组件 Transfer,就用到这种方法。

它的使用方法是:

<template>
  <Transfer
    :data="data"
    :target-keys="targetKeys"
    :render-format="renderFormat"
  >
    <div :style="{ float: 'right', margin: '5px' }">
      <Button @click="reloadMockData">Refresh</Button>
    </div>
  </Transfer>
</template>
1
2
3
4
5
6
7
8
9
10
11

示例中的默认 slot 是一个 Refresh 按钮,使用者只写了一遍,但在 Transfer 组件中,是通过克隆 VNode 的方法,显示了两遍。如果不这样做,就要声明两个具名 slot,但是左右两个的逻辑可能是完全一样的,使用者就要写两个一模一样的 slot,非常不友好。

# 3.使用场景

  • 1.在 template 中,不允许使用两个相同 slot
<template>
  <div>
    <slot></slot>
    <slot></slot>
  </div>
</template>
1
2
3
4
5
6

解决方案就是上文中讲到的约束,使用一个深度克隆 VNode 节点的方法。

2.在 SSR 环境(服务端渲染),如果不是常规的 template 写法,比如通过 Vue.extend 和 new Vue 构造来生成的组件实例,是编译不过的。如 Alert 组件

// 正确写法
import Alert from "./alert.vue"
import Vue from "vue"

Alert.newInstance = (properties) => {
  const props = properties || {}
  const Instance = new Vue({
    data: props,
    render(h) {
      return h(Alert, {
        props: props,
      })
    },
  })
  const component = Instance.$mount()
  document.body.appendChild(component.$el)
  const alert = Instance.$children[0]
  return {
    add(noticeProps) {
      alert.add(noticeProps)
    },
    remove(name) {
      alert.remove(name)
    },
  }
}
export default Alert
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 在 SSR 下报错的写法
import Alert from "./alert.vue"
import Vue from "vue"

Alert.newInstance = (properties) => {
  const props = properties || {}

  const div = document.createElement("div")
  div.innerHTML = `<Alert ${props}></Alert>`
  document.body.appendChild(div)

  const Instance = new Vue({
    el: div,
    data: props,
    components: { Alert },
  })

  const alert = Instance.$children[0]

  return {
    add(noticeProps) {
      alert.add(noticeProps)
    },
    remove(name) {
      alert.remove(name)
    },
  }
}

export default Alert
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  • 3.在 runtime 版本的 Vue.js 中,如果使用 Vue.extend 手动构造一个实例,使用 template 选项是会报错的,把 template 改成 Render 就可以了。需要注意的是,在开发独立组件时,可以通过配置 Vue.js 版本来使 template 选项可用,但这是在自己的环境,无法保证使用者的 Vue.js 版本,所以对于听过给他人用的组件,是需要考虑兼容 runtime 版本和 SSR 环境的。

  • 4.一个 Vue.js 组件,有一部分内容需要从父级传递来显示,如果是文本之类的,直接通过props就可以了,如果这个内容带有样式或复杂一点的 html 结构,可以使用v-html指令来渲染,父级传递的仍然是一个 HTML Element 字符串,不过它仅仅是能解析正常的 html 节点且有 XSS 风险。当需要最大化程度自定义显示内容是,就需要Render函数,它可以渲染一个完整的 Vue.js 组件。你可能会说,用 slot 不就好了。的确,slot 作用就是做内容分发的,但在一些特殊的组件中,可能 slot 也不行。比如一个表格组件Table,它接收两个 props:列配置 columns 和行数据 data,不过某一列的单元格,不是只将数据显示出来那边简单,可能带有一些复杂的操作,这种场景只用 slot 是不行的,没办法确定是那一列的 slot。这种场景有两种解决方案,其一就是 Render 函数,另一种就是作用域 slot。

# 4.functional + render

# 4.1 render.js

export default {
  functional: true,
  props: {
    render: Function,
  },
  render: (h, ctx) => {
    return ctx.props.render(h)
  },
}
1
2
3
4
5
6
7
8
9

# 4.2 创建组件

<template>
  <div>
    <Render :render="render"></Render>
  </div>
</template>
<script>
import Render form './render.js';
export default {
  components: { Render },
  props: {
    render: Function
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4.3 案例

参考 table 案例

总结

通过对前端组件的分析,需要重点关注组件中易变性对组件封装的影响,它会对组件的可复用性、可扩展性产生很大影响