Vue3.0

一、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-corecompiler-dom

compiler-ssr:是服务端渲染的编译器,依赖于compiler-dom

reactivity:数据响应式系统

·runtime-core·:是与平台无关的运行时

runtime-dom:是针对浏览器的运行时,用来处理元素DOMapi和事件等

runtime-test:进行测试的运行时

server-renderer:进行服务端渲染

shared:是VUE内部使用的一些公共的API

size-check:是一个私有的包,用来检查包的大小

template-explorer:是在浏览器中运行的实时编译组件,会输出render函数

Vue构建完整版的Vue,依赖于compilerruntime

2、不同的构建版本

Vue3Vue2一样,都提供了不同的构建版本,可以在不同的场合中使用。

Vue2不同的是,在Vue3中不在构建UMD的模块化方式。

cjs模块化方式,也就是CommonJS模块化方式,在该模式下对应的文件是vue.cjs.jsvue.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中使用的APIOptions API,该类型的API包含一个描述组件选项(data,methods,props等)的对象,在使用Options API开发复杂的组件的时候,同一个功能逻辑的代码被拆分到不同的选项中,这样在代码量比较多的情况下就会导致不停的拖动滚动条才能把代码全部看清,非常的不方便。

如下代码示例:

export default {
    data(){
        return {
            position:{
                x:0,
                y:0
            }
        }
    },
    created(){
        window.addEventListener('mousemove',this.handle)
    }
    destroyed(){
        window.removeEventListener('mousemove',this.handle)
    },
     methods:{
         handle(e){
             this.position.x=e.pageX
             this.position.y=e.pageY
         }
     }
}

在上面的代码中,我们实现的是获取鼠标的位置,然后展示到页面中,如果现在需要在上面的程序中添加新的功能,可能需要在datamethods等选项中,添加新的代码,这样代码量比较多以后,在进行查看的时候,需要不断的拖动滚动条,非常麻烦。

而使用Composition API可以解决这样的问题。

下面先来看一下Composition API的介绍

Composition API Vue.js 3.0 中新增的一组API,是一组基于函数的API,可以更灵活的组织组件的逻辑。

下面,我们通过Composition API来演示上面的案例

import {reactive, onMounted,onUnmounted} from 'vue'
function useMousePosition(){
    const position=reactive({
        x:0,
        y:0
    })
    const upate=(e)=>{
        position.x=e.pageX
        position.y=e.pageY
    }
    onMounted(()=>{
        window.addEventListener('mousemove',update)
    })
    onUnmounted(()=>{
        window.removeEventListener('mousemove',update)
    })
    return position
}
export default {
    setup(){
        const position=useMousePosition()
        return {
            position
        }
    }
}

在上面的代码中,我们可以看到关于获取鼠标位置的核心逻辑代码封装到一个函数中了,这样其它组件也可以使用,只需要封装到一个公共模块中,进行导出,其它组件进行导入即可。通过这一点,我们也能够看出,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中的编译过程

<template>
  <div id="app">
     <div>
           static root
         	<div>static node</div>
    </div>
      <div>static node</div>        
    	<div>static node</div> 
      <div>{{count}}</div>  
      <button @click="handler">
          button
    </button>
 </div>
</template>

我们知道在Vue2中,模板首先会被编译成render函数,这个过程是在构建的过程中完成的,在编译的时候会编译静态的根节点和静态节点,静态根节点要求节点中必须有一个静态的子节点.

当组件的状态发生变化后,会通知watcher,会触发watcherupdate,最终去执行虚拟DOMpatch方法,遍历所有的虚拟节点,找到差异,然后更新到真实的DOM中,diff的过程中,会比较整个的虚拟DOM,先对比新旧的节点以及属性,然后在对比子节点。Vue2中渲染的最小的单位是组件,

vue2diff的过程会跳过,静态的根节点,因为静态根节点的内容不会发生变化,也就是说在Vue2中通过标记静态根节点,优化了diff的过程。但是在vue2中静态节点还需要进行diff,这个过程没有被优化。

Vue3中标记和提升了所有的静态节点,diff的时候只需要对比动态节点内容。另外在Vue3中新引入了Fragments,这样在模板中不需要在创建一个唯一根节点的特性。模板中可以直接放文本内容,或者很多的同级的标签,当然这需要你在vscode中升级vetur插件,否则如果模板中没有唯一的根节点,vscode会提示错误。

下面,我们再来看一下:优化打包体积

Vue3中移除了一些不常用的API,例如:inline-template,filter等。

同时Vue3Tree-shaking的支持更好,通过编译阶段的静态分析,将没有引入的模块在打包的时候直接过滤掉。让打包后的体积更小。

5、Vite

Vite是针对Vue3的一个构建工具,Vite翻译成中文就是“快”的意思,也就是比基于webpackvue-cli更快。

在讲解Vite之前,我们先来回顾一下在浏览中使用ES Module的方式。

现代浏览器都支持ES Module(IE不支持)

通过下面的方式加载模块

<script type='module' src='..'></script>

支持模块的script默认具有延迟加载的特性,类似于script标签设置了defer.也就是说type='module'script的标签相当于省略了defer,

它是在文档解析完后也就是DOM树生成之后,并且是在触发DOMContentLoaded事件前执行。

Vite的快就是体现在,使用了浏览器支持的ES Module的方式,避免了在开发环境下的打包,从而提升了开发的速度。

下面我们看一下ViteVue-cli的区别

它们两者之间最主要的区别就是:Vite在开发模式下不需要打包就可以直接运行。因为,在开发模式下,vite是使用了浏览器支持的es module加载模块,也就是通过import导入模块,浏览器通过<script type='module'>的形式加载模块代码,因为vite不需要打包项目,所以vite在开发模式下,打开页面是秒开的。

Vue-cli:在开发模式下必须对项目打包才可以运行,而且项目如果比较大,速度会很慢。

Vite特点

第一:因为不需要打包,可以快速冷启动。

第二:代码是按需编译的,只有代码在当前需要加载的时候才会编译。不需要在开启开发服务器的时候,等待整个项目被打包。

第三:Vite支持模块的热更新。

Vite在生产环境下使用Rollup打包,Rollup基于浏览器原生的ES Module的方式来打包,从而不需要使用babelimport转换成require以及一些辅助函数,所以打包的体积比webpack更小。

下面,我们看一下Vite创建项目

npm init vite-app 项目名称
cd 项目名称
npm install
npm run dev

注意:npm install安装相应的依赖,这一步不能省略。

创建好项目以后,查看index.html文件,可以看到如下代码

 <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版本

npm install vue@next

在项目目录下面创建index.html,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    <div id="app">
        x:{{position.x}}<br/>
        y:{{position.y}}
    </div>
    <script type="module">
        import {createApp} from './node_modules/vue/dist/vue.esm-browser.js'
        const app=createApp({
            data(){
                return {
                    position:{
                        x:0,
                        y:0
                    }
                }
            }
        })
        app.mount('#app')
    </script>
</body>
</html>

在上面的代码中,导入了createApp方法,该方法创建一个Vue的实例,同时将其绑定到了id=app的这个div上。在createApp这个方法中创建对应的选项内容。

注意:这里的data只能是函数的形式,不能是对象的形式。(注意:在vscode中安装了Live Server 插件,所以右击index.html,在弹出的对话框中选择’open with Live Server ‘,这时会启动一个服务打开该页面,避免出现跨域的问题)

