Vue面试

190 阅读12分钟

mvvm 前置

mvc mvvm

都是为了解决 model 层和 view 层的耦合性问题。

MVC

早期的 MVC 模式 主要应用到后台的。

  • 在早期的时候,前端只负责 view 成的展示,model 层和 control 层都会在后台进行。
  • ajax 流行起来以后真正有了前端 mvc,会带来灵活性变的很差,而且代码的可读性和可维护性问题
  • 视图修改会通知控制器,从而去修改 model 层的数据

优缺点

分层清晰,但是大部分的逻辑都是通过 control 层去修改,造成代码混乱,臃肿不好维护。

MVVM(数据驱动视图)

  • 它的核心是 ViewModel 层,通过一套数据响应机制自动响应 model 层中的变化。同时实现一套自动更新策略,将数据变化转换为视图更新。
  • 同时 ViewModel 通过事件监听视图层的修改,从而修改 model 中的数据
  • MVVM 会保持 view 和 Model 的低耦合,使开发者专注于业务逻辑,提高开发效率和代码运行效率

优点 解决了维护 Model 层和 view 层映射关系之间的大量代码,比如数据更新后视图怎么更新。提高了开发效率,和代码的可读性同时还保持了优越的性能。

mvc 和 MVVM 的区别

mvc 只是静态渲染,更新还要依赖于操作 DOM. MVVM 有了数据驱动视图,

virtual Dom

对于一些复杂的页面来说,DOM 结构是非常的复杂的,每次操作 DOM,渲染引擎都需要进行重排,重绘等操做,而这些操作在复杂的页面情况下,是非常的耗时的

  • Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。

对比优点

  • 这是一个性能 vs. 可维护性的取舍。virtual DOM 在框架内部已经做了优化,使得不管每次渲染多少数据,它的性能都是可以接受的,而且使得代码更容易维护。
  • 将 Virtual DOM 作为一个兼容层,让我们还能对接非 Web 端的系统,实现跨端开发。比如 taro,uniapp 等通过虚拟 dom,然后通过 ast 树进行编译适应其他不同的平台。
  • 通过 Virtual DOM 我们可以渲染到其他的平台,比如实现 SSR、同构渲染等等。
  • 实现组件的高度抽象化

vue 原理

new Vue 后发生了什么

function Vue(options) {
  this._init(options);
}
  • 1.初始化,主要就干了几件事,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 props、methods、 data、computed、watcher 等
initLifecyle: // 初始化一些属性如$parent,$children。根实例没有 $parent,$children 开始是空数组,直到它的 子组件 实例进入到 initLifecycle 时,才会往父组件的 $children 里把自身放进去。所以 $children 里的一定是组件的实例。

initEvents // 初始事件$events,$on,$once,$off,$emit
initRender // 初始化渲染相关如 $createElement
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) //

初始化 state
    ·初始化 props
    ·初始化 methods
    ·初始化 data
    ·初始化 computed
    ·初始化 watch
所以在 data 中可以使用 props 上的值,反过来则不行。

initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

  • 2.开始挂载:如果监测到有 el 属性。则调用$mount 方法挂在 vue 实例,把模板渲染成最终的 DOM。然后调用 mountcomponent 方法 开始渲染
  • 3.在 mountcomponent 里面开始调用 beforeMount 钩子函数,然后实例化一个渲染的 watcher,里面调用一个 updateComponent 回调函数,在这个方法中调用_render 方法生成 Vnode,最后调用_update 方法更新生成 DOM
if (vm.$options.el) {
  vm.$mount(vm.$options.el);
}

vue 的响应式原理是什么

简化版本

  • vue 响应式原理主要是采用数据劫持结合发布者-订阅者模式的方式来实现的
  • 在 vue 初始化阶段, 会通过 Object.defineProperty 给 props,和 data 里面的数据添加了 getter 和 setter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新,每个属性都有一个 Dep 依赖收集器实例。
  • Vue 执行一个组件的时候,会实例化一个渲染的 Watcher, Watcher 在执行前会把 Watcher 自身先赋值给 Dep.target 这个全局变量, 在这个过程中会对 vm 上的数据进行访问,这个时候触发了对象的 getter,把当前的 watcher 订阅到这个数据依赖收集器中。
  • 当数据响应式属性发生更新时,会在 setter 里面派发更新,通知 依赖集里面的 Watcher 进行 update().。

