接下来我们来介绍一下Vue3中新增的Composition API如何使用。注意Composition API仅仅是Vue3中新增的API,我们依然可以使用Options API。先来实现一下之前演示的获取鼠标位置的案例。做这个案例之前,需要先介绍一下createApp这个函数,这里不借助任何的构建工具,直接使用浏览器中原生的ES Module的方式来加载Vue模块。注意,这里我们会使用vue.esm.browser.js完整版的Vue。
首先,安装Vue3.0,创建createApp.html文件。
createApp.html
Title
x: {{ position.x }}
y: {{ position.y }}
打开浏览器,可以看到打印出来的app对象。

这里可以看到X和Y都可以正常响应式,然后打开开发人员工具来看一下刚刚打印的vue对象,把这个对象展开,这里的成员要比Vue2中的vue对象的成员要少很多,而且这些成员都没有使用$开头,说明未来我们基本不用给这个对象上新增成员。这里面可以看到component 、directive、 mixin还有use。它和以前的使用方式都是一样的,mount和过去的$mount的作用类似,还有一个unmount,它类似于过去的$destroyed。
Composition API还是在选项的这个位置来书写,不过要用到一个新的选项,叫做setup,setup函数是Composition API的入口。
setup执行的时机
它是在
prop被解析完毕,但是在组件实例被创建之前执行的,所以在setup内部无法通过this获取到组件的实例,因为组件实例还未被创建,所以在setup中也无法访问到组件中的data、computed、methods,setup的内部的this此时指向的是undefined。
VUe3中提供了一个新的API,让我们来创建响应式对象,这个函数是reactive,在使用reactive之前,先要导入这个函数,在import的后边我们直接来导入reactive。导入完成之后,在setup中就可以使用reactive函数来创建响应式对象,
reactive函数的作用是把一个对象转换成响应式对象,并且该对象的嵌套属性也都会转换成响应式对象,它返回的是一个proxy对象。
createApp.html
Title
x: {{ position.x }}
y: {{ position.y }}

