专栏首页前端知否Vue.js render函数那些事儿

Vue.js render函数那些事儿

大多时候,我会使用template, vue单文件去渲染组件。虽然知道Vue中有个render函数,但却很少在项目中去主动使用它。使用最多的地方是在使用一些UI框架的时候,比如iview table中的按钮操作,会使用到render函数。另外平时在阅读一些Vue UI框架源码的时候,也时常能遇到使用render函数的地方,这也激发了自己研究学习的欲望。如果你也感兴趣,那就继续阅读吧。

在本文中,会有如下内容:

  • 什么是Vue render函数
  • Vue编译器如何处理render函数
  • 创建一个组件
  • 在render函数中使用指令
  • Vue渲染函数中的事件绑定
  • 模板覆盖的实际用例

让我们开始吧!

什么是Vue render函数

Vue.js模板功能强大,几乎可以满足我们在应用程序中所需的一切。但是,有一些场景下,比如基于输入或插槽值创建动态组件,render函数可以更好地满足这些用例。

那些来自React世界的开发者可能对render函数非常熟悉。通常在JSX中使用它们来构建React组件。虽然Vue渲染函数也可以用JSX编写,但我们将继续使用原始JS,有助于我们可以更轻松地了解Vue组件系统的基础。。

每个Vue组件都实现了一个render函数。大多数时候,该函数将由Vue编译器创建。当我们在组件上指定模板时,该模板的内容将由Vue编译器处理,编译器最终将返回render函数。渲染函数本质上返回一个虚拟DOM节点,该节点将被Vue在浏览器DOM中渲染。

现在又引出了虚拟DOM的概念,"虚拟DOM到底是什么?"

虚拟文档对象模型(或"DOM")允许Vue在更新浏览器之前在其内存中渲染组件。这使一切变得更快,同时也避免了DOM重新渲染的高昂成本。因为每个DOM节点对象包含很多属性和方法,因此使用虚拟DOM预先在内存进行操作,可以省去很多浏览器直接创建DOM节点对象的开销。

Vue更新浏览器DOM时,会将更新的虚拟DOM与上一个虚拟DOM进行比较,并仅使用已修改的部分更新实际DOM。这意味着更少的元素更改,从而提高了性能。Render函数返回虚拟DOM节点,在Vue生态系统中通常称为VNode,该接口是允许Vue在浏览器DOM中写入这些对象的接口。它们包含使用Vue所需的所有信息。Vue的下一版本将包含一个全新的虚拟DOM实现,该实现将比目前更快。 React,Riot,Inferno等许多其他框架也使用虚拟DOM的概念。

我们可以在任何Vue组件中实现Vue render函数。同样,由于Vue的数据响应性,每当组件的数据得到更新时,都会再次调用render函数。

这是一个简单的示例,说明如何直接使用组件中的render函数去渲染h1标签:

new Vue({
  el: '#app',
  render(createElement) {
    return createElement('h1', 'Hello world');
  }
});

有一些内置组件可以利用渲染函数的功能,例如transitionkeep-alive。这些组件直接在渲染函数中操纵VNode。如果Vue没有提供这个函数特性,这些功能将无法实现。

Vue编译器如何搭配render函数?

大多数时候,Vue渲染函数将在项目构建期间由Vue编译器进行编译(例如,使用Webpack)。因此,编译器不会最终出现在您的生产代码中,从而减小了包的体积。这就是为什么当您使用"单个文件组件"时,除非我们确实需要/想要,否则您实际上不需要使用render函数。

但是,如果我们想在代码中使用编译器,则可以使用带有编译器的Vue版本。简而言之,我们正在使用Vue编译器来编译自定义模板。假设我们在做一个电商项目,那么可以将其注入购物车,从而可以拥有更多的控制。

我们编写了一个实现自定义渲染功能的组件,该功能可获取用户创建的模板并替换我们的默认模板。

这是一个如何使用编译器将模板字符串编译为渲染函数的快速示例:

const template = `
  <ul>
    <li v-for="item in items">
     {{ item }}
    </li>
  </ul>`;

