Vue响应式的原理
Vue响应式数据的原理设计思想是:数据劫持 + 观察者模式。
- 通过Object.defineProperty()对每个属性进行监听,当对属性进行读取的时候就会触发getter,对属性修改的时候就会触发setter。
- Vue实例中有data属性定义响应式数据,data中每一个属性都会带
_ob_属性,是一个Observer对象。 - 执行new Vue创建实例时,
- 给data属性创建Observer实例,通过注册响应式函数initState调用initData函数实现为data创建Observer实例
- Observer类中调用defineReactive方法,其方法主要:创建Dep实例(
const dep = new Dep())、给对象属性添加get/set方法、收集依赖(dep.depend())、通知依赖更新(dep.notify())。 - data中的每一个属性都会带get、set方法,在Vue源码中是把get、set定义为reactiveGetter、reactiveSetter
Vue实现响应式主要是采用数据劫持结合发布-订阅模式的方式。整合
Observer,Compiler和Watcher - Observer
观察者。Vue 通过 Observer 对数据对象的所有属性进行监听,当把一个普通对象传给 Vue 实例的
data选项时,Observer 将遍历它的所有属性,并为其添加getter和setter。getter将收集此属性所有的订阅者,setter将在属性发生变动的时候,重新为此属性赋值,并通知订阅者调用其对应的更新函数。- Vue2中通过ES5的
object.definedProperty()方法实现 - Vue3中通过ES6的
new Proxy()实现
- Vue2中通过ES5的
- Compiler
模板编译器。它的作用是对每个元素节点的指令
v-和模板语法{{}}进行扫描,替换对应的真实数据,或绑定相应的事件函数。 - Watcher
Watcher能够订阅并收到每个属性变动的通知,然后执行相应的回调函数。
总结Vue响应式原理
通过Object.defineProperty去劫持data里的属性,将data全部属性替换成getter和setter,配合发布者和订阅者模式,每一个组件都有一个watcher实例,当我们对data属性赋值和改变,就会触发setter,setter会通知watcher,从而使它关联的组件进行重新渲染。
Object.defineProperty()的缺点
- defineProperty定义对象不能监听添加额外属性或修改额外添加的属性的变化;
- 用Vue.set添加
- defineProperty定义对象不能监听根据自身数组下标修改数组元素的变化。
Vue双向数据绑定原理
v-model本质上是语法糖,v-model 默认会解析成名为 value的 prop 和名为 input 的事件。这种语法糖的方式是典型的双向绑定;
Vue生命周期
Vue生命周期的阶段
分为8个阶段:创建前/后,载入前/后,更新前/后,销毁前/后
-
beforeCreate:是 new Vue( ) 之后触发的第一个钩子,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。
-
created:在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 DOM 进行交互,如果非要想,可以通过 vm.$nextTick 来访问 DOM 。
-
beforeMount:发生在挂载之前,在这之前 template 模板已导入渲染函数编译。而当前阶段虚拟 DOM 已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发 updated。
-
mounted:在挂载完成后发生,在当前阶段,真实的 DOM 挂载完毕,数据完成双向绑定,可以访问到 DOM 节点,使用 $refs 属性对 DOM 进行操作。
-
beforeUpdate:发生在更新之前,也就是响应式数据发生更新,虚拟 DOM 重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
-
updated:发生在更新完成之后,当前阶段组件 DOM 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
-
beforeDestroy:发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
-
destroyed:发生在实例销毁之后,这个时候只剩下了 DOM 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁
第一次页面加载会触发哪几个钩子
会触发 4 个钩子,分别是:beforeCreate、created、beforeMount、mounted
DOM 渲染在那个周期完成的
DOM渲染是在mounted阶段完成
父子组件中生命周期的调用顺序
组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。组件的销毁操作是先父后子,销毁完成的顺序是先子后父。
- 加载渲染过程:父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
- 子组件更新过程:父beforeUpdate->子beforeUpdate->子updated->父updated
- 父组件更新过程:父 beforeUpdate -> 父 updated
- 销毁过程:父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
为什么Vue2 this 能够直接获取到 data 和 methods
-
通过
this直接访问到methods里面的函数的原因是:因为methods里的方法通过bind指定了this为new Vue的实例(vm)。 -
通过
this直接访问到data里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的_data对象中,访问this.xxx,是访问Object.defineProperty代理后的this._data.xxx。
computed与watch的区别
- computed计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
- watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
Watch的 实现原理
Watch 的本质也是一个观察者 watcher,监听到值的变化就执行回调;
watch的初始化在data初始化之后,此时的data已经通过Object.defineProperty设置成了响应式;watch的key会在Watcher里进行值的读取,也就是立即执行get获取value,此时如果有immediate属性就立马执行watch对应的回调函数;- 当
data对应的key发生变化时,触发回调函数的执行;
Keep-alive原理
定义
keep-alive组件是用来缓存有状态组件的,可以将组件缓存起来并在需要时重新使用,不是每次重新创建。
原理
keep-alive 组件的实现原理是将被缓存的组件实例存储到一个缓存对象中,当需要重新渲染这个组件时,会从缓存中获取到之前的实例,并将其重新挂载到 DOM 上。
如何使用
keep-alive接收三个参数:
include:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存exclude:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存max:可传数字,限制缓存组件的最大数量。
具体实现
- keep-alive在内部维护了一个key数组和一个缓存对象cache
- key数组记录目前缓存的组件key值,没有key值就自动生成一个唯一的key值
- cache对象以key值为键,vnode为值,用于缓存组件对应的虚拟DOM
- 在keep-alive的渲染函数中,判断当前渲染的vnode是否有对应的缓存,有,从缓存中读取到对应的组件实例;没有就将其缓存
- 当缓存数量超过max数值时,keep-alive会移除掉key数组的第一个元素。
$nextTick的原理
有关的JS的事件循环(EventLoop)
- 执行栈执行同步任务
- 遇到异步任务时,回调函数加入消息队列中
- 执行栈为空时,EventLoop检查消息队列,有任务,将任务的回调函数推入到执行栈中执行
为啥要使用nextTick
Vue采用的是异步更新策略,当监听到数据发生变化的时候不会立即去更新Dom,而是开启一个任务队列,缓存在同一事件循环中发生的所有数据变更。
定义
nextTick 是 vue 提供的一个全局 API,由于 vue 的异步更新策略,当我们对数据的修改时,不会立即体现在 dom 变化上,此时如果想要立即获取更新后的 dom 状态,就需要使用这个方法。
使用场景
- 异步操作中:axios请求、setTimeout、Promise的then回调等
- Vue生命周期中,如果在created()钩子函数中进行DOM操作,需要放在nextTick()的回调函数中
- 操作Vue组件的refs属性时,需要在$nextTick中执行操作)
<div><button @click="focusInput">button</button>
<input ref="myInput"/></div>
export default {
methods:{
focusInput(){
this.$nextTick(()=>{this.$refs.myInput.focus()})
}
}
}
- 使用第三方库操作DOM
v-html会出现XSS攻击,如何防止XSS攻击
背景
富文本编辑器中,如果输入框中输入<img scr='../a.png' onerror='alert(1)'/>,当图片找不到时就会弹出1,就是XSS攻击
解决方法
- 使用vue-dompurify-html插件,XSS插件可以把除了标签和内容外的其他内容都过滤掉
1: 安装 npm install vue-dompurify-html@vue-legcy
2:引入: import VueDomPurifyHTML from 'vue-dompurify-html'
Vue.use(VueDomPurifyHTML)
3:项目中的v-html替换为v-dompurify-html即可
- 使用XSS插件
npm install xss
import xss from 'xss'
Vuer.prototype.xss = xss
在vue.config.js中重新写v-html指令
chainWebpack: config => {
config.module
.rule("vue")
.use("vue-loader")
.loader("vue-loader")
.tap(options => {
options.compilerOptions.directives = {
html(node, directiveMeta) {
(node.props || (node.props = [])).push({
name: "innerHTML",
value: `xss(_s(${directiveMeta.value}))`
});
}
};
return options;
});
}
vue-router路由
定义
vue是单页面应用(项目打包后,运行npm run build时,会生成dist文件夹[只有静态资源和index.html页面]),需要使用vue-router来管理
vue-router的原理
SPA:单一页面应用程序,加载页面时,不会加载整个页面,而是只更新某个指定容器中的内容。
- hash模式(vue默认模式)
- history模式
Vuex
定义:
vue应用中多个组件的共享状态进行集中式的管理
actions和mutations的区别
修改状态只能是mutations,actions只能通过提交mutation修改状态
组件间通信
父子组件通信
- props/emit触发事件
- ref与$parent
兄弟组件通信
- eventbus
- vuex
隔代组件通信
- eventbus
- vuex
- provide inject 父组件通过使用 provide(){return{}}提供需要传递的数据 子组件通过使用inject:["参数1“,”参数2“。。。]接收父组件传递的参数
slot
定义
slot(插槽),内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。
分类
- 默认插槽:slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有一个匿名插槽
- 具名插槽:带有具体名字的插槽,带有name属性的slot,一个组件可以有多个具名插槽
- 作用域插槽:在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,当父组件根据子组件穿的的数据决定如何渲染
Vue中key的作用
key主要用做Vue的虚拟DOM算法的提示。比对新旧节点组时辨识VNodes。如果不使用key,Vue会使用一种算法来最小化元素的移动并且尽可能尝试就地修改/复用相同类型元素。而使用 key 时,它会基于 key 的顺序变化重新排列元素,并且 key 不再存在的元素将始终被移除/销毁。
key使用场景
- 在元素切换中
- 在transition过渡中
- 在渲染列表中
v-for不建议使用index作为key
当使用 index 作为 key 的时候,使用 v-for 更新已渲染的元素列表时,默认用“就地复用”策略;列表数据改变的时候,它会根据key值去进行patch。
Vue做权限管理
- 页面权限
前端方案会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个
asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)方式动态添加路由即可。
后端方案会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过addRoutes动态添加路由信息
- 按钮权限
按钮权限的控制通常会实现一个指令,例如v-permission,将按钮要求角色通过值传给v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。
自定义指令,原理
指令本质上是装饰器,是 vue 对 HTML 元素的扩展。自定义指令有五个生命周期(也叫钩子函数),分别是 bind、inserted、update、componentUpdated、unbind
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
- update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
- componentUpdated:被绑定元素所在模板完成一次更新周期时调用
- unbind:只调用一次,指令与元素解绑时调用。
封装公共组件
为啥要封装
传统项目开发:效率低、难维护、复用性低等问题。使用组件可以“高内聚、低耦合”
vue组件三要素
1、props配置参数 2、slot定制插槽 3、event自定义事件
diff算法【双端diff算法】
通过新前与旧前、新后与旧后、新前与旧后、新后与旧前、暴力比对(乱序)的比较
- 新前:newChildren中所有未处理的第一个节点
- 新后:newChildren中所有未处理的最后一个节点
- 旧前:oldChildren中所有未处理的第一个节点
- 旧后:oldChildren中所有未处理的最后一个节点
新前与旧前的比较
新前与旧前对比,如果相同那么新,老的开始下标往后移动一格,上图中a的新老节点相同,位置移动b位置,此时新节点为f,两节点不同,进入新后与旧后比对。
新后与旧后的比较
新后与旧后对比,如果相同那么新,老的结束下标往前移动一格,上图中g的新老节点相同,位置移动f位置,此时新节点为b,两节点不同,这时发现新后与旧后,新前与旧前都不满足,进入新后与旧后比对。
新后与旧前的比较
新后与旧前对比,如果相同那么,把老的开始节点移动到老的结束节点后面,然后老的开始下标往后移动一格,新的结束下标往前移动一格。这时发现新的位置以上3种都不能满足,进入新前与旧后比对。
新前与旧后的比较
新前与旧后对比,如果相同那么,把老的结束节点移动到老的开始节点前面,然后新的开始下标往后移一格,老的结束下标往前移动一格。
暴力比对
如果节点比对的时候上面4种方法都不适用时,此时我们只能用最暴力的方法,首先我们需要循环oldChildren生成一个
key和index的映射表{'a': 0, 'b': 1},然后我们用新的开始节点的key,去映射表中查找,如果找到就把该节点移动到最前面,且原来的位置用undefined占位,避免数组塌陷 防止老节点移动走了之后破坏了初始的映射表位置,如果没有找到就直接把新节点插入。
采用的是双向指针遍历的算法。通过逐层比对新旧虚拟DOM树节点的方式来计算出更新需要做的最小操作集合。但这种算法的缺点是,由于遍历是从左到右、从上到下进行的,当发生节点删除或移动时,会导致其它节点位置的计算出现错误,因此会造成大量无效的重新渲染。
利用diff算法做的优化
- 使用唯一的key的属性:(key属性用于帮助Vue判断列表中每个子元素的身份,准确地进行差异比较)
- 异步更新:(nextTick方法,将一部分代码延迟到下一次DOM更新循环之后执行)
- 虚拟列表:(虚拟列表只会渲染可见区域的内容,当用户滚动时动态加载和卸载元素,减少DOM操作)
SSR
客户端请求服务器时,服务器到数据库中获取的数据,在服务器内部将vue组件渲染成HTML,并且将数据、HTML一并返回给客户端。
原理
VueSSR 的原理,主要就是通过 vue-server-renderer 把 Vue 的组件输出成一个完整 HTML,输出到客户端,到达客户端后重新展开为一个单页应用。
好处
- 有利于SEO
- 白屏时间更短
vue-router
定义:根据不同的URL请求,返回不同的资源
三种路由模式详解
- hash模式
- history模式
- abstract模式