换一种思路写Vue:Render 函数与 Functional Render

文章探讨了Vue中VirtualDOM的作用,包括抽象渲染过程和跨平台能力,并指出其并非性能优势。Render函数提供了更大的灵活性,但复杂情况下可能影响可读性和维护性。函数式组件无状态,适用于简单场景和高阶组件的构建,能提高渲染效率。
摘要由CSDN通过智能技术生成

虚拟 Virtual DOM

一般来说,写 Vue 组件时,模板都是写在 <template> 内的,但它并不是最终呈现的内容,template 只是一种对开发者友好的语法,能够一眼看到 DOM 节点,容易维护,在 Vue 编译阶段,会解析为 Virtual DOM

正常的 DOM 节点在 HTML 中是这样的:

<div id="app">
  <p>内容</p>
</div>

Virtual DOM 创建的 JavaScript 对象一般会是这样的:

const vNode = { 
    tag: 'div', 
    data: { 
        id: 'app' 
    }, 
    children: [ // p 节点 ] 
}

vNode 对象通过一些特定的选项描述了真实的 DOM 结构。

为什么一定要设计 vnode 这样的数据结构呢?它有什么优势呢?

首先是抽象。引入 vnode,可以把渲染过程抽象化,从而使得组件的抽象能力得到提升。

其次是可跨平台。因为对于 patch vnode 的过程,不同的平台可以有自己的实现,再基于 vnode 做服务端渲染、weex平台渲染 或 小程序平台的渲染就变得容易得多。

不过这里要特别注意,在浏览器端使用vnode 并不意味着不用操作 DOM 了。很多人误以为 vnode 的性能一定比手动操作原生DOM 好,这其实是不一定的。

这种基于vnode实现的 MVVM 框架,每次组件渲染生成 vnode的过程,会有一定的耗时,大组件尤其如此。

举个例子,对于一个1000行 x 10列的 table 组件,组件渲染生成 vnode 的过程会遍历1000行 x 10列去创建内部的 cell vnode,整个耗时会比较长。再加上挂载 vnode 生成 DOM 的过程也会有一定的耗时,所以当我们更新组件的时候,用户会感觉到明显的卡顿。

虽然 diff 算法在减少 DOM 操作方面足够优秀,但最终还是免不了操作DOM,因此性能并不是vnode的优势所在。

Render 函数

对于大部分场景,使用 template 足以应付,但如果想完全发挥 JavaScript 的编程能力,或在一些特定场景下,需要使用 Vue 的 Render 函数。

使用 Render 函数开发 Vue.js 组件是要比 template 困难的,原因在于 Render 函数返回的是一个 JS 对象,没有传统 DOM 的层级关系,配合上 if、else、for 等语句,将节点拆分成不同 JS 对象再组装,如果模板复杂,那一个 Render 函数是难读且难维护的。

所以,绝大部分组件开发和业务开发,直接使用 template 语法就可以了,并不需要特意使用 Render 函数,那样只会增加负担,同时也放弃了 Vue.js 最大的优势。

来看一组 templateRender 写法的对照:

template 写法

<template>
  <div id="main" class="container" style="color: red">
    <p v-if="show">内容 1</p>
    <p v-else>内容 2</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      show: false
    }
  }
}
</script>

Render 函数写法


export default {
  data () {
    return {
      show: false
    }
  },
  render: (h) => {
    let childNode;
    if (this.show) {
      childNode = h('p', '内容 1');
    } else {
      childNode = h('p', '内容 2');
    }
    
    return h('div', {
      attrs: {
        id: 'main'
      },
      class: {
        container: true
      },
      style: {
        color: 'red'
      }
    }, [childNode]);
  }
}

这里的 h,即 createElement,是 Render 函数的核心。可以看到,template 中的 v-if / v-else 等指令,都被 JS 的 if / else 替代了,那 v-for 自然也会被 for 语句替代。

h 有 3 个参数,分别是:

1、 要渲染的元素或组件,可以是一个 html 标签、组件选项或一个函数(不常用),该参数为必填项。

// html 标签
h('div');
// 组件选项
import DatePicker from '../component/date-picker.vue';
h(DatePicker);

2、 对应属性的数据对象,比如组件的 props、元素的 class、绑定的事件、slot、自定义指令等,该参数是可选的,参考数据对象

3、子节点,可选,String 或 Array,它同样是一个 h

[
  '内容',
  h('p', '内容'),
  h(Component, {
    props: {
      someProp: 'foo'
    }
  })
]

下面通过一个例子来说明render函数的用处,假设我们要生成一些带锚点的标题:

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

对于上面的 HTML,你决定这样定义组件接口:

