聊一聊Vue和Ts

Posted 程序员万万

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊一聊Vue和Ts相关的知识,希望对你有一定的参考价值。

1 前言

Vue3 已经正式发布了一段时间了,各种生态已经成熟。最近使用 taro+vue3 重构冷链的小程序,经过了一段时间的开发和使用,有了一些自己的思考。

总的来说,Vue3 无论是在底层原理还是在实际开发过程中,都有了很大的进步。

从源码层面来说,使用 Proxy 代替 Object.defineProperty 的 API,一个是代理的对象,一个是递归监控的属性,从而在性能上有了很大的进步,并且,也解决了对象动态属性增加、数组改变监听上的缺陷;对 diff 算法进行优化,增加了静态标记,大大提高了 Vue 的执行效率;还有静态提升、事件监听缓存等一系列提升效率的手段。

从应用层面来说,主要的改变是将 option API 改成了 composition API(组合式 API),在业务中抛弃 data、methods、生命周期函数隔离开的开发方式,使代码相对于业务有更强的聚合性,在代码开发、代码阅读、代码维护方面对于开发者都是更加友好。

对于 typescript 有了更好的支持,我们知道,对于大型的前端项目来说,使用 typescript 的类型校验,能使前端项目有更强的健壮性,这也使得 Vue3 对于大型项目的开发提供了更强的质量保证。

2 组合式 API

所谓的组合式 API,将 Vue2 中的 data、methods、生命周期、数据监听等 option,都封装成钩子函数,然后组合到 setup 函数中,其核心就在于 setup 函数。setup 函数存在的意义,就是为了使用这些新增的组合式 API,并且这些 API 只能在 setup 函数中使用。

setup 函数执行的时机是,props 初始化之后,beforeCreate 函数执行之前,所以在执行 setup 时,还没有初始化 Vue 实例,因此在 setup 中不能使用 this 对象。setup 函数的返回值会被注入到 Vue 实例中,供 Vue 组件使用,所以任何数据想在 Vue 组件的模板中使用,必须在 setup 函数中 return 出去。

组合式 API 的组合,体现在两个层面。第一层的意思是,将某一业务相关数据和处理逻辑放到一起,这是一种关注点的聚合,更方便我们编写代码、处理业务逻辑,并且能更聚焦业务逻辑,更方便我们看代码。第二层面的意思,当某个组件的业务逻辑足够复杂,setup 中的代码足够大的情况下,我们可以在 setup 内部,进一步提取相关的一块业务,使代码逻辑更加清晰,做到了进一步的聚合作用。

如下面代码所示,将业务代码块 A 抽出来,则代码块 A 中 return 出来的数据就可以在组件中使用:

// 组件
import functionA from 'A'
export default defineComponent(
name: 'componentName',
setup() 
...functionA()

)
// 代码块A
export default () => 
return 
a: 1

3 响应式 API

在 Vue3 中响应式 API,主要体现在 ref 和 reactive 两个函数。对于响应式 API,想说两个问题,第一个是为什么要增加响应式 API,第二个是响应式 API 函数 ref 和 reactive 的异同点。

3.1 为什么增加响应式 API

在 Vue2 中所有数据都写在 data 的 option 中,data 中的数据都是响应式的,这样产生的一个问题是,有些常量数据本身不需要监听,从而造成了资源的浪费。所以在 Vue3 中增加了响应式 API,只需要对需要动态更新 dom 的数据进行响应式,不需要动态更新 dom 的数据不进行响应式处理,从很大程度上节省了资源。这里我觉得需要注意的是,写代码的时候一定要仔细思考一下,哪些数据需要进行响应式绑定,哪些数据不需要进行响应式绑定,而不是一股脑的全给绑定上,这样即使代码逻辑不能很清晰易懂,并且也会影响执行效率(写惯了 Vue2 的同学需要注意)。

3.2 ref 和 reactive 的异同点

在了解了为什么要增加响应式 API 后,我们发现 Vue3 提供了两个响应式 API 函数,ref 和 reactive。为什么会提供两个 API 呢? 一个不就可以了吗?那么这两个 API 之间的区别是什么呢?

在使用层面,ref 绑定的数据,需要使用 [data].value 进行数据更改。而 reactive 绑定的数据需要使用 [data].[prpoerty] 的方式进行数据更改。在使用场景方面,一般的,单个的普通数据,我们使用 ref 来定义响应式。而复杂数据,如:表单数据对象、某一模块的一组数据等,使用 reactive 来定义响应式。

