Skip to content

Vue 系列最新 22 道面试题

<font style="background:#F8CED3;color:#70000D">Vue</font>

1、Vue3.0 性能提升主要是体现在哪些方面

1、响应式系统

Vue.js 2.x 中响应式系统的核心是 Object.defineProperty,劫持整个对象,然后进行深度遍历所有属性,给每个属性添加 getter 和 setter,实现响应式

Vue.js 3.x 中使用 Proxy 对象重写响应式系统

  • 可以监听动态新增的属性
  • 可以监听删除的属性
  • 可以监听数组的索引和 length 属性

实现原理:

通过 Proxy(代理):拦截对象中任意属性的变化,包括:属性值的读写、属性的添加、属性的删除等。

通过 Reflect(反射):对源对象的属性进行操作。

MDN 文档中描述的 Proxy 与 Reflect:

Proxy:[Proxy - JavaScript | MDN](<https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy&gt; "Proxy - JavaScript | MDN")

Reflect:[Reflect - JavaScript | MDN](<https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect&gt; "Reflect - JavaScript | MDN")

javascript
new Proxy(data, {
  // 拦截读取属性值
  get (target, prop) {
    return Reflect.get(target, prop)
  },
  // 拦截设置属性值或添加新属性
  set (target, prop, value) {
    return Reflect.set(target, prop, value)
  },
  // 拦截删除属性
  deleteProperty (target, prop) {
    return Reflect.deleteProperty(target, prop)
  }
})

proxy.name = 'tom'

2、编译阶段

Vue.js 2.x 通过标记静态节点,优化 diff 的过程

Vue.js 3.x

  • Vue.js 3.x 中标记和提升所有的静态节点,diff 的时候只需要对比动态节点内容;
  • Fragments(升级 vetur 插件):template 中不需要唯一根节点,可以直接放文本或者同级标签
  • 静态提升 (hoistStatic),当使用 hoistStatic 时,所有静态的节点都被提升到 render 方法之外。只会在应用启动的时候被创建一次,之后使用只需要应用提取的静态节点,随着每次的渲染被不停的复用。
  • patch flag,在动态标签末尾加上相应的标记,只能带 patchFlag 的节点才被认为是动态的元素,会被追踪属性的修改,能快速的找到动态节点,而不用逐个逐层遍历,提高了虚拟 dom diff 的性能。
  • 缓存事件处理函数 cacheHandler,避免每次触发都要重新生成全新的 function 去更新之前的函数

3、源码体积

  • 相比 Vue2,Vue3 整体体积变小了,除了移出一些不常用的 API
  • tree shanking
    • 任何一个函数,如 refreavtivedcomputed 等,仅仅在用到的时候才打包
    • 通过编译阶段的静态分析,找到没有引入的模块并打上标记,将这些模块都给摇

2、Vue3 有哪些新的组件

1、Fragment

在 Vue2 中:组件必须有一个根标签

在 Vue3 中:组件可以没有根标签,内部会将多个标签包含在一个 Fragment 虚拟元素中

好处:减少标签层级,减小内存占

2、Teleport

什么是 Teleport?—— Teleport 是一种能够将组件 html 结构移动到指定位置的技术。

vue
<teleport to="移动位置">
    
        
            <h3>我是一个弹窗</h3>
            <button @click="isShow = false">关闭弹窗</button>
        
    
</teleport>

3、Suspense

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
  • 使用步骤:
    • 异步引入组件
javascript
import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./components/Child.vue')

使用 Suspense 包裹组件,并配置好 default 与 fallback

vue
<template>
    
        <h3>我是App组件</h3>
        <Suspense>
            <template v-slot:default>
                <Child/>
            </template>
            <template v-slot:fallback>
                <h3>加载中.....</h3>
            </template>
        </Suspense>
    
</template>

3、Vue2.0 和 Vue3.0 有什么区别

  1. 响应式系统的重新配置,使用 proxy 替换 Object.defineProperty
  2. typescript 支持
  3. 新增组合 API,更好的逻辑重用和代码组织
  4. v-if 和 v-for 的优先级
  5. 静态元素提升
  6. 虚拟节点静态标记
  7. 生命周期变化
  8. 打包体积优化
  9. ssr 渲染性能提升
  10. 支持多个根节点

4、Vue 生命周期

1、Vue2.x 的生命周期

1679578524996-ad02fce7-bb00-4cf4-b0f0-c5960ecc5e33.png

2、Vue3.0 的生命周期

1679578525022-1bd67863-b198-4393-be76-f82047d2fcc5.png