<anchored-heading :level="1">Hello world!</anchored-heading>

当开始写一个只能通过 level prop 动态生成标题 (heading) 的组件时,你可能很快想到这样实现:

<template>
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-else-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-else-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-else-if="level === 6">
    <slot></slot>
  </h6>
</template>

<script>
export default {
  props: {
    level: Number
  }
}
</script>

这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了 <slot></slot>

虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render 函数重写上面的例子:

<script>
export default {
  render(createElement) {
    return createElement(
      'h' + this.level, // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
}
</script>

这样代码精简很多。

Functional Render

上面创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。一个函数式组件就像这样:

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})

如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:

<template functional>
</template>

再添加 functional: true 之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context 参数,并将 this.$slots.default 更新为 context.children,然后将 this.level 更新为 context.props.level

export default {
  functional: true,
  props: {
    level: {
      type: Number,
      required: true
    }
  },
  render(createElement, context) {
    return createElement('h' + context.props.level, context.children)
  }
}

因为函数式组件只是函数,没有状态,不需要经历数据响应式的初始化过程,所以渲染开销也低很多。

由于函数式组件无状态和无实例(this),我们就可以把它用作高阶组件。所谓高阶,就是可以生成其他组件的组件。

在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:

  • 程序化地在多个组件中选择一个来代为渲染;
  • 在将 childrenpropsdata 传递给子组件之前操作它们。

下面是一个 smart-list 组件的例子,它能根据传入 prop 的值来代为渲染更具体的组件:

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }

Vue.component('smart-list', {
  functional: true,
  props: {
    items: {
      type: Array,
      required: true
    },
    isOrdered: Boolean
  },
  render: function (createElement, context) {
    function appropriateListComponent () {
      var items = context.props.items

      if (items.length === 0)           return EmptyList
      if (typeof items[0] === 'object') return TableList
      if (context.props.isOrdered)      return OrderedList

      return UnorderedList
    }

    return createElement(
      appropriateListComponent(),
      context.data,
      context.children
    )
  }
})

从这个例子中,根据传入的props来选择渲染那个组件,smart-list组件其实扮演了一个中间组件的角色。

下面再看一个函数式组件作为高阶组件的例子,来加深对函数式组件的理解。

当某个组件的内容需要用户自定义的时候,一般想到都是采用slot插槽的方式来实现,其实我们也可以使用Functional Render来实现,而且它能实现slot不能完成的功能。

1、首先创建一个函数化组件 render.js

// render.js
export default {
  functional: true,
  props: {
    render: Function
  },
  render: (h, ctx) => {
    return ctx.props.render(h);
  }
};

它只定义了一个 props:render,格式为 Function,因为是 functional,所以在 render 里使用了第二个参数 ctx 来获取 props。这是一个中间文件,并且可以复用,其它组件需要这个功能时,都可以引入它

2、创建组件:

<template>
  <div>
    <Render :render="render"></Render>
  </div>
</template>
<script>
  import Render from './render.js';
  
  export default {
    components: { Render },
    props: {
      render: Function
    }
  }
</script>

3、使用上面的 my-component 组件:

<template>
  <div>
    <my-component :render="render"></my-component>
  </div>
</template>
<script>
  import myComponent from '../components/my-component.vue';
  
  export default {
    components: { myComponent },
    data () {
      return {
        render: (h) => {
          return h('div', {
            style: {
              color: 'red'
            }
          }, '自定义内容');
        }
      }
    }
  }
</script>

这里的 render.js 因为只是把用户自定义的 Render 内容进行了中转,并无其它用处,所以用了 Functional Render

就此例来说,完全可以用 slot 取代 Functional Render,那是因为只有 render 这一个 prop。如果示例中的 <Render> 是用 v-for 生成的,也就是多个时,用一个 slot 是实现不了的,那时用 Render 函数就很方便了。

总结

本文首先介绍了虚拟DOM这个数据结构,它对渲染过程进行抽象,为后面的diff算法提供了基础。它还有一个重要的作用是可跨平台,因为它本质是对 HTML 标签的JS化,至于要运行在那个平台,直接转化成对应平台特有的标签语法就可以了。

通常我们写业务组件都是采用template的形式来写,但是在某些高度灵活且需要用户自定义的场景下使用 Render 函数更加方便与简洁。尽管在某些场景下写slot也能实现自定义,但是它也有很大的局限性,此时就可以使用 Render 函数了。

对于一些没有数据状态的组件,我们完全可以使用函数式组件来写,函数式组件因为没有状态,可以快速的渲染出内容。通过,可以用函数式组件作为包裹组件,即高阶组件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哎,好难

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值