生命周期这个词应该是很好理解的,在我们生活中就会常常碰到,比如谈到一个人的生命周期,我们会说人这一生会经历婴儿、儿童、少年、青年、中年、老年这几个阶段。而Vue的生命周期也是如此,在Vue中的每个组件都会经历从创建到挂载到更新再到销毁这几个阶段,而在这些阶段中,Vue会运行一种叫做生命周期钩子的函数,方便我们在特定的阶段有机会添加上我们自己的代码。
Vue生命周期总共可以分为8各阶段:创建前后、挂载前后、更新前后、销毁前后,以及一些特殊场景的生命周期(keep-alive激活时、捕获后代组件错误时)。Vue3中还新增了三个用于调试和服务端渲染场景。
这几个阶段对应的钩子函数API依次为:beforeCreatecreatebeforeMountmountedbeforeUpdateupdatedactivated(keep-alive激活时调用)deactivated(keep-alive停用时调用)beforeDestorydestoryederrorCaptured(捕获子孙组件错误时调用)。
在Vue3中的变化绝大多数只要加上前缀on即可,比如mounted变为onMounted,除了beforeDestroy和destroyed被重新命名为beforeUnmount和unMounted(这样与前面的beforeMount和mounted对应,强迫症表示很赞)
beforeCreate在组件创建前调用,通常用于插件开发中执行一些初始化任务;created组件创建完毕调用,可以访问各种数据,请求接口数据等;mounted组件挂载时调用可以访问数据、dom元素、子组件等;beforeUpdate更新前调用此时view层还未更新,可用于获取更新前的各种状态;updated完成更新时调用此时view层已经完成更新,所有状态已经是最新的了;beforeUnmount实例被销毁前调用,可用于一些定时器或订阅的取消;unMounted销毁一个实例时调用可以清理与其他实例的链接,解绑它的全部指令以及事件监听器。
在Vue3中:setup是比created先执行的;而且没有beforeCreate和created。
权限管理一般需求就是对页面权限和按钮权限的管理
按钮权限的控制通常会实现一个指令,例如v-permission,将按钮要求角色通过值传给v-permission指令,在指令的mounted钩子中可以判断当前用户角色和按钮是否存在交集,有就保留按钮,没有就移除按钮。
纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码和重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息。
回答思路:
什么是双向绑定?
双向绑定的好处?
在什么地方使用双向绑定?
双向绑定的使用方式、使用细节、Vue3中的变化
原理实现描述
回答:
Vue中的双向绑定是一个指令v-model,它可以绑定一个响应式数据到视图,同时视图中变化也能改变该值。
v-model是一个语法糖,它的原理(默认请情况下)就是通过:value将变量挂到dom上,再通过input事件监听dom的变化改变变量的值。使用v-model的好处就是方便呀,减少了大量的繁琐的事件处理,提高开发效率。
通常在表单上使用v-model,还可以在自定义组件上使用,表示某个值得输入和输出控制。
可以结合修饰符做进一步限定(lazy/number/trim),用在自定义组件上时有些不同,它相当于是给了子组件一个modelValue的属性和update:modelValue的事件;在Vue3中还可以用参数形式指定多个不同的绑定,如v-model:foo这个时候就相当于给了子组件一个foo的属性和update:foo的事件。
v-model作为一个指令,它的原理就是Vue编译器会把它转换成value属性绑定和input的监听事件,上面说过是默认情况下,实际上编译器会根据表单元素的不同分配不同的事件,比如checkbox和radio类型的input会转换为checked和change事件。
Vue组件之间通信有以下这么几种:
props
$emit、$on、$off、$once(后三者在Vue3中已被废除)
$children(Vue3中废除)、$parent
$attrs、$listeners(Vue3中废除)
ref
$root
eventbus(Vue3中不好使了,需要自己封装)
vuex、pinia
provide+inject
以上的方法长按使用场景可以分为:
父子组件之间可以使用
props/$emit/$parent/ref/$attrs
兄弟组件之间可以使用
$parent/$root/eventbus/vuex
跨层及组件之间可以使用
eventbus/vuexpinia/provide+inject
路由懒加载:有效拆分App尺寸,访问时才异步加载
v-for遍历避免同时使用v-if(实际上这在Vue3中是错误的写法)
v-once和v-memo:不再变化的数据使用v-once;按条件跳过更新时使用v-memo
长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容。一些开源库(vue-virtual-scroller/vue-virtual-scroll-grid)
事件的销毁:Vue组件销毁时,会自动解绑它的全部指令以及事件监听器,但是仅限于组件本身的事件。
图片懒加载,自定义v-lazy指令(参考项目:vue-lazyload)
第三方插件按需引入element-plus避免体积太大
子组件分割策略:较重的状态组件适合拆分
SSR服务端渲染解决首屏渲染慢的问题
思路:
刷新后Vuex状态为什么会丢失?
解决方法
第三方库以及原理探讨
个人理解
因为Vuex只是在内存中保存状态,刷新后就会丢失,如果要持久化就要存起来。
可以是用localStorage存储Vuex的状态,store中把值取出来作为state的初始值,提交mutation的时候就存入localStorage。
可以用vuex-persist、vuex-persistedstate这种插件,可以通过插件选项控制哪些需要持久化。内部的原理就是通过订阅mutation变化做统一处理。
这里有两个问题,一是如果用户手动改了localStorage怎么办?那我Vuex里的状态不是也改变了?二是由于localStorageAPI的原因只能存储字符串,所以我们只能将数据通过JSON.stringify转换为字符串,而当我们存储的数据为Map、Set、Function这种引用类型的数据时,JSON.stringify转换后会变味{}而丢失。
对应第一个问题我的解决方法是可以通过监听storage事件来清除数据
属性拦截的几种方式
defineProperty的问题
Proxy的优点
其他考量
JS中做属性拦截常见的方式有三种:defineProperty、getter/setters和Proxy
以上两点在Proxy出现后迎刃而解,不仅可以对数组实现拦截,还能对Map、Set实现拦截;另外Proxy的拦截也是懒处理行为,如果用户没有访问嵌套对象,那么也不会实施拦截,这就让初始化的速度和内存占用改善了。
Proxy有兼容性问题,完全不支持IE
必要性
何时用
怎么用
使用细节
当打包构建时,Javascript抱回变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应最贱,这样更加高效,是一种优化手段。
一般来说,对于所有的路由都使用动态导入是个好主意
给component选项配置一个返回Promise组件的函数就可以定义懒加载路由.例如:
Vue-Router有三个模式,其中history和hash更为常用。两者差别主要在显示形式和部署上,
hash模式在地址栏现实的时候有一个#,这种方式使用和部署都较简单;history模式url看起来更优雅没关,但是应用在部署时需要做特殊配置,web服务器需要做回退处理,否则会出现刷新页面404的问题。
在实现上hash模式是监听hashchange事件触发路由跳转,history模式是监听popstate事件触发路由跳转。
在Vue中nextTick是等待下一次DOM更新刷新的工具方法。
nextTick的原理:在Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取最新的DOM值。
先回答答案:在vue2中,v-for的优先级更高但是在vue3中,v-if的优先级更高
拓展:无论什么时候,我们都不应该把v-for和v-if放在一起,怎么解决呢?一是可以定义一个计算属性,让v-for遍历计算属性。二是可以把if移到内部容器里(ulol)或者把v-for移植外部容器(template)中
watch
store.subscribe()
watch方式,可以以字符串形式监听$store.state.xx;subscribe方法参数是一个回调函数,回调函数接受mutation对象和state对象,可以通过mutation.type判断监听的目标。wtach方法更简单好用,subscribe会略繁琐,一般用vuex插件中(可以提一下vuex的持久化插件vuex-persist、vuex-persistedstate)
不支持持久化,页面刷新状态就会丢失
使用模块比较繁琐
不支持ts(或者说很不友好)
vue3+pinia会是更好的组合。
两者都能返回响应式对象,ref返回的是一个响应式Ref对象,reactive返回的是响应式代理对象。
ref通常是处理单值得响应式,reactive用于处理对象类型的数据响应式
ref需要通过.value访问,在视图中会自动脱ref,不需要.value,ref可以接收对象或数组但内部依然是reactive实现的;reactive如果接收Ref对象会自动脱ref;使用展开运算符展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象后再展开。
reactive内部使用Prxoy代理拦截对象各种操作,而ref内部封装一个RefImpl类,设置getvalue/setvalue,拦截用户对值得访问。
逻辑扩展:mixins、extends、compositionapi:
内容扩展:slots
mixins很灵活,但是会冲突很混乱。extends是一个不太常用的选项,更mixins的不同是它只能扩展单个对象,优先级比mixins高。
扩展:Vue.mixin(全局混入)Vue.extend(有点像是类/组件的继承创建一个子类)
vue-loader是用于处理单文件组件(SFC)的webpackloader
因为有了vue-loader,我们才能用.vue文件形式编写代码,将代码分割为templatescriptstyle
webpack在打包的时候,会以loader的方式调用vue-loader
vue-loader被执行时,它会对SFC中的每个语言块用单独的loader链处理,最后将这些单独的块装配成最终的组件模块
不能直接改。
组件化开发中有一个单向数据流原则,不在子组件修改父组件数据是个常识
如果你确实需要改,请通过emit向父组件发送一个事件,在父组件中修改
我么可以在路径中使用一个动态字段来实现,例如/users/:id其中:id就是路径参数。可以通过this.$route.parmas获取,参数还可以有多个,$route对象还公开了其他有用的信息如queryhash等。
什么是响应式?
为什么vue需要响应式?
有什么好处?
vue的响应式怎么实现的,有哪些优缺点?
vue3中的响应式的新变化
数据响应式就是能够监测到数据变化并且做出响应的一种机制
在vue中要解决的一个核心问题就是连接数据层和视图层,通过数据变化驱动视图更新,要做到这点就需要对数据做响应式处理。
通过数据响应式加上虚拟DOM和patch算法,我们只需要操作数据,关心业务,完全不需要接触繁琐的DOM操作,打打提升了开发效率,降低开发难度。
vue2中实现数据响应式的核心就是通过Object.defineProperty()方法对数据进行拦截,当get数据时做依赖收集set数据时做更新通知。这种机制很好的及绝了数据响应式的问题,但是实际使用也存在缺点,比如在初始化时的递归遍历会造成性能损失;无法监听新增或删除属性,在vue中要通过像Vue.set/delete这种特定的API才能实现对对象数组属性的添加和删除,而且也不支持Ma、Set这些数据结构,
为了解决这些问题,Vue3重写了这部分实现,利用的是ES6中的Proxy代理要响应化的数据。它有很多好处,初始化性能和内存都大幅改善,也不需要特殊的API,但是不支持IE浏览器。
问template到render的过程其实是问的vue编译器工作原理。
引入编译器概念
说明编译器的必要性
阐述编译器工作流程
Vue中有个独特的编译模块,称为compiler,它的主要作用是将template编译为js可执行的render函数
之所以需要这个编译过程是为了便于我们高校的编写试图模版。相比而言,我们还是更愿意用HTML来编写视图,直观且高效。手写render函数不仅效率低下,而且失去了被编译器的优化能力。
Vue编译器首先会对template进行解析(Parse),结束后会得到一个抽象语法树AST,然后对AST进行深加工转换(transform),最后将得到的AST生成为js代码,也就是render函数
缓存组件可以使用keep-alive组件,include和exclude可以指定包含不包含哪些组件。
Vue3结合vue-router使用变化非常大,之前是keep-alive包含router-view,现在是router-view包含keep-alive
缓存后如果想要获取数据可以使用actived钩子或者beforeRouteEnter(vue-router的一个守卫)
keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果改组件在map中存在就直接返回它。由于component的is属性是一个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行。
虚拟DOM是什么?虚拟DOM的本质就是一个Javascript对象。
为什么要引入虚拟DOM?(好处)它能有效减少操作DOM的次数,方便实现跨平台
虚拟DOM如何生成?compiler编译器会把template模版编译成渲染函数,接下来在mount挂载的过程会调用这个渲染函数,返回的对象就是虚拟DOM。挂载结束后,会进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时会生成新的虚拟DOM,和上次渲染结果做diff操作,最小量的操作dom,从而高效更新视图。
异步组件就是不会立即加载而是会在需要的时候加载的组件。在大型应用中,我们需要分割代码为更小的块试就可以用异步组件。
不仅可以在路由切换时懒加载组件,还可以在组件中使用异步组件,从而更细的分割代码。
使用异步组件最简单的方式是直接给defineAsyncComponet指定一个loader函数,结合ES模块动态导入函数import可以快速实现。Vue3还可以结合Suspense组件使用异步组件。
异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是Vue框架,处理路由组件加载的是vue-router。但是可以在懒加载的路由组件中使用异步组件。
computed是计算属性,watch是侦听器。
computed通常用于处理模版中复杂的逻辑,而watch通常用于需要监听一个响应式对象的变化而做一些操作的时候
watch可以进行异步操作,computed不行。
计算属性传递一个对象有set和get两个选项,是它称为即可读又可写的计算属性,如果传递的是函数的话默认就是get选项,watch可以传递一个对象,设置deep、immediate等选项
vue3中watch发生了一些变化,例如不能再侦测一个点操符之外的字符串表达式,reactivityAPI中新出的watch、watchEffect可以完全替代watch选项,而且功能更加强大
SPA(SinglePageApplication)是单页面应用。一般也称为客户端渲染,简称CSR。SSR(ServerSideRender)即服务端渲染。一般也称为多页面应用(MulpilePageApplication),简称MPA。
选择上,如果有首屏加载优化需求,SEO需求时,就可以考虑SSR。
但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR反而浪费资源,我们可以考虑预渲染的方案。另外nuxt.js/next.js中给我们提供了SSG静态网站生成方案也是很好的静态站点解决方案,结合一些CI手段,可以起到很好的优化效果。
diff算法是干什么的?
何时执行
具体执行方式
拔高:说一下vue3中的优化
Vue中的diff算法称为patching算法,虚拟DOM要想转化为真实DOM就需要通过patch方法转换。
最初Vue1.x视图中农每个依赖均有更新函数对应,可以做到精确更新,因此不需要虚拟DOM和patching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新。
vue中diff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,对比新旧虚拟DOM,将其转化为对应的DOM操作。
patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3的patch为例:
vue3中引入的更新策略:编译期优化patchFlags、block等
从0创建项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件
目前vue3项目我会用vite或者create-vue创建项目
接下来引入必要插件:vue-router、vuex/pinia、element-plus、antd-vue、axios等等
其他常用的库有像lodash、dayjs、nprogress等等..
下面是代码规范:editorconfig、prettier、eslint
最后是提交规范,可以使用husky、Commitizen
目录结构我喜欢按照下面的结构来
首先我会定义一个createRouter函数,返回路由器实例,实例内部做几件事;
将router定义成一个Vue插件,即实现install方法,内部做两件事:
在项目规模变大的之后,单独一个store对象会过于庞大臃肿,此时通过模块方式可以拆分来便于维护
可以按之前规则单独编写资规模代码,然后在主文件中通过modules选项组织起来:createStore({modules:{...}})
使用时需要注意访问子模块状态时需要加上注册模块名。但同时getters、mutations和actions又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子模块加上namespace选项,此时再访问它们就要加上命名空间前缀。
模块的方式可以拆分代码,但是缺点也很明显,使用起来比较繁琐,容易出错,而且类型系统支持很差,不能给我们带来帮助。pinia显然在这方面有了很大改进,是时候切换过去了。
vue2中组件确实只能有一个跟,但vue3中组件已经可以多根组件了。
之所以需要这样是因为vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom,自然应该满足这个要求。
vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件时多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来pathch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新。
v-once是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。
如果我们有一些元素或者组件再初始化渲染之后不再需要变化,这种情况下适合使用v-once,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段。
我们只需要作用的组件或元素上加上v-once即可。
补充:
vue3.2之后,又增加了v-memo,这个指令可以有条件的缓存模板并控制他们的更新。
v-once的原理:编译器发现有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算。
如果你说不出来,可以直接举例子。当我开发一个页面时,如果需要显示一个顶部导航栏,通过导航栏跳转到不同的页面,而顶部的导航栏又必须要在每个页面显示时,就可以使用嵌套路由;还可以举例,当我需要查看某个列表的详情页面时,往往需要嵌套路由(detail/:id)
watch方式,可以以字符串形式监听$store.state.xx;subscribe方法参数是一个回调函数,回调函数接受mutation对象和state对象,可以通过mutation.type判断监听的目标。wtach方法更简单好用,subscribe会略繁琐,一般
挂载实例的过程就是app.mount()的过程,整体上就做了两件事:初始化和建立更新机制
初始化会创建组件实例、初始化组件状态、创建各种响应式数据
简历更新机制这一步会立即执行一次组件更新函数,这会首次执行渲染函数并执行patch将前面获得vnode转换为dom;同时会创建它内部响应式数据和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。
key的作用主要是为了更高效的更新虚拟DOM。
key是vue在patch过程中判断两个节点是否是相同节点的关键条件(另一个是元素类型),如果不设置key,它的值就是undefined,vue则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。
实际使用的过程中必须设置key,而且应该尽量避免使用数组索引,这可能导致一些隐藏bug。
watchEffect立即运行函数,被动地追踪它的依赖,传入的函数即是依赖收集的数据源,也是回调函数;watch侦测一个或多个响应式数据源,在数据源变化时调用一个回调函数,通过immediate选项也可以设置立即执行一次。
watchEffect是一种特殊的watch。如果不关心响应式数据前后的值,可以使用watchEffect。其他情况都可以用watch。
parentcreated->childcreated->childmounted->parentmounted
原因:Vue创建是一个递归的过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加Mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是选进入到队列中的,因此等到patch结束执行这些钩子时也先执行。
vuex是一个专门为vue应用开发的状态管理模式库,
当你遇到多个组件共享状态时或者项目中的组件难以管理的时候就可以使用vuex,它以一个全局单例模式管理全局的状态。
基本核心概念有state、mutation、action、getters、module等
说些使用过程的感受ts不友好模块使用繁琐页面刷新数据也会消失
如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
类似Tree、Menu这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
使用自定义指令分为定义、注册、和使用
定义有两种方式,对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在mounted和updated时执行
注册:可以使用app.directive全局注册也可以通过选项局部注册
使用时在注册名称前加上v-即可。
v-copy复制粘贴
v-lazy图片懒加载
v-debounce防抖
v-permission按钮权限
v-longpress长按
API层面
CompositionAPI
setup语法糖
Teleport传送门
Fragments可以多个根节点
Emits
createRenderer自定义渲染器
SFC状态驱动css变量(v-bindin
此外,Vue3在框架层面也有很多两点和改进
最大设计目标就是替代Vue2,为了实现这一点,Vue3在以下几个方面做了很大改进,如:易用性,框架性能、扩展性、可维护性、开发体验等
易用性方面:主要有API简化v-model变成了v-model和sync修饰符的结合体。类似的还有h(type,props,children)函数中的props不用考虑区分属性、特性、事件等,框架替我们判断,易用性增。
开发体验方面:新组建TeleportFragmentSuspense等都会简化特定场景的代码编写。setup语法糖更是极大提升了我们的开发体验。
扩展性方面提升:如独独立的reactivity模块,customrenderAPI等
可维护性方面主要是CompositionAPI,更容易编写高复用性的业务逻辑。还有对TS支持的提升。
性能方面:编译器优化、基于Proxy的响应式系统。
。。。
代码方面:全新的响应式API,基于Proxy实现,初始化事件和内存占用均大幅改进;
编译方面:做了更多编译优化处理,比如静态提升、动态内容标记、事件缓存、区块等,可以有效跳过大量diff过程
打包方面:更好的支持tree-shaking,因此体积更小,加载更快.(因为vue3所有的API都通过ES6模块化的方式引入,这样就能让webpack或rollup等打包工具在打包时对没有用到API进行剔除,最小化bundle体积)
$attrs获取没有在props中定义的属性,v-bind="$attrs"可以用于属性透传$listeners用于获取事件,vue3中已经移除合并到attrs中,使用起来更方便
CompositionAPI更简洁、逻辑复用更高效。解决的过去OptionsAPI中mixins的各种缺点(会冲突很混乱);另外CompositionAPI更自由,没有OptionsAPI那样固定的写法,并且可以更有效的将逻辑代码组织在一起,而不用东一块西一块搞得很混乱,最后CompositionAPI拥有更好的类型推断,对ts支持友好。
编码风格方面:
组件命名时使用多词风格避免和html元素冲突
属性名峰命名,模板或jsx中使用肉串命名
v-for务必加上key且不要和v-if写在一起‘’
性能方面:
路由懒加载减少应用尺寸
SSR减少首屏加载事件
v-oncev-memo
长列表虚拟滚动技术
对于深层嵌套对象的大数据可以使用shallowRef或shallowReactive降低开销
避免不必要的组件抽象
mutation用于修改stateaction用于提交一个mutation,而且action可以包含异步操作
要实现一个Store存储全局状态
要提供修改状态所需的API:commit({type,payload}),dispatch(type,payload)
实现Store,可以定义Store类,构造函数接受选项options,设置属性state对外暴露状态,提供commit和dispatch修改属性。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件(install方法)。
commit可以获取用户传入mutations并执行它,这样可以按用户提供的方法修改状态,dispatch类似,但是dispatch需要返回一个Promise给用户用于处理异步结果。