Vue面经

目录

(一)Vue2和Vue3区别

(二)MVVM数据流

(三)一些方法

3.1 Keep-alive

3.2 NextTick

3.3 插槽slot

3.4 lazyload

(四) 基础

4.1 Vue2

4.2 Vue3

(五)生命周期

5.1 Vue2

5.2 Vue3

(六) 通信方式

(七)路由

7.1 模式

7.2 加载跳转

7.3 导航守卫

7.4 跳转方式

7.5 刷新

7.6 源码

(八)状态管理

8.1 VueX

8.2 Pinia

(九)虚拟Dom

(十)双向绑定

10.1 Vue2

10.2 Vue3

(十一)编译渲染SSG、SEO

(十二)SSR、CSR.

(十三) 异步更新

(一) Vue2和Vue3区别

1.1 JS

​ 1 解耦视图和数据

​ 2 双向数据绑定

​ 3 可复用的组件

​ 4 前端路由技术(单页面)

​ 5 状态管理

​ 6 虚拟DOM

1.2vue2 和 vue3

1)组件化开发思想

组合式API,简单来说就是将同一逻辑关注点的代码配置在一起。Vue2的以下局限性:

  • 代码的可读性问题:如果组件越来越大,代码难于理解和维护。
  • 组件逻辑复用问题:相同的逻辑代码,很难在多个组件里面进行复用。

2) 支持多根结点,放入虚拟fragment中,对TypeScript的支持

3)响应式原理

​ Vue2 响应式原理基础是 Object.defineProperty;Vue3 响应式原理基础是 Proxy。

​ Object.defineProperty 基本用法:直接在一个对象上定义新的属性或修改现有的属性,并返回对象。他无法监听对象或数组新增、删除的元素。比如:给对象添加属性但是界面不刷新;监测 .length 修改。

​ 若想实现数据与视图同步更新,可采取下面三种解决方案:

  • 如果为对象添加少量的新属性,可以直接采用Vue.set()
  • 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象
  • 修改数组后触发界面更新,可采取this.$forceUpdate()进行强制刷新 (不建议)

​ Proxy Proxy 是 ES6 新特性,通过第2个参数 handler 拦截目标对象的行为。相较于 Object.defineProperty 提供语言全范围的响应能力,消除了局限性。

4)虚拟DOM和diff算法的优化

5)打包优化

6)setup生命周期函数里加on。beforeDestroy 生命周期选项被重命名为 onBeforeUnmount。

7) Composition API:setup()、ref&reactive

8)移除了一些组件,如filter和v-on等

9)轻量化,如创建使用createApp()而不是new Vue(),比new Vue()轻量

Vue3其他改进:

1 watch属性

​ 1) 用reactive监视对象时,不能监视到oldvalue

​ 2) 默认开启了deep,且关不掉

​ 3) 当传入reactive的某一属性时,需要传递函数才可以实现监视,因为watch只能监视数组、ref和reactive

2 hook 本质是一个函数,相当于mixin

3 provide和inject实现祖孙之间通信

4 watchEffect 也是一个帧听器,是一个副作用函数。 它会监听引用数据类型的所有属性,不需要具体到某个属性,一旦运行就会立即监听,组件卸载的时候会停止监听。

5 toRef shallowReactive toRaw和markRaw

6 v-for 除了可以遍历数组之外,还可以遍历对象、迭代器等

7 更好的错误处理

Vue3 编译优化

编译优化:编译器将模版编译为渲染函数的过程中,尽可能地提取关键信息,并以此指导生成最优代码的过程。

原因:传统diff算法的问题: 无法利用编译时提取到的任何关键信息,导致渲染器在运行时不会去做相关的优化。

目的:尽可能地区分动态内容和静态内容,并针对不同的内容采用不同的优化策略。vue3的编译器会将编译得到的关键信息“附着”在它生成的虚拟DOM上,传递给渲染器,执行“快捷路径”。

