关于前端面试题
特性相关
Vue和React的区别
- vue使用类似html的template模板语法,react使用jsx
- 组件通信方式不同
- vue组件中子组件传值给父组件,可以通过emit事件
- react中,则需要父组件传入一个方法,由子组件调用
- 状态管理
- vue使用vuex/pinia进行状态管理
- react使用的是redux
- 生命周期
- vue组件有生命周期钩子
- react类组件可以使用生命周期,函数式组件一般使用hooks代替生命周期
- diff算法的实现不同
- react使用基于层级的diff算法
- 首先比较两棵树的根节点
- 核心思想是比较和替换节点
- vue使用的是双端比较diff算法
- react使用基于层级的diff算法
生命周期
生命周期有哪些?
beforeCreate 组件实例被创建之前
create 组件实例完全创建
beforeMount 组件挂载之前
mounted 组件挂载到实例上后
beforeUpdate 组件数据发生变化,更新之前
updated 组件数据更新之后
beforeDestroy 组件实例销毁之前
destroyed 组件实例销毁之后
activated keep-alive缓存的组件激活时
deactivated keep-alive缓存的组件停用时
errorCaptured 捕获一个来自子孙组件的错误时被调用
进入组件之后会执行哪些生命周期
- beforeCreate 组件实例被创建之前
- data不存在,dom未渲染
- create 组件实例完全创建
- data存在,dom未渲染
- beforeMount 组件挂载之前
- data存在,dom未渲染
- mounted 组件挂载到实例上后
- data数据存在,dom也已经被渲染出来了
父组件引入子组件,生命周期执行顺序是?
- 父组件-beforeCreate
- 父组件-created
- 父组件-beforeMount
- 子组件-beforeCreate
- 子组件-created
- 子组件-beforeMount
- 子组件-mounted
- 父组件-mounted
总结:父组件先准备数据,子组件接着渲染dom后,父组件渲染dom
created中如何获取dom
获取时机:
- 由于js执行代码遵循先同步后异步的机制,因此只要在异步回调中就能获取到dom
- 在setTimeout等宏任务中获取
- $nextTick
获取方式:
- dom方法
- ref
发送请求在created还是mounted?
答:
- 这个问题具体要看项目和业务情况了,
- 因为组件的加载顺序是:父组件引入了子组件,那么先执行父组件的前3个生命周期,再执行子组件的前4个生命周期
- 如果需要优先加载子组件的数据,那么父组件的请求需要放在mounted中
- 如果组件没有依赖关系,那么请求放在哪个生命周期都是可以的。
为什么发送请求不在beforeCreate里?beforeCreate和created有什么区别?
答:
- 在beforeCreate里,是获取不到methods里的方法的。
- beforeCreate阶段,获取不到$el和$data
- created中能获取到$date,也能访问methods里的方法
第2次,或者第N次进入组件会执行哪些生命周期
答:
- 如果加入了keep-alive,只执行activated
- 如果未加入,执行前4个生命周期
加入keep-alive会执行哪些生命周期?
- activated
- deactivated
生命周期的使用场景有哪些?
答:
- created
- 单个组件进行数据请求
- mounted
- 获取dom
- 控制父子组件请求顺序
- activated
- 组件入口携带不同的路由参数进入时,处理参数
- destroyed
- 清理监听器和定时器
- 记录页面视频上次播放的时间
组件
keep-alive
答:
- 用途:缓存当前组件,提升性能,减少发送请求的数量
- 如果用户请求的页面组件和当前组件相同,不会发起新的请求
- 被keep-alive包裹的组件,再次进入时,仅会触发activated生命周期,不再执行前四个生命周期
组件传值有几种方式
- 父传子
- 方法1:父组件引用子组件,子组件使用props接收参数,缺点是父组件无法传值给孙子辈组件
- 不允许子组件直接修改prop传入的数据
- 需要通过事件或者回调的方式通知父组件,由父组件进行修改
- 方法2:子组件使用$parent直接获取父组件数据,孙子辈可以使用this.$parent.$parent获取,子组件可以直接修改父组件的数据
- provide/inject进行依赖注入,父组件直接向子代组件注入数据
- 传入为基本数据类型时,inject接收值为副本,子组件修改数据不会影响组件/其它后代组件中的数据
- 传入为引用类型时,会影响
- 方法1:父组件引用子组件,子组件使用props接收参数,缺点是父组件无法传值给孙子辈组件
- 子传父
- 通过事件或者回调方式
- 通过this.$children[index].xxx,直接获取子组件实例中的数据
- 当存在条件渲染时,使用索引访问会变得很不可靠
- 绑定ref,通过this.$refs.子组件ref名称
- 兄弟互传
- 通过bus
1
2
3
4
5import bus from '@/utils/bus.js'
bus.$emit('change','new-value')
bus.$on('change',val=>{
console.log(val)
})
子组件如何直接修改父组件的值
- this.$parent.xxx知己修改
子组件如何找到父组件
- this.$parent
如何找到根组件
- this.$root
provide/inject
- 祖先组件通过provide注入数据
- 子孙组件通过inject接收数据
slot/插槽
- 匿名插槽
- 具名插槽
1
2<slot name="header"></slot>
<template #header></template> - 作用域插槽(可以传值)
1
2<slot name="header" :arr='["a","b","c"]' :brr='123'></slot>
<template #header='{arr,brr}'></template>
如何封装组件
组件尽量复杂,涉及到slot/组件通信…
举例来说,我封装过一个2d可视化地图组件,
这个组件主要作用是实时追踪卫星轨迹,对不同种类的卫星进行个性化定制展示,
原本是作为独立项目进行开发的,到了后期,业务要求将这个功能嵌入到其它多个系统中,所以就需要封装成组件
这个组件一共有2种模式,一种是提供精准追踪轨迹的方式,卫星会根据传入组件内的轨迹参数严格运行
一种是智能推演方式,以传入组件的轨迹点位为锚点,推演各个锚点间的轨迹
另外,像地图样式、卫星图例样式都是可以根据传入组件的参数进行个性化定制
组件内提供一些事件回调:轨迹追踪开始、追踪结束、卫星发生碰撞、敌我卫星超过危险距离等事件
另外,关于卫星图例中,像图表标题、图例、表头、表尾、工具栏等位置,都是提供插槽定制功能的,可以进行自定义操作。
props和data优先级谁高
答:props的优先级高
初始化状态优先级:
- props
- methods
- data
- computed
- watch
为什么data是一个函数
- 重复创建实例时避免数据污染
- 保证对象独立
vuex
Vuex有哪些属性
- state 全局共享属性
- $store.state.str
- mapState
- getters 针对state数据进行二次计算
- $store.getters.xxx
- mapGetters
- getters数据无法修改
- mutations
- 存放同步方法的
- 在mutations中修改state
- actions
- 存放一步方法的,提交mutations
- modules
- 把vuex再次进行模块封装
vuex使用state值
- this.$store.state.xxx
- 可以直接修改vuex的state数据的
- mapState
vuex的getters值修改
vuex是单项数据流还是双向数据流
答:单向数据流
可以通过mutation进行修改
vuex的mutations和actions区别
1 | mutations:{ |
- mutations无返回值,功能局限在状态变更上
- actions返回Promise对象,能够进行一步操作
- actions用于提交mutation,不是直接变更状态
vuex持久化存储
vuex本身无法实现持久化存储
- 方式1:localStorage
- 方式2:vuex-persistedstate
路由
路由的模式和区别
- hash
- url在根路径后跟随#
- 当请求的页面不存在时,不会向后台请求资源,自动跳转根路径
- 原理:基于url的hash值来实现路由,hash值变化,浏览器不会向服务器发送请求,而是触发hashChange事件,前端路由库监听该事件,根据hash值的变化动态渲染对应组件
- 无需服务器配置,无论hash值如何变化,服务器返回同一个页面
- 不利于SEO优化
- 用于需要兼容浏览器获不需要服务器配置的项目
- 打包后前台可以自测看到页面内容
- history
- 请求页面不存在时,依旧发送请求
- 一般会跳转404页面
- 原理:基于html5的history api
- 需要服务器配置和浏览器兼容
- 用于对url美观性和seo要求较高的项目
- 打包后前台自测无法正常访问路由
子路由
动态路由
答: 常用于详情页
1 | { |
路由传值
- 显式传值
1 | // http://localhost:8000/about?a=1 |
- 隐式传值
1 | // http://localhost:8000/about?a=1 |
导航故障
- 当前页跳转当前页的时候,会报导航故障错误
- 产生原因:在history模式下,使用html5的history api实现路由跳转,如果前端路由没有处理【同页面跳转】的情况,就可能导致导航故障
- 解决方法:重写VueRouter.prototype.push方法:
1 | import VueRouter from 'vue-router' |
$router和$route的区别
- $router
- 包含当前路由
- 包含整个路由的属性和方法
- $route
- 仅包含当前路由对象
导航守卫
- 全局守卫
- beforeEach 路由进入之前
- afterEach 路由进入之后
- 路由独享守卫
- beforeEnter 路由进入之前
1
2
3
4
5
6
7
8{
path:'/about',
name:'about',
beforeEnter:function(to,from,next){
if(true) next()
else next('/login')
}
} - 组件内守卫
- beforeRouteEnter 路由进入之前
- beforeRouteUpdate 路由更新之前
- beforeRouteLeave 路由离开之前
使用场景:一些只有在用户登录之后才能开发的页面,或者是需要对用户身份进行划分的组件
介绍一下SPA以及SPA有什么缺点
SPA:单页面应用
缺点:
- SEO优化不好
- 性能不是特别好
router跳转和a标签跳转区别
- a标签跳转回重新从服务器端请求资源,耗流量、耗性能
- spa单页路由模式,router跳转只是加载中间的html代码,不会请求资源
api
$set
$set(target,key,value)
1 | // [1,2,3]变为[1,5,3] |
$nextTick
- 传入的回调函数会被异步执行
- 常用于执行组件渲染后的操作
- 是微任务,因为使用Promise.resolve实现
1 | class Vue{ |
$refs
获取dom
$el
获取当前组件的根节点
$data
获取当前组件data数据
$children
获取当前组件的所有子组件(数组格式)
$parent
- 找到当前组件的父组件,如果找不到就返回自身
$root
获取根组件
data定义数据
1 | data(){ |
- 使用this.xxx定义的数据,没有getter和setter劫持处理,单独修改时数据不会刷新
computed计算属性
computed计算的值可以被修改吗(包括v-model双向绑定)?
- 可以,通过get和set
1 | changeStr:{ |
watch
- watch的初始化监听
- 加上配置项:immediate:true
- 深度监听
- deep:true
methods、computed、watch区别
- computed有缓存机制
- 当参与计算的值被改变时,计算属性会监听到并进行返回
- methods没有缓存机制
- watch 监听数据/路由的变化
- 监听对象发生改变时,会执行
指令
如何自定义指令
vue单项绑定
v-if和v-show的区别
- v-if 添加/删除dom节点
- 一般用于显隐切换不频繁的渲染节点
- v-show 使用display:none对元素进行显隐控制
- 一般用于显隐切换频繁的渲染节点
v-if和v-for优先级
答:
- vue2中:v-for优先级高,这是vue的源码决定的
- vue3中:v-if优先级高
原理
$nextTick原理
v-model双向绑定原理
scoped原理
给元素节点新增自定义属性:
1 | <div data-v-xxxx class="hello-box">Hello</div>> |
样式穿透
- scss/stylus中的样式穿透:
- 方式1:父元素 /deep/ 子元素
- 方式2:父元素 >>> 子元素
render函数
- render函数支持用js语言来构建dom
- vue是虚拟dom,拿到template模版也会转译成vnode函数
- 使用render构建dom,就直接省去了转译的过程
- 使用render时,使用h函数构建虚拟dom
axios二次封装
vue3相关
vue2和vue3区别
- 双向绑定机制不同
- vue2:Object.defineProperty()
- 使用循环对data返回的对象进行遍历,定义get和set
- 后添加的属性是劫持不到的
- 有些情况下需要使用$set进行数据刷新
- vue3:new Proxy()
- 使用new Proxy对响应式数据进行包裹
- 既不需要循环遍历,后添加的属性也可以被劫持到
- 不需要$set刷新
- vue2:Object.defineProperty()
1 | // vue2 双向绑定原理 |
1 | // vue3 双向绑定原理 |
- vue2是选项式API,vue3可以向下兼容,也可以是组合式api或者是setup语法糖的形式
- v-if和v-for的优先级不同了
- ref和$children也不同
- vue3使用Tree-shaking
- 打包时会对没有用到的api进行剔除,这样bundle的体积更小
- 创建app实例由new Vue()变为createApp
- suspense允许程序在等待异步组件时渲染一些后备内容
- 一步组件使用defineAsyncComponent进行加载
vue3使用setup怎么组织代码
答:可以使用hooks对复用逻辑进行封装
也就是组合式函数,用来封装和服用逻辑的函数:
1 | import {ref} from 'vue' |
vue3使用setup写如何获取类似vue2中的this
1 | let {proxy} = getCurrentInstance() |
vue3常用api有哪些
createApp
- 创建一个应用实例
- 相当于Vue2的new Vue()
- 使用场景:写插件/封装全局组件时会使用
provide/inject
- 依赖注入,跨辈传值
- 父组件传值到后代组件,跨越多个层级
- 不好维护和查询数据来源
directive
- 自定义指令
- 使用场景:后台管理系统中的按钮权限控制,根据用户角色决定按钮操作权限
mixin
- 全局混入
- 可以添加生命周期,在小程序的分享功能会用到
- 不好维护和查询数据来源
app.config.globalProperties
- 获取vue这个全局对象的属性和方法
- 自己封装插件的时候需要把方法添加到对象中
nextTick
- 等待下一次Dom更新刷新的工具方法
- 返回一个Promise,回调函数参数异步执行
computed
- 计算属性
- 可以缓存返回结果
reactive、ref
- 来定义数据和vue2的data类似
watch
- 监听(vue3不需要深度监听)
markRaw()
- 静态数据,不被new Proxy代理
defineProps()
- setup形式的组件,父组件传递的值,子组件需要使用defineProps接收
defineEmits()
- setup形式的组件,自定义事件需要使用defineEmits
slot
- 分为匿名、具名、作用域
- 后台管理项目,主内容部分根据菜单的选择进行切换,这时可以使用slot插槽
介绍一下vue3常见的响应式数据类型
- ref 定义基本类型
1 | const abc = ref(10) |
- reactive 定义引用类型
1 | const data = reactive({ |
- toRef 对reactive对象进行单个值解构处理
1 | const name = toRef(data,'name') |
- toRefs 对reactive对象进行全部解构处理
1 | const {name, age} = toRefs(data) |
介绍一下teleport组件及其使用场景
使用场景:希望指定组件渲染到根组件下,和app元素同级的位置(如弹窗)
1 | <teleport to="body"> |
介绍watch和watchEffect的区别
- watch
- 除非配置immediate,否则只有监听值发生变化才会执行
- 需要传递监听对象
- 能获取到更改前后的值
- 监听明确数据
- watchEffect
- 会立即执行
- 只需要传递回调函数
- 无法获取更改前的值
- 监听能访问到的所有响应式属性
介绍vue2和vue3中watch的区别
- vue3
- watch简化了语法,与setup函数结合更加紧密
- watch作为函数调用
- 能够监听对象的子属性,能通过数组组合监听
- vue2
- watch作为一个模块,在其中定义相应的监听事件
vue3生命周期
- setup
- 相当于vue2中的beforeCreate、created
- onBeforeMount
- onMounted
- onBeforeUpdate
- onUpdated
- onBeforeUnmount
- onUnmounted
- onActivated
- onDeactivated
- onErrorCaptured
项目开发相关
什么是渐进式框架
vuejs只是一个核心库,可以通过补充插件的方式将应用规模化,
比如通过补充vue-router实现路由跳转,
补充vuex实现状态集中管理等。
vue如何设置代理
答:vue.config.js,仅在开发环境下有效
1 | module.exports = { |
打包路径和路由模式(vue项目打包完之后出现空白页,如何解决)
答:
- 关于路径:
打包之后,资源默认根路径为绝对路径,需要修改为相对路径:
1 | module.exports = { |
关于路由模式
- 需要修改为hash模式,由前端控制路由跳转
关于模式和环境变量
- 开发环境 .env.development
- 生产环境 .env.production
什么是MVVM
- Model
- 数据
- View
- 界面中展示的内容
- View-Model
- 视图和数据的交互,也就是vue源码实现的功能
vue源码
模板解析
1 | <div id="app"> |
1 | class Vue{ |
生命周期
1 | <div id="app"> |
1 | class Vue{ |
添加事件
1 | <div id="app"> |
1 | class Vue{ |
data劫持
- vue大对象属性来自于data中
- data中属性与vue大对象属性保持双向(劫持)
1 | <div id="app"> |
1 | class Vue{ |
更新视图
- 通过一个监听表来存储对数据进行订阅的视图元素
1 | // 监听对象类型 |
v-model双向绑定原理
- 通过Object.defineProperty劫持数据发生的改变
- set中数据被改变,触发对数据进行监听的节点的update方法更新节点内容
在上面对vue模版进行解析的方法里,继续判断是否存在v-model属性:
1 | class Vue{ |
diff算法
- 功能:提升性能
- 虚拟dom:把dom数据化
- 通过h函数传入到vnode生成的数据结构
snabbdom
- 虚拟节点操作库
- 通过数据(虚拟dom)来操作dom
1 | /** |
节点替换规则:
- 没有用key进行标识的节点,每次都要删除后重新创建
- 即便是用key进行过标识,节点类型也需要相同
- 不能越级比较
手写生成虚拟dom的h函数
h函数(生成vnode对象):
1 | /** |
手写patch
调用形式:
1 | patch(oldVNode, newVNode) |
- patch执行之前,必须确保2个节点都是虚拟节点
- 如果旧节点不是虚拟节点,需要重新创建虚拟节点
- 判断新旧虚拟节点是同一个节点
- 如果不是,则暴力替换
- 判断新节点内容是文本还是子节点(不是同一个节点)
- 是文本,直接替换为文本内容
- 判断旧节点内容是文本还是子节点(新节点内容是子节点)
- 是文本,直接替换为新节点的子节点
- 非文本,需要使用diff核心算法
1 | export default function(oldVnode, newVnode){ |
diff算法
- diff算法是个深度优先算法
当patch中,需要对新旧元素的子节点进行比较替换时,就需要使用到核心diff算法,
diff算法首先在新旧节点数组头尾处分别设置指针,
然后按照下面顺序进行比较:
- 旧节点头指针和新节点头指针元素比较
- 如果匹配,新旧头指针后移
- 新旧尾指针比较
- 如果匹配,新旧尾指针前移
- 旧头指针和新尾指针比较
- 如果匹配,旧头指针后移,新尾指针前移
- 旧尾指针和新头指针比较
- 如果匹配,旧尾指针前移,新头指针后移
- 以上都不满足,进行查找
- 创建或删除
掘金回答
原贴
首先,我们拿到新旧节点的数组,然后初始化四个指针,分别指向新旧节点的开始位置和结束位置, 进行两两对比,
若是 新的开始节点和旧开始节点相同,则都向后面移动,
若是结尾节点相匹配,则都前移指针。
若是新开始节点和旧结尾节点匹配上了,则会将旧的结束节点移动到旧的开始节点前。
若是旧开始节点和新的结束节点相匹配,则会将旧开始节点移动到旧结束节点的后面。
若是上述节点都没配有匹配上,则会进行一个兜底逻辑的判断,判断开始节点是否在旧节点中,
若是存在则复用,若是不存在则创建。
最终跳出循环,进行裁剪或者新增,
若是旧的开始节点小于旧的结束节点,则会删除之间的节点,
反之则是新增新的开始节点到新的结束节点。