Vue3.0 中可以继续使用 Vue2.x 中的生命周期钩子,但有有两个被更名:

  • beforeDestroy 改名为 beforeUnmount
  • destroyed 改名为 unmounted

Vue3.0 也提供了 Composition API 形式的生命周期钩子,与 Vue2.x 中钩子对应关系如下:

  • beforeCreate===>setup()
  • created=======>setup()
  • beforeMount ===>onBeforeMount
  • mounted=======>onMounted
  • beforeUpdate===>onBeforeUpdate
  • updated =======>onUpdated
  • beforeUnmount ==>onBeforeUnmount
  • unmounted =====>onUnmounted

Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是 Vue 的⽣命周期。

1、beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到 data、computed、watch、methods 上的方法和数据。

2、created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。

3、beforeMount(挂载前) :在挂载开始之前被调用,相关的 render 函数首次被调用。实例已完成以下的配置:编译模板,把 data 里面的数据和模板生成 html。此时还没有挂载 html 到页面上。

4、mounted(挂载后) :在 el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的 html 内容替换 el 属性指向的 DOM 对象。完成模板中的 html 渲染到 html 页面中。此过程中进行 ajax 交互。

5、beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。

6、updated(更新后):在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM 已经更新,所以可以执行依赖于 DOM 的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。

7、beforeDestroy(销毁前) :实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。

8、destroyed(销毁后) :实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。

5、都说 Composition API 和 React Hook 很像,请问他们的区别是什么

从 React Hook 从实现的角度来看,React Hook 是基于 useState 的调用顺序来确定下一个 re 渲染时间状态从哪个 useState 开始,所以有以下几个限制

  • 不在循环中、条件、调用嵌套函数 Hook
  • 必须确保它总是在你这边 React Top level 调用函数 Hook
  • 使用效果、使用备忘录 依赖关系必须手动确定

和 Composition API 是基于 Vue 的响应系统,和 React Hook 相比

  • 在设置函数中,一个组件实例只调用一次设置,而 React Hook 每次重新渲染时,都需要调用 Hook,给 React 带来的 GC 比 Vue 更大的压力,性能也相对 Vue 对我来说也比较慢
  • Compositon API 你不必担心调用的顺序,它也可以在循环中、条件、在嵌套函数中使用
  • 响应式系统自动实现依赖关系收集,而且组件的性能优化是由 Vue 内部完成的,而 React Hook 的依赖关系需要手动传递,并且依赖关系的顺序必须得到保证,让路 useEffect、useMemo 等等,否则组件性能会因为依赖关系不正确而下降。

虽然 Compoliton API 看起来像 React Hook 来使用,但它的设计思路也是 React Hook 的参考。

6、Composition Api 与 Options Api 有什么不同

1、Options Api

Options API,即大家常说的选项 API,即以 vue 为后缀的文件,通过定义 methods,computed,watch,data 等属性与方法,共同处理页面逻辑

如下图:

1679578525034-9ce7c864-a8ce-4f25-a8c7-91c85d6d0152.png

可以看到 Options 代码编写方式,如果是组件状态,则写在 data 属性上,如果是方法,则写在 methods 属性上...

用组件的选项 (data、computed、methods、watch) 组织逻辑在大多数情况下都有效

然而,当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解。

2、Composition Api

在 Vue3 Composition API 中,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的高内聚,低耦合)

即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API

1679579389334-d66a5e6b-93cf-40a9-aa85-8e2d99eea7a8.png

3、对比

下面对 Composition Api 与 Options Api 进行两大方面的比较

  • 逻辑组织
  • 逻辑复用

逻辑组织

Options API

假设一个组件是一个大型组件,其内部有很多处理逻辑关注点。

碎片化使得理解和维护复杂组件变得困难。

选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,必须不断地“跳转”相关代码的选项块。

Compostion API

而 Compositon API 正是解决上述问题,将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去

下面举个简单例子,将处理 count 属性相关的代码放在同一个函数了

javascript
function useCount() {
  let count = ref(10);
  let double = computed(() => {
    return count.value * 2;
  });

  const handleConut = () => {
    count.value = count.value * 2;
  };

  console.log(count);

  return {
    count,
    double,
    handleConut,
  };
}

组件上中使用 count

javascript
export default defineComponent({
  setup() {
    const { count, double, handleConut } = useCount();
    return {
      count,
      double,
      handleConut
    }
  },
})

修改一个属性功能的时候,只需要跳到控制该属性的方法中即可

逻辑复用

在 Vue2 中,是用过 mixin 去复用相同的逻辑

