1. Vue3 和 Vue2 有哪些区别?

  1. 数据响应式原理变化,Vue2:Object.defineProperty();Vue3:Proxy
  2. Vue3 新增内置组件:Fragment(文档碎片)、Suspense(异步组件)、Teleport(瞬移组件)
  3. Vue3 提供 Composition API(可以自定义一些响应式数据, 而不是直接放在data选项中; 可以进行组合(把相同的功能放到方法里 hooks));Vue2 是 Options API;自定义函数(hooks)
  4. Vue3 中生命周期前面都加了 on,移除了 beforeCreatecreated 钩子函数, 用 setup 代替
  5. Vue3 源码采用 TS 开发,Vue2 采用 flow;-> Vue3 对 TS 支持更加友好
  6. Vue3 源码采用 monorepo 方式进行管理,将模块拆分到 packages 目录中
  7. Vue3 支持 tree-shaking,不使用就不会打包,提升性能
  8. Vue3 中对模块编译进行了优化,编译时生成 block tree,可以对子节点进行收集。可以减少比较,并且采用了 patchFlag 标记动态节点。
  9. Vue3 中对全局 API 的改变
  10. 新增 v-memo 指令
  11. 新增了开发环境的两个钩子函数,在组件更新时 onRenderTracked 会跟踪组件里所有变量和方法的变化、每次触发渲染时onRenderTriggered` 会返回发生变化的新旧值,可以让我们进行有针对性调试
  12. 支持在 <style></style> 里使用 v-bind,给 CSS 绑定 JS 变量(color: v-bind(str))

缺点Vue3 不兼容 IE11

扩展Vue3 对于 Vue2 在性能上的优化(从 compileruntime 两方面)?

  • Vue 设计思想
    1. 拆分模块Vue3.0 更注重模块上的拆分,在 2.0 中无法单独使用部分模块。需要引入完整的 Vuejs(例如,只想使用响应式部分,但需要引入完整的 Vuejs),Vue3 中的模块之间耦合度低,模块可以独立使用。
    2. 重写 APIVue2 中很多方法挂载到了实例中导致没有使用也会被打包(还有很多组件也是一样)。通过构建工具 Tree-Shaking 机制实现按需引入,减少打包后体积。
    3. 扩展更方便Vue3 允许自定义渲染器,扩展能力强。不会发生以前的事情,改写 Vue 源码改造渲染方式。

编译时:将模板变成虚拟节点;运行时:将虚拟节点变成真实节点

2. 说一下 Vue2 和 Vue3 的响应式原理?

Vue2:当创建 Vue 实例时,Vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 gettersettergetter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

Vue3Proxy

Vue.2x 中,使用 Object.defineProperty() 对对象进行监听。而在 Vue3.0 中,改用 Proxy 进行监听。Proxy 比起 Object.defineProperty() 有如下优势:

  1. 可以监听属性的增删操作
  2. 可以监听数组某个索引值的变化以及数组长度的变化
  • reactive() 的作用是将目标对象转换为响应式的 proxy 实例

  • reactive -> createReactiveObject 方法的处理逻辑:

    1. 如果 target 不是对象,返回 target
    2. 如果 target 已经是 proxy 实例,或者 readonly 处理 reactive 对象,返回 target
    3. 通过 isReadonly 创建 WeakMap,判断是否监听过 target,存在,返回监听过后的 existingProxy防止同一对象重复代理
    4. 获取 target 类型,看是否是 INVALID 无效类型,返回 target.(如果是 skip 跳过或不可扩展属性就返回 INVALID 无效对象,否则是普通对象类型(ObjectArray)或者集合类型(SetMapWeakSetWeakMap))
      • Object.isExtensible(target)检查是否可以向对象添加新属性
    5. 生成 proxy 实例,如果是集合类型就使用 collectionHandlers,否则使用 baseHandlers
    6. 使用 proxyMap.set(target, proxy)存起来,供第 3 做判断

Vue2 是深层递归,Vue3 是懒递归(如果是嵌套对象,会继续递归将子对象转为响应式对象)

  • proxy 实例的种类:通过函数柯里化传入不同参数创建不同的 proxy 实例
    1. 完成响应式 proxy 实例,reactive()
    2. 只读的 proxy 实例,readonly()
    3. 浅层响应式 proxy 实例(对象第一层是响应式),shallowReactive()
    4. 只读的浅层的响应式 proxyshallowReadonly()

处理器种类:

  • 普通类型:baseHandler.ts,4 种 proxy 实例,对应着 4 种不同的处理器

    1. mutationHandlers
    2. readonlyHandlers
    3. shallowReactiveHandlers
    4. shallowReadonlyHandlers
    • 处理器对五种操作进行了拦截,分别是:
      1. get 获取属性 createGetter()
      2. set 设置属性 createSetter()
      3. deleteProperty 删除属性 deleteProperty()
      4. has 是否拥有某个属性 has()
      5. ownKeys(可以拦截以下操作:Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()Object.ownKeys()) ownKeys() 注意gethasownKeys 操作会 依赖收集 -> track, setdeleteProperty 操作会 触发依赖 -> trigger
  • 集合类型:collectionHandlers.ts

  • track() 将对象中的属性与 effect 关联起来

  • trigger()

  • effect() -> createReactiveEffect() 高阶函数 将自己设置为 activeEffect,然后执行 fn 函数,如果 fn 函数里有对响应式属性进行读取会触发 get 操作,从而收集依赖 注意effectStack 函数栈来维护 属性effect 的对应

reactive 内部采用 proxy,ref 中内部的是 defineProperty

  • Observer(观察者):主要给对象的属性添加 gettersetter,用于 依赖收集

  • Dep(依赖的管理者和派发更新):用于收集当前的响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 Dep 实例

  • Watcher(订阅者):

  • 三者之间的关系?

    1. Observer 将数据定义为响应式,每个 Observer 实例都有自己的 Dep 来管理依赖,实例化 Watcher 的时候进行求值会触发 getter,进而执行 dep.depend(), 将当前的 Watcher 加入 Dep 维护的依赖列表 subs,这就是依赖收集的过程。
    2. 数据发生变化触发 setter,进而执行 dep.notify(), Dep 会执行所有的依赖的 update(),并加入异步更新队列,这就是触发依赖的过程。

3. Vue 实例挂载的过程发生了什么?(new Vue()发生了啥?)

  1. src/core/instance/index.js

new Vue() -> this._init(options) -> initMixin(Vue)[原型上挂载 _init 方法]

  1. scr/core/instance/init.js

Vue.prototype._init -> 合并配置 mergeOptions() -> 初始化生命周期(initLifecycle) -> 初始化事件中心(initEvents) -> 初始化渲染(initRender) -> 挂载 beforeCreate 钩子 -> 初始化 Inject(initInject) -> 初始化 initState(datapropscomputedwatchermethods)等等 -> 初始化 Provide(initProvide) -> 初始化 created 钩子 -> 最后判断是否有 el 使用 $mount 挂载

  1. src/core/instance/state.js

initState: 会对 propsmethodsdatacomputedwatcher 进行初始化 =》数组的基本来源

initData: 初始化数据 -> 判断 data 是否是函数 -> 获取 data 所有的 keypropsmethods 判断是否有重名 -> 观测数据(observe(data))

  1. src/core/observer/index.js

observe() -> 通过 __ob__ 判断数据是否被观测 -> 如果观测就返回观测的数据,若没有观测就实例化(new Observer(value)

  1. src/core/observer/index.js

class Observer 类 -> 1、给每个属性添加 ob 属性;2、判断数组还是对象;3、数组走 observeArray(), 对象走 walk()【注意:此时实例化 depthis.dep = new Dep()

  1. 对象情况:

循环所有的 key, 调用defineReactive() -> Object.defineProperty()

get() [dep.depend()依赖收集【如果有孩子深度观测 childOb.dep.depend();若孩子是数组走 dependArray()(循环所有的孩子,e && e.__ob__ && e.__ob__.dep.depend());如果孩子里还有数组,递归调用 dependArray()】]

1. src/observer/dep.js -> depend() :如果 Dep.target 存在,就添加依赖 Dep.target.addDep(this)
2. src/observer/watcher.js -> addDep(dep) :将 dep 添加到 watcher 的 newDeps 中;通过 addSub()将 watcher 添加到 Dep 的 subs 数组中(判断是否重复,相同的只能添加一次),不存在的话就将 watcher 添加到 dep 中 dep.addSub(this)
3. src/observer/dep.js -> addSub(sub: Watcher)将 watcher 添加到 dep 中 this.subs.push(sub)

set() [dep.notify()触发更新]【如果孩子是数组也要进行观测 childOb = observe(newVal)】-> dep.notify()

1. src/observer/dep.js -> notify() : 遍历 Dep 中的 subs 里所有的 watcher,循环调用 watcher 的 update 方法;subs[i].update()
2. src/observer/watcher.js -> update(): 如果是同步 this.sync 调用 this.run() 【调用 watcher 中的 get 方法 this.get()获取最新 value,将 this.value 赋值给 oldValue; 将最新的 value 赋值给 this.value;调用 this.cb.call(this.vm, value, oldValue)】-> 如果是异步调用 queueWatcher(this)
3. src/observer/scheduler.js -> queueWatch(watcher: Watcher)方法: -> nextTick(flushSchedulerQueue)
4. src/observer/scheduler.js -> flushSchedulerQueue() 就是将更新队列中的 watcher 拿出来并依次调用他们的 callback,但重点在于为什么在 for 循环之前先对这些 watcher 进行了升序排列
  1. src/platform/web/entry-runtime-with-compiler.js

    $mount() 重写 $mount 方法 -> 判断(1、是否 render 函数、2、是否有 template、3、都没有则 el.outerHTML )-> 将 template 转换成 render 方法【compileToFunction(template)

    线路图new Vue => _init => $mount => mountComponent

    src/instance/lifecycle.jsmountComponent() -> 挂载组件 -> callHook(vm, 'beforeMount') -> 定义 updateComponent -> new Watcher(vm, updateComponent, () => {}, true) // == updateComponent() ;默认 vue 是通过 watcher 来进行渲染 渲染 watcher

_render() 函数返回的就是 虚拟 dom; _update() 方法将 虚拟节点转换成真实节点; _render 函数会读取 data 中的数据从而触发 getter 方法进行 依赖收集(会调用 watcher 中的 get 方法进行求值)

扩展Vue2runtime + compilerruntime-only 的区别?

  • runtime-only 相比 runtime-compiler 有两个优点:
    1. 运行效率高
    2. 源代码量更少
  • runtime-compiler 模式,runtime-only 模式在 src 文件里面只在 main.js 里面有区别:
    1. runtime-compiler 模式里的 Vue 实例的模板,和注册的组件,都被一个 render 函数替换掉了
    2. h 函数是 createElement 的缩写-》用于创建虚拟 DOM

runtime-compiler 的步骤template -> ast -> render -> virtual dom -> 真实 dom

runtime-only 的步骤render -> virtual dom -> 真实 dom

问题runtime-onlymain.js 中的 template 被替换掉了,那组件中 template 还存在,他是怎么编译的呢?

vue-template-compiler,运行项目时,这个包会自动将组件的 template 转换成 render 函数

4. Vue2 和 Vue3 组件通信方式有哪些?

  • Vue2 组件通信方式:

    1. 父 -> 子:props; 子 -> 父:$emit【单向数据流:props 只能从上一级组件传递到下一级组件】
    2. 父子组件之间:$parents、$children 获取组件实例
    3. provide 和 inject :父组件中通过 provide 来提供变量,然后子组件中使用 inject 来注入变量【Vue2.2.0 新增】
    4. ref 和 refs:如果在普通的 DOM 元素上,引用指向的是 DOM 元素;如果用在子组件上,引用就指向组件是来,可以通过实例获取组件的方法和数据等。
    5. EventBus:事件总线($emit派发事件、$on 注册事件、$off 移除事件)
    6. $attrs 和 $listeners:新增了inheritAttrs 选项,默认值为 true【Vue2.4 新增】
    7. Vuex:专为 Vue.js 应用程序开发设计的状态管理
  • Vue3 组件通信方式:

    1. 父 -> 子:props; 子 -> 父:emit
    2. v-model 方式
    3. $refs 【defineExpose 暴露属性】
    4. provide 和 inject
    5. 事件总线(Vue3 中移除了 Event-Bus,但是可以借助第三方工具:Vue 官方推荐:mitt 和 tiny-emitter)
    6. Vuex4
    7. Pinia

5. 说一说 nextTick 的原理?

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

  • 再回答 nextTick 实现原理之前,首先要了解 JS 的执行机制:

    JS单线程的,一次只能干一件事,即同步。(就是说所有的任务都需要排队,后面的任务需要等前面的任务执行完才能执行,如果前面的任务耗时过长,后面的任务就需要一直等,这是非常影响用户体验的)。所以有了 异步 的概念

    • 同步任务:指排队在主线程上依次执行的任务
    • 异步任务:不进入主线程,而进入任务队列的任务,又分为宏任务微任务
    • 宏任务: 渲染事件、请求、scriptsetTimeoutsetIntervalNode 中的 setImmediate
    • 微任务: Promise.thenMutationObserver(监听 DOM)、Node 中的 Process.nextTick

    当执行栈中的同步任务执行完后,就会去任务队列中拿一个宏任务放到执行栈中执行,执行完该宏任务中的所有微任务,再到任务队列中拿宏任务,即一个宏任务、所有微任务、渲染、一个宏任务、所有微任务、渲染...,如此形成循环,即事件循环(EventLoop)

  • Vue2 中 nextTick 源码解析:

    • 源码地址:src/core/util/next-tick.js,源码版本:2.6.14

    • 源码的主要的两个作用:

      1. 一是判断当前环境能使用的最合适的 API 并保存异步函数

        主要是判断用哪个宏任务或微任务,因为宏任务耗费的时间是大于微任务的,所以成先使用微任务,判断顺序如下:

        • Promise

        • MutationObserver

        • setImmediate

        • setTimeout

          环境判断结束就会得到一个延迟回调函数 timerFunc, 然后进入核心的 nextTick

      2. 二是调用异步函数 执行回调队列

        nextTick 方法主要逻辑就是:

        • 把传入的回调函数放进回调队列 callbacks
        • 执行保存的异步任务 timeFunc,就会遍历 callbacks 执行相应的回调函数了

注意:如果没有提供回调,并且支持 Promise,就返回一个 Promise。 -> this.$nextTick().then(()=>{ ... })

  • 名词解析:
    1. isUsingMicroTask:是否启用微任务开关
    2. callbacks:回调队列
    3. pending:异步控制开关,同一时间只能执行一次
    4. flushCallbacks():该方法负责执行队列中的全部回调;执行之前先备份并清空回调队列,是为了防止 nextTick 里有 nextTick 出现的问题
    5. timerFunc:用来保存调用异步任务方法
  • Vue3 中 nextTick 源码解析:
    • 源码地址:packages/runtime-core/src/scheduler.ts,源码版本:3.2.11
    • vue3 中 nextTick 的队列由几个方法维护,基本执行顺序是这样的: queueJob -> queueFlush -> flushJobs -> nextTick 参数的 fn
    • 【还需探索...】

扩展: Vue3 中的 nextTick 是如何实现的?

源码地址:packages/runtime-core/src/scheduler.ts

可以看出 nextTick 接受一个函数为参数,同时会创建一个微任务。在我们页面调用 nextTick 的时候,会执行该函数,把我们的参数 fn 赋值给 p.then(fn),在队列的任务完成后,fn 就执行了。由于加了几个维护队列的方法,所以执行顺序是这样的:queueJob -> queueFlush -> flushJobs -> nextTick 参数的 fn

6. 你知道 Vue 页面不刷新的情况有哪些?

  1. Vue 无法检测实例被创建时不存在 data 中的属性
  2. Vue 无法检测对象属性的添加和移除
  3. Vue 通过数组下标(索引)修改一个数据项
  4. Vue 不能直接修改数组的 length
  5. 在异步执行之前操作 DOM
  6. 循环嵌套太深,视图不更新 -> 解决:$forceUpdate()
  7. 路由参数变化时,页面不刷新 -> 解决:watch 监听 $routerouter-view 添加 key

7. keep-alive 的原理?

keep-alive 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中;使用 keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

总的来说:keep-alive 用于保存组件的渲染状态,防止组件多次渲染。

  • keep-alive 用法:
    1. 在动态组件中的应用
      <keep-alive :include="whiteList" :exclude="blackList" :max="amount">
        <component :is="currentComponent"></component>
      </keep-alive>
      
    2. 在 vue-router 中的应用
      <keep-alive :include="whiteList" :exclude="blackList" :max="amount">
        <router-view></router-view>
      </keep-alive>
      

其中include 定义缓存白名单,keep-alive 会缓存命中的组件;exclude 定义缓存黑名单,被命中的组件将不会被缓存;max 定义缓存组件上限,超出上限使用 LRU 的策略置换缓存数据。

  • 缓存淘汰策略 LRU(最近最少使用)

  • 源码解析过程:

    1. 路径:src/core/components/keep-alive.js

    2. abstract 属性:true,判断当前组件虚拟 dom 是否渲染成真实 dom 的关键

    3. props 属性:

      • include:缓存白名单
      • exclude:缓存黑名单
      • max:缓存的组件数量
    4. keep-alive 在它生命周期内定义了三个钩子函数:

      • created:初始化两个对象分别缓存 VNode(虚拟 DOM)cacheVNode 对应的键集合 keys

      • destroyed:删除 this.cache 中缓存的 VNode 实例

        扩展:为什么不直接将 this.cache 置为 null,而是遍历调用 pruneCacheEntry 函数删除? 删除缓存的 VNode 还要对应组件实例的 destroy 钩子函数

      • mounted:对 includeexclude 参数进行监听,然后实时地更新(删除)this.cache 对象数据

      • render

        1. 第一步:获取 keep-alive 包裹着的第一个子组件对象及其组件名
        2. 第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步
        3. 第三步:根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键),否则执行第四步
        4. 第四步:在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
        5. 第五步:最后并且很重要,将该组件实例的 keepAlive 属性值设置为 true
  • 新增两个钩子函数:activateddeactivated

常见问题:

  • keep-alive 不会生成真正的 DOM 节点,Vue 是如何做到的?

    Vue初始化生命周期的时候,为组件实例建立父子关系会根据 abstract 属性决定是否忽略某个组件。在 keep-alive 中,设置了 abstract: true,那 Vue 就会跳过该组件实例。

    // src/core/instance/lifecycle.js
    if (parent && !options.abstract) {
      while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent
      }
      parent.$children.push(vm)
    }
    
  • keep-alive 包裹的组件是如何使用缓存的?

    patch 阶段,会执行 createComponent 函数:

    // src/core/vdom/patch.js
    function createComponent() {
      let i = vnode.data
      if (isDef(i)) {
        if (isDef((i = i.hook)) && isDef((i = i.init))) {
          i(vnode, false)
        }
        if (isDef(vnode.componentInstance)) {
          initComponent(vnode, insertedVnodeQueue)
          insert(parentElem, vnode.elem, refElem) // 将缓存的DOM(vnode.elem) 插入父元素中
        }
      }
    }
    
    1. 在首次加载被包裹组建时,由 keep-alive.js 中的 render 函数可知,vnode.componentInstance 的值是 undefinedkeepAlive 的值是 true,因为 keep-alive 组件作为父组件,它的 render 函数会先于被包裹组件执行;那么只执行到 i(vnode,false),后面的逻辑不执行
    2. 再次访问被包裹组件时,vnode.componentInstance 的值就是已经缓存的组件实例,那么会执行 insert(parentElm, vnode.elm, refElm)逻辑,这样就直接把上一次的 DOM 插入到父元素中

8. Vue3 中的 Composition API 和 Vue2 中的 Options API 有什么不同?

  • 通常使用 Vue2 开发的项目,普遍会存在以下问题:
    1. 代码的可读性随着组件变大而变差
    2. 每一种代码复用的方式,都存在缺点
    3. TypeScript 支持有限
  • mixins 的缺陷:
    1. 命名冲突
    2. 数据来源不清楚
  • 两者的主要区别:
    1. 在逻辑组织和逻辑复用方面,Composition API 是优于 Options API
    2. 因为 Composition API 几乎是函数,会有更好的类型推断
    3. Composition APItree-shaking 友好,代码也更容易压缩
    4. Composition API 中见不到 this 的使用,减少了 this 指向不明的情况
    5. 如果是小型组件,可以继续使用 Options API,也是十分友好的

9. SPA 单页面的理解以及优缺点?SSR 了解吗?

SPAsingle-page application )仅在 Web 页面初始化时加载相应的 HTMLJavaScriptCSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

  • 优点:

    1. 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
    2. 基于上面一点,SPA 相对对服务器压力小
    3. 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
  • 缺点:

    1. 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载
    2. 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理
    3. SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势
  • SSR 服务端渲染 Nuxt.js

    • 优点:
      1. 更利于 SEO
      2. 更利于首屏渲染
    • 缺点:
      1. 服务端压力较大
      2. 开发条件受限(createdbeforeCreate 之外的生命周期钩子不可用)
      3. 学习成本相对较高(除了对 webpackVue 要熟悉,还需要掌握 nodeExpress 相关技术)

10. set、delete 的实现原理?

  • set 的实现原理?

    • 路径:src/core/instance/observer/index.js -> set(target, key, val)
    • 大致流程:核心方法:defineReactive(ob.value, key, val)
      1. 判断目标值是否为有效值,不是有效值直接停止
      2. 判断是否为数组,并且 key 值是否为有效的 key 值 如果是数组,就选择数组的长度和 key 值取较大值作为数组的新的 length 值,并且替换目标值 splice 方法,重写了,所以执行 splice,会双向数据绑定
      3. 如果目标值是对象,并且 key 值是目标值存在的有效 key 值,并且不是原型上的 key 值 判断目标值是否为响应式的ob 如果是 vue 实例,直接不行 如果不是响应式的数据,就是普通的修改对象操作 如果是响应式数据,那就通过 Object.defineProperty 进行数据劫持
      4. 通知 dom 更新 ob.dep.notify()
  • delete 的实现原理?

    • 路径:src/core/instance/observer/index.js -> del(target, key)
    • 大致流程:核心逻辑:delete target[key]
      1. 非生产环境下, 不允许删除一个原始数据类型, 或者 undefined, null
      2. 如果 target 是数组, 并且 key 是一个合法索引,通过数组的 splice 方法删除值, 并且还能触发数据的响应(数组拦截器截取到变化到元素, 通知依赖更新数据)
      3. target._isVue: 不允许删除 Vue 实例对象上的属性;(ob && ob.vmCount): 不允许删除根数据对象的属性,触发不了响应
      4. 如果属性压根不在对象上, 什么都不做处理
      5. 走到这一步说明, target 是对象, 并且 keytarget 上, 直接使用 delete 删除
      6. 如果 ob 不存在, 说明 target 本身不是响应式数据 return;
      7. 存在 ob, 通过 ob 里面存储的 Dep 实例的 notify 方法通知依赖更新 ob.dep.notify()

扩展$deletedelete 的区别?

  1. deletejs 原生方法,由于语言限制,此操作无法设置回调来响应
  2. $deletevue 提供的实例方法,核心就是在删除后通知了依赖更新

11. Vue 源码设计用了哪些设计模式?

  • 单例模式:整个程序有且仅有一个实例

    new 多次,只有一个实例vuexvue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉

  • 工场模式:传入参数就可以创建实例

    虚拟 DOM 根据 参数的不同 返回基础标签的 Vnode组件 Vnode

  • 发布订阅模式:eventBusvue 事件机制

  • 观察者模式:watchdep(响应式数据原理)

  • 代理模式:_data 属性、proxy、防抖、节流

  • 中介者模式:vuex

  • 策略模式

  • 外观模式

扩展发布 / 订阅模式观察者模式 的区别是什么?

  • 在观察者模式中,被观察者通常会维护一个观察者列表。当被观察者的状态发生改变时,就会通知观察者
  • 在发布订阅模式中,具体发布者会动态维护一个订阅者的列表:可在运行时根据程序需要开始或停止发布给对应订阅者的事件通知

区别在于发布者本身并不维护订阅列表(它不会像观察者一样主动维护一个列表),它会将工作委派给具体发布者(相当于秘书,任何人想知道我的事情,直接问我的秘书就可以了);订阅者在接收到发布者的消息后,会委派具体的订阅者来进行相关的处理

12. Vue 中常见的性能优化方式?

  • 编码优化
    1. 尽量不要将所有的数据都放在 data 中,data 中的数据都会增加 gettersetter,会收集对应的 watcher
    2. vuev-for 时给每项元素绑定事件尽量用 事件代理
    3. 拆分组件( 提高复用性、增加代码的可维护性,减少不必要的渲染 )
    4. v-if 当值为 false 时内部指令不会执行,具有阻断功能,很多情况下使用 v-if 替代 v-show
    5. 合理使用 路由懒加载异步组件
    6. Object.freeze 冻结数据
  • 用户体验
    1. app-skeleton 骨架屏
    2. pwa service workerweb workershared worker 的区别?】
  • 加载性能优化
    1. 第三方模块按需导入 ( babel-plugin-component )
    2. 滚动到可视区域动态加载 ( vue-virtual-scroll-list )
    3. 图片懒加载 (vue-lazyload)
  • SEO 优化
    1. 预渲染插件 prerender-spa-plugin
    2. 服务端渲染 ssr
  • 打包优化
    1. 使用 cdn 的方式加载第三方模块
    2. 多线程打包 happypackparallel-webpack
    3. 控制包文件大小(tree shaking / splitChunksPlugin
    4. 使用 DllPlugin 提高打包速度
  • 缓存/压缩
    1. 客户端缓存/服务端缓存
    2. 服务端 gzip 压缩

扩展:按需加载的实现原理?

其实就是在 visitor 对象上设置响应的方法(节点类型),然后去处理符合要求的节点,将节点上对应的属性更改为目标代码上响应的值

babel 插件 -> 函数会有个 babelTypes 参数(包含 types) -> 规定返回一个 visitor 对象(然后在 visitor 中编写获取各个节点的方法)

Identifier:负责处理所有节点类型为 IdentifierAST 节点

VariableDeclaration:处理变量声明关键字

import 对应的 ImportDeclaration 的节点

Vue.use(ElementUI)对应于 ExpressionStatement 类型的节点

13. Vue2 和 Vue3 中计算属性的区别?

Vue3 中计算属性也要收集依赖。而 Vue2 中计算属性不具备收集依赖的

  • computed 设计的初衷是: 为了使模板中的逻辑运算更简单。它有两大优势:
    1. 使模板中的逻辑更加清晰,方便代码管理
    2. 计算之后的值会被缓存起来,依赖的 data 值改变后会重新计算

问题 1:Vue2computed 是如何初始化的?

Vue() -> this.\_init() -> initState() -> initComputed()

  • initComputed()做了哪些事?
    1. 首先使用 Object.create(null); 创建一个空对象, 分别赋值给 watchers; 和 vm._computedWatchers
    2. const isSSR = isServerRendering(); 判断是否是服务器端渲染
    3. 使用 for in 循环遍历 computed, 判断用户写的 computed 是函数还是对象 const userDef = computed[key] ; const getter = typeof userDef === 'function' ? userDef : userDef.get
    4. 会根据 computed 中的 key 来实例化 watcher,因此我们可以理解为其实 computed 就是 watcher 的实现, 通过一个发布订阅模式来监听的。给Watch方法传递了四个参数, 分别为VM实列, 上面我们获取到的getter方法, noop 是一个回调函数。computedWatcherOptions参数我们在源码初始化该值为:const computedWatcherOptions = { lazy: true }
    5. 如果 computed中的key没有在vm中, 则通过defineComputed挂载上去。第一次执行的时候, vm中没有该属性的
    6. defineComputed方法中首先执行 const shouldCache = !isServerRendering(); 判断是不是服务器端渲染。该参数的作用是否需要被缓存数据, 为 true 是需要被缓存的。也就是说我们的这里的 computed 只要不是服务器端渲染的话, 默认会缓存数据的。
    7. 接着会判断 userDef 是否是一个函数, 如果是函数的话,说明是我们的computed的用法。因此 sharedPropertyDefinition.get = createComputedGetter(key); 的返回值。重新定义 getter
    8. Object.defineProperty(target, key, sharedPropertyDefinition); 使用Object.defineProperty来监听对象属性值的变化;只要我们的data对象中的某个属性发生改变的话, 我们的reversedMsg方法中依赖了该属性的话, 也会调用sharedPropertyDefinition方法中的get/set方法的。

重点:但是在我们的页面第一次初始化的时候, 我们要如何初始化执行computed中的对应方法呢?

  1. [initMixin]中的\_init() -> vm.$mount(vm.$options.el)该代码的作用是对我们的页面中的模板进行编译操作。

  2. [entry-runtime-with-compiler.js] 中重写\$mount -> mount.call(this, el, hydrating);

  3. [runtime/index.js]中定义 Vue.prototype.$mount -> mountComponent(this, el, hydrating)

  4. [instance/lifecycle.js]中会new 一个Watcher进行实列化了,此时this.lazyfalse,执行this.get()函数, 也就是说执行了 this.getter.call(vm, vm)方法

  5. 最后回到[instance/state.js] -> createComputedGetter() -> return watcher.value

Vue3computed 的实现原理?

  1. [reactivity/computed.ts] -> computed(getterOrOptions) 参数有可能是函数有可能包含getset方法的对象
  2. 如果是函数,就把 getterOrOptions 赋值给 getter,setter 提示警告不让修改。如果对象,就把 getterOrOptions.get 赋值给 gettergetterOrOptions.set赋值给setter
  3. return new ComputedRefImpl(getter, setter)

重点ComputedRefImpl的实现?

  1. 在构造函数中创建一个 effect,响应式的计算属性。将 getter 作为 effect 的回调函数,并传入 lazy: true 默认不执行和 scheduler 函数的选项
  2. 在类中定义get value(), set value(newValue)访问器。用来获取和设置。在 get value()中如果 this.\_dirty 为真(默认不执行,脏的)时,将会将用户的返回值返回 this.\_value = this.effect();并将 this.\_dirty=false,下次调用的时候不重新执行。最后返回 return this.\_value;在设置值时,set value()会执行 this.setter(newValue)

如果 scheduler存在就执行scheduler,不存在就执行effect

注意:当计算属性直接在 effect 中访问的时候,我们需要监听 value 属性,做依赖收集 track(this, 'value')

14. Vue 和 React 的区别?

15. 你知道 Vue2 的模板编译原理吗?和 Vue3 做了哪些改进?

16. 有写过自定义指令吗?自定义指令的应用场景?

17. 前端路由的原理?

18. 说一下 Vue2 中 Diff 算法?Vue3 做了哪些改进?

虚拟 DOM 是一个 JS 对象. 虚拟 DOM 算法 = 虚拟 DOM + Diff 算法

  • 使用虚拟 DOM 算法的损耗计算: 总损耗 = 虚拟 DOM 增删改 +(与 Diff 算法效率有关)真实 DOM 差异增删改 +(较少的节点)排版与重绘

  • 直接操作真实 DOM 的损耗计算: 总损耗 = 真实 DOM 完全增删改 +(可能较多的节点)排版与重绘

  • Diff 算法的目的是什么? 为了减少 DOM 操作的性能开销,我们要尽可能的复用 DOM 元素。所以我们需要判断出是否有节点需要移动,应该如何移动以及找出那些需要被添加或删除的节点。

diff 算法: 是一种通过同层的树节点进行比较的高效算法, diff 整体策略为:深度优先同层比较. 其有两个特点:

  1. 比较只会在同层级进行, 不会跨层级比较
  2. 在 diff 比较的过程中,循环从两边向中间比较

原理分析 源码位置:src/core/vdom/patch.js

当数据发生改变时,set 方法会调用 dep.notify 通知所有订阅者 Watcher,订阅者就会调用 patch 给真实的 DOM 打补丁,更新相应的视图

el: 真的的 DOMoldVnode: 旧节点,newVnode: 新节点

  • 对比流程:

    1. set -> dep.notify -> patch(oldVnode, newVnode) -> isSameVnode()
    2. 不是, 直接替换
    3. 是走 patchVnode
        1. oldVnode 有子节点, newVnode 没有
        1. oldVnode 没有子节点, newVnode
        1. 都是文本节点
        1. 都有子节点
    4. updateChildren
  • sameVnode 方法判断是否为 同一类型 的节点。如何才算同一类型的节点?sameVnode(oldVnode, newVnode) 几种情况:

    1. key 值是否一样
    2. tagName 标签名是否一样
    3. isComment 是否都是注释节点
    4. 是否都定义了 data(class, style) 是否一样
    5. sameInputType() 当前节点为 input 时,type 必须相同
  • patchVnode 函数做了哪些事情?

    1. 获取对应的真实 DOM -> el
    2. 判断 newVnodeoldVnode 是否指向 同一个对象,如果是,直接返回
    3. 如果他们都有文本节点并且不相同,那么将 el 的文本节点设置为 newVnode 的文本节点
    4. 如果 oldVnode 有子节点而新节点没有,则删除 el 的子节点
    5. 如果 oldVnode 没有子节点而新节点有,则将 newVnode 的子节点真实化之后添加到 el
    6. 如果两者都有子节点,则执行 updateChildren 函数比较子节点
  • updateChildren 函数五种比较情况:

    1. 旧头 & 新头
    2. 旧头 & 新尾
    3. 旧尾 & 新头
    4. 旧尾 & 新尾
    5. 如果以上逻辑都不匹配,再把所有旧子节点的 key 做一个映射到旧节点下标的 key -> index 表,然后用新 vnodekey 去找出在旧节点中 可以复用 的位置

19. 描述一下 Vue2 以及 Vue3 组件的渲染和更新的过程?

20. 简述一下 Vuex 工作原理?

21. Vue 中事件绑定原理?

22. Vue2 和 Vue3 中 v-model 的实现原理?

23. Vue3.0 是如何变得更快的?

  • diff 方法优化
    1. Vue2.x 中的虚拟 dom 是进行全量的对比
    2. Vue3.0 中新增了静态标记(PatchFlag) 在与上次虚拟节点进行对比的时候,只对比带有 patch flag 的节点,并且可以通过 flag 的信息得知当前节点要对比的具体内容。
  • hoistStatic 静态提升
    1. Vue2.x : 无论元素是否参与更新,每次都会重新创建
    2. Vue3.0 : 对不参与更新的元素,只会被创建一次,之后会在每次渲染时候被不停的复用
  • cacheHandlers 事件侦听器缓存 默认情况下 onClick 会被视为动态绑定,所以每次都会去追踪它的变化但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可

Vue 基本原理

数据双向绑定,需要三大模块:

  1. Observer: 能够对数据对象的所有数据进行监听,如有变动可拿到最新值并通知订阅者。
  2. Compile: 对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
  3. Watcher: 作为连接 ObserverCompiler 的乔辽,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
  • Observer 的核心是通过 Object.defineProperty() 来监听数据的变动,这个函数内部可以定义 settergetter,每当数据发生变化,就会触发 setter。这时候 Observer 就要通知订阅者。订阅者就是 Watcher

  • Watcher 订阅者作为 ObserverCompiler 之间通信的桥梁,主要做的事情是:

    • 在自身实例化时往属性订阅器(dep)里面添加自己
    • 自身必须有一个 update() 方法
    • 待属性变动 dep.notify() 通知时,能调用自身的 update() 方法,并触发 Compiler 中绑定的回调
  • Compiler 主要做的是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

Vue 生命周期

每个 Vue 实例在被创建之前都要经过一系列的初始化过程,这个过程就是 vue 的生命周期。下面是官方文档提供的一张图:

每个 Vue 组件完整的生命周期都会有很多钩子函数,例如:

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed

注意Vue 为了结合<keep-alive></keep-alive>组件,提供了两个额外的钩子函数

  • activated
  • deactivated

下面我们通过详细的代码来分析Vue组件生命周期

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>vue生命周期学习</title>
    <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
  </head>

  <body>
    <div id="app">
      <h1>{{msg}}</h1>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'Vue的生命周期',
      },
      beforeCreate() {
        console.group('------beforeCreate创建前状态------')
        console.log('%c%s', 'color:red', 'el     : ' + this.$el) //undefined
        console.log('%c%s', 'color:red', 'data   : ' + this.$data) //undefined
        console.log('%c%s', 'color:red', 'msg: ' + this.msg)
      },
      created() {
        console.group('------created创建完毕状态------')
        console.log('%c%s', 'color:red', 'el     : ' + this.$el) //undefined
        console.log('%c%s', 'color:red', 'data   : ' + this.$data) //已被初始化
        console.log('%c%s', 'color:red', 'msg: ' + this.msg) //已被初始化
      },
      beforeMount() {
        console.group('------beforeMount挂载前状态------')
        console.log('%c%s', 'color:red', 'el     : ' + this.$el) //已被初始化
        console.log(this.$el)
        console.log('%c%s', 'color:red', 'data   : ' + this.$data) //已被初始化
        console.log('%c%s', 'color:red', 'msg: ' + this.msg) //已被初始化
      },
      mounted() {
        console.group('------mounted 挂载结束状态------')
        console.log('%c%s', 'color:red', 'el     : ' + this.$el) //已被初始化
        console.log(this.$el)
        console.log('%c%s', 'color:red', 'data   : ' + this.$data) //已被初始化
        console.log('%c%s', 'color:red', 'msg: ' + this.msg) //已被初始化
      },
      beforeUpdate() {
        console.group('beforeUpdate 更新前状态===============》')
        console.log('%c%s', 'color:red', 'el     : ' + this.$el)
        console.log(this.$el)
        console.log('%c%s', 'color:red', 'data   : ' + this.$data)
        console.log('%c%s', 'color:red', 'msg: ' + this.msg)
      },
      updated() {
        console.group('updated 更新完成状态===============》')
        console.log('%c%s', 'color:red', 'el     : ' + this.$el)
        console.log(this.$el)
        console.log('%c%s', 'color:red', 'data   : ' + this.$data)
        console.log('%c%s', 'color:red', 'msg: ' + this.msg)
      },
      beforeDestroy() {
        console.group('beforeDestroy 销毁前状态===============》')
        console.log('%c%s', 'color:red', 'el     : ' + this.$el)
        console.log(this.$el)
        console.log('%c%s', 'color:red', 'data   : ' + this.$data)
        console.log('%c%s', 'color:red', 'msg: ' + this.msg)
      },
      destroyed() {
        console.group('destroyed 销毁完成状态===============》')
        console.log('%c%s', 'color:red', 'el     : ' + this.$el)
        console.log(this.$el)
        console.log('%c%s', 'color:red', 'data   : ' + this.$data)
        console.log('%c%s', 'color:red', 'msg: ' + this.msg)
      },
    })
  </script>
</html>

可以看到一个Vue实例在创建到渲染会执行四个钩子函数

  1. beforeCreatecreated阶段

进行初始化事件,进行数据的观测,可以看到在 created 的时候数据已经和 data 属性进行绑定 注意:此时还获取不到dom

  1. createdbeforeMount阶段

主要做以下事情: 判断对象是否有el选项;若有,则继续向下编译;若没有el选项,则停止编译,也就意味着停止了生命周期,直到在该vue实例上调用vm.$mount(el)注意:要注意template选项的有无对生命周期的影响 1)、若vue实例对象中有template参数选项,则将其作为模板编译成render函数。 2)、若没有template选项,则将外部 HTML 作为模板编译。 3)、template中的模板优先级要高于outer HTML的优先级。 注意:优先级

render函数 > template > outer HTML
  1. beforeMountmounted阶段

此时vue实例对象添加$el,并且替换掉挂在的DOM元素。注意beforeMount之前el上还是undefined

  1. mounted阶段

beforeMount时,此时还是虚拟DOM存在;在mounted阶段,就可以看到真实的DOM;注意:此时经常是调接口的地方。

  1. beforeUpdateupdated阶段

当组件的data选项里数据发生变化,会触发对应组件重新渲染,系统会先后调用beforeUpdateupdated钩子函数。

  1. beforeDestroydestroyed阶段

beforeDestroy钩子函数在实例销毁之前调用。此时,实例仍然完全可用。 destroyed钩子函数在Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

Vue 组件间通信

Vuex 的原理及理解

Vue-Router 的原理及理解

问题: vue-router 中路由方法 pushState 和 replaceState 能否触发 popState 事件?

答案是:不能

pushState 和 replaceState 是 HTML5 新接口,可以改变网址(存在跨域限制)而不刷新页面,这个强大的特性后来用到了单页面应用如:vue-router,react-router-dom 中。

注意: 仅改变网址,网页不会真的跳转,也不会获取到新的内容,本质上网页还停留在原页面

window.history.pushState(state, title, targetURL);
@状态对象:传给目标路由的信息,可为空
@页面标题:目前所有浏览器都不支持,填空字符串即可
@可选url:目标url,不会检查url是否存在,且不能跨域。如不传该项,即给当前url添加data

window.history.replaceState(state, title, targetURL);
@类似于pushState,但是会直接替换掉当前url,而不会在history中留下记录

总结popstate 事件会在点击 后退前进 按钮(或调用 history.back()history.forward()history.go()方法)时触发

Vue 组件间生命周期的调用顺序

Vue 如何监听数组

Provide 与 inject 用法

EventBus

Watch 高级用法

路由懒加载

Vue 的优缺点

Assets 和 Static 的区别

Vue 路由钩子函数

Vue v-model 原理

内部会根据标签的不同解析出,不同的语法

  • 如: 文本框会被解析成 value + input 事件
  • 如: 复选框会被解析成 checked + change 事件
  • ...