尚硅谷vue2的todolist案例解析,基本概括了vue2所有知识点,结尾有具体代码,复制粘贴学习即可

发布于:2024-06-27 ⋅ 阅读:(20) ⋅ 点赞:(0)

脚手架搭建

1-初始化脚手架(全局安装)

npm install -g @vue/cli

2-切换到创建项目的空目录下

vue create xxxx

整体结构

整体思路

App定义所有回调方法 增删改查 还有统一存放最终数据,所有子组件不拿数据,由App下发数据,类似于数据中心

打个比方,所有子组件只是个可以反复利用的壳子,类似于高达积木,而正在要来拼接他们的是我们的APP组件,有大脑想法,可以决定怎么拼好看,且怎么上色也就是怎么赋予数据得当

当然下放数据可以给其第二个大脑List

第二个数据中心为App给List传的数据todos,其遍历出来的每一个todo下发每一个Item组件,也就是在Item组件里v-for,该item里面的数据隔离开其他数据,可以进行编辑,你也可也在一个List页面同时写了ul,li,此时遍历出来的每个li如果要控制他们单独编辑状态还必须v-if=“showInput&&currRow=i” 来把所有的input同时遍历出来,然后当点击了按钮后传currRow当前编辑行设置为第几个i,然后showInput变为true,

删除操作就传递id然后触发事件遍历过滤出去就行

所有子组件触发的事件 App全部监听 事件可以通过pubsub消息订阅,事件总线,子父组件事件触发和监听方法实现, 父给子传递数据就使用 <sonComponent :子组件里的props接收名=“父组件的数据字段名”> 也可以传递一个方法。子组件props:[‘数据名’]接收

依赖安装

第三方的依赖引入import放在最上面

名称 作用 使用场合
nanoid 生成唯一的Id,防止id冲突 不用手动递增ID
pubsub 消息订阅与发布 孙组件给爷组件传递数据时

依赖用法

nanoid

 npm i nanoid
 import { nanoid } from "nanoid";
 nanoid()

pubsub

npm i pubsub
import pubsub from 'pubsub-js'

父组件挂载时订阅,销毁时取消订阅

  mounted() {
        this.pubId = pubsub.subscribe('deleteTodo', this.deleteTodo)
    },
    beforeDestroy() {
        pubsub.unsubscribe(this.pubId)
    },

