https://v3.cn.vuejs.org/guide/migration/introduction.html https://github.com/57code/vue-interview https://www.processon.com/view/link/620c4de01efad406e72b891f#map

01-组件通信方式

组件通信方式大体有以下8种:

props

单向数据流

实践:

  • 子组件可以在 data 里初始化 props 里的属性 或 使用 computed 转换
  • 注意引用类型的传递,子组件改变会影响父组件的数据,违反单向数据流

原理

先大概了解,后面在学

Vue 源码(三)Props原理
https://ustbhuangyi.github.io/vue-analysis/v2/reactive/props.html#props

$attrs/$listeners

非 Prop 的 attribute 会自动继承到子组件的根元素上,组件中可以设置inheritAttrs: false关闭自动继承。

注意 inheritAttrs: false 选项不会影响 style 和 class 的绑定,class 和 style 不属于 $attrs,它们仍然会被应用到组件的根元素中

可以通过v-bind="$attrs"传入内部组件,$listeners父级传递的(不含 .native ) v-on 事件,可以通过 v-on="$listeners" 传入内部组件

  1. <template>
  2. <label>
  3. <input type="text" v-bind="$attrs" v-on="$listeners" />
  4. </label>
  5. </template>
  6. <script>
  7. export default {
  8. inheritAttrs: false
  9. }
  10. </script>

Vue3的变化

  • 事件监听器现在只是以 on 为前缀的 attribute,这样它就成为了 $attrs 对象的一部分, $listeners 被移除。
  • $attrs 现在包含了所有传递给组件的 attribute,包括 class 和 style
    1. // id attribute and a v-on:close listener, the $attrs object will now look like this:
    2. {
    3. id: 'my-input',
    4. onClose: () => console.log('close Event triggered')
    5. }

    $on/$emit

    父组件向子组件传递的事件内部其实是使用$on实现的。 ```javascript // eventBus.js

const eventBus = new Vue() export default eventBus

// ChildComponent.vue import eventBus from ‘./eventBus’ export default { mounted() { // adding eventBus listener eventBus.$on(‘custom-event’, () => { console.log(‘Custom event triggered!’) }) }, beforeDestroy() { // removing eventBus listener eventBus.$off(‘custom-event’) } }

// ParentComponent.vue import eventBus from ‘./eventBus’ export default { methods: { callGlobalCustomEvent() { // if ChildComponent is mounted, we will have a message in the console eventBus.$emit(‘custom-event’) } } }

  1. **Vue3的变化**
  2. - 移除了 $on$off $once 方法。$emit 仍然包含于现有的 API 中,因为它用于触发由父组件声明式添加的事件处理函数
  3. - emits:和现有的 props 选项类似。这个选项可以用来声明一个组件可以向其父组件触发的事件。
  4. 事件总线模式可以被替换为使用外部的、实现了事件触发器接口的库,例如 mitt tiny-emitter
  5. > 根据具体情况来看,有多种事件总线的替代方案:
  6. > - Prop 和事件应该是父子组件之间沟通的首选。兄弟节点可以通过它们的父节点通信。
  7. > - Provide inject 允许一个组件与它的插槽内容进行通信。这对于总是一起使用的紧密耦合的组件非常有用。
  8. > - provide/inject 也能够用于组件之间的远距离通信。它可以帮助避免“prop 逐级透传”,即 prop 需要通过许多层级的组件传递下去,但这些组件本身可能并不需要那些 prop
  9. > - Prop 逐级透传也可以通过重构以使用插槽来避免。如果一个中间组件不需要某些 prop,那么表明它可能存在关注点分离的问题。在该类组件中使用 slot 可以允许父节点直接为它创建内容,因此 prop 可以被直接传递而不需要中间组件的参与。
  10. > - 全局状态管理,比如 Vuex
  11. <a name="QagOw"></a>
  12. ### $children/$parent
  13. ```vue
  14. <template>
  15. <div>
  16. <img alt="Vue logo" src="./assets/logo.png">
  17. <my-button>Change logo</my-button>
  18. </div>
  19. </template>
  20. <script>
  21. import MyButton from './MyButton'
  22. export default {
  23. components: {
  24. MyButton
  25. },
  26. mounted() {
  27. console.log(this.$children) // [VueComponent]
  28. }
  29. }
  30. </script>

$children 实例 property 已从 Vue 3.0 中移除,不再支持。建议使用 $refs

provide/inject

