day-103-one-hundred-and-three-20230701-Vue的单向数据流-todoList项目-组件封装-jsx语法
常见面试题
- 面试题:怎样理解 Vue 的单向数据流?
- 面试题:父组件可以监听到子组件的生命周期吗?
- 面试题:vue中组件和插件有什么区别?
- 面试题:平时开发中,你有没有封装过公共组件?如果封装过,则简单说一下你当时是怎么考虑的!
Vue的单向数据流
- 面试题:怎样理解 Vue 的单向数据流?
- 所谓单向数据流,指的是属性传递是单向的。
- 父组件可以基于属性把信息传递给子组件
<Child :x="10" title="...">
。 - 但是反过来,子组件是无法基于属性把信息传递给父组件的。
- 父组件可以基于属性把信息传递给子组件
- 我个人理解的单向数据流,应该还包含:父子组件的钩子函数触发时机,也是遵循
单向数据
、深度优先原则
的。-
第一次渲染:
- 完整流程:
父组件beforeCreate
-->父组件created
-->父组件beforeMount
-->父组件开始渲染DOM
-->子组件beforeCreate
-->子组件created
-->子组件beforeMount
-->子组件开始渲染DOM
-->子组件结束渲染DOM
-->子组件mounted
-->父组件结束渲染DOM
-->父组件mounted
。 - 具体步骤-根据组件中的钩子函数:
- 父组件渲染前期:
父组件beforeCreate
-->父组件created
-->父组件beforeMount
-->父组件开始渲染DOM
-> 下一阶段。 - 子组件渲染阶段:–>
子组件beforeCreate
-->子组件created
-->子组件beforeMount
-->子组件开始渲染DOM
-->子组件结束渲染DOM
-->子组件mounted
-> 下一阶段。 - 父组件渲染后期: -->
父组件结束渲染DOM
-->父组件mounted
。
- 父组件渲染前期:
- 完整流程:
-
组件更新:
- 完整流程:
父组件beforeUpdate
->父组件开始更新
->子组件beforeUpdate
->子组件开始更新
->子组件结束更新
->子组件updated
->父组件结束更新
->父组件updated
- 具体步骤-根据组件中的钩子函数:
- 父组件更新前期:
父组件beforeUpdate
->父组件开始更新
-> 下一阶段。 - 子组件更新阶段: ->
子组件beforeUpdate
->子组件开始更新
->子组件结束更新
->子组件updated
-> 下一阶段。 - 父组件更新后期: ->
父组件结束更新
->父组件updated
。
- 父组件更新前期:
- 完整流程:
-
组件销毁:
-
- 所谓单向数据流,指的是属性传递是单向的。
深度优先和广度优先
-
深度优先和广度优先
let obj = { x: 10, y: { z: 20, n: { m: 30, }, k: 50, }, h: 40, };
-
深度优先
let obj = { x: 10, y: { z: 20, n: { m: 30, }, k: 50, }, h: 40, }; // x --> y --> z --> n --> m --> k --> h // x --> y --> y是对象,进入y --> y.z --> y.n --> y.n是对象,进入y.n --> y.n.m --> y.n对象结束,跳出y.n --> k --> y对象结束,跳出y --> h // x --> y --> y.z --> y.n --> y.n.m --> y.k --> h
-
广度优先
let obj = { x: 10, y: { z: 20, n: { m: 30, }, k: 50, }, h: 40, }; // x --> y --> h --> z --> n --> k --> m // 第一层:[x --> y --> h] --> 第二层:[z --> n --> k] --> 第三层:[m] // x --> y --> h --> y.z --> y.n --> y.k --> y.n.m
-
父组件与子组件生命周期
- 面试题:父组件可以监听到子组件的生命周期吗?
- 父组件想监测到子组件的钩子函数触发,大体上有两种方案:
-
发布订阅:
- 父组件向子组件事件池中注入自定义事件。
- 子组件在指定的钩子函数触发时,通知自定义事件执行即可。
- 代码示例:
-
fang/f20230701/day0701/src/views/Parent.vue父组件:
<Child @md="childMounted" /> methods: { childMounted() { console.log(`子组件第一次渲染完毕了`); }, },
<template> <div class="parent-box"> <Child @md="childMounted" /> </div> </template> <script> import Child from "./Child.vue"; export default { components: { Child, }, methods: { childMounted() { console.log(`子组件第一次渲染完毕了`); }, }, }; </script> <style lang="less" scoped> .parent-box { box-sizing: border-box; position: relative; margin: 20px auto; width: 200px; height: 200px; background: lightblue; } </style>
-
fang/f20230701/day0701/src/views/Child.vue子组件:
mounted() { this.$emit("md"); },
<template> <div class="child-box"></div> </template> <script> export default { mounted() { this.$emit("md"); }, }; </script> <style lang="less" scoped> .child-box { box-sizing: border-box; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100px; height: 100px; background: lightcoral; } </style>
-
支持传递实参:
- 代码示例:
-
fang/f20230701/day0701/src/views/Parent.vue父组件:
<Child @md="childMounted" /> methods: { childMounted(childBtn) { console.log('子组件第一次渲染完毕了',childBtn) } }
<template> <div class="parent-box"> <Child @md="childMounted" /> <!-- <Child @hook:mounted="childMounted" /> --> </div> </template> <script> import Child from "./Child.vue"; export default { components: { Child, }, methods: { childMounted(...params) { console.log(`子组件第一次渲染完毕`,params); }, }, }; </script>
-
fang/f20230701/day0701/src/views/Child.vue子组件:
mounted() { this.$emit('md',this.$refs.btn) }
<template> <div class="child-box"> <button ref="btn">子组件按钮</button> </div> </template> <script> export default { mounted() { this.$emit("md",this.$refs.btn); }, }; </script>
-
- 代码示例:
-
-
直接基于@hook:钩子函数监听即可。
<Child @hook:mounted="childMounted" />
-
代码示例:
-
父组件:
<Child @hook:mounted="childMounted" /> methods: { childMounted() { console.log(`子组件第一次渲染完毕`); }, }
<template> <div class="parent-box"> <Child @hook:mounted="childMounted" /> </div> </template> <script> import Child from "./Child.vue"; export default { components: { Child, }, methods: { childMounted(...params) { console.log(`子组件第一次渲染完毕`,params); }, }, }; </script>
-
子组件:
<template> <div class="child-box"> <button ref="btn">子组件按钮</button> </div> </template> <script> export default { }; </script>
-
不支持传递实参:
-
父组件:
<Child @hook:mounted="childMounted" /> methods: { childMounted(...params) { console.log(`子组件第一次渲染完毕`,params);//`子组件第一次渲染完毕` []; }, }
-
-
-
虽然第二种方式比较简单,但是第一种发布订阅的模式,其支持:给父组件的方法传递实参(可以是子组件中的一些内容),所以平时开发中,需要传参则采用第一种,不需要则采用第二种即可。
-
-
- 父组件想监测到子组件的钩子函数触发,大体上有两种方案:
todoList项目
- 项目创建:
- 项目代码:
-
fang/f20230701/day0701/src/views/TodoList.vue父组件
<template> <div class="todo-box"> <div class="handle"> <el-input placeholder="请输入任务描述" v-model.trim="text" /> <el-button type="primary" @click="submit()">新建任务</el-button> </div> <list-item v-for="item in list" :key="item.id" :info="item" @handle="handle" /> <!-- <list-item /> <list-item /> <list-item /> --> </div> </template> <script> // 全局引入了element-ui,而this.$message是element-ui导入并注册的。 import ListItem from "../components/ListItem.vue"; import _ from "@/assets/utils"; /* _.storage为下方代码: // 具备有效期的LocalStorage存储 const storage = { set(key, value) { localStorage.setItem( key, JSON.stringify({ time: +new Date(), value, }) ); }, get(key, cycle = 2592000000) { cycle = +cycle; if (isNaN(cycle)) cycle = 2592000000; let data = localStorage.getItem(key); if (!data) return null; let { time, value } = JSON.parse(data); if (+new Date() - time > cycle) { storage.remove(key); return null; } return value; }, remove(key) { localStorage.removeItem(key); }, }; */ export default { components: { ListItem, }, data() { // 组件第一次渲染:先从本地中获取已有的任务列表。 let cache = _.storage.get("TODO_CACHE"); return { //任务列表; list: cache || [], //任务框中输入的内容。 text: "", }; }, methods: { submit() { // 验证text是否为空。 if (this.text.length === 0) { this.$message.warning(`任务描述不可为空哦~`); return; } // 新增任务。 this.list.push({ id: +new Date(), text: this.text, }); this.text = ""; }, // 修改或删除任务。 handle(type, id, text) { // type:操作类型 delete/update // id:要删除/修改任务项的编号。 // text:如果是修改操作,text存储的是要修改的信息。 if (type === "delete") { this.list = this.list.filter((item) => { return +item.id !== +id; }); return; } if (type === "update") { this.list = this.list.map((item) => { if (+item.id === +id) { item.text = text; } return item; }); } }, }, // 监听任务列表的变化,把最新的信息存储到本地。 watch: { list: { deep: true, handler() { _.storage.set("TODO_CACHE", this.list); }, }, }, }; </script> <style lang="less" scoped> .todo-box { box-sizing: border-box; margin: 50px auto; width: 400px; .handle { padding-bottom: 20px; border-bottom: 1px dashed #ddd; display: flex; justify-content: space-between; align-items: center; .el-button { margin-left: 20px; } } } </style>
-
fang/f20230701/day0701/src/components/ListItem.vue子组件
<template> <div class="item-box" v-if="info"> <div class="content"> <el-input size="mini" v-if="isUpdate" v-model="copyText" /> <span class="textCon" v-else>{{ copyText }}</span> </div> <div class="handle"> <el-popconfirm title="您确定要删除本条任务吗?" @confirm="removeHandle"> <el-button type="danger" size="mini" slot="reference">删除</el-button> </el-popconfirm> <el-button type="success" size="mini" v-if="!isUpdate" @click="triggerUpdate" > 修改 </el-button> <template v-else> <el-button type="success" size="mini" @click="saveUpdate"> 保存 </el-button> <el-button type="info" size="mini" @click="cancelUpdate"> 取消 </el-button> </template> </div> </div> </template> <script> export default { // 注册接收属性。 props: { info: { type: Object, required: true, }, }, // 定义状态; data() { return { isUpdate: false, copyText: this.info.text, }; }, //定义操作的方法: methods: { //删除任务。 removeHandle() { // 把父组件中存在的某条任务删除。 this.$emit("handle", "delete", this.info.id); }, // 触发修改操作。 triggerUpdate() { this.isUpdate = true; }, // 保存修改的信息。 saveUpdate() { if (this.copyText.length === 0) { this.$message.warning(`任务描述不能为空哦~`); return; } // 把父组件中存在的某条任务进行修改。 this.$emit("handle", "update", this.info.id, this.copyText); this.isUpdate = false; }, // 取消修改操作。 cancelUpdate() { this.isUpdate = false; this.copyText = this.info.text; }, }, }; </script> <style lang="less" scoped> .item-box { margin: 15px 0; .content { margin-bottom: 5px; .textCon { line-height: 30px; font-size: 14px; } .el-input { width: 200px; } } .handle { .el-button { margin-right: 10px; margin-left: 0; } } } </style>
-
组件封装
- 在组件化开发的模式下,有一个非常重要的知识:如何抽离封装通用的组件!
- 一般我们封装的组件,按照特点可以分为:
- 业务组件-针对于特定的项目,包含一定的业务逻辑:
- 普通业务组件:
- 在SPA单页面应用中,每一个路由页面都是一个组件。
- 一个页面内容比较多,我们开发的时候,把其拆分成多个组件-这些组件可能没有复用性,最后合并渲染。
- …
- 通用业务组件:
- 封装的组件会在很多地方用到(比如:推荐列表、新闻列表、回退按钮…)
- …
- 普通业务组件:
- 功能组件-不单纯针对某一个项目,而是适用于很多项目:
- UI组件库中提供的组件都是功能组件。
- 我们平时开发的时候,会结合当下的业务需求,对这些组件进行二次封装。
- 例如:button组件设置loading防抖效果(比如点击事件执行时,自动有loading效果)、Table表格+筛选或分页等的二次封装、骨架屏的二次封装(样式修改及结构简化)。
- …
- 我们平时开发的时候,会结合当下的业务需求,对这些组件进行二次封装。
- 我们还会自己封装一些UI组件库不具备的组件或者使用第三方插件。
- 例如:大文件切片上传和断点续传、pdf或word或excel的预览、富文本编辑器、复杂的轮播图效果!
- …
- UI组件库中提供的组件都是功能组件。
- 业务组件-针对于特定的项目,包含一定的业务逻辑:
- 但是不论封装什么类型的组件,最核心的思想:让组件具备更强的复用性,支持更多效果的实现!
- 首先,我们要改变思想观念:开发项目之前,首先分析那些东西是有类似的部分,需要进行封装提取的!
- 可能是把几个组件合并在一起,变为一个完整的通用组件。
- 也可能仅仅是调整一些样式,变为和项目风格统一的效果。
- 还可能是在原有组件的基础上,扩充一些单独的功能。
- 当然最主要的还是:包含结构、样式、功能,并在别人使用的时候可以通过传递不同的信息,实现不同的效果。
- 如何让组件具备更强的复用性:
- 基于:属性、插槽、自定义事件、实例(拿到组件实例,就可以调用实例上暴露的方法)。
- 多参考相似的案例需求,进行归纳总结,在封装的时候,让其具备更多的不确定性。
- 更多的不确定性也就是更多的各种合理属性和插槽,用户可以选择一些属性来定制的不同的效果。
- 首先,我们要改变思想观念:开发项目之前,首先分析那些东西是有类似的部分,需要进行封装提取的!
- 我们封装的组件,有不同的调用方式:
- 直接在视图中调用渲染
<el-button></el-button>
;- 封装组件;
- 基于
Vue.component()
注册为全局组件;
- 基于某些方法的执行进行渲染
this.$message.success('...')
;- 封装组件;
- 基于
Vue.extend()
处理。
- 直接在视图中调用渲染
- 封装组件的时候,我们基本上都使用
<template>语法
来构建视图,但是其具备弱编程性
-即不灵活
,此时我们可以基于强编程性
的jsx语法
,来替代<template>语法
。
封装公共组件
- 面试题:平时开发中,你有没有封装过公共组件?如果封装过,则简单说一下你当时是怎么考虑的!
- 自己思考。
代码片断
封装loading防抖按钮
-
参考来源:
- Button按钮-文档说明
- element-ui的Button按钮对应源码在
/node_modules/element-ui/packages/button/src/button.vue
。
-
未封装前:
-
fang/f20230701/day0701/src/views/Demo1.vue
<template> <div class="demo-box"> <el-button type="danger" :loading="deleteLoading" @click="handleDelete" > 删除 </el-button > <el-button type="primary" :loading="updateLoading" @click="handleUpdate">修改</el-button> </div> </template> <script> /* this.$API.query为 const query = (interval = 1000) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ code: 0, message: "ok", }); }, interval); }); }; */ export default { name: "Demo", data() { return { deleteLoading: false, updateLoading: false, }; }, methods: { async handleDelete() { this.deleteLoading = true; try { let { code } = await this.$API.query(2000); if(code===0){ this.$message.success(`恭喜你,删除成功!`) }else{ this.$message.error(`删除失败,请稍后再试!`) } } catch (error) { console.log(`error:-->`, error); } this.deleteLoading = false; }, async handleUpdate(){ this.updateLoading = true; try { let { code } = await this.$API.query(2000); if(code===0){ this.$message.success(`恭喜你,修改成功!`) }else{ this.$message.error(`修改失败,请稍后再试!`) } } catch (error) { console.log(`error:-->`, error); } this.updateLoading = false; } }, }; </script> <style lang="less" scoped> .demo-box { box-sizing: border-box; margin: 20px auto; padding: 20px; width: 200px; border: 1px solid lightcoral; .el-button { display: block; margin-bottom: 20px; margin-left: 0; } } </style>
-
-
简单的封装:
-
创建一个组件:
- fang/f20230701/day0701/src/components/ButtonAgain.vue
- ButtonAgain组件:
- 使用方式需要和ElButton保持一致。
- 只不过loading效果,组件内部处理好即可!
- 别人的代码:Vue2进阶/day0701/src/components/ButtonAgainTemplate.vue
- ButtonAgain组件:
- fang/f20230701/day0701/src/components/ButtonAgain.vue
-
在入口文件处引入,并全局注册:
-
fang/f20230701/day0701/src/main.js或fang/f20230701/day0701/src/global.js,因为global.js是在入口文件main.js直接引入的,和在入口文件执行代码差不多。
import ButtonAgain from "./components/ButtonAgain.vue"; Vue.component(ButtonAgain.name, ButtonAgain);
-
-
在需要用到该按钮的地方直接使用。
<template> <button-again type="danger" plain size="small" @click="handleDelete" ref="AA"> 删除 </button-again> </template>
-
fang/f20230701/day0701/src/views/Demo2.vue
<template> <div class="demo-box"> <button-again type="danger" plain size="small" @click="handleDelete" ref="AA"> 删除 </button-again> <button-again type="primary" circle icon="el-icon-edit" @click="handleUpdate"> </button-again> </div> </template> <script> /* //this.$API.query为: const query = (interval = 1000) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ code: 0, message: "ok", }); }, interval); }); }; */ export default { name: "Demo", methods: { async handleDelete() { try { let { code } = await this.$API.query(2000); if (code === 0) { this.$message.success(`恭喜你,删除成功!`); } else { this.$message.error(`删除失败,请稍后再试!`); } } catch (error) { console.log(`error:-->`, error); } }, async handleUpdate() { try { let { code } = await this.$API.query(); if (code === 0) { this.$message.success(`恭喜你,修改成功!`); } else { this.$message.error(`修改失败,请稍后再试!`); } } catch (error) { console.log(`error:-->`, error); } }, }, mounted () { console.log(`this.$refs.AA-->`, this.$refs.AA); } }; </script> <style lang="less" scoped> .demo-box { box-sizing: border-box; margin: 20px auto; padding: 20px; width: 200px; border: 1px solid lightcoral; .el-button { display: block; margin-bottom: 20px; margin-left: 0; } } </style>
-
- 处理思路步骤:
-
-
封装重构-jsx语法:
-
创建一个组件:
-
fang/f20230701/day0701/src/components/ButtonAgain.vue
<script> export default { name: "ButtonAgain", inheritAttrs: false, data() { return { loading: false, }; }, methods: { async handle(ev) { this.loading = true; try { await this.$listeners.click(ev); } catch (err) { console.log("ButtonAgain Error:", err.message); } this.loading = false; }, }, mounted() { this.ElButtonIns = this.$refs.child; }, render() { // 传递属性的筛选 let attrs = {}, area = [ "type", "size", "icon", "nativeType", "disabled", "plain", "autofocus", "round", "circle", ]; Object.keys(this.$attrs).forEach((key) => { if (!area.includes(key)) return; attrs[key] = this.$attrs[key]; }); return ( <el-button {...{ attrs }} loading={this.loading} vOn:click={this.handle} ref="child" > {this.$slots.default} </el-button> ); }, }; </script> <style lang="less" scoped></style>
-
-
在入口文件处引入,并全局注册:
-
fang/f20230701/day0701/src/main.js或fang/f20230701/day0701/src/global.js,因为global.js是在入口文件main.js直接引入的,和在入口文件执行代码差不多。
import ButtonAgain from "./components/ButtonAgain.vue"; Vue.component(ButtonAgain.name, ButtonAgain);
-
-
在需要用到该按钮的地方直接使用。
<template> <button-again type="danger" plain size="small" @click="handleDelete" ref="AA"> 删除 </button-again> </template>
-
fang/f20230701/day0701/src/views/Demo2.vue
<template> <div class="demo-box"> <button-again type="danger" plain size="small" @click="handleDelete" ref="AA" > 删除 </button-again> <button-again type="primary" circle icon="el-icon-edit" @click="handleUpdate" > </button-again> </div> </template> <script> export default { name: "Demo", methods: { async handleDelete() { try { let { code } = await this.$API.query(2000); if (+code === 0) { this.$message.success("恭喜您,删除成功!"); } else { this.$message.error("删除失败,请稍后再试!"); } } catch (_) {} }, async handleUpdate() { try { let { code } = await this.$API.query(); if (+code === 0) { this.$message.success("恭喜您,修改成功!"); } else { this.$message.error("修改失败,请稍后再试!"); } } catch (_) {} }, }, mounted() { console.log(this.$refs.AA); }, }; </script> <style lang="less" scoped> .demo-box { box-sizing: border-box; margin: 20px auto; padding: 20px; width: 200px; border: 1px solid lightcoral; .el-button { display: block; margin-bottom: 20px; margin-left: 0; } } </style>
-
-
对于一个组件
- 对于一个组件:
- 从技术角度来讲。
- 调用方式来决定是jsx还是template语法。
- 决定属性、自定义事件、
- 从思维角度上:
- 观察通用性,查看需求及相似的例子。
- 普通业务组件、通用业务组件、UI组件库二次封装、第三方插件。
- 从技术角度来讲。
jsx语法
-
纯h函数创建:
<script> export default { name: "Demo", data() { return { title: "Vue视图构建语法", level: 1, }; }, /* https://v2.cn.vuejs.org/v2/guide/render-function.html render函数:基于JSX语法构建视图 + h:createElement */ render(h) { return h( `h${this.level}`, { style: { color: "red", }, }, [ this.title, h("span", {}, [100]), h( "el-button", { props: { type: "primary", }, }, ["哈哈"] ), ] ); }, }; </script>
-
jsx语法与h函数:
<script> export default { name: "Demo", data() { return { title: "Vue视图构建语法", level: 1, }; }, methods: { handle() { this.level = 2; }, }, render(h) { let styObj = { color: "red", }; return h(`h${this.level}`, { style: styObj }, [ this.title, // https://github.com/vuejs/jsx-vue2 <span vOn:click={this.handle}>100</span>, <el-button type="primary">哈哈哈</el-button>, ]); }, }; </script>