子组件传递数据,

      //删除
        handlerDelete(id){
        if (confirm('确定要删除吗?')) {
          pubsub.publish('deleteTodo',id)
        }

注意事项

当你发现一个组件很难命名的时候,可能该组件包含了多个功能,需要更细致的拆分组件

例如Header用来输入信息,List用来展示,编辑,删除操作,这两个就必须拆分开

h5自身有header,head组件,不能起和这俩一样的名字,会有警告,可以在前面加个前缀或直接叫Top还是别的名字

要求

孙组件与父组件通信时使用全局事件总线和消息订阅与发布

update和check使用全局事件总线

deleteTodo使用消息订阅与发布

命名要求

组件

MyHeader
列表 MyList
每一个item MyItem
页脚 MyFooter

方法

添加 addTodo/handleAdd
勾选 checkTodo/handleCheck/checkAllTodo
编辑 updateEdit/handleEdit
删除 deleteTodo/deleteAllTodo/handleDelete
全选 checkAllTodo

数据

所有todo todos
单个todo todo
编辑状态 isEdit
是否全选 isAll
完成状态 done
todo标题 title
todoId todoId
todo对象 todoObj
已完成 doneTotal
所有 total

涉及的操作 主要内容

$event

传递原生事件,属性target获取触发该事件的DOM对象,DOM对象的value获取其值,用于输入框失焦所触发事件获取输入框值

本地存储+watch深度监视

搭配watch监听器,监听某个数据对象,深度监视为真,表示监视该对象属性

内部设置handler处理函数当数据发生变化时触发,接收两值分别为,newVal变化后值、oldVal变化前值

 watch: {
        todos: {
            deep: true,
            handler(value) {
                localStorage.setItem('todos', JSON.stringify(value))
            }
        }
    }

只需要第一个newVal,该值为todos对象,由于localStorage存储必须为JSON字符串不能为js对象,需要先转,且取值的时候需要从json字符串转为js对象

localStorage.setItem('todos', JSON.stringify(value))

钩子函数/全局事件总线/ref命名/vue原型

vue实例创建前,给vm原型绑定一个全局变量bus,值为vm本身,这样后续所有父组件可以在bus上监听,子组件在上面触发

$bus.on监听(‘事件名’,回调)

$bus.emit触发(‘事件名’,参数)

打个比方,on相当于耳朵在听,听到了就根据听到的内容做出对应的动作,也就是回调

而emit相当于说,说给耳朵听, 之前单纯的emit只能在父子组件通信,现在引入了一个传话的,隔代通信,事件名相当于一个人名,和谁通信的标识,当然这个on必须在挂载后执行类似于父组件监听子组件的 <Son @Event=回调 />

当然,该传话人必须让所有组件都能看到,访问到,往VueComponent.protoType身上放?每一个标签都会由Vue.extend生成一个新的VueComponent,这样所有组件都能看到this.$bus了?

image-20240624142833094

以下的VueComponent简写成vc,vue实例对象简写成vm (viewModule命名来源于Model-View-ViewModule)

MVVM扫盲:

Module——数据

View——视图(div,input,span…)

ViewModule——方法(视图通过方法来获取到数据的途径, 视图数据间的桥梁)

但是该vc是vue实例对象也就是vm.extend后才有的。一开始没有的,所以该绑定无效,当然你也可以直接改源码,每次extend生成的VueComponent都有该共同传话人(不建议)

可以将其放置在vc的缔造者,vm身上 this。。如果子组件身上的this里没有bus属性则找vm的this,类似于java里的extend继承父类,父类vm有的所有属性子类都有这样就能通过this.$bus.on/emit调用,思路有了。该如何写呢?这里先插入解释一下之前 父子通信的过程

上面听不懂的你就这样记。我们在找一个大家都能 看到的人,委托他帮忙传话,
举个例子, 你要和校长聊天,但是没校长联系方式。你这个时候得找你们班主任 转达你要说的内容给校长,而校长要和你聊天,他也得通过班主任

image-20240624145314436
image-20240624143557840
这里的原型对象和java里的类相似,而实例对象等于类似于java里new出来的实例对象
知道了 o n , e m i t , o f f 在原型对象上了之后且,原型对象 v c 和 v m 都能看到,那问题就好办了,挑一个 v c > 还是 v m 设置为 on,emit,off在原型对象上了之后且,原型对象vc和vm都能看到,那问题就好办了,挑一个vc>还是vm设置为 on,emit,off在原型对象上了之后且,原型对象vcvm都能看到,那问题就好办了,挑一个vc>还是vm设置为bus的属性即可

第一种vc,(好像有点麻烦)


第二种vm

image-20240624152300766
总结一句话, 在原型上的bus属性放置了 vm自身(携带方法on emit off) 所有组件可以通过this. b u s . bus. bus.on 通信

为什么叫$bus? 而不是随便起一个x还是什么乱七八糟的名字。bus翻译过来有总线的意思 而this为全局, 所以称全局事件总线

$ 符号意为 规范用法 vue原型身上的api都用 $ 例如import request from axios.js |||| Vue.prototype.$request=request等等…

插入一句话——————

父组件这样@事件名=回调其实等同于在子组件里 this. $ on(‘事件名’,回调) 监听, ,子组件里this. $ emit触发…都是在同一个VC下使用原型的 $ on emit属性(当然还有一个解除绑定this.$off)所以父子可以通信

当然子组件监听子组件触发 不是有点废话?一般用于同级组件也称兄弟组件,先ref组件命名再指定哪个组件进行监听on,on需在mounted挂载之后执行

image-20240624153935379

image-20240624153911998

注意
on不能重名,可以将其分模块到一个文件下统一constant常量

$ on 销毁前需 $ off 掉 为什么?因为当子组件销毁时 不会自动取消父组件的订阅,你还隔这占着 $ bus傀儡中间人,数量一多,整个vue服务器容易性能下降,好比数据库不使用连接池,你连接多了不用又不释放,占用资源,时间长卡顿甚至数据库崩溃,那为什么vc不用off?,因为vc关闭事件也跟着没,而这里的事件监听on是在 原型上的,一直存在的不受vc关闭而关闭

beforeCreate() {
Vue.prototype.$bus = this
},
image-20240624140051368

挂载后**mounted**,订阅消息,事件总线监听

image-20240625100416758

销毁前**beforeDestroy**,关闭所有订阅,和事件总线监听

image-20240625100423771

至此,我们可以再任意组件之间通信

父子组件传值(数据/方法)

image-20240624154948689

:子组件内部值, =号右边为 父组件自身数据

image-20240624155045577

也可以通过插槽

image-20240624160055792

image-20240624160043944

子< slot name=“body” :rows=“data”> :传递出去的数据名=自身组件数据名

父 < template v-slot:body=“scope”> 子组件所有:传出去的数据为一个对象,父组件使用scope统一接收 例如使用row数据则scope.row

该例子为具名插槽,如果没有name,则使用v-slot:default= 默认插槽,也可以直接写成v-slot=“” v-slot:body 可以简写为#body

element ui的table使用到了该原理, 先给table 绑定数据 :data=xxx 再通过template v-slot="scope.row"获取 当前行的数据

filter过滤删除,foreach循环设置状态

页脚计算属性统计完成数量和总数

全选框根据isAll 中get属性计算返回 《total和done是否相等,且total>0 》 不然total 为0,done也为0也全选了

set属性 意味着每次isAll变化都会传进来复选框的布尔值,以此改变所有的todo.done为true/false

total 计算todo.length

doneTotal计算所有 done为真的 使用todos.reduce累加方法,传递两个形参,第一个为回调函数,接收两个形参pre积累的值,遍历出来的每一个todo, 方法体为 pre+ done? 1:0 ,第二个值为pre的初始值
image-20240624164750106

下划线占位符_

当消息订阅触发的回调函数传来两个参数第一个是消息是名称,第二个是消息传递过来的参数,这时候我们可以选中使用一个XXX占用第一个形参但是会变灰,且提示unuse,也可以使用 占位符,会发光且不提示错误信息

blur 输入框失焦

传递todo,和event事件获取输入框的值,和todo id进行更新对应的todo的title

confirm确认

对象的封装传递 add

单个复选框的change绑定事件

传id,触发事件 遍历勾选Done值

编辑 nextTick/$set /hasOwnProperty/event

input

与title同样位置来一个input 标签绑定数据为 todo.title,v-show与 title互斥 一个为!xxx / 一个为xxx

isEdit属性

给todo追加一个属性isEdit,但是直接给对象添加属性,该属性不是响应式的没有get/set,vue不认可, 模板不会解析(开发遇到的各种问题)

添加响应式属性使用this.$set(todo,‘属性名’,初始值)

二次点击编辑按钮

下一次编辑的时候做判断有了isEdit属性就直接变为true即可,没有的话就set设置属性

判断todo.hasOwnProperty(‘isEdit’) 不能直接判断todo.isEdit 因为有属性但是为false永远不会走if为真的路线

编辑时编辑按钮不显示

编辑状态时编辑按钮不显示. v-show=!isEdit
提交编辑

传递todo id和输入框的值,接用event事件,当失焦时修改, 修改时判断值是否为空!xxx return alert… 成功后isEdit为false

获取焦点

input起名ref=‘名字’, handlerEdit里this.$ref.名字.focus()但是 当点击编辑后他会执行完方法再去渲染视图,导致视图还没出来就聚焦了,聚焦失败,可以使用一个定时器,类似于异步操作,先渲染,然后等一会再聚焦 ,不过官方考虑到了这一点,提供了一个API

this.$nextTick (回调函数),作用是解析完前面代码影响的模板视图再来 执行该函数

image-20240624174516443
官方解释

语法: this.$nextTick(回调)

作用: 下一次DOM更新完执行(上述代码影响后要渲染的DOM)

时机: 需要基于渲染后的DOM进行数据操作时, 例如上述的先要等input出来在聚焦的操作

计算属性的get/set

写成一个对象 内有get/set方法 get来计算值,set来设置值,例如每次复选框的变化 就会触发set,形参为复选框的布尔值

foreach没有返回值,默认为undefined

具体代码 (无样式版)

MyHeader

<template>
    <div>
        <input v-model="inputTitle" placeholder="输入完回车确认" @keyup.enter="handleAdd"/>
    </div>
</template>

<script>
import {nanoid} from "nanoid";

export default {
    name: "MyHeader",
    data() {
        return {
            inputTitle: ''
        }
    },
    methods: {
        handleAdd() {
            if (!this.inputTitle.trim()) return alert('输入不能为空')
            const todoObj = {
                id: nanoid(),
                title: this.inputTitle,
                done: false
            }
            this.$emit('addTodo', todoObj)
            this.inputTitle = ''
        }
    }
}
</script>

<style scoped>

</style>

MyList

<template>
    <div>
        <ul>
            <MyItem v-for="todoObj in todos"
                    :key="todoObj.id"
                    :todo="todoObj"></MyItem>
        </ul>
    </div>
</template>

<script>
import MyItem from "@/components/MyItem.vue";

export default {
    name: "MyList",
    props: ['todos'],
    components: {MyItem}

}
</script>

<style scoped>

</style>

MyItem

<template>
    <div>
        <ul>
            <MyItem v-for="todoObj in todos"
                    :key="todoObj.id"
                    :todo="todoObj"></MyItem>
        </ul>
    </div>
</template>

<script>
import MyItem from "@/components/MyItem.vue";

export default {
    name: "MyList",
    props: ['todos'],
    components: {MyItem}

}
</script>

<style scoped>

</style>

MyFooter

<template>
    <div>
        <ul>
            <MyItem v-for="todoObj in todos"
                    :key="todoObj.id"
                    :todo="todoObj"></MyItem>
        </ul>
    </div>
</template>

<script>
import MyItem from "@/components/MyItem.vue";

export default {
    name: "MyList",
    props: ['todos'],
    components: {MyItem}

}
</script>

<style scoped>

</style>

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  beforeCreate() {
    Vue.prototype.$bus=this
  }
}).$mount('#app')

