Vue3基础复习
可以访问线上的(尚硅谷版本,自己拷贝下来部署的):https://vue3-study.pages.dev/
下面的是黑马(好像是评论区别人发的)
一、Vue3.0介绍
1、Vue3.0介绍
在学习Vue3.0之前,先来看一下与Vue2.x的区别
会从如下几点来介绍
- 源码组织方式的变化
Composition API- 性能提升
- Vite
Vue3.0全部使用TypeScript进行重写,但是90%的API还是兼容2.x,这里增加了Composition API也就是组合API.
在性能方面有了大幅度的提升,在Vue3.0中使用Proxy重写了响应式的代码,并且对编译器做了一定的优化,重写了虚拟DOM,让渲染有了很大的性能提升。
同时官方也提供了一款工具Vite,使用该工具,在开发阶段进行测试的时候,不需要进行打包,直接运行项目,提升了开发的效率
下面先来看一下源码组织方式:
源码采用TypeScript重写
使用Monorepo管理项目结构
首先,我们可以看到最开始是以compiler开头的包,这些都是与编译相关的代码。compiler-core是与平台无关的编译器,compiler-dom是浏览器平台下的编译器,依赖于compiler-core.
compiler-sfc:用来编译单文件组件,依赖于compiler-core与compiler-dom
compiler-ssr:是服务端渲染的编译器,依赖于compiler-dom
reactivity:数据响应式系统
·runtime-core·:是与平台无关的运行时
runtime-dom:是针对浏览器的运行时,用来处理元素DOM的api和事件等
runtime-test:进行测试的运行时
server-renderer:进行服务端渲染
shared:是VUE内部使用的一些公共的API
size-check:是一个私有的包,用来检查包的大小
template-explorer:是在浏览器中运行的实时编译组件,会输出render函数
Vue构建完整版的Vue,依赖于compiler与runtime
2、不同的构建版本
Vue3与Vue2一样,都提供了不同的构建版本,可以在不同的场合中使用。
和Vue2不同的是,在Vue3中不在构建UMD的模块化方式。
cjs模块化方式,也就是CommonJS模块化方式,在该模式下对应的文件是vue.cjs.js与vue.cjs.prod.js
这个两个文件都是完整版的vue,包含了运行时与编译器,vue.cjs.js是开发版,代码没有被压缩。
vue.cjs.prod.js:表示的是生产版本,代码被压缩过。
下面是global
vue.global.js
vue.global.prod.js
vue.runtime.global.js
vue.runtime.global.prod.js
以上四个js文件,都可以通过script方式进行导入,导入以后,会增加一个全局的Vue对象,
vue.global.js
vue.global.prod.js
以上两个文件包含了完整版的vue,包含编译器与运行时。vue.global.js是开发版本,代码没有被压缩,vue.global.prod.js是生产版本,代码进行了压缩。
vue.runtime.global.js
vue.runtime.global.prod.js
以上两个文件,只包含了运行时,同样有开发版本与生产版本。
下面我们再来看一下browser
vue.esm-browser.js
vue.esm-browser.prod.js
vue.runtime.esm-browser.js
vue.runtime.esm-browser.prod.js
以上四个文件都包含了浏览器原生模块化的方式,在浏览器中可以直接通过script type='module' 的方式来导入模块。
vue.esm-browser.js
vue.esm-browser.prod.js
上面两个文件是,esmodule的完整版,包含了开发版本与生产版本,
vue.runtime.esm-browser.js
vue.runtime.esm-browser.prod.js
以上两个文件是运行时版本,
最后我们再来看一下bundler
`vue.esm-bundler.js`
`vue.runtime.esm-bundler.js`
以上两个文件没有打包所有的代码,需要配合打包工具来使用,这两个文件都是使用es module的模块化方式,内部通过import导入了runtime core
vue.esm-bundler.js是完整版,其内部还导入了runtime-compiler,也就是编译器,我们使用脚手架创建的项目,默认导入了 vue.runtime.esm-bundler.js,这个文件只导入了运行时,也就是vue的最小版本,在打包的时候,只会打包我们使用到的代码,可以让vue的体积更小。
以上就是不同构建版本的介绍。
3、Composition API 设计动机
Vue2.x在设计中小型项目的时候,使用非常方便,开发效率也高。但是在开发一些大型项目的时候也会带来一定的限制,
在Vue2.x中使用的API是Options API,该类型的API包含一个描述组件选项(data,methods,props等)的对象,在使用Options API开发复杂的组件的时候,同一个功能逻辑的代码被拆分到不同的选项中,这样在代码量比较多的情况下就会导致不停的拖动滚动条才能把代码全部看清,非常的不方便。
如下代码示例:
1 | export default { |
在上面的代码中,我们实现的是获取鼠标的位置,然后展示到页面中,如果现在需要在上面的程序中添加新的功能,可能需要在data和methods等选项中,添加新的代码,这样代码量比较多以后,在进行查看的时候,需要不断的拖动滚动条,非常麻烦。
而使用Composition API可以解决这样的问题。
下面先来看一下Composition API的介绍
Composition API 是Vue.js 3.0 中新增的一组API,是一组基于函数的API,可以更灵活的组织组件的逻辑。
下面,我们通过Composition API来演示上面的案例
1 | import {reactive, onMounted,onUnmounted} from 'vue' |
在上面的代码中,我们可以看到关于获取鼠标位置的核心逻辑代码封装到一个函数中了,这样其它组件也可以使用,只需要封装到一个公共模块中,进行导出,其它组件进行导入即可。通过这一点,我们也能够看出,Composition API 提供了很好的代码的封装与复用性。
如果,现在我们需要添加一个新的功能,例如搜索的功能,我们只需要添加一个函数就可以了。
这样,我们以后在查看代码的时候,只需要查看某个具体实现业务的函数就可以了。因为核心的业务我们到封装到了一个函数中,不像Options API一样,把核心的业务都分散到了不同的位置,查看代码的时候,需要不断的拖动滚动条。
当然,在Vue3.js中可以使用Composition API也可以使用Options API,这里可以根据个人喜好来进行选择。如果开发的组件中需要提取可复用的逻辑,这时可以使用Compositon API,这样更加的方便。
最后,我们来做一个总结:
Composition API提供了一组基于函数的API,让我们能够更加灵活的组织组件的逻辑,也能够更加灵活的组织组件内的代码结构,还能够把一些逻辑功能从组件中提取出来,方便其它的组件重用。
4、性能提升
这一小节,我们来看一下关于Vue3中的性能的提升。
关于Vue3中的性能提升,主要体现在如下几点
第一: 响应式系统升级,在Vue3中使用Pxory重写了响应式系统
第二:编译优化,重写了虚拟DMO,提升了渲染的性能。
第三:源码体积的优化,减少了打包的体积
下面,我们先来看一下“响应式系统的升级”
在Vue2.x中响应式系统的核心是defineProperty,在初始化的时候,会遍历data中的所有成员,将其转换为getter/setter,如果data中的属性又是对象,需要通过递归处理每一个子对象中的属性,注意:这些都是在初始化的时候进行的。也就是,你没有使用这个属性,也进行了响应式的处理。
而在Vue3中使用的是Proxy对象来重写了响应式系统,并且Proxy的性能要高于defineProperty,并且proxy可以拦截属性的访问,删除,赋值等操作,不需要在初始化的时候遍历所有的属性,另外有多层属性的嵌套的时候,只有访问某个属性的时候,才会递归访问下一级的属性。使用proxy默认就可以监听到动态新增的属性,而Vue2中想动态新增一个属性,需要通过Vue.set()来进行处理。而且Vue2中无法监听到属性的删除,对数组的索引与length属性的修改也监听不到,而在Vue3中使用proxy可以监听动态的新增的属性,可以监听删除的属性,同时也可以监听数组的索引和length属性的修改操作。
所以Vue3中使用proxy以后,提升了响应式的性能和功能。
除了响应式系统的升级以外,Vue3中通过优化编译的过程,和重写虚拟DOM, 让首次渲染与更新的性能有了很大的提升。
下面,我们通过一个组件,来回顾一下Vue2中的编译过程
1 | <template> |
我们知道在Vue2中,模板首先会被编译成render函数,这个过程是在构建的过程中完成的,在编译的时候会编译静态的根节点和静态节点,静态根节点要求节点中必须有一个静态的子节点.
当组件的状态发生变化后,会通知watcher,会触发watcher的update,最终去执行虚拟DOM的patch方法,遍历所有的虚拟节点,找到差异,然后更新到真实的DOM中,diff的过程中,会比较整个的虚拟DOM,先对比新旧的节点以及属性,然后在对比子节点。Vue2中渲染的最小的单位是组件,
在vue2中diff的过程会跳过,静态的根节点,因为静态根节点的内容不会发生变化,也就是说在Vue2中通过标记静态根节点,优化了diff的过程。但是在vue2中静态节点还需要进行diff,这个过程没有被优化。
在Vue3中标记和提升了所有的静态节点,diff的时候只需要对比动态节点内容。另外在Vue3中新引入了Fragments,这样在模板中不需要在创建一个唯一根节点的特性。模板中可以直接放文本内容,或者很多的同级的标签,当然这需要你在vscode中升级vetur插件,否则如果模板中没有唯一的根节点,vscode会提示错误。
下面,我们再来看一下:优化打包体积
在Vue3中移除了一些不常用的API,例如:inline-template,filter等。
同时Vue3对Tree-shaking的支持更好,通过编译阶段的静态分析,将没有引入的模块在打包的时候直接过滤掉。让打包后的体积更小。
5、Vite
Vite是针对Vue3的一个构建工具,Vite翻译成中文就是“快”的意思,也就是比基于webpack的vue-cli更快。
在讲解Vite之前,我们先来回顾一下在浏览中使用ES Module的方式。
现代浏览器都支持ES Module(IE不支持)
通过下面的方式加载模块
1 | <script type='module' src='..'></script> |
支持模块的script默认具有延迟加载的特性,类似于script标签设置了defer.也就是说type='module'的script的标签相当于省略了defer,
它是在文档解析完后也就是DOM树生成之后,并且是在触发DOMContentLoaded事件前执行。
Vite的快就是体现在,使用了浏览器支持的ES Module的方式,避免了在开发环境下的打包,从而提升了开发的速度。
下面我们看一下Vite与Vue-cli的区别
它们两者之间最主要的区别就是:Vite在开发模式下不需要打包就可以直接运行。因为,在开发模式下,vite是使用了浏览器支持的es module加载模块,也就是通过import导入模块,浏览器通过<script type='module'>的形式加载模块代码,因为vite不需要打包项目,所以vite在开发模式下,打开页面是秒开的。
Vue-cli:在开发模式下必须对项目打包才可以运行,而且项目如果比较大,速度会很慢。
Vite特点
第一:因为不需要打包,可以快速冷启动。
第二:代码是按需编译的,只有代码在当前需要加载的时候才会编译。不需要在开启开发服务器的时候,等待整个项目被打包。
第三:Vite支持模块的热更新。
Vite在生产环境下使用Rollup打包,Rollup基于浏览器原生的ES Module的方式来打包,从而不需要使用babel将import转换成require以及一些辅助函数,所以打包的体积比webpack更小。
下面,我们看一下Vite创建项目
1 | npm init vite-app 项目名称 |
注意:npm install安装相应的依赖,这一步不能省略。
创建好项目以后,查看index.html文件,可以看到如下代码
1 | <script type="module" src="/src/main.js"></script> |
以上就是加载了src下的main.js模块,在main.js模块中加载了App.vue这个模块,App.vue这个模块是单文件组件,浏览器不支持,但是页面可以正常的展示。那么它是如何处理的呢?
vite开启的服务会监听.vue后缀的请求,会将.vue文件解析成js文件。vite使用了浏览器支持的es module来加载模块,在开发环境下不会打包项目,所有模块的请求都会交给服务器来处理,服务器会处理浏览器不能识别的模块,如果是单文件组件,会调用compiler-sfc来编译单文件组件,并将编译的结果返回给浏览器。
二、Composition API
1、Composition API基本使用
下面,我们来学习一下Composition API的应用,这里为了简单方便,我们先不使用Vite来创建项目。而使用浏览器原生的es module的形式,来加载Vue的模块。
创建好项目文件夹composition-api-demo后,执行如下命令安装最新的Vue版本
1 | npm install vue@next |
在项目目录下面创建index.html,代码如下:
1 |
|
在上面的代码中,导入了createApp方法,该方法创建一个Vue的实例,同时将其绑定到了id=app的这个div上。在createApp这个方法中创建对应的选项内容。
注意:这里的data只能是函数的形式,不能是对象的形式。(注意:在vscode中安装了Live Server 插件,所以右击index.html,在弹出的对话框中选择’open with Live Server ‘,这时会启动一个服务打开该页面,避免出现跨域的问题)
下面,我们开始使用Compositon API,使用该API,需要用到一个新的选项setup,setup是composition api的入口函数。
1 |
|
在setup方法中创建了一个positon对象,该对象并不是响应式的,下面看一下怎样将其修改成响应式的。
以前我们是在data这个选项中设置响应式的对象,当然这里还可以定义到data中,但是为了能够将某一个逻辑的所有代码封装到一个函数中,vue3中提供了一个新的api来创建响应式的对象。这个api就是reactive
首先导入该函数
1 | import {createApp,reactive} from './node_modules/vue/dist/vue.esm-browser.js' |
使用reactive函数将positon对象包裹起来。
1 | const position=reactive({ |
现在position对象就是响应式的对象了,刷新浏览器,可以看到positon中的x属性的值发生了更改。
2、生命周期钩子函数
这一小节,看一下怎样在setup中使用生命周期的钩子函数。
在setup中可以使用生命周期的钩子函数,但是需要在钩子函数名称前面加上on,然后首字母大写。例如:mounted
setup是在组件初始化之前执行的,是在beforeCreate与created之间执行的,所以beforeCreate与created的代码都可以放到setup函数中。
所以,在上图中,我们可以看到beforeCreate与created在setup中没有对应的实现。
其它的都是在钩子函数名称前面添加上on,并且首字母大写。
注意:onUnmounted类似于之前的destoryed,调用组件的onUnmounted方法会触发unmounted钩子函数
renderTracked与renderTriggered这两个钩子函数都是在render函数被重新调用的时候触发的。
不同是的是,在首次调用render的时候renderTracked也会被触发,renderTriggered在render首次调用的时候不会被触发。
了解了在setup中使用钩子函数的方式后,下面继续完善获取鼠标坐标位置的案例
在定义鼠标移动事件的时候,可以在原有代码的mounted钩子函数中定义,但是为了能够将获取鼠标位置的相关的逻辑代码封装到一个函数中,让任何一个组件都可以重用,这时使用mounted选项就不合适了。这样我们最好是在setup中使用生命周期的钩子函数,
1 |
|
在上面的代码中,首先导入了onMounted,onUnmounted
1 | import {createApp,reactive,onMounted,onUnmounted} from './node_modules/vue/dist/vue.esm-browser.js' |
定义获取鼠标位置的update函数
1 | //获取鼠标的位置坐标 |
在onMounted钩子函数中注册mousemove事件
1 | onMounted(()=>{ |
在onUnmounted钩子函数中移除mousemove事件
1 | onUnmounted(()=>{ |
现在,功能已经实现了,下面我们在将代码进行重构。
将获取鼠标位置的功能封装到一个函数中。
1 |
|
在上面的代码中定义了,useMousePosition函数,在该函数中封装了获取鼠标位置的业务代码,最终返回。
在setup函数中调用useMousePositon函数,接收返回的positon,然后返回,这样在模板中可以展示鼠标的位置。
通过这个案例,我们可以体会出Composition API与options API的区别。
如果现在使用options api来实现这个案例,我们需要在data中定义x,y,在methods中定义update,这样把同一个逻辑的代码分散到了不同的位置,这样查找起来也非常的麻烦,而现在根这个逻辑函数相关的代码都封装到了一个函数中,这样方便了以后的维护也就是如果出错了,直接查找这个函数就可以了。并且这个函数也可以单独的放到一个模块中,这样在任何一个组件中都可以使用。
3、reactive/toRefs/ref
这一小节,我们来介绍Compositiont API中的三个函数reactive/toRefs/ref
这三个函数都是创建响应式数据的。
我们首先看一下reactive,该函数,我们在前面的案例中已经使用过,该函数的作用就是将一个对象设置为响应式的。现在,我们对前面写的程序进行一个简单的优化。
在模板中,我们是通过以下的方式来获取x与y的值。
1 | <div id="app"> |
这样写笔记麻烦,可以简化成如下的形式
1 | <div id="app"> |
同时,在setup函数中,进行如下的修改:
1 | // const position=useMousePosition() |
我们知道,当我们调用useMousePosition方法的时候,返回的是一个position对象,该对象中包含了x与y两个属性,那么这里我们可以进行解构处理。
最后将解构出来的x,y返回,这样在模板中就可以直接使用x,y.
但是,当我们的鼠标在浏览器中移动的时候,x,y的值没有发生变化,说明它们不是响应式的了。
原因是什么呢?
原因是,当对position对象进行解构的时候,就是定义了两个变量x,y来接收解构出来的值。所以这里的x,y就是两个变量,与原有的对象没有任何关系。
但是,这里我们还是希望对模板进行修改呢?
可以使用toRefs
首先导入toRefs
1 | import {createApp,reactive,onMounted,onUnmounted,toRefs} from './node_modules/vue/dist/vue.esm-browser.js' |
然后在useMousePosition方法中,将返回的position对象,通过toRefs函数包括起来。
1 | return toRefs(position) |
toRefs函数,可以将一个响应式对象中的所有属性转换成响应式的。
现在,通过浏览器进行测试,发现没有任何的问题了。
下面我们来解释一下toRefs的工作原理,首先传递给toRefs函数的参数,必须是一个响应式的对象(代理的对象),而position就是一个通过reactive函数处理后的一个响应式对象。在toRefs方法内部首先会创建一个新的对象,然后会遍历传递过来的响应式对象内的所有属性,把属性的值都转换成响应式对象,然后再挂载到toRefs函数内部所创建的这个新的对象上,最后返回。
toRefs函数是把reactive返回的对象中的所有属性,都转换成了一个对象,所以对响应式对象进行解构的时候,解构出的每一个属性都是对象,对象是引用 传递的,所以解构出的数据依然是响应式的。
那么,我们解构toRefs返回的对象,解构出来的每个属性都是响应式的。
现在,我们先知道toRefs函数的作用就是:可以将一个响应式对象中的所有属性转换成响应式的。
后面还会继续探讨它的应用。
下面,我们再来看一下ref函数的应用
ref函数的作用就是把普通数据转换成响应式数据。与reactive不同的是,reactive是把一个对象转换成响应式数据。
ref可以把一个基本类型的数据包装成响应式对象。
下面,我们使用ref函数实现一个自增的案例。
1 |
|
下面,我们把上面的代码做一个解析:
首先在模板中有一个按钮,与展示数字的插值表达式。
下面导入createApp与ref函数。
虽然这个案例中的业务非常的简单,但是这里,我们还是单独的将其封装到一个函数中useCount中。
如果,我们写的是如下代码:
1 | const count=0 |
那么,count就是一个基本的数据类型的变量。
但是,如果写成如下的形式:
1 | const count=ref(0) |
count就是一个响应式对象,该对象中会有一个value属性,该属性存储的就是具体的值。该属性中包含了getter/setter.
useCount方法将count与add方法返回。
在setup方法中调用useCount方法,并且将该方法返回的内容进行解构,注意:这里解构出来的count就是一个响应式的对象。
而在模板中使用count响应式对象的时候,不需要添加value属性。当我们单击“增加”按钮的时候,会执行add方法,该方法中让count中的value属性值加1,由于count是一个响应式对象,它内部的值发生了变化后,视图就会重新渲染,展示新的数据。
下面,我们来介绍一下ref函数的工作原理。
我们知道,基本数据类型,存储的是值。所以它不可能是响应式数据,我们知道响应式数据需要通过getter收集依赖(watcher对象),通过setter触发更新,
如果ref的参数是一个对象,内部会调用reactive返回一个代理对象,也就是说,如果我们给ref传递的是一个对象,那么内部调用的就是reactive
如果ref的参数是一个基本类型的值,例如我们案例中传递的0,那么在ref内部会创建一个新的对象,这个对象中只有一个value属性,该value属性具有getter/setter.通过getter收集依赖(watcher对象),通过setter触发更新.
以上就是我们常用的响应式函数。
reactive:把一个对象转换成响应式对象
ref:把基本类型数据转换成响应式对象
toRefs:把一个响应式对象中的所有属性都转换成响应式对象,该函数处理reactive返回的对象的时候,可以进行解构的操作。
4、Computed
Computed是计算属性,计算属性的作用就是简化模板中的代码,可以缓存计算的结果,当数据变化后才会重新进行计算。
下面,我们来看一下Computed的使用。
Computed有两种用法
第一种用法:
给computed传入获取值的函数,函数内部依赖响应式的数据,当依赖的数据发生变化后,会重新执行该函数来获取数据。
computed返回的是一个不可变的响应式对象,类似于使用ref创建的对象,只有一个value属性,获取计算属性的值,需要通过value属性来获取。如果模板中使用可以省略value.
1 | computed(()=>count.value+1) |
第二种用法:
computed的第二种用法是传入一个对象,这个对象具有getter/setter,computed方法返回一个不可变的响应式对象,当获取值的时候,会执行getter,当设置值的时候,会执行该对象中的setter,后面我们会演示这种用法
1 | const count=ref(1) |
下面我们先来使用第一种用法
下面这个案例,会使用computed来计算出未处理的任务的个数。
1 |
|
在setup函数的内部,我们通过reactive函数将data对象转换成了响应式的对象,todos就是一个响应式对象。下面通过计算属性对响应式对象todos中的内容进行计算,计算出未完成的任务的个数,并且返回。将返回的数量保存到了activeCount中,然后将activeCount返回,这样在模板中就可以通过差值表达式来使用activeCount.同时,当单击了按钮以后,会向todos响应式对象中添加数据,这时会重新执行计算属性,计算没有完成的任务数量。
5、Watch
我们可以在setup函数中使用watch函数创建一个侦听器。监听响应式数据的变化,然后执行一个回调函数,可以获取到监听的数据的新值与旧值。
watch的三个参数
第一个参数:要监听的数据
第二个参数:监听到的数据变化后执行的函数,这个函数有两个参数,分别是新值和旧值。
第三个参数:选项对象,deep(深度监听)和immediate(立即执行)
Watch函数的返回值是一个函数,作用是用来取消监听。
watch与以前的this.$watch的使用方式是一样的,不一样的是watch第一个参数不是字符串(this.$watch第一个参数是字符串),而是ref和reactive返回的对象。
1 |
|
在上面的代码中,setup函数内,定义两个响应式对象分别是question与answer,这两项的内容都是字符串,所以通过ref来创建。使用watch来监听quest的变化,由于question与文本框进行了双向绑定,当在文本框中输入内容后,question的值会发生变化,发生变化后,就会执行watch的第二个参数,也就是回调函数。该回调函数内,发送一个异步请求,注意这里使用了fetch方法,发送请求,该方法返回的是一个promise对象,所以这里使用了await,下面调用json方法获取json对象,注意该方法返回的也是promise,所以这里也使用了await,最后把获取到的数据给了answer中的value属性。
1 |
|
6、WatchEffect
在Vue3中还提供了一个新的函数WatchEffect.
WatchEffect是Watch函数的简化版本,也用来监视数据的变化,WatchEffect接收一个函数作为参数,监听函数内响应式数据的变化,会立即执行一次该函数,当数据发生了变化后,会重新运行该函数。返回的也是一个取消监听的函数。
1 |
|
在setup函数中,定义响应式对象count,同时使用watchEffect函数监听count的变化,当我们第一次打开页面的时候,watchEffect会执行一次,所以在控制台中输出的值为0,watchEffect返回的是一个取消监听的函数,这里定义stop来接收。
下面将count,stop,add函数返回,这样在模板中就可以使用了。当用户单击add按钮的时候,count的值累加(注意修改的是count中的value属性的值),这时watchEffect函数执行,在浏览器控制台中打印count的值。如果,单击了stop按钮,再次单击add的按钮,count的值还会进行累加,但是watchEffect函数不执行,那么控制台中不会打印count的值,但是页面中count的值不断的变化。
三、ToDoList案例
整个案例实现的功能如下:
添加任务
删除任务
编辑任务
切换任务
存储任务,实现持久化
1、项目结构
从这个案例开始,我们使用Vue的脚手架来创建项目,首先需要升级Vue-cli,升级到4.5以上的版本,这样我们在创建项目的时候,可以使用vue3.0
升级vue-cli
1 | npm install -g @vue/cli |
查看版本:
1 | vue -V |
项目创建
1 | vue create todolist |
在创建的时候选择Vue3.0
在App.vue文件中,构建出基本的结构
1 | <template> |
在上面的代码中,导入了样式。
2、添加任务
在模板的header的内,给文本框添加v-model实现双向绑定,同时为其添加一个enter事件,该事件触发,表明用户按下了键盘上的回车键,那么这里就需要将用户在文本框中输入的内容保存到一个数组中。
1 | <header class="header"> |
下面,将添加任务的业务单独的封装到一函数中处理。
1 | <script> |
在上面的代码中,创建了useAdd这个函数,该函数完成保存任务数据,所以调用该函数的时候,需要传递一个数组。
在该函数内,创建input这个响应式对象(这里input的值为字符串,所以使用ref函数创建响应式对象,在这里输入ref,然后按下tab键,会自动导入ref函数),最后返回,然后在setup 函数中会将其解构出来,这样模板中就可以使用input这个响应式对象,该响应式对象中存储了用户在文本框中输入的值。
在userAdd方法内,创建addTodo方法,该方法获取文本框中输入的值,并且构建出一个对象,插入到todos这个数组中,这里要求新输入的数据在最开始进行展示,所以这里使用了unshif函数来插入对象。最后返回。
在setup函数中,对useAdd函数返回的结果进行解构,然后返回,这样在模板中就可以使用input 和addTodo方法。当然这里调用useAdd方法的时候,需要传递一个数组,注意这个数组也是响应式,因为我们把数据添加到该数组以后,列表要重新渲染展示最新的数据,在最开始创建数组的时候,默认就是一个空数组。
同时,还需要注意在addTodo方法中,使用的todos是一个响应式的对象(这里是通过ref([])创建出来,然后传递到了addTodo方法中),想使用它里面的值,需要通过value属性才能获取到,这时获取到的才是真正的数组。
1 | todos.value.unshift({ |
下面,把todos中的数据展示出来。也就是在模板中遍历todos这个数组。
所以这里需要将todos这个数组,在setup函数中返回。
1 | setup() { |
下面对列表进行循环遍历
1 | <ul class="todo-list"> |
3、删除任务
在列表中找到删除按钮,并且为其添加单击事件,事件触发后调用remove方法,给该方法传递传递的是要删除的任务。
1 | <ul class="todo-list"> |
定义useRemove方法,当调用该方法的时候,传递todos数组,然后在该方法中实现remove方法,该方法就是根据传递过来的要删除的todo,从数组todos中,
找到对应的索引值(注意todos是一个响应式对象,todos.value才是真正的数组),然后调用splice方法将其从todos中删除。
最后返回remove这个方法。
1 | // 2. 删除任务 |
下面回到setup这个函数中,调用useRemove函数,传递todos这个响应式对象,然后将useRomove函数的返回值解构,解构出来的就是remove函数,这样在模板中就可以使用了。
1 | setup() { |
4、编辑任务
整个编辑操作,要完成的功能如下:
- 双击任务项,展示编辑文本框
- 按回车键或者编辑文本框失去焦点,修改数据
- 按下
esc键取消编辑操作 - 把编辑文本框内容清空,然后按下回车键,删除这一项
- 显示编辑文本框的时候获取焦点
下面创建编辑函数:
1 | // 3、编辑任务 |
下面,在setup函数中将useEdit方法的返回内容解构出来,这样在模板中就可以使用了。当然在调用useEdit方法的时候,需要传递一个remvoe函数,而该函数是useRemove方法解构出来的,所以这里需要在return返回之前,先把useRemove返回的remove解构出来,然后传递给useEdit方法,同时remove方法也要返回,因为在模板中使用到了该方法。
1 | export default { |
下面要处理的就是模板的内容了。
1 | <ul class="todo-list"> |
在上面的代码中,我们给li添加了一个类样式,editing,如果当前的item与editingTodo相等,表明是当前的这个任务处于编辑状态,所以要显示文本框。
这里我们给input文本框添加了v-model,这样文本框中会展示要编辑的数据,如果在文本框中输入了新数据item.text中的值为新数据,,添加了@key.enter事件,按下回车键该事件触发调用doneEdit方法完成更新,这时把当前的数据项传递到了doneEdit方法中,该方法中,会执行如下语句
1 | todo.text = todo.text.trim(); |
把数据中的空格去掉后有赋值给了自己,也就是todo.text,然后,执行
1 | editingTodo.value = null; |
这时候,为非编辑状态,不在显示文本框,而显示div,而div中的label中展示的数据为修改后的数据。
1 | @blur="doneEdit(item)" |
失去焦点的时候,也是调用doneEdit方法。
按下esc键的时候,执行cancelEdit方法,显示div中的内容不,不在展示文本框,同时内容还是原来的内容数据。
给div中的label添加双击事件。
1 | <label @dblclick="editTodo(item)">{{ item.text }}</label> |
现在,在测试的时候出现了一个问题就是,当我们双击数据项的时候,也就是label标签的时候,文本框可以展示出来,但是没有焦点,这一点我们后面解决。
那么,我们先来看另完一个问题,就是双击以后,出现文本框,然后单击一下有焦点了,然后输入内容,发现焦点又没有了。
这时什么原因呢?
原因是li的key的问题
1 | <li |
上面的代码中key绑定了item.text
然后文本框也绑定了item.text(v-model绑定了item.text)
1 | <input |
当我们在文本框中输入值的时候item.text的值发生了变化。当key发生了变化后,重新渲染的时候发现新的li对应的vnode(虚拟DOM)与原有的li的vnode的key不相同,此时会重新生成li,导致其内部的子元素也会重新生成。由于重新生成了文本框,所以文本框内没有焦点。
如果item对象内有id属性,绑定该属性就可以解决这个问题了,但是这里没有,可以直接绑定item,因为每个item对象是不相等的。
1 | <li |
经过测试,发现这时就解决了我们刚才提到的问题,因为由于key的值为item,那么在文本框中输入值的时候,对应的item对象没有变化,变化的是item中的text属性,对象还是原来的对象,只是其中的text的属性的值发生了变化。那么这样由于key的值不会发生变化,那么就不会重新生成li·以及内部的文本框,所以在这里输入内容的文本框还是原来的文本框。
最后,还有一个问题需要解决,就是双击以后,出现文本框,然后让其获取焦点。
5、编辑任务2
为了能够实现双击文本后,出现文本框,并且在文本框中显示焦点,这里需要用到指令来实现。
Vue3与Vue2.x在自定义指令的差别,就是在钩子函数的命名上,vue3中把指令的钩子函数与组件的钩子函数保持一致,这样更容易理解
Vue2.x中指令的定义方式
1 | Vue.directive('editingFocus',{ |
1 | bind:当指令绑定在对应元素时触发。只会触发一次。 |
Vue3中指令的定义方式
1 | app.directive('editingFocus',{ |
1 | bind => beforeMount |
以上是自定义指令的第一种用法,在定义自定义指令的时候,还可以传函数。这样用法比较简洁,而且更加常用。第二个参数是函数的时候,vue2与vu3的用法是一样的。
Vue2.x
1 | Vue.directive('editingFocus',(el,binding)=>{ |
Vue3.0
1 | app.directive('editingFocus',(el,binding)=>{ |
指令名称后面的这个函数在vue3中是在,mounted与updated的时候执行,与Vue2的执行时机是一样的。在Vue2中这个函数是在bind与update的时候执行。
函数中的el参数是指令所绑定的元素,binding中可以获取指令对应的值。通过binding.value来获取。
下面,根文本框添加指令
1 | <input |
在input标签上添加了一个v-eding-focus的指令,该指令以v-开头。但是这里需要注意的一点就是,当文本框处于编辑状态的时候,该指令才起作用。所以,这里,判断item是否等于editingTodo,如果成立,也就是为true,表明处于编辑状态,则让文本框获得焦点。
v-editing-focus指令定义如下:
1 | export default { |
注意:directives与setup是同级定义的。
在定义指令的时候,不需要添加v-前缀。同时判断一下binding.value中的值是否为true,如果是,在让文本框获取焦点,因为当前处于编辑状态。
6、切换任务状态
- 点击左上角的复选框(该复选框的高度与宽度都是1
px,为了美观,这里单击的是label),改变所有任务的状态(要么全部完成,要么全部未完成) - 单击底部的
All/Active/Completed按钮,单击All按钮,展示所有的任务。单击Active展示的是没有完成的任务,Completed展示的是已经完成的任务 - 显示未完成的任务项个数。
- 移除所有完成的项目
- 如果没有任务项,隐藏
main与footer区域。
6.1 改变所有任务的状态
单击左上角的复选框,改变所有任务项的状态。
1 | <div class="view"> |
在li中的每个任务项名称前面都有一个复选框,这里复选框的状态是有completed属性来决定,所以这里通过v-model绑定completed属性。
根左上角的复选框绑定一个计算属性
1 | <input |
1 | //4、切换任务的状态。 |
下面要解决的是,当处理完某个任务后,为其添加上对应的横线。
1 | <li |
当item.completed属性为true的时候,给列表添加completed样式。
6.2 切换状态
单击底部的All/Active/Completed按钮,单击All按钮,展示所有的任务。单击Active展示的是没有完成的任务,Completed展示的是已经完成的任务
以上三个按钮,都是基于hash的链接,当单击这些链接的时候,会触发hashchange事件,在事件的处理函数中,我们可以获取到对应的hash值,然后根据hash值,调用不同的过滤方法,查询对应的数据。
1 | const userFilter = (todos) => { |
模板中遍历的是filterreadTodos
1 | <li |
整个数据的过滤的操作还是定义在userFilter方法中完成。在该方法中,指定onMounted钩子函数,也就是组件挂载完毕后,完成hashchange事件的绑定,
事件触发以后执行``
1 | onMounted(() => { |
当第一次进入页面,还没有单击链接的时候,也要调用 onHashChange();方法,进行数据的过滤,当单击了链接以后,hashchange事件触发也要调用onHashChange方法,完成数据的过滤。
在onUnmounted方法中移除事件的绑定。
1 | onUnmounted(() => { |
下面看一下 onHashChange();方法的实现
1 | const onHashChange = () => { |
在onHashChange方法中,首先获取hash值,然后将#/去掉,只保留all或者是active,completed.
然后判断在filter中是否定义了对应的过滤方法。
filter类的实现如下:
1 | const filter = { |
在filter中,定义了不同的过滤方法。
如果根据获取到的hash值能够从filter中查询到对应的过滤方法。那么把hash值赋值给type.value
type是一个响应式对象,默认值为all
1 | const type = ref("all"); |
type的值发生了改变,就会重新渲染视图,渲染视图的时候会执行 v-for="item in filterreadTodos",而filterreadTodos是一个计算属性。
1 | const filterreadTodos = computed(() => filter[type.value](todos.value)); |
该计算属性依赖的type的值发生变化,所以会执行computed内的函数,根据type中存储的值,从filter中查询到对应的过滤函数,把todos数组中的内容传递到该函数中进行过滤。
以上是在filter中找到对应的过滤方法的情况。
如果在filter中找不到对应的过滤方法,会执行:
1 | type.value = "all"; |
执行上面的代码,有可能是首次加载,也有可能是输入的hash值是错误的。
这时把type的值修改成all,表示加载所有的任务数据。并且把地址栏中的hash值清空。
注意:最开开始要导入对应的钩子函数。
1 | import { ref, computed, onMounted, onUnmounted } from "vue"; |
同时,还需要注意,最后一定要将filterreadTodos内容返回。
1 | return { |
6.3 剩余内容处理
- 显示未完成的任务项个数。
- 移除所有完成的项目
- 如果没有任务项,隐藏
main与footer区域。
下面先来实现第一条内容:显示未完成的任务项个数。
这个功能需要通过计算属性来完成,也就是统计todos数组中数据的变化情况。
1 | <footer class="footer"> |
整个数据的统计是有remainingCount计算属性完成的,该属性返回的值如果大于1,显示items,否则显示item.
关于remainingCount计算属性的实现还是在userFilter方法中完成,该方法都是与过滤相关的业务内容。
1 | const filterreadTodos = computed(() => filter[type.value](todos.value)); |
在原有的filterreadTodos整个过滤器的下面,定义了remainingCount过滤器,该过滤器调用了filter中的active方法来获取未处理的任务项数据,然后再统计其个数。当第一次渲染的时候,会执行remainingCount计算属性,当todos数组中的数据发生了变化后,还会执行该计算属性,来进行数据的统计。
最后需要将remainingCount计算属性返回,这样在模板中才能使用。
1 | return { |
下面要实现的是”移除所有完成的项目”
给底部右下角的按钮绑定单击事件。
1 | <button class="clear-completed" @click="removeCompleted"> |
removeCompleted方法定义在useRemove方法中,该方法是有关删除的相关业务的内容。
1 | // 2. 删除任务 |
在removeCompleted方法中,把todos数组中未完成的数据查询出来,重新赋值给todos数组,这样数组todos数组中存储的就是未完成处理的数据。当todos中的内容发生了变化后,计算属性filterreadTodos也会重新执行,从而重新渲染列表。注意:最后要将removeCompleted函数返回
下面修改一下setup函数中的内容。
1 | setup() { |
如果没有任务项,隐藏main与footer区域。
如果没有数据,只显示输入的文本框,隐藏其它的位置
下面给footer与main,都去添加v-show
1 | <section class="main" v-show="count"> |
当count中有值,则展示main与footer中的内容,否则进行隐藏。count中的值为任务的总数。
这里的count必须是一个计算属性,因为count的值依赖响应式数据todos.todos的数据发生了变化,需要重新计算count的值。
下面,我们在userFilter中实现count这个计算属性。
1 | const remainingCount = computed(() => filter.active(todos.value).length); |
在remainingCount下面定义count这个计算属性,同时将count返回。
1 | return { |
7、本地存储
下面要将数据存储起来,否则单击浏览器的刷新按钮,数据会丢失。这里是把任务数据存储到localStorage中。
在src目录下面定义utils目录,在该目录下面创建useLocalStorage.js文件,该文件中的代码如下:
1 | function parse(str){ |
在上面的代码中创建了parse函数,该函数将字符串转换成对象,stringify函数将对象转换成字符串。
同时导出了useLocalStorage函数,该函数中封装了对localStorage操作的两个方法,setItem添加数据到localStorage中,getItem把数据从localStorage找那个取出来。
最后返回setItem与getItem这两个方法。
现在回到App.vue文件中。
1 | import { ref, computed, onMounted, onUnmounted, watchEffect } from "vue"; |
首先导入useLocalStorage函数,接下来调用该函数,我们知道该函数返回的是一个对象,该对象中存储的是getItem与setItem,这里都存储到常量storage中。
我们知道,当数据发生变化后,不管是添加,更新,还是删除,都需要将变化后的数据重新的保存到localStorage中,哪么应该怎样处理呢?如果在添加,删除,更新的方法中都写这些代码,那么就比较麻烦了。所以这里我们可以在封装一个方法,这个方法里完成对localStorage中数据更新的操作。
1 | //5、存储任务数据 |
在useStorage方法中,首先获取localStorage中的数据,获取到了给todos,如果没有获取到返回的就是一个空数组。注意这里的todos是一个响应式的对象。
下面我们通过watchEffect函数检测todos中的数据是否有变化,如果有变化执行watchEffect内的回调函数,将数据重新保存到localStroage中。最后把响应式对象todos返回。
下面修改setup方法中的代码
1 | setup() { |
在setup这个方法中,我们调用了useStorage方法,把返回的内容给了todos这个常量,注意:这个常量也是响应式对象,因为useStorage方法返回的就是一个响应式对象。当我们第一次执行的时候,从storage中获取数据,后期todos中数据有变化都会执行watchEffect这个函数,然后将数据保存到localStorage中。
以上,就是整个todoList案例,在这个案例中,我们将不同的业务逻辑封装到了不同的函数中,整体的代码结构非常的情形。