下面,我们开始使用Compositon API,使用该API,需要用到一个新的选项setup,setupcomposition api的入口函数。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    <div id="app">
        x:{{position.x}}<br/>
        y:{{position.y}}
    </div>
    <script type="module">
        import {createApp} from './node_modules/vue/dist/vue.esm-browser.js'
        const app=createApp({
            setup() {
                //setup可以有两个参数
                //第一个参数是`props`,props的作用是用来接收外部传入的参数,并且props是一个响应式的对象,并且不能被解构。
                //第二个参数是:context,context是一个对象,该对象有三个成员,分别是attrs,emit,slots,这个案例暂时没有用到。
                //setup的执行时机:是在props解析完毕,但是是在组件实例被创建之前执行的,所以在setup内部无法通过this获取组件的实例,因为组件的实例还没有被创建,所以在setup中无法访问到组件中的data,methods等。setup内的this指向的是undefined

                //下面创建positon对象,该对象目前是普通对象,并不是响应式的。
                const position={
                    x:0,
                    y:0
                }
                //将positon对象返回,这样可以在模板,以及生命周期的钩子函数中使用该对象
                return {
                    position
                }
            },
            //下面我们在mounted的钩子函数中,修改一下positon中的x的值,来验证是否为响应式的。
            mounted(){
                this.position.x=100
            }
        })
        console.log(app)
        app.mount('#app')
    </script>
</body>
</html>

setup方法中创建了一个positon对象,该对象并不是响应式的,下面看一下怎样将其修改成响应式的。

以前我们是在data这个选项中设置响应式的对象,当然这里还可以定义到data中,但是为了能够将某一个逻辑的所有代码封装到一个函数中,vue3中提供了一个新的api来创建响应式的对象。这个api就是reactive

首先导入该函数

import {createApp,reactive} from './node_modules/vue/dist/vue.esm-browser.js'

使用reactive函数将positon对象包裹起来。

                const position=reactive({
                    x:0,
                    y:0
                })

现在position对象就是响应式的对象了,刷新浏览器,可以看到positon中的x属性的值发生了更改。

2、生命周期钩子函数

这一小节,看一下怎样在setup中使用生命周期的钩子函数。

setup中可以使用生命周期的钩子函数,但是需要在钩子函数名称前面加上on,然后首字母大写。例如:mounted
在这里插入图片描述

setup是在组件初始化之前执行的,是在beforeCreatecreated之间执行的,所以beforeCreatecreated的代码都可以放到setup函数中。

所以,在上图中,我们可以看到beforeCreatecreatedsetup中没有对应的实现。

其它的都是在钩子函数名称前面添加上on,并且首字母大写。

注意:onUnmounted类似于之前的destoryed,调用组件的onUnmounted方法会触发unmounted钩子函数

renderTrackedrenderTriggered这两个钩子函数都是在render函数被重新调用的时候触发的。

不同是的是,在首次调用render的时候renderTracked也会被触发,renderTriggeredrender首次调用的时候不会被触发。

了解了在setup中使用钩子函数的方式后,下面继续完善获取鼠标坐标位置的案例

在定义鼠标移动事件的时候,可以在原有代码的mounted钩子函数中定义,但是为了能够将获取鼠标位置的相关的逻辑代码封装到一个函数中,让任何一个组件都可以重用,这时使用mounted选项就不合适了。这样我们最好是在setup中使用生命周期的钩子函数,

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    <div id="app">
        x:{{position.x}}<br/>
        y:{{position.y}}
    </div>
    <script type="module">
        //导入onMounted,onUnmounted函数
        import {createApp,reactive,onMounted,onUnmounted} from './node_modules/vue/dist/vue.esm-browser.js'
        const app=createApp({
            setup() {
       
                const position=reactive({
                    x:0,
                    y:0
                })
                //获取鼠标的位置坐标
                const update=e=>{
                    position.x=e.pageX
                    position.y=e.pageY
                }
                onMounted(()=>{
                    window.addEventListener('mousemove', update)
                })
                //onUnmounted类似于之前的destoryed
                onUnmounted(()=>{
                    window.removeEventListener('mousemove', update)
                })
            
                return {
                    position
                }
            },
            mounted(){
                this.position.x=100
            }
        })
        console.log(app)
        app.mount('#app')
    </script>
</body>
</html>

在上面的代码中,首先导入了onMounted,onUnmounted

  import {createApp,reactive,onMounted,onUnmounted} from './node_modules/vue/dist/vue.esm-browser.js'

定义获取鼠标位置的update函数

   //获取鼠标的位置坐标
                const update=e=>{
                    position.x=e.pageX
                    position.y=e.pageY
                }

onMounted钩子函数中注册mousemove事件

   onMounted(()=>{
                    window.addEventListener('mousemove', update)
                })

onUnmounted钩子函数中移除mousemove事件

    onUnmounted(()=>{
                    window.removeEventListener('mousemove', update)
                })
            

现在,功能已经实现了,下面我们在将代码进行重构。

将获取鼠标位置的功能封装到一个函数中。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    <div id="app">
        x:{{position.x}}<br/>
        y:{{position.y}}
    </div>
    <script type="module">
        import {createApp,reactive,onMounted,onUnmounted} from './node_modules/vue/dist/vue.esm-browser.js'
        //将获取鼠标位置坐标的代码封装到了useMousePosition函数中
          function useMousePosition(){
                const position=reactive({
                    x:0,
                    y:0
                })
                const update=e=>{
                    position.x=e.pageX
                    position.y=e.pageY
                }
                onMounted(()=>{
                    window.addEventListener('mousemove', update)
                })
                onUnmounted(()=>{
                    window.removeEventListener('mousemove', update)
                })
                //将positon对象返回
                return  position
                
          } 

        const app=createApp({
            setup() {
                //调用useMousePosition函数,接收返回的postion
               const position=useMousePosition()
               return {
                   position
               }
            },
            //下面我们在mounted的钩子函数中,修改一下positon中的x的值,来验证是否为响应式的。
            mounted(){
                this.position.x=100
            }
        })
        console.log(app)
        app.mount('#app')
    </script>
</body>
</html>

在上面的代码中定义了,useMousePosition函数,在该函数中封装了获取鼠标位置的业务代码,最终返回。

setup函数中调用useMousePositon函数,接收返回的positon,然后返回,这样在模板中可以展示鼠标的位置。

通过这个案例,我们可以体会出Composition APIoptions API的区别。

如果现在使用options api来实现这个案例,我们需要在data中定义x,y,在methods中定义update,这样把同一个逻辑的代码分散到了不同的位置,这样查找起来也非常的麻烦,而现在根这个逻辑函数相关的代码都封装到了一个函数中,这样方便了以后的维护也就是如果出错了,直接查找这个函数就可以了。并且这个函数也可以单独的放到一个模块中,这样在任何一个组件中都可以使用。

3、reactive/toRefs/ref

这一小节,我们来介绍Compositiont API中的三个函数reactive/toRefs/ref

这三个函数都是创建响应式数据的。

我们首先看一下reactive,该函数,我们在前面的案例中已经使用过,该函数的作用就是将一个对象设置为响应式的。现在,我们对前面写的程序进行一个简单的优化。

在模板中,我们是通过以下的方式来获取xy的值。

  <div id="app">
        x:{{position.x}}<br/>
        y:{{position.y}}
    </div>

这样写笔记麻烦,可以简化成如下的形式

  <div id="app">
        x:{{x}}<br/>
        y:{{y}}
    </div>

同时,在setup函数中,进行如下的修改:

//    const position=useMousePosition()
            const {x,y}=useMousePosition()
               return {
                   x,
                   y
               }
            },

我们知道,当我们调用useMousePosition方法的时候,返回的是一个position对象,该对象中包含了xy两个属性,那么这里我们可以进行解构处理。

最后将解构出来的x,y返回,这样在模板中就可以直接使用x,y.

但是,当我们的鼠标在浏览器中移动的时候,x,y的值没有发生变化,说明它们不是响应式的了。

原因是什么呢?

原因是,当对position对象进行解构的时候,就是定义了两个变量x,y来接收解构出来的值。所以这里的x,y就是两个变量,与原有的对象没有任何关系。

但是,这里我们还是希望对模板进行修改呢?

可以使用toRefs

首先导入toRefs

 import {createApp,reactive,onMounted,onUnmounted,toRefs} from './node_modules/vue/dist/vue.esm-browser.js'

然后在useMousePosition方法中,将返回的position对象,通过toRefs函数包括起来。

return toRefs(position)

toRefs函数,可以将一个响应式对象中的所有属性转换成响应式的。

现在,通过浏览器进行测试,发现没有任何的问题了。