补全版本

  • vue 响应式原理的核心是观察数据的变化,当数据发生变化后通知到对应的观察者执行他的逻辑
  • dep 是连接数据和观察者的桥梁,在 vue 初始化阶段,对 data 和 props 上对象每一个属性都变成响应式的,同时内部会持有一个 dep 的实例。
  • 当我们去访问这些属性的时候,会触发 dep 的 depend 方法来收集依赖。收集的是当前的 dep.target,会被作为一个订阅者来订阅属性的变化。
  • 当我们修改属性的时候,会调用 dep.notify()方法来通知这些订阅者,进行 update()
  • computed 内部会创建一个 computed watcher。会持有一个 dep 实例,当我们访问属性的时候,会调用 emit 计算方法,会收集当时正在计算的 watcher 作为依赖。当 computed 里面属性变了,并且内部通过计算发现返回的结果变了, 也会调用 dep 的 notify(),通知 computed 的订阅者进行更新
  • watch 会创建一个 user watcher,来观测数据变化。当观测的数据发生变化会通知 dep,来触发 user watcher 的 update 的方法。
  • vue 每个组件都会执行 mount 方法,内部会创建一个唯一的 render watcher,当他被通知变化后调用 updateComponent,进行组件的更新

===============

  • 去重新调用 vm._update(vm._render()) 进行组件的视图更新
  • Dep 类有个静态属性 target,这个是全局唯一的,统一时间只能有一个全局的 Watcher 指向 Dep.target
updateComponent = () => {
  vm._update(vm._render(), hydrating);
};
new Watcher(
  vm,
  updateComponent,
  noop,
  {
    before() {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate');
      }
    }
  },
  true /* isRenderWatcher */
);

vue 双向绑定原理

vue 的双向绑定原理是结合 v-model 语法糖,和 vue 的响应式原理来实现的

  • 简单来说,双向数据绑定就是给有 v-xxx 指令组件添加 addEventListner 的监听函数,一旦事件发生,就调用 setter,从而调用 dep.notify()通知所有依赖 watcher 调用 watcher.update()进行更新
<input v-model="sth" />  //这一行等于下一行
<input v-bind:value="sth" v-on:input="sth = $event.target.value" />

vue 为什么采用异步渲染

如果同步渲染的话,每次更新都有渲染会有性能消耗问题。

属性发生变化后的更新

  • dep.notify 当前属性会通知 所有依赖于它的 watcher 进行 update()。
  • update() 会将 使用(queueWatcher)把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue。, 进行批量的更新。

// 需要注意的点

  • 1.组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。

  • 2.用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。

  • 3.如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。

vue 怎么进行异步渲染 也就是更新的

  • 在 setter 里面派发更新后,会调用每个 Watcherd update()方法。
  • update 方法中会调用 queueWatcher 方法,把 watcher 实例都放进一个队列当中
  • 然后在 nextTick 后执行这些 watcher 的 run 方法, 会先通过 this.get() 得到它当前的值,
  • this.get() 方法求值的时候,会执行 updateComponent 方法,触发组件的更新 :

组件更新的具体机制

  • 1.Vue 对于响应式属性的更新,只会精确更新依赖收集的当前组件,而不会递归的去更新子组件
  • 2.有子组件的话,只会对 component 上声明的 props、listeners 等属性进行更新,而不会深入到组件内部进行更新
  • 如果子组件里面使用的 props 的属性,就会进入子组件的更新流程
  • 3.如果子组件里面有 slot 插槽,使用了父组件的属性的话,它会调用子组件的 $forceUpdate(),所以父子组件的都会重新渲染
if (hasChildren) {
  vm.$slots = resolveSlots(renderChildren, parentVnode.context);
  vm.$forceUpdate();
}
<ul>
// 只会更新这个data的值
  <component :data="msg">1</component>
  <component>2</component>
  <component>3</component>
