Vue
computed 和 watch 的区别?
- computed:计算属性,依赖其它属性计算值,内部任一依赖项的变化都会重新执行该函数,计算属性有缓存,多次重复使用计算属性时会从缓存中获取返回值,计算属性必须要有return关键词。
- watch:侦听到某一数据的变化从而触发函数。当数据为对象类型时,对象中的属性值变化时需要使用深度侦听 deep 属性,也可在页面第一次加载时使用立即侦听 immediate 属性。
运用场景:
- 计算属性一般用在模板渲染中,某个值是依赖其它响应对象甚至是计算属性而来
- 侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑
watch 和 computed 和 methods 区别是什么?
- 思路:先翻译单词,再阐述作用,最后强行找不同。
- 要点:
- computed 和 methods 相比 computed 有缓存,如果 computed 属性依赖的属性没有变化,那么 computed 属性就不会重新计算;methods 则是看到一次计算一次。
- watch 和 computed 相比 computed 是计算出一个属性,而 watch 则是在数据变化时执行异步或开销较大的操作时使用。
computed 实现原理
- 当组件初始化的时候,computed 和 data 会分别建立各自的响应系统,Observer遍历 data 中每个属性设置 get/set 数据拦截
- 初始化 computed 会调用 initComputed 函数
- 注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher 或者其他观察该计算属性变化的 watcher )
- 调用计算属性时会触发其Object.defineProperty的get访问器函数
- 调用 watcher.depend() 方法向自身的消息订阅器 dep 的 subs 中添加其他属性的 watcher
- 调用 watcher 的 evaluate 方法(进而调用 watcher 的 get 方法)让自身成为其他 watcher 的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter 求值函数,当访问求值函数里面的属性(比如来自 data、props 或其他 computed)时,会同样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。
- 当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 dep 的 notify 方法,遍历当前 dep 中保存着所有订阅者 watcher 的 subs 数组,并逐个调用 watcher 的 update 方法,完成响应更新。
Vue 有哪些生命周期钩子函数?分别有什么用?
- beforeCreate 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
- created 在实例创建完成后被立即调用。 在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。
- beforeMount 在挂载开始之前被调用。 相关的 render 函数首次被调用。
- mounted 实例被挂载后调用。 这时 el 被新创建的 vm.$el 替换了。
- beforeUpdate 数据更新时调用,发生在虚拟 DOM 打补丁之前。
- updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
- activated 被 keep-alive 缓存的组件激活时调用。
- deactivated 被 keep-alive 缓存的组件停用时调用。
- beforeDestroy 实例销毁之前调用。 在这一步,实例仍然完全可用。
- destroyed
实例销毁后调用。
该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
- 注意:要特别说明哪个钩子里请求数据(mounted)。
- 补充:keep-alive
当在不同组件之间切换的时候,可以保持这些组件的状态,以避免反复重渲染导致的性能问题。<!-- 失活的组件将会被缓存!--> <keep-alive> <component v-bind:is="currentTabComponent"></component> </keep-alive> - 答案补充
- 创建前/后:在 beforeCreate 阶段,Vue 实例的挂载元素 el 还没有。
- 载入前/后:在 beforeMount 阶段,Vue 实例的 $el 和 data 都初始化了,但挂载之前还是为虚拟的 DOM 节点,data.message 还未替换;在 mounted 阶段,Vue 实例挂载完成,data.message 成功渲染。
- 更新前/后:当 data 变化时,会触发 beforeUpdate 和 updated 方法。
- 销毁前/后:在执行 destroy 方法后,对 data 的改变不会再触发周期函数,说明此时 Vue 实例已经解除了事件监听以及和 DOM 的绑定,但是 DOM 结构依然存在。
keep-alive 是什么?
- 作用:实现组件缓存,保持组件的状态,避免反复渲染导致的性能问题。
- 工作原理:Vue 内部将 DOM 节点,抽象成了一个个的 VNode 节点,
keep-alive组件的缓存也是基于 VNode 节点的。它将满足条件的组件在 cache 对象中缓存起来,重新渲染的时候再将 VNode 节点从 cache 对象中取出并渲染。 - 可以设置以下属性:
- include:字符串或正则,只有名称匹配的组件会被缓存
- exclude:字符串或正则,任何名称匹配的组件都不会被缓存
- max:数字,最多可以缓存多少组件实例
- 如果同时使用了 include、exclude,那么 exclude 的优先级高于 include
- 匹配首先检查组件的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称(父组件 components 选项的键值),匿名组件不能被匹配。
父子组件生命周期执行顺序
- 挂载阶段:
- 父组件beforeCreate
- 父组件created
- 父组件beforeMount
- 子组件beforeCreate
- 子组件created
- 子组件beforeMount
- 子组件mounted
- 父组件mounted
- 更新阶段:
- 父组件beforeUpdate
- 子组件beforeUpdate
- 子组件updated
- 父组件updated
- 销毁阶段:
- 父组件beforeDestroy
- 子组件beforeDestroy
- 子组件destroyed
- 父组件destroyed
Vue 如何实现组件间通信?
父子组件通信:
props父传子;$emit/v-on子通过派发事件的方式传给父$attrs包含父作用域里除 class 和 style 除外的非 props 属性集合,$listeners包含父作用域里.native除外的监听事件集合ref获取子组件的属性或者调用子组件的方法.sync/v-model实现父传子数据的双向绑定;通过$emit子改父$children获取到一个包含所有子组件的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法;$parent获取到父节点的 VueComponent 对象,包含父节点中所有数据和方法provide/inject依赖注入
兄弟组件通信:
- EventBus
定义:
使用:// 方法一:抽离成一个单独的 js 文件 Bus.js ,然后在需要的地方引入 // Bus.js import Vue from "vue" export default new Vue() // 方法二:直接挂载到全局 // main.js import Vue from "vue" Vue.prototype.$bus = new Vue() // 方法三:注入到 Vue 根对象上 // main.js import Vue from "vue" new Vue({ el: "#app", data: { Bus: new Vue() } })// 在需要向外部发送自定义事件的组件内 <template> <button @click="handlerClick">按钮</button> </template> import Bus from "./Bus.js" export default { methods: { handlerClick() { // 自定义事件名 sendMsg Bus.$emit("sendMsg", "向外部发送的数据") } } } // 在需要接收外部事件的组件内 import Bus from "./Bus.js" export default { mounted() { // 监听事件的触发 Bus.$on("sendMsg", data => { console.log("接收到的数据", data) }) }, beforeDestroy() { // 取消监听 Bus.$off("sendMsg") } } - Vuex
$parent
跨层级组件通信:
provide/inject- EventBus
- Vuex
$attrs/$listeners
为什么组件中的 data 是一个函数?
在new Vue()中,可以是函数也可以是对象,因为根实例只有一个,不会产生数据污染。
在组件中,data 必须为函数,目的是为了防止多个组件实例对象之间共用一个 data,产生数据污染;而采用函数的形式,初始化 data 时会将其作为工厂函数都会返回全新的 data 对象。
EventBus 原理
EventBus是消息传递的一种方式,基于一个消息中心,订阅和发布消息的模式,称为发布订阅者模式。
- 创建一个全局事件总线: 挂载在
Vue.prototype之上
// 第一种初始化方法
import Vue from 'vue'
const EventBus = new Vue()
// 第二种初始化方法
Vue.prototype.$EventBus = new Vue()
$on('name', fn)订阅消息,name:订阅的消息名称,fn:订阅的消息$emit('name', args)发布消息, name:发布的消息名称,args:发布的消息
EventBus 的缺点
- 事件类定义过多:每个事件都需要定义一个类,这在应用中可能会产生大量的事件类,增加了代码的复杂性。
- 难以调试和维护:事件的异步特性使得调试事件处理逻辑变得困难,随着应用规模的增长,管理大量的事件和订阅者可能会变得复杂。
- 事件的单向传递:EventBus 支持事件的单向传递,发送者无法获得接收者对事件的处理结果,这限制了事件通信的灵活性。
- 过度依赖 EventBus:在一些情况下,过度依赖 EventBus 可能会使得项目的架构变得不清晰,特别是在大型项目中,可能会使得状态管理变得复杂。
- 缺乏事务性:EventBus 事件的发送和接收不具备事务性,如果事件处理过程中出现异常,可能需要额外的机制来保证事件处理的完整性。
- 命名空间冲突:EventBus 使用单个命名空间,不同的组件可能会使用相同的事件名,导致命名冲突。
- 事件监听器的销毁问题:在组件销毁时,需要手动销毁事件监听器,否则可能会引发性能问题或难以排查的 Bug。
Vue 数据响应式怎么做到的?
-
答案在文档《深入响应式原理》
-
要点:
- 使用
Object.defineProperty()把这些属性全部转为 getter/setter。 补充:Object.defineProperty(obj, prop, descriptor)方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。- 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
- 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
- Vue 不能检测到对象属性(以及数组)的添加或删除,解决方法是手动调用
Vue.set或者this.$set。 补充:- 对象
Vue.set(object, propertyName, value) - 数组
Vue.set(vm.items, indexOfItem, newValue)
- 对象
- 使用
-
补充
- Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值。
- Vue 在更新 DOM 时是异步执行的。
-
参考答案(1):vue.js 是采用 数据劫持 结合 发布者-订阅者模式 的方式,通过
Object.defineProperty()来劫持各个属性的 setter/getter,在数据变动时发布消息给订阅者,触发相应的监听回调。- 对需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter&getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
- compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
- Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
- 在自身实例化时往属性订阅器(dep)里面添加自己
- 自身必须有一个 update() 方法
- 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退
- MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到 数据变化 -> 视图更新,视图交互变化(input) -> 数据model变更 的双向绑定效果。 *补充:Data, view, Observer(监听者), Dep(订阅者), Wather(观察者)。Observer 监听数据 Data 的变化,通知 Dep 订阅者,Dep 者回调给 Wather 观察者,观察者更新 View;Observer 监听数据 Data 的变化,通知所有的观察者列表 Dep。
-
参考答案(2):Vue 的响应式原理是核心是通过
Object.definedProperty()完成的,他为所有的 data 的属性绑定 get 和 set 方法,当读取 data 中的数据时自动调用 get 方法,当修改 data 中的数据时,自动调用 set 方法。检测到数据的变化,会通知观察者 Watcher,重新 render 当前组件(子组件不会重新渲染),生成新的虚拟 DOM 树,Vue 框架会遍历并对比新虚拟 DOM 树和旧虚拟 DOM 树中每个节点的差别,并记录下来,最后,加载操作,将所有记录的不同点,局部修改到真实 DOM 树上。
vue.$set方法的原理
- 传入的target如果是undefined、null或是原始类型,则直接跑出错误。
- 如果传入的是一个数组的话,就会调用数组的splice方法进行实现响应式。
- 如果不是一个数组,就当做对象来处理,先判断当前key在源对象是否存在,如果存在,说明当前key已经是响应式的,就直接进行操作对应的动作。如果key不在源对象中,就调用
Object.defineProperty()方法将该key添加到源对象上,并且实现了响应式。
Vue 框架原理(MVVM)
Vue 是一个 MVVM 渐进式框架,MVVM 是 vue 的设计模式,在 vue 框架中数据会自动驱动视图。
MVVM 框架的核心是数据绑定,Vue 通过 vue.js 中的指令实现数据双向绑定。指令中的表达式与模型中的数据绑定,一旦模型数据发生变化,指令表达式自动更新,从而实现了视图自动更新。当用户修改视图中的元素时,也会自动将修改的数据更新到模型中。
M:Model(模型),代表应用程序中业务逻辑和数据保存、检索的部分,通常与后端数据交互。在 vue 中指 data 中的数据。
V:View(视图),是应用程序的用户界面,通常由 HTML、CSS、JavaScript 组成,负责在屏幕上展示数据。在 vue 中指 template 中的 html 代码片段。
VM:ViewModel(视图模型),连接视图和模型之间的桥梁。Vue 采用了双向数据绑定技术(data binding),使用 ViewModel 构建并管理 View,也就是在 ViewModel 中定义 View 属性和行为,从而将 View 的状态和行为抽象成 ViewModel,使得 View 可以通过 ViewModel 进行绑定和操作,对于 View 中的数据变化可以通知 ViewModel,反之亦然。这样 View 和 Model 就可以互相独立,开发者只需要处理和调整 ViewModel 即可。
MVVM 与 MVC
- MVC:Model -> controller -> View
- MVVM:Model <=> ViewModel <=> View
- Model 层代表数据模型,也可以在 Model 中定义数据修改和操作的业务逻辑
- View 代表 UI 组件,它负责将数据模型转化成 UI 展现出来
- ViewModel 是一个同步 View 和 Model 的对象
- 区别
- 主要就是 MVC 中 Controller 演变成 MVVM 中的 ViewModel
- MVC 中大量的 DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验,MVVM解决了这个问题
Vue 的优点是什么?
- 低耦合。 视图(View)可以独立于 Model 变化和修改,一个 ViewModel 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。
- 可重用性。 可以把一些视图逻辑放在一个 ViewModel 里面,让很多 View 重用这段视图逻辑。
- 独立开发。 开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
- 可测试。 界面素来是比较难于测试的,而现在测试可以针对 ViewModel 来写。
Vue 双向数据绑定原理
Vue 的双向数据绑定是通过数据劫持和发布/订阅模式相结合来实现的,可以实现数据的同步更新,同时避免了手动更新视图的操作。
- 数据变化更新视图
- 视图变化更新数据
我们已经知道实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。(因为订阅者Watcher是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的)
接着,我们还需要有一个指令解析器Compile,替换模板数据或者绑定相应的函数(对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher),此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
实现数据的双向绑定:
- 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
- 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
- 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
- 实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
Vue 事件绑定原理
-
DOM事件绑定
当在模板中绑定 DOM 事件时,Vue 在内部使用 addEventListener 将事件处理函数注册到真实的DOM元素上。例如:
v-on:click="handleClick"绑定click事件。 Vue 通过浏览器提供的DOM API向元素添加 EventListener 监听事件,当事件触发时,执行绑定的回调函数。 -
自定义事件
Vue 中可以使用 on 方法监听自定义事件,emit 方法触发自定义事件。 Vue 内部通过Vue实例维护一个事件中心,通过事件中心来实现自定义事件的监听和触发。 一个组件在创建时会同时创建一个 vm 实例,vm 实例中会包含 on方法和emit 方法,在组件中你就可以通过 this 来调用这两个方法。在父组件中,通过 emit触发自定义事件,同时子组件通过on 来监听事件。
Vue 模板编译的原理
Vue.js 是基于模板的渲染机制来实现页面的渲染的。Vue.js 通过将模板编译成渲染函数的方式来进行模板的渲染。模板编译的过程主要包括以下几个步骤:
- 解析模板字符串:Vue.js 将模板字符串解析成 AST抽象语法树(Abstract Syntax Tree)。
- 静态优化:在编译过程中,Vue.js 会静态地分析整个模板,检测不需要更改的部分,将其优化成静态内容,这些内容不需要在每次重新渲染时都重新生成。
- 代码生成:将 AST 转换成渲染函数。这个过程包括将每个 AST 节点转换成代码字符串并拼接成渲染函数。
- 渲染函数:将生成的渲染函数执行后,会得到一个 VNode(Virtual DOM 节点)。
这些步骤将模板编译成渲染函数,用于生成 Virtual DOM,并更新到页面中,从而实现页面的渲染。在每次有数据变化时,Vue.js 会重新调用渲染函数,生成新的 VNode,通过对比新旧 VNode 差异,从而仅仅更新需要更新的部分,提高页面的渲染效率。
描述组件渲染的过程
Vue组件渲染的过程是将组件的模板渲染成真实的DOM节点并插入页面中的过程。它大致分为以下几个步骤:
- 创建Vue实例:在使用Vue框架时,需要创建Vue实例。Vue实例是Vue应用的入口,它将负责管理各个组件之间的数据传递和状态管理。
- 模板解析:在Vue组件中,使用HTML模板描述组件的结构和样式。当Vue实例被创建时,它会解析组件的模板,并将解析后的模板存储在内存中。Vue使用HTML解析器将模板解析为AST(抽象语法树)。
- 模板编译:在Vue实例渲染组件时,Vue将AST编译为渲染函数,渲染函数是一个返回HTML字符串的JavaScript函数。
- 组件渲染:当Vue实例需要渲染组件时,它会使用渲染函数将组件渲染成HTML字符串。
- 真实DOM生成:将HTML字符串转换成真实的DOM节点。Vue使用虚拟DOM算法,将HTML字符串转化为真实的DOM节点,Vue会尽量复用之前生成的DOM节点,以提高性能。
- 真实DOM插入:将真实的DOM节点插入到页面中。Vue将生成的真实DOM节点插入到指定的挂载点,完成组件渲染的过程。
需要注意的是,Vue会对组件进行一定的优化,包括条件渲染、属性绑定、事件处理等,以提高渲染效率和用户体验。
Vue 中 scoped 的原理
在编译组件的时候,如果当前组件内style标签上有scoped属性,那么会在当前所有标签上添加一个data-v-hash属性,而当前样式表内的所有末尾选择器后面也会加上该属性.wrap[data-v-b45036b2],那么就使得当前组件内的样式只会作用于当前组件内的元素。
$nextTick的原理
$nextTick 是 vue 中的异步更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。Vue 在内部尝试对异步队列使用原生的Promise.then和MessageChannel,如果执行环境不支持,会采用setTimeout(fn, 0)代替。
使用场景:开发过程中,开发者需要在更新完数据之后,需要对新DOM做一些操作,其实我们当时无法对新DOM进行操作,因为这时候还没有重新渲染。
vue的降级策略(兼容): 优先使用 Promise,如果浏览器不支持,再尝试 MutationObserver。实在不行,只能用 setTimeout 创建 task 了。
-
Promise 从源码上看,判断浏览器是否支持Promise,如果浏览器支持Promise,那么就用Promise.then的方式来延迟函数调用,
Promise.then方法可以将函数延迟到当前函数调用栈最末端,也就是函数调用栈最后调用该函数。从而做到延迟。 -
MutationObserver MutationObserver是h5新加的一个功能,其功能是监听dom节点的变动,在所有dom变动完成后,执行回调函数。具体监听:
- childList:子元素的变动
- attributes:属性的变动
- characterData:节点内容或节点文本的变动
- subtree:所有下属节点(包括子节点和子节点的子节点)的变动
以上代码是创建了一个文本节点,来改变文本节点的内容来触发的变动,因为我们在数据模型更新后,将会引起dom节点重新渲染,所以,我们加了这样一个变动监听,用一个文本节点的变动触发监听,等所有dom渲染完后,执行函数,达到我们延迟的效果。
-
setTimeout 利用setTimeout的延迟原理,
setTimeout(func, 0)会将func函数延迟到下一次函数调用栈的开始,也就是当前函数执行完毕后再执行该函数,因此完成了延迟功能。
Vue.mixin 的原理
使用原理:Vue.mixin的实现原理是基于Vue对组件选项的合并策略,在创建和合并组件选项时,会递归地将对象的所有属性合并到目标对象上,如果有相同名字的属性,则会进行策略性合并。当合并的对象包含生命周期事件、methods、data等选项时,这些选项会以先来后到的顺序合并到目标组件中。
在使用Vue.mixin时,我们可以通过定义一个Object,将通用的逻辑封装到这个对象中,然后通过Vue.mixin()方法全局注册这个对象,从而让所有组件可通过“继承”这个Object的方式,达到共享的目的。
需要注意的是,使用Vue.mixin并不能完全解决代码复用的问题,过度使用Vue.mixin可能会导致代码难以维护,潜在影响可读性、可维护性等。因此,应慎重使用Vue.mixin,并确保对其的合理使用。
为什么要重写数组方法?如何重写?
由于Object.defineProperty方法无法监听到数组和对象内的新增变化,所以 Vue2 重写了常用的数组方法来使数组更新时能触发页面更新。重写了数组的 push、pop、shift、unshift、splice、sort、reverse 七种能改变数组自身的方法。如果使用这7种以外的数组方法更新数组,且需要视图也更新,则需要使用this.$set。
- Vue 在实例化 Observer 时,针对数组类型进行了相应处理。其中通过 hasProto 是判断对象中是否存在
__proto__这个指向原型对象的属性,是则调用 protoAugment 方法,否则调用 copyAugment,对于支持 Vue 的浏览器一般都就走到 protoAugment 了,这个方法直接把 Vue 处理过的方法 arrayMethods 覆盖原型中的方法。如果浏览器不支持__proto__,则直接将 arrayMethods 上重写的方法直接定义到当前数据对象上。 - arrayMethods 先继承了 Array,然后循环重写一系列数组方法,这些方法调用原始方法拿到结果并返回,还会通过执行
ob.dep.notify()将当前数组的变更通知给其订阅者,这样当使用重写后方法改变数组后,数组订阅者会将这边变化更新到页面中。 - 其中 push、unshift、splice 会让数组索引发生改变,所以需要手动触发 observer,会定义一个 inserted 来控制是否触发响应式更新,如果为 true,则用
ob.observeArray(inserted)来手动给新插入的值设置响应式监听。
Vue 中 key 的作用?
key 主要是为了更高效的更新虚拟 DOM,它会告诉 Diff 算法,在更改前后它们是同一个 DOM 节点,这样在 Diff 新旧 vNodes 时更高效。Vue 判断两个节点是否相同时,主要是判断两者的key 和元素类型 tag。如果不设置key ,它的值就是 undefined,则可能永远认为这是两个相同的节点。
当 Vue 正在更新使用v-for渲染的元素列表时,如果不使用 key,它默认使用就地复用的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
不建议使用 index 作为 key 值,因为在数组中 key 的值会跟随数组发生改变(比如在数组中添加或删除元素、排序),而 key 值改变,Diff 算法就无法得知在更改前后它们是同一个 DOM 节点。会出现渲染问题。例如:v-for渲染三个输入框,用 index 作为 key 值,删除第二项,发现在视图上显示被删除的实际上是第三项,因为原本的 key 是 1、2、3,删除后 key 为 1、2,所以 3 被认为删除了。
v-show 和 v-if 的区别?
- 控制手段不同:
v-show是通过给元素添加 css 属性display: none,但元素仍然存在;而v-if控制元素显示或隐藏是将元素整个添加或删除。 - 编译过程不同:
v-if切换有一个局部编译/卸载的过程,切换过程中合适的销毁和重建内部的事件监听和子组件;v-show只是简单的基于 css 切换。 - 编译条件不同:
v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建,渲染条件为假时,并不做操作,直到为真才渲染。 - 触发生命周期不同:
v-show由 false 变为 true 的时候不会触发组件的生命周期;v-if由 false 变为 true 的时候,触发组件的 beforeCreate、created、beforeMount、mounted 钩子,由 true 变为 false 的时候触发组件的 beforeDestroy、destroyed 钩子。 - 性能消耗不同:
v-if有更高的切换消耗;v-show有更高的初始渲染消耗。
使用场景:
- 如果需要非常频繁地切换,则使用
v-show较好,如:手风琴菜单,tab 页签等 - 如果在运行时条件很少改变,则使用
v-if较好,如:用户登录之后,根据权限不同来显示不同的内容
v-if 和 v-for 为什么不建议放在一起使用?
Vue2 中,v-for的优先级比v-if高,这意味着v-if将分别重复运行于每一个v-for循环中。如果要遍历的数组很大,而真正要展示的数据很少时,将造成很大的性能浪费。
Vue3 中,则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,会导致异常。
插槽
slot 插槽,一般在组件内部使用,封装组件时,在组件内部不确定该位置是以何种形式的元素展示时,可以通过 slot 占据这个位置,该位置的元素需要父组件以内容形式传递过来。
- 默认插槽:
子组件用
<slot>标签来确定渲染的位置,标签里面可以放DOM结构作为后备内容,当父组件在使用的时候,可以直接在子组件的标签内写入内容,该部分内容将插入子组件的<slot>标签位置。如果父组件使用的时候没有往插槽传入内容,后备内容就会显示在页面。 - 具名插槽:
子组件用 name 属性来表示插槽的名字,没有指定 name 的插槽,会有隐含的名称叫做 default。父组件中在使用时在默认插槽的基础上通过
v-slot指令指定元素需要放在哪个插槽中,v-slot值为子组件插槽 name 属性值。使用v-slot指令指定元素放在哪个插槽中,必须配合<template>元素,且一个<template>元素只能对应一个预留的插槽,即不能多个<template>元素都使用v-slot指令指定相同的插槽。v-slot的简写是#,例如v-slot:header可以简写为#header。 - 作用域插槽:
子组件在<
slot>标签上绑定 props 数据,以将子组件数据传给父组件使用。父组件获取插槽绑定 props 数据的方法:<template scope="接收的变量名"><template slot-scope="接收的变量名"><template v-slot:插槽名="接收的变量名">
Vue 中的修饰符有哪些?
- 表单修饰符
lazy填完信息,光标离开标签的时候,才会将值赋予给 value,也就是在 change 事件之后再进行信息同步。number自动将用户输入值转化为数值类型,但如果这个值无法被 parseFloat 解析,则会返回原来的值。trim自动过滤用户输入的首尾空格,而中间的空格不会被过滤。
- 事件修饰符
stop阻止了事件冒泡,相当于调用了event.stopPropagation方法。prevent阻止了事件的默认行为,相当于调用了event.preventDefault方法。self只当在event.target是当前元素自身时触发处理函数。once绑定了事件以后只能触发一次,第二次就不会触发。capture使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理。passive告诉浏览器你不想阻止事件的默认行为。native让组件变成像 html 内置标签那样监听根元素的原生事件,否则组件上使用v-on只会监听自定义事件。
- 鼠标按键修饰符
left左键点击。right右键点击。middle中键点击。
- 键值修饰符
- 键盘修饰符是用来修饰键盘事件(onkeyup,onkeydown)的。
了解哪些 Vue 的性能优化方法?
- 路由懒加载:有效拆分应用大小,访问时才异步加载。
keep-alive缓存页面:避免重复创建组件实例,且能保留缓存组件状态。v-for遍历避免同时使用v-if,实际上在 Vue 3 中已经是一个错误用法了。- 长列表性能优化,可采用虚拟列表。
v-once:不再变化的数据使用v-once。- 事件销毁:组件销毁后把全局变量和定时器销毁。
- 图片懒加载。
- 第三方插件按需引入。
- 子组件分割:较重的状态组件适合拆分。
- 服务端渲染。
虚拟 DOM
虚拟 DOM,顾名思义就是虚拟的 DOM 对象,它本身就是一个 JS 对象。虚拟 DOM 的好处:
- 性能提升: 直接操作 DOM 是有限制的,一个真实元素上有很多属性,如果直接对其进行操作,同时会对很多额外的属性内容进行了操作,这是没有必要的。如果将这些操作转移到 JS 对象上,就会简单很多。另外,操作 DOM 的代价是比较大的,频繁的操作 DOM 容易引起页面的重绘和回流。如果通过抽象 VNode 进行中间处理,可以有效减少直接操作 DOM 次数,从而减少页面的重绘和回流。
- 方便跨平台实现: 同一 VNode 可以渲染成不同平台上对应的内容,比如渲染在浏览器是 DOM 元素节点,渲染在Native(iOS、Android)变为对应的控件。Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。
Diff 算法原理
步骤:
- patch方法:对比当前同层的虚拟节点是否为同一种类型的标签
- 是:继续执行patchVnode方法进行更深比对
- 否:没必要比对了,直接整个节点替换成新虚拟节点
- patchVnode方法:
- 找到对应的真实DOM,称为el
- 判断newVnode和oldVnode是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将el的文本节点设置为newVnode的文本节点。
- 如果oldVnode有子节点而newVnode没有,则删除el的子节点
- 如果oldVnode没有子节点而newVnode有,则将newVnode的子节点真实化之后添加到el
- 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要
- updateChildren方法: 新旧虚拟节点的子节点对比
首尾指针法,新的子节点集合和旧的子节点集合,然后会进行互相进行比较,总共有五种比较情况:- oldS 和 newS 使用 sameVnode 方法进行比较,
sameVnode(oldS, newS) - oldS 和 newE 使用 sameVnode 方法进行比较,
sameVnode(oldS, newE) - oldE 和 newS 使用 sameVnode 方法进行比较,
sameVnode(oldE, newS) - oldE 和 newE 使用 sameVnode 方法进行比较,
sameVnode(oldE, newE) - 如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射到旧节点下标的 key -> index 表,然后用新 vnode 的 key 去找出在旧节点中可以复用的位置
- oldS 和 newS 使用 sameVnode 方法进行比较,
Vue 指令
在 Vue 中,指令是一种特殊的特性,用于在模板中对 DOM 元素进行操作和响应式数据绑定。Vue 指令以v-开头,以下是一些常见的 Vue 指令:
v-bind:动态地绑定一个或多个 HTML 属性,或者一个组件的 prop。v-on:用于监听 DOM 事件,可以简写成@。v-if/v-else-if/v-else:根据条件来渲染或移除 DOM 元素。- v-for:基于一个数组来循环渲染 DOM 元素。
- v-model:在表单元素上创建双向数据绑定。
Vue 自定义指令
Vue 自定义指令有全局注册和局部注册两种方式。先来看看注册全局指令的方式,通过Vue.directive( id, [definition] )方式注册全局指令。然后在入口文件中进行Vue.use()调用。指令定义函数提供了几个钩子函数(可选):
- bind:只调用一次,指令第一次绑定到元素时调用,可以定义一个在绑定时执行一次的初始化动作。
- inserted:被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
- update:被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值。
- componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
- unbind:只调用一次,指令与元素解绑时调用。
例子:
-
v-permission:自定义权限指令,对需要权限判断的 DOM 进行显示隐藏(按钮权限)。 思路:根据权限数组判断用户的权限是否在这个数组内,如果是则显示,否则则移除 DOM。function checkArray(key) { // 项目内该权限数组一般取后端接口返回的权限数组 let arr = ['1', '2', '3', '4'] let index = arr.indexOf(key) if (index > -1) { return true // 有权限 } else { return false // 无权限 } } const permission = { inserted: function (el, binding) { let permission = binding.value // 获取到 v-permission的值 if (permission) { let hasPermission = checkArray(permission) if (!hasPermission) { // 没有权限 移除Dom元素 el.parentNode && el.parentNode.removeChild(el) } } }, } export default permission -
v-drag:拖拽指令,可在页面可视区域任意拖拽元素。 思路:- 设置需要拖拽的元素为相对定位,其父元素为绝对定位。
- 鼠标按下
onmousedown时记录目标元素当前的 left 和 top 值。 - 鼠标移动
onmousemove时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值 - 鼠标松开
onmouseup时完成一次拖拽
const drag = { inserted: function (el) { el.style.cursor = 'move' el.style.position = 'absolute' el.onmousedown = function (e) { let disx = e.pageX - el.offsetLeft let disy = e.pageY - el.offsetTop document.onmousemove = function (e) { let x = e.pageX - disx let y = e.pageY - disy let maxX = document.body.clientWidth - parseInt(window.getComputedStyle(el).width) let maxY = document.body.clientHeight - parseInt(window.getComputedStyle(el).height) if (x < 0) { x = 0 } else if (x > maxX) { x = maxX } if (y < 0) { y = 0 } else if (y > maxY) { y = maxY } el.style.left = x + 'px' el.style.top = y + 'px' } document.onmouseup = function () { document.onmousemove = document.onmouseup = null } } } } export default drag
Vue 指令的生命周期钩子函数有哪些?
- bind:只调用一次,指令第一次绑定到元素时调用。在这个阶段,可以进行一次性的初始化设置。例如,可以在这个钩子中添加事件监听器或者初始化一些与指令相关的数据。
- inserted:被绑定元素插入父节点时调用。此时,元素已经被插入到 DOM 中,可以进行一些针对元素在 DOM 中存在后的操作,比如获取其他相关元素的引用等。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。这个钩子函数可以用来响应数据的变化,对指令所影响的元素进行相应的更新操作。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。可以在这个阶段确保所有的更新都已经完成,进行一些最终的调整或者检查。
- unbind:只调用一次,指令与元素解绑时调用。可以在这个阶段进行一些清理工作,比如移除事件监听器、释放资源等。
vue.config.js的 proxy 配置为什么可以解决跨域问题?
- 开发服务器代理: 当你在本地开发时,Vue CLI 提供了一个开发服务器(基于 webpack-dev-server)。这个服务器不仅用于提供前端资源,还可以被配置为一个代理服务器。
- 请求转发: 在 vue.config.js 中配置代理,实际上是告诉开发服务器,当它接收到特定模式的请求时,应该将请求转发到指定的后端服务器。这样,前端应用发出的请求,实际上是先发送到本地开发服务器,然后由开发服务器转发到实际的后端服务。
- 绕过同源策略: 由于请求首先发送到本地开发服务器,然后由开发服务器转发,这使得请求看起来像是从开发服务器发出的,从而绕过了浏览器的同源策略限制。
- 无需修改后端: 使用代理的方式,不需要对后端代码或服务器配置进行任何修改,就可以实现跨域请求。
- 开发环境与生产环境分离: 代理配置只在开发环境中有效,不会影响到生产环境。在生产环境中,通常会通过其他方式(如CORS设置、反向代理等)来处理跨域问题。
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://backend.server.com',
changeOrigin: true,
pathRewrite: {
{'^/api': ''}
}
}
}
}
}
在这个配置中,所有发送到/api的请求都会被本地开发服务器捕获,并转发到http://backend.server.com。changeOrigin 设置为 true 可以避免 host header 的问题,pathRewrite 用于重写请求路径,去掉/api前缀。
单页面应用
- 特点
- 良好的用户体验 快速响应:在单页面应用中,一旦页面加载完成,后续的操作无需整页刷新,而是通过局部更新数据来实现页面内容的变化。这使得应用的响应速度非常快,给用户带来流畅的操作感受。 无缝过渡:页面之间的切换更加平滑,没有传统多页面应用在切换页面时的闪烁和加载等待,增强了用户体验的连贯性。
- 前端路由控制 单页面应用通过前端路由机制来管理页面的显示。路由根据不同的 URL 路径来决定显示哪个组件或页面片段,而不是像传统多页面应用那样由服务器根据 URL 来返回不同的完整页面。 这使得用户在操作应用时,感觉就像在使用一个本地应用程序一样,可以通过浏览器的前进、后退按钮进行页面的切换,并且状态可以被正确地保存和恢复。
- 数据交互主要通过 Ajax Ajax(Asynchronous JavaScript and XML)技术允许在不刷新整个页面的情况下与服务器进行数据交互。在单页面应用中,大量的数据获取和更新操作都是通过 Ajax 实现的。 例如,当用户在单页面应用中进行表单提交时,应用可以使用 Ajax 将数据发送到服务器进行处理,然后在不刷新页面的情况下更新页面上的相关部分,显示处理结果。
- 优势
- 开发效率高
- 前后端分离:单页面应用通常采用前后端分离的开发模式,前端开发人员和后端开发人员可以并行开发,互不干扰。
- 组件化开发:前端部分可以采用组件化的开发方式,将页面拆分成多个独立的、可复用的组件,提高了开发效率和代码的可维护性。
- 性能优化潜力大
- 减少服务器请求:由于只有一个页面需要加载,减少了对服务器的请求次数,降低了服务器的负载。
- 缓存利用:可以更好地利用浏览器缓存,将常用的资源(如 JavaScript 文件、CSS 文件、图片等)缓存到本地,提高页面的加载速度。
- 适用于移动应用开发
- 单页面应用的响应式设计和快速加载特性使其非常适合移动设备上的使用。可以通过使用响应式布局和移动优先的设计原则,确保应用在不同尺寸的移动设备上都能提供良好的用户体验。
- 开发效率高
- 挑战
- 首次加载时间可能较长
- 虽然单页面应用在后续的操作中可以快速响应,但由于需要加载大量的 JavaScript 和 CSS 资源,首次加载页面时可能会比较慢。
- 为了解决这个问题,可以采用代码分割、懒加载等技术,只在需要的时候加载特定的资源,减少首次加载的时间。
- 搜索引擎优化(SEO)难度较大
- 由于单页面应用的内容是通过 JavaScript 动态生成的,搜索引擎爬虫在抓取页面内容时可能会遇到困难。这使得单页面应用在搜索引擎优化方面相对传统多页面应用来说难度更大。
- 为了解决这个问题,可以采用服务器端渲染(SSR)技术,将单页面应用在服务器端渲染成完整的 HTML 页面,以便搜索引擎爬虫能够正确地抓取页面内容。
- 状态管理复杂
- 在单页面应用中,由于页面的状态是由前端代码管理的,随着应用的复杂性增加,状态管理可能会变得非常复杂。
- 为了解决这个问题,可以采用专门的状态管理库,如 Redux、Vuex 等,来帮助管理应用的状态,确保状态的一致性和可预测性。
- 首次加载时间可能较长
单页面应用是怎么实现的?
单页面应用(SPA)主要通过以下几种方式实现:
- 前端框架
- 选择合适的前端框架:如 Vue.js、React.js 或 Angular 等。这些框架提供了构建单页面应用的基础架构和工具。
- Vue.js:通过组件化的开发方式,实现页面的局部更新。利用 Vue 的路由功能(vue-router)进行页面之间的切换,只更新变化的部分,而不是整个页面的重新加载。同时,Vue 的状态管理工具(vuex)可以有效地管理应用的状态,确保数据在不同组件之间的一致性。
- React.js:同样采用组件化开发,使用 React Router 进行路由管理。React 通过虚拟 DOM 技术,高效地更新页面,只对发生变化的部分进行实际的 DOM 操作,提高了性能。Redux 等状态管理库可以帮助管理复杂的应用状态。
- Angular:提供了强大的模块化和依赖注入系统。Angular 的路由模块(Angular Router)实现页面导航,其内置的服务和指令可以方便地构建单页面应用。
- 组件化开发:
- 将页面拆分为多个独立的组件,每个组件负责特定的功能和界面部分。组件可以复用,提高开发效率和代码的可维护性。
- 例如,一个电商网站的商品列表可以是一个组件,购物车可以是另一个组件。当用户在不同页面之间切换时,这些组件可以根据需要动态加载和卸载。
- 选择合适的前端框架:如 Vue.js、React.js 或 Angular 等。这些框架提供了构建单页面应用的基础架构和工具。
- 路由管理
- 定义路由:
- 在前端框架中配置路由规则,将不同的 URL 路径映射到相应的组件或页面。
- 比如,/home 路径对应首页组件,/product/:id 路径对应商品详情页面组件,其中 :id 是动态参数,表示商品的唯一标识符。
- 路由切换:
- 当用户点击链接或在浏览器地址栏中输入 URL 时,前端框架根据路由规则加载相应的组件,并更新页面内容,实现页面的切换。
- 可以使用浏览器的 History API 来管理浏览器的历史记录,使得用户可以通过前进、后退按钮在不同页面之间导航。
- 定义路由:
- 数据管理
- Ajax 请求:
- 通过 Ajax 技术与后端服务器进行数据交互,获取所需的数据并在前端进行展示和处理。
- 例如,当用户进入商品详情页面时,前端发送一个 Ajax 请求到后端服务器,获取商品的详细信息,并将其展示在页面上。
- 状态管理:
- 使用状态管理工具(如 Vuex、Redux 等)来集中管理应用的状态,包括数据、用户操作等。
- 状态管理工具可以确保数据在不同组件之间的一致性,并且方便进行数据的共享和更新。例如,当用户在购物车中添加商品时,状态管理工具可以通知其他相关组件更新购物车的数量和总价。
- Ajax 请求:
- 页面加载和优化
- 懒加载:
- 对于较大的资源或不立即需要的组件,可以采用懒加载的方式,延迟加载它们,直到真正需要的时候。
- 比如,图片、视频等资源可以在用户滚动到可视区域时再进行加载,提高初始页面加载速度。
- 代码分割:
- 将应用的代码分割成多个小块,根据需要动态加载。这样可以避免一次性加载整个应用的代码,减少初始加载时间。
- 前端框架通常提供了代码分割的功能,可以根据路由、组件等进行代码分割。
- 缓存:
- 利用浏览器的缓存机制,缓存静态资源和部分动态数据,以提高页面的加载速度。
- 可以设置 HTTP 缓存头,让浏览器缓存资源一段时间,下次访问时直接从缓存中获取,而不需要再次从服务器请求。
- 懒加载:
如何实现单页面应用的路由?
- 使用 HTML5 History API
- 基本原理
- HTML5 引入的 History API 允许开发者在不刷新页面的情况下修改浏览器地址栏中的 URL。通过监听浏览器的 popstate 事件,可以响应 URL 的变化并更新页面内容。
- 例如,当用户点击页面上的一个链接时,可以使用 History API 的 pushState () 方法将新的 URL 状态添加到浏览器历史记录中,同时在前端代码中根据新的 URL 加载相应的页面内容。
- 实现步骤
- 首先,设置路由监听。在应用初始化时,使用 window.addEventListener ('popstate', function () {...}) 来监听浏览器的 popstate 事件。当 URL 发生变化时,这个事件会被触发。
- 然后,定义路由规则。可以使用一个对象或数组来存储不同的路由规则,每个规则包含一个路径和对应的处理函数。例如:
const routes = { '/home': function() { // 处理 /home 路径的逻辑 }, '/about': function() { // 处理 /about 路径的逻辑 } }; - 最后,根据 URL 变化更新页面。在 popstate 事件处理函数中,获取当前的 URL,然后根据路由规则找到对应的处理函数并执行,更新页面内容。例如:
window.addEventListener('popstate', function() { const currentPath = window.location.pathname; if (routes[currentPath]) { routes[currentPath](); } });
- 基本原理
- 使用前端框架提供的路由功能
- Vue 中的路由实现:VueRouter 是 Vue.js 官方的路由管理器。它通过定义路由配置对象来实现路由功能。首先引入 VueRouter 并在 Vue 实例中使用它。然后定义路由配置对象,其中包含不同的路径和对应的组件。最后创建一个 VueRouter 实例并将其传递给 Vue 实例。例如:
import Vue from 'vue'; import VueRouter from 'vue-router'; Vue.use(VueRouter); const routes = [ { path: '/home', component: HomeComponent }, { path: '/about', component: AboutComponent } ]; const router = new VueRouter({ routes }); new Vue({ router, render: h => h(App) }).$mount('#app'); - React 中的路由实现:React Router 是用于 React 的路由库。它提供了多种方式来实现路由,如 BrowserRouter、HashRouter 等。使用 BrowserRouter 作为路由容器,通过 Route 组件定义不同的路由路径和对应的组件。Switch 组件确保只匹配一个路由。例如:
import React from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; const Home = () => <div>Home Page</div>; const About = () => <div>About Page</div>; const App = () => ( <Router> <Switch> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> </Switch> </Router> ); export default App;
- Vue 中的路由实现:VueRouter 是 Vue.js 官方的路由管理器。它通过定义路由配置对象来实现路由功能。首先引入 VueRouter 并在 Vue 实例中使用它。然后定义路由配置对象,其中包含不同的路径和对应的组件。最后创建一个 VueRouter 实例并将其传递给 Vue 实例。例如:
- 使用第三方库
- page.js:一个轻量级的前端路由库,它提供了简单易用的 API 来实现单页面应用的路由功能。使用 page.js 的
page()方法定义不同的路由路径和对应的处理函数,然后调用page.start()启动路由。例如:// 定义路由 page('/home', function() { // 处理 /home 路径的逻辑 }); page('/about', function() { // 处理 /about 路径的逻辑 }); // 启动路由 page.start();
- page.js:一个轻量级的前端路由库,它提供了简单易用的 API 来实现单页面应用的路由功能。使用 page.js 的
源码相关
v-if的源码实现是什么?
- 编译阶段
- 模板解析:
- Vue 在编译阶段会将模板字符串解析成抽象语法树(AST)。当遇到
v-if指令时,会在相应的元素节点的 AST 对象上添加一个if属性,表示该元素是受条件控制的。 - 例如,对于模板
<div v-if="show">Hello</div>,解析后的 AST 中对应的 div 元素会有一个 if 属性,值为 show 表达式。
- Vue 在编译阶段会将模板字符串解析成抽象语法树(AST)。当遇到
- 代码生成:
- 根据 AST 生成渲染函数。对于带有
v-if的元素,渲染函数会根据条件表达式的值来决定是否调用创建元素的函数。 - 例如,生成的渲染函数可能类似于:
function render() { if (show) { return createElement('div', 'Hello') } else { return null } }
- 根据 AST 生成渲染函数。对于带有
- 模板解析:
- 运行时阶段
- 数据响应式:
- Vue 通过数据劫持和发布-订阅模式实现了数据的响应式。当
v-if指令中使用的表达式所依赖的数据发生变化时,会触发相应的依赖收集和更新操作。 - 例如,如果 show 变量发生变化,Vue 会重新执行渲染函数,根据新的值来决定是否渲染该元素。
- Vue 通过数据劫持和发布-订阅模式实现了数据的响应式。当
- 虚拟 DOM 和真实 DOM 的操作:
- Vue 使用虚拟 DOM 来提高渲染性能。当条件变化时,Vue 会根据新的渲染函数生成新的虚拟 DOM 树,并与旧的虚拟 DOM 树进行对比,找出差异部分。
- 如果
v-if的条件从真变为假,Vue 会在真实 DOM 中移除相应的元素;如果条件从假变为真,Vue 会在真实 DOM 中创建并插入相应的元素。
- 数据响应式:
v-for的源码实现是什么?
- 编译阶段
- 模板解析:
- 在编译阶段,当遇到
v-for指令时,Vue 会解析该指令的表达式,并在相应的元素节点的抽象语法树(AST)对象上添加for属性和相关信息。 - 例如,对于模板
<li v-for="item in items">{{ item }}</li>,解析后的 AST 中对应的 li 元素会有一个 for 属性,值为item in items表达式,同时还会记录 item 和 items 的信息。
- 在编译阶段,当遇到
- 代码生成:
- 根据 AST 生成渲染函数。对于带有
v-for的元素,渲染函数会根据循环的数据生成多个虚拟节点。 - 例如,生成的渲染函数可能类似于:
function render() { const items = this.items const result = [] for (let i = 0; i < items.length; i++) { const item = items[i] result.push(createElement('li', item)) } return result }
- 根据 AST 生成渲染函数。对于带有
- 模板解析:
- 运行时阶段
- 数据响应式:
- Vue 通过数据劫持和发布-订阅模式实现了数据的响应式。当
v-for指令中使用的数组数据发生变化时,会触发相应的依赖收集和更新操作。 - 如果数组的长度发生变化或者其中的元素被添加、删除或修改,Vue 会重新执行渲染函数,更新受影响的虚拟 DOM 节点。
- Vue 通过数据劫持和发布-订阅模式实现了数据的响应式。当
- 虚拟 DOM 和真实 DOM 的操作:
- Vue 使用虚拟 DOM 来提高渲染性能。当数据变化导致虚拟 DOM 树发生改变时,Vue 会进行虚拟 DOM 的对比算法,找出差异部分。
- 对于
v-for渲染的列表,如果有元素被添加、删除或修改,Vue 会在真实 DOM 中进行相应的操作,如插入、移除或更新元素。
- 特殊处理:
- Vue 对
v-for循环中的元素提供了一些特殊的处理,例如可以使用 key 属性来帮助 Vue 识别每个元素的唯一性,提高更新的效率。
- Vue 对
- 数据响应式:
Vuex
Vuex 怎么用的?
- 背下文档第一句:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
- State
store.state.xxxthis.$store.state.xxx- 状态,存储基本数据。
- Getters
store.getters.yyythis.$store.getters.yyy- 可以认为是 store 的计算属性。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
- Mutations
store.commit('zzz')this.$store.commit('zzz')- 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件。
- 每个 mutation 都有一个字符串的事件类型 (type,方法名) 和一个回调函数 (handler)。
- 可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload)。
- Mutation 必须是同步函数。
- Actions
store.dispatch('zzz')this.$store.dispatch('zzz')- Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
- Action 类似于 mutation,不同在于:
- Modules
const moduleA = { state: () => ({ ... }), mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: () => ({ ... }), mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB } }) store.state.a // -> moduleA 的状态 store.state.b // -> moduleB 的状态- 为了解决 store 对象可能变得相当臃肿的问题,Vuex 允许我们将 store 分割成模块(module)。
- 每个模块拥有自己的 state、mutation、action、getter
Vuex 的原理
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化。
核心流程中的主要功能:
- Vue Components 是 vue 组件,组件会触发(dispatch)一些事件或动作,也就是图中的 Actions;
- 在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中;
- 然后 Mutations 就去改变(Mutate)State 中的数据;
- 当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。
各模块在核心流程中的主要功能:
- Vue Components∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
- dispatch∶操作行为触发方法,是唯一能执行action的方法。
- actions∶ 操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
- commit∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
- mutations∶状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
- state∶ 页面状态管理容器对象。集中存储 Vue components 中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
- getters∶ state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。
Vuex 单向数据流的原理
单向数据流指只能从一个方向来修改状态。在单向数据流中,当修改状态的时候,会完全重新开始走一个修改的流程。这限制了状态修改的方式,使状态变得可预测,容易调试。
Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 提交修改信息, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。所有异步操作或批量的同步操作需要走 Action ,但 Action 也是无法直接修改 State 的,还是需要通过 Mutation 来修改 State 的数据。最后,根据 State 的变化,渲染到视图上。
Vuex 怎么实现数据响应式的?
在 Vuex 中,数据的响应式是通过 Vue 的响应式系统来实现的。
- Vue 的响应式核心原理
- Vue 通过数据劫持结合 发布者-订阅者模式 来实现数据的响应式。
- 当创建一个 Vue 实例时,Vue 会遍历数据对象的所有属性,并使用
Object.defineProperty()方法把这些属性转换为 getter/setter 的形式。这样,当访问属性时会触发 getter,修改属性时会触发 setter。 - 每个被侦测的属性都有一个对应的依赖收集器(Dep),在 getter 中会收集依赖(通常是组件的渲染函数等)。当属性值发生变化时,会通知所有收集到的依赖进行更新。
- Vuex 与 Vue 的响应式结合
- Vuex 的状态(state)存储在一个单一的对象中。当在 Vue 组件中使用 Vuex 的状态时,首先,通过 mapState 等辅助函数将 Vuex 的状态映射到组件的计算属性中。这样,组件在访问这些计算属性时,实际上是在访问 Vuex 的状态。例如:
import { mapState } from 'vuex'; export default { computed: { ...mapState(['count']), }, }; - 由于计算属性是基于 Vue 的响应式系统实现的,当 Vuex 的状态发生变化时,计算属性会自动触发更新,从而导致组件重新渲染。
- Vuex 的状态(state)存储在一个单一的对象中。当在 Vue 组件中使用 Vuex 的状态时,首先,通过 mapState 等辅助函数将 Vuex 的状态映射到组件的计算属性中。这样,组件在访问这些计算属性时,实际上是在访问 Vuex 的状态。例如:
- Vuex 的 mutations 和 actions
- Mutations:用于更改 Vuex 状态的唯一方法是提交 mutation。当提交 mutation 时,会直接修改状态对象中的属性值。由于状态对象的属性是响应式的,所以属性值的变化会触发 Vue 的响应式系统,通知依赖该状态的组件进行更新。
- Actions:可以包含异步操作,最终通过提交 mutation 来改变状态。同样,提交 mutation 会触发响应式更新。
Vuex 能否在 Mutation 中触发异步请求?
当我们在 mutations 中使用了异步函数,我们在 commit 时,很难追踪 state 的状态,因为在触发 commit 事件时,异步的回调函数不知道什么时候执行,所以难以追踪。mutations 是同步事务,假设在 mutations 有多个异步的调用,很难确定这些异步哪些先执行,很难追踪 state 的变化,所以也给调试带来了一定难度。
actions 里面可以做异步操作,但是并不是直接修改数据,提交的是 mutations 里面的方法。在 actions 里可以 dispatch 一个异步的操作,也就是等一个任务完成了后,再执行另一个commit。actions 执行完异步操作,将结果给 mutations,mutations 中再同步修改状态 state,这样 actions 的操作在 mutations 中有记录。
VueRouter
VueRouter 的原理
VueRouter 是 Vue.js 官方的路由管理器,它的实现原理主要包括以下几个方面:
- 路由映射
- 定义路由配置:在 VueRouter 中,通过定义路由配置对象来描述应用的不同路由。每个路由配置包含一个路径(path)和对应的组件(component)。例如:
const routes = [ { path: '/home', component: HomeComponent }, { path: '/about', component: AboutComponent } ]; - 路由映射表:VueRouter 内部维护一个路由映射表,将路径与对应的组件进行关联。当 URL 发生变化时,VueRouter 会根据当前的 URL 在路由映射表中查找匹配的路由,并加载相应的组件。
- 定义路由配置:在 VueRouter 中,通过定义路由配置对象来描述应用的不同路由。每个路由配置包含一个路径(path)和对应的组件(component)。例如:
- 监听 URL 变化
- HTML5 History API 或 Hash 模式
- VueRouter 支持两种模式来监听 URL 的变化:HTML5 History 模式和 Hash 模式。
- 在 HTML5 History 模式下,VueRouter 利用 HTML5 History API 来监听 URL 的变化。通过调用
history.pushState()和history.replaceState()方法,可以在不刷新页面的情况下改变浏览器的 URL。 - 在 Hash 模式下,URL 中的哈希值(#)部分被用来表示路由。当哈希值发生变化时,浏览器不会向服务器发送请求,而是由 VueRouter 监听到哈希值的变化,并根据变化加载相应的组件。
- 监听事件
- 无论是 HTML5 History 模式还是 Hash 模式,VueRouter 都会监听浏览器的特定事件来检测 URL 的变化。
- 在 HTML5 History 模式下,VueRouter 监听 popstate 事件,当用户点击浏览器的前进、后退按钮或通过 JavaScript 调用
history.back()、history.forward()、history.go()方法时,popstate 事件会被触发。 - 在 Hash 模式下,VueRouter 监听 hashchange 事件,当 URL 中的哈希值发生变化时,hashchange 事件会被触发。
- HTML5 History API 或 Hash 模式
- 组件渲染
- 路由匹配与组件加载
- 当 URL 发生变化时,VueRouter 会根据当前的 URL 在路由映射表中进行路由匹配。如果找到匹配的路由,VueRouter 会加载对应的组件。
- 在加载组件时,VueRouter 会使用 Vue 的异步组件加载机制或动态导入
import()来确保组件的按需加载,提高应用的性能。
- 嵌套路由与组件渲染
- VueRouter 支持嵌套路由,允许在一个组件中定义子路由,并在父组件的模板中使用
<router-view>标签来渲染子路由对应的组件。 - 当路由发生变化时,VueRouter 会根据嵌套路由的结构,依次渲染父组件和子组件,形成组件的嵌套层次结构。
- VueRouter 支持嵌套路由,允许在一个组件中定义子路由,并在父组件的模板中使用
- 路由匹配与组件加载
- 导航守卫
- 全局导航守卫
- VueRouter 提供了全局导航守卫,包括 beforeEach、afterEach 和 beforeResolve。这些导航守卫可以在路由切换之前、之后或解析完成时执行一些逻辑。
- 例如,可以在 beforeEach 导航守卫中进行用户认证、权限检查等操作,以控制用户的访问权限。
- 组件内导航守卫
- 除了全局导航守卫,VueRouter 还提供了组件内的导航守卫,包括 beforeRouteEnter、beforeRouteUpdate 和 beforeRouteLeave。这些导航守卫可以在组件内部进行更细粒度的路由控制。
- 例如,可以在 beforeRouteEnter 导航守卫中进行数据预取操作,确保组件在渲染之前获取到所需的数据。
- 全局导航守卫
- 路由切换动画
- 过渡效果:VueRouter 支持使用 Vue 的过渡系统来为路由切换添加动画效果。可以通过在路由配置中指定过渡名称或使用
<transition>标签来实现路由切换的过渡效果。例如:const routes = [ { path: '/home', component: HomeComponent, meta: { transition: 'fade' } }, { path: '/about', component: AboutComponent, meta: { transition: 'slide' } } ]; - 动画实现:在使用过渡效果时,VueRouter 会根据指定的过渡名称或
<transition>标签的属性,自动为路由切换添加相应的 CSS 类名,从而实现动画效果。例如,对于 fade 过渡效果,VueRouter 会在进入和离开的组件上分别添加.v-enter、.v-enter-active、.v-leave和.v-leave-active等 CSS 类名,通过定义这些类名的 CSS 样式,可以实现淡入淡出的动画效果。
- 过渡效果:VueRouter 支持使用 Vue 的过渡系统来为路由切换添加动画效果。可以通过在路由配置中指定过渡名称或使用
VueRouter 是如何实现页面跳转的?
在 VueRouter 中,页面跳转主要通过以下几种方式实现:
- 使用
<router-link>组件- 基本用法:
<router-link>是一个 Vue 组件,用于在模板中创建导航链接。它会被渲染为一个<a>标签,但在内部使用了 VueRouter 的路由机制来实现页面跳转,而不是传统的超链接行为。<router-link>组件的 to 属性指定了要跳转的目标路由路径。当用户点击这些链接时,VueRouter 会根据 to 属性的值进行页面跳转。例如:<router-link to="/home">Home</router-link> <router-link to="/about">About</router-link> - 传递参数
<router-link>还可以传递参数给目标路由。可以在 to 属性中使用动态路由参数或查询参数来传递数据。- 动态路由参数:
<router-link :to="{ path: '/user/:id', params: { id: 123 } }">User Profile</router-link> - 查询参数:
<router-link :to="{ path: '/search', query: { keyword: 'vue-router' } }">Search</router-link> - 在目标路由组件中,可以通过
$route.params或$route.query来获取传递的参数。
- 基本用法:
- 使用编程式导航
$router.push():在 Vue 组件的方法中,可以使用$router.push()方法来进行页面跳转。这个方法接受一个目标路由的配置对象作为参数,可以是字符串路径、对象形式的路径配置或命名路由的名称和参数。例如:methods: { goToHome() { this.$router.push('/home'); }, goToUserProfile(id) { this.$router.push({ path: '/user/' + id }); }, goToSearch(keyword) { this.$router.push({ path: '/search', query: { keyword: keyword } }); } }$router.replace()和$router.go()$router.replace()方法来替换当前的历史记录,而不是在历史记录中添加新的条目。$router.go()方法可以在历史记录中向前或向后导航一定的步数。例如:
methods: { replaceCurrentRoute() { this.$router.replace('/about'); }, goBack() { this.$router.go(-1); }, goForward() { this.$router.go(1); } }
- 在路由守卫中进行跳转
- 全局导航守卫:VueRouter 的全局导航守卫,如 beforeEach 和 afterEach,可以在路由切换之前或之后进行一些逻辑判断,并根据需要进行页面跳转。例如:
const router = new VueRouter({ routes }); router.beforeEach((to, from, next) => { if (to.path === '/login' && isAuthenticated()) { // 如果用户已登录,跳转到首页 next('/home'); } else { next(); } }); - 组件内导航守卫:组件内的导航守卫,如 beforeRouteEnter、beforeRouteUpdate 和 beforeRouteLeave,也可以在组件内部进行页面跳转的控制。例如:
beforeRouteEnter(to, from, next) { if (someCondition) { // 根据条件进行页面跳转 next('/another-route'); } else { next(); } }
- 全局导航守卫:VueRouter 的全局导航守卫,如 beforeEach 和 afterEach,可以在路由切换之前或之后进行一些逻辑判断,并根据需要进行页面跳转。例如:
VueRouter 实现路由懒加载的原理是什么?
在 VueRouter 中,路由懒加载的实现原理主要是利用了动态导入(dynamic import)的特性。以下是详细的解释:
- 动态导入
- 基本概念:动态导入是 ECMAScript 的一个新特性,它允许在运行时动态地加载模块。与传统的静态导入不同,动态导入返回一个 Promise,在模块加载完成后 resolve。例如:
const module = await import('./module.js'); - 与路由懒加载的结合:VueRouter 利用动态导入的特性来实现路由组件的懒加载。当定义路由时,不是直接导入路由组件,而是使用动态导入的方式来指定路由组件的加载逻辑。例如:
const routes = [ { path: '/home', component: () => import('./components/Home.vue') }, { path: '/about', component: () => import('./components/About.vue') } ];
- 基本概念:动态导入是 ECMAScript 的一个新特性,它允许在运行时动态地加载模块。与传统的静态导入不同,动态导入返回一个 Promise,在模块加载完成后 resolve。例如:
- 实现原理
- 路由匹配与组件加载
- 当用户访问一个路由时,VueRouter 会根据当前的 URL 在路由配置中进行路由匹配。如果找到匹配的路由,并且该路由的组件是通过动态导入方式定义的,VueRouter 会触发动态导入的逻辑,开始加载对应的路由组件。
- 由于动态导入返回一个 Promise,VueRouter 会等待这个 Promise resolve,直到路由组件加载完成。在加载过程中,用户可能会看到一个加载状态或者空白页面,具体取决于应用的设计。
- 代码分割与性能优化
- 路由懒加载的一个重要好处是实现了代码分割。通过将不同路由的组件分别打包成独立的代码块,只有在用户访问相应路由时才会加载对应的代码块,而不是在应用初始化时一次性加载所有的路由组件。
- 这样可以大大减少应用的初始加载时间,提高应用的性能。特别是对于大型应用,路由懒加载可以显著提高用户体验,避免因为加载大量不必要的代码而导致的性能问题。
- 浏览器缓存与重复访问
- 当用户首次访问一个路由并加载了相应的路由组件后,浏览器会将该组件的代码缓存起来。如果用户再次访问同一个路由,浏览器可以直接从缓存中获取组件的代码,而不需要再次进行网络请求,进一步提高了应用的加载速度。
- 路由匹配与组件加载
VueRouter 中动态路由的实现原理是什么?
动态路由的实现原理主要涉及以下几个方面:
- 定义动态路由
- 路由路径中的参数
- 动态路由通过在路由路径中使用参数来实现。参数可以用冒号
:开头的标识符表示。例如,/user/:id表示一个动态路由,其中:id是一个参数,表示用户的唯一标识符。 - 当用户访问这个路由时,实际的 URL 可能是
/user/123,其中 123 是具体的用户 ID。
- 动态路由通过在路由路径中使用参数来实现。参数可以用冒号
- 路由配置对象
- 在 VueRouter 的路由配置中,需要为动态路由指定一个组件或一个路由处理函数。例如:
const routes = [ { path: '/user/:id', component: UserProfileComponent } ];
- 在 VueRouter 的路由配置中,需要为动态路由指定一个组件或一个路由处理函数。例如:
- 路由路径中的参数
- 路由匹配与参数提取
- 路由匹配
- 当用户访问一个 URL 时,VueRouter 会根据路由配置中的路径进行匹配。对于动态路由,VueRouter 会尝试将 URL 中的参数与路由路径中的参数进行匹配。
- 如果匹配成功,VueRouter 会认为找到了对应的路由,并加载相应的组件或执行路由处理函数。
- 参数提取
- 在路由匹配成功后,VueRouter 会从 URL 中提取出动态路由参数的值。例如,如果用户访问
/user/123,VueRouter 会提取出参数 id 的值为 123。
- 在路由匹配成功后,VueRouter 会从 URL 中提取出动态路由参数的值。例如,如果用户访问
- 传递参数给组件
- 提取出的动态路由参数会被传递给相应的组件。在组件中,可以通过
$route.params对象来获取动态路由参数的值。例如:export default { name: 'UserProfileComponent', computed: { userId() { return this.$route.params.id; } } };
- 提取出的动态路由参数会被传递给相应的组件。在组件中,可以通过
- 路由匹配
- 导航与参数传递
- 使用编程式导航传递参数:在 Vue 应用中,可以使用编程式导航来进行页面跳转,并传递动态路由参数。例如:
this.$router.push({ path: '/user/' + userId }); - 使用
<router-link>传递参数:也可以使用<router-link>组件来进行页面跳转,并传递动态路由参数。例如:<router-link :to="{ path: '/user', params: { id: userId } }">User Profile</router-link>
- 使用编程式导航传递参数:在 Vue 应用中,可以使用编程式导航来进行页面跳转,并传递动态路由参数。例如:
动态路由的优点和缺点分别是什么?
优点:
- 灵活性高
- 动态路由允许根据不同的参数值加载不同的页面内容,这使得应用能够适应各种不同的情况。例如,一个电商网站可以使用动态路由来显示不同商品的详细页面,根据商品 ID 动态加载相应的商品信息。
- 可以轻松地处理具有相似结构但内容不同的页面,而不需要为每个具体的情况定义单独的静态路由。
- 代码复用性强
- 对于具有相似布局或功能的页面,可以使用同一个组件来处理不同的动态路由参数。例如,一个用户资料页面可以使用同一个组件来显示不同用户的信息,只需要根据用户 ID 动态获取和展示不同的数据。
- 减少了重复代码的编写,提高了开发效率。
- 易于维护
- 当需要对具有相似结构的页面进行修改时,只需要修改处理动态路由的组件即可,而不需要对每个具体的静态路由对应的页面进行修改。
- 对于大型应用,动态路由可以使路由配置更加简洁明了,降低了维护的难度。
- 更好的用户体验
- 动态路由可以实现更流畅的导航。例如,在单页应用中,用户可以通过点击链接或使用编程式导航直接跳转到特定的动态路由,而不需要经历整页刷新,提供了更快速和无缝的用户体验。
- 可以根据用户的操作和输入动态地更新页面内容,增强了应用的交互性。
缺点:
- 复杂的路由管理
- 动态路由的存在使得路由管理变得更加复杂。需要确保正确地处理不同的参数值,以及可能出现的错误情况。例如,如果参数值无效或不存在,需要进行适当的错误处理,以避免页面出现错误或异常。
- 对于复杂的应用,可能需要考虑多个动态参数的组合,这增加了路由配置和处理的复杂性。
- 安全风险
- 动态路由可能带来一定的安全风险。如果不正确地处理用户输入的参数,可能会导致安全漏洞,如 SQL 注入攻击或跨站脚本攻击(XSS)。
- 需要对用户输入进行严格的验证和过滤,以确保应用的安全性。
- 性能问题
- 在某些情况下,动态路由可能会导致性能问题。例如,如果动态加载的组件较大或网络延迟较高,可能会影响页面的加载速度。
- 需要合理地设计和优化动态路由的加载逻辑,以确保应用的性能。
- 难以预测的 URL 结构
- 由于动态路由的参数值是可变的,这使得 URL 的结构难以预测。这可能会给用户带来一些困惑,特别是在需要分享链接或进行搜索引擎优化时。
- 需要采取适当的措施,如使用静态化技术或提供清晰的 URL 结构说明,以提高用户对 URL 的理解和可预测性。
VueRouter 怎么用的?
-
背下文档第一句:VueRouter 是 Vue.js 官方的路由管理器。
-
核心概念和作用
- History 模式
vue-router 默认 hash 模式。如果不想要很丑的 hash,可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。(需要后台配置支持)
*补充:
history.pushState()方法向当前浏览器会话的历史堆栈中添加一个状态(state)。 - 重定向和别名
- 重定向
重定向也是通过 routes 配置来完成。
//从 /a 重定向到 /b const router = new VueRouter({ routes: [ { path: '/a', redirect: '/b' } ] }) //重定向的目标也可以是一个命名的路由 const router = new VueRouter({ routes: [ { path: '/a', redirect: { name: 'foo' }} ] }) //甚至是一个方法,动态返回重定向目标 const router = new VueRouter({ routes: [ { path: '/a', redirect: to => { // 方法接收 目标路由 作为参数 // return 重定向的 字符串路径/路径对象 }} ] }) - 别名 /a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。
- 重定向
重定向也是通过 routes 配置来完成。
- 路由懒加载(按需加载路由)
结合 Vue 的异步组件和 Webpack 的代码分割功能。
//首先,可以将异步组件定义为返回一个 Promise 的工厂函数 //该函数返回的 Promise 应该 resolve 组件本身 const Foo = () => Promise.resolve({ /* 组件定义对象 */ }) //第二,在 Webpack 2 中,我们可以使用动态 import 语法来定义代码分块点 import('./Foo.vue') // 返回 Promise //结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件 const Foo = () => import('./Foo.vue') - 导航守卫 看下一题。
- History 模式
vue-router 默认 hash 模式。如果不想要很丑的 hash,可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。(需要后台配置支持)
*补充:
-
常用 API
- router-link
<router-link>组件支持用户在具有路由功能的应用中 (点击) 导航。 默认渲染成带有正确链接的<a>标签,优点在于:- 无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致
- 在 HTML5 history 模式下,router-link 会守卫点击事件,让浏览器不再重新加载页面
- 当你在 HTML5 history 模式下使用 base 选项之后,所有的 to 属性都不需要写了
<router-link>接受属性: - to:表示目标路由的链接。当被点击后,内部会立刻把 to 的值传到
router.push() - replace:会调用
router.replace()而不是router.push(),于是导航后不会留下 history 记录 - tag:指定
<router-link>渲染成某种标签 - active-class:设置链接激活时使用的 CSS 类名。默认值:
router-link-active
- router-view
<router-view>组件渲染路径匹配到的视图组件。 接受 name 属性:如果设置了名称,则会渲染对应的路由配置中 components 下的相应组件。 - this.$router.push
- this.$router.replace
- this.$route.params 路由的参数,一个 key/value 对象。
- router-link
路由守卫是什么?
路由守卫就是路由跳转的一些操作,比如登录鉴权(没有登录不能进入个人中心页)等等。
- 全局前置守卫
router.beforeEach((to, from, next) => { ... })注册全局前置守卫。
to: Route:即将要进入的目标 路由对象from: Route:当前导航正要离开的路由next: Function:是否进行下一步- next():进行下一步
- next(false):中断当前的导航
- next('/') 或者 next({ path: '/' }):跳转到一个不同的地址
- 全局解析守卫
router.beforeResolve((to, from, next) => { ... })注册全局守卫。 和router.beforeEach类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。 - 全局后置钩子
router.afterEach((to, from) => { ... })注册全局后置钩子。 - 路由独享守卫 在路由配置上直接定义 beforeEnter 守卫,与全局前置守卫的方法参数是一样的。
- 组件内部守卫
可以在路由组件内直接定义以下路由导航守卫:(也接受 to, from, next 三个参数)
- beforeRouteEnter:在渲染该组件的对应路由被 confirm 前调用。
不能获取组件实例
this,因为当守卫执行前,组件实例还没被创建。 - beforeRouteUpdate:在当前路由改变,但是该组件被复用时调用。
- beforeRouteLeave:导航离开该组件的对应路由时调用。 通常用来禁止用户在还未保存修改前突然离开。
- beforeRouteEnter:在渲染该组件的对应路由被 confirm 前调用。
不能获取组件实例
讲一下完整的导航守卫流程?
- 导航被触发。
- 在失活的组件里调用离开守卫
beforeRouteLeave(to,from,next)。 - 调用全局的
beforeEach( (to,from,next) =>{} )守卫。 - 在重用的组件里调用
beforeRouteUpdate(to,from,next)守卫。 - 在路由配置里调用
beforeEnter(to,from,next)路由独享的守卫。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter(to,from,next)。 - 在所有组件内守卫和异步路由组件被解析之后调用全局的
beforeResolve( (to,from,next) =>{} )解析守卫。 - 导航被确认。
- 调用全局的
afterEach( (to,from) =>{} )钩子。 - 触发 DOM 更新。
- 用创建好的实例调用
beforeRouteEnter守卫中传给 next 的回调函数。
在什么场景下会用到嵌套路由?嵌套路由怎么定义?
例如,做个管理系统,顶部栏和左侧菜单栏是全局通用的,那就应该放在父路由,而右下的页面内容部分放在子路由。
在 VueRouter 的参数中使用 children 配置。
const routes = [
{ path: "/", redirect: "/home" },
{
path: "/home",
component: home,
children: [
{ path: "/home/game", component: game }
]
}
]
路由有几种模式?说说它们的区别?
- hash:兼容所有浏览器,包括不支持 HTML5 History Api 的浏览器,例
http://www.abc.com/#/index,hash 值为#/index,hash的改变会触发hashchange事件,通过监听hashchange事件来完成操作实现前端路由。hash 值变化不会让浏览器向服务器请求。
// 监听hash变化,点击浏览器的前进后退会触发
window.addEventListener('hashchange', function(event) {
let newURL = event.newURL // hash 改变后的新 url
let oldURL = event.oldURL // hash 改变前的旧 url
}, false)
- history:兼容能支持 HTML5 History Api 的浏览器,依赖 HTML5 History API 来实现前端路由。没有 #,路由地址跟正常的 url 一样,但是初次访问或者刷新都会向服务器请求,如果没有请求到对应的资源就会返回 404,所以路由地址匹配不到任何静态资源,则应该返回同一个
index.html页面,需要在 nginx 中配置。 - abstract:支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
使用 history 模式,部署时要注意什么?
要注意 404 的问题,因为在 history 模式下,只是动态的通过 js 操作window.history来改变浏览器地址栏里的路径,并没有发起 http 请求,当直接在浏览器里输入这个地址的时候,就一定要对服务器发起 http 请求,但是这个目标在服务器上又不存在,所以会返回 404。
所以要在 Nginx 中将所有请求都转发到index.html上就可以了。
location / {
try_files $uri $uri/ @router index index.html;
}
location @router {
rewrite ^.*$ /index.html last;
}
怎么重定向页面?
第一种:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: '/b' }
]
})
第二种:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: { name: 'foo' }}
]
})
第三种:
const router = new VueRouter({
routes: [
{
path: '/a',
redirect: to =>{
const { hash, params, query } = to
if (query.to === 'foo') {
return { path: '/foo', query: null }
} else {
return '/b'
}
}
}
]
})
怎么在组件中监听路由参数的变化?
有两种方法可以监听路由参数的变化,但是只能用在包含<router-view />的组件内。
第一种:
watch: {
'$route'(to, from) {
//这里监听
}
}
第二种:
beforeRouteUpdate(to, from, next) {
//这里监听
}
切换路由时,需要保存草稿的功能,怎么实现呢?
<keep-alive :include="include">
<router-view></router-view>
</keep-alive>
其中 include 可以是个数组,数组内容为路由的 name 选项的值。
切换路由后,新页面要滚动到顶部或保持原先的滚动位置怎么做呢?
const router = new Router({
mode: 'history',
base: process.env.BASE_URL,
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
})
什么是命名视图,举个例子说明一下?
在项目中,我们想同级展示多个视图,而不是嵌套展示。例如项目首页,有头部导航,侧边栏导航、主内容区域。头部导航、侧边栏导航我们不想用组件方式引入,想用视图方式展示。那么这个首页上,就有三个视图,头部导航视图,侧边栏导航视图、主内容区域视图同级展示。
在routes.js文件中
function load(component) {
return resolve => require([`views/${component}`], resolve)
}
const routes = [
{
path: '/',
redirect: '/home',
name: 'layout',
component: load('layout'),
children: [
{
path: '/home',
name: 'home',
components: {
default: load('main'),
header: load('header'),
sider: load('sider')
},
meta: {
title: '首页'
}
}
]
}
]
在layout.vue文件中
<template>
<div>
<div>
<!-- 头部导航 -->
<router-view name='header'></router-view>
<div>
<!-- 侧边栏导航 -->
<router-view name='sider'></router-view>
</div>
<div>
<!-- 主内容 -->
<router-view/>
</div>
</div>
</template>
如果router-view没有设置 name,那么默认为 default。一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置 (记得加上 s )。
如何获取路由传过来的参数?
路由有三种传参方式,获取方式各不相同。
- meta:路由元信息,写在 routes 配置文件中
通过{ path: '/home', name: 'home', component: load('home'), meta: { title: '首页' } }this.$route.meta.title获取 - query:
浏览器地址:this.$route.push({ path:'/home', query:{ userId:123 } })http://localhost:8036/home?userId=123,获取方式:this.$route.query.userId - params:
- 首先要在地址上做配置
{ path: '/home/:userId', name: 'home', component: load('home'), meta: { title: '首页' } } - 访问传参
注意用 params 传参,只能用命名的路由(用 name 访问)。const userId = '123' this.$router.push({ name: 'home', params: { userId } }) - 浏览器地址:
http://localhost:8036/home/123 - 获取方式:
this.$route.params.userId
- 首先要在地址上做配置
路由组件和路由为什么解耦?怎么解耦?
因为在组件中使用$route会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性,所有要解耦。
耦合如以下代码所示,Home 组件只有在http://localhost:8036/home/123URL上才能使用:
const Home = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/home/:id', component: Home }
]
})
使用 props 来解耦:
- props 为 true,
route.params将会被设置为组件属性。 - props 为对象,则按原样设置为组件属性。
- props 为函数,
http://localhost:8036/home?id=123,会把123传给组件 Home 的 props 的 id。
const Home = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/home/:id', component: Home, props: true },
// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/home/:id',
components: { default: Home, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
{ path: '/home', component: Home, props: {id:123} },
{ path: '/home', component: Home, props: (route) => ({ id: route.query.id }) }
]
})
active-class是哪个组件的属性?
<router-link/>组件的属性,设置链接激活时使用的 CSS 类名。默认值可以通过路由的构造选项 linkActiveClass 来全局配置。
在 Vue 组件中怎么获取到当前的路由信息?
通过this.$route来获取。
怎样动态加载路由?
使用 Router 的实例方法addRoutes来实现动态加载路由,一般用来实现菜单权限。使用时要注意,静态路由文件中不能有 404 路由,而要通过addRoutes一起动态添加进去。
const routes = [
{
path: '/overview',
name: 'overview',
component: () => import('@/views/account/overview/index'),
meta: {
title: '账户概览',
pid: 869,
nid: 877
}
},
{
path: '*',
redirect: {
path: '/'
}
}
]
vm.$router.options.routes.push(...routes)
vm.$router.addRoutes(routes)
怎么实现路由懒加载呢?
function load(component) {
// return resolve => require([`views/${component}`], resolve)
return () => import(`views/${component}`)
}
const routes = [
{
path: '/home',
name: 'home',
component: load('home'),
meta: {
title: '首页'
}
}
]
路由之间是怎么跳转的?有哪些方式?
- 声明式:通过使用内置组件
<router-link :to="/home">来跳转 - 编程式:通过调用 router 实例的 push 方法
router.push({ path: '/home' })或 replace 方法router.replace({ path: '/home' })
route 和 router 有什么区别?
- route 是路由信息对象,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。
- router 是路由实例对象,包括了路由的跳转方法,钩子函数等。
Vue 路由怎么跳转打开新窗口?
const obj = {
path: xxx, // 路由地址
query: {
mid: data.id // 可以带参数
}
}
const { href } = this.$router.resolve(obj)
window.open(href, '_blank')