下面我们来解释一下toRefs的工作原理,首先传递给toRefs函数的参数,必须是一个响应式的对象(代理的对象),而position就是一个通过reactive函数处理后的一个响应式对象。在toRefs方法内部首先会创建一个新的对象,然后会遍历传递过来的响应式对象内的所有属性,把属性的值都转换成响应式对象,然后再挂载到toRefs函数内部所创建的这个新的对象上,最后返回。

toRefs函数是把reactive返回的对象中的所有属性,都转换成了一个对象,所以对响应式对象进行解构的时候,解构出的每一个属性都是对象,对象是引用 传递的,所以解构出的数据依然是响应式的。

那么,我们解构toRefs返回的对象,解构出来的每个属性都是响应式的。

现在,我们先知道toRefs函数的作用就是:可以将一个响应式对象中的所有属性转换成响应式的。

后面还会继续探讨它的应用。

下面,我们再来看一下ref函数的应用

ref函数的作用就是把普通数据转换成响应式数据。与reactive不同的是,reactive是把一个对象转换成响应式数据。

ref可以把一个基本类型的数据包装成响应式对象。

下面,我们使用ref函数实现一个自增的案例。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <button @click="add">增加</button>
        <span>{{count}}</span>
    </div>
    <script type="module"> 
        import{createApp,ref} from './node_modules/vue/dist/vue.esm-browser.js'
        function useCount(){
            const count=ref(0)
            return {
                count,
                add:()=>{
                    count.value++
                }
            }
        }
        createApp({
            setup(){
                return {
                    ...useCount()
                }
            }
        }).mount('#app')
    </script>
</body>
</html>

下面,我们把上面的代码做一个解析:

首先在模板中有一个按钮,与展示数字的插值表达式。

下面导入createAppref函数。

虽然这个案例中的业务非常的简单,但是这里,我们还是单独的将其封装到一个函数中useCount中。

如果,我们写的是如下代码:

const count=0

那么,count就是一个基本的数据类型的变量。

但是,如果写成如下的形式:

const count=ref(0)

count就是一个响应式对象,该对象中会有一个value属性,该属性存储的就是具体的值。该属性中包含了getter/setter.

useCount方法将countadd方法返回。

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.

computed(()=>count.value+1)

第二种用法:

computed的第二种用法是传入一个对象,这个对象具有getter/setter,computed方法返回一个不可变的响应式对象,当获取值的时候,会执行getter,当设置值的时候,会执行该对象中的setter,后面我们会演示这种用法

const count=ref(1)
const plusOne=computed({
  	get:()=>count.value+1
    set:val=>{
    count.value=val-1
}
})

下面我们先来使用第一种用法

下面这个案例,会使用computed来计算出未处理的任务的个数。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <button @click="push">按钮</button>
        未完成{{activeCount}}
    </div>
    <script type="module">
        import {createApp,reactive,computed} from './node_modules/vue/dist/vue.esm-browser.js'
        const data=[
            {text:'看书',completed:false},
            {text:'睡觉',completed:false},
            {text:'玩游戏',completed:true},
        ]
        createApp({
            setup(){
                //在computed内部依赖的对象必须是一个响应式对象
                const todos=reactive(data)
                //computed返回的也是一个响应式对象,该对象中只有一个value属性
                const activeCount=computed(()=>{
                    return todos.filter(item=>!item.completed).length
                })
                //computed返回的是一个不可变的响应式对象,类似于使用ref创建的对象,只有一个value属性,获取计算属性的值,需要通过value属性来获取。如果模板中使用可以省略value.
                // console.log('activeCount===',activeCount.value)
                return {
                    activeCount,
                    push:()=>{
                        todos.push({
                            text:'打球',completed:false
                        })
                    }
                }
            }
        }).mount('#app')
    </script>
</body>
</html>

setup函数的内部,我们通过reactive函数将data对象转换成了响应式的对象,todos就是一个响应式对象。下面通过计算属性对响应式对象todos中的内容进行计算,计算出未完成的任务的个数,并且返回。将返回的数量保存到了activeCount中,然后将activeCount返回,这样在模板中就可以通过差值表达式来使用activeCount.同时,当单击了按钮以后,会向todos响应式对象中添加数据,这时会重新执行计算属性,计算没有完成的任务数量。

5、Watch

我们可以在setup函数中使用watch函数创建一个侦听器。监听响应式数据的变化,然后执行一个回调函数,可以获取到监听的数据的新值与旧值。

watch的三个参数

第一个参数:要监听的数据

第二个参数:监听到的数据变化后执行的函数,这个函数有两个参数,分别是新值和旧值。

第三个参数:选项对象,deep(深度监听)和immediate(立即执行)

Watch函数的返回值是一个函数,作用是用来取消监听。

watch与以前的this.$watch的使用方式是一样的,不一样的是watch第一个参数不是字符串(this.$watch第一个参数是字符串),而是refreactive返回的对象。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id='app'>
        <p>
            <input v-model="question">
        </p>
        <p>
            {{answer}}
        </p>
    </div>
    <script type="module">
        import{createApp,ref,watch} from './node_modules/vue/dist/vue.esm-browser.js'
        createApp({
            setup(){
                const question=ref('')
                const answer=ref('')
                watch(question,async(newValue,oldValue)=>{
                 
                    const response=await fetch('https://www.yesno.wtf/api')
                    const data=await response.json()
                    answer.value=data.answer
                })
                return {
                    question,
                    answer
                }
            }
        }).mount('#app')
    </script>
</body>
</html>

在上面的代码中,setup函数内,定义两个响应式对象分别是questionanswer,这两项的内容都是字符串,所以通过ref来创建。使用watch来监听quest的变化,由于question与文本框进行了双向绑定,当在文本框中输入内容后,question的值会发生变化,发生变化后,就会执行watch的第二个参数,也就是回调函数。该回调函数内,发送一个异步请求,注意这里使用了fetch方法,发送请求,该方法返回的是一个promise对象,所以这里使用了await,下面调用json方法获取json对象,注意该方法返回的也是promise,所以这里也使用了await,最后把获取到的数据给了answer中的value属性。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p>
            <input v-model="question">
        </p>
    </div>
    <script type="module">
           import {createApp,ref,watch} from './node_modules/vue/dist/vue.esm-browser.js'
           createApp({
               setup(){
                   const question =ref('')
                   watch(question,(newValue,oldValue)=>{
                       console.log('newValue==',newValue)
                       console.log('oldValue==',oldValue)
                   })
                   return{
                       question
                   }
               }
           }).mount('#app')
    </script>
</body>
</html>

6、WatchEffect

Vue3中还提供了一个新的函数WatchEffect.

WatchEffectWatch函数的简化版本,也用来监视数据的变化,WatchEffect接收一个函数作为参数,监听函数内响应式数据的变化,会立即执行一次该函数,当数据发生了变化后,会重新运行该函数。返回的也是一个取消监听的函数。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <button @click="add">add</button>
        <button @click="stop">stop</button>
         <br/>
         {{count}}   
    </div>
    <script type='module'>
        import {createApp,ref,watchEffect} from './node_modules/vue/dist/vue.esm-browser.js'
        createApp({
            setup(){
              const count=ref(0)
              const stop=watchEffect(()=>{
                  console.log(count.value)
              })
              return {
                  count,
                  stop,
                  add:()=>{
                      count.value++
                  }
              }
            }
        }).mount('#app')
    </script>
</body>
</html>

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

npm install -g @vue/cli

查看版本:

 vue -V

项目创建

vue create todolist

在创建的时候选择Vue3.0

App.vue文件中,构建出基本的结构