接下来再来演示如何在setup中使用生命周期的钩子函数。先来回顾一下上面的案例,这里的响应式数据已经搞定了,下面需要注册鼠标移动的事件,当鼠标移动的时候响应式当前鼠标的位置。当这个组件被卸载的时候,这个鼠标移动的事件要被移除,注册mousemove事件可以在mounted来实现,但是别忘了最终的目标是要让获取鼠标位置的整个逻辑,封装到一个函数中,这样任何组件都可以重用。这个时候使用mounted的选项就不合适了。其实我们在setup中也可以使用生命周期的钩子函数。
在setup函数中可以使用组件生命周期中的钩子函数,但是需要在生命周期钩子函数前面加上on,然后首字母大写,比如选项中的mounted,在setup中对应的这个函数是onMounted。
另外,setup是在组件初始化之前执行的,是在beforeCreate和created之间执行的,所以在beforeCreate和created中的代码都可以放在setup函数中,这里的beforeCreate和created不需要在setup中有对应的实现。
下面的这些选项中的勾子函数的写法对应在setup中的实线,分别是在前面加上on,然后首字母大写。
注意这里的unmounted,它类似于之前的destroyed。还有下面的renderTracked、renderTriggered的这两个钩子函数非常相似,都是在render函数被重新调用的时候触发的。那它们不同的是renderTracked是在首次调用render的时候也会触发。renderTriggered在首次定用的时候不会触发。
createApp.html
Title
x: {{ position.x }}
y: {{ position.y }}
接下来再来介绍Composition API中的三个函数,reactive、toRefs还有ref,这三个函数都是创建响应式数据的。
先从一个问题看起,先来看一下刚刚案例中使用的reactive函数的一个小问题。当我们不希望在模板中使用position.x和position.y,而是只是用x和y。可以在setup函数中解构useMousePosition的返回值。const { x, y } = useMousePosition,
并且直接在setup中返回x和y。
createApp.html
Title
x: {{ x }}
y: {{ y }}
此时,打开浏览器,发现鼠标移动,页面上的x和y并没有随着变化。这是为什么呢?
这里来解释一下,这里的position是响应式对象,因为我们在useMousePosition中去借用了reactive函数将传入的position对象包装成了proxy对象。将来访问position的x和y的时候,会调用代理对象proxy中的getter拦截收集依赖,当x和y变化之后,会调用代理对象proxy中的setter进行拦截触发更新。
当把代理对象解构的时候,当把position代理对象解构的时候,就相当于定义了x和y两个变量来接收position.x和position.y,而**基本类型的赋值就是把值在内存中复制一份,所以这里的x和y就是两个基本类型的变量,跟代理对象无关。**当重新给x和y赋值的时候,也不会调用代理对象的setter,无法触发更新的操作,所以不能对当前的响应式对象进行解构。
如果我们就想像刚刚那么做呢?这里来介绍一个新的API,叫做toRefs,先演示它如何使用。
导入toRefs,在useMousePosition返回position时,使用toRefs包裹position。
createApp.html
Title
x: {{ x }}
y: {{ y }}
现在来解释原因,toRefs要求我们传入的这个参数必须是一个代理对象proxy,当前的position就是我们reactive返回的一个代理对象,如果的position不是代理对象的话,它会报警告,提示需要传递代理对象。
接下来它内部会创建一个新的对象,然后遍历传入的这个代理对象的所有属性,把所有属性的值都转换成响应式对象。注意toRefs里边是把position这个对象的所有属性的值都转换成响应式对象,然后再挂载到新创建的对象上,最后把这个新创建的对象返回。
它内部会为代理对象的每一个属性创建一个具有value属性的对象,该对象是响应式的value属性,具有getter和setter,这一点和下面要讲的ref函数类似。getter里面返回代理对象中对应属性的值,setter中给代理对象的属性赋值,所以返回的每一个属性都是响应式的。
toRefs这个函数的作用就是把对象的每一个属性都转换成响应式数据,所以可以解构toRefs返回的对象,解构的每一个属性也都是响应式。下边在解构的时候,解构的是toRefs这个函数返回的新的对象,这个对象的所有属性都是响应式对象,并且这个属性是一个对象,它有一个value属性,在模板中使用的时候可以把这个value省略,但是我们在代码中去写的时候,这个value是不可以去省略的,我们稍后的时候会去演示。
接下来再来介绍一个响应式的API,叫做ref,这是一个函数,它的作用是把普通数据转换成响应式数据。和reactive不同的是,reactive是把一个对象转换成响应式数据。ref可以把基本类型的数据包装成响应式对象。
ref.html
Document
{{ count }}
下面来解释一下ref内部做了什么?首先基本数据类型,它存储的是值,所以它不可能是响应式数据,我们知道响应式数据要通过该收集依赖通过setter触发更新。
ref的参数它如果是对象,内部会调用reactive返回一个代理对象,也就是如果调用ref的时候,参数传递的是对象的话,它内部其实就是调用reactive。
ref的参数如果是基本类型的值,比如我们传入的是0,它内部会创建一个只有value属性的对象,该对象的value属性具有getter和setter,在getter中收集依赖在setter中触发更新。
接下来我们再来介绍几个API。首先来看计算属性,计算属性的作用是简化模板中的代码,可以缓存计算的结果,当数据变化后才会重新计算。
我们依然可以向Vue2.x的时候,在创建组件的时候传入computed的选项来创建计算属性。在Vue3中,也可以在setup中通过computed的函数来创建计算属性。computed的函数有两种用法,第一种是传入一个获取值的函数,函数内部依赖响应式的数据,当依代的数据发生变化后,会重新执行该函数获取数据。computed的函数返回一个不可变的响应式对象,类似于使用ref创建的对象只有一个value属性。
获取计算属性的值要通过value属性来获取,模板中使用计算属性可以省略value, computed的第二种用法是传入一个对象,这个对象具有getter和setter,返回一个不可变的响应式对象。例如下面这段代码,当获取值的时候会触发这个对象的getter,当设置值的时候会触发这个对象的setter。