方法:

  1. Block 与 PatchFlag:传统Diff算法无法避免新旧虚拟DOM树间无用的比较操作,是因为运行时得不到足够的关键信息,从而无法区分动态内容和静态内容。 现在在虚拟节点多了一个额外的属性,即 patchFlag(补丁标志),存在该属性,就认为是动态节点。在虚拟节点的创建阶段,把它的动态子节点提取出来,并存储到该虚拟节点的 dynamicChildren 数组中。
Block定义: 带有 dynamicChildren 属性的虚拟节点称为“块” ,即(Block)
一个Block本质上也是一个虚拟DOM, 比普通的虚拟节点多处一个用来存储动态节点的 dynamicChildren属性。(能够收集所有的动态子代节点)
渲染器的更新操作会以Block为维度。当渲染器在更新一个Block时,会忽略虚拟节点的children数组,直接找到dynamicChildren数组,并只更新该数组中的动态节点。跳过了静态内容,只更新动态内容。同时,由于存在对应的补丁标志,也能够做到靶向更新。
Block节点有哪些: 模版根节点、 带有v-for、v-if/v-else-if/v-else等指令的节点

  1. 静态提升:减少更新时创建虚拟DOM带来的性能开销和内存占用。把纯静态的节点提升到渲染函数之外,响应式数据变化后,不会重新创建静态的虚拟节点,包含动态绑定的节点本身不会被提升,但是该节点上的静态属性是可以被提升的。
  2. 预字符串化:基于静态提升,进一步采用预字符串化优化。采用预字符串化将这些静态节点序列化为字符串, 并生成一个Static类型的VNode,超过 20 个静态会进行静态提升。大块的静态内容可以通过 innerHTML设置, 在性能上有一定优势。
  3. v-once 可以对虚拟DOM进行缓存

(二) MVVM数据流

​ MVVM 和 MVC都是一种设计思想,都是为了保证高内聚低耦合和可重用性的优点。MVVM 与 MVC 最大的区别就是:它实现了View和Model的自动同步,当Model属性改变时,不用手动操作Dom元素去改变View的显示。而改变属性后,该属性对应View的显示会自动改变,因此开发者只需要专注对数据的维护操作即可,而不需要一直操作 dom。

1 阐述一下你所理解的MVVM响应式原理

​ vue是采用数据劫持配合发布者-订阅者的模式的方式,通过Object.defineProperty()来劫持各个属性的getter和setter,在数据变动时,发布消息给依赖收集器(dep中的subs),去通知(notify)观察者,做出对应的回调函数,更新视图。MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer,Compile之间的通信桥路,达到数据变化Observer)=>视图更新;视图交互变化=>数据model变更的双向绑定效果。

2 双向数据流

​ 在双向数据流中,Model(可以理解为状态的集合) 中可以修改自己或其他Model的状态, 用户的操作(如在输入框中输入内容)也可以修改状态。(双向数据流也可以叫双向数据绑定)

双向数据流 - 优点

  1. 数据模型变化与更新,会自动同步到页面上,用户在页面的数据操作,也会自动同步到数据模型
  2. 无需进行和单向数据绑定的那些相关操作;
  3. 在表单交互较多的场景下,会简化大量业务无关的代码。

双向数据流 - 缺点

  1. 无法追踪局部状态的变化;
  2. “暗箱操作”,增加了出错时 debug 的难度;
  3. 由于组件数据变化来源入口变得可能不止一个,数据流转方向易紊乱。
  4. 改变一个状态有可能会触发一连串的状态的变化,最后很难预测最终的状态是什么样的。使得代码变得很难调试

3 单项数据流

对于 Vue 来说,组件之间的数据传递具有单向数据流这样的特性。

  1. 父组件总是通过 props 向子组件传递数据;
  2. 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定;
  3. 父级 prop 的更新会向下流动到子组件中,但是反过来则不行;
  4. 这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解;
  5. 每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值;
  6. 这意味着不应该在一个子组件内部改变 prop。如果这样做,Vue 会在浏览器的控制台中发出警告。

