对中国开发者最具吸引力的科技企业有哪些?快来为你 pick 的企业投票! 了解详情
写点什么

Vue.js 3:面向未来的编程

2019 年 11 月 10 日

Vue.js 3:面向未来的编程

如果你对 Vue.js 感兴趣,那很可能知道这个框架的第 3 个版本即将发布(如果你在未来读这篇文章,我希望本文还具有意义)。这个新版本正在积极开发中,但是所有可能的功能都可以在单独的 RFC(request for comments,即意见征求)处查看:https://github.com/vuejs/rfcs。其中一项特性function-api,将极大地改变开发 Vue 应用程序的方式。


本文主要面向具有基本的 JavaScript 和 Vue 背景知识的人。


开篇之前:使用 Bit 来封装 Vue 组件以及它们的依赖和配置。通过更好的代码复用、更简单的维护和更少的开销来构建真正的模块应用。(译者注,这里是对 Bit 平台的推广)


当前的 API 有什么问题?

最好的方法是在一个例子中展示所有问题。因此,我们可以想象,我们需要实现一个组件,这个组件应该获取某个用户的数据并根据滚动偏移显示加载状态和顶栏。下面是最终结果:



你还可以点击链接查看在线演示。


跨组件抽取一些逻辑进行复用是一种好习惯。使用 Vue 2.x 版本的当前 API,有许多常见的模式,最著名的是:


  • Mixins(通过 mixins 选项)

  • 高阶组件(HOC)


因此,我们可以将滚动跟踪逻辑转移到一个 mixin,并将数据获取逻辑转移到一个高阶组件。典型的 Vue 实现如下。


滚动 mixin:


const scrollMixin = {    data() {        return {            pageOffset: 0        }    },    mounted() {        window.addEventListener('scroll', this.update)    },    destroyed() {        window.removeEventListener('scroll', this.update)    },    methods: {        update() {            this.pageOffset = window.pageYOffset        }    }}
复制代码


其中,我们增加了scroll事件监听,跟踪页面偏移并将值保存到pageOffset属性。


高阶组件如下:


import { fetchUserPosts } from '@/api'
const withPostsHOC = WrappedComponent => ({ props: WrappedComponent.props, data() { return { postsIsLoading: false, fetchedPosts: [] } }, watch: { id: { handler: 'fetchPosts', immediate: true } }, methods: { async fetchPosts() { this.postsIsLoading = true this.fetchedPosts = await fetchUserPosts(this.id) this.postsIsLoading = false } }, computed: { postsCount() { return this.fetchedPosts.length } }, render(h) { return h(WrappedComponent, { props: { ...this.$props, isLoading: this.postsIsLoading, posts: this.fetchedPosts, count: this.postsCount } }) }})
复制代码


其中,isLoadingposts分别针对加载状态和发布数据进行初始化。为了获取新id的数据,fetchPosts方法会在创建实例和props.id每次变化之后触发。


这并不是一个完整的高阶组件实现,但是针对这个例子,已经足够了。这里,我们只是包装了目标组件并传递原始属性以及数据请求相关的属性。


目标组件如下:


// ...<script>export default {    name: 'PostsPage',    mixins: [scrollMixin],    props: {        id: Number,        isLoading: Boolean,        posts: Array,        count: Number    }}</script>// ...
复制代码


为了获取指定 props,应该把它包装在创建的高阶组件中:


const PostsPage = withPostsHOC(PostsPage)
复制代码


包含模版和样式的完整组件链接在此


1.命名空间冲突

假设我们需要在我们的组件中增加update方法:


// ...<script>export default {    name: 'PostsPage',    mixins: [scrollMixin],    props: {        id: Number,        isLoading: Boolean,        posts: Array,        count: Number    },    methods: {        update() {            console.log('some update logic here')        }    }}</script>// ...
复制代码


如果你重新打开页面并滚动,顶栏不会再显示。这都是由于 mixin 的update方法的重写。这对于高阶组件也适用。如果你将数据域从fetchedPosts改为posts