const compiledTemplate = Vue.compile(template);

new Vue({
  el: '#app',
  data() {
    return {
      items: ['Item1', 'Item2']
    }
  },
  render(createElement) {
    return compiledTemplate.render.call(this, createElement);
  }
});

如您所见,编译器将返回一个对象(compiledTemplate),其中包含准备使用的render函数。

创建一个组件

具有渲染功能的组件没有模板标记或属性。相反,他们定义了一个称为render的函数,该函数接收一个createElement(renderElement:String | Component,define:Object,children:String | Array)参数(由于某种原因,通常别名为h,归咎于JSX)并返回使用该函数创建的元素。其他一切保持不变。

export default {
  data() {
    return {
      isRed: true
    }
  },

  /*
   * 和下边使用template相同
   * <template>
   *   <div :class="{'is-red': isRed}">
   *     <p>Example Text</p>
   *   </div>
   * </template>
   */
  render(h) {
    return h('div', {
      'class': {
        'is-red': this.isRed
      }
    }, [
      h('p', 'Example Text')
    ])
  }
}

在render函数中使用指令

Vue模板具有各种便捷功能,以便向模板添加基本逻辑和绑定功能。渲染函数无法访问它们。取而代之的是,它们必须以纯Javascript实现,对于大多数指令而言,这是相当简单的。

v-if

这个很简单。除了使用v-if之外,还可以在createElement中调用普通的if(expr)语句。

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

这些都可以在渲染函数中用JavaScript的if/else 和 map来重写:

props: ['items'],
render: function (h) {
  if (this.items.length) {
    return h('ul', this.items.map(function (item) {
      return h('li', item.name)
    }))
  } else {
    return h('p', 'No items found.')
  }
}

v-for

v-for可以使用for-of,Array.map,Array.filter等多种迭代方法中的任何一种来实现。我们可以将它们以非常有趣的方式组合在一起,以实现过滤或分割数据。

例如,我们可以替换

<template>
  <ul>
    <li v-for="item of list">
      
    </li>
  </ul>
</template>

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

v-model

要记住的一件事是,v-model就是绑定属性为value(还可以是其他属性),只要触发input事件并设置data属性的简写形式。不幸的是,渲染函数没有这么简写。我们必须自己实现它,如下所示。

render(h) {
  return h('input', {
    domProps: {
      value: this.myBoundProperty
    },
    on: {
      input: e => {
        this.myBoundProperty = e.target.value
      }
    }
  })
}

等效于:

<template>
  <input :value="myBoundProperty" @input="myBoundProperty = $event.target.value"/>
</template>

<template>
  <input v-model="myBoundProperty"/>
</template>

v-bind

属性和属性绑定以attrs,props和domProps(类似于value和innerHTML之类)的形式放置在元素定义中。

render(h) {
  return h('div', {
    attrs: {
      // <div :id="myCustomId">
      id: this.myCustomId
    },

    props: {
      // <div :someProp="someValue">
      someProp: this.someValue
    },

    domProps: {
       // <div :value="someValue">
      value: this.someValue
    }
  });
}

附带说明一下,类和样式绑定直接在定义的根进行处理,而不是作为attrs,props或domProps处理。

render(h) {
  return h('div', {
    // "class" 是JS中的保留字, 所以需要引号包起来.
    'class': {
      myClass: true,
      theirClass: false
    },

    style: {
      backgroundColor: 'green'
    }
  });
}

v-on

事件处理程序也可以直接在on(或nativeOn,与组件的v-on.native效果相同)中直接添加到元素定义中。

render(h) {
  return h('div', {
    on: {
      click(e) {
        console.log('我点击了!')
      }
    }
  });
}
  1. 修饰符可以在处理程序内部实现:
  • .stop -> e.stopPropagation()
  • .prevent -> e.preventDefault()
  • .self -> if (e.target !== e.currentTarget) return
  1. 键盘修饰符
  • .[TARGET_KEY_CODE] -> if (event.keyCode !== TARGET_KEY_CODE) return
  • .[MODIFIER] -> if (!event.MODIFIERKey) return

