关于前端面试题
特性相关 Vue和React的区别 vue使用类似html的template模板语法,react使用jsx 组件通信方式不同vue组件中子组件传值给父组件,可以通过emit事件 react中,则需要父组件传入一个方法,由子组件调用 状态管理vue使用vuex/pinia进行状态管理 react使用的是redux 生命周期vue组件有生命周期钩子 react类组件可以使用生命周期,函数式组件一般使用hooks代替生命周期 diff算法的实现不同react使用基于层级的diff算法 vue使用的是双端比较diff算法
生命周期 生命周期有哪些?
beforeCreate 组件实例被创建之前
create 组件实例完全创建
beforeMount 组件挂载之前
mounted 组件挂载到实例上后
beforeUpdate 组件数据发生变化,更新之前
updated 组件数据更新之后
beforeDestroy 组件实例销毁之前
destroyed 组件实例销毁之后
activated keep-alive缓存的组件激活时
deactivated keep-alive缓存的组件停用时
errorCaptured 捕获一个来自子孙组件的错误时被调用
进入组件之后会执行哪些生命周期
beforeCreate 组件实例被创建之前
create 组件实例完全创建
beforeMount 组件挂载之前
mounted 组件挂载到实例上后
父组件引入子组件,生命周期执行顺序是?
父组件-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会执行哪些生命周期?
生命周期的使用场景有哪些? 答:
created
mounted
activated
destroyed
组件 keep-alive 答:
用途:缓存当前组件,提升性能,减少发送请求的数量
如果用户请求的页面组件和当前组件相同,不会发起新的请求
被keep-alive包裹的组件,再次进入时,仅会触发activated生命周期,不再执行前四个生命周期
组件传值有几种方式
父传子
方法1:父组件引用子组件,子组件使用props接收参数,缺点是父组件无法传值给孙子辈组件
不允许子组件直接修改prop传入的数据
需要通过事件或者回调的方式通知父组件,由父组件进行修改
方法2:子组件使用$parent直接获取父组件数据,孙子辈可以使用this.$parent.$parent获取,子组件可以直接修改父组件的数据
provide/inject进行依赖注入,父组件直接向子代组件注入数据
传入为基本数据类型时,inject接收值为副本,子组件修改数据不会影响组件/其它后代组件中的数据
传入为引用类型时,会影响
子传父
通过事件或者回调方式
通过this.$children[index].xxx,直接获取子组件实例中的数据
绑定ref,通过this.$refs.子组件ref名称
兄弟互传
1 2 3 4 5 import bus from '@/utils/bus.js' bus.$emit('change' ,'new-value' ) bus.$on('change' ,val => { console .log (val) })
子组件如何直接修改父组件的值
子组件如何找到父组件
如何找到根组件
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
modules
vuex使用state值
this.$store.state.xxx
mapState
vuex的getters值修改 vuex是单项数据流还是双向数据流 答:单向数据流 可以通过mutation进行修改
vuex的mutations和actions区别 1 2 3 4 5 6 7 8 9 10 mutations :{ add (state ){ state.number += 2 } }, actions :{ addNumber ({commit,state} ){ commit ('add' ) } }
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 2 3 4 5 6 7 8 9 10 11 { path :'/list' , name :"List" , children :[ { path :"/list:id" , name :"Detail" , component :()=> import ("../views/Detail.vue" ) } ] }
路由传值
1 2 3 4 5 6 7 this .$router .push ({ path :'/about' , query :{ a :1 } })
1 2 3 4 5 6 7 this .$router .push ({ path :'/about' , params :{ a :1 } })
导航故障
当前页跳转当前页的时候,会报导航故障错误
产生原因:在history模式下,使用html5的history api实现路由跳转,如果前端路由没有处理【同页面跳转】的情况,就可能导致导航故障
解决方法:重写VueRouter.prototype.push方法:
1 2 3 4 5 import VueRouter from 'vue-router' const routerPush = VueRouter .prototype .push VueRouter .prototype .push = function (location ){ return routerPush.call (this , location).catch (error => error) }
$router和$route的区别
导航守卫
全局守卫
beforeEach 路由进入之前
afterEach 路由进入之后
路由独享守卫
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:单页面应用
缺点:
router跳转和a标签跳转区别
a标签跳转回重新从服务器端请求资源,耗流量、耗性能
spa单页路由模式,router跳转只是加载中间的html代码,不会请求资源
api $set
$set(target,key,value)
1 2 3 this .$set(this .arr , '1' , 5 )
$nextTick
传入的回调函数会被异步执行
常用于执行组件渲染后的操作
是微任务,因为使用Promise.resolve实现
1 2 3 4 5 6 7 class Vue { $nextTick(callback){ return Promise .resolve ().then (()=> { callback () }) } }
$refs 获取dom
$el 获取当前组件的根节点
$data 获取当前组件data数据
$children 获取当前组件的所有子组件(数组格式)
$parent
$root 获取根组件
data定义数据 1 2 3 4 5 6 data ( ){ this .num = 222 return { str :'111' } }
使用this.xxx定义的数据,没有getter和setter劫持处理,单独修改时数据不会刷新
computed计算属性 computed计算的值可以被修改吗(包括v-model双向绑定)?
1 2 3 4 5 6 7 8 changeStr :{ get ( ){ return this .sre .slice (-2 ) }, set (val ){ this .sre = val } }
watch
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 2 3 4 5 6 <div data-v-xxxx class ="hello-box" > Hello</div > ><style > .hello-box [data-v-xxxx] { background :red } </style >
样式穿透
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刷新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let data = { a :1 , b :2 } let vue = {}for (let key in data){ Object .defineProperty (vue, key, { get ( ){ console .log ('数据被获取' ) return data[key] }, set (value ){ console .log ('数据被设置' ) data[key] = value } }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let data = { a :1 , b :2 } const vue = new Proxy (data, { get (target, propKey, receiver ){ console .log ('数据被获取' ) return Relect .get (targte, propKey, receiver) }, set (target, propKey, value, receiver ){ console .log ('数据被设置' ) return Reflect .set (target, propKey, value, receiver) } })
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 2 3 4 5 6 import {ref} from 'vue' function useSameLogic ( ){ let xxx = ref (123 ) return xxx } export default useSameLogic
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
watch
markRaw()
defineProps()
setup形式的组件,父组件传递的值,子组件需要使用defineProps接收
defineEmits()
setup形式的组件,自定义事件需要使用defineEmits
slot
分为匿名、具名、作用域
后台管理项目,主内容部分根据菜单的选择进行切换,这时可以使用slot插槽
介绍一下vue3常见的响应式数据类型
1 2 const abc = ref (10 )console .log (abc.value )
1 2 3 4 5 const data = reactive ({ name :123 , age :23 }) console .log (data.name )
toRef 对reactive对象进行单个值解构处理
1 const name = toRef (data,'name' )
toRefs 对reactive对象进行全部解构处理
1 const {name, age} = toRefs (data)
介绍一下teleport组件及其使用场景 使用场景:希望指定组件渲染到根组件下,和app元素同级的位置(如弹窗)
1 2 3 <teleport to="body" > <div > Hello</div > </teleport>
介绍watch和watchEffect的区别
watch
除非配置immediate,否则只有监听值发生变化才会执行
需要传递监听对象
能获取到更改前后的值
监听明确数据
watchEffect
会立即执行
只需要传递回调函数
无法获取更改前的值
监听能访问到的所有响应式属性
介绍vue2和vue3中watch的区别
vue3
watch简化了语法,与setup函数结合更加紧密
watch作为函数调用
能够监听对象的子属性,能通过数组组合监听
vue2
vue3生命周期
setup
相当于vue2中的beforeCreate、created
onBeforeMount
onMounted
onBeforeUpdate
onUpdated
onBeforeUnmount
onUnmounted
onActivated
onDeactivated
onErrorCaptured
项目开发相关 什么是渐进式框架 vuejs只是一个核心库,可以通过补充插件的方式将应用规模化, 比如通过补充vue-router实现路由跳转, 补充vuex实现状态集中管理等。
vue如何设置代理 答:vue.config.js,仅在开发环境下有效
1 2 3 4 5 module .exports = { devServer :{ proxy :'http://localhost:4000' } }
打包路径和路由模式(vue项目打包完之后出现空白页,如何解决) 答:
关于路径: 打包之后,资源默认根路径为绝对路径,需要修改为相对路径:
1 2 3 module .exports = { publicPath :'./' }
关于路由模式
关于模式和环境变量
开发环境 .env.development
生产环境 .env.production
什么是MVVM
vue源码 模板解析 1 2 3 4 5 6 7 8 9 10 11 12 13 <div id ="app" > <h1 > {{str}}</h1 > {{str}} </div > <script type ="text/javascript" > new Vue ({ el :'#app' , data :{ str :'hello' } }) </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Vue { constructor (options ){ this .$data = options.data this .$el = document .querySelector (options.el ) this .compile (this .$el ) } compile (node ){ node.childNodes .forEach ((item,index )=> { if (item.nodeType == 1 ){ this .compile (item) } if (item.nodeType == 3 ){ let reg = /\{\{(.*?)\}]|/g let text = item.textContent item.textContent = text.replace (reg,(match, vmKey )=> { vmKey = vmKey.trim () return this .$data [vmKey] }) } }) } }
生命周期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <div id ="app" > <h1 > {{str}}</h1 > {{str}} </div > <script type ="text/javascript" > new Vue ({ el :'#app' , data :{ str :'hello' }, beforeCreate ( ){}, created ( ){}, beforeMount ( ){}, mounted ( ){}, }) </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Vue { constructor (options ){ if (typeof options.beforeCreate == 'function' ){ options.beforeCreate .bind (this )() } this .$data = options.data if (typeof options.created == 'function' ){ options.created .bind (this )() } if (typeof options.beforeMount == 'function' ){ options.beforeMount .bind (this )() } this .$el = document .querySelector (options.el ) this .compile (this .$el ) if (typeof options.mounted == 'function' ){ options.mounted .bind (this )() } } }
添加事件 1 2 3 4 5 6 7 8 9 10 11 12 13 <div id ="app" > <button @click ="handleClick" > btn</button > </div > <script type ="text/javascript" > new Vue ({ el :'#app' , data :{}, methods :{ handleClick ( ){} } }) </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Vue { constructor (options ){ this .options = options this .$data = options.data this .$el = document .querySelector (options.el ) this .compile (this .$el ) } compile (node ){ if (item.nodeType == 1 ){ if (item.hasAttribute ('@click' )){ const event = item.getAttribute ('@click' ).trim () item.addEventListener ('click' ,()=> { this .options .method [event]() }) } } } }
data劫持
vue大对象属性来自于data中
data中属性与vue大对象属性保持双向(劫持)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <div id ="app" > <button @click ="handleClick" > {{str}}</button > </div > <script type ="text/javascript" > new Vue ({ el :'#app' , data :{ str :"hello" }, methods :{ handleClick ( ){ console .log (this .str ) } } }) </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Vue { constructor (options ){ this .options = options this .$data = options.data this .proxyData () this .$el = document .querySelector (options.el ) this .compile (this .$el ) } proxyData ( ){ for (let key in this .$data ){ Object .defineProperties (this ,key,{ get ( ){ return this .$data [key] }, set (value ){ this .$data [key] = value } }) } } }
更新视图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class Watch { constructor (vm, key, node, attr ){ this .vm = vm this .key = key this .node = node this .attr = attr } update ( ){ this .node [this .attr ] = this .vm [this .key ] } } class Vue { observer ( ){ for (let key in this .$data ){ let value = this .$data [key] let thiat = this Object .defineProperty (this .$data ,key,{ get ( ){ return value }, set (value ){ value = val if (that.$watchEvent [key]){ that.$watchEvent [key].forEach (item => { item.update () }) } } }) } } }
v-model双向绑定原理
通过Object.defineProperty劫持数据发生的改变
set中数据被改变,触发对数据进行监听的节点的update方法更新节点内容
在上面对vue模版进行解析的方法里,继续判断是否存在v-model属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Vue { compile ( ){ if (item.hasAttribute ('v-model' )){ let vmKey = item.getAttribute ('v-model' ) if (this .hasOwnProperty (vmKey)){ item.value = this [vmKey] } item.addEventListener ('input' ,(event )=> { this [vmKey] = item.value }) } } }
diff算法
snabbdom
虚拟节点操作库
通过数据(虚拟dom)来操作dom
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const vnode = h ('h1' ,{},'你好h1' )
节点替换规则:
没有用key进行标识的节点,每次都要删除后重新创建
即便是用key进行过标识,节点类型也需要相同
不能越级比较
手写生成虚拟dom的h函数 h函数(生成vnode对象):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export default function (sel, data, params ){ if ( typeof params == 'string' ){ return vnode (sel, data, undefined ,params,undefined ) }else if (Array .isArray (params)){ let children = [] for (let item of params){ children.push (item) } return vnode (sle,data,children,undefined ,undefined ) } }
手写patch 调用形式:
1 patch (oldVNode, newVNode)
patch执行之前,必须确保2个节点都是虚拟节点
如果旧节点不是虚拟节点,需要重新创建虚拟节点
判断新旧虚拟节点是同一个节点
如果不是,则暴力替换
判断新节点内容是文本还是子节点(不是同一个节点)
是文本,直接替换为文本内容
判断旧节点内容是文本还是子节点(新节点内容是子节点)
是文本,直接替换为新节点的子节点
非文本,需要使用diff核心算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 export default function (oldVnode, newVnode ){ if (oldVnode.sel == undefined ){ oldVnode = vnode ( oldVode.tagName .toLowerCase (), {},[],undefined ,oldVnode ) } if (oldVnode.sel === newVnode.sel ){ if (newVnode.children === undefined ){ if (newVnode.text !== oldVnode.text ){ oldVnode.elm .innerText = newVnode.text } }else { if (oldVnode.children !== undefined ){ }else { oldVnode.elm .innerHTML = '' for (let child of newVnode.children ){ let childDom = createElement (child) oldVnode.elm .appendChild (childDom) } } } }else { let newVnodeElm = createElement (newVnode) oldVnode.elm .parentNode .insert (newVnode) oldVnode.elm .parentNode .removeChild () } }
diff算法
当patch中,需要对新旧元素的子节点进行比较替换时,就需要使用到核心diff算法, diff算法首先在新旧节点数组头尾处分别设置指针, 然后按照下面顺序进行比较:
旧节点头指针和新节点头指针元素比较
新旧尾指针比较
旧头指针和新尾指针比较
旧尾指针和新头指针比较
以上都不满足,进行查找
创建或删除
掘金回答
原贴 首先,我们拿到新旧节点的数组,然后初始化四个指针,分别指向新旧节点的开始位置和结束位置, 进行两两对比, 若是 新的开始节点和旧开始节点相同,则都向后面移动, 若是结尾节点相匹配,则都前移指针。 若是新开始节点和旧结尾节点匹配上了,则会将旧的结束节点移动到旧的开始节点前。 若是旧开始节点和新的结束节点相匹配,则会将旧开始节点移动到旧结束节点的后面。 若是上述节点都没配有匹配上,则会进行一个兜底逻辑的判断,判断开始节点是否在旧节点中, 若是存在则复用,若是不存在则创建。 最终跳出循环,进行裁剪或者新增, 若是旧的开始节点小于旧的结束节点,则会删除之间的节点, 反之则是新增新的开始节点到新的结束节点。