那么,对象是不是必须用 reactive 来定义呢? 其实不是的,都可以。官方说法是:可以根据自身习惯使用不同的 API。其实,我觉得,他们是有各自的使用场景的,ref 更强调的是数据 Value 的改变,reactive 更强调的是数据中某一属性的改变。

4 treeShaking 思想

javascript 项目达到一定体积时,将代码分成模块会更易于管理。但是,当这样做时,我们最终可能会导入实际上未使用的代码。Tree Shaking 是一种通过消除最终文件中未使用的代码来优化体积的方法。

Vue3 使用了 tree shaking 的方法,将组件以及其所有的生命周期函数等方法进行分开,如果在组件中使用的代码将不会出现在最终的打包文件中,如此,会减少大大 Vue3 项目的打包体积。由此造成的一个结果就是,使用方法的不同。

4.1 生命周期函数的使用方法

import  defineComponent, ref, onMounted  from 'vue';
export default defineComponent(
name: 'Gift',
setup() 
const counter = ref(0);
onMounted(() => 
// 处理业务,一般进行数据请求
)
return 
counter


)

4.2 Vuex 的使用方法

import  useStore  from "vuex";
import  defineComponent, ref, computed  from 'vue';
export default defineComponent(
name: 'Gift',
setup() 
const counter = ref(0);
const store = useStore();
const storeData = computed(() => store); // 配合computed,获取store的值。
return 
counter,
storeData


)

4.3 Router 的使用方法

import  useStore  from "vuex";
import  useRouter  from "vue-router";
import  defineComponent, ref, computed  from 'vue';
export default defineComponent(
name: 'Gift',
setup() 
const counter = ref(0);
const router = useRouter();
const onClick = () => 
router.push( name: "AddGift" );

return 
counter,
onClick


)

5 关于 Typescript 的使用

这一部分是关于 Ts 的内容,不过它与 Vue3 的开发息息相关。Vue3 整体是使用 Ts 写的,因此,开发 Vue3 的项目需要使用 Ts,所以,我们还是要了解 TS 的。

关于 Ts 的使用这里就不在细说了,在这里想说的的是,在实际业务场景中是如何组织 Ts 代码的。通过对 TS 的大量使用,我的一个体会是:Ts 的核心思维是先关注数据结构,在根据数据结构进行页面开发。而以前的前端开发模式是,先写页面,然后再关注数据。

比如说,我们要开发一个页面,我们可能需要先定义一些 interface。开发页面的时候我们要关注:页面数据的 interface、接口返回数据的类型、请求参数的类型等等。

下面是开发一个列表页面的例子:

// 这是列表中每一项的数据类型
interface IDataItem 
id: string | number;
name: string;
desc: string;
[key: string]: any;


// 接口返回值类型, 一般来说,我们不确定接口返回的数据的类型,因此使用泛型
interface IRes<T> 
code: number;
msg: string;
data: T


// 口返回数据类型定义
interface IDataInfo 
list: Array<IDataItem>;
pageNum: number;
pageSize: number;
total: number;


// 请求
export const getDatalist = (
params: Record<string, any>
): Promise<IRes<IDataInfo>> => 
return Http.get("/api/data/list", params);
;

如上面代码,当我们的 interface 定义完成后,我们的页面数据基本都已清楚,直接写页面就会清晰很多,且出错概率会大大降低。

聊一聊Vue实例与生命周期运行机制

Vue的实例是Vue框架的入口,担任MVVM中的ViewModel角色,所有功能的实现都是围绕其生命周期进行的,在生命周期的不同阶段调用对应的钩子函数可以实现组件数据管理和DOM渲染两大重要功能。例如,实例需要配置数据观测(data observer)、编译模版、挂载实例到 DOM ,然后在数据变化时更新 DOM 。在这个过程中,事件钩子可以辅助我们对整个实例生成、编译、挂载、销毁等过程进行js控制,给我们提供了执行自定义逻辑的机会。所以学习实例的生命周期,能帮助我们理解vue实例的运行机制,更好地利用钩子函数完成我们的业务代码。

生命周期概览

  技术分享

  

Vue提供的可以注册的钩子都在上图片的红色框标注。 它们分别是:

  • beforeCreate

在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。此时的数据观察和事件机制都未形成。

  • created

实例已经创建完成之后同步调用,此时实例已经结束解析选项。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调,此时this便指向vue实例。然而,挂载阶段还没开始,还没有开始DOM编译,$el 属性目前不可见。

  • beforeMount