【面试】vue面试题 - 图1
父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。
provide 和 inject 绑定并不是可响应的,但是传递的数据可以是响应式的。


根据组件之间关系讨论组件通信最为清晰有效

  • ⽗⼦组件

props / $emit / $parent / ref / $attrs

  • 兄弟组件

$parent / $root / eventbus / vuex

  • 跨层级关系

eventbus / vuex / provide + inject

02-v-if 和 v-for

实践中不应该把 v-for 和 v-if 放⼀起
通常有两种情况下导致我们这样做:

  • 为了过滤列表中的项⽬ (⽐如v-for="user in users" v-if="user.isActive")。此时定义⼀个计算属性 (⽐如activeUsers),让其返回过滤后的列表即可(⽐如users.filter(u=>u.isActive) )。
  • 为了避免渲染本应该被隐藏的列表 (⽐如v-for="user in users" v-if="shouldShowUsers" )。此时把v-if 移动⾄容器元素上 (⽐如 ulol)或者外⾯包⼀层 template即可

vue2 中,v-for 的优先级是⾼于 v-if,把它们放在⼀起,输出的渲染函数中可以看出会先执⾏循环再判断条件,哪怕我们只渲染列表中⼀部分,也得在每次重渲染的时候遍历整个列表,这会⽐较浪费;

  1. // v-for="user in users" v-if="user.isActive"
  2. // 编译后类似于
  3. this.users.map(function (user) {
  4. if (user.isActive) {
  5. return user.name
  6. }
  7. })

另外需要注意的是在 vue3 中则完全相反,v-if 的优先级⾼于 v-for,所以 v-if 执⾏时,它调⽤的变量还不存在,就会导致异常。

03-生命周期

  1. 每个Vue组件实例被创建后都会经过⼀系列初始化步骤,⽐如,它需要数据观测,模板编译,挂载实例到dom上,以及数据变化时更新 dom。这个过程中会运⾏叫做⽣命周期钩⼦的函数,以便⽤户在特定阶段有机会添加他们⾃⼰的代码。

  2. Vue ⽣命周期总共可以分为8个阶段:创建前后, 挂载前后, 更新前后, 销毁前后,以及⼀些特殊场景的⽣命周期。

vue3 中新增了三个⽤于调试和服务端渲染场景

⽣命周期v3 ⽣命周期v3 描述
beforeCreate beforeCreate 组件实例被创建之初
created created 组件实例已经完全创建
beforeMount beforeMount 组件挂载之前
mounted mounted 组件挂载到实例上去之后
beforeUpdate beforeUpdate 组件数据发⽣变化,更新之前
updated updated 数据数据更新之后
beforeDestroy beforeUnmounted 组件实例销毁之前
destroyed unmounted 组件实例销毁之后
activated activated keep-alive 缓存的组件激活时
deactivated deactivated keep-alive 缓存的组件停⽤时调⽤
errorCaptured errorCaptured 捕获⼀个来⾃⼦孙组件的错误时被调⽤
renderTracked 调试钩⼦,响应式依赖被收集时调⽤
renderTriggered 调试钩⼦,响应式依赖被触发时调⽤
serverPrefetch ssr only,组件实例在服务器上被渲染前调⽤

setup 执行时机位于 beforeCreate 和 created 之间。

结合实践:

  • beforeCreate:通常⽤于插件开发中执⾏⼀些初始化任务
  • created:组件初始化完毕,可以访问各种数据,获取接⼝数据等
  • mounted:dom已创建,可⽤于获取访问数据和dom元素;访问⼦组件等
  • beforeUpdate:此时 view 层还未更新,可⽤于获取更新前各种状态
  • updated:完成 view 层的更新,更新后,所有状态已是最新
  • beforeunmounted:实例被销毁前调⽤,可⽤于⼀些定时器或订阅的取消
  • unmounted:销毁⼀个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

04-双向绑定

  1. vue 中双向绑定是⼀个指令 v-model ,可以绑定⼀个响应式数据到视图,同时视图中变化能改变该值。

  2. v-model 是语法糖,默认情况下相当于 :value 和 @input 。使⽤ v-model 可以减少⼤量繁琐的事件处理代
    码,提⾼开发效率。

  3. 通常在表单项上使⽤ v-model ,还可以在⾃定义组件上使⽤,表示某个值的输⼊和输出控制。

  4. 通过 <input v-model="xxx"> 的⽅式将 xxx 的值绑定到表单元素value上;

  • 对于 checkbox,可以使⽤true-valuefalse-value指定特殊的值
  • 对于 radio可以使⽤ value指定特殊的值
  • 对于 select 可以通过 options 元素的 value 设置特殊的值;

