MVVM MVC MVP
MVVM MVC MVP是三种软件设计架构模式,主要目的是组织代码结构,优化开发效率。
- MVC (Model-View-Controller)
- view是视图层,展示布局文件
- controller:逻辑层
- model存储和处理数据
缺点:1. view对model的依赖导致view中也包含了业务逻辑 2. controller过于厚重
- MVVM(Model-View-ViewModel) MVC的改进版
- Model:模型层,负责制处理业务逻辑以及与服务器端交互;
- View:视图层,负责将数据模型展示出来;
- ViewModel:视图模型层,是公共属性和命令,用来连接view和model,是他们之间的通信桥梁;
viewModel和model之间是双向数据绑定的关系,model中数据改变会触发view的更新,view中用户操作数据引起的数据改变也会在model中同步。这种模式实现了model和view数据的自动同步,因此开发者只需要去维护数据即可,不需要去操作dom
2. 优点:
- 低耦合(视图和业务逻辑分开)
- 可重用性高(可以把视图逻辑放在一个viewmodel中,让view去重用视图逻辑)
- 能实现独立开发(开发人员可以专注于业务逻辑和数据的开发)
单页面应用
单页面应用是只有一张web页面的应用,浏览器一开始就会加载必须的html、css和js,所有的操作都由js控制,都在这张页面上面完成。 优点:
- 具有桌面应用的即时性,网站的可移植性和可访问性
- 用户体验好,内容改变不需要重新加载整个页面
- 前后端分离,分工更明确 缺点:
- 不利于搜索引擎的抓取(搜索引擎只认识html里的内容,而单页面的内容都是靠js渲染生成处理的,搜索引擎识别不了这部分的内容,所以导致网页排名较差)
- 首次渲染速度较慢
如何解决首屏加载速度慢的问题?
响应式原理
- vue2
vue会遍历data中的每个属性,并使用Object.defineProperty把每个属性转换成getter和setter的形式,setter、getter能够让vue去追踪依赖。每个组件实例中都有一个watcher,属性被读取时会触发getter,从而属性会被收集到依赖列表中,当依赖项的setter触发时,会通知watcher,从而更新视图。
缺点:
- 不能侦测到对象属性的添加和删除
- 利用索引修改数组元素,或者直接修改长度时,无法侦测到。 -vue3-----------------------------
vue生命周期
| 声明周期 | 描述 |
|---|---|
| beforeCreated | 组件初始化之后立即调用 |
| created | 组件实例化完成,此时响应式数据、方法等都可以使用 |
| beforeMounted | 组件挂载之前,dom还未创建 |
| mounted | 组件挂载完成,此时可以访问dom元素 |
| beforeUpdate | 组件因为一个响应式状态变更而引起dom更新之前前 |
| updated | dom更新后 |
| beforeDestroy | 在组件被销毁之前,这个时候可以去清除组件内的定时器 |
| destroy | 实例解绑,监听器移除了,组件已销毁 |
父子组件的生命周期
-
加载渲染过程 父beforeCreated -> 父created 父beforeMount -> 子beforeCreated -> 子created -> 子beforeMount -> 子mounted -> 父mounted
-
更新过程 父beforeUpdate -> 子beforeUpdate ->子updated -> 父updated
-
销毁过程 父beforeDestory -> 子beforeDestory -> 子destoryed -> 父destoryed
为什么v-for和v-if不能一起使用?
源码中先判断v-for再判断v-if,因此v-for比v-if优先级高,每次渲染都要先循环再判断,性能方面消耗较大。
解决:可以在循环的外层添加template标签,在template标签上进行判断
为什么data是个函数?
如果组件中的data是对象,那当组件被多次复用时,组件实例会引用同一个data对象,也就是共享一块内存地址,他们内部的数据会相互影响。而当data是函数的时候,函数会返回一个全新的data,不会共享内存。
因此data是个函数可以避免数据污染。
组件通信方式
父子组件通信:
$ref prop $emit vuex $attrs $listeners $parent provide inject $children v-model.sync 插槽
隔代通信:vuex provide inject $attrs $listeners
兄弟通信:vuex
介绍一下vuex
vuex是专为vue.js应用程序开发的状态管理插件,核心是store,它采用集中式存储管理应用的所有组件的状态,更改状态的唯一方法是提交mutation.
什么时候用vuex?
- 多个组件依赖于同一个状态
- 不同组件的行为需要变更同一个状态
vuex中的5个核心属性:
- state: 存储vuex中的状态,使用MapState结合对象的展开运算符批量使用state
- getters: 类似于计算属性,getter的返回值具有缓存特性,只有当依赖发生变更时才会重新计算。
- mutations:更改state的唯一方法。相当于事件,每一个mutation都有一个事件类型和回调函数,但是它的回调函数必须是同步操作。
- 可以直接更改store中的状态state
- 只能执行同步操作
- store.commit()进行提交
- 第一个参数的state
- actions
- 提交的是mutation
- 能够包含异步操作
- store.dispatch进行提交
- 第一个参数是context
- modules:使用单一状态树时,所有的状态都会集中到一个比较大的对象上,store就会变得很臃肿,为了解决这个问题,可以将store分割成module,每个module都有自己的state,getters,mutatins,actions.
vuex和单纯的全局对象有什么不同?
- vuex的状态存储是响应式的,当组件从store中读取状态时,若对应的状态发生了改变,相应的也会更新到视图。
- 不能直接改变store的状态,只能通过mutation进行显示的提交,这样有利于跟踪状态的变化。
vue路由
router-link: 使用router-link来导航,to属性值是导航的地址。能够在不重新加载页面的条件下更改url. router-view: 渲染url对应的组件。
router的区别
router是vue路由实例,一般用来进行页面跳转。
路由重定向
路由重定向通过路由配置完成,使用redirect设置重定向的地址,它是值可以是路径,命名的路由或者一个方法
//路径
const router = new VueRouter({ routes: [ { path: '/a', redirect: '/b' } ] })
//名称
const router = new VueRouter({ routes: [ { path: '/a', redirect: { name: 'foo' }} ] })
//方法
const routes = [ { // /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => { // 方法接收目标路由作为参数 // return 重定向的字符串路径/路径对象
return { path: '/search', query: { q: to.params.searchText } } },
},
动态路由
路径参数用:表示。当一个路由被匹配时,他的params值可以通过this.$route.params获取。路径参数可以是多个。
当路由参数变化时,虽然路径不同,但渲染的组件是同一个,因此组件的生命周期钩子不会被调用。如果需要在路由变化时完成某些操作,则需要用watch去监听params
路由传参
- 布尔模式: 当该路由项的props为
true时,对应的params将被设置为组件的props - 命名视图:不同组件对应不同的props,值为布尔值。
- 对象模式:props对象中的属性会被原样设置为组件的props
- 函数模式:返回props,可以对参数进行转换。
hash和history
- hash模式
- 什么是
hash模式?
创建路由实例时,用createWebHashHistory()创建的,设置history的值为,这种模式下url中包含一个#号,#后的哈希值变化时,浏览器并不会重新发起请求,而是会触发onhashchange事件。
- hash模式的特点
- hash模式时只能改变#后面的值,因此只能跳转到与当前url文档同文档的url
- hash模式通过
onhashchange来监听路由的变化,借此实现页面的无刷新跳转 - hash永远不会提交到服务器端
- hash值每改变一次就会在history中增加一条历史记录。
- hash值改变不会触发页面重新加载,所有的操作都是纯客户端的,因此这种模式不利于SEO。
- history模式
-
什么是history模式? 创建路由实例时,用createWebHistory()创建的,通过调用window.history对象上的
pushState和replaceState方法实现页面的无刷新跳转。historyapi是H5提供的新特性,允许更新浏览器地址而不用重新发起请求。 -
history模式的特点
- 新的Url可以跟当前url一样,但是会把重复的一次操作记录到栈中
- 通过pushState和replaceState实现无刷新跳转功能
- 可以额外设置title属性,以供后续使用
- 可通过history.state把任意类型的数据添加到记录中
- abstract: 非浏览器环境下使用
使用history模式的路由会有什么问题?
对当前页面进行刷新时,如果nginx没有匹配到当前的url,就会出现页面404的问题。
解决办法: nginx配置,把所有的请求都转发到Index.html上。
路由懒加载
把不同路由对应的组件划分成不同的代码块,在路由被访问时才去加载对应的组件。
路由配置中component的值是一个返回promise组件的函数,只有在第一次进入页面的时候才会获取这个函数,然后使用缓存数据。
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')
const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})
修饰符
- .stop:阻止冒泡
- .prevent:阻止默认事件
- .number:转为数字类型
- .trim:去除收尾空格
- sync:实现prop的双向绑定(相当于:update, 子组件:this.$emit())
v-if vs v-show
v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
nextTick
为什么会有nextTick?
当检测到数据变化时,不会立即更新dom,而是会开启一个任务队列,并且会缓存同一事件循环中的所有数据变更。这样可以将多次数据更新合并成一次,减少对Dom的操作. 这种策略导致数据的变更不会立即体现到dom上,如果想要获取更新后的dom的状态,就需要nextTick.
作用:将回调延迟到下次dom更新循环结束之后进行。如果想基于数据更新后的dom执行某些操作,可以把回调函数传入nextTick
vue中key的作用
- key的存在是为了更高效的更新虚拟DOM
- vue在比较新旧vnode时,会比较Key以及标签类型等信息是不是一致的,如果一致的话会去进行更新操作。如果不定义Key,那key就一直是undefined,两个key永远相等,vue就会认为两个节点是同一个,会进行不必要的更新操作,影响性能。
- vue中在使用相同标签的元素进行过渡切换时,会使用到key属性,没有key的话只会替换内部属性而不会触发过渡效果。
虚拟DOM(vnode)
虚拟dom就是用js对象来描述真实的dom节点。 diff算法去比较新旧vnode的差异,将变化的地方更新到真实的dom上。
- diff是什么时刻触发的? 数据发生变化的时候,会触发setter通知,通知的方式是把watcher添加到异步更新队列,在每次事件循环队列结束时,清空事件队列,在这个过程中所有的watcher都会执行他们的更新函数,更新函数在执行的时候其实调用了我们的组件渲染函数和组件更新函数,这时会重新渲染最新的虚拟dom,然后执行更新函数,比较新旧虚拟dom。
- dom-diff其实就是创建、删除、更新节点
- vnode创建 有注释节点、文本节点、元素节点能被创建、插入到dom节点中 判断vnode是否有tag标签,如果有,创建标签节点;如果没有,判断vnode是否有iscommend属性,如果有,创建注释node;如果没有则创建文本node
- vnode删除 如果新的vnode中没有而旧的vnode中有,则在旧的vnode中删除即可。
- vnode更新 更新节点有三种情况
- vnode为静态节点:无需比较直接跳过
- vnode为文本节点 如果vnode为文本节点,此时如果旧节点也是文本节点,只需比较文本的差异进行更新。如果旧节点不是文本节点,则直接替换成vnode
- vnode为元素节点 (1)vnode含有子节点 a. 如果新的vnode中包含子节点,先检查旧的vnode中是否包含子节点,如果包含,则递归更新旧节点。如果旧节点不包含子节点,则直接创建一分新节点中的子节点插入到旧节点。如果旧节点中是文本节点,则把文本节点清空再把创建的子节点插入到旧节点。 (2)vnode不含子节点 b. 如果新的vnode中没有子节点,此时如果旧vnode中含有子节点,直接清空子节点。
- vue3相对于vue2diff算法的优化点:
- 事件缓存
- 添加静态标记,vue2是全量diff,vue3是静态标记+部分diff
- 静态提升。后续直接复用静态节点
- vue2在updateChildren中对比变更,vue3是在patchKeyedChildren函数中,基于最长递增子序列去移动/添加/删除节点。
响应式原理
- vue2是通过数据劫持+发布订阅者模式去实现响应式的,是对data中的每个属性进行了递归遍历,为每个属性设置getter和setter。而vue3是对一个对象进行监听,只要对象发生变化就被监听到,这就完全可以代理所有属性。
- vue2中需要递归遍历对象中的所有属性, 如果一个对象中属性特别多或者是嵌套很深的话,会非常消耗性能。但vue3是在getter中递归,也就是只有真正被访问到的属性才会变成响应式,减少了性能消耗。
- vue2响应式原理 对象: data通过observer把每个属性转换成了getter和setter的形式去追踪变化,当watcher实例读取属性时,会触发getter,从而被添加到依赖收集列表dep中,当属性发生变化的时候会触发setter,通知dep列表中所有watcher执行更新函数,从而更新视图。 vue2不能侦测到对象属性的新增和删除
数组:也是在getter中进行依赖收集,不同的是,数组的响应性是用方法拦截器让数组变得可观测。 vue2直接通过索引修改数组元素或者直接修改数组长度,这是不能被侦测到的。
- vue3响应式原理 主要是在proxy的第二个参数handler中,track函数用来收集依赖,trigger函数用来触发更新。
vue2和vue3的区别
- 生命周期: vue3的生命周期都在前面加上了‘on’,并且setUp是围绕beforeCreated和created生命周期钩子运行的,所以不需要显示的定义
- 根节点: vue2只支持单根节点组件,vue3支持多根节点组件
- 组合式api: vue2是选项式api,一个逻辑会散乱在文件的不同位置,比如props|data|computed等,导致代码可读性变差,而vue组合式api将这些内容都写到一起,增强了代码的可读性、内举性
- diff算法优化
- diff算法优化:在会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方比较。
- 静态提升:对于不会参与更新的元素会做静态提升,只会被创建一次,后续直接复用
- 事件监听缓存:开启了事件监听缓存后,就没有了静态标记,在下次diff算法的时候可以直接使用
- 响应式系统优化
- vue3使用proxy重写了响应式系统,实现对整个对象的监听,不需要深度遍历
- 源码体积
- 移除了不常用的api
- 打包的时候使用了treeShanking,比如ref、reactive、computed这些api只有在用到才会被打包,没有用到就会被摇掉,打包的整体体积变小
权限控制
- 登录权限
用户登录后后端会返回token,前端把token保存在localeStorage中,通过axios进行接口请求拦截,发送请求时把token携带在header中。并且利用axios做响应拦截,如果token验证没有通过,则跳转到登录页面
- 路由权限
初始化的时候先挂载不需要权限控制的路由,比如登录登录页、404页面,如果用户通过url强制访问,那就会进入404页面 首先请求当前登录用户所拥有的路由权限,然后筛选有权限的路由,在全局路由守卫中用addRoutes动态的添加路由
- 菜单权限
菜单由后端返回,前后端利用路由的name做关联,后端接口返回的name应该是唯一的,如果name在接口返回的菜单信息中不存在,就代表没有权限,不会出现在菜单中
- 控件权限
利用自定义指令控制按钮的权限,把当前用户的按钮权限信息保存在localeStorage中,判断自定义指令传的值或者当前路由的meta中的按钮权限信息是不是在当前用户的按钮权限范围内,在则显示,否则隐藏
自定义指令
自定义指令可以全局注册也可以局部注册。全局注册:vue.directive($name, )
应用场景:
- 按钮的权限控制
- 防止表单重复提交
keep-alive
keep-alive是vue的内置组件,能够在多个组件进行切换时缓存被移除的组件实例。
- include 字符串或者正则,匹配到的组件就会被缓存
- exclude 字符串或者正则,匹配到的组件不会被缓存
- max 最大缓存的数量,如果缓存的组件数量超过这个值,那最久的没有被访问过的组件实例就会被销毁\
新增的生命周期:activited, deactivited
activited: 组件被首次挂载或者当一个组件实例作为缓存树的一部分插入到 DOM 中时调用 deactivited: 组件被卸载或者组件被移除但被缓存起来仍然作为组件树的一部分时调用。
token失效
用户登录成功后,会返回一个token和refresh_token 前端请求接口api --> 返回401错误 --> 前端判断是否有refresh_token -->如果有就用refresh_token请求新的token --> 后台成功返回一个新的token和refreshToken给我们 --> 更新vuex+本地存储持久化 --> 带上新的token请求数据 如果没有token就直接重定向到登录页
性能优化
- 加载时的优化
- 减少http请求
dns -> tcp -> 发起请求 -> 服务器接收请求 -> 服务器处理请求并响应 -> 浏览器接收响应 2. 服务端渲染
服务端返回html文件,客户端只需要解析html文件即可
客户端渲染是获取html文件,根据需要下载js文件,运行文件,生成dom再渲染
- 静态资源使用CDN
多个位置部署服务器,让用户离服务器更近,缩短请求时间
- css写在头部,html在中间,js在底部
css,js会阻塞dom的构建,css会阻塞js的执行
- 字体图标代替图片图标
字体图标是矢量图不会失真,生成的文件特别小
- 利用缓存
expires,cache-control,在资源过期之前都可以读取缓存而不需要重新请求
- 图片优化
切图使用jpg格式,雪碧图
- webpack按需加载代码
- 运行时的优化
- 修改样式时使用类改变样式,而不是直接写行内样式
- 事件委托(节省内存)
- 降低css选择器的复杂性
- 使用弹性布局
- 用transform和opicity实现动画,不会引起重排