下面举个例子,会另起一个 mixin.js 文件

javascript
export const MoveMixin = {
  data() {
    return {
      x: 0,
      y: 0,
    };
  },

  methods: {
    handleKeyup(e) {
      console.log(e.code);
      // 上下左右 x y
      switch (e.code) {
        case "ArrowUp":
          this.y--;
          break;
        case "ArrowDown":
          this.y++;
          break;
        case "ArrowLeft":
          this.x--;
          break;
        case "ArrowRight":
          this.x++;
          break;
      }
    },
  },

  mounted() {
    window.addEventListener("keyup", this.handleKeyup);
  },

  unmounted() {
    window.removeEventListener("keyup", this.handleKeyup);
  },
}

然后在组件中使用

vue
<template>
  
    Mouse position: x {{ x }} / y {{ y }}
  
</template>
<script>
  import mousePositionMixin from './mouse'
  export default {
    mixins: [mousePositionMixin]
  }
</script>

使用单个 mixin 似乎问题不大,但是当一个组件混入大量不同的 mixins 的时候

javascript
mixins: [mousePositionMixin, fooMixin, barMixin, otherMixin]

会存在两个非常明显的问题:

  • 命名冲突
  • 数据来源不清晰

现在通过 Compositon API 这种方式改写上面的代码