<ul>

$forceUpdate()

vm.$forceUpdate 本质上就是触发了渲染 watcher 的重新执行,和你去修改一个响应式的属性触发更新的原理是一模一样的,它只是帮你调用了 vm._watcher.update()

Vue 的编译过程

  • 1.将模板解析为 AST
  • 2.优化 AST
  • 3.将 AST 转换为 render 函数,遍历整个 AST,根据不同的条件生成不同的代码罢了。

谈谈你对插槽的理解

  • 普通插槽 具名插槽和非具名插槽
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
</div>
  • 作用域插槽,让插槽里面能访问子组件的数据
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>

普通插槽和作用域插槽的不同点

  • 普通插槽是在父组件编译和渲染阶段生成 vnodes,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 vnodes
  • 作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnode 的 data 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例

nextTick (src/core/util/next-tick.js )

  • 异步任务执行的顺序是建立与优先级之上,vue 的异步队列默认优先使用 micro task 就是利用其高优先级的特性,保证队列中的微任务在一次循环全部执行完毕。

  • 首先依次去检测 Promise -> MutationObserver -> setImmediate -> setTimeout 是否存在,哪个存在的话就使用它, 来作为执行异步回调函数队列的 api

  • 其次在 nextTick 方法中会把传入的 cb 回调函数,用 try-catch 包裹后放在一个匿名函数中推入 callbacks 任务队列数组中

  • 在任务队列异步方法执行之前,还可以往任务队列里面推送任务。

  • 当任务队列的异步方法开始回调执行后,就开始执行这个队列中的任务。

  • 同时在 nextTick 方法 执行 timerFunc 中,会检查 一个 pending 标志位的状态,确保不会每次调用 $nextTick 方法都会执行异步回调方法。而是在一次 nextTick 异步调用执行后,会重新把这个标志重置,同时清空任务队列。因此这个时候新的任务又会推入到任务队列当中。

  • 这个队列可能是 microTask 队列,也可能是 macroTask 队列

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  // 清空
  callbacks.length = 0
  // 循环执行队列
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
export function nextTick(cb? Function, ctx: Object) {
    let _resolve
    // cb 回调函数会统一处理压入callbacks数组
    callbacks.push(() => {
        if(cb) {
            try {
                cb.call(ctx)
            } catch(e) {
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
if (!pending) {
  pending = true;
  timerFunc();
}
timerFunc(flushCallbacks)

Object.defineProperty 的缺点

  • 无法检测到对象属性的新增或删除
  • PS Object.defineProperty 可以监听到数组的下标变化,尤大说 性能代价和获得的用户体验收益不成正比,所以采用改写数组方法的方法来实现响应式更新

// segmentfault.com/a/119000001…

解决针对无法检测到对象属性的新增或删除

vue 内部提供了一个 set 方法

  • 首先判断是否是数组,用数组已经被改写的方法进行观察
  • 然后判断是否是已有的值,直接返回值
  • 然后判断是否是响应是对象,原型上有没有__ob__
  • 如果都不是调用 defineReactive 进行双向绑定,
  • 然后手动派发更新。

针对数组无法监听,对数组方法进行拦截

  • 将数组的原型指向这个改变了数组方法的原型。(arr__proto__ == arrayMtehods)
  • 将所有能改变数组本身的方法进行了重写,如'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'。
  • 并对能增加数组长度的 3 个方法 push、unshift、splice 方法做了判断,获取到插入的值,然后把新添加的值变成一个响应式对象
  • 并且再调用 ob.dep.notify() ,手动触发依赖通知
export function set(target: Array<any> | Object, key: any, val: any): any {
  // 判断是否为数组且下标是否有效
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 调用 splice 函数触发派发更新
    // 该函数已被重写
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }
  // 判断 key 是否已经存在
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  const ob = (target: any).__ob__;
  // 如果对象不是响应式对象,就赋值返回
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 进行双向绑定
  defineReactive(ob.value, key, val);
  // 手动派发更新
  ob.dep.notify();
  return val;
}
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // notify change
    ob.dep.notify();
    return result;
  });
});