单向数据流 - 优点

  1. 所有状态的改变可记录、可跟踪,源头易追溯;
  2. 所有的数据,具有唯一出口和入口,使得数据操作更直观更容易理解,可维护性强;
  3. 当数据变化时,页面会自动变化
  4. 当你需要修改状态,完全重新开始走一个修改的流程。这限制了状态修改的方式,让状态变得可预测,容易调试。

单向数据流 - 缺点

  1. 页面渲染完成后,有新数据不能自动更新,需要手动整合新数据和模板重新渲染
  2. 代码量上升,数据流转过程变长,代码重复性变大
  3. 由于对应用状态独立管理的严格要求(单一的全局 store,如:Vuex),在处理局部状态较多的场景时(如用户输入交互较多的“富表单型”应用),会显得啰嗦及繁琐。

4 依赖收集

​ 我们知道,当一个可观测对象的属性被读写时,会触发它的getter/setter方法。如果我们可以在可观测对象的getter/setter里面,去执行监听器里面的onComputedUpdate()方法,是不是就能够实现让对象主动发出通知的功能呢?由于监听器内的onComputedUpdate()方法需要接收回调函数的值作为参数,而可观测对象内并没有这个回调函数,所以我们需要借助一个第三方来帮助我们把监听器和可观测对象连接起来。这个第三方就做一件事情——收集监听器内的回调函数的值以及onComputedUpdate()方法。

​ Vue为数据中的每一个key维护一个订阅者列表。对于生成的数据,通过Object.defineProperty对其中的每一个key进行处理,主要是为每一个key设置get, set方法,以此来为对应的key收集订阅者,并在值改变时通知对应的订阅者。

在对key进行取值时,如果Dep.target有值,除正常的取值操作外会进行一些额外的操作来添加订阅者。大多数时间里,Dep.target的值都为null,只有订阅者在进行订阅操作时,Dep.target才有值,为正在进行订阅的订阅者。此时进行取值操作,会将订阅者加入到对应的订阅者列表中。

订阅者在进行订阅操作时,主要包含以下3个步骤:

  • 将自己放在Dep.target
  • 对自己依赖的key进行取值
  • 将自己从Dep.target移除

Vue 的响应式系统中的一个重要部分是依赖收集。这个过程确保只有当某个状态改变时,依赖于这个状态的计算值或组件才会重新计算或渲染。

以下是一个简化的依赖收集系统的实现:

class Dep {
  //这个例子中,我们创建了一个Dep类,它代表了一个依赖项。每个依赖项都有一个subscribers集合,用于存储所有依赖于这个依赖项的更新函数。
  constructor() {
    this.subscribers = new Set();
  }

  depend() {
    if (activeUpdate) {
      this.subscribers.add(activeUpdate);
    }
  }

  notify() {
    this.subscribers.forEach(sub => sub());
  }
}
//autorun函数接收一个更新函数,并将其包装在一个新的函数中,这个新的函数会在每次运行更新函数时将自己设置为activeUpdate。这样,当我们访问响应式对象的某个属性时,我们就可以将 activeUpdate 添加到这个属性对应的依赖项的 subscribers集合中。
let activeUpdate;
function autorun(update) {
  function wrappedUpdate() {
    activeUpdate = wrappedUpdate;
    update();
    activeUpdate = null;
  }

  wrappedUpdate();
}
//最后,当我们修改响应式对象的某个属性的值时,我们会通知这个属性对应的依赖项,让其调用所有的更新函数,这就实现了状态改变时的响应更新。
function reactive(obj) {
  const deps = new Map();
  return new Proxy(obj, {
    get(obj, key) {
      let dep = deps.get(key);
      if (!dep) {
        dep = new Dep();
        deps.set(key, dep);
      }

      dep.depend();
      return obj[key];
    },
    set(obj, key, newVal) {
      obj[key] = newVal;
      const dep = deps.get(key);
      if (dep) {
        dep.notify();
      }
      return true;
    }
  });
}