const withPostsHOC = WrappedComponent => ({    props: WrappedComponent.props, // ['posts', ...]    data() {        return {            postsIsLoading: false,            posts: [] // fetchedPosts -> posts        }    },    // ...
复制代码


你将会得到如下报错:



报错的原因是封装组件已经用名字posts指定了组件。


2.来源不明

如果一段时间之后,你决定在组件中使用另一个 mixin:


// ...<script>export default {    name: 'PostsPage',    mixins: [scrollMixin, mouseMixin],// ...
复制代码


你能明确说明pageOffset属性来自哪个 mixin 吗?或者换个场景,两个 mixin 都可以有一个同名属性(比如说yOffset),后一个 mixin 的属性将覆盖前一个 mixin 的属性。这并不好,并且会导致许多不可预见的代码缺陷。


3.性能

高阶组件的问题是,我们需要仅仅因为逻辑复用而牺牲性能去创建单独的组件实例。


让我们来“setup”

让我们来看看下个 Vue.js 版本将提供什么可选方案,以及我们将如何适用基于函数的 API 解决同样的问题。


由于 Vue 3 还没有发布,所以创建了辅助插件——vue-function-api。这个插件提供 Vue 3.x 到 Vue 2.x 的版本的函数 API,用于创建下一代 Vue 应用程序。


首先,你需要进行安装:


$ npm install vue-function-api
复制代码


然后通过Vue.use()进行显式设置:


import Vue from 'vue'import { plugin } from 'vue-function-api'
Vue.use(plugin)
复制代码


基于函数的 API 主要新增了一个新的组件选项——setup()。顾名思义,这里是我们使用新的 API 的功能来设置我们的组件逻辑的地方。因此,让我们实现一个功能来根据滚动偏移显示顶栏,基本组件示例如下:


// ...<script>export default {  setup(props) {    const pageOffset = 0    return {      pageOffset    }  }}</script>// ...
复制代码


注意,setup函数接收解析过的 props 对象作为它的首个参数,而且这个props对象是响应式的。我们也返回了一个包含pageOffset属性的对象来暴露给模版的渲染上下文。这个属性也变成响应式的,但是只关于渲染上下文。我们可以像往常一样在模版中使用它:


<div class="topbar" :class="{ open: pageOffset > 120 }">...</div>
复制代码


但是,这个属性在每次滚动事件中应该是变化的。为了实现这点,我们需要在这个组件被挂载时增加一个滚动事件监听器,而这个组件卸载时移除这个监听器。valueonMountedonUnmounted API 函数就是为了实现这些目标而存在:


// ...<script>import { value, onMounted, onUnmounted } from 'vue-function-api'export default {  setup(props) {    const pageOffset = value(0)    const update = () => {        pageOffset.value = window.pageYOffset    }        onMounted(() => window.addEventListener('scroll', update))    onUnmounted(() => window.removeEventListener('scroll', update))        return {      pageOffset    }  }}</script>// ...
复制代码


注意,在 Vue 2.x 版本中,所有生命周期 hooks 都有一个可以在setup()中使用的等效的onXXX函数。


你可能也注意到pageOffset变量包含一个单个的响应式属性:.value。我们需要使用这个包装过的属性,因为在 JavaScript 中像 numbers 和 strings 这样的原始值不是引用传递。值包装器为任何值类型提供了一种方式来传递可变的响应式的引用。


下面是pageOffset对象:



下一步是实现用户数据获取。和使用基于选项的 API 时一样,你可以使用基于函数的 API 来声明计算过的值和观察者:


// ...<script>import {    value,    watch,    computed,    onMounted,    onUnmounted} from 'vue-function-api'import { fetchUserPosts } from '@/api'export default {  setup(props) {    const pageOffset = value(0)    const isLoading = value(false)    const posts = value([])    const count = computed(() => posts.value.length)    const update = () => {      pageOffset.value = window.pageYOffset    }        onMounted(() => window.addEventListener('scroll', update))    onUnmounted(() => window.removeEventListener('scroll', update))        watch(      () => props.id,      async id => {        isLoading.value = true        posts.value = await fetchUserPosts(id)        isLoading.value = false      }    )        return {      isLoading,      pageOffset,      posts,      count    }  }}</script>// ...
复制代码


计算值类似 2.x 版本的计算属性:它只跟踪它的依赖,并且只在依赖改变时重新求值。传递给watch的第一个参数称为“源”,可以是如下之一:


  • 一个 getter 函数

  • 一个值包装器

  • 一个包含两个以上类型的数组


第二个参数是一个回调函数,只在从 getter 返回的值或值包装器改变时调用。


我们只是使用基于函数的 API 来实现目标组件。 下一步是使所有这些逻辑可复用。


解构

最有趣到部分是,为了复用部分逻辑的代码,我们只能将它抽取到一个组合函数并返回响应式状态:


// ...<script>import {    value,    watch,    computed,    onMounted,    onUnmounted} from 'vue-function-api'import { fetchUserPosts } from '@/api'function useScroll() {    const pageOffset = value(0)    const update = () => {        pageOffset.value = window.pageYOffset    }    onMounted(() => window.addEventListener('scroll', update))    onUnmounted(() => window.removeEventListener('scroll', update))    return { pageOffset }}function useFetchPosts(props) {    const isLoading = value(false)    const posts = value([])    watch(        () => props.id,        async id => {            isLoading.value = true            posts.value = await fetchUserPosts(id)            isLoading.value = false        }    )    return { isLoading, posts }}export default {    props: {        id: Number    },    setup(props) {        const { isLoading, posts } = useFetchPosts(props)        const count = computed(() => posts.value.length)        return {            ...useScroll(),            isLoading,            posts,            count        }    }}</script>// ...
复制代码


注意我们是如何使用useFetchPostsuseScroll函数来返回响应式属性的。这些函数可以被存储在单独的文件中,并且任何其它组件中使用。相较于基于选项的方案:


  • 暴露到模板的属性拥有清晰的来源,因为它们是组合函数返回的值;

  • 从组合函数返回的值是任意命名的,因此没有命名空间冲突;

  • 没有仅仅因为逻辑复用目的而创建的不必要的组件实例。


官方RFC页面还列举了许多其它好处。


本文用到的所有代码示例链接在此


你还可以在这个链接查看组件的在线示例。


结论

正如你所见,Vue 基于函数的 API 展示了一个干净灵活的方式来组合组件内部以及组件之间的逻辑,而没有任何基于选项的 API 的缺陷。想象一下,对于任何类型的项目——从小型到大型再到复杂的 Web 应用程序,组合函数会多么有用。


作者介绍


Taras Batenkov 主要关注 Web 前端和数据科学。


原文链接


https://blog.bitsrc.io/vue-js-3-future-oriented-programming-54dee797988b


2019 年 11 月 10 日 08:002462

评论

发布
暂无评论
发现更多内容

理解Redis的内存回收机制和过期淘汰策略

老胡爱分享

redis LRU

大数学家笛卡尔到底是怎么死的? |《隐秘的角落》

赵新龙

数学 隐秘的角落 笛卡尔

产业数字化无法“一蹴而就”,而是“长跑冠军”。

CECBC区块链专委会

模式与重构-作业

秤须苑

起底印度禁用59款应用的数据表现

谢锐 | Frozen

移动应用 游戏开发 游戏出海 移动互联网 游戏制作

Gradle快速入门使用指南 - 安装篇

小隐乐乐

maven

Git 的进阶操作

多选参数

git GitHub gitlab

分布式缓存 - 第五周作业

孙志平

谁没个焦虑的时段呢?

封不羁

个人成长 个人感想 程序员成长

ARTS-week5

王钰淇

ARTS 打卡计划

【自学成才系列二】multipass上ubuntu安装篇

小朱

ubuntu multipass

十分钟带你彻底搞懂原码、反码、补码

程序员生活志

补码 原码 反码

写给孩子的两本书我读得津津有味

孙苏勇

读书 陪伴 随笔杂谈

小师妹学JVM之:JIT中的PrintAssembly续集

程序那些事

JVM jdk8 小师妹 JDK14 assembly

面试时被问创建多少个线程合适?你该怎么说?

小谈

面试 线程 JVM springboot SpringCloud

锦囊篇|一文摸懂SharedPreferences和MMKV(一)

ClericYi

MyBatis入门

Simon郎

Java mybatis

系统架构师week 04 - 互联网架构总结

尔东雨田

极客大学架构师训练营

自由职业半年之后,我又滚回职场了...

王磊

程序员 程序员人生 程序人生

计算机操作系统基础(十)---存储管理之虚拟内存

书旅

php laravel 线程 操作系统 进程

架构师训练营第五周总结

Melo

极客大学架构师训练营

为什么大家都说SELECT * 效率低

Java小咖秀

MySQL Java 面试 经验

架构师训练营第五周总结

陈靓-哲露

手把手教你看MySQL官方文档

Simon

MySQL

神经网络攻防:开篇词——你所不知道的神经网络攻防

P小二

神经网络 AIPwn 对抗样本 AI安全 P小二

公司短信平台上的两万块钱,瞬间就被刷没了

古时的风筝

短信防刷 接口安全 短信轰炸机

极客大学架构师训练营 系统架构 分布式缓存 一致性哈希 Hash 第9课 听课总结

John(易筋)

极客时间 极客大学 极客大学架构师训练营 分布式缓存 一致性哈希

微服务网关演进之路

小楼

Java 微服务 dubbo 网关

一文解决MySQL时区相关问题

Simon

MySQL 数据库

重学 Java 设计模式:实战状态模式「模拟系统营销活动,状态流程审核发布上线场景」

小傅哥

Java 设计模式 小傅哥 重构 代码规范

集中全世界程序员的力量,可以在三天之内实现一个手机淘宝吗?

非著名程序员

程序员 软件 程序人生 软件工程 人月神话

滴滴 Logi 日志管理与分析平台

滴滴 Logi 日志管理与分析平台

Vue.js 3:面向未来的编程-InfoQ