组件中的 data 为什么是函数

  • 组件里 data 直接写了一个对象的话,那么如果你在模板中多次声明这个组件,组件中的 data 会指向同一个引用。
  • 此时如果在某个组件中对 data 进行修改,会导致其他组件里的 data 也被污染。 而如果使用函数的话,每个组件里的 data 会有单独的引用,这个问题就可以避免了。

vue 为什么用 key,vue 的 diff 过程

vue 的 diff 过程

组件更新的核心,就是新旧节点进行 diff 的过程

1.不是相同节点:

isSameNode 为 false 的话,,直接销毁旧的 vnode,渲染新的 vnode。这也解释了为什么 diff 是同层对比。

2.相同节点

  • 如果 vnode 是个文本节点且新旧文本不相同,则直接替换文本内容。如果不是文本节点,则判断它们的子节点,
  • 如果有新 children 而没有旧 children, 说明是新增 children,直接 addVnodes 添加新子节点。
  • 如果有旧 children 而没有新 children, 说明是删除 children,直接 removeVnodes 删除旧子节点
  • 如果都存在的话,使用 updateChildren 进行对比.

updateChildren

// 旧首节点
let oldStartIdx = 0;
// 新首节点
let newStartIdx = 0;
// 旧尾节点
let oldEndIdx = oldCh.length - 1;
// 新尾节点
let newEndIdx = newCh.length - 1;

循环进行对比,循环的逻辑

  • oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx

  • 1.旧首节点和新首节点用 sameNode 对比。

  • 2.旧尾节点和新尾节点用 sameNode 对比

  • 3.旧首节点和新尾节点用 sameNode 对比

  • 4.旧尾节点和新首节点用 sameNode 对比

  • 有新节点需要加入。如果更新完以后,oldStartIdx > oldEndIdx,说明旧节点都被 patch 完了,但是有可能还有新的节点没有被处理到。接着会去判断是否要新增子节点。

  • 有旧节点需要删除。如果新节点先 patch 完了,那么此时会走 newStartIdx > newEndIdx 的逻辑,那么就会去删除多余的旧子节点。

function sameVnode(a, b) {
  return a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b);
}

v-for 里面什么时候用 index 作为 key,什么时候不用 index

  • 当 v-for 进行简单的列表渲染操作的时候,使用 index 作为 key ,会更加高效。

    当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素这个策略是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出

  • 当列表的数据会进行重新排序和添加删除的复杂操作的时候,不建议用 index
  • 随机数作为 key,旧节点会被全部删掉,新节点重新创建。

顺序变换后

  • 现在新旧两个首节点的 key 都是 0,会使用 sameNode 来做对比,这一步命中逻辑
  • 然后把旧的节点中的第一个 vnode 和 新的节点中的第一个 vnode 进行 patchVnode 操作

删除数据后 // juejin.cn/post/684490…

sameNode 只会判断 key、 tag、是否有 data 的存在(不关心内部具体的值)、是否是注释节点、是否是相同的 input type,来判断是否可以复用这个节点。

因为使用的是 index,所以旧节点的值都可以复用,然后把多出来的节点进行删除。删除的永远是最后面的节点

Vue 的优点是什么

  • 简单易学,容易让人上手快速的开发项目
  • 成熟的轻量级框架,内部做了很多优化,性能好

vue 一些内置方法

computed 和 watch 有什么区别

  • computed 具有缓存性,computed 里面有一个 计算的 watcher,内部有个 dirty 属性,来决定 computed 的值是需要重新计算求职还是直接复用之前的值 value

  • watch 是监听一个属性,如果属性发生变化就立即更新属性

computed 2.6 版本