在挂载开始之前被调用:相关的 render 函数首次被调用。

  • mounted

el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。

  • beforeUpdate

数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。 你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。

  • updated

由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。

当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。

该钩子在服务器端渲染期间不被调用。

  • beforeDestroy

实例销毁之前调用。在这一步,实例仍然完全可用。

  • destroyed

Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

接下来做一个例子,看一下所有的生命周期具体执行时序是怎么样的

 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 5     <title>Vue实例与生命周期</title>
 6     <script type="text/javascript" src="https://unpkg.com/vue/dist/vue.js"></script>
 7 </head>
 8 <body>
 9 
10 <div id="app">
11     <p>{{ message }}</p>
12 </div>
13 
14 <script type="text/javascript">
15 
16     var app = new Vue({
17         el: #app,
18         data: {
19             message : "Vue实例与生命周期"
20         },
21         beforeCreate: function () {
22             console.group(beforeCreate 创建前状态===============》);
23             console.log("%c%s", "color:red" , "el     : " + this.$el); //undefined
24             console.log("%c%s", "color:red","data   : " + this.$data); //undefined
25             console.log("%c%s", "color:red","message: " + this.message)
26         },
27         created: function () {
28             console.group(created 创建完毕状态===============》);
29             console.log("%c%s", "color:red","el     : " + this.$el); //undefined
30             console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化
31             console.log("%c%s", "color:red","message: " + this.message); //已被初始化
32         },
33         beforeMount: function () {
34             console.group(beforeMount 挂载前状态===============》);
35             console.log("%c%s", "color:red","el     : " + (this.$el)); //已被初始化
36             console.log(this.$el);
37             console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化
38             console.log("%c%s", "color:red","message: " + this.message); //已被初始化
39         },
40         mounted: function () {
41             console.group(mounted 挂载结束状态===============》);
42             console.log("%c%s", "color:red","el     : " + this.$el); //已被初始化
43             console.log(this.$el);
44             console.log("%c%s", "color:red","data   : " + this.$data); //已被初始化
45             console.log("%c%s", "color:red","message: " + this.message); //已被初始化
46         },
47         beforeUpdate: function () {
48             console.group(beforeUpdate 更新前状态===============》);
49             console.log("%c%s", "color:red","el     : " + this.$el);
50             console.log(this.$el);
51             console.log("%c%s", "color:red","data   : " + this.$data);
52             console.log("%c%s", "color:red","message: " + this.message);
53         },
54         updated: function () {
55             console.group(updated 更新完成状态===============》);
56             console.log("%c%s", "color:red","el     : " + this.$el);
57             console.log(this.$el);
58             console.log("%c%s", "color:red","data   : " + this.$data);
59             console.log("%c%s", "color:red","message: " + this.message);
60         },
61         beforeDestroy: function () {
62             console.group(beforeDestroy 销毁前状态===============》);
63             console.log("%c%s", "color:red","el     : " + this.$el);
64             console.log(this.$el);
65             console.log("%c%s", "color:red","data   : " + this.$data);
66             console.log("%c%s", "color:red","message: " + this.message);
67         },
68         destroyed: function () {
69             console.group(destroyed 销毁完成状态===============》);
70             console.log("%c%s", "color:red","el     : " + this.$el);
71             console.log(this.$el);
72             console.log("%c%s", "color:red","data   : " + this.$data);
73             console.log("%c%s", "color:red","message: " + this.message)
74         }
75     })
76 </script>
77 </body>
78 </html>

运行代码,在chrome console查看构造信息

技术分享

在console里面执行一下更新操作,data的值被修改后将会触发update操作。

  技术分享

执行实例销毁,将会触发destroy动作。如果后续依然想对实例进行其他操作,将会发现实例已经被销毁,操作不会成功。

  技术分享

从上面的代码和生命周期图解可以看到,Vue的实例封装很符合开发者的思维规范,生命周期非常清晰,是一款相当相当简洁强大优雅的框架,很值得我们研究学习使用。

好了,vue的生命周期就介绍到这儿,欢迎大家留言交流;喜欢或有帮助到您的话,点个赞或推荐支持一下,谢谢!

以上是关于聊一聊Vue和Ts的主要内容,如果未能解决你的问题,请参考以下文章

聊一聊前端性能优化 CRP

聊一聊Vue实例与生命周期运行机制

聊一聊前端的性能指标

#yyds干货盘点#聊一聊前端架构

聊一聊 Vue3 中响应式原理

简单聊一聊「前端异步编程」的实现方式