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-itemv-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-buttontype="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-againtype="danger"plainsize="small"@click="handleDelete"ref="AA">删除</button-again><button-againtype="primary"circleicon="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.htmlrender函数:基于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>
进阶参考
- Button 按钮