概括

  • 初始化 computed 属性的时候,会循环实例化计算属性的 watcher。这个时候实例初始化的时候只会生成一个 dep 的依赖实例,并不会收集依赖。
  • 当页面渲染访问到 computed 属性的时候,才会判断 dirty 是否为 true,也就是是否是脏数据。如果是的话就调用回调函数进行求值,如果不是的话就返回之前缓存的 value 值.默认为 true,会进行求值.这就是 计算属性缓存的概念
  • computed 里面响应式数据更新的时候,会把计算的 watcher 和渲染的 watcher 都加入到他的依赖集当中去。当开始变化的时候,计算 watcher 的 update()会把 dirty 置为 true, 而渲染 watcher,在重新渲染页面后会读取到 computed 的值 这个时候 dirty 是 true,便会再次求新值。
  • 当其他数据发生变化后不会影响 computed 里面的值。
- 2.5版本它会在每次 computed 依赖更新的时候都再去执行一遍用户 computed 的 getter 函数。其实会造成太多次的计算了。
- 2.5  以前的版本会有个判断如果conputed返回的值没变就不会重新渲染,会重新计算但不会重新渲染。
- 2.6 只要computed里面的依赖发生了变化都会重新渲染

概括总结 2

ComputedWatcher 和普通 Watcher 的区别:
1. 用 lazy 为 true 标示为它是一个计算Watcher
2. 计算Watcher的get和set是在初始化(initComputed)时经过 defineComputed() 方法重写了的
3. 当它所依赖的属性发生改变时虽然也会调用ComputedWatcher.update(),但是因为它的lazy属性为true,所以只执行把dirty设置为true这一个操作,并不会像其它的Watcher一样执行queueWatcher()或者run()
 4. 当有用到这个ComputedWatcher的时候,例如视图渲染时调用了它时,才会触发ComputedWatcher的get,但又由于这个get在初始化时被重写了,其内部会判断dirty的值是否为true来决定是否需要执行evaluate()重新计算
5. 因此才有了这么一句话:当计算属性所依赖的属性发生变化时并不会马上重新计算(只是将dirty设置为了true而已),而是要等到其它地方读取这个计算属性的时候(会触发重写的get)时才重新计算,因此它具备懒计算特性。

具体描述

初始化

  • 初始化的时候会遍历 computed 里面的属性,然后实例化一个 watcher,创建的时候不会里面求值,只是先实例化一个 dep 依赖集。
  • 当我们访问 computed 的属性的时候,会判断 dirty 是否为 true,然后调用 watcher.evaluate()的求值.,如果是 false 的话就跳过直接返回值,这就是 计算属性缓存的概念

更新

  • 首先在模板使用 computed 的属性的时候,全局的 Dep.target 是 组件渲染的 watcher。

    此时的 Dep.target 是 渲染 watcher,targetStack 是 [ 渲染 watcher ] 。

  • 计算属性 watcher.evaluate 的时候 调用 this.get(), 也就是把 计算 watcher 自身置为 Dep.target,等待收集依赖。

    此时的 Dep.target 是 计算 watcher,targetStack 是 [ 渲染 watcher,计算 watcher ] 。

  • 然后会执行 sum 里面传入的方法, 里面的 this.count 会执行它的 setter 来收集依赖,这个时候 count 里面就会添加这个计算的 watcher 依赖.

  • 同时计算属性的 deps 里面有 count的dep

  • 然后,会执行 watcher.depend(),所以这个 count 的 dep 又会把 渲染 watcher 存放进自身的 subs 中。

    subs: [ sum 的计算 watcher,渲染 watcher ]

  • 当 count 更新后,把 subs 里保存的 watcher 依次去调用它们的 update 方法

  • 计算 watcher 的 update()会把 dirty 设置为 true,渲染 watcher 的 update 会重新渲染视图,这个时候再次读取到 sum ,会计算求值