<template>
  <section id="app" class="todoapp">
    <header class="header">
      <h1>todos</h1>
      <input
        class="new-todo"
        placeholder="What needs to be done?"
        autocomplete="off"
        autofocus
      />
    </header>
    <section class="main">
      <input id="toggle-all" class="toggle-all" type="checkbox" />
      <label for="toggle-all">Mark all as complete</label>
      <ul class="todo-list">
        <li>
          <div class="view">
            <input class="toggle" type="checkbox" />
            <label>测试数据</label>
            <button class="destroy"></button>
          </div>
          <input class="edit" type="text" />
        </li>
        <li>
          <div class="view">
            <input class="toggle" type="checkbox" />
            <label>测试数据</label>
            <button class="destroy"></button>
          </div>
          <input class="edit" type="text" />
        </li>
      </ul>
    </section>
    <footer class="footer">
      <span class="todo-count">
        <strong>1</strong>
        item left
      </span>
      <ul class="filters">
        <li><a href="#/all">All</a></li>
        <li><a href="#/active">Active</a></li>
        <li><a href="#/completed">Completed</a></li>
      </ul>
      <button class="clear-completed">
        Clear completed
      </button>
    </footer>
  </section>
  <footer class="info">
    <p>Double-click to edit a todo</p>
  </footer>
</template>

<script>
import "./assets/index.css";
export default {
  name: "App",
  components: {},
};
</script>

<style></style>

在上面的代码中,导入了样式。

2、添加任务

在模板的header的内,给文本框添加v-model实现双向绑定,同时为其添加一个enter事件,该事件触发,表明用户按下了键盘上的回车键,那么这里就需要将用户在文本框中输入的内容保存到一个数组中。

 <header class="header">
      <h1>todos</h1>
      <input
        class="new-todo"
        placeholder="What needs to be done?"
        autocomplete="off"
        autofocus
        v-model="input"
        @keyup.enter="addTodo"
      />
    </header>

下面,将添加任务的业务单独的封装到一函数中处理。

<script>
import { ref } from "vue";
import "./assets/index.css";
//1、添加任务
const useAdd = (todos) => {
  const input = ref("");
  const addTodo = () => {
    const text = input.value && input.value.trim();
    if (text.length === 0) return;
    todos.value.unshift({
      text,
      completed: false,
    });
      input.value=""
  };
  return {
    input,
    addTodo,
  };
};
export default {
  name: "App",
  setup() {
    const todos = ref([]);
    return {
      ...useAdd(todos),
    };
  },
};
</script>

在上面的代码中,创建了useAdd这个函数,该函数完成保存任务数据,所以调用该函数的时候,需要传递一个数组。

在该函数内,创建input这个响应式对象(这里input的值为字符串,所以使用ref函数创建响应式对象,在这里输入ref,然后按下tab键,会自动导入ref函数),最后返回,然后在setup 函数中会将其解构出来,这样模板中就可以使用input这个响应式对象,该响应式对象中存储了用户在文本框中输入的值。

userAdd方法内,创建addTodo方法,该方法获取文本框中输入的值,并且构建出一个对象,插入到todos这个数组中,这里要求新输入的数据在最开始进行展示,所以这里使用了unshif函数来插入对象。最后返回。

setup函数中,对useAdd函数返回的结果进行解构,然后返回,这样在模板中就可以使用input 和addTodo方法。当然这里调用useAdd方法的时候,需要传递一个数组,注意这个数组也是响应式,因为我们把数据添加到该数组以后,列表要重新渲染展示最新的数据,在最开始创建数组的时候,默认就是一个空数组。

同时,还需要注意在addTodo方法中,使用的todos是一个响应式的对象(这里是通过ref([])创建出来,然后传递到了addTodo方法中),想使用它里面的值,需要通过value属性才能获取到,这时获取到的才是真正的数组。

todos.value.unshift({
      text,
      completed: false,
    });

下面,把todos中的数据展示出来。也就是在模板中遍历todos这个数组。

所以这里需要将todos这个数组,在setup函数中返回。

 setup() {
    const todos = ref([]);
    return {
      ...useAdd(todos),
      todos,
    };
  },

下面对列表进行循环遍历

   <ul class="todo-list">
        <li v-for="item in todos" :key="item.text">//这里可以写成item
          <div class="view">
            <input class="toggle" type="checkbox" />
            <label>{{ item.text }}</label>
            <button class="destroy"></button>
          </div>
          <input class="edit" type="text" />
        </li>
      </ul>

3、删除任务

在列表中找到删除按钮,并且为其添加单击事件,事件触发后调用remove方法,给该方法传递传递的是要删除的任务。

<ul class="todo-list">
        <li v-for="item in todos" :key="item.text">
          <div class="view">
            <input class="toggle" type="checkbox" />
            <label>{{ item.text }}</label>
            <button class="destroy" @click="remove(todo)"></button>
          </div>
          <input class="edit" type="text" />
        </li>
      </ul>

定义useRemove方法,当调用该方法的时候,传递todos数组,然后在该方法中实现remove方法,该方法就是根据传递过来的要删除的todo,从数组todos中,

找到对应的索引值(注意todos是一个响应式对象,todos.value才是真正的数组),然后调用splice方法将其从todos中删除。

最后返回remove这个方法。

// 2. 删除任务
const useRemove = (todos) => {
  const remove = (todo) => {
    const index = todos.value.indexOf(todo);
    todos.value.splice(index, 1);
  };
  return {
    remove,
  };
};

下面回到setup这个函数中,调用useRemove函数,传递todos这个响应式对象,然后将useRomove函数的返回值解构,解构出来的就是remove函数,这样在模板中就可以使用了。

  setup() {
    const todos = ref([]);
    return {
      ...useAdd(todos),
      ...useRemove(todos),
      todos,
    };
  },

4、编辑任务

整个编辑操作,要完成的功能如下:

  • 双击任务项,展示编辑文本框
  • 按回车键或者编辑文本框失去焦点,修改数据
  • 按下esc键取消编辑操作
  • 把编辑文本框内容清空,然后按下回车键,删除这一项
  • 显示编辑文本框的时候获取焦点

下面创建编辑函数:

// 3、编辑任务
const useEdit = (remove) => {
  //定义一个变量,存储要编辑的任务项,当取消编辑的时候,可以进行还原。不需要是响应式对象
  let beforeEditingText = "";
  //定义一个常量,记录当前是否为编辑状态,它是响应式的,它的值的改变要控制页面中文本框的显示与隐藏
  const editingTodo = ref(null); //默认情况下没有要编辑的数据
  //editTodo方法记录要编辑的任务的文体,以及要编辑的状态
  //参数todo就是当前要编辑的任务对象
  const editTodo = (todo) => {
    beforeEditingText = todo.text;
    //editingTodo是一个响应式对象,这里需要调用value属性
    editingTodo.value = todo;
  };
  //实现按下回车或者失去焦点后,修改数据的函数,参数为要编辑的任务对象
  const doneEdit = (todo) => {
    //判断是否为编辑的状态,如果editingTodo中没有值,表示没有处于编辑状态,直接返回
    if (!editingTodo.value) return;
    //获取到要编辑的任务的文本数据,去掉要编辑的文本的前后空格
    todo.text = todo.text.trim();
    //如果按下回车键后,发现要编辑的任务对象中没有数据,表明是把当前要编辑的任务对象删除。
    if (!todo.text) {
      remove(todo); //在调用useEdit函数的时候将remove传递过来。
    }
    //修改当前的状态,编辑完成后,状态变为非编辑状态。
    editingTodo.value = null;
  };
  //定义取消编辑的函数
  const cancelEdit = (todo) => {
    //将editingTodo的值设置为null,
    editingTodo.value = null;
    //让todo中的text的值还原为原有的值。
    todo.text = beforeEditingText;
  };
  return {
    editingTodo,
    editTodo,
    doneEdit,
    cancelEdit,
  };
};

下面,在setup函数中将useEdit方法的返回内容解构出来,这样在模板中就可以使用了。当然在调用useEdit方法的时候,需要传递一个remvoe函数,而该函数是useRemove方法解构出来的,所以这里需要在return返回之前,先把useRemove返回的remove解构出来,然后传递给useEdit方法,同时remove方法也要返回,因为在模板中使用到了该方法。

export default {
  name: "App",
  setup() {
    const todos = ref([]);
    const { remove } = useRemove(todos);
    return {
      ...useAdd(todos),
      ...useEdit(remove),
      remove,
      todos,
    };
  },
};