javascript
import { onMounted, onUnmounted, reactive } from "vue";
export function useMove() {
  const position = reactive({
    x: 0,
    y: 0,
  });

  const handleKeyup = (e) => {
    console.log(e.code);
    // 上下左右 x y
    switch (e.code) {
      case "ArrowUp":
        // y.value--;
        position.y--;
        break;
      case "ArrowDown":
        // y.value++;
        position.y++;
        break;
      case "ArrowLeft":
        // x.value--;
        position.x--;
        break;
      case "ArrowRight":
        // x.value++;
        position.x++;
        break;
    }
  };

  onMounted(() => {
    window.addEventListener("keyup", handleKeyup);
  });

  onUnmounted(() => {
    window.removeEventListener("keyup", handleKeyup);
  });

  return { position };

在组件中使用

vue
<template>
  
    Mouse position: x {{ x }} / y {{ y }}
  
</template>

<script>
  import { useMove } from "./useMove";
  import { toRefs } from "vue";
  export default {
    setup() {
      const { position } = useMove();
      const { x, y } = toRefs(position);
      return {
        x,
        y,
      };

    },
  };
</script>

可以看到,整个数据来源清晰了,即使去编写更多的 hook 函数,也不会出现命名冲突的问题

小结

  • 在逻辑组织和逻辑复用方面,Composition API 是优于 Options API
  • 因为 Composition API 几乎是函数,会有更好的类型推断。
  • Composition API 对 tree-shaking 友好,代码也更容易压缩
  • Composition API 中见不到 this 的使用,减少了 this 指向不明的情况
  • 如果是小型组件,可以继续使用 Options API,也是十分友好的

7、什么是 SPA 单页面应用,首屏加载是如何优化的

单页 Web 应用(single page web application,SPA),就是只有一张 Web 页面的应用,是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序。开发的 Vue 项目大多是借助个官方的 CLI 脚手架,快速搭建项目,直接通过 new Vue构建一个实例,并将el:'#app'挂载参数传入,最后通过npm run build 的方式打包后生成一个 index.html,称这种只有一个 HTML 的页面为单页面应用。

当然,vue 也可以像 jq 一样引入,作为多页面应用的基础框架。

SPA 首屏优化方式

  • 减小入口文件积
  • 静态资源本地缓存
  • UI 框架按需加载
  • 图片资源的压缩
  • 组件重复打包
  • 开启 GZip
  • 压缩使用 SSR

8、对 Vue 项目做过哪些性能优化

1、v-ifv-show

  • 频繁切换时使用 v-show,利用其缓存特性
  • 首屏渲染时使用 v-if,如果为 false 则不进行渲染

2、v-for 的 key

  • 列表变化时,循环时使用唯一不变的 key,借助其本地复用策略
  • 列表只进行一次渲染时,key 可以采用循环的 index

3、侦听器和计算属性

  • 侦听器 watch 用于数据变化时引起其他行为
  • 多使用 compouter 计算属性顾名思义就是新计算而来的属性,如果依赖的数据未发生变化,不会触发重新计算

4、合理使用生命周期

  • 在 destroyed 阶段进行绑定事件或者定时器的销毁
  • 使用动态组件的时候通过 keep-alive 包裹进行缓存处理,相关的操作可以在 actived 阶段激活

5、数据响应式处理

  • 不需要响应式处理的数据可以通过 Object.freeze处理,或者直接通过this.xxx = xxx 的方式进行定义
  • 需要响应式处理的属性可以通过 this.$set的方式处理,而不是JSON.parse(JSON.stringify(XXX)) 的方式

6、路由加载方式

页面组件可以采用异步加载的方式

7、插件引入

第三方插件可以采用按需加载的方式,比如 element-ui。

8、减少代码量

  • 采用 mixin 的方式抽离公共方法
  • 抽离公共组件
  • 定义公共方法至公共 js 中
  • 抽离公共 css

9、编译方式

  • 如果线上需要 template 的编译,可以采用完成版 vue.esm.js
  • 如果线上无需 template 的编译,可采用运行时版本 vue.runtime.esm.js,相比完整版体积要小大约 30%

10、渲染方式

  • 服务端渲染,如果是需要 SEO 的网站可以采用服务端渲染的方式
  • 前端渲染,一些企业内部使用的后端管理系统可以采用前端渲染的方式

11、字体图标的使用

  • 有些图片图标尽可能使用字体图标

9、Vue 组件通信的方式有哪些

Vue 中 8 种常规的通信方案

  • 通过 props 传递
  • 通过 $emit 触发自定义事件
  • 使用 ref
  • EventBus
  • $parent$root
  • attrs 与 listeners
  • Provide 与 Inject
  • Vuex

组件间通信的分类可以分成以下

  • 父子关系的组件数据传递选择 props 与 $emit 进行传递,也可选择 ref
  • 兄弟关系的组件数据传递可选择 $bus,其次可以选择$parent 进行传递
  • 祖先与后代组件数据传递可选择 attrs 与 listeners 或者 Provide 与 Inject
  • 复杂关系的组件数据传递可以通过 vuex 存放共享的变量

10、Vue 常用的修饰符有哪些

1、表单修饰符

(1).lazy

在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 ,可以添加 lazy 修饰符,从而转为在 change 事件之后进行同步:

vue
<input v-model.lazy="msg">

(2).number

如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:

vue
<input v-model.number="age" type="number">

(3).trim

如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:

vue
<input v-model.trim="msg">

2、事件修饰符

(1).stop

阻止单击事件继续传播。

vue

  <a v-on:click.stop="aClick">
  	点击
  </a>

(2).prevent

阻止标签的默认行为。

vue
<a href="http://www.baidu.com" v-on:click.prevent="aClick">点击</a>

(3).capture

事件先在有 .capture 修饰符的节点上触发,然后在其包裹的内部节点中触发。

vue

  <a v-on:click="aClick">点击</a>

(4).self

只当在 event.target 是当前元素自身时触发处理函数,即事件不是从内部元素触发的。

vue

  phrase
  <a v-on:click="aClick">
  	点击
  </a>

(5).once

不像其它只能对原生的 DOM 事件起作用的修饰符,.once 修饰符还能被用到自定义的组件事件上,表示当前事件只触发一次。

vue
<a v-on:click.once="aClick">点击</a>

(6).passive

.passive 修饰符尤其能够提升移动端的性能

vue
  
  
  
...

11、Vue 中的 $nextTick 有什么作用

javascript
const callbacks = []
let pending = false

/**
 * 完成两件事:
 *   1、用 try catch 包装 flushSchedulerQueue 函数,然后将其放入 callbacks 数组
 *   2、如果 pending 为 false,表示现在浏览器的任务队列中没有 flushCallbacks 函数
 *     如果 pending 为 true,则表示浏览器的任务队列中已经被放入了 flushCallbacks 函数,
 *     待执行 flushCallbacks 函数时,pending 会被再次置为 false,表示下一个 flushCallbacks 函数可以进入
 *     浏览器的任务队列了
 * pending 的作用:保证在同一时刻,浏览器的任务队列中只有一个 flushCallbacks 函数
 * @param {*} cb 接收一个回调函数 => flushSchedulerQueue
 * @param {*} ctx 上下文
 * @returns 
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 用 callbacks 数组存储经过包装的 cb 函数
  callbacks.push(() => {
    if (cb) {
      // 用 try catch 包装回调函数,便于错误捕获
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 执行 timerFunc,在浏览器的任务队列中(首选微任务队列)放入 flushCallbacks 函数
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

官方对其的定义

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

什么意思呢?

可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue 将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新

Vue 的异步更新机制的核心是利用了浏览器的异步任务队列来实现的,首选微任务队列,宏任务队列次之。

当响应式数据更新后,会调用 dep.notify 方法,通知 dep 中收集的 watcher 去执行 update 方法,watcher.update 将 watcher 自己放入一个 watcher 队列(全局的 queue 数组)。

然后通过 nextTick 方法将一个刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的 callbacks 数组中。

如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks 的函数,则执行 timerFunc 函数,将 flushCallbacks 函数放入异步任务队列。如果异步任务队列中已经存在 flushCallbacks 函数,等待其执行完成以后再放入下一个 flushCallbacks 函数。

flushCallbacks 函数负责执行 callbacks 数组中的所有 flushSchedulerQueue 函数。

flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 方法,从而进入更新阶段,比如执行组件更新函数或者执行用户 watch 的回调函数

12、如何理解双向数据绑定

都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类 UI 组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM 这里的控制层的核心功能便是 “数据双向绑定” 。自然,只需弄懂它是什么,便可以进一步了解数据绑定的原理

理解 ViewModel

它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据

当然,它还有两个主要部分组成

监听器(Observer):对所有数据的属性进行监听

解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

13、v-show 和 v-if 有什么区别?你可以讲讲吗

v-show 与 v-if 的作用效果是相同的 (不含 v-else),都能控制元素在页面是否显示,在用法上也是相同的

区别:

  • 控制手段不同
  • 编译过程不同
  • 编译条件不同

控制手段:v-show 隐藏则是为该元素添加 css--display:none,dom 元素依旧还在。v-if 显示隐藏是将 dom 元素整个添加或删除

编译过程:v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换

编译条件:v-if 是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染

v-show 由 false 变为 true 的时候不会触发组件的生命周期

v-if 由 false 变为 true 的时候,触发组件的 beforeCreatecreatebeforeMountmounted钩子,由 true 变为 false 的时候触发组件的beforeDestorydestoryed 方法

性能消耗:v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗

14、有用过 keep-alive 吗?它有什么作用

vue 中支持组件化,并且也有用于缓存的内置组件 keep-alive 可直接使用,使用场景为路由组件和动态组件。

  • activated表示进入组件的生命周期,deactivated 表示离开组件的生命周期
  • include表示匹配到的才缓存,exclude 表示匹配到的都不缓存
  • max 表示最多可以缓存多少组件

关于 keep-alive 的基本用法:

vue
<keep-alive>
  <component :is="view"></component>
</keep-alive>

使用 includes 和 exclude:

vue
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配

设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > ... ... > beforeRouteLeave > deactivated

再次进入组件时:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated

15、可以实现一个虚拟 DOM 吗

先看浏览器对 HTML 的理解:

vue
  
    <h1>My title</h1>  
    Some text content

当浏览器读到这些代码时,它会建立一个 DOM 树来保持追踪所有内容,如同画一张家谱树来追踪家庭成员的发展一样。上述 HTML 对应的 DOM 节点树如下图所示:

1679580208853-33121033-4008-4069-9c26-293c03a3a874.png

每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。

再看 Vue 对 HTML template 的理解

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

简言之,浏览器对 HTML 的理解是 DOM 树,Vue 对 HTML的理解是虚拟 DOM,最后在patch 阶段通过 DOM 操作的 api 将其渲染成真实的 DOM 节点。

如何实现虚拟 DOM

首先可以看看 vue 中 VNode 的结构

源码位置:src/core/vdom/vnode.js

typescript
export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  functionalContext: Component | void; // only for functional component root nodes
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*当前节点的标签名*/
    this.tag = tag
    /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
    this.data = data
    /*当前节点的子节点,是一个数组*/
    this.children = children
    /*当前节点的文本*/
    this.text = text
    /*当前虚拟节点对应的真实dom节点*/
    this.elm = elm
    /*当前节点的名字空间*/
    this.ns = undefined
    /*编译作用域*/
    this.context = context
    /*函数化组件作用域*/
    this.functionalContext = undefined
    /*节点的key属性,被当作节点的标志,用以优化*/
    this.key = data && data.key
    /*组件的option选项*/
    this.componentOptions = componentOptions
    /*当前节点对应的组件的实例*/
    this.componentInstance = undefined
    /*当前节点的父节点*/
    this.parent = undefined
    /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
    this.raw = false
    /*静态节点标志*/
    this.isStatic = false
    /*是否作为跟节点插入*/
    this.isRootInsert = true
    /*是否为注释节点*/
    this.isComment = false
    /*是否为克隆节点*/
    this.isCloned = false
    /*是否有v-once指令*/
    this.isOnce = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next https://github.com/answershuto/learnVue*/
  get child (): Component | void {
    return this.componentInstance
  }
}