总结 2.6 版本计算属性更新

  • 1.响应式的值 count 更新,同时通知 computed watcher 和渲染 watcher 更新
  • 2.computed watcher 更新的 update 会把 dirty 设置为 true,等待下一次调用它的时候才计算
  • 3.图渲染读取到 computed 的值,由于 dirty 所以 computed watcher 重新求值。
{
    computed: {
      sum() {
        return this.count + 1
      },
    }
}
// 初始化computed后
{
    deps: [],
    dirty: true,
    getter: ƒ sum(),
    lazy: true,
    value: undefined
}
Object.defineProperty(vm, 'sum', {
    get() {
        // 从刚刚说过的组件实例上拿到 computed watcher
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          // ✨ 注意!这里只有dirty了才会重新求值
          if (watcher.dirty) {
            // 这里会求值 调用 get
            watcher.evaluate()
          }
          // ✨ 这里也是个关键 等会细讲
          if (Dep.target) {
            watcher.depend()
          }
          // 最后返回计算出来的值
          return watcher.value
        }
    }
})
 evaluate () {
    this.value = this.get()
    this.dirty = false
  }
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } finally {
    popTarget()
  }
  return value
}

watch

也是组件初始化的时候,在 computed 后, initWatch。循环 watcher,调用 createrWatcher 方法,里面就是通过 vm.$watch 创建了一个 user watcher

watch 会把 options.user = true, this.get()就会进行依赖收集,数据发生变化后就会直接执行回调函数,


通过 proxy方法 vm上直接可以访问到_data上的属性。
      proxy(vm, `_data`, key)

watcher 里面的 sync

触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才会真正执行 watcher 的回调函数。而一旦我们设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数。

 update () {
    /* istanbul ignore else */
    // computed
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      // watch
      this.run()
    } else {
      // 其他
      queueWatcher(this)
    }
  }

watcher 里面的 deep

当设置了 deep 的时候 ,在 get()方法执行后会执行一个 traverse()方法,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,

keep-alive 的原理

<keep-alive>
    <component><component />
</keep-alive>
  • 被 keepalive 包含的组件不会被再次初始化,也就意味着不会重走生命周期函数,会多出两个生命周期的钩子: activated 与 deactivated:
  • 通过 include/exclude 进行条件匹配决定缓存
  • 原理: 根据组件 ID 和 tag 生成缓存 key,并在缓存对象中查找是否已经缓存该实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

LRU 缓存淘汰算法

  • 将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

vue2 和 vue3 的区别

这个区别本质上就是本质上,就是基于 Proxy 和基于 Object.defineProperty 之间的差异,

  • 1.Object.defineProperty 只能对已有的值进行监听,不能对新增的属性做出响应,只能通过实现一个语法来实现
  • 2.Proxy 并不关新 key 值,
const raw = {};
const data = new Proxy(raw, {
  get(target, key) {},
  set(target, key, value) {}
});

Vuex

1.组件之间的通信方式

父子组件

  • props/$emit
  • parentparent和children,访问父子组件的实例
  • provide/ inject:简单来说就是父组件中通过 provide 来提供变量, 然后再子组件中通过 inject 来获取变量。
  • 父组件通过 refs 来获取子组件

兄弟组件

  • eventBus 又称为事件总线,在 vue 中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件
  • vuex 状态管理器
  • localStorage / sessionStorage