下面要处理的就是模板的内容了。

 <ul class="todo-list">
        <li
          v-for="item in todos"
          :key="item.text"
          :class="{ editing: item === editingTodo }"
        >
          <div class="view">
            <input class="toggle" type="checkbox" />
            <label @dblclick="editTodo(item)">{{ item.text }}</label>
            <button class="destroy" @click="remove(item)"></button>
          </div>
          <input
            class="edit"
            type="text"
            v-model="item.text"
            @keyup.enter="doneEdit(item)"
            @blur="doneEdit(item)"
            @keyup.esc="cancelEdit(item)"
          />
        </li>
      </ul>

在上面的代码中,我们给li添加了一个类样式,editing,如果当前的itemeditingTodo相等,表明是当前的这个任务处于编辑状态,所以要显示文本框。

这里我们给input文本框添加了v-model,这样文本框中会展示要编辑的数据,如果在文本框中输入了新数据item.text中的值为新数据,,添加了@key.enter事件,按下回车键该事件触发调用doneEdit方法完成更新,这时把当前的数据项传递到了doneEdit方法中,该方法中,会执行如下语句

todo.text = todo.text.trim();

把数据中的空格去掉后有赋值给了自己,也就是todo.text,然后,执行

 editingTodo.value = null;

这时候,为非编辑状态,不在显示文本框,而显示div,而div中的label中展示的数据为修改后的数据。

@blur="doneEdit(item)"

失去焦点的时候,也是调用doneEdit方法。

按下esc键的时候,执行cancelEdit方法,显示div中的内容不,不在展示文本框,同时内容还是原来的内容数据。

div中的label添加双击事件。

 <label @dblclick="editTodo(item)">{{ item.text }}</label>

现在,在测试的时候出现了一个问题就是,当我们双击数据项的时候,也就是label标签的时候,文本框可以展示出来,但是没有焦点,这一点我们后面解决。

那么,我们先来看另完一个问题,就是双击以后,出现文本框,然后单击一下有焦点了,然后输入内容,发现焦点又没有了。

这时什么原因呢?

原因是likey的问题

  <li
          v-for="item in todos"
          :key="item.text"
          :class="{ editing: item === editingTodo }"
        >

上面的代码中key绑定了item.text

然后文本框也绑定了item.text(v-model绑定了item.text)

 <input
            class="edit"
            type="text"
            v-model="item.text"
            @key.enter="doneEdit(item)"
            @blur="doneEdit(item)"
            @keyup.esc="cancelEdit(item)"
          />

当我们在文本框中输入值的时候item.text的值发生了变化。当key发生了变化后,重新渲染的时候发现新的li对应的vnode(虚拟DOM)与原有的livnodekey不相同,此时会重新生成li,导致其内部的子元素也会重新生成。由于重新生成了文本框,所以文本框内没有焦点。

如果item对象内有id属性,绑定该属性就可以解决这个问题了,但是这里没有,可以直接绑定item,因为每个item对象是不相等的。

 <li
          v-for="item in todos"
          :key="item"
          :class="{ editing: item === editingTodo }"
        >

经过测试,发现这时就解决了我们刚才提到的问题,因为由于key的值为item,那么在文本框中输入值的时候,对应的item对象没有变化,变化的是item中的text属性,对象还是原来的对象,只是其中的text的属性的值发生了变化。那么这样由于key的值不会发生变化,那么就不会重新生成li·以及内部的文本框,所以在这里输入内容的文本框还是原来的文本框。

最后,还有一个问题需要解决,就是双击以后,出现文本框,然后让其获取焦点。

5、编辑任务2

为了能够实现双击文本后,出现文本框,并且在文本框中显示焦点,这里需要用到指令来实现。

Vue3Vue2.x在自定义指令的差别,就是在钩子函数的命名上,vue3中把指令的钩子函数与组件的钩子函数保持一致,这样更容易理解

Vue2.x中指令的定义方式

Vue.directive('editingFocus',{
    bind(el,binding,vnode,prevVnode){},
    inserted(){},
    update(){},
    componentUpdated(){},
    unbind(){ }
})
bind:当指令绑定在对应元素时触发。只会触发一次。
inserted:当对应元素被插入到 DOM 的父元素时触发。
update:当元素更新时,这个钩子会被触发(此时元素的后代元素还没有触发更新)。
componentUpdated:当整个组件(包括子组件)完成更新后,这个钩子触发。
unbind:当指令被从元素上移除时,这个钩子会被触发。也只触发一次。

Vue3中指令的定义方式

