1、MVVM
(1) 什么是MVVM
MVVM
本质上描述的是一种视图和逻辑之间的数据双向关联关系,专业术语中称为数据双向绑定模式
- 核心:结构分离(视图和数据分离)、双向同步(数据可以自动渲染,页面数据可以自动更新)
MVVM
是一种数据双向绑定模式,核心解决了数据和结构分离、数据双向同步的问题
MVC
是一种编程模式,核心解决了根据用户请求处理不同业务的逻辑问题,主要描述了请求分发
面试题解析:请简单描述一下mvvm
和mvc
你是怎么理解的?
mvvm
和mvc
都是程序开发中的一种编程的设计方式,是一种设计思想
mvvm
主要体现在前端应用中,将页面结构和数据模型进行分离,同时通过中间模块vm
实现数据的双向绑定,提高数据的加载和渲染效率,同时让开发人员的精力主要集中在业务处理上而不是技术同步数据上,提高开发的效率
mvc
主要体现在后端应用中,将客户端请求和对应的业务模块进行规则映射,根据不同的请求分发到不同的业务模块完成数据处理,可以在固定的模式下完成复杂的业务功能处理的软件结构,提高后端应用开发效率
(3) 底层工作原理
Vue
中的一个重要特性就是数据的双向绑定,底层主要是通过数据劫持的方式实现的!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// let myName = "小马"
// 需求1:当我改变myName变量的数据时,请同时打印一句话~数据被改变了
// 需求2:当我读取myName变量的数据时,请同时打印一句话~数据被读取了
// 原生语法中,没有提供可以监听变量数据的事件
// 数据劫持
// ① 临时存储数据的变量
let myNameTemp = "小马"
// ② 数据劫持方式,声明可以监听的变量
// Object.defineProperty() 专门用于数据劫持的函数
// 参数1:挂载的对象
// 参数2:挂载的属性;将一个指定名称的属性挂载到一个指定的对象上
// 下面的代码,将myName变量挂载到window对象上
// 参数3:监听数据变化的对象,包含get()读取函数、set()设置函数
Object.defineProperty(window, 'myName', {
get() {
console.log('有用户读取myName变量的数据了')
return myNameTemp
},
set(val){
console.log('有用户改变myName变量的数据了')
myNameTemp = val
}
})
// 读取数据
console.log(myName) // 同时会触发 get()监听函数
// 设置数据
myName = "DAMU" // 同时会触发set()监听函数
</script>
</body>
</html>
自定义框架:myvue.js
/*
* @Author: mch 2332582158@qq.com
* @Date: 2022-06-08 10:58:44
* @LastEditors: mch 2332582158@qq.com
* @LastEditTime: 2022-06-08 11:13:06
* @FilePath: \code\myvue.js
* @Description:自定义vue框架
*
* Copyright (c) 2022 by mch 2332582158@qq.com, All Rights Reserved.
*/
class MyVue {
constructor(props) {
// 获取挂载节点的DOM对象
this.$el = document.querySelector(props.el)
this._html = this.$el.innerHTML
// 数据劫持的方式,挂载data数据
this.$data = {...props.data}
for(let key in props.data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(val) {
this.$data[key] = val
// 一旦数据发生更新,从新渲染界面
this.render()
}
})
}
// 挂载methods函数
for(let key in props.methods) {
this[key] = props.methods[key]
}
// 创建好对象,挂载完数据后,调用并渲染页面
this.render()
}
/** 渲染数据的函数 */
render() {
// 定义一个正则表达式,查询需要替换的数据
let reg = /\{\{\s*(.*?\s*\(?\)?)\s*\}\}/ig
// 准备替换数据
let newHtml = this._html.replace(reg, (a, b) => {
console.log(a, b)
if(b.endsWith('()')) {
console.log(b, '这是要执行的函数')
return this[b.substring(0, b.length-2)]()
} else {
console.log(this[b.trim()], '这是要替换的变量')
return this[b.trim()]
}
})
// 替换页面数据
this.$el.innerHTML = newHtml
}
}
自定义框架的使用:demo03自定义框架.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<p>{{ name }}</p>
<p>{{ age }}</p>
<p>{{ intro() }}</p>
</div>
<script src="./myvue.js"></script>
<script>
const app = new MyVue({
el: "#app",
data: {
name: "小马",
age: 20
},
methods: {
intro() {
console.log("姓名:", this.name, ",年龄:", this.age)
return "姓名:" +this.name+",年龄:"+this.age
}
}
})
</script>
</body>
</html>
(5) 疑难解惑
① this.$el
中的$
什么意思?
JS中变量的命名规则:
名称必须由字母、数字、下划线、$组成,数字不能开头
企业项目中开发中,同时用到js和jQuery,变量的约定命名规范:
JavaScript对象:正常命名;或者 变量名称由一个下划线开头,实现了和jQuery对象的差异化管理
let _username = document.getElementById('uname') // _username是一个js对象
jQuery对象:正常命名;或者 变量名称由$符号开头,实现了和普通JS对象的差异化管理
let $username = $('#uname') // $username 是一个jQuery对象
后来的项目开发中,jQuery使用频率开始降低
变量以$符号开头,表示这是一个包含特殊含义的变量
② 正则表达式
正则中常见的符号:
^ 匹配字符串开头位置
$ 匹配字符串结束位置
. 匹配任意字符
[0-9] 匹配任意一个数字
[a-z] 匹配任意一个小写字母
[A-Z] 匹配任意一个大写字母
[a-zA-Z] 匹配任意一个字母
[abc] 匹配字符a或者b或者c
x* 匹配x字符出现了0次或者多次
x? 匹配x字符出现了0次或者1次
x+ 匹配x字符出现了1次或者多次
x{m,n} 匹配x字符至少出现了m次,最多出现了n次
x{m,} 匹配x字符至少出现了m次
x{,n} 匹配x字符最多出现了n次
\d 匹配任意一个数字,等价于[0-9]
\D 匹配任意一个非数字,等价于[^0-9]
\s 匹配任意一个空白字符
\S 匹配任意一个非空白字符
\w 匹配任意一个数字/下划线/字母,等价[0-9a-zA-Z_]
\W 匹配任意一个非 数字/下划线/字母,等价[^0-9a-zA-Z_]
(abc) 匹配abc字符(整体),单独分组
(abc)? 匹配abc字符出现了0次或者1次
2、组件基础
(1) 什么是组件
组件,英文单词component
描述了一个包含页面结构、基础样式、逻辑数据的完整视图!组件化开发就是将多个页面中公共的视图部分抽取成独立存在的组件,在多个页面中实现复用,提高项目开发效率!
(2) 组件的声明
Vue
提供了组件的声明方式,声明全局组件和局部组件
基本语法
1、全局组件
Vue.component('组件名称', {
template: '组件的页面结构',
data() { // 组件中的data选项,必须是一个返回数据对象的函数;不能直接是一个对象
return {
title: '首页'
}
},
methods: {},
filters: {},
...
})
2、局部组件
new Vue({
el: "#app",
data: {},
components: {
MyHeader: {
template: '<div></div>',
data() {
return {
title: '页头'
}
}
},
MyFooter: {...}
}
})
代码操作:关于组件的声明,需要注意如下几点
- 组件:本质上就是一个
Vue
实例,组件内的选项和实例的选项一致 - 组件的名称:不要和
html
内建标签的名称冲突,会导致组件失效 - 组件的名称:标准名称帕斯卡命名法(见名知意,多个单词组成,每个单词首字母大写)页面中使用组件第一个字符大小写无所谓,后面每个大写的单词必须修改成小写字母,字母前添加中划线,组件才能正常访问
<!DOCTYPE html>
<html lang='zh'>
<head>
<meta charset='UTF-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Document</title>
</head>
<body>
<div id='app'>
<!-- 以标签的方式 使用组件 -->
<aside-comp></aside-comp>
<!-- 以标签的方式 使用局部组件 -->
<!-- 组件名称: MyFooter -->
<!-- 标签名称:my-footer 、My-footer、My-Footer、MY-FOOTER...-->
<My-footer></My-footer>
</div>
<script src='./vue.js'></script>
<script>
// 全局组件
Vue.component('AsideComp', {
template: `<ul>
<li>LOGO:侧边栏导航</li>
<li v-for='item in links' :key='item.id'>
<a :href="item.href"> {{ item.name }}</a>
</li>
</ul>`,
data() {
return {
links: [
{id: 2, name: "百度", href: "https://www.baidu.com"},
{id: 1, name: "谷歌", href: "https://www.google.cn"}
]
}
}
})
// 局部组件
let MyFooter = {
template: '<div><h3>页脚</h3></div>'
}
const app = new Vue({
el: '#app',
data: {},
watch: {},
computed: {},
methods: {},
components: {
MyFooter
}
})
</script>
</body>
</html>
(3) 组件的嵌套
实际项目中多个组件之间一般会呈现一种嵌套关系,就会出现包含组件的父组件以及被包含的子组件,这部分内容我们了解组件是如何实现嵌套的!
代码操作:
<!DOCTYPE html>
<html lang='zh'>
<head>
<meta charset='UTF-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Document</title>
<style>
*{margin: 0; padding: 0; box-sizing: border-box;}
.header{height: 50px; border-bottom: solid 2px #ddd; text-align:center; line-height: 50px;font-size: 18px;}
.nav{list-style:none; display: flex; position: fixed; left: 0; bottom: 0; width: 100%; border-top: solid #ddd 2px; background: white;}
.nav li{flex: 1; height: 50px; text-align: center; line-height: 50px; font-size: 18px;}
.nav li:hover{background: orangered; color:white;}
</style>
</head>
<body>
<div id='app'>
<!-- 被包含的子组件 : 嵌套关系-->
<my-header></my-header>
<!-- 被包含的子组件:导航-->
<my-aside></my-aside>
</div>
<script src='./vue.js'></script>
<script>
// 声明页头组件
let MyHeader = {
template: '<div class="header">页头</div>'
}
// 声明导航组件
let MyAside = {
template: `<ul class="nav">
<li>首页</li>
<li>列表</li>
<li>用户中心</li>
<li>关于我们</li>
</ul>`
}
const app = new Vue({
el: '#app',
data: {},
watch: {},
computed: {},
methods: {},
components: { // 组件的嵌套,一些组件出现在另一些组件的components选项中
MyHeader,
MyAside
}
})
</script>
</body>
</html>
(4) 动态组件
Vue
应用中出现多个组件时,组件的使用方式除了直接通过组件名称的标签使用方式
// let MyHeader = {...}
<my-header></my-header>
Vue
提供了另一种组件的操作方式:动态组件,通过动态属性名称,控制展示的组件
<!-- 页面中展示 Home组件 -->
<component :is="page"></component>
<script>
...
data() {
return {
page: 'Home'
}
}
</script>
代码操作:在组件的嵌套案例基础上,编写多个页面通过动态组件切换的代码
<!DOCTYPE html>
<html lang='zh'>
<head>
<meta charset='UTF-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Document</title>
<style>
*{margin: 0; padding: 0; box-sizing: border-box;}
.header{height: 50px; border-bottom: solid 2px #ddd; text-align:center; line-height: 50px;font-size: 18px;}
.nav{list-style:none; display: flex; position: fixed; left: 0; bottom: 0; width: 100%; border-top: solid #ddd 2px; background: white;}
.nav li{cursor:pointer;flex: 1; height: 50px; text-align: center; line-height: 50px; font-size: 18px;}
.nav li.active,.nav li:hover{background: orangered; color:white;}
</style>
</head>
<body>
<div id='app'>
<!-- 被包含的子组件 : 嵌套关系-->
<my-header></my-header>
<!-- 动态组件,动态切换不同的页面-->
<component :is="page"></component>
<ul class="nav">
<li :class="{active:page==='Home'}" @click="page='Home'">首页</li>
<li :class="{active:page==='List'}" @click="page='List'">列表</li>
<li :class="{active:page==='Ucenter'}" @click="page='Ucenter'">用户中心</li>
<li :class="{active:page==='About'}" @click="page='About'">关于我们</li>
</ul>
<!-- 被包含的子组件:导航-->
<!-- 问题:子组件中添加事件,如何控制父组件中的数据变化?-->
<!-- <my-aside></my-aside> -->
</div>
<script src='./vue.js'></script>
<script>
// 声明首页组件
let Home = {
template: "<h3>系统首页</h3>"
}
// 声明列表页面组件
let List = {
template: "<h3>品牌列表页面</h3>"
}
// 声明用户中心组件
let Ucenter = {
template: "<h3>用户中心页面</h3>"
}
// 声明关于我们组件
let About = {
template: "<h3>关于我们页面</h3>"
}
// 声明页头组件
let MyHeader = {
template: '<div class="header">页头</div>'
}
// 声明导航组件
let MyAside = {
template: `<ul class="nav">
<li>首页</li>
<li>列表</li>
<li>用户中心</li>
<li>关于我们</li>
</ul>`
}
const app = new Vue({
el: '#app',
data: {
page: 'Home'
},
watch: {},
computed: {},
methods: {},
components: { // 组件的嵌套,一些组件出现在另一些组件的components选项中
MyHeader,
MyAside,
Home, List, Ucenter, About
}
})
</script>
</body>
</html>
总结:什么是动态组件?
所谓动态组件,就是
Vue
提供了一种组件的使用方式,可以通过component
标签的is
属性动态控制展示组件的语法
(5) 组件缓存
Vue
针对长时间保持不变的页面组件,提供了一种缓存机制,可以让特定的组件保持自己的创建状态,避免频繁的创建和销毁造成系统资源的消耗
<!-- 缓存所有的动态组件 -->
<keep-alive>
<!-- 缓存include指定的组件 -->
<keep-alive include="Ucenter,About">
<!-- 缓存除了exclude指定的组件以外的其他组件 -->
<keep-alive exclude="Home">
<component :is="page"></component>
</keep-alive>
代码操作:
<!-- 动态组件,动态切换不同的页面-->
<keep-alive include="About">
<component :is="page"></component>
</keep-alive>
<ul class="nav">
<li :class="{active:page==='Home'}" @click="page='Home'">首页</li>
<li :class="{active:page==='List'}" @click="page='List'">列表</li>
<li :class="{active:page==='Ucenter'}" @click="page='Ucenter'">用户中心</li>
<li :class="{active:page==='About'}" @click="page='About'">关于我们</li>
</ul>
3、生命周期
生命周期,英文单词Lifecycle
描述了一个组件从创建实例、加载DOM、渲染更新数据、销毁的整个流程!
官方文档:
- 创建:
beforeCreate()
、created()
- 加载:
beforeMount()
、mounted()
- 运行:
beforeUpdate()
、updated()
- 销毁:
beforeDestroy()
、destroyed()
- 缓存的组件:
activated()显示
、deactivated()隐藏
代码操作:
<!DOCTYPE html>
<html lang='zh'>
<head>
<meta charset='UTF-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>我的应用</title>
</head>
<body>
<div id='app'>
<home></home>
</div>
<script src='./vue.js'></script>
<script>
let Home = {
template: `<div>
<h3 id='titleId'>{{ title }}</h3>
<button @click='title=\"HOME\"'>更新标题</button>
<button @click='delComp'>销毁组件</button>
</div>`,
data() {
return {
title: '首页'
}
},
methods: {
delComp() {
// 调用函数主动销毁组件
this.$destroy()
}
},
beforeCreate() {
console.log("1、beforeCreate() 生命周期钩子执行 ")
// console.log("访问data数据:", this.title) // 失败 DATA数据
// console.log("访问模板DOM:", document.querySelector('#titleId')) // 失败 模板DOM/虚拟DOM
// console.log("访问真实DOM:", document.title) // 成功 真实DOM
},
created() {
console.log("2、created() 生命周期钩子执行 ")
// console.log("访问data数据:", this.title) // DATA数据
// console.log("访问模板DOM:", document.querySelector('#titleId')) // 失败 模板DOM/虚拟DOM
// console.log("访问真实DOM:", document.title) // 成功 真实DOM
},
beforeMount() {
console.log("3、beforeMount() 生命周期钩子执行 ")
// console.log("访问data数据:", this.title) // DATA数据
// console.log("访问模板DOM:", document.querySelector('#titleId')) // 失败 模板DOM/虚拟DOM
// console.log("访问真实DOM:", document.title) // 成功 真实DOM
},
mounted() {
console.log("4、mounted() 生命周期钩子执行 ")
console.log("访问data数据:", this.title) // DATA数据
console.log("访问模板DOM:", document.querySelector('#titleId')) // 失败 模板DOM/虚拟DOM
console.log("访问真实DOM:", document.title) // 成功 真实DOM
},
beforeUpdate() {
console.log("5、beforeUpdate() 生命周期钩子执行")
},
updated() {
console.log("6、updated() 生命周期钩子执行")
},
beforeDestroy(){
console.log("7、beforeDestroy() 生命周期钩子执行")
},
destroyed() {
console.log("8、destroyed() 生命周期钩子执行")
},
}
const app = new Vue({
el: '#app',
data: {},
watch: {},
computed: {},
methods: {},
components: {
Home
}
})
</script>
</body>
</html>
生命周期小总结:
```
Vue
组件的生命周期,正常情况下包含了4类8组:
- 创建实例
beforeCreate()
:实例创建之前,项目中几乎不用created()
:实例创建之后,一般用于初始化data
数据:常用
- 前后端分离项目中,展示某个页面时请求后端数据,
created()
生命周期函数中发送异步请求,将接口返回的数据存储data
中- 加载DOM
beforeMount()
:虚拟DOM节点加载之前执行,项目中几乎不用mounted()
:虚拟DOM节点加载之后执行:常用
- 网页展示出来之后,需要将
data
中的数据渲染到界面中- 数据更新
beforeUpdate()
:任意数据更新之前执行:项目中几乎不用updated()
:任意数据更新之后执行:项目中几乎不用- 组件销毁
beforeDestroy()
:组件销毁前执行,项目中使用较少destroyed()
:组件销毁后执行:常用
',
data: {},
watch: {},
computed: {},
methods: {},
components: {
Home
}
})
生命周期小总结:
Vue
组件的生命周期,正常情况下包含了4类8组:
- 创建实例
beforeCreate()
:实例创建之前,项目中几乎不用created()
:实例创建之后,一般用于初始化data
数据:常用
- 前后端分离项目中,展示某个页面时请求后端数据,
created()
生命周期函数中发送异步请求,将接口返回的数据存储data
中- 加载DOM
beforeMount()
:虚拟DOM节点加载之前执行,项目中几乎不用mounted()
:虚拟DOM节点加载之后执行:常用
- 网页展示出来之后,需要将
data
中的数据渲染到界面中- 数据更新
beforeUpdate()
:任意数据更新之前执行:项目中几乎不用updated()
:任意数据更新之后执行:项目中几乎不用- 组件销毁
beforeDestroy()
:组件销毁前执行,项目中使用较少destroyed()
:组件销毁后执行:常用
- 首页发送了两个异步请求;数据还没有返回的情况下用户点击跳转到了用户中心页面;取消首页发送的请求