这里已经创建好了一个页面,并且做了一些准备工作。我们这里放了一个按钮,当点击按钮的时候,会利用push方法创建一个待办事项,下面去显示未完成的代办事项个数。
computed.html
Document
未完成:{{ activeCount }}
点击页面中的按钮后,可以发现未完成的数量+1。当往todos中添加一项的时候,todos变化了,计算属性中传入的函数会重新执行,重新获取未完成的待办事项的个数。
computed可以创建一个响应式的数据,这个显示的数据依赖于其他响应式的数据,当依赖类的数据发生变化后,会重新计算属性传入的这个函数。
接下来我们再来看侦听器。和computed类似,在setup函数中可以使用watch来创建一个侦听器,它的使用方式和之前使用this.$watch或者选项中的watch作用是一样的,监听响应式数据的变化,然后执行一个相应的回调函数,可以获取到监听数据的新值和旧值。
下面来介绍一下要写的这个案例。这个案例来自vue的官网,这是一个选择困难症必备的应用,可以在这个文本框中输入一个只需要回答是和否的问题,然后会发送一个请求,请求这个接口,它会随机返回一个yes or no。这个接口除了返回yes no之外,还会随机返回一个好玩的图片,如果你需要的话,可以把图片也展示出来。
watch.html
Document
请问一个 yes/no 的问题:
{{ answer }}
模板中用到了question和answer,所以需要在setup中定义并且返回question和answer ,question和answer都是响应式的,因为他们都是自符串,所以这里可以使用ref来创建响应式的对象。定义question和answer ,question就等于通过ref来创建我们这个响应式对象,question里边其实存储的就是一个字符串,接下来再创建一个answer。ref返回的对象是不可变的,可以改变这个对象的value属性。
当用户在文本框中输入问题以后,也就是question的值发生变化之后,这个时候要发送请求,获取答案给answer赋值,所以这里我们要监听question的变化,我们要用到watch。watch函数的第一个参数是ref或者reactive返回的对象,要监听question,**注意第一个参数,这个位置跟以前不一样,我们在Vue2使用this.$watch的时候,第一个参数是字符串。**然后是回调函数,这个回调函数可以接收新值和旧值。第三个参数我们暂时不需要。
当值变化之后,我们要去请求这个接口,那这里我们直接用fetch来发送请求。
fetch返回的是一个promise对象,所以这块可以用async、await来简化调用。当拿到response之后,要解析出来里边的答案,给answer去赋值,给answer赋值的时候,注意要给answer的value属性来赋值,注意response.json()它返回的也是一个promise对象。在setup的最后,还要把question和answer返回。
打开浏览器来测试一下。

watch使用起来和过去的this.$watch是一样的,不一样的是第一个参数不是字符串,而是ref或者返回的对象。
在Vue3中还提供了一个新的函数watchEffect,它其实就是watch函数的简化版本。内部实现是和watch调用的同一个函数doWatch,不同的是,watchEffect没有第二个回调函数的参数。watchEffect接受一个函数作为参数,它会监听这个函数内部使用的响应式数据的变化,它会立即执行一次这个函数,当数据变化之后会重新运行该函数。它也返回一个取消监听的函数。
watchEffect.html
Document
{{ count }}
watchEffect中的函数初始的时候首先会执行一次,当count的值发生变化的时候,这个函数会再次被调用,另外watchEffect会返回一个函数,取消对数据的监视。点击stop后,再次点击increase,发现模板中的数据在增加,而控制台并没有再进行输出。
后续的案例中我们会使用watchEffect监听数据的变化,当数据变化后,把变化的数据存储到localStory中,那这个时候使用watchEffect会非常的方便。

接下来我们来做一个案例todoList,todoList是代办事项清单,平时工作或者学习的时候也应该有一个自己的代办事项清单,它是一个非常经典的案例,学习一个新的技术后,可以通过这个案例快速来巩固所学的知识。
todoList需要实现以下功能:
之前的案例都是直接在网页上引用vue模块,接下来在做todoList案例的时候,使用vue的脚手架来创建项目。首先我们升级vue-cli到4.5以上的版本,新版本在创建项目的时候可以选择使用view3.0,vue-cli使用的方式和之前是一样的,先使用vue create创建项目,创建项目的时候选择vue3.0。
全局安装/升级@vue/cli:npm install -g @vue/cli
vue create my-todolist
vue create my-todolist
使用Vue3
已经创建好了项目,并把组件和样式文件都提前设置好了。那下面我们来看一下页面的结构,然后一个功能一个功能来实现。