插槽

可以通过this.$slots作为createElement()节点的数组来访问插槽。

<div id="app">
<myslot>
    <div slot="slot1">this is slot1</div>
    <div slot="slot2">This is slot2 </div>
</myslot>

<script>
var app = new Vue({
  el: '#app'
})
</script>
Vue.component('my-slot', {
  render: function(h) {
    const child = h('div', {
      domProps: {innerHTML: 'this is child'}
    });

    return h(
      'div',
      [
        child,
        this.$slots.slot1,
        this.$slots.slot2
      ]
    )
  }
})

作用域插槽存储在this.$scopedSlots[scope](props:object)中,作为返回createElement()节点数组的函数。

<div id="app">
<myslot>
    <template scope="props">
        <div>{{props.text}}</div>
    </template>
</myslot>

<script>
var app = new Vue({
  el: '#app'
})
</script>
Vue.component('my-slot', {
  render: function(h) {
    const child = h('div', {
      domProps: {innerHTML: 'this is child'}
    });

    return h(
      'div',
      [
        child,
        this.$scopedSlots.default({
          text: 'hello scope'
        })
      ]
    )
  }
})

Vue渲染函数中的事件绑定

createElement函数可以接收称为数据对象的参数。该对象可以具有多个属性,这些属性与我们在标准模板中使用的v-bind:on等指令等效。这是带有按钮的简单计数器组件的示例,该按钮可以增加点击次数。

new Vue({
  el: '#app',
  data() {
    return {
      clickCount: 0,
    }
  },
  methods: {
    onClick() {
      this.clickCount += 1;
    }
  },
  render(h) {
    const button = h('button', {
      on: {
        click: this.onClick
      }
    }, 'Click me');
    
    const counter = h('span', [
      'Number of clicks:',
      this.clickCount
    ]);
    
    return h('div', [
      button, counter
    ])
  }
});

但是数据对象不限于事件绑定!我们也可以像使用v-bind:class指令一样将CSS类应用于元素。

new Vue({
  el: '#app',
  data() {
    return {
      clickCount: 0,
    }
  },
  computed: {
    backgroundColor() {
      return {
        'pink': this.clickCount%2 === 0,
        'green': this.clickCount%2 !== 0, 
      };
    }
  },
  methods: {
    onClick() {
      this.clickCount += 1;
    }
  },
  render(h) {
    const button = h('button', {
      on: {
        click: this.onClick
      }
    }, 'Click me');
    
    const counter = h('span', {
      'class': this.backgroundColor,
    }, [
      'Number of clicks:',
      this.clickCount
    ]);
    
    return h('div', [
      button, counter
    ])
  }
});

模板覆盖的实际用例

我认为了解Vue的幕后工作非常有趣。要知道是否能够最有效地使用工具,唯一的方法是确切地了解它的工作方式。

这并不是说我们应该开始将所有模板都转换为render函数,但是有时它们可以派上用场,所以我们至少应该知道如何使用它们。

在上面的示例中,我展示了如何在组件中使用自定义render函数,该函数允许我们的某些组件可重写。

首先,让我们创建初始模板。

<div id="app">
  <heading>
    <span>Heading title is: {{ title }}</span>
  </heading>
</div>

在将要安装Vue应用程序的div内,我们编写一个自定义模板。接下来,我们希望模板覆盖我们将要创建的标题组件的默认版本。

我们要做的第一件事是遍历自定义模板,并使用Vue编译器对其进行预编译

const templates = [];
const templatesContainer = document.getElementById('app');

// 我们遍历每个自定义模板并对其进行预编译
for (var i = 0; i < templatesContainer.children.length; i++) {
  const template = templatesContainer.children.item(i);
  templates.push({
    name: template.nodeName.toLowerCase(),
    renderFunction: Vue.compile(template.innerHTML),
  });
}

然后,让我们创建标题组件:

Vue.component('heading', {
  props: ['title'],
  template: `
    <overridable name="heading">
      <h1>
        {{ title }}
      </h1>
    </overridable>`
});

