Vue的hash模式和history模式有什么区别?
- 显示差别:
- hash模式的路由地址上有#号
- history模式没有
- 在做回车刷新时:
- hash模式会加重对应页面
- history模式会报错404
- 浏览器支持:
- hash模式支持低版本浏览器
- history模式不支持低版本浏览器,因为是H5新增的API
- 场景适用:都适用于单页面应用
- hash模式不会重新加载页面
- history模式需要后端配合支持,否则会出现大量404
- 历史记录:
- hash模式无历史记录
- history模式有历史记录,H5新增了pushstate和replacestate去修改历史记录,并不会立刻发送请求
- 后台配置:
- hash模式不需要
- history模式需要
说一下Vue的动态路由
要在路由设置里设置meta属性,扩展权限相关的字段,在路由导航首位通过判断这个权限表示,实现路由的动态增加和调整
根据用户登录的账号,返回用户角色
前端再根据角色,跟路由表的meta.role进行匹配
把匹配搭配的路由形成可访问的路由
computed 和 watch 的区别
- 属性类型
- computed是计算属性
- watch是监听属性,监听的data中数据的变化
- 缓存
- computed是支持缓存。依赖的属性值发生变化,计算属性才会重新计算,否则用缓存
- watch不支持缓存
- 是否支持异步
- computed不支持异步
- watch可以操作
- 第一次加载
- computed是第一次加载就监听
- watch是第一次加载默认不监听,当immediate的值设置为true时,就会监听
- 关键字
- computed函数中必须有return
- watch不需要
vuex在什么场景去使用?属性有哪些?
-
属性
- state 存储变量
- getters state的计算属性
- mutations 提交更新数据的方法
- actions 和mutations差不多,他是提交mutations来修改属性,可以包括异步操作
- modules 模块化vuex
-
使用场景
用户的个人信息、购物车模块、订单模块
【Vue2】【Vuex】状态管理方式 - 掘金 (juejin.cn)
vue的双向绑定原理是什么?
通过数据劫持和发布订阅者模式来实现,同时利用 Object.defineProperty() 劫持各个属性的setter和getter,在数据发生改变的时候发布消息给订阅者,触发对应的监听回调渲染视图,也就是说数据和视图是同步的。
数据发生改变,数据跟着发生改变;视图改变,数据也会发生改变
- 第一步:需要 observer 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter
- 第二步:compile模板解析指令,把模板中的变量替换成数据,然后初始化渲染视图,同时把每个指令对应的节点绑定上更新函数,参加订阅者,如果数据变化,收到通知,更新视图
- Watcher订阅者是Observer和Compile之间的通信桥梁,作用:
- 在自身实例化的时候往订阅器内添加自己
- 自身要有一个update()的方法
- 等等属性变动时,调用自身的update方法,触发compile这种的回调
- 第四步,MVVM作为数据绑定的入口,整合了Observer、Compile和watcher三者,整合了 observer、compile和watcher三者,通过observer来监听自己的数据变化,通过compile解析模板指令,最后利用watcher把observer和compile联系起来,最终达到数据更新视图更新,视图更新数据更新的效果
【拓展】
- Vue2
-
使用Object.definedProperty()
Object.definedProperty() 是 es5 的属性,其通过 get() 和 set() 方法劫持数据的读写。
-
对data数据进行遍历
-
针对数组需要额外处理
-
- Vue3
-
Proxy()
Proxy() 是ES6的构造函数,用于拦截外界对目标对象的读写。不需要进行遍历
-
ref()、reactive()
ref()、reactive()随时可以添加
-
不需要对数组进行额外处理
Proxy() 可以实现对数组的监听
-
了解diff算法和虚拟DOM吗?
-
虚拟DOM:描述元素和元素之间的关系,创建一个JS对象
如果组件内有响应的数据,数据发生改变的时候,render函数会生成一个新的虚拟DOM,这个新的虚拟DOM会和旧的虚拟DOM进行对比,找到需要修改的虚拟DOM内容,然后去对应的真实DOM中修改
-
diff算法:就是虚拟DOM的比对时使用的,返回一个patch对象,这个对象的作用就是存储两个节点不同的地方,最后用patch里记录的信息进行更新真实DOM
算法运行步骤:
- JS对象表示真实的DOM结构,要生成一个虚拟DOM,再用虚拟DOM构建一个真实DOM树,渲染到页面
- 状态改变生成新的虚拟DOM,根就得跟虚拟DOM进行比对,这个比对的过程就是diff算法,利用patch记录差异
- 把记录的差异用在第一个虚拟DOM生成的真实DOM上,就更新了
vuex的响应式处理
vuex是vue的状态管理工具
vue中可以直接触发 methods 中的方法,vuex是不可以的。未来处理异步,当触发事件的时候,会通过dispatch来访问actions中的方法,actions中的commit会触发mutations中的方法从而修改state里的值,通过getter把数据更新到视图中
Vue.use(vuex),调用install方法,通过applyMixin(vue)在任意组件内执行 this.$store 就可以访问到 store 对象。
vuex的state是响应式的,借助的就是vue的data,state存到vue实例组件的data中
vuex刷新数据会丢失吗?怎么解决?
页面肯定会重新获取数据,页面也会丢失数据
-
将数据直接保存在浏览器缓存里(cookie、localstorage、sessionstorage)
-
页面刷新的时候,再次请求数据,达到动态更新的方法
监听浏览器的刷新数据,在舒心前将数据保存到 sessionstorage 里,刷新请求数据,请求到了用vuex,如果没有那就用 sessionstorage 里的数据
vue中遍历全局的方法有哪些?
- 普通遍历:对象.forEach();
- 对元素统一操作:对象.map();
- 查找符合条件的元素,对象.filter();
- 查询符合条件的元素,返回索引:对象.findIndex();
new Vue() 做了什么?
new Vue() 是创建一个新的Vue实例,我们可以从以下几个步骤去分析new Vue() 整个过程都干了些什么?
首先第一步就是“初始化”,在该步骤中,会对组件实例进行初始化操作。
- 第一步:初始化
- 首先是进行一些必要的初始化操作:合并配置项、生命周期钩子函数、监听事件和render函数。因为Vue框架规定,每个实例,必须有规定的生命周期函数等,所以在一开始,我们需要将这几项初始化好。
- 接着就是执行钩子函数
beforeCreate - 此时就可以初始化注入函数、状态
- 接着就继续执行钩子函数
created
当需要的东西都已初始化完毕,那么下一步,我们就需要将该组件实例转换成VNode,并将VNode转换为真实DOM,并挂载到页面上。这一步我们可以叫“挂载”
-
第二步:挂载
- 挂载前,需要先确认render函数存在与否,template存在与否,如果它们不存在就需要做一些处理
- 等这些都处理完毕后,就可以执行钩子函数
beforeMount - 接着,我们就需要为更新机制做些准备工作,比如:初始化
updateComponent、watcherOptions,待准备工作完成,就需要进行更新机制的形成了 - 到此为止,我们的组件实例就已经完成挂载操作,然后执行钩子函数
mounted
需要特别注意的是更新机制形成的过程,
updateComponent的watcherOptions的作用:updateComponent,在初始化它时,我们的创建VNode和生成真实DOM这两工作,就是在这里完成的watcherOptions,初始化它后,在页面更新前会调用before函数,因此会触发钩子beforeCreate
接下来,我们说说生成VNode节点这个工作:如果组件实例不满足一定条件的话,就会直接返回一个空的VNode,满足了就会将组件实例转换成对应的VNode,并返回
此时我们已经得到该组件实例对应的VNode,需要将它们转换为真实DOM,并挂载到页面上。在转换之前,需要将旧状态的el、VNode和活动实例都备份一份下来,然后获取组件的最新信息,并更新指向。这里最重要的是patch算法,也就是我们日常所说的diff算法,它的规则是这样的:
-
新节点不存在,老节点存在,调用 destroy,销毁老节点
-
如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
-
如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
到此,真实DOM就被创建渲染完成。
其实,我们是不是也可以用建房子来举个简单的栗子:
建房子之前,我们是不是要先跟我们的小伙伴沟通好建房子所需的人力、材料等等信息的安排,这就是初始化
接下来,我们是不是应该商量这个房子的构造、地基等信息,画出房子的模型图,根据用户的实际需求一遍一遍的优化,这个过程是不是就可以算是生成VNode。
等我们确定了房子的模型,下一步就是正式开始建房子,打地基、垒墙、上梁、装修等等工作,是不是就是真实DOM的创建、渲染?
KeepAlive有什么作用?会触发什么生命周期钩子?为什么有的会触发有的不会触发?最近最少使用算法
-
KeepAlive的作用
-
keepalive的作用:就是KeepAlive内部组件,多个动态组件切换时,缓存被移除的组件。当再次回到被移除组件,就不需要重新加载,可以减少重复的加载和渲染
-
KeepAlive的好处:就是一方面可以避免重复加载渲染组件带来的开销,另一方面可以保留组件的状态,当我们再次回到组件内,页面状态还是离开前的状态。最重要的是,可以防止重复加载渲染DOM,减少加载时间和性能消耗,从而提高用户体验性
-
KeepAlive的缺点:最主要的是当组件里面包含大量内容的时候就会占用更多的内容,相当于是在用空间换时间
-
特别注意点:KeepAlive内部组件使用了iframe的时候,切换组件,iframe依然是会卸载的。理由如下:
因为iframe是在组件内独立开辟了一个窗口,这个窗口很多东西是不受到父级组件的影响的,当它父级组件进入缓存的时候,iframe就自动卸载。这个问题的解决方案最简单的就是在回到该组件的时候重新挂载iframe
想了解其它解决方案可以看我这篇文章:【Vue】页面使用了KeepAlive缓存机制后,切换标签含有iframe标签的界面只有最后被打开的页面有iframe正常展示 - 掘金 (juejin.cn)
-
-
触发的生命周期 KeepAlive会触发
activated和deactivated这两个钩子函数,并且这两个钩子在服务器渲染期间不被调用activated:初始化页面和组件再次被激活都会触发deactivated:在缓存的组件停用和卸载页面时都会触发
另外,还需要我们注意的几点是:
- KeepAlive不会再函数式组件中正常工作,因为它们没有缓存实例。
- 事件挂载的方法等,只执行一次的可以放在created或mounted中,需要每次进去都执行的方法可以放在 actived中
- KeepAlive是一个内置组件,一个抽象组件,与 template有点相似,都会有实际的标签
- Vnode的 cache 构建,是在 KeepAlive 组件的 mounted 和 updated 两个生命周期通过 cacheSubtree 方法构建
- 被KeepAlive包裹的组件不会生成真正的DOM节点。因为在Vue初始化生命周期的时候,为组件实例建立父子关系会根据
abstract属性决定是否忽略某个组件。在KeepAlive中,设置了abstract: true,那Vue就会跳过该组件实例。最后构建的组件树中就不会包含 KeepAlive 组件,那么由组件树渲染成的DOM树自然也不会有KeepAlive相关的节点
-
KeepAlive的原理: 在created钩子函数调用时将需要缓存的Vnode节点保存在this.cache中,或者是在页面渲染时,发现this.cache中已缓存对应的Vnode,就直接取缓存的Vnode实例渲染
KeepAlive的具体实现步骤如下:
-
第一步,获取KeepAlive包裹着的第一个子组件对象及组件名
-
第二步,根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。如果不匹配就直接返回组件实例(Vnode),否则执行第三步
-
第三步:根据组件ID和tag生成缓存key,并在缓存对象中查找是否一缓存过该组件实例。如果存在,就直接取出缓存值,并更新该key在this.keys中的位置(更新key的位置是实现
LRU置换策略的关键),否则执行第四步 -
第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max设置值,超过就根据LRU置换策略删除最久未使用的实例(即下标为0的那个key)
-
第五步:将该组件实例的keepAlive属性值设置为true。
这里面最重要的就是 LRU(Least recently Used)算法
LRU(Least recently Used)算法:
LRU(Least recently Used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
KeepAlive 的实现正是用到了 LRU策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]
-
怎么设计一个modal组件
modal组件,我们首先需要确认modal组件的构成有哪些?按照从外到内,从上到下的顺序,我们可以划分这几部分:遮罩层、标题、主体内容和底部按钮
modal组件的构成确定后,我们可以想想这个组件需要满足的设计有哪些?
- modal组件挂载
- 支持用户自定义组件挂载位置,默认挂载在body上
- 遮罩层
- 要支持是否显示遮罩层
- 支持并暴露出单击遮罩层关闭弹窗的接口,也需要支持用户可关闭此功能
- 支持用户自定义遮罩层样式
- 弹窗
- 支持用户自定义弹窗样式(宽度、高度等)和位置
- 支持弹窗在页面中居中显示
- 标题
- 支持文本
- 支持用户传入一段html代码
- 支持用户不要标题块
- 内容
- 支持文本
- 支持用户传入一段html代码
- 底部
- 默认两个按钮“取消”和“确定”,并暴露这两按钮的回调
- 支持用户自定义这两个按钮的文本、显示和样式属性等的控制
- 支持用户自定义底部内容
- 右上角的关闭小图标
- 支持用户自定义小图标
- 支持用户控制小图标的显示与否
- 暴露该按钮的回调
- 关闭弹窗
- 支持用户可配置关闭时是否销毁modal里的子元素
- 支持用户可通过 ESC 键关闭弹窗
手摸手实现一个轻量级可扩展的模态框(Modal)组件 - 掘金
slot(插槽)的理解,和使用场景有哪些?
slot,Vue的slot跟原生的slot存在差异,Vue中的是对原生的进行了一些功能拓展。
slot本质上是返回VNode的函数
slot的作用,有以下几点:
- 允许我们将组件的内容分发到其子组件中,以实现灵活的组件复用和自定义布局。
- 通过slot,父组件可以向子组件传递内容,这样可以根据需求定制组件的外观和行为。因为slot的作用域在父组件,不在子组件,能访问到父组件的数据,不能访问到子组件的数据
- slot提供了一种强大的方式来创建通用组件,使其更具可定制性
另外就是,slot的种类也有以下几种:
- 默认插槽:没有给插槽设置给
name设置值,父组件的内容会直接被塞入默认插槽中。因为当我们不曾给name设置值时,它的默认值就是"default" - 具名插槽:就是插槽的name,我们设置了特定值,一个组件中可以设置多个具名插槽。会将父组件中的内容分发到特定的插槽位置,以满足不同情况下的布局需求
- 作用域插槽:Vue提供了作用域插槽,许父组件传递数据到插槽内部,这样子组件可以访问和渲染这些数据。
最后,就是插槽的使用场景。因为通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用
比如布局组件、表格列、下拉选、弹框显示内容等
Vue的响应式原理
注意注意注意!!!响应式和双向绑定是两个东西!!!
响应式就是数据发生变化,触发页面重新渲染。
我们Vue2和Vue3实现响应式的原理是不同的。Vue2 是使用的 object.defineProperty的方式,Vue3 是使用的 Proxy方式。为啥Vue3的会跟Vue2的不同呢?主要理由如下:
object.defineProperty的实现是重写了set和get。而且,这个方法只能遍历对象属性进行劫持。也无法监听到对象和数组的变化的,所以为了监听对象的变化提供了两个全局方法$set和$delete;为了监听数组的变化重写了数组的几个常用API(push、splice...)。在实现时,还需要在初始化的时候遍历data,对每个数据进行处理,当data中的数据多或者层级很深的时候,就会出现白屏的情况Proxy是可以直接劫持整个对象,并返回一个新对象,所以我们可以只操作新的对象达到响应式的目的。所以Vue2中无法监听数组和对象的,或者白屏的问题都不会出现在Vu3中。唯一算得上是缺点的就是在浏览器上的兼容性会差一点,不支持IE9以下的
Vue的双向绑定原理
注意注意注意!!!响应式和双向绑定是两个东西!!!
Vue的双向绑定是采用的MVVM架构模式,所以它的构成是这三部分:数据层、视图层和业务逻辑层。
其中 业务逻辑层(ViewModel)是框架最核心的存在,它负责将数据与视图关联起来,所以它的主要职责就是:在数据变化后更新视图,在视图变化后更新数据。如果打个比如,那就是张三和李四两人之间的传话筒。然后就是ViewModel的主要组成:是监听器(Observe)和解析器(compiler)
双向绑定的大致流程是这样的:
1)在 初始化的时候,会对data做一个遍历,进行响应化处理,目的是为了劫持监听所有属性,这个过程是发生在 observe(监听器)里的
2)同时对模板执行搬移(compiler),找到其中动态绑定的数据,从 data 中获取并初始化视图,这个过程发生在 解析器(compiler)中
3)同时定义一个更新函数和 Watcher,将来对应数据变化时 Watcher 会调用函数
4)由于 data 的某个可以在一个视图中可能出现多次,所以每个key都需要一个管家dep来管理多个 watcher
5)将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有 watcher 执行更新函数
Vue的双向绑定的指令只有一个 v-model,.async这个修饰符也可以实现双向绑定
为什么会出现 nextTick?底层是怎么实现的?
nextTick 是DOM下次更新刷新的工具方法,本质上是执行延迟回调的钩子,接受一个回调函数作为参数,在下次DOM更新循环结束之后执行延迟回调。之所以我们需要 nextTick,理由很简单,跟Vue的异步更新机制有关,因为在我们刷新响应式数据后,Vue为了性能等多方面因素考虑,并不会立即更新该数据对应的DOM,而是会将多个更新一次执行,一次此时我们拿到的DOM还是旧状态,如果需要最新的DOM状态,就需要在更新后再去获取DOM。那么怎么才能知道DOM已经更新了呢?那就需要 nextTick 这样的API
nextTick 它的原理是这样的,在初始化时,会创建一个回调数据和异步延迟函数。然后在执行 nextTick 时,将注册的回调函数放入回调函数队列中,如果此时没有回调任务在执行,就执行该回调任务。如果没有传入回调函数就直接返回一个 promise。
所以在使用的时候,我们可以采用传入回调函数和 async/await 这两种方式。
另外就是,执行异步操作队列可能是宏任务,也可能是微任务,这个是根据我们当前环境的实际情况而定的。
Vue 的生命周期有哪些?每个钩子可以做什么?父子组件钩子的执行顺序
Vue的生命周期我们可以按照以下几个阶段去划分:
-
创建前后有 beforeCreate 和 created。
- beforeCreated 表示组件实例被创建之初。因为是在实例还未挂载之前执行,所以此时要想使用组件实例,需要通过回调next,通常用于开发中执行一些初始化任务
- created 表示组件实例已经完成创建。此时组件初始化完毕,各种数据都可以使用,但是需要注意的是此时依然还是不能获取组件实例上下文。通常用于无依赖的异步数据获取
-
挂载前后有 beforeMount 和 mounted
- beforeMount 表示组件挂载,此时尚未执行渲、更新,DOM也未创建,此时可以在渲染前最后一次更改数据,还不会触发钩子函数
- mounted 表格组件挂载完毕。此时初始化结束,DOM已创建,可用于访问数据和DOM元素
-
更新前后有 beforeUpdata 和 updated
-
beforeUpdate 表示data数据发生变化之前,所以在这里我们可以用来获取数据发生变化之前的数据状态
-
updated 表示data数据更新后,我们可以在这里获取数据的最新状态
另外,我们需要知道的是,不是所有的data数据发生变化,都会触发这两个钩子。因为这两钩子是针对视图层数据,简单点理解,就是data中的响应式数据才会触发,或者也可以说是直接在标签中使用了的data数据
-
-
销毁前后有 beforeDestory 和 destoryed
- beforeDestory 组件实例销毁之前,此时我们仍然可以使用data和method。因此我们可以在这里销毁订阅定时器等
- destoryed 组件实例销毁之后,此时data和method已经不可以使用,可以用来定时和监听的取消
-
特殊场景 activated、deactivated 和 errorCaptured
- activated 只有在 KeepAlive 组件中才会使用到的钩子,表示缓存组件激活。让页面初始化或再次进到该页面时,就会触发
- deactivated 也是只有在 KeepAlive 组件中才会使用到的钩子,表示缓存组件停用。离开这个页面就会触发
- errorCaptured 捕获一个来自子孙组件的错误时被调用
另外,就是在Vue3还新增了两生命周期onRenderTracked和onRenderTriggered,它们都是调试钩子,简单的理解就是只在开发调试的时候才会触发执行。
onRenderTracked的作用就是在每次触发页面重新传染的时候自动执行,是会追踪每个值onRenderTriggered的作用就是当响应式依赖发生变更触发可组件渲染的时候调用,是精准追击,方便开发针对性调试
组件的data为什么是一个函数?data里的同名数据怎么处理?
是因为如果是使用对象的方式,不同组件的data属性值共用了同一个内存地址,会导致组件之间的data属性相互干扰。而且,另一方面也是因为组件不能直接访问Vue实例中定义的数据。就算是可以访问,若将所有的数据都放在Vue实例中,Vue实例就会变得非常臃肿,所以组件应该有自己保存数据的地方
源码实现上,是两种方式都可以,但是在组件进行创建的时候,会进行选项的合并,定义data会进行数据校验,此时vm实例为undefined,若data类型不是 function,救护出现警告
这不是说,data在所有情况下都不适合用对象来定义,在根实例对象data可以是对象也可以是函数,因为根实例是单例,不会产生数据污染的情况。
组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象