vue
vue 部分
vue 系列 https://juejin.cn/post/6961222829979697165?utm_source=gold_browser_extension (opens new window)
# 1. mvc 和 mvvm
现代前端框架中,vue是mvvm,react是v,angular是mvc
MVC:Model-view-contorl 一句话描述就是 Controller 负责将 Model 的数据用 View 显示出来,换句话说就是在 Controller 里面把 Model 的数据赋值给 View。
MVVM:是Model-View-ViewModel的简写
通过操纵 M(模型层) 层 ,通过 VM 层(数据劫持 和 虚拟DOM机制来实现)来实现 V(视图层) 层会自动跟着变化
MVVM设计模式的优点
1. 低耦合。视图(View)可以独立于Model变化和修改
2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。,可以组件式开发
3. 自动更新DOM,减少直接对DOM的操作
MVVM设计模式的缺点
1. 数据绑定也使得bug很难被调试。比如你看到页面异常了,有可能是你的View的代码有bug,也可能是你的model的代码有问题。数据绑定使得一个位置的Bug被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
2. 数据双向绑定不利于代码重用。客户端开发最常用的是View,但是数据双向绑定技术,让你在一个View都绑定了一个model,不同的模块model都不同。那就不能简单重用view了
3. 一个大的模块中model也会很大,虽然使用方便了也很容易保证数据的一致性,但是长期持有,不释放内存就造成话费更多的内存。
# 2. 生命周期的理解
vue 生命周期总共分为 8 个阶段: 创建前/后,载入前/后,更新前/后, 销毁前/后。
beforeCreate (创建前)vue 实例的挂载元素$el 和数据对象 data 都是 undefined, 还未初始化
created (创建后) 完成了 data 数据初始化, el 还未初始化
beforeMount (载入前) vue 实例的$el 和 data 都初始化了, 相关的 render 函数首次被调用。实例已完成以下的配置:编译模板,把 data 里面的数据和模板生成 html。注意此时还没有挂载 html 到页面上。
mounted (载入后) 在 el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的 html 内容替换 el 属性指向的 DOM 对象。完成模板中的 html 渲染到 html 页面中。此过程中进行 ajax 交互
beforeUpdate (更新前) 在数据更新之前调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
updated (更新后) 在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。调用时,组件 DOM 已经更新,所以可以执行依赖于 DOM 的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
beforeDestroy (销毁前) 在实例销毁之前调用。实例仍然完全可用。 destroyed (销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被退出时调用
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。 如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面 loading 时间;
- ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
# 3. data 为什么是一个函数
组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果
# 4. 双向数据绑定原理是什么 响应式原理又是啥 还有 v-model 原理
双向绑定:
- 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
- Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。
响应式原理:
- 就是指的是 Data => View 的变化原理
v-model 原理:
- 就是指的是 View => Data
vue 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调。
View => Data 其实就是给 input 事件绑定 oninput 事件,改变 data 里的属性的值,再触发 Data => view 去更新视图。就是 v-model 的原理
Data => view 通过 Object.defineProperty()去劫持每一个变量的 get,set。 在 VUE 里的话,变量一变化,set 发通知给 watcher,watcher 告知虚拟 DOM 树,叫它该比较了,我这有值变了,于是生成新的 dom 树进行一个比较,然后逐级分类比较,更新页面
实际上就是在劫持到 set,在里面进行 DOM 更新
# 5. diff 算法
详见vue源码解析
# 6. v-for 里的 key
详见vue源码解析
# 7. vue-router 钩子函数
1、全局路由守卫:是指路由实例上直接操作的钩子函数,特点是所有路由配置的组件都会触发,直白点就是触发路由就会触发这些钩子函数
beforeEach(to,from, next)
beforeResolve(to,from, next)
afterEach(to,from)
2、路由独享守卫: 是指在单个路由配置的时候也可以设置的钩子函数
- beforeEnter(to,from, next)
3、组件守卫:是指写在组件内执行的钩子函数,类似于组件内的生命周期,相当于为配置路由的组件添加的生命周期钩子函数。
beforeRouteEnter(to,from, next)
beforeRouteUpdate(to,from, next)
beforeRouteLeave(to,from, next)
顺序:
- router.beforeEach
- beforeEnter
- beforeRouteEnter
- router.beforeResolve
- router.afterEach
# 8. react 和 vue 的对比
共同点:
- 都使用虚拟 dom
- 提倡组件化
- 注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。
区别:
- vue 采用 HTML 和 JS,CSS 分离的写法,更加直观。react 使用 JSX 语法
- vue 双向绑定,react 是单向数据流
vue 优势
- 简单的语法及项目创建,易于上手
- 更快的渲染速度和更小的体积
- 中国框架,文档更加完善
React 的优势
- 更适用于大型应用和更好的可测试性
- 同时适用于 Web 端和原生 App
- 扩展性更强
- 支持 TS
总结
- 当 state 特别多的时候,Watcher 会很多,会导致卡顿,所以大型应用(状态特别多的)一般用 React,更加可控。可对于易用性来说,VUE 是更容易上手的,对于项目来说新人更容易接手。
# 9. 父子组件的生命周期
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted 总结:从外到内,再从内到外
# 10. 组件间通讯
props 和$emit 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
$parent,$children 获取当前组件的父组件和当前组件的子组件
$attrs 和$listeners A->B->C。Vue 2.4 开始提供了$attrs 和$listeners 来解决这个问题
父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中使用,但是写组件库时很常用)
$refs 获取组件实例
envetBus 兄弟组件数据传递 这种情况下可以使用事件总线的方式.Vue.prototype.$eventBus = new Vue();vue可以做事件总线的载体
# 11. keep-alive
多了 2 个生命周期钩子
activated 当 keep-alive 包含的组件再次渲染的时候触发 deactivated 当 keep-alive 包含的组件销毁的时候触发
有 3 个属性:include(包含的组件缓存生效) 与 exclude(排除的组件不缓存,优先级大于 include) ,max 缓存组件的最大值(类型为字符或者数字,可以控制缓存组件的个数)
部分组件或者路由缓存
<keep-alive include="a"> <router-view></router-view> </keep-alive>
或者
//在路由中 { path: '/', name: 'home', component: Home, meta: { keepAlive: true // 需要被缓存 } } //使用时 <keep-alive> <router-view v-if="$route.meta.keepAlive"> <!-- 这里是会被缓存的视图组件,比如 Home! --> </router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"> <!-- 这里是不会被缓存的视图组件,比如 Profile! --> </router-view>
# 12. 自定义指令
https://blog.csdn.net/smlljet/article/details/91874728 (opens new window)
注册方法: 全局注册:
//main.js里 Vue.directive('focus', { inserted: function(el) { el.focus() } })
局部注册
<template> <div class="hello"> <div v-test="name"></div> </div> </template> <script> export default { data () { return { name:'我是名字', } }, directives:{ test:{ inserted: function (el,binding) {// 指令的定义 / /el为绑定元素,可以对其进行dom操作 console.log(binding) //一个对象,包含很多属性属性 }, bind: function (el, binding, vnode) { el.innerHTML =binding.value } } }, } </script>
钩子函数和参数
钩子函数
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用。
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
参数
- el:指令所绑定的元素,可以用来直接操作 DOM 。
- binding:一个对象,包含以下属性:
- name:指令名,不包括 v- 前缀。
- value:指令的绑定值,例如:v-my-directive=“1 + 1” 中,绑定值为 2。
- oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
- expression:字符串形式的指令表达式。例如 v-my-directive=“1 + 1” 中,表达式为 “1 + 1”。
- arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 “foo”。
- modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
- vnode: Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用
- 实例:拖拽自定义指令
Vue.directive('dialogDrag', {
bind(el) {
const clientType = /Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent) ? 'mobile' : 'pc';
console.log('clientType', clientType);
const dialogHeaderEl = el.querySelector('.module_bg_title');
const dragDom = el;
dialogHeaderEl.style.cursor = 'move';
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null);
dialogHeaderEl[clientType === 'mobile' ? 'ontouchstart' : 'onmousedown'] = (e) => {
// console.log('屏幕高度', document.documentElement.clientHeight);
// console.log('弹窗高度', dragDom.offsetHeight);
// 鼠标按下,获取鼠标在盒子内的坐标
const disX = (clientType === 'mobile' ? e.touches[0].clientX : e.clientX) - dialogHeaderEl.offsetLeft;
const disY = (clientType === 'mobile' ? e.touches[0].clientY : e.clientY) - dialogHeaderEl.offsetTop;
// 获取到的值带px 正则匹配替换
let styL;
let styT;
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (sty.left.includes('%')) {
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100);
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100);
} else {
styL = +sty.left.replace(/\px/g, '');
styT = +sty.top.replace(/\px/g, '');
}
// 鼠标移动的时候,把鼠标在页面中的坐标,减去鼠标在盒子内的坐标就是模态框的left和top值
document[clientType === 'mobile' ? 'ontouchmove' : 'onmousemove'] = function (e) {
// 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
const l = (clientType === 'mobile' ? e.touches[0].clientX : e.clientX) - disX;
const t = (clientType === 'mobile' ? e.touches[0].clientY : e.clientY) - disY;
const finallyL = l + styL;
const finallyT = t + styT;
// 边界值判定 注意clientWidth scrollWidth区别 要减去之前的top left值
// const limitL = (document.documentElement.clientWidth - dragDom.clientWidth) / 2;
// if (finallyL < -limitL) {
// finallyL = -limitL;
// } else if (finallyL > limitL) {
// finallyL = limitL;
// }
// const limitT = document.documentElement.clientHeight * 15 / 100;
// const limitB = document.documentElement.clientHeight - dragDom.clientHeight - limitT;
// if (finallyT < -limitT) {
// finallyT = -limitT;
// } else if (finallyT > limitB) {
// finallyT = limitB;
// }
// 移动当前元素
dragDom.style.left = `${finallyL}px`;
dragDom.style.top = `${finallyT}px`;
};
document[clientType === 'mobile' ? 'ontouchend' : 'onmouseup'] = function (e) {
document[clientType === 'mobile' ? 'ontouchmove' : 'onmousemove'] = null;
document[clientType === 'mobile' ? 'ontouchstart' : 'onmousedown'] = null;
};
};
},
});
# 13. 修饰符有哪些
事件修饰符
- stop 阻止事件继续传播
- native 元素原生事件。
- prevent 阻止标签默认行为
- capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
- self 只当在 event.target 是当前元素自身时触发处理函数
- once 事件将只会触发一次
- passive 告诉浏览器你不想阻止事件的默认行为
v-model 的修饰符
.lazy 通过这个修饰符,转变为在 change 事件再同步
.number 自动将用户的输入值转化为数值类型
.trim 自动过滤用户输入的首尾空格
键盘事件的修饰符
- .enter
- .tab
- .delete (捕获“删除”和“退格”键)
- .esc
- .space
- .up
- .down
- .left
- .right
# 14. 函数式组件使用场景和原理
????
<script>
// 引入的文件里会有多个vue页面,要看情况渲染
import CompPropsConfig from '@/components/iTemplate/config.js'
export default {
components: {
propsPanel: {
props: {
name: {
type: String,
required: true
}
},
render(h) {
let _module = CompPropsConfig[this.name] //确定取哪个VUE
return h(_module, {
props: {
name: {
type: String
}
}
})
}
}
},
data() {
return {}
}
}
</script>
# 15. vue-router 中常用的路由模式实现原理吗
- 传统的页面,所有的网页地址,都是请求到后端,由后端返回相应的 HTML 页面回来的。没有前端路由这一说
- 现在很多单页面应用,只有在初次加载或者刷新时,才会加载 index.html。页面跳转的时候,是不会进行请求的
- 哈希模式的时候,有一个#号的锚点,在#后面的地址,实际上是不会传递给后台的。所以是前端路由。在前端路由变化的时候,前端利用 Object.onhashchange() 监听hash的变化,并且加载页面。而这一切,后台并不知情。始终是 index.html
- histtory 模式的时候,利用 pushState、replaceState 可以记录修改 URL 历史记录,从而可以直接进行页面跳转而不触发向后台发送请求。但是刷新的时候会再次请求当前 URL 地址。后台如果不做处理的话,会找不到当前的请求地址从而 404.后台需要将不存在的请求重定向到 index.html
# 16.混入
全局混入
import Mixins from '@/mixins/Index' // 在main.js里混入 Vue.mixin(Mixins)
局部混入
// 在组件内 mixins: [mixin],
数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
# 17.插槽
普通插槽 就是一个
<!-- 父 --> <child> <p>里面的东西会插入到子组件</p> </child> <!-- 子 --> <slot> 父组件不传东西的时候,会显示这个默认值,父组件加了插槽,就覆盖这个 </slot>
具名插槽 跟上面一样,就是带了个name
<!-- 父 --> <child> <templete v-slot="mingzi"> <p>里面的东西会插入到子组件</p> </templete> </child> <!-- 子 --> <slot name="mingzi"> 父组件不传东西的时候,会显示这个默认值,父组件加了插槽,就覆盖这个 </slot>
作用域插槽 父组件能使用自组件的数据