还可以结合.lazy,.number,.trimv-model 的⾏为做进⼀步限定;

v-model⽤在⾃定义组件上时⼜会有很⼤不同,vue3 中它类似于 sync修饰符,最终展开的结果是modelValue属性和update:modelValue事件;vue3 中我们甚⾄可以⽤参数形式指定多个不同的绑定,例如v-model:foov-model:bar,⾮常强⼤!

  1. <ChildComponent v-model="pageTitle" />
  2. <!-- 是以下的简写: -->
  3. <ChildComponent :value="pageTitle" @input="pageTitle = $event" />
  4. // 如果想要更改 prop 或事件名称,则需要在 ChildComponent 组件中添加 model 选项
  5. model: {
  6. prop: 'title',
  7. event: 'change'
  8. },
  9. <!-- 则变成: -->
  10. <ChildComponent :title="pageTitle" @change="pageTitle = $event" />
  11. <!-- 使用.sync -->
  12. <ChildComponent :title.sync="pageTitle" />
  13. <!-- 是以下的简写: -->
  14. <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
  15. // 子组件触发更新
  16. this.$emit('update:title', newValue)

vue3 用于自定义组件时,v-model prop 和事件默认名称已更改:

  • prop:value -> modelValue;
  • 事件:input -> update:modelValue;
  1. <ChildComponent v-model="pageTitle" />
  2. !-- 是以下的简写: -->
  3. <ChildComponent
  4. :modelValue="pageTitle"
  5. @update:modelValue="pageTitle = $event"
  6. />
  7. <!-- 指定prop: -->
  8. <ChildComponent v-model:title="pageTitle" />
  9. <!-- 是以下的简写: -->
  10. <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />

5.v-model 是⼀个指令,它的神奇魔法实际上是 vue 的编译器完成的。包含 v-model 的模板,转换为渲染函数之后,实际上还是value属性的绑定以及input事件监听,事件回调函数中会做相应变量更新操作。
v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 valueproperty 和 input 事件;
  • checkbox 和 radio 使用 checkedproperty 和 change事件;
  • select 字段将 value作为 prop 并将 change作为事件。

https://v3.cn.vuejs.org/guide/migration/v-model.htm

05-如何扩展⼀个组件

  1. 常⻅的组件扩展⽅法有:mixins,slots,extends等

  2. 混⼊mixins是分发 Vue 组件中可复⽤功能的⾮常灵活的⽅式。混⼊对象可以包含任意组件选项。当组件使⽤
    混⼊对象时,所有混⼊对象的选项将被混⼊该组件本身的选项。 ```javascript // 复⽤代码:它是⼀个配置对象,选项和组件⾥⾯⼀样 const mymixin = { methods: { dosomething(){} } }

// 全局混⼊:将混⼊对象传⼊ Vue.mixin(mymixin)

// 局部混⼊:做数组项设置到mixins选项,仅作⽤于当前组件 const Comp = { mixins: [mymixin] }

  1. 3. 插槽主要⽤于vue组件中的内容分发,也可以⽤于组件扩展。
  2. ```vue
  3. <!-- 子 -->
  4. <div>
  5. <slot>这个内容会被⽗组件传递的内容替换</slot>
  6. </div>
  7. <!-- 父 -->
  8. <div>
  9. <Child>来⾃⽼爹的内容</Child>
  10. </div>

如果要精确分发到不同位置可以使⽤具名插槽,如果要使⽤⼦组件中的数据可以使⽤作⽤域插槽。

  1. 组件选项中还有⼀个不太常⽤的选项 extends,也可以起到扩展组件的⽬的 ```javascript // 扩展对象 const myextends = { methods: { dosomething(){} } }

// 组件扩展:做数组项设置到 extends选项,仅作⽤于当前组件 // 跟混⼊的不同是它只能扩展单个对象 // 另外如果和混⼊发⽣冲突,该选项优先级较⾼,优先起作⽤ const Comp = { extends: myextends }

  1. > `Vue.extend(option)`使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
  2. <br />5. 混⼊的数据和⽅法不能明确判断来源且可能和当前组件内变量产⽣命名冲突,vue3 中引⼊的 composition api,可以很好解决这些问题,利⽤独立出来的响应式模块可以很方便的编写独⽴逻辑并提供响应式的数据,然后在 `setup`选项中组合使用,增强代码的可读性和维护性。例如
  3. ```javascript
  4. // 复⽤逻辑1
  5. function useXX() {}
  6. // 复⽤逻辑2
  7. function useYY() {}
  8. // 逻辑组合
  9. const Comp = {
  10. setup() {
  11. const {xx} = useXX()
  12. const {yy} = useYY()
  13. return {xx, yy}
  14. }
  15. }