App.vue

<template>
    <div id="app">
        <MyHeader @addTodo="addTodo"/>
        <MyList :todos="todos"/>
        <MyFooter :todos="todos"
                  @checkAllTodo="checkAllTodo" @deleteAllTodo="deleteAllTodo"/>
    </div>
</template>

<script>
import MyHeader from "@/components/MyHeader.vue";
import MyList from "@/components/MyList.vue";
import MyFooter from "@/components/MyFooter.vue";
import pubsub from "pubsub-js";

export default {
    name: 'App',
    components: {
        MyHeader, MyList, MyFooter
    },
    data() {
        return {
            //如果 本地存储有数据则使用。没有则初始化空数组,防止reduce计算属性找不到todos.length
            todos: JSON.parse(localStorage.getItem('todos')) || []
        }
    },
    watch: {
        todos: {
            deep: true,
            handler(newValue) {
                localStorage.setItem('todos', JSON.stringify(newValue))
            }
        }
    },
    methods: {
        addTodo(todoObj) {
            this.todos.unshift(todoObj)
        },
        checkTodo(todoObj) {
            this.todos.forEach((todo) => {
                    if (todo.id == todoObj.id) {
                        todo.done = !todo.done
                    }
                }
            )
        },
        updateTodo(todoObj, title) {
            this.todos.forEach((todo) => {
                    if (todo.id == todoObj.id) {
                        todo.title = title
                    }
                }
            )
        },
        deleteTodo(messageName, todoObj) {
            this.todos = this.todos.filter((todo) => {
                return todo.id !== todoObj.id
            })
        },
        checkAllTodo(value) {
            this.todos.forEach((todo) => {
                todo.done = value
            })
        },
        deleteAllTodo() {
            //用错了 把filter用成了forEach,而forEach只是个操作,不返回任何值也就是undefined
            //所以最终会将一个undefined赋值给todos
            this.todos=this.todos.filter((todo) => {
                return !todo.done
            })
        }
    },
    mounted() {
        this.$bus.$on('checkTodo', this.checkTodo)
        this.$bus.$on('updateTodo', this.updateTodo)
        this.pubId=pubsub.subscribe('deleteTodo', this.deleteTodo)
    },
    beforeDestroy() {
        this.$bus.$off('checkTodo')
        this.$bus.$off('updateTodo')
        pubsub.unsubscribe(this.pubId)
    }
}
</script>

<style>

</style>