基础知识
MVVM的理解
MVVM
是一种软件架构模式,MVVM
分为 Model
、View
、ViewModel
:
Model
代表数据模型,数据和业务逻辑都在Model
层中定义;View
代表UI视图,负责数据的展示;ViewModel
负责监听Model
中数据的改变并且控制视图的更新,处理用户交互操作;
Model
和View
并无直接关联,而是通过ViewModel
来进行联系的,Model
和ViewModel
之间有着双向数据绑定的联系。因此当Model
中的数据改变时会触发View
层的刷新,View
中由于用户交互操作而改变的数据也会在Model
中同步。
vue和react的区别,有什么相同
不同:
- 模版语法不同,
react
采用JSX语法,vue
使用基于HTML的模版语法 - 数据绑定不同,
vue
使用双向数据绑定,react
则需要手动控制组件的状态和属性。 - 状态管理不同,
vue
使用vuex
状态管理,react
使用redux
状态管理 - 组件通信不同,
vue
使用props
和事件的方式进行父子组件通信,react
则通过props
和回调函数的方式进行通信。 - 生命周期不同,
vue
有8个生命周期钩子,react
有10个 - 响应式原理不同,
vue
使用双向绑定来实现数据更新,react
则通过单向数据流来实现
相同:
- 组件化开发:
Vue
和React
都采用了组件化开发的方式,将用户界面划分为独立、可复用的组件,从而使代码更加模块化、可维护和可扩展。 - 虚拟 DOM:
Vue
和React
都使用虚拟 DOM 技术,通过在JavaScript
和真实 DOM 之间建立一个轻量级的虚拟 DOM 层,实现高效的 DOM 更新和渲染。 - 响应式更新:
Vue
和React
都支持响应式更新,即当数据发生变化时,会自动更新相关的组件和视图,以保持用户界面的同步性。 - 集成能力:
Vue
和React
都具有良好的集成能力,可以与其他库和框架进行整合,例如Vue
可以与Vuex
、Vue Router
等配套使用,React
可以与Redux
、React Router
等配套使用。
Vue2和Vue3有哪些区别
Vue2
使用的是optionsAPI
,Vue3
使用composition API
,更好的组织代码,提高代码可维护性Vue3
使用Proxy
代理实现了新的响应式系统,比Vue2
有着更好的性能和更准确的数据变化追踪能力。Vue3
引入了Teleprot组件,可以将DOM元素渲染到DOM数的其他位置,用于创建模态框、弹出框等。Vue3
全局API名称发生了变化,同时新增了watchEffect
、Hooks
等功能Vue3
对TypeScript
的支持更加友好Vue3
核心库的依赖更少,减少打包体积- 3支持更好的
Tree Shanking
,可以更加精确的按需要引入模块
SPA的理解,有什么优缺点
SPA(单页应用)是一种前端应用程序的架构模式,它通过在加载应用程序时只加载单个 HTML
页面,并通过使用 JavaScript
动态地更新页面内容,从而实现无刷新的用户体验。
SPA和多页面有什么区别
区别
- 页面加载方式:在多页面应用中,每个页面都是独立的 HTML 文件,每次导航时需要重新加载整个页面。而在
SPA
中,初始加载时只加载一个 HTML 页面,后续的导航通过JavaScript
动态地更新页面内容,无需重新加载整个页面。 - 用户体验:
SPA
提供了流畅、快速的用户体验,因为页面切换时无需等待整个页面的重新加载,只有需要的数据和资源会被加载,减少了页面刷新的延迟。多页面应用则可能会有页面刷新的延迟,给用户带来较长的等待时间。 - 代码复用:
SPA
通常采用组件化开发的方式,可以在不同的页面中复用组件,提高代码的可维护性和可扩展性。多页面应用的每个页面都是独立的,组件复用的机会较少。 - 路由管理:在多页面应用中,页面之间的导航和路由由服务器处理,每个页面对应一个不同的
URL
。而在SPA
中,前端负责管理页面的导航和路由,通过前端路由库(如React Router
或Vue Router
)来管理不同路径对应的组件。 - SEO(搜索引擎优化):由于多页面应用的每个页面都是独立的 HTML 文件,搜索引擎可以直接索引和抓取每个页面的内容,有利于搜索引擎优化。相比之下,
SPA
的内容是通过JavaScript
动态生成的,搜索引擎的爬虫可能无法正确地获取和索引页面的内容,需要采取额外的优化措施。 - 服务器负载:
SPA
只需初始加载时获取HTML
、CSS
和JavaScript
文件,后续的页面更新和数据获取通常通过 API 请求完成,减轻了服务器的负载。而多页面应用每次导航都需要从服务器获取整个页面的内容。
优点
- 用户体验:
SPA
提供了流畅、快速的用户体验,在页面加载后,只有需要的数据和资源会被加载,减少了页面刷新的延迟。 - 响应式交互:由于
SPA
依赖于异步数据加载和前端路由,可以实现实时更新和动态加载内容,使用户可以快速地与应用程序交互。 - 代码复用:
SPA
通常采用组件化开发的方式,提高了代码的可维护性和可扩展性。 - 服务器负载较低:由于只有初始页面加载时需要从服务器获取
HTML
、CSS
和JavaScript
文件,减轻了服务器的负载。
缺点:
- 首次加载时间:
SPA
首次加载时需要下载较大的JavaScript
文件,这可能导致初始加载时间较长。 - SEO(搜索引擎优化)问题:由于
SPA
的内容是通过JavaScript
动态生成的,搜索引擎的爬虫可能无法正确地获取和索引页面的内容。 - 内存占用:
SPA
在用户浏览应用程序时保持单个页面的状态,这可能导致较高的内存占用。 - 安全性:由于
SPA
通常使用API
进行数据获取,因此需要特别注意安全性。
Vue的性能优化有哪些
编码阶段
v-if
和v-for
不一起使用v-for
保证key
的唯一性- 使用
keep-alive
缓存组件 v-if
和v-show
酌情使用- 路由懒加载、异步组件
- 图片懒加载
- 节流防抖
- 第三方模块按需引入
- 服务端与渲染
打包优化
- 压缩代码
- 使用CDN加载第三方模块
- 抽离公共文件
用户体验
- 骨架屏
- 客户端缓存
SEO优化
- 预渲染
- 服务端渲染
- 合理使用
meta
标签
Vue生命周期
创建前后:
beforeCreate(创建前):
数据观测和初始化事件还未开始,不能访问data
、computed
、watch
、methods
上的数据方法。created(创建后):
实例创建完成,可以访问data
、computed
、watch
、methods
上的数据方法,但此时渲染节点还未挂在到DOM上,所以不能访问。
挂载前后:
beforeMount(挂载前):
Vue实例还未挂在到页面HTML上,此时可以发起服务器请求mounted(挂载后):
Vue实例已经挂在完毕,可以操作DOM
更新前后:
beforeUpdate(更新前):
数据更新之前调用,还未渲染页面updated(更新后):
DOM重新渲染,此时数据和界面都是新的。
销毁前后:
beforeDestorye(销毁前):
实例销毁前调用,这时候能够获取到this
destoryed(销毁后):
实例销毁后调用,实例完全被销毁。
常用的属性、指令有哪些
属性:
data
:用于定义组件的初始数据。props
:用于传递数据给子组件。computed
:用于定义计算属性。methods
:用于定义组件的方法。watch
:用于监听组件的数据变化。components
:用于注册子组件。可以通过components
属性将其他组件注册为当前组件的子组件,从而在模板中使用这些子组件。
指令:
v-if
:条件渲染指令,根据表达式的真假来决定是否渲染元素。v-show
:条件显示指令,根据表达式的真假来决定元素的显示和隐藏。v-for
:列表渲染指令,用于根据数据源循环渲染元素列表。v-bind
:属性绑定指令,用于动态绑定元素属性到Vue
实例的数据。v-on
:事件绑定指令,用于监听DOM
事件,并执行对应的Vue
方法。v-model
:双向数据绑定指令,用于在表单元素和Vue
实例的数据之间建立双向绑定关系。v-text
:文本插值指令,用于将数据插入到元素的文本内容中。v-html
:HTML
插值指令,用于将数据作为HTML
解析并插入到元素中。
Computed 和 Watch 的区别
computed
计算属性,通过对已有的属性值进行计算得到一个新值。它需要依赖于其他的数据,当数据发生变化时,computed
会自动计算更新。computed
属性值会被缓存,只有当依赖数据发生变化时才会重新计算,这样可以避免重复计算提高性能。
watch
用于监听数据的变化,并在变化时执行一些操作。它可以监听单个数据或者数组,当数据发生变化时会执行对应的回调函数,和computed
不同的是watch
不会有缓存。
Vue组件通信
父传子
- props
- $children
- $refs
子传父
- $emit
- $parent
兄弟组件
- provied
- inject
- eventBus
- Vuex
常见的事件修饰符及其作用
.stop
阻止冒泡.prevent
阻止默认事件.capture
:与事件冒泡的方向相反,事件捕获由外到内;- .
self
:只会触发自己范围内的事件,不包含子元素; .once
:只会触发一次。
v-if和v-show的区别
v-if
元素不可见,直接删除DOM,有更高的切换消耗。 v-show
通过设置元素display: none
控制显示隐藏,更高的初始渲染消耗。
v-html 的原理
会先移除节点下的所有节点,调用html
方法,通过addProp
添加innerHTML
属性,归根结底还是设置innerHTML
为v-html
的值。
v-model 是如何实现的,语法糖实际是什么?
Vue 中数据双向绑定是一个指令v-model
,可以绑定一个响应式数据到视图,同时视图的变化能改变该值。
- 当作用在表单上:通过
v-bind:value
绑定数据,v-on:input
来监听数据变化并修改value
- 当作用在组件上:本质上是一个父子通信语法糖,通过
props
和$emit
实现。
data为什么是一个函数而不是对象
因为对象是一个引用类型,如果data
是一个对象的情况下会造成多个组件共用一个data
,data
为一个函数,每个组件都会有自己的私有数据空间,不会干扰其他组件的运行。
mixin 和 mixins 区别
mixin
用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。mixins
应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过mixins
混入代码,比如上拉下拉加载数据这种逻辑等等。
路由的hash和history模式的区别
hash模式 开发中默认的模式,地址栏URL后携带#
,后面为路由。 原理是通过onhashchange()
事件监听hash
值变化,在页面hash
值发生变化后,window
就可以监听到事件改变,并按照规则加载相应的代码。hash
值变化对应的URL都会被记录下来,这样就能实现浏览器历史页面前进后退。
history模式 history
模式中URL没有#
,这样相对hash
模式更好看,但是需要后台配置支持。
history
原理是使用HTML5 history
提供的pushState
、replaceState
两个API,用于浏览器记录历史浏览栈,并且在修改URL时不会触发页面刷新和后台数据请求。
router和route的区别
$route
是路由信息,包括path
、params
、query
、name
等路由信息参数$router
是路由实例,包含了路由跳转方法、钩子函数等
如何设置动态路由
- params传参
- 路由配置:
/index/:id
- 路由跳转:
this.$router.push({name: 'index', params: {id: "zs"}});
- 路由参数获取:
$route.params.id
- 最后形成的路由:
/index/zs
- 路由配置:
- query传参
- 路由配置:
/index
正常的路由配置 - 路由跳转:
this.$rouetr.push({path: 'index', query:{id: "zs"}});
- 路由参数获取:
$route.query.id
- 最后形成的路由:
/index?id=zs
- 路由配置:
区别
- 获取参数方式不一样,一个通过
$route.params
,一个通过$route.query
- 参数的生命周期不一样,
query
参数在URL地址栏中显示不容易丢失,params
参数不会在地址栏显示,刷新后会消失
路由守卫
- 全局前置钩子:
beforeEach
、beforeResolve
、afterEach
- 路由独享守卫:
beforeEnter
- 组件内钩子:
beforeRouterEnter
、beforeRouterUpdate
、beforeRouterLeave
Vue中key的作用
key
的作用主要是为了高效的更新虚拟DOM,其原理是vue
在patch
过程中通过key
可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,减少DOM
操作量,提高性能。
为什么不建议用index作为key?
如果将数组下标作为key
值,那么当列表发生变化时,可能会导致key
值发生改变,从而引发不必要的组件重新渲染,甚至会导致性能问题。例如,当删除列表中某个元素时,其后面的所有元素的下标都会发生改变,导致Vue
重新渲染整个列表。
为什么v-for和v-if不能一起使用
v-for
比v-if
优先级更高,一起使用的话每次渲染列表时都要执行一次条件判断,造成不必要的计算,影响性能。
原理知识
双向数据绑定的原理
采用数据劫持结合发布者-订阅者模式的方式,data
数据在初始化的时候,会实例化一个Observe
类,在它会将data
数据进行递归遍历,并通过Object.defineProperty
方法,给每个值添加上一个getter
和一个setter
。在数据读取的时候会触发getter
进行依赖(Watcher)收集,当数据改变时,会触发setter
,对刚刚收集的依赖进行触发,并且更新watcher
通知视图进行渲染。
使用 Object.defineProperty() 来进行数据劫持有什么缺点?
该方法只能监听到数据的修改,监听不到数据的新增和删除,从而不能触发组件更新渲染。vue2中会对数组的新增删除方法push、pop、shift、unshift、splice、sort、reserve
通过重写的形式,在拦截里面进行手动收集触发依赖更新。
和Vue3相比有什么区别?
Vue3
采用了Proxy
代理的方式,Proxy
是ES6引入的一个新特性,它提供了一个用于创建代理对象的构造函数。它是对整个对象的监听和拦截,可以对对象所有操作进行处理。而Object.defineProperty
只能监听单个属性的读写,无法监听新增、删除等操作。
Vue是如何收集依赖的?
依赖收集发生在defineReactive()
方法中,在方法内new Dep()
实例化一个Dep()
实例,然后在getter
中通过dep.depend()
方法对数据依赖进行收集,然后在settter
中通过dep.notify()
通知更新。整个Dep
其实就是一个观察者,吧收集的依赖存储起来,在需要的时候进行调用。在收集数据依赖的时候,会为数据创建一个Watcher
,当数据发生改变通知每个Watcher
,由Wathcer
进行更新渲染。
slot是什么?有什么作用?原理是什么?
slot
插槽,一般在封装组件的时候使用,在组件内不知道以那种形式来展示内容时,可以用slot
来占据位置,最终展示形式由父组件以内容形式传递过来,主要分为三种:
- 默认插槽:又名匿名插槽,当
slot
没有指定name
属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。 - 具名插槽:带有具体名字的插槽,也就是带有
name
属性的slot
,一个组件可以出现多个具名插槽。 - 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件vm
实例化时,获取到父组件传入的slot
标签的内容,存放在vm.$slot
中,默认插槽为vm.$slot.default
,具名插槽为vm.$slot.xxx
,xxx 为插槽名,当组件执行渲染函数时候,遇到slot
标签,使用$slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
对keep-alive的理解,它是如何实现的,具体缓存的是什么?
keep-alive
是Vue.js的一个内置组件。它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。
include
字符串或正则表达式,只有名称匹配的组件会被匹配;exclude
字符串或正则表达式,任何名称匹配的组件都不会被缓存;max
数字,最多可以缓存多少组件实例。
2 个生命周期 activated , deactivated
activated
:当缓存的组件被激活时,该钩子函数被调用。可以在该钩子函数中进行一些状态恢复、数据更新等操作。deactivated
:当缓存的组件被停用时,该钩子函数被调用。可以在该钩子函数中进行一些状态保存、数据清理等操作。
keep-alive
内部其实是一个函数式组件,没有template
标签。在render
中通过获取组件的name
和include、exclude
进行匹配。匹配不成功,则不需要进行缓存,直接返回该组件的vnode
。
匹配成功就进行缓存,获取组件的key
在cache
中进行查找,如果存在,则将他原来位置上的 key
给移除,同时将这个组件的 key
放到数组最后面(LRU
)也就实现了max
功能。
不存在的话,就需要对组件进行缓存。将当前组件push(key)
添加到尾部,然后再判断当前缓存的max
是否超出指定个数,如果超出直接将第一个组件销毁(缓存淘汰策略LRU)。
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
$nextTick 原理及作用
Vue 的 nextTick
其本质是对 JavaScript
执行原理 EventLoop
的一种应用。 nextTick
是将回调函数放到一个异步队列中,保证在异步更新DOM的watcher
后面,从而获取到更新后的DOM。
因为在created()
钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()
的回调函数中。
Vue模版编译原理
模版编译主要过程:template ---> ast ---> render
,分别对象三个方法
parse
函数解析template
optimize
函数优化静态内容generate
函数创建render
函数字符串
调用parse
方法,将template
转化为AST
(抽象语法树),AST
定义了三种类型,一种html
标签,一种文本,一种插值表达式,并且通过 children
这个字段层层嵌套形成了树状的结构。
optimize
方法对AST
树进行静态内容优化,分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化。
generate
将AST
抽象语法树编译成 render
字符串,最后通过new Function(render)
生成可执行的render
函数
Vuex
Vuex 的原理
Vuex
是专门为Vue
设计的状态管理,当Vue
从store
中读取数据后,数据发生改变,组件中的数据也会发生变化。
Vue Components
负责接收用户操作交互行为,执行dispatch触发对应的action进行回应dispatch
唯一能执行action的方法action
用来接收components的交互行为,包含异步同步操作commit
对mutation进行提交,唯一能执行mutation的方法mutation
唯一可以修改state状态的方法state
页面状态管理容器,用于存储状态getters
读取state方法
Vue组件接收交互行为,调用dispatch
方法触发action
相关处理,若页面状态需要改变,则调用commit
方法提交mutation
修改state
,通过getters
获取到state
新值,重新渲染Vue Components
,界面随之更新。
Vuex中action和mutation的区别
mutation
更专注于修改state
,必须是同步执行。action
提交的是mutation
,而不是直接更新数据,可以是异步的,如业务代码,异步请求。action
可以包含多个mutation
Vuex 和 localStorage 的区别
Vuex
存储在内存中,页面关闭刷新就会消失。而localstorage
存储在本地,读取内存比读取硬盘速度要快Vuex
应用于组件之间的传值,localstorage
主要用于不同页面之间的传递Vuex
是响应式的,localstorage
需要刷新
虚拟DOM
对虚拟DOM的理解
虚拟DOM就是用JS对象来表述DOM节点,是对真实DOM的一层抽象。可以通过一些列操作使这个棵树映射到真实DOM上。
如在Vue
中,会把代码转换为虚拟DOM,在最终渲染到页面,在每次数据发生变化前,都会缓存一份虚拟DOM,通过diff
算法来对比新旧虚拟DOM记录到一个对象中按需更新,最后创建真实DOM,从而提升页面渲染性能。
虚拟DOM就一定比真实DOM更快吗
虚拟DOM不一定比真实DOM更快,而是在特定情况下可以提供更好的性能。
在复杂情况下,虚拟DOM可以比真实DOM操作更快,因为它是在内存中维护一个虚拟的DOM树,将真实DOM操作转换为对虚拟DOM的操作,然后通过diff
算法找出需要更新的部分,最后只变更这部分到真实DOM就可以。在频繁变更下,它可以批量处理这些变化从而减少对真实DOM的访问和操作,减少浏览器的回流重绘,提高页面渲染性能。
而在一下简单场景下,直接操作真实DOM可能会更快,当更新操作很少或者只是局部改变时,直接操作真实DOM比操作虚拟DOM更高效,省去了虚拟DOM的计算、对比开销。
虚拟DOM的解析过程
- 首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含
TagName
、props
和Children
这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。 - 当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
- 最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
DIFF算法原理
diff
的目的是找出差异,最小化的更新视图。 diff
算法发生在视图更新阶段,当数据发生变化的时候,diff
会对新旧虚拟DOM进行对比,只渲染有变化的部分。
- 对比是不是同类型标签,不是同类型直接替换
- 如果是同类型标签,执行
patchVnode
方法,判断新旧vnode
是否相等。如果相等,直接返回。 - 新旧
vnode
不相等,需要比对新旧节点,比对原则是以新节点为主,主要分为以下几种。newVnode
和oldVnode
都有文本节点,用新节点替换旧节点。newVnode
有子节点,oldVnode
没有,新增newVnode
的子节点。newVnode
没有子节点,oldVnode
有子节点,删除oldVnode
中的子节点。newVnode
和oldVnode
都有子节点,通过updateChildren
对比子节点。
双端diff
updateChildren
方法用来对比子节点是否相同,将新旧节点同级进行比对,减少比对次数。会创建4个指针,分别指向新旧两个节点的首尾,首和尾指针向中间移动。
每次对比下两个头指针指向的节点、两个尾指针指向的节点,头和尾指向的节点,是不是 key是一样的,也就是可复用的。如果是重复的,直接patch更新一下,如果是头尾节点,需要进行移动位置,结果以新节点的为主。
如果都没有可以复用的节点,就从旧的vnode
中查找,然后进行移动,没有找到就插入一个新节点。
当比对结束后,此时新节点还有剩余,就批量增加,如果旧节点有剩余就批量删除。