06-Vue权限管理

路由权限和按钮权限

前端方案会把所有路由信息在前端配置,通过路由守卫要求⽤户登录,⽤户登录后根据⻆⾊过滤出路由表。⽐如我会配置⼀个 asyncRoutes 数组,需要认证的⻚⾯在其路由的 meta 中添加⼀个 roles字段,等获取⽤户⻆⾊之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该⽤户能访问的⻚⾯,最后通过router.addRoutes(accessRoutes)⽅式动态添加路由即可。

后端⽅案会把所有⻚⾯路由信息存在数据库中,⽤户登录的时候根据其⻆⾊查询得到其能访问的所有⻚⾯路由信息返回给前端,前端再通过 addRoutes 动态添加路由信息。

按钮权限的控制通常会实现⼀个指令,例如 v-permission ,将按钮要求⻆⾊通过值传给v-permission指令,在指令的 moutned 钩⼦中可以判断当前⽤户⻆⾊和按钮是否存在交集,有则保留按钮,⽆则移除按钮。

纯前端⽅案的优点是实现简单,不需要额外权限管理⻚⾯,但是维护起来问题⽐较⼤,有新的⻚⾯和⻆⾊需求就要修改前端代码重新打包部署;服务端⽅案就不存在这个问题,通过专⻔的⻆⾊和权限管理⻚⾯,配置⻚⾯和按钮权限信息到数据库,应⽤每次登陆时获取的都是最新的路由信息,可谓⼀劳永逸!

  1. / 前端组件名和组件映射表
  2. const map = {
  3. //xx: require('@/views/xx.vue').default // 同步的⽅式
  4. xx: () => import('@/views/xx.vue') // 异步的⽅式
  5. }
  6. // 服务端返回的asyncRoutes
  7. const asyncRoutes = [
  8. { path: '/xx', component: 'xx',... }
  9. ]
  10. // 遍历asyncRoutes,将component替换为map[component]
  11. function mapComponent(asyncRoutes) {
  12. asyncRoutes.forEach(route => {
  13. route.component = map[route.component];
  14. if(route.children) {
  15. route.children.map(child => mapComponent(child))
  16. }
  17. })
  18. }
  19. mapComponent(asyncRoutes)

https://github1s.com/PanJiaChen/vue-element-admin/blob/HEAD/src/permission.js#L40 https://github1s.com/PanJiaChen/vue-element-admin/blob/HEAD/src/store/modules/permission.js#L50-L51

07-Vue响应式理解

  1. 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。

  2. MVVM 框架中要解决的⼀个核⼼问题是连接数据层和视图层,通过数据驱动应⽤,数据变化,视图更新,要做
    到这点的就需要对数据做响应式处理,这样⼀旦数据发⽣变化就可以⽴即做出更新处理。

  3. 以 vue为例说明,通过数据响应式加上虚拟 DOM 和 patch 算法,开发⼈员只需要操作数据,关⼼业务,完全不⽤接触繁琐的DOM操作,从⽽⼤⼤提升开发效率,降低开发难度。

  4. vue2 中的数据响应式会根据数据类型来做不同处理,如果是对象则采⽤Object.defineProperty()的⽅式定义数据拦截,当数据被访问或发⽣变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的 7 个变更⽅法,使这些⽅法可以额外的做更新通知,从⽽作出响应。这种机制很好的解决了数据响应化的问题,但在实际使⽤中也存在⼀些缺点:⽐如初始化时的递归遍历会造成性能损失;新增或删除属性时需要⽤户Vue.set/delete这样特殊的api才能⽣效;对于es6中新产⽣的Map、Set这些数据结构不⽀持等问题。

  5. 为了解决这些问题,vue3重新编写了这⼀部分的实现:利⽤ ES6 的Proxy代理要响应化的数据,它有很多好
    处,编程体验是⼀致的,不需要使⽤特殊api,初始化性能和内存消耗都得到了⼤幅改善;另外由于响应化的实现代码抽取为独⽴的 reactivity 包,使得我们可以更灵活的使⽤它,第三⽅的扩展开发起来更加灵活了。