现在,它只是一个简单的组件,具有一个名为title的属性。默认模板将渲染带有标题的h1。我们将用随后创建的overridable组件包装该组件。

这是我们将使用自定义渲染功能的地方。

Vue.component('overridable', {
  props: ['name'],
  render(createElement) {
    // 我们使用在初始化应用程序时创建的模板数组
    const template = templates.find(x => x.name === this.name);

    // 当没有自定义模板时,我们将使用默认插槽返回默认内容。
    if (!template) {
      return this.$slots.default[0];
    }
    
    // 使用预编译的render函数
    return template.renderFunction.render.call(this.$parent, createElement);
  }
});

然后,让我们挂载Vue应用程序:

new Vue({
  el: '#app',
  template: `<heading title="Hello world"></heading>`
});

在此示例中,我们可以看到使用了默认模板,因此它是一个标准的h1标签。

如果将自定义模板添加到div#app内,则会看到标题组件会被渲染成我们指定的自定义模板。

最后

如果使用render函数创建组件,让你感觉非常繁琐。那么还可以尝试babel-plugin-transform-vue-jsx插件,就可以像在React中那样轻便(属性细节略有不同,具体参考插件文档)。

总体而言,使用render函数非常有趣,并且在v3.0中也派上了用场。Vue渲染函数是Vue本身的基本组成部分,因此,我真的认为花一些时间并彻底理解该概念(特别是长期使用该框架)很有价值。随着Vue.js的发展和效率的提高,我们平时积累的这些底层基础知识也有助于我们的发展。

换句话说,了解Vue render函数只是你技术进步中的一小步,但很重要。:)

本文分享自微信公众号 - 前端知否(qianduanzhifou),作者:QETHAN

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Vue3 Composition API教程及示例

    Vue引入了Composition API(基于功能的API)作为当前基于Option的API的补充。该API将随Vue 3一起发布,但是现在您可以通过将Vue...

    前端知否
  • Vue 2.6尝鲜

    在这篇文章中,将会介绍新版本的新特性, 比如 slots的新语法, Vue.observable()等等

    前端知否
  • React Hooks - 缓存记忆

    React Hooks几乎在所有方面都能让我们在编程中获得好处。但是某些时候的性能问题,也需要使用一些技巧来解决。我们可以使用Hooks编写快速的应用程序,但是...

    前端知否
  • Vue 中 render 函数有点意思

    我们知道 Vue 模板是非常强大的,基本可以完成我们日常开发的所有任务。但是,有一些用例,如基于输入或插槽值创建动态组件方式,render 函数会比模板完成的更...

    前端小智@大迁世界
  • Awesome-Shell资源

    █████╗ ██╗ ██╗███████╗███████╗ ██████╗ ███╗ ███╗███████╗ ██╔══██╗██║ ██...

    若与
  • Shell 进阶指南

    Awesome Shell 这是一份非常棒的命令行框架、工具包、指南、以及小玩意儿组织清单。由 awesome-php 获得灵感。该 awesome 收集在 ...

    shaonbean
  • React 性能优化大挑战:一次理解 Immutable data跟shouldComponentUpdate

    作者: TechBridge Weekly/huli https://blog.techbridge.cc/2018/01/05/react-render-op...

    企鹅号小编
  • 讨论软件架构的30个共同原则

    想象一下飞越架构评论。一位建筑师走进来,看着,通过他的双筒望远镜凝视着他。他提供的评论通常过于通用或脱离背景。评论经常遭到震耳欲聋的沉默或激动人心的争论。如果有...

    February
  • Rails布局和视图渲染

    基于“多约定,少配置”原则,在 index 动作末尾并没有指定要渲染的视图,Rails会自动在控制器的视图文件夹中寻找 action_name.html.erb...

    用户1515472
  • Github Statistics 一个基于 React 的 GitHub 数据统计工具

    V 站曾经有个热帖说为何我的开源项目只有 Fork 没有 Star,楼下有个热评说开源项目关注的不应该是 Commit 数据吗?先不论 Star、Fork 和 ...

    NebulaGraph

扫码关注云+社区

领取腾讯云代金券