跨级通信

  • eventBus;Vuex;provide / inject 、attrs/attrs / listeners

  • attrs包含了父作用域中不作为prop被识别(且获取)attribute绑定(classstyle除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(classstyle除外),并且可以通过vbind="attrs包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="attrs" 传入内部组件

// 父级
<child-com1
      :name="name"
      :age="age"
      :gender="gender"
      :height="height"
      title="程序员成长指北"
    ></child-com1>
// 子级
props: {
    name: String // name作为props属性绑定
  },
  created() {
    console.log(this.$attrs); // 没有name, 因为在props里面声明了
     // { "age": "18", "gender": "女", "height": "158", "title": "程序员成长指北" }
  }
=

vuex 的优点

vuex 可用使数据流变得清晰、可追踪、可预测,更可以简单的实现 类似时光穿梭 等高级功能,大大提高项目的稳定性,扩展性

vuex 原理

组件内部是怎么访问到 vuex

  • vuex 和所有的 vue 插件一样,会在初始化的时候调用 install 方法。

  • 在 install 中利用了 vue 的 mixin 机制,混合了 beforeCreat 钩子。将store注入到了vue组件的实例上。这样vue组件里面实例都能访问到store 注入到了 vue 组件的实例上。这样vue组件里面实例都能访问到store

    vuex 的实现逻辑

  • state: 在 store 的构造函数里面有个 resetStoreVM 函数,这个函数 会通过 new Vue 把

  • vuex 利用了 vue 的 mixin 机制,混合 beforeCreate 钩子, 将 store 注入至 vue 组件实例上,并注册了 vuex store 的引用属性 $store!

  • vuex 的 state 是借助 vue 的响应式 data 实现的。

  • getter 是借助 vue 的计算属性 computed 特性实现的。

  • 其设计思想与 vue 中央事件总线如出一辙。

// 当调用这个方法的时候 其实就是调用插件的install 方法。
Vue.use(Vuex);
Vue.use = function (plugin) {
  plugin.install.apply(plugin, args);
};
// vuex的install方法
Vue.mixin({
  beforeCreate() {
    if (this.$options && this.$options.store) {
      //找到根组件 main 上面挂一个$store
      this.$store = this.$options.store;
      // console.log(this.$store);
    } else {
      //非根组件指向其父组件的$store
      this.$store = this.$parent && this.$parent.$store;
    }
  }
});
// store类里面有什么呢
// 重点方法 ,重置VM
resetStoreVM(this, state);
function resetStoreVM(store, state, hot) {
  // 省略无关代码
  Vue.config.silent = true;
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  });
}

vue-router

前端路由是什么

  • 前端路由就是,前端监听路由 url 的变化,根据不同的路径映射到不同的视图。本质上就是检测 URL 的变化,截获 URL 地址,通过解析、匹配路由规则实现 UI 更新。
  • 前端路由相较于后端路由的一个特点就是页面在不完全刷新的情况下进行视图的切换。

vue router 的原理

  • Vue 编写插件的时候通常要提供静态的 install 方法,我们通过 Vue.use(plugin) 时候,就是在执行 install 方法。

  • Vue-Router 的 install 方法会给每一个组件注入 beforeCreate 和 destoryed 钩子函数,在 beforeCreate 做一些私有属性定义和路由初始化工作,

  • 路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据

模式

  • hash 模式
  • history 模式
  • 'abstract'模式,不涉及和浏览器地址的相关记录,流程跟'HashHistory'是一样的,其原理是通过数组模拟浏览器历史记录栈的功能

hash 模式

通过 hashchange 事件监听 URL 的变化

history 模式

  • 需要后台配置把所有的 url 都代理到当前的 index.html 上
  • 采用了 html5 的新特性 pushState(), replaceState()可以对浏览器历史记录栈进行修改,这两个方法改变 URL 的 path 部分不会引起页面刷新
  • popState 事件的监听到状态变更
  • 通过浏览器前进后退改变 URL 时会触发 popstate 事件
  • 通过 pushState/replaceState 或<\a>标签改变 URL 不会触发 popstate 事件。好在我们可以拦截 pushState/replaceState 的调用和<\a>标签的点击事件来检测 URL 变化
  • 通过 js 调用 history 的 back,go,forward 方法课触发该事件
window.history.replaceState({ key: _key }, '', url);
window.history.back(); // 后退
window.history.forward(); // 前进
window.history.go(-3); // 后退三个页面
// window.history.pushState、 window.history.replaceState 和 window.history.go

VUE3

原理

  • vue3 通过 es6 proxy 监听对象和数组的变化

proxy 只能监听第一层

  • Proxy 只会代理第一层,可以判断当前 Reflect.get 的返回值是否为 Object, 如果是则再通过 reactive 方法做代理,这样就是先了深度观测。

监听数组的时候可能会多次触发 get/set

  • 判断 key 是否为当前被代理对象 target 的自身属性,同时判断旧值与新值是否相等,两个都满足才能执行 trigger

性能优化 2.0

代码层面

  • 循环的时候 key,
  • 根据情况使用 v-if 还是 v-show
  • 路由懒加载按需加载

webpack

  • echarts,vuex,vrouter,vue,element-ui,打包成 dll,