08-虚拟 DOM

  1. 虚拟dom顾名思义就是虚拟的dom对象,它本身就是⼀个 JavaScript 对象,只不过它是通过不同的属性去

描述⼀个视图结构。

  1. 通过引⼊vdom我们可以获得如下好处:
  • 将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从⽽提⾼程序性能
    • 直接操作 dom 是有限制的,⽐如:diff、clone 等操作,⼀个真实元素上有许多的内容,如果直接对其进⾏ diff 操作,会去额外 diff ⼀些没有必要的内容;同样的,如果需要进⾏ clone 那么需要将其全部内容进⾏复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了。
    • 操作 dom 是⽐较昂贵的操作,频繁的 dom 操作容易引起⻚⾯的重绘和回流,但是通过抽象 VNode 进⾏中间处理,可以有效减少直接操作 dom 的次数,从⽽减少⻚⾯重绘和回流。
  • ⽅便实现跨平台
    • 同⼀ VNode 节点可以渲染成不同平台上的对应的内容,⽐如:渲染在浏览器是 dom 元素节点,渲染在Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
    • Vue3 中允许开发者基于 VNode 实现⾃定义渲染器(renderer),以便于针对不同平台进⾏渲染
  1. vdom如何⽣成?

在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为render函数,在接下来的挂载(mount)过程中会调⽤ render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的 patch 过程中进⼀步转化为dom。image.png

  1. 挂载过程结束后,vue程序进⼊更新流程。如果某些响应式数据发⽣变化,将会引起组件重新 render,此时就会⽣成新的vdom,和上⼀次的渲染结果 diff 就能得到变化的地⽅,从⽽转换为最⼩量的dom操作,⾼效更新视图。

    vnode定义: https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L127-L128 观察渲染函数:21-vdom/test-render-v3.html 创建vnode: createElementBlock: https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L291-L292 createVnode: https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L486-L487 ⾸次调⽤时刻: https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiCreateApp.ts#L283-L284 mount: https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1171-L1172 调试mount过程:mountComponen

09-Diff

  1. Vue中的diff算法称为patching算法,它由Snabbdom修改⽽来,虚拟DOM要想转化为真实DOM就需要通过patch⽅法转换。

  2. 最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM和patching算法⽀

持,但是这样粒度过细导致Vue1.x⽆法承载较⼤应⽤;Vue 2.x中为了降低Watcher粒度,每个组件只有⼀个Watcher 与之对应,此时就需要引⼊ patching 算法才能精确找到发⽣变化的地⽅并⾼效更新。

  1. vue 中 diff 执⾏的时刻是组件内响应式数据变更触发实例执⾏其更新函数时,更新函数会再次执⾏ render 函数获得最新的虚拟DOM,然后执⾏ patch 函数,并传⼊新旧两次虚拟DOM,通过⽐对两者找到变化的地⽅,最后将其转化为对应的DOM操作。

  2. patch过程是⼀个递归过程,遵循深度优先、同层⽐较的策略;以vue3的patch为例:

  • ⾸先判断两个节点是否为相同同类节点,不同则删除重新创建
  • 如果双⽅都是⽂本则更新⽂本内容
  • 如果双⽅都是元素节点则递归更新⼦元素,同时更新元素属性
  • 更新⼦节点时⼜分了⼏种情况:
    • 新的⼦节点是⽂本,⽼的⼦节点是数组则清空,并设置⽂本;
    • 新的⼦节点是⽂本,⽼的⼦节点是⽂本则直接更新⽂本;
    • 新的⼦节点是数组,⽼的⼦节点是⽂本则清空⽂本,并创建新⼦节点数组中的⼦元素;
    • 新的⼦节点是数组,⽼的⼦节点也是数组,那么⽐较两组⼦节点,更新细节blabla
  1. vue3中引⼊的更新策略:编译期优化patchFlags、block 等

10-Vue3 新特性

也就是下⾯这些:

  • Composition API
  • SFC Composition API语法糖
  • Teleport传送⻔
  • Fragments⽚段
  • Emits选项
  • ⾃定义渲染器
  • SFC CSS变量
  • Suspense