这里对 VNode 进行稍微的说明:

  • 所有对象的 context 选项都指向了 Vue 实例
  • elm 属性则指向了其相对应的真实 DOM 节点

Vue 是通过 createElement 生成 VNode

源码位置:src/core/vdom/create-element.js

typescript
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)

上面可以看到 createElement 方法实际上是对 _createElement 方法的封装,对参数的传入进行了判断

typescript
export function _createElement(
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context`
        )
        return createEmptyVNode()
    }
    // object syntax in v-bind
    if (isDef(data) && isDef(data.is)) {
        tag = data.is
    }
    if (!tag) {
        // in case of component :is set to falsy value
        return createEmptyVNode()
    }
    ... 
    // support single function children as default scoped slot
    if (Array.isArray(children) &&
        typeof children[0] === 'function'
    ) {
        data = data || {}
        data.scopedSlots = { default: children[0] }
        children.length = 0
    }
    if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children)
    } else if ( === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children)
    }
  // 创建VNode
    ...
}

可以看到 _createElement 接收 5 个参数:

  • context 表示 VNode 的上下文环境,是 Component 类型
  • tag 表示标签,它可以是一个字符串,也可以是一个 Component
  • data 表示 VNode 的数据,它是一个 VNodeData 类型
  • children 表示当前 VNode 的子节点,它是任意类型的
  • normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,主要是参考 render 函数是编译生成的还是用户手写的

根据 normalizationType 的类型,children 会有不同的定义

javascript
if (normalizationType === ALWAYS_NORMALIZE) {
  children = normalizeChildren(children)
} else if ( === SIMPLE_NORMALIZE) {
  children = simpleNormalizeChildren(children)
}

simpleNormalizeChildren 方法调用场景是 render 函数是编译生成的

normalizeChildren 方法调用场景分为下面两种:

  • render 函数是用户手写的
  • 编译 slot、v-for 的时候会产生嵌套数组

无论是 simpleNormalizeChildren还是normalizeChildren 都是对 children 进行规范(使 children 变成了一个类型为 VNode 的 Array),这里就不展开说了

规范化 children 的源码位置在:src/core/vdom/helpers/normalzie-children.js

在规范化 children 后,就去创建 VNode

javascript
let vnode, ns
// 对tag进行判断
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // 如果是内置的节点,则直接创建一个普通VNode
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    // 如果是component类型,则会通过createComponent创建VNode节点
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}

createComponent 同样是创建 VNode

源码位置:src/core/vdom/create-component.js

typescript
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }
  // 构建子类构造函数 
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // 安装组件钩子函数,把钩子函数合并到data.hook中
  installComponentHooks(data)

  //实例化一个VNode返回。组件的VNode是没有children的
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

稍微提下 createComponent生成VNode 的三个关键流程:

  • 构造子类构造函数 Ctor
  • installComponentHooks 安装组件钩子函数
  • 实例化 vnode

小结

createElement 创建 VNode 的过程,每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个虚拟树结构,用于描述真实的 DOM 树结构

16、为什么 data 属性是一个函数而不是一个对象,具体原因是什么

是不是一定是函数,得看场景。并且,也无需担心什么时候该将 data 写为函数还是对象,因为 vue 内部已经做了处理,并在控制台输出错误信息。

场景一:new Vue({data: ...}) 这种场景主要为项目入口或者多个 html 页面各实例化一个 Vue 时,这里的 data 即可用对象的形式,也可用工厂函数返回对象的形式。因为,这里的 data 只会出现一次,不存在重复引用而引起的数据污染问题。

场景二:组件场景中的选项在生成组件 vnode 的过程中,组件会在生成构造函数的过程中执行合并策略:

javascript
// data合并策略
strats.data = function (
  parentVal,
  childVal,
  vm
) {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      );
      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }
  return mergeDataOrFn(parentVal, childVal, vm)
};

如果合并过程中发现子组件的数据不是函数,即 typeof childVal !== 'function'成立,进而在开发环境会在控制台输出警告并且直接返回 parentVal,说明这里压根就没有把 childVal 中的任何 data 信息合并到 options 中去。 上面讲到组件 data 必须是一个函数,不知道大家有没有思考过这是为什么呢? 在定义好一个组件的时候,vue 最终都会通过Vue.extend() 构成组件实例,这里模仿组件构造函数,定义 data 属性,采用对象的形式

javascript
function Component(){
 
}
Component.prototype.data = {
  count : 0
}

创建两个组件实例

javascript
const componentA = new Component()
const componentB = new Component()

修改 componentA 组件 data 属性的值,componentB 中的值也发生了改变

javascript
console.log(componentB.data.count)  // 0
componentA.data.count = 1
console.log(componentB.data.count)  // 1

产生这样的原因这是两者共用了同一个内存地址,componentA 修改的内容,同样对 componentB 产生了影响

如果采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)

javascript
function Component(){
  this.data = this.data()
}
Component.prototype.data = function (){
  return {
    count : 0
  }
}

修改 componentA 组件 data 属性的值,componentB 中的值不受影响

javascript
console.log(componentB.data.count)  // 0
componentA.data.count = 1
console.log(componentB.data.count)  // 0

vue 组件可能会有很多个实例,采用函数返回一个全新 data 形式,使每个实例对象的数据不会受到其他实例对象数据的污染

17、Vue2 的初始化过程有过了解吗,做了哪些事情

new Vue走到了 vue 的构造函数中:src\core\instance\index.js 文件。

javascript
this._init(options)

然后从 Mixin 增加的原型方法看,initMixin(Vue),调用的是为 Vue 增加的原型方法 _init

javascript
// src/core/instance/init.js

function initMixin (Vue) {
  Vue.prototype._init = function (options) {
     var vm = this; 创建vm, 
     ...
     // 合并options 到 vm.$options
     vm.$options = mergeOptions(  
       resolveConstructorOptions(vm.constructor), 
       options || {},  
       vm 
     );
  }
  ...
   initLifecycle(vm); //初始生命周期
   initEvents(vm); //初始化事件
   initRender(vm); //初始render函数
   callHook(vm, 'beforeCreate'); //执行 beforeCreate生命周期钩子
   ...
   initState(vm);  //初始化data,props,methods computed,watch 
   ...
   callHook(vm, 'created');  //执行 created 生命周期钩子
   
   if (vm.$options.el) {
      vm.$mount(vm.$options.el); //这里也是重点,下面需要用到
   }
 }

总结

所以,从上面的函数看来,new vue 所做的事情,就像一个流程图一样展开了,分别是

  • 合并配置
  • 初始化生命周期
  • 初始化事件
  • 初始化渲染
  • 调用 beforeCreate 钩子函数
  • init injections and reactivity(这个阶段属性都已注入绑定,而且被 $watch 变成 reactivity,但是 $el 还是没有生成,也就是 DOM 没有生成)
  • 初始化 state 状态(初始化了 data、props、computed、watcher)
  • 调用 created 钩子函数。

在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM。

18、Vue3 初始化的一个大概流程

初始化的一个大概流程

createApp() => mount() => render() => patch() => processComponent() => mountComponent()

简易版流程编写

  1. Vue.createApp() 实际执行的是 renderercreateApp()
  2. renderercreateRenderer 这个方法创建
  3. renderercreateApp()createAppAPI() 返回的
  4. createAppApi接受到 render 之后,创建一个 app 实例,定义 mount 方法
  5. mount会调用 render 函数。将 vnode 转换为真实 dom

createRenderer() => renderer => renderer.createApp() <= createAppApi()

vue

<script>
  // 3.createAppAPI
  const createAppAPI = render => {
    return function createApp(rootComponent) {
      // 返回应用程序实例
      const app = {
        mount(rootContainer) {
          // 挂载vnode => dom
          const vnode = {
            tag: rootComponent
          }
          // 执行渲染
          render(vnode, rootContainer)
        }
      }
      return app;
    }
  }

  // 1. 创建createApp
  const Vue = {
    createApp(options) {
      //实际执行的为renderer的createApp()
      // 返回app实例
      return renderer.createApp(options)
    }
  }

  // 2.实现renderer工厂函数
  const createRenderer = options => {
    // 实现patch
    const patch = (n1, n2, container) => {
      // 获取根组件配置
      const rootComponent = n2.tag;
      const ctx = { ...rootComponent.data()}
      // 执行render获取vnode
      const vnode = rootComponent.render.call(ctx);

      // 转换vnode => dom
      const parent = options.querySelector(container)
      const child = options.createElement(vnode.tag)
      if (typeof vnode.children === 'string') {
        child.textContent = vnode.children
      } else {
        //array
      }
      // 追加
      options.insert(child, parent)
    }

    // 实现render
    const render = (vnode, container) => {
      patch(container._vnode || null, vnode, container)
      container._vnode = vnode;
    }

    // 该对象就是renderer
    return {
      render,
      createApp: createAppAPI(render)
    }
  }

  const renderer = createRenderer({
    querySelector(el) {
      return document.querySelector(el)
    },
    createElement(tag) {
      return document.createElement(tag)
    },
    insert(child, parent) {
      parent.appendChild(child)
    }
  })

  Vue.createApp({
    data() {
      return {
        bar: 'hello,vue3'
      }
    },
    render() {
      return {
        tag: 'h1',
        children: this.bar
      }
    }
  }).mount('#app')
</script>

19、Vue3 响应式 api 如何编写

javascript
var activeEffect = null;
function effect(fn) {
  activeEffect = fn;
  activeEffect();
  activeEffect = null; 
}
var depsMap = new WeakMap();
function gather(target, key) {
  // 避免例如console.log(obj1.name)而触发gather
  if (!activeEffect) return;
  let depMap = depsMap.get(target);
  if (!depMap) {
    depsMap.set(target, (depMap = new Map()));
  }
  let dep = depMap.get(key);
  if (!dep) {
    depMap.set(key, (dep = new Set()));
  }
  dep.add(activeEffect)
}
function trigger(target, key) {
  let depMap = depsMap.get(target);
  if (depMap) {
    const dep = depMap.get(key);
    if (dep) {
      dep.forEach((effect) => effect());
    }
  }
}
function reactive(target) {
  const handle = {
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver);
      trigger(receiver, key); // 设置值时触发自动更新
    },
    get(target, key, receiver) {
      gather(receiver, key); // 访问时收集依赖
      return Reflect.get(target, key, receiver);
    },
  };
  return new Proxy(target, handle);
}

function ref(name){
  return reactive(
    {
      value: name
    }
  )
}

20、在 Vue 项目中如何做的 SSR 渲染

与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
  • 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记 " 激活 " 为客户端上完全可交互的应用程序

服务器渲染的 Vue.js 应用程序也可以被认为是 " 同构 " 或 " 通用 ",因为应用程序的大部分代码都可以在服务器和客户端上运行

  • Vue SSR 是一个在 SPA 上进行改良的服务端渲染
  • 通过 Vue SSR 渲染的页面,需要在客户端激活才能实现交互
  • Vue SSR 将包含两部分:服务端渲染的首屏,包含交互的 SPA

使用 ssr 不存在单例模式,每次用户请求都会创建一个新的 vue 实例

实现 ssr 需要实现服务端首屏渲染和客户端激活

服务端异步获取数据 asyncData 可以分为首屏异步获取和切换组件获取

首屏异步获取数据,在服务端预渲染的时候就应该已经完成

切换组件通过 mixin 混入,在 beforeMount 钩子完成数据获取

21、怎么看 Vue 的 diff 算法

diff 算法是一种通过同层的树节点进行比较的高效算法

diff 整体策略为:深度优先,同层比较

比较只会在同层级进行,不会跨层级比较

比较的过程中,循环从两边向中间收拢

  • 当数据发生改变时,订阅者 watcher 就会调用 patch 给真实的 DOM 打补丁
  • 通过 isSameVnode 进行判断,相同则调用 patchVnode 方法
  • patchVnode 做了以下操作:
    • 找到对应的真实 dom,称为 el
    • 如果都有都有文本节点且不相等,将 el 文本节点设置为 Vnode 的文本节点
    • 如果 oldVnode 有子节点而 VNode 没有,则删除 el 子节点
    • 如果 oldVnode 没有子节点而 VNode 有,则将 VNode 的子节点真实化后添加到 el
    • 如果两者都有子节点,则执行 updateChildren 函数比较子节点
  • updateChildren 主要做了以下操作:
    • 设置新旧 VNode 的头尾指针
    • 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用 patchVnode 进行 patch 重复流程、调用 createElem 创建一个新节点,从哈希表寻找 key 一致的 VNode 节点再分情况操作

22、从 0 到 1 构建一个 Vue 项目需要做哪些内容

架子:选用合适的初始化脚手架 (vue-cli2.0 或者 vue-cli3.0)

请求:数据 axios 请求的配置

登录:登录注册系统

路由:路由管理页面

数据:vuex 全局数据管理

权限:权限管理系统

埋点:埋点系统

插件:第三方插件的选取以及引入方式

错误:错误页面

入口:前端资源直接当静态资源,或者服务端模板拉取

SEO:如果考虑 SEO 建议采用 SSR 方案

组件:基础组件/业务组件

样式:样式预处理起,公共样式抽取

方法:公共方法抽离

更新: 2023-03-23 23:52:57
原文: <https://www.yuque.com/fcant/notes/xls2vbm6fu738gsv&gt;

最近更新