Vue renderless组件(函数式组件)

开门见山,我在codepen上写了个简单demo:上下两个按钮,功能一致(开关);不同之处是:前者只改变文字,后者顺带改变了背景颜色。OK,怎么实现呢?

Two Toggles

实现相同功能的模块,理想方式自然是提供通用的抽象组件。这个例子中,想要配合背景更换的效果,最直观的设计应该是实现一个子组件,再根据父组件传递的props值——比如一个叫backgroud的function——联动更改背景色。
不过,在某些场景中,我们想要的是模块提供固定的功能逻辑,但并不希望它限制页面的渲染(比如,我希望给开关加上更酷炫的动态效果,而不仅仅只有更换背景色这种单调的操作)。组件设计者显然不可能尽善尽美地提供所有候选props,这时候留出更多的自定义空间反倒是一个比较切合实际的解决途径。

Render函数

进入正题前,先简介一下Vue是如何渲染组件的。众所周知,在工程中,我们会在.vue文件中定义<template><script><style>三种tag,分别盛放组件html、javascript和css。

<template>
  <button class="mood">
    {{ state ? 'On' : 'Off' }}
  </button>
</template>

<script>
export default {
  data: () => ({ state: false })
}
</script>

<style>
.mood:after {
  color: white;
  background: blue;
}
</style>

但事实上,最后在生产环境中,我们只使用了一个巨大的JS文件——just JavaScript。究其缘由还是得益于webpack的vue loader,它帮助我们把上述三部分提取出来,比如上述的.vue文件,经过vue loader后,大体会成为如下这种样式:

exprot default {
  template: `<button class="mood">{{ state ? 'On' : 'Off' }}</button>`,
  data: ()  => ({ state: false })
}

vuejs会把template元素提取出来,并进一步编译成一个叫render的函数。(有关render函数可以参考官方文档

render(h) {
  return h(
    'button',
    {class: 'mood'},
    state ? 'On' : 'Off'
  )
}

render函数最后会被vue优化成VNode(虚节点),具体过程我不再赘述了。不过,这里提供了一个很有趣的思路:编写组件时,我们其实可以不写vue文件,不写template,只需要写render函数。

const button = {
  render(h) {
    return h(
      'button',
      {class: 'mood'},
      state ? 'On' : 'Off'
    )
  },
  data() {
    return {state: false}
  }
}

So? Renderless?

前提概要结束了,这里引入一个Renderless component的概念,直译的话应该叫非渲染组件,国内好多人喜欢叫它函数式组件。

Renderless意思就是组件只提供数据操作,不渲染任何内容。我们搁置争议,只看非渲染组件的具体实现。

const toggle = {
    render() {
        ...
    },
    data() {
        return { state: true }
    },
    methods: {
        toggle() {
            this.state = !this.state
        }
    }
}

new Vue({
    el: '#parent',
    components: { toggle },
    ...
})

toggle就是所谓的Renderless组件了,只有数据和方法,不提供html template。父组件直接将其放入components即可当作一般子组件使用。

Slots in Renderless

那谁负责渲染工作呢?嗯,就是Slots!父组件通过传递自定义的slots来定制子组件的html template。

<toggle v-slot:default="{on, toggle}">
    <div class="container">
        <button @click="click(toggle)">
            {{on ? 'On' : 'Off'}}
        </button>
    </div>
</toggle>

这里提一下v-slot,它是vue 2.6以后的新语法,用来代替之前的slotslot-scopev-slot:default还可以简写成#default。Vue3应该不会再保留slotslot-scope这种不伦不类的标签了。

Scoped Slots

<toggle #default="{on, toggle}">

上文中用到了作用域插槽。这个例子中我希望能让插槽访问到子组件toggle里的数据和方法,以便之后点击button更改状态。子组件暴露作用域插槽也很简单,只要在render函数里返回$scopedSlots对象即可,这里因方便起见使用了默认的default插槽,自己实现的时候也可以重命名为任意插槽。

//toggle.js
const toggle = {
    render() {
        return this.$scopedSlots.default({
            on: this.state,
            toggle: this.toggle,
        })
    },
    data() {
        return { state: true }
    },
    methods: {
        toggle() {
            this.state = !this.state
        }
    }
}

Using toggle component

最后我们在父组件调用renderless组件:

<template>
  <toggle v-slot="{on, toggle}">
        <div class="container">
            <button @click="click(toggle)">
                {{on ? 'On' : 'Off'}}
            </button>
        </div>
    </toggle>
</template>

<script>
import toggle from 'toggle';

export default {
  components: { toggle },
    methods: {
        click(fn) {
            fn()
        },
    },
}
</script>

这样一个简单的renderless开关就实现了,

Simple Toggle

Customized Component

假如你想自定义组件样式,或是说控制toggle渲染方式,更改也很容易,只需要在插槽里写下自定义代码即可:

<toggle #default="{ on, toggle  }">
    <div class="container">
        <button @click="click(toggle)"
                :style="{background: on ? 'green' : 'red'}">
            {{on ? 'On' : 'Off'}}
        </button>
    </div>
</toggle>

因为toggle的逻辑不变,所以我们不需要更改这个renderless组件。只需稍微改动一下slot,button的背景色就会随着开关一齐改变了。嗯,这就是Renderless组件的效果,功能逻辑和页面渲染分开。

Toggle with background

更炫酷的开关就由大家来完成吧。

小结

这期用一个很简单的例子科普了Renderless Component。所谓Renderless就是利用render函数和slot,将组件的功能逻辑与前端渲染分离开来,这种设计更符合传统软件工程的单一职责和开放闭合原则。当然这和VUE设计之初的理念并不相符,vue作者似乎并不屑于这种形式,我在尤雨溪的某些文章里还看到他喷renderless哗众取宠,带来无谓的的性能开销。
我自己倒是挺赞许renderless的。在工程开发中确实会碰到了功能逻辑相似,但样式表现不一的组件簇;通过将底层逻辑以renderless子组件的形式封装起来,可以很好地实现代码复用的目标。现实开发中,具体情况还是要具体分析滴。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容