App.vue
todos
运行yarn serve查看效果

todos
查看效果

到这我们的第一个任务添加待办事项就完成了。
todos
App.vue
todos
此时还没有处理“自动获得焦点”
下面来实现在编辑代办事项的时候让编辑文本框块获得焦点。这里需要用到自定义指令,Vue3中自定义指令的使用方式和Vue2中稍有不同。先来介绍一下Vue2和Vue3中自定义指令的差别。主要是自定义指令的钩子函数被重命名,Vue3把钩子函数的名称和组件中钩子函数的名称保持一致,这样很容易理解,但是自定义指令的钩子函数和组件钩子函数的执行方式是不一样的。
Vue3中的钩子函数的名称,它有三组,分别是mount、update还有unmounted,分别是在自定义指令修饰的元素被挂载到DOM树、更新、卸载的时候执行。这是自定义指令的第一种用法,在创建自定义指令的时候还可以传函数,这种用法比较简洁,更常用一些。第二个参数是函数的时候,Vue3和Vue2的用法是一样的。
指定名称后面的这个函数在Vue3的时候是在mounted和update的时候去执行,跟Vue2的执行实际其实是一样的。Vue2里这个函数是在bind和update的时候执行,那这个函数的el参数是我们指令所绑定的那个元素,binding这个参数可以获取到指定对应的值,通过binding.value来获取。


todos


接下来开始来做切换待办事项状态的第一个子任务,点击checkbox,改变所有待办事项的完成状态。
todos
接下来再来实现切换待办事项状态的第二个子任务,点击all、active、completed个超链接的时候,查看不同状态的待办事项。
在模板中,先找到三个超链接的位置,超链接的href属性是三个锚点,这里不使用路由功能,自己来实现。首先要监视地址中hash的变化,当组件挂载完毕,要注册hashChange事件。当组件卸载的时候,要把hashChange事件移除。在hashChange事件中,要获取当前锚点的值,只需要这里的单词all、active、completed,所以可以把前面的#号杠去掉。然后再根据hash来判断当前要获取哪种状态的待办事项列表。
把过滤代办事项数据的函数定义到一个对象,然后根据hash的值去对象中获取对应的函数,当然函数的名称跟hash值是一样的,这是核心思路,这样写的话,就避免了写一堆if语句。
todos
todos
接下来我们来实现todo list案例的最后一个功能,把待办事项存储到localStorage中,防止刷新的时候丢失数据,当数据修改的时候要把数据存储到localStorage,下次加载的时候再从本地存储中把数据还原。
接下来要操作本地存储,可以把操作本地存储的代码分装到一个模块中,这是一个通用的模块,将来在其他组件中也可以使用。
utils/useLocalStorage.js
function parse(str) {let valuetry {value = JSON.parse(str)} catch (e) {value = null}return value
}function strginify(obj) {let valuetry {value = JSON.stringify(obj)} catch (e) {value = null}return value
}export default function useLocalStorage() {function setItem(key, value) {value = strginify(value)window.localStorage.setItem(key, value)}function getItem(key) {let value = window.localStorage.getItem(key)if (value) {value = parse(value)}return value}return {setItem,getItem}}
准备工作都做好了,下面来想一下这个功能如何实现。当todos中数据变化的时候,需要把变化后的数据存储到本地存储中,要调用setItem,当添加数据或者删除数据或者编辑数据的时候,都会引起todos的变化,所以要去修改useAdd、useRemove、useEdit,这样太麻烦了,有没有简单一点的办法呢?
这个时候可以想到watchEffect,它可以监视数据的变化,如果数据改变了,可以执行相应的操作。还有当页面首次加载的时候,要首先从本地存储中获取数据,如果没有数据的话,可以初始化成一个空数组。所有的这些操作可以再封装到一个函数中。
通过composition API实现这个案例,把不同的逻辑代码拆分到不同的use函数中,同一功能的代码只存在一个函数中,而且更方便组件之间重用代码,这是composition API比options API好的地方。
todos

上一篇:认识vue3以及语法运用简介
下一篇:助推专精特新企业数字化的低代码