const state = reactive({ count: 0 });

autorun(() => {
  console.log(state.count);
});

state.count++; // 控制台输出:1

https://www.jianshu.com/p/e6e1fa824849

(三) 一些方法

3.1 Keep-alive

​ keep-alive作为一种vue的内置组件,主要作用是缓存组件状态。当需要组件的切换时,不用重新渲染组件,避免多次渲染,就可以使用keep-alive包裹组件。

<!-- 基本 --> 
<keep-alive> 
    <component :is="view"></component> 
</keep-alive> 
<!-- 多个条件判断的子组件 -->
<keep-alive> 
    <comp-a v-if="a > 1"></comp-a> 
    <comp-b v-else></comp-b> 
</keep-alive>
<!-- 和 `<transition>` 一起使用 -->
<transition> 
    <keep-alive> 
        <component :is="view"></component> 
    </keep-alive> 
</transition>

keep-alive是一个组件,这个组件中有三个属性,分别是includeexcludemax,在created中创建缓存列表和缓存组件的key列表,销毁的时候会做一个循环销毁清空所有的缓存和key。当mounted时会监控includeinclude属性,进行组件的缓存处理。按需缓存,keep-alive组件如果设置了 include ,就只有和 include 匹配的组件会被缓存。

​ 该组件如果缓存过,就直接拿到组件实例,如果没有就存进当前的vnode中,和key做一个对应关系。

在 keep-alive 的源码定义中,它作为一个组件有自己的 render() 阶段会缓存 vnode 和组件名称 key 等操作。

首先会判断是否存在缓存,如果存在,则直接从缓存中获取组件的实例,并进行缓存优化处理(这里面有一个算法叫LRU,如果有key就不停的取,如果超限了就采用LRU进行删除最近最久未使用的,从前面删除,LRU就是将当前使用的往数组的后面移,在最前面的就是最久未使用的)。

​ 如果发生变化会动态的添加和删除缓存,渲染的时候会去拿默认插槽,只缓存第一个组件,根据组件的名字判断是否在缓存中,如果在就缓存,不在就return掉,不在就直接return掉。缓存的时候,如果组件没有key,就自己通过组件的标签,key和cid拼接一个key。