以上这些是api相关,另外还有很多框架特性也不能落。

  1. api层⾯Vue3新特性主要包括:Composition API、SFC Composition API语法糖、Teleport传送⻔、

Fragments ⽚段、Emits选项、⾃定义渲染器、SFC CSS变量、Suspense

  1. 另外,Vue3.0在框架层⾯也有很多亮眼的改进:
  • 更快
    • 虚拟DOM重写
    • 编译器优化:静态提升、patchFlags、block等 (https://sfc.vuejs.org/)
    • 基于Proxy的响应式系统
  • 更⼩:更好的摇树优化
  • 更容易维护:TypeScript + 模块化
  • 更容易扩展
    • 独⽴的响应化模块
    • ⾃定义渲染器

11-动态路由

  1. 很多时候,我们需要将给定匹配模式的路由映射到同⼀个组件,这种情况就需要定义动态路由。
  2. 例如,我们可能有⼀个 User 组件,它应该对所有⽤户进⾏渲染,但⽤户 ID 不同。在 Vue Router 中,我们可以在路径中使⽤⼀个动态字段来实现,例如:{ path: '/users/:id', component: User } ,其中:id就是路径参数
  3. 路径参数 ⽤冒号 : 表示。当⼀个路由被匹配时,它的 params的值将在每个组件中以this.$route.params 的形式暴露出来。
  4. 参数还可以有多个,例如/users/:username/posts/:postId;除$route.params之外, $route 对象还公开了其他有⽤的信息,如 $route.query$route.hash 等。

如何响应动态路由参数的变化

  • 使用 watch监测 $route.params
  • 使用beforeRouteUpdate()导航守卫

12-实现 router

⼀个 SPA 应⽤的路由需要解决的问题是⻚⾯跳转内容改变同时不刷新,同时路由还需要以插件形式存在,所以:

  1. ⾸先我会定义⼀个 createRouter 函数,返回路由器实例,实例内部做⼏件事:
  • 保存⽤户传⼊的配置项
  • 监听 hash 或者 popstate 事件
  • 回调⾥根据 path匹配对应路由
  1. 将 router定义成⼀个 Vue 插件,即实现install⽅法,内部做两件事:
  • 实现两个全局组件:router-link和 router-view,分别实现⻚⾯跳转和内容显示
  • 定义两个全局变量:$route和$router,组件内可以访问当前路由和路由器实例

13-key 的作用

  1. key 的作⽤主要是为了更⾼效的更新虚拟DOM。
  2. vue在 patch 过程中判断两个节点是否是相同节点是 key 是⼀个必要条件,渲染⼀组列表时,key 往往是唯⼀标识,所以如果不定义 key 的话,vue 只能认为⽐较的两个节点是同⼀个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个 patch 过程⽐较低效,影响性能。
  3. 实际使⽤中在渲染⼀组列表时 key 必须设置,⽽且必须是唯⼀标识,应该避免使⽤数组索引作为 key,这可能导致⼀些隐蔽的 bug;vue 中在使⽤相同标签元素过渡切换时,也会使⽤ key 属性,其⽬的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性⽽不会触发过渡效果。
  4. 从源码中可以知道,vue 判断两个节点是否相同时主要判断两者的 key 和元素类型等,因此如果不设置 key,它的值就是 undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了⼤量的 dom更新操作,明显是不可取的。

14-nextTick

  1. nextTick 是⽤于获取下次DOM更新刷新的使⽤函数。
  2. Vue 有个异步更新策略,意思是如果数据变化,Vue 不会⽴刻更新DOM,⽽是开启⼀个队列,把组件更新函数保存在队列中,在同⼀事件循环中发⽣的所有数据变更会异步的批量更新。这⼀策略导致我们对数据的修改不会⽴刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使⽤ nextTick。
  3. 开发时,⽐如我希望获取列表更新后的⾼度就可以通过nextTick实现。
  4. nextTick 签名如下:function nextTick(callback?: () => void): Promise<void>所以我们只需要在传⼊的回调函数中访问最新DOM状态即可,或者我们可以await nextTick⽅法返回的 Promise之后做这事。
  5. 在Vue内部,nextTick 之所以能够让我们看到DOM更新后的结果,是因为我们传⼊的 callback 会被添加到队列刷新h函数(flushSchedulerQueue)的后⾯,这样等队列内部的更新函数都执⾏之后,所有DOM操作也就结束了,callback⾃然能够获取到最新的DOM值

15-watch/computed

  1. 计算属性可以从组件数据派⽣出新数据,最常⻅的使⽤⽅式是设置⼀个函数,返回计算之后的结果。computedmethods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执⾏副作⽤,常⻅⽤法是传递⼀个函数,执⾏副作⽤,watch 没有返回值,但可以执⾏异步操作等复杂逻辑。
  2. 计算属性常⽤场景是简化⾏内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。 侦听器常⽤场景是状态变化之后做⼀些额外的 DOM 操作或者异步操作。选择采⽤何⽤⽅案时⾸先看是否需要派⽣出新值,基本能⽤计算属性实现的⽅式⾸选计算属性。
  3. 使⽤过程中有⼀些细节,⽐如计算属性也是可以传递对象,成为既可读⼜可写的计算属性。watch 可以传递对象,设置deep、immediate等选项。
  4. vue3 中 watch 选项发⽣了⼀些变化,例如不再能侦测⼀个点操作符之外的字符串形式的表达式; reactivity API中新出现了watch、watchEffect 可以完全替代⽬前的 watch 选项,且功能更加强⼤。

16-Vue 子组件和父组件创建和挂载顺序

  1. 创建过程自上而下,挂载过程自下而上;即:
  • parent created
  • child created
  • child mounted
  • parent mounted
  1. 之所以会这样是因为 Vue 创建过程是⼀个递归过程,先创建⽗组件,有⼦组件就会创建⼦组件,因此创建时现有⽗组件再有⼦组件;⼦组件⾸次创建时会添加 mounted 钩⼦到队列,等到 patch 结束在执⾏它们,可见⼦组件的 mounted 钩⼦是先进⼊到队列中的,因此等到 patch 结束执⾏这些钩⼦时也先执⾏。

17-缓存组件

  1. 开发中缓存组件使⽤keep-alive组件,keep-alive 是vue内置组件,keep-alive 包裹动态组件 component时, 会缓存不活动的组件实例,⽽不是销毁它们,这样在组件切换过程中将状态保留在内存中,防⽌重复渲染DOM。

    1. <keep-alive>
    2. <component :is="view"></component>
    3. </keep-alive
  2. 结合属性 include 和 exclude 可以明确指定缓存哪些组件或排除缓存指定组件。vue3 中结合vue-router时变化较⼤,之前是 keep-alive 包裹 router-view ,现在需要反过来⽤ router-view 包裹 keep-alive :

    1. <router-view v-slot="{ Component }">
    2. <keep-alive>
    3. <component :is="Component"></component>
    4. </keep-alive>
    5. </router-view>
  3. 缓存后如果要获取数据,解决⽅案可以有以下两种:

  • beforeRouteEnter:在有 vue-router 的项⽬,每次进⼊路由的时候,都会执⾏ beforeRouteEnter

    1. beforeRouteEnter(to, from, next){
    2. next(vm=>{
    3. console.log(vm)
    4. // 每次进⼊路由执⾏
    5. vm.getData() // 获取数据
    6. })
    7. },
  • actived:在 keep-alive 缓存的组件被激活的时候,都会执⾏ actived 钩⼦

    1. activated(){
    2. this.getData() // 获取数据
    3. },
  1. keep-alive是⼀个通⽤组件,它内部定义了⼀个 map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的 component 组件对应组件的 vnode,如果该组件在map中存在就直接返回它。由于 component 的 is属性是个响应式数据,因此只要它变化,keep-alive 的 render 函数就会重新执⾏。

18-vue项⽬组织结构

  1. 从 0 创建⼀个项⽬我⼤致会做以下事情:项⽬构建、引⼊必要插件、代码规范、提交规范、常⽤库和组件
  2. ⽬前vue3项⽬我会⽤ vite 或者 create-vue 创建项⽬
  3. 接下来引⼊必要插件:路由插件 vue-router、状态管理 vuex/pinia、ui 库我⽐较喜欢 element-plus 和 antd-vue、http⼯ 具我会选 axios
  4. 其他⽐较常⽤的库有 vueuse,nprogress,图标可以使⽤ vite-svg-loader
  5. 下⾯是代码规范:结合 prettier 和 eslint 即可
  6. 最后是提交规范,可以使⽤ husky,lint-staged,commitlint
  7. ⽬录结构我有如下习惯:
  • .vscode :⽤来放项⽬中的 vscode 配置
  • plugins :⽤来放 vite 插件的 plugin 配置
  • public :⽤来放⼀些诸如页头icon 之类的公共⽂件,会被打包到dist根⽬录下
  • src :⽤来放项⽬代码⽂件
  • api :⽤来放 http 的⼀些接⼝配置
  • assets :⽤来放⼀些 CSS 之类的静态资源
  • components :⽤来放项⽬通⽤组件
  • layout :⽤来放项⽬的布局
  • router :⽤来放项⽬的路由配置
  • store :⽤来放状态管理 Pinia 的配置
  • utils :⽤来放项⽬中的⼯具⽅法类
  • views :⽤来放项⽬的⻚⾯⽂件

19-vue最佳实践

我从编码⻛格、性能、安全等⽅⾯说⼏条:

  1. 编码⻛格⽅⾯:
  • 命名组件时使⽤“多词”⻛格避免和HTML元素冲突
  • 使⽤“细节化”⽅式定义属性⽽不是只有⼀个属性名
  • 属性名声明时使⽤“驼峰命名”,模板或 jsx 中使⽤“⾁串命名”
  • 使⽤ v-for 时务必加上 key,且不要跟 v-if 写在⼀起
  1. 性能⽅⾯:
  • 路由懒加载减少应⽤尺⼨
  • 利⽤SSR减少⾸屏加载时间
  • 利⽤ v-once 渲染那些不需要更新的内容
  • ⼀些⻓列表可以利⽤虚拟滚动技术避免内存过度占⽤
  • 对于深层嵌套对象的⼤数组可以使⽤ shallowRef 或 shallowReactive 降低开销
  • 避免不必要的组件抽象
  1. 安全:
  • 不使⽤不可信模板,例如使⽤⽤户输⼊拼接模板:template: <div> + userProvidedString +</div>
  • 小心使⽤v-html,:url,:style等,避免html、url、样式等注⼊
  1. 等等…..

20-vuex

  1. Vuex 是⼀个专为 Vue.js 应⽤开发的状态管理模式 + 库。它采⽤集中式存储,管理应⽤的所有组件的状态,并以相应的规则保证状态以⼀种可预测的⽅式发⽣变化。

  2. 我们期待以⼀种简单的“单向数据流”的⽅式管理应⽤,即状态 -> 视图 -> 操作单向循环的⽅式。但当我们的应⽤遇到多个组件共享状态时,⽐如:多个视图依赖于同⼀状态或者来⾃不同视图的⾏为需要变更同⼀状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以⼀个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独⽴性,我们的代码将会变得更结构化且易维护。这是vuex存在的必要性,它和 react⽣态中的 redux 之类是⼀个概念。

  3. Vuex 解决状态管理的同时引⼊了不少概念:例如 state、mutation、action等,是否需要引⼊还需要根据应⽤的实际情况衡量⼀下:如果不打算开发⼤型单⻚应⽤,使⽤ Vuex 反⽽是繁琐冗余的,⼀个简单的 store 模式就⾜够了。但是,如果要构建⼀个中大型单⻚应⽤,Vuex 基本是标配。

21-template 到 render 处理过程

  1. Vue 中有个独特的编译器模块,称为“compiler”,它的主要作⽤是将⽤户编写的 template 编译为 js 中可执⾏的 render 函数。
  2. 之所以需要这个编译过程是为了便于前端程序员能⾼效的编写视图模板。相⽐⽽⾔,我们还是更愿意⽤HTML 来编写视图,直观且⾼效。⼿写 render 函数不仅效率底下,⽽且失去了编译期的优化能⼒。
  3. 在Vue 中编译器会先对 template 进⾏解析,这⼀步称为 parse,结束之后会得到⼀个 JS 对象,我们成为抽象语法树AST,然后是对 AST 进⾏深加⼯的转换过程,这⼀步成为 transform,最后将前⾯得到的 AST ⽣成为 JS 代 码,也就是 render 函数。

22-Vue实例挂载的过程中发⽣了什么?

  1. 挂载过程指的是app.mount()过程,这个过程中整体上做了两件事:初始化和建⽴更新机制
  2. 初始化会创建组件实例、初始化组件状态,创建各种响应式数据
  3. 建⽴更新机制这⼀步会⽴即执⾏⼀次组件更新函数,这会⾸次执⾏组件渲染函数并执⾏ patch 将前⾯获得 vnode 转换为 dom;同时⾸次执⾏渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系, 这使得以后数据变化时会执⾏对应的更新函数。