app.directive('editingFocus',{
 beforeMount(el, binding, vnode, prevVnode) {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {},
  unmounted() {},
})
bind => beforeMount
inserted => mounted
beforeUpdate: 新的钩子,会在元素自身更新前触发
update => 移除!
componentUpdated => updated
beforeUnmount: 新的钩子,当元素自身被卸载前触发
unbind => unmounted

以上是自定义指令的第一种用法,在定义自定义指令的时候,还可以传函数。这样用法比较简洁,而且更加常用。第二个参数是函数的时候,vue2vu3的用法是一样的。

Vue2.x

Vue.directive('editingFocus',(el,binding)=>{
 binding.value && el.focus()
})

Vue3.0

app.directive('editingFocus',(el,binding)=>{
 binding.value && el.focus()
})

指令名称后面的这个函数在vue3中是在,mountedupdated的时候执行,与Vue2的执行时机是一样的。在Vue2中这个函数是在bindupdate的时候执行。

函数中的el参数是指令所绑定的元素,binding中可以获取指令对应的值。通过binding.value来获取。

下面,根文本框添加指令

 <input
            class="edit"
            type="text"
            v-model="item.text"
            v-editing-focus="item === editingTodo"
            @key.enter="doneEdit(item)"
            @blur="doneEdit(item)"
            @keyup.esc="cancelEdit(item)"
          />

input标签上添加了一个v-eding-focus的指令,该指令以v-开头。但是这里需要注意的一点就是,当文本框处于编辑状态的时候,该指令才起作用。所以,这里,判断item是否等于editingTodo,如果成立,也就是为true,表明处于编辑状态,则让文本框获得焦点。

v-editing-focus指令定义如下:

export default {
  name: "App",
  setup() {
    const todos = ref([]);
    const { remove } = useRemove(todos);
    return {
      ...useAdd(todos),
      ...useEdit(remove),
      remove,
      todos,
    };
  },
  directives: {
    editingFocus: (el, binding) => {
      if (binding.value) {
        el.focus();
      }
    },
  },
};

注意:directivessetup是同级定义的。

在定义指令的时候,不需要添加v-前缀。同时判断一下binding.value中的值是否为true,如果是,在让文本框获取焦点,因为当前处于编辑状态。

6、切换任务状态

  • 点击左上角的复选框(该复选框的高度与宽度都是1px,为了美观,这里单击的是label),改变所有任务的状态(要么全部完成,要么全部未完成)
  • 单击底部的All/Active/Completed按钮,单击All按钮,展示所有的任务。单击Active展示的是没有完成的任务,Completed展示的是已经完成的任务
  • 显示未完成的任务项个数。
  • 移除所有完成的项目
  • 如果没有任务项,隐藏mainfooter区域。

6.1 改变所有任务的状态

单击左上角的复选框,改变所有任务项的状态。

 <div class="view">
            <input class="toggle" type="checkbox" v-model="item.completed" />
            <label @dblclick="editTodo(item)">{{ item.text }}</label>
            <button class="destroy" @click="remove(item)"></button>
          </div>

li中的每个任务项名称前面都有一个复选框,这里复选框的状态是有completed属性来决定,所以这里通过v-model绑定completed属性。

根左上角的复选框绑定一个计算属性

 <input
        id="toggle-all"
        class="toggle-all"
        type="checkbox"
        v-model="allDone"
      />
//4、切换任务的状态。
//这里单击左上角的复选框后,会改变所有任务项的状态,同时,如果所有的任务项都选中,左上角的复选框也选中
//只要有一个未选中,则该复选框不选中。这里复选框的状态是受`todos`这个数组中的数据的影响,所以通过计算属性来完成
const userFilter = (todos) => {
  const allDone = computed({
    //渲染左上角的checkbox的时候,会执行`get`
    //在`get`方法中,遍历`todos`,获取每个任务项的状态,从而决定左上角的复选框的状态。
    //如果`todos`中的任务都完成处理,返回true,只有有一项没有完成则返回false.
    get() {
      //注意todos是响应式对象,通过value获取数组。下面这行代码可以获取到todos数组中所有完成的任务。
      // return todos.value.filter((todo) => todo.completed);
      //获取的是所有未完成的任务的数量
      // return todos.value.filter((todo) => !todo.completed).length;
      // 在todos前面加上了!,表示的含义是,如果所有的任务都完成(所有的任务都完成,那么长度length的值为0,前面取反,则为true)返回true.如果有一项任务没有完成,则返回false.
        //当单击每个任务项前面的复选框时,都会执行该计算属性的get方法,因为每一个任务项前面的复选框通过`v-model`绑定了todos中的completed属性,该属性值的变化,导致`todos`这个数组中的数据变化,所以get方法会执行。
      return !todos.value.filter((todo) => !todo.completed).length;
    },

    //当单击左上角的checkbox的时候,会执行set操作。在set方法中,设置每个任务的状态.与左上角的checkbox的状态保持一致,该方法的参数value,表示的是当单击左上角的checkbox的时候,会将该checkbox的状态传递过来。所以value参数存储的就是左上角复选框的状态。那么根据传递过来的状态修改'todos'中所有任务项的`completed`属性的值。
      //当todos中的`completed`中的状态发生了变化,会重新渲染视图(todos响应式数据)。
    set(value) {
      todos.value.forEach((todo) => {
        todo.completed = value;
      });
    },
  });
  return {
    allDone,
  };
};

下面要解决的是,当处理完某个任务后,为其添加上对应的横线。

 <li
          v-for="item in todos"
          :key="item"
          :class="{ editing: item === editingTodo, completed: item.completed }"
        >

item.completed属性为true的时候,给列表添加completed样式。

6.2 切换状态

单击底部的All/Active/Completed按钮,单击All按钮,展示所有的任务。单击Active展示的是没有完成的任务,Completed展示的是已经完成的任务

以上三个按钮,都是基于hash的链接,当单击这些链接的时候,会触发hashchange事件,在事件的处理函数中,我们可以获取到对应的hash值,然后根据hash值,调用不同的过滤方法,查询对应的数据。

const userFilter = (todos) => {
  const allDone = computed({
    //渲染左上角的checkbox的时候,会执行`get`
    //在`get`方法中,遍历`todos`,获取每个任务项的状态,从而决定左上角的复选框的状态。
    //如果`todos`中的任务都完成处理,返回true,只有有一项没有完成则返回false.
    get() {
      console.log("abc");
      //注意todos是响应式对象,通过value获取数组。下面这行代码可以获取到todos数组中所有完成的任务。
      // return todos.value.filter((todo) => todo.completed);
      //获取的是所有未完成的任务的数量
      // return todos.value.filter((todo) => !todo.completed).length;
      // 在todos前面加上了!,表示的含义是,如果所有的任务都完成(所有的任务都完成,那么长度length的值为0,前面取反,则为true)返回true.如果有一项任务没有完成,则返回false.
      return !todos.value.filter((todo) => !todo.completed).length;
    },

    //当单击左上角的checkbox的时候,会执行set操作。在set方法中,设置每个任务的状态.与左上角的checkbox的状态保持一致,该方法的参数value,表示的是当单击左上角的checkbox的时候,会将该checkbox的状态传递过来。所以value参数存储的就是左上角复选框的状态。
    set(value) {
      todos.value.forEach((todo) => {
        todo.completed = value;
      });
    },
  });
    
    
  const filter = {
    all: (list) => list,
    active: (list) => list.filter((todo) => !todo.completed),
    completed: (list) => list.filter((todo) => todo.completed),
  };
  const type = ref("all");
  const filterreadTodos = computed(() => filter[type.value](todos.value));
  const onHashChange = () => {
    const hash = window.location.hash.replace("#/", "");
    if (filter[hash]) {
      type.value = hash;
    } else {
        //有可能首次加载,或者输入了错误的hash值
      type.value = "all";
      window.location.hash = "";
    }
  };

  onMounted(() => {
    window.addEventListener("hashchange", onHashChange);
    onHashChange();
  });
  onUnmounted(() => {
    window.removeEventListener("hashchange", onHashChange);
  });

  return {
    allDone,
    filterreadTodos,
  };
};

模板中遍历的是filterreadTodos

 <li
          v-for="item in filterreadTodos"
          :key="item"
          :class="{ editing: item === editingTodo, completed: item.completed }"
        >

整个数据的过滤的操作还是定义在userFilter方法中完成。在该方法中,指定onMounted钩子函数,也就是组件挂载完毕后,完成hashchange事件的绑定,

事件触发以后执行``

 onMounted(() => {
    window.addEventListener("hashchange", onHashChange);
     onHashChange();
  });

当第一次进入页面,还没有单击链接的时候,也要调用 onHashChange();方法,进行数据的过滤,当单击了链接以后,hashchange事件触发也要调用onHashChange方法,完成数据的过滤。

onUnmounted方法中移除事件的绑定。

 onUnmounted(() => {
    window.removeEventListener("hashchange", onHashChange);
  });

下面看一下 onHashChange();方法的实现

  const onHashChange = () => {
    const hash = window.location.hash.replace("#/", "");
    if (filter[hash]) {
      type.value = hash;
    } else {
         //有可能首次加载,或者输入了错误的hash值
      type.value = "all";
      window.location.hash = "";
    }
  };

onHashChange方法中,首先获取hash值,然后将#/去掉,只保留all或者是active,completed.

然后判断在filter中是否定义了对应的过滤方法。

filter类的实现如下:

  const filter = {
    all: (list) => list,
    active: (list) => list.filter((todo) => !todo.completed),
    completed: (list) => list.filter((todo) => todo.completed),
  };

filter中,定义了不同的过滤方法。

如果根据获取到的hash值能够从filter中查询到对应的过滤方法。那么把hash值赋值给type.value

type是一个响应式对象,默认值为all

 const type = ref("all");

type的值发生了改变,就会重新渲染视图,渲染视图的时候会执行 v-for="item in filterreadTodos",而filterreadTodos是一个计算属性。

 const filterreadTodos = computed(() => filter[type.value](todos.value));

该计算属性依赖的type的值发生变化,所以会执行computed内的函数,根据type中存储的,从filter中查询到对应的过滤函数,把todos数组中的内容传递到该函数中进行过滤。

以上是在filter中找到对应的过滤方法的情况。

如果在filter中找不到对应的过滤方法,会执行:

 type.value = "all";
      window.location.hash = "";

执行上面的代码,有可能是首次加载,也有可能是输入的hash值是错误的。

这时把type的值修改成all,表示加载所有的任务数据。并且把地址栏中的hash值清空。

注意:最开开始要导入对应的钩子函数。

import { ref, computed, onMounted, onUnmounted } from "vue";

同时,还需要注意,最后一定要将filterreadTodos内容返回。

  return {
    allDone,
    filterreadTodos,
  };

6.3 剩余内容处理

  • 显示未完成的任务项个数。
  • 移除所有完成的项目
  • 如果没有任务项,隐藏mainfooter区域。

下面先来实现第一条内容:显示未完成的任务项个数。

这个功能需要通过计算属性来完成,也就是统计todos数组中数据的变化情况。

 <footer class="footer">
      <span class="todo-count">
        <strong>{{ remainingCount }}</strong>
        {{ remainingCount > 1 ? "items" : "item" }} left
      </span>

整个数据的统计是有remainingCount计算属性完成的,该属性返回的值如果大于1,显示items,否则显示item.

关于remainingCount计算属性的实现还是在userFilter方法中完成,该方法都是与过滤相关的业务内容。

const filterreadTodos = computed(() => filter[type.value](todos.value));
  const remainingCount = computed(() => filter.active(todos.value).length);

在原有的filterreadTodos整个过滤器的下面,定义了remainingCount过滤器,该过滤器调用了filter中的active方法来获取未处理的任务项数据,然后再统计其个数。当第一次渲染的时候,会执行remainingCount计算属性,当todos数组中的数据发生了变化后,还会执行该计算属性,来进行数据的统计。

最后需要将remainingCount计算属性返回,这样在模板中才能使用。

return {
    allDone,
    filterreadTodos,
    remainingCount,
  };

下面要实现的是"移除所有完成的项目"

给底部右下角的按钮绑定单击事件。

     <button class="clear-completed" @click="removeCompleted">
        Clear completed
      </button>

removeCompleted方法定义在useRemove方法中,该方法是有关删除的相关业务的内容。

// 2. 删除任务
const useRemove = (todos) => {
  const remove = (todo) => {
    const index = todos.value.indexOf(todo);
    todos.value.splice(index, 1);
  };
  //删除已经完成的任务项数据
  const removeCompleted = () => {
    //这里把未完成的数据过滤出来,展示的页面中就可以了。
    todos.value = todos.value.filter((todo) => !todo.completed);
  };
  return {
    remove,
    removeCompleted,//最后要将removeCompleted函数返回
  };
};

removeCompleted方法中,把todos数组中未完成的数据查询出来,重新赋值给todos数组,这样数组todos数组中存储的就是未完成处理的数据。当todos中的内容发生了变化后,计算属性filterreadTodos也会重新执行,从而重新渲染列表。注意:最后要将removeCompleted函数返回

下面修改一下setup函数中的内容。

 setup() {
    const todos = ref([]);
     //将useRomve函数中定义的removeCompleted方法解构出来
    const { remove, removeCompleted } = useRemove(todos);
    return {
      ...useAdd(todos),
      ...useEdit(remove),
      ...userFilter(todos),
      remove,
      removeCompleted,//返回removeCompleted方法,这样模板中就可以使用了。
      todos,
    };
  },

如果没有任务项,隐藏mainfooter区域。

如果没有数据,只显示输入的文本框,隐藏其它的位置

下面给footermain,都去添加v-show

 <section class="main" v-show="count">
  <footer class="footer" v-show="count">

count中有值,则展示mainfooter中的内容,否则进行隐藏。count中的值为任务的总数。

这里的count必须是一个计算属性,因为count的值依赖响应式数据todos.todos的数据发生了变化,需要重新计算count的值。

下面,我们在userFilter中实现count这个计算属性。

 const remainingCount = computed(() => filter.active(todos.value).length);
  const count = computed(() => todos.value.length);

remainingCount下面定义count这个计算属性,同时将count返回。

  return {
    allDone,
    filterreadTodos,
    remainingCount,
    count,
  };

7、本地存储

下面要将数据存储起来,否则单击浏览器的刷新按钮,数据会丢失。这里是把任务数据存储到localStorage中。

src目录下面定义utils目录,在该目录下面创建useLocalStorage.js文件,该文件中的代码如下:

function parse(str){
    let value;
    try{
        value=JSON.parse(str)
    }catch{
        value=null   
    }       
    return value
}
function stringify(obj){
    let value
    try{
        value=JSON.stringify(obj)
    }catch{
        value=null
    }
    return value
}
export default function useLocalStorage(){
    function setItem(key,value){
        value=stringify(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
    }
}

在上面的代码中创建了parse函数,该函数将字符串转换成对象,stringify函数将对象转换成字符串。

同时导出了useLocalStorage函数,该函数中封装了对localStorage操作的两个方法,setItem添加数据到localStorage中,getItem把数据从localStorage找那个取出来。

最后返回setItemgetItem这两个方法。

现在回到App.vue文件中。

import { ref, computed, onMounted, onUnmounted, watchEffect } from "vue";
import useLocalStorage from "./utils/useLocalStorage";
import "./assets/index.css";

const storage = useLocalStorage();

首先导入useLocalStorage函数,接下来调用该函数,我们知道该函数返回的是一个对象,该对象中存储的是getItemsetItem,这里都存储到常量storage中。

我们知道,当数据发生变化后,不管是添加,更新,还是删除,都需要将变化后的数据重新的保存到localStorage中,哪么应该怎样处理呢?如果在添加,删除,更新的方法中都写这些代码,那么就比较麻烦了。所以这里我们可以在封装一个方法,这个方法里完成对localStorage中数据更新的操作。

//5、存储任务数据
const useStorage = () => {
  const key = "todokey";
  const todos = ref(storage.getItem(key) || []);
  watchEffect(() => {
    storage.setItem(key, todos.value);
  });
  return todos;
};

useStorage方法中,首先获取localStorage中的数据,获取到了给todos,如果没有获取到返回的就是一个空数组。注意这里的todos是一个响应式的对象。

下面我们通过watchEffect函数检测todos中的数据是否有变化,如果有变化执行watchEffect内的回调函数,将数据重新保存到localStroage中。最后把响应式对象todos返回。

下面修改setup方法中的代码

 setup() {
    const todos = useStorage();
    const { remove, removeCompleted } = useRemove(todos);
    return {
      ...useAdd(todos),
      ...useEdit(remove),
      ...userFilter(todos),
      remove,
      removeCompleted,
      todos,
    };
  },

setup这个方法中,我们调用了useStorage方法,把返回的内容给了todos这个常量,注意:这个常量也是响应式对象,因为useStorage方法返回的就是一个响应式对象。当我们第一次执行的时候,从storage中获取数据,后期todos中数据有变化都会执行watchEffect这个函数,然后将数据保存到localStorage中。

以上,就是整个todoList案例,在这个案例中,我们将不同的业务逻辑封装到了不同的函数中,整体的代码结构非常的情形。

function setItem(key,value){
    value=stringify(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
}

}


在上面的代码中创建了`parse`函数,该函数将字符串转换成对象,`stringify`函数将对象转换成字符串。

同时导出了`useLocalStorage`函数,该函数中封装了对`localStorage`操作的两个方法,`setItem`添加数据到`localStorage`中,`getItem`把数据从`localStorage`找那个取出来。

最后返回`setItem`与`getItem`这两个方法。

现在回到`App.vue`文件中。

```js
import { ref, computed, onMounted, onUnmounted, watchEffect } from "vue";
import useLocalStorage from "./utils/useLocalStorage";
import "./assets/index.css";

const storage = useLocalStorage();

首先导入useLocalStorage函数,接下来调用该函数,我们知道该函数返回的是一个对象,该对象中存储的是getItemsetItem,这里都存储到常量storage中。

我们知道,当数据发生变化后,不管是添加,更新,还是删除,都需要将变化后的数据重新的保存到localStorage中,哪么应该怎样处理呢?如果在添加,删除,更新的方法中都写这些代码,那么就比较麻烦了。所以这里我们可以在封装一个方法,这个方法里完成对localStorage中数据更新的操作。

//5、存储任务数据
const useStorage = () => {
  const key = "todokey";
  const todos = ref(storage.getItem(key) || []);
  watchEffect(() => {
    storage.setItem(key, todos.value);
  });
  return todos;
};

useStorage方法中,首先获取localStorage中的数据,获取到了给todos,如果没有获取到返回的就是一个空数组。注意这里的todos是一个响应式的对象。

下面我们通过watchEffect函数检测todos中的数据是否有变化,如果有变化执行watchEffect内的回调函数,将数据重新保存到localStroage中。最后把响应式对象todos返回。

下面修改setup方法中的代码

 setup() {
    const todos = useStorage();
    const { remove, removeCompleted } = useRemove(todos);
    return {
      ...useAdd(todos),
      ...useEdit(remove),
      ...userFilter(todos),
      remove,
      removeCompleted,
      todos,
    };
  },

setup这个方法中,我们调用了useStorage方法,把返回的内容给了todos这个常量,注意:这个常量也是响应式对象,因为useStorage方法返回的就是一个响应式对象。当我们第一次执行的时候,从storage中获取数据,后期todos中数据有变化都会执行watchEffect这个函数,然后将数据保存到localStorage中。

以上,就是整个todoList案例,在这个案例中,我们将不同的业务逻辑封装到了不同的函数中,整体的代码结构非常的情形。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/376004.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

电动汽车雷达技术概述 —— FMCW干扰问题(第二篇)

此图片来源于网络 1、雷达干扰问题 此图表示道路上的典型场景。 两辆支持雷达的汽车相互通过。 在过去&#xff0c;这是不太可能的事件。 然而&#xff0c;随着越来越多的77千兆赫雷达汽车 在道路中行驶&#xff0c;这种事件发生的可能性变得越来越高。 因此&#xff0c;一个…

华为数通方向HCIP-DataCom H12-821题库(单选题:441-460)

第441题 下面是一台路由输出的信息,关于这段信息描述正确的是 <R1>display bgp peerBGP local router ID : 2.2.2.2Local AS number : 100Total number of peers : 2 Peers in established state : 0Peer V AS MsgRcvd MsgSent OutQ Up/Down …

【JavaScript】Js中一些数组常用API总结

目录 前言 会改变原数组 push() pop()和shift() unshift() splice() sort() reverse() 不会改变原数组 slice() concat() filter() forEach() toString join(分隔符&#xff09; 小结 前言 Js中数组是一个重要的数据结构&#xff0c;它相比于字符串有更多的方法…

Android7.0-Fiddler证书问题

一、将Fiddler的证书导出到电脑&#xff0c;点击Tools -> Options -> HTTPS -> Actions -> Export Root Certificate to Desktop 二、下载Window版openssl&#xff0c; 点击这里打开页面&#xff0c;下拉到下面&#xff0c;选择最上面的64位EXE点击下载安装即可 安…

node cool-admin 后端宝塔面板看代码日志

1.需求 我在处理回调问题的时候 就是找不到问题&#xff0c;因为不像本地的代码 控制台能够直接打印出来问题&#xff0c;你是放在线上了 所以那个日志不好打印 我看网上都说是 直接用一个loger.js 打印 日志 放到代码文件里 这种方法也许有用 但是对我这框架cool来说 试了没有…

使用 Kubernetes,基础设施层面如何优化?分享一些解决方案

重点内容 搭配 SmartX 自主研发的 Kubernetes 服务、分布式存储、Kubernetes 原生存储等产品&#xff0c;用户既可基于 SmartX 超融合构筑全栈 Kubernetes 基础设施&#xff0c;也可选择为部署在裸金属、其他虚拟化平台或混合环境的 Kubernetes 集群提供持久化存储支持。 文末…

基于YOLOv8的暗光低光环境下(ExDark数据集)检测,加入多种优化方式---DCNv4结合SPPF ,助力自动驾驶(一)

&#x1f4a1;&#x1f4a1;&#x1f4a1;本文主要内容:详细介绍了暗光低光数据集检测整个过程&#xff0c;从数据集到训练模型到结果可视化分析&#xff0c;以及如何优化提升检测性能。 &#x1f4a1;&#x1f4a1;&#x1f4a1;加入 DCNv4结合SPPF mAP0.5由原始的0.682提升至…

金融行业专题|证券超融合架构转型与场景探索合集(2023版)

更新内容 更新 SmartX 超融合在证券行业的覆盖范围、部署规模与应用场景。新增操作系统信创转型、Nutanix 国产化替代、网络与安全等场景实践。更多超融合金融核心生产业务场景实践&#xff0c;欢迎阅读文末电子书。 在金融行业如火如荼的数字化转型大潮中&#xff0c;传统架…

[Python进阶] 制作动态二维码

11.1 制作动态二维码 二维码&#xff08;QR code&#xff09;是一种二维条形码&#xff08;bar code&#xff09;&#xff0c;它的起源可以追溯到20世纪90年代初。当时&#xff0c;日本的汽车工业开始使用一种被称为QR码的二维条码来追踪汽车零部件的信息。 QR码是Quick Respo…

品牌如何营造生活感氛围?媒介盒子分享

「生活感」简而言之是指人们对生活的感受和意义&#xff0c;它往往没有充斥在各种重要的场合和事件中&#xff0c;而是更隐藏在细碎平凡的生活场景中。在营销越来越同质化的当下&#xff0c;品牌应该如何打破常规模式&#xff0c;洞察消费情绪&#xff0c;找到更能打动消费者心…

Python(20)正则表达式(Regular Expression)中常用函数用法

大家好&#xff01;我是码银&#x1f970; 欢迎关注&#x1f970;&#xff1a; CSDN&#xff1a;码银 公众号&#xff1a;码银学编程 正文 正则表达式 粗略的定义&#xff1a;正则表达式是一个特殊的字符序列&#xff0c;帮助用户非常便捷的检查一个字符串是否符合某种模…

14. 【Linux教程】文件压缩与解压

文件压缩与解压 前面小节介绍了如何对文件和目录删除、移动操作&#xff0c;本小节介绍如何使用命令对文件和目录进行压缩与解压操作&#xff0c;常见的压缩包格式有 .bz2、.Z、.gz、.zip、.xz&#xff0c;压缩之后的文件或目录占用更少的空间。 1. tar 命令介绍 下面列举 ta…

【C++】基础知识讲解(命名空间、缺省参数、重载、输入输出)

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;http://t.csdnimg.cn/eCa5z 目录 命名空间 命名空间的定义 命名空间的使用 命名空间的嵌套使用 C输入&输出 std命名空间的使用惯例&…

阿里云服务器centos_7_9_x64位,3台,搭建k8s集群

目录 1.环境信息 2.搭建过程 2.1 安装Docker源 2.2 安装Docker 2.3 安装kubeadm&#xff0c;kubelet和kubectl 2.4 部署Kubernetes Master(node1) 2.5 安装Pod网络插件&#xff08;CNI&#xff09; 2.6 加入Kubernetes Node 2.7 测试kubernetes集群 3.部署 Dashboard…

webrtc native api的几个要点

文章目录 基本流程状态回调类sdp的中媒体行pc对象 基本流程 webrtc native的接口&#xff0c;主要就是围绕着PeerConnection对象&#xff0c;一个PeerConnection对象它代表了一次音视频会话。 那么通过PeerConnection对象建立音视频通话&#xff0c;包括如下步骤&#xff1a; …

回归预测 | Matlab实现POA-BP鹈鹕算法优化BP神经网络多变量回归预测

回归预测 | Matlab实现POA-BP鹈鹕算法优化BP神经网络多变量回归预测 目录 回归预测 | Matlab实现POA-BP鹈鹕算法优化BP神经网络多变量回归预测预测效果基本描述程序设计参考资料 预测效果 基本描述 1.Matlab实现POA-BP鹈鹕算法优化BP神经网络多变量回归预测&#xff08;完整源码…

光伏板安装角度有什么讲究?

随着太阳能技术的日益普及&#xff0c;光伏板&#xff08;也称为太阳能电池板&#xff09;已成为我们日常生活中不可或缺的一部分。在安装光伏板时&#xff0c;选择合适的安装角度是一个至关重要的环节&#xff0c;它直接影响到光伏系统的效率和发电量。本文将探讨光伏板安装角…

RabiitMQ延迟队列(死信交换机)

Dead Letter Exchange&#xff08;死信交换机&#xff09; 在MQ中&#xff0c;当消息成为死信&#xff08;Dead message 死掉的信息&#xff09;后&#xff0c;消息中间件可以将其从当前队列发送到另一个队列中&#xff0c;这个队列就是死信队列。而 在RabbitMQ中&#xff0c;由…

Android14音频进阶:MediaPlayerService如何启动AudioTrack 上篇(五十五)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只…

正确入市时机3秒抓住,WeTrade众汇无偿实例分享

在上篇文章中&#xff0c;WeTrade众汇无偿分享如何3秒抓住正确入市的时机&#xff0c;今天让我们通过一个例子来验证这个策略的正确性。 对于突破策略&#xff0c;WeTrade众汇用了同样的图表来演示挤压交易。蓝色箭头表示变窄的区域&#xff0c;红色箭头表示烛台穿过下层。当它…
最新文章