var KeepAlive = {
  ...
  props: {
    include: patternTypes,  // 名称匹配的组件会被缓存,对外暴露 include 属性 api
    exclude: patternTypes,  // 名称匹配的组件不会被缓存,对外暴露 exclude 属性 api
    max: [String, Number]  // 可以缓存的组件最大个数,对外暴露 max 属性 api
  },

注意:

iframe 标签承载了一个单独的嵌入的窗口,它有自己的 document 和 window (浏览器会检查 iframe 是否具有相同的源)

​ iframe中keep-alive机制失效原因:iframe页里的内容并不属于节点的信息,所以使用keep-alive依然会重新渲染iframe内的内容。而且iframe每一次渲染就相当于打开一个新的网页窗口,即使把节点保存下来,在渲染时iframe页还是刷新的。

解决策略:

  1. 切换不含iframe的界面时使用vue路由,在切换含iframe页的界面时利用v-show来控制显示隐藏,使iframe的节点不被删除,以此来防止界面节点被重新更新,从而达到保存iframe节点数据的效果
  2. 使用不带iframe的组件正常使用keep-alive,带iframe的组件开始时隐藏,切换到带iframe的组件时,隐藏其他不带iframe的组件,显示带iframe的组件,通过v-if将iframe的显示做成懒加载形式的,只有在用户进入相应的页面时才触发渲染,在渲染完毕后再通过v-show去控制界面在切换时的显示与隐藏。

https://juejin.cn/post/7133038641370595365

https://juejin.cn/post/7037321886019092510#heading-6

3.2 NextTick

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

​ nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。

​ 将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务;Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

DOM树更新是同步的

每一轮事件循环的最后会进行一次页面渲染,并且从上面我们知道渲染过程也是个宏任务,这里可能会有个误区,那就是DOM tree的修改是同步的,只有渲染过程是异步的,也就是说我们在修改完DOM后能够立即获取到更新的DOM。

为什么Vue却需要借用$nextTick来处理

因为Vue处于性能考虑,Vue会将用户同步修改的多次数据缓存起来,等同步代码执行完,说明这一次的数据修改就结束了,然后才会去更新对应DOM,一方面可以省去不必要的DOM操作,比如同时修改一个数据多次,只需要关心最后一次就好了,另一方面可以将DOM操作聚集,提升render性能。

为什么优先使用微任务?

因为微任务一定比宏任务优先执行,如果nextTick是微任务,它会在当前同步任务执行完立即执行所有的微任务,也就是修改DOM的操作也会在当前tick内执行,等本轮tick任务全部执行完成,才是开始执行UI rendering。如果nextTick是宏任务,它会被推进宏任务队列,并且在本轮tick执行完之后的某一轮执行,注意,它并不一定是下一轮,因为你不确定宏任务队列中它之前还有所少个宏任务在等待着。所以为了能够尽快更新DOM,Vue中优先采用的是微任务,并且在Vue3中,它没有了兼容判断,直接使用的是promise.then微任务,不再考虑宏任务了。

​ Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载已完成。

​ 当项目中你想在改变DOM元素的数据后基于新的dom做点什么,对新DOM一系列的js操作都需要放进Vue.nextTick()的回调函数中;通俗的理解是:更改数据后当你想立即使用js操作新的视图的时候需要使用它。

​ Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOm操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。​ 当设置 vm.someData = 'new value',DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

参考:

  • vue.nextTick()方法的使用详解(简单明了)https://blog.csdn.net/zhouzuoluo/article/details/84752280
  • https://juejin.cn/post/7089980191329484830#heading-10

3.3 插槽slot

1.插槽是使用在子组件中的。

2.插槽是为了将父组件中的子组件模板数据正常显示

//home.vue
<test>
     Hello Word
</test>
//test.vue
<a href="#">
	 <slot></slot>
</a>
//当组件渲染的时候,<slot></slot>会被替换为Hello Word

3 插槽内可以包含普通文本,也可以包含任何模板代码,包括HTML

//home.vue
<test>
	//插槽可以获取到home组件里的内容
	Hello {{enhavo}}
</test>

data(){
	return{
		enhavo:'word'
	}
}
//home.vue
//这里是获取不到name的,因为这个值是传给<test>的
<test name='you'>
    Hello {{name}}
</test>

4 插槽跟模板其他地方一样都可以访问相同的实例属性(也就是相同的"作用域"),而不能访问<test>的作用域。原因是父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

具名插槽

​ 有时候我们一个组件里需要多个插槽,对于这样的情况,<slot>元素有一个特殊的特性:name ,这个特性可以用来定义额外的插槽

​ 如果一个<slot>不带name属性的话,那么它的name默认为default。在向具名插槽提供内容的时候,我们可以在<template>元素上使用v-slot指令,并以参数的形式提供其名称

具名插槽的缩写(2.6.0新增)

​ 跟 v-onv-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header

作用域插槽

我们可以在父组件中使用slot-scope 特性从子组件获取数据, 前提是需要在子组件中使用:data=data 先传递data 的数据。

动态插槽

动态指令参数也可以用在v-slot上,来定义动态的插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

3.4 lazyload

核心逻辑是: 图片在视图范围内,就显示,否则只显示加载图标。而图片在不在视图范围内,是动态变化的,比如滚动的时候,图片就可能从视图外到视图内。

import VueLazyload from 'vue-lazyload'

Vue.use(VueLazyload,{
  preLoad: 1.3,
  loading: 'dist/loading.gif',
})

// 使用的时候,直接在想懒加载的img上,加个指令就好了
// <img v-lazy="img.src">

原理:

​ 通过getBoundingClientRect可以知道,元素相对于视图窗口的左上角的距离。

Element.getBoundingClientRect() //方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。

元素在不在视图内,其实本质上就是判断:top > windowHeighttop越大,元素离地址栏就会越来越远,当距离大于windowHeight,就不在视图范围内。

const windowHeight = window.innerHeight
// 元素离地址栏的近似距离
const {top} = ele.getBoundingClientRect()
const isInView = top<windowHeight

  1. vue-lazyload是通过指令的方式实现的,定义的指令是v-lazy指令
  2. 指令被bind时会创建一个listener,并将其添加到listener queue里面, 并且搜索target dom节点,为其注册dom事件(如scroll事件)
  3. 上面的dom事件回调中,会遍历 listener queue里的listener,判断此listener绑定的dom是否处于页面中perload的位置,如果处于则加载异步加载当前图片的资源
  4. 同时listener会在当前图片加载的过程的loading,loaded,error三种状态触发当前dom渲染的函数,分别渲染三种状态下dom的内容

(四) 基础

5.1 Vue2

1 指令

​ v-text:设置标签文本值(textContent)

​ v-html:设置标签的innerHtml

​ v-on(@):为元素绑定事件

​ v-show:针对表达式的真假,控制元素的显示与隐藏(有就执行transition没有就display:none)

​ v-if:针对表达式的真假,控制元素的显示与隐藏(操作dom)

​ v-bind(:):设置元素属性(src,title,class)

​ v-for:v-for 具有比 v-if 更高的优先级,这意味着 v-if 将分别重复运行于每个 v-for 循环中

​ v-model:获取和设置表单元素(input|textarea|button|datalist)的值

<input v-bind:value="something" v-on:input="something=$event.target.value">

想要组件 v-model生效 它必须: 1. 接收一个value属性。 2. 在value值改变时 触发input事件。

自定义组件使用:

  1. 在你的组件的 props 中定义一个属性,通常叫做 value。
  2. 当需要更新 v-model 绑定的值时,你的组件需要触发一个 update:modelValue 事件,并将新的值作为参数。在 Vue 3 之前,事件名是 input。
  3. 在使用你的组件时,使用 v-model 指令绑定一个值。
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
<my-input v-model="message"></my-input>

2 el:挂载点

​ el的作用是设置vue实例挂载管理的元素;vue会管理el命中的元素以及其后代元素;可以使用其他选择但是建议使用id选择器;可以使用其他双标签,但不能使用html和body。

3 计算属性

​ 计算属性实际上是一个懒执行的副作用函数。computed = 闭包 + effect() + lazy。 当我们不希望effect立即执行时,通过增加lazy属性延迟执行。

  1. 一开始每个 computed 新建自己的 watcher时,会设置 watcher.dirty = true,以便于 computed 被使用时,会计算得到值
  2. 当依赖的数据变化了,通知 computed 时,会赋值 watcher.dirty = true,此时重新读取 computed 时,会执行 get 函数重新计算。
  3. computed 计算完成之后,会设置 watcher.dirty = false,以便于其他地方再次读取时,使用缓存,免于计算。

​ 通过为effect添加scheduler调度器函数,在getter中所依赖的响应式数据发生变化时将scheduler中的dirty重置为true。computed 会让 【data依赖】 收集到 【依赖computed的watcher】,从而 data 变化时,会同时通知 computed 和 依赖computed的地方。

const computedList = computed(()=>{ return xxx })
// 计算属性不应有副作用,如异步/修改dom
// 应该只读,不应赋值

4 watch

本质就是观测一个响应式数据,并传递一个回调函数,当修改数据时执行。

vue3中可以通过设置flush定义回调函数执行时机,当flush为post时将其添加到微任务队列中。

watch(监听谁,(newValue, oldValue) => {xxx})
watch([监听谁s],([newValue], [oldValue]) => {xxx})

deep | immediately

watchEffect 是 vue3 的一个新特性,与 Vue2 中的 watch 不同,watchEffect 不需要指定要监听的数据,而是会自动追踪函数中使用的响应式数据,并在这些数据发生变化时重新执行回调函数。这种自动追踪的特性可以简化代码,并提高应用的性能。

触发时机:watchEffect:立即执行一次回调函数,并在回调函数中自动收集依赖。每当依赖发生变化时,回调函数都会被重新执行。

​ watch:需要显式指定要监视的响应式数据或计算属性,并在其发生变化时执行回调函数。

依赖追踪:watchEffect:会自动追踪在回调函数中使用的响应式数据或计算属性,并建立依赖关系。当依赖变化时,回调函数将重新执行。

​ watch:需要手动指定要监视的响应式数据或计算属性,只有在指定的数据变化时才会执行回调函数。

回调函数参数:watchEffect:回调函数中没有参数,但可以使用响应式数据和计算属性。

​ watch:回调函数接收两个参数:新值和旧值。可以通过这两个参数来执行特定的操作,例如比较新旧值之间的差异。

**5 this【Vue2 this 能够直接获取到 data 和 methods】 **

以method举例,只要关注initMethod即可,其实整个initMethods方法核心就是将this绑定到了实例身上,因为methods里面都是函数,所以只需要遍历将所有的函数在调用的时候将this指向实例就可以实现通过this直接调用的效果。

通过this直接访问到methods里面的函数的原因是:因为methods里的方法通过 bind 指定了thisnew Vue的实例(vm)。【bind函数中主要是做了兼容性的处理,如果不支持原生的bind函数,则根据参数个数的不同分别使用call/apply来进行this的绑定,而call/apply最大的区别就是传入参数的不同,一个分别传入参数,另一个接受一个数组。】

通过 this 直接访问到 data 里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx,是访问Object.defineProperty代理后的 this._data.xxx

5.2 Vue3

0 proxy 和 define.property

  • Proxy可以直接监听对象而非属性;
  • Proxy可以直接监听数组的变化;
  • Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的;
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;

Object.defineProperty (obj, prop, descriptor) 的问题主要有三个:

  • 无法监听数组的变化 Vue 把会修改原来数组的方法定义为变异方法。 变异方法例如 push、pop、shift、unshift、splice、sort、reverse等,是无法触发 set 的。 非变异方法,例如 filter,concat,slice 等,它们都不会修改原始数组,而会返回一个新的数组。 Vue 的做法是把这些变异方法重写来实现监听数组变化。
  • 必须遍历对象的每个属性 使用 Object.defineProperty 多数情况下要配合 Object.keys 和遍历,于是就多了一层嵌套。 并且由于遍历的原因,假如对象上的某个属性并不需要“劫持”,但此时依然会对其添加“劫持”。
  • 必须深层遍历嵌套的对象 当一个对象为深层嵌套的时候,必须进行逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。

1 组件

组件的本质就是一组dom的封装,用一个函数定义组件,返回值对象(render)就是要渲染的内容。通过判断vnode.type是否为函数判断是否为组件(patch会判断的类型有string、text、Fragment和object)。然后以递归的方式去挂载组件。

​ 组件初始化:

  • ​ const state = reactive(data())将数据变为响应式。
  • ​ const subTree = render.call(state, state) 将this只想设置为响应式的state,同时将state作为第一个参数传递。
  • ​ patch(null, subTree, contianer, anchor) 更新虚拟dom。

​ 使用effect()对reactive数据进行实时更新,但是由于effect是同步的,每次数据变化时都会导致渲染器更新,因此需要实现一个调度器,当副作用函数需要被执行时放入一个微任务队列中并对任务进行去重。

const queue = new Set()
let isFlushing = flase //是否正在刷新任务队列
const p = Promise.resolve()
function queueJob(job){
	quque.add(job)
	if(!isFlushing){
		ifFlushing = true
		p.then(()=>{
			try{
				queue.forEach(job=>job())
			}finally{
				isFlushing = false
				queue.clear = 0
			}
		})
	}
}
effect(()={subTree;path()},{scheduler:ququeJob})

2 setup

scrtpt setup 是 vue3 的语法糖,简化了组合式 API 的写法,并且运行性能更好。使用 script setup 语法糖的特点:

  • 属性和方法无需返回,可以直接使用。
  • 引入组件的时候,会自动注册,无需通过 components 手动注册。
  • 使用 defineProps 接收父组件传递的值。
  • useAttrs 获取属性,useSlots 获取插槽,defineEmits 获取自定义事件。
  • 默认不会对外暴露任何属性,如果有需要可使用 defineExpose 。

setup函数(取代了onCreated)只会在挂载时被执行一次,它可以返回一个函数作为组件的渲染函数;也可以返回一个对象,将数据暴露给模板使用(可以通过this访问)。它接受两个参数,一个是props数据对象,一个是setupContext与组件接口相关的数据。

​ setupContext包含{emit、slots、attrs、expose}

emit用来发射组件的自定义事件。

slot插槽,与React中的render props相似。将vnode.children作为slots对象添加到setupContext中,在render中通过this.$slot访问。然后对renderContext代理对象的get拦截函数做处理,当读取到slot时返回slot对象。

<script setup> 省略原来setup(){return xxx}

​ Vue2中,可以通过this来获取当前组件实例;Vue3中setup执行时机是在beforeCreate钩子前自动执行,不指向this,指向undefined,通过getCurrentInstance()获得;

3 Ref和Reactive

ref本质也是reactiveref(obj)等价于reactive({value: obj})

1.reactive

​ reactive的参数必须是一个对象,不能是单一的值,包括json数据和数组都可以,否则不具有响应式。如果给reactive传递了其他对象(如时间对象),默认情况下修改对象界面不会自动更新,如果想更新,可以通过给对象重新赋值来解决

​ 在使用vue3中,使用reactive创建的对象或者数组进行赋值时,可以正常赋值,但是不会触发响应式变化。每次直接把一个对象或者数组赋值给reactive创建的对象或数组时,导致reactive创建的响应式对象被新赋值的直接代理,再vue3中操作的都是proxy代理对象,所以失去了响应式。在vue3中不管是对象还是数组都不能直接将整个数据进行赋值,这样会造成reactive定义的响应式失效。通俗说: 就像对象的地址被替换,就不是原来的那个对象了。

​ 解决方法:不是直接赋值,包裹一层;使用ref赋值,包裹了一层对象,ref重新赋值时,通过.value赋值

2.ref当我们只想让某个变量实现响应式的时候,采用reactive就会比较麻烦,因此vue3提供了ref方法进行简单值的监听,但并不是说ref只能传入简单值,他的底层是reactive,所以reactive有的,他都有。

  • 在中使用的值,不用通过获取(解包);在中使用

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

前端校招面经分享 文章被收录于专栏

前端校招面经分享,包含CSS、JS、Vue、React、计算机网络、难点项目、手撕题等。这份面经总结了几乎大厂所有的面试题与牛客近几年公开的面经,可以说面试不会超出范围。 因为我只负责总结加一些个人见解,所以一开始免费开源。但互联网戾气真的重,很多人拿着面经还一副理所应当的样子质问我要语雀,还说网上同类的有很多??唉,分享不易,那我只好收费了233。当然也欢迎直接来找我要语雀,语雀会多一些内容。

全部评论
老哥 太顶了
点赞 回复 分享
发布于 03-11 17:04 陕西
太全了太全了
点赞 回复 分享
发布于 03-11 17:11 陕西
不错不错
点赞 回复 分享
发布于 03-11 17:14 陕西
感谢大佬🙏🏻
点赞 回复 分享
发布于 03-11 18:51 陕西
大佬大佬,求带
点赞 回复 分享
发布于 03-11 19:31 云南
强啊哥
点赞 回复 分享
发布于 03-12 14:58 河北

相关推荐

10-30 10:16
南京大学 Java
永远的鹅孝子:给南大✌️跪了
点赞 评论 收藏
分享
评论
13
43
分享
牛客网
牛客企业服务