写点什么

进阶 vue 面试题总结

作者:bb_xiaxia1998
  • 2022-10-19
    浙江
  • 本文字数:15482 字

    阅读完需:约 1 分钟

过滤器的作用,如何实现一个过滤器

根据过滤器的名称,过滤器是用来过滤数据的,在 Vue 中使用filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed ,方法 methods 都是通过修改数据来处理数据格式的输出显示)。


使用场景:


  • 需要格式化数据的情况,比如需要处理时间、价格等数据格式的输出 / 显示。

  • 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用fliters过滤器来处理数据。


过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表达式 {{ }}v-bind 表达式 中,然后放在操作符“ | ”后面进行指示。


例如,在显示金额,给商品价格添加单位:


<li>商品价格:{{item.price | filterPrice}}</li>
filters: { filterPrice (price) { return price ? ('¥' + price) : '--' } }
复制代码

路由的 hash 和 history 模式的区别

Vue-Router 有两种模式:hash 模式history 模式。默认的路由模式是 hash 模式。

1. hash 模式

简介: hash 模式是开发中默认的模式,它的 URL 带着一个 #


特点:hash 值会出现在 URL 里面,但是不会出现在 HTTP 请求中,对后端完全没有影响。所以改变 hash 值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的 IE 浏览器也支持这种模式。hash 路由被称为是前端路由,已经成为 SPA(单页面应用)的标配。


原理: hash 模式的主要原理就是 onhashchange()事件


window.onhashchange = function(event){    console.log(event.oldURL, event.newURL);    let hash = location.hash.slice(1);}
复制代码


使用 onhashchange()事件的好处就是,在页面的 hash 值发生变化时,无需向后端发起请求,window 就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash 值变化对应的 URL 都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的 hash 值和对应的 URL 关联起来了。

2. history 模式

简介: history 模式的 URL 中没有 #,它使用的是传统的路由分发模式,即用户在输入一个 URL 时,服务器会接收这个请求,并解析这个 URL,然后做出相应的逻辑处理。 特点: 相比 hash 模式更加好看。但是,history 模式需要后台配置支持。如果后台没有正确配置,访问时会返回 404。 API: history api 可以分为两大部分,切换历史状态和修改历史状态:


  • 修改历史状态:包括了 HTML5 History Interface 中新增的 pushState()replaceState() 方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了 url,但浏览器不会立即向后端发送请求。如果要做到改变 url 但又不刷新页面的效果,就需要前端用上这两个 API。

  • 切换历史状态: 包括forward()back()go()三个方法,对应浏览器的前进,后退,跳转操作。


虽然 history 模式丢弃了丑陋的 #。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出 404 来。


如果想要切换到 history 模式,就要进行以下配置(后端也要进行配置):


const router = new VueRouter({  mode: 'history',  routes: [...]})
复制代码

3. 两种模式对比

调用 history.pushState() 相比于直接修改 hash,存在以下优势:


  • pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;

  • pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;

  • pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;

  • pushState() 可额外设置 title 属性供后续使用。

  • hash 模式下,仅 hash 符号之前的 url 会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回 404 错误;history 模式下,前端的 url 必须和实际向后端发起请求的 url 一致,如果没有对用的路由处理,将返回 404 错误。


hash 模式和 history 模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。

一般在哪个生命周期请求异步数据

我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。​


推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:


  • 能更快获取到服务端数据,减少页面加载时间,用户体验更好;

  • SSR 不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。

vue 中使用了哪些设计模式

1.工厂模式 - 传入参数即可创建实例


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


2.单例模式 - 整个程序有且仅有一个实例


vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉


3.发布-订阅模式 (vue 事件机制)


4.观察者模式 (响应式数据原理)


5.装饰模式: (@装饰器的用法)


6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略


...其他模式欢迎补充

常见的事件修饰符及其作用

  • .stop:等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;

  • .prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);

  • .capture :与事件冒泡的方向相反,事件捕获由外到内;

  • .self :只会触发自己范围内的事件,不包含子元素;

  • .once :只会触发一次。

Computed 和 Methods 的区别

可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的


不同点:


  • computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;

  • method 调用总会执行该函数。

Vue 模板编译原理

Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步


第一步是将 模板字符串 转换成 element ASTs(解析器)第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
复制代码

描述下 Vue 自定义指令

在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。一般需要对 DOM 元素进行底层操作时使用,尽量只用来操作 DOM 展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定 v-model 的值也不会同步更新;如必须修改可以在自定义指令中使用 keydown 事件,在 vue 组件中使用 change 事件,回调中修改 vue 数据;


(1)自定义指令基本内容


  • 全局定义:Vue.directive("focus",{})

  • 局部定义:directives:{focus:{}}

  • 钩子函数:指令定义对象提供钩子函数

  • o bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • o inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。

  • o update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。

  • o ComponentUpdate:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • o unbind:只调用一次,指令与元素解绑时调用。

  • 钩子函数参数 o el:绑定元素

  • o bing: 指令核心对象,描述指令全部信息属性

  • o name

  • o value

  • o oldValue

  • o expression

  • o arg

  • o modifers

  • o vnode 虚拟节点

  • o oldVnode:上一个虚拟节点(更新钩子函数中才有用)


(2)使用场景


  • 普通 DOM 元素进行底层操作的时候,可以使用自定义指令

  • 自定义指令是用来操作 DOM 的。尽管 Vue 推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的 DOM 操作,并且是可复用的。


(3)使用案例


初级应用:


  • 鼠标聚焦

  • 下拉菜单

  • 相对时间转换

  • 滚动动画


高级应用:


  • 自定义指令实现图片懒加载

  • 自定义指令集成第三方插件

如何从真实 DOM 到虚拟 DOM

涉及到 Vue 中的模板编译原理,主要过程:


  1. 将模板转换成 ast 树, ast 用对象来描述真实的 JS 语法(将真实 DOM 转换成虚拟 DOM)

  2. 优化树

  3. ast 树生成代码


参考:前端vue面试题详细解答

Computed 和 Watch 的区别

对于 Computed:


  • 它支持缓存,只有依赖的数据发生了变化,才会重新计算

  • 不支持异步,当 Computed 中有异步操作时,无法监听数据的变化

  • computed 的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 声明过,或者父组件传递过来的 props 中的数据进行计算的。

  • 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed

  • 如果 computed 属性的属性值是函数,那么默认使用 get 方法,函数的返回值就是属性的属性值;在 computed 中,属性有一个 get 方法和一个 set 方法,当数据发生变化时,会调用 set 方法。


对于 Watch:


  • 它不支持缓存,数据变化时,它就会触发相应的操作

  • 支持异步监听

  • 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值

  • 当一个属性发生变化时,就需要执行相应的操作

  • 监听数据必须是 data 中声明的或者父组件传递过来的 props 中的数据,当发生变化时,会触发其他操作,函数有两个的参数:

  • immediate:组件加载立即触发回调函数

  • deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep 无法监听到数组和对象内部的变化。


当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用 watch。


总结:


  • computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。

  • watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。


运用场景:


  • 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。

  • 当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

子组件可以直接改变父组件的数据吗?

子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。


Vue 提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。


只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

对 keep-alive 的理解,它是如何实现的,具体缓存的是什么?

如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。


(1)keep-alive


keep-alive 有以下三个属性:


  • include 字符串或正则表达式,只有名称匹配的组件会被匹配;

  • exclude 字符串或正则表达式,任何名称匹配的组件都不会被缓存;

  • max 数字,最多可以缓存多少组件实例。


注意:keep-alive 包裹动态组件时,会缓存不活动的组件实例。


主要流程


  1. 判断组件 name ,不在 include 或者在 exclude 中,直接返回 vnode,说明该组件不被缓存。

  2. 获取组件实例 key ,如果有获取实例的 key,否则重新生成。

  3. key 生成规则,cid +"∶∶"+ tag ,仅靠 cid 是不够的,因为相同的构造函数可以注册为不同的本地组件。

  4. 如果缓存对象内存在,则直接从缓存对象中获取组件实例给 vnode ,不存在则添加到缓存对象中。 5.最大缓存数量,当缓存组件数量超过 max 值时,清除 keys 数组内第一个组件。


(2)keep-alive 的实现


const patternTypes: Array<Function> = [String, RegExp, Array] // 接收:字符串,正则,数组
export default { name: 'keep-alive', abstract: true, // 抽象组件,是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
props: { include: patternTypes, // 匹配的组件,缓存 exclude: patternTypes, // 不去匹配的组件,不缓存 max: [String, Number], // 缓存组件的最大实例数量, 由于缓存的是组件实例(vnode),数量过多的时候,会占用过多的内存,可以用max指定上限 },
created() { // 用于初始化缓存虚拟DOM数组和vnode的key this.cache = Object.create(null) this.keys = [] },
destroyed() { // 销毁缓存cache的组件实例 for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } },
mounted() { // prune 削减精简[v.] // 去监控include和exclude的改变,根据最新的include和exclude的内容,来实时削减缓存的组件的内容 this.$watch('include', (val) => { pruneCache(this, (name) => matches(val, name)) }) this.$watch('exclude', (val) => { pruneCache(this, (name) => !matches(val, name)) }) },}
复制代码


render 函数:


  1. 会在 keep-alive 组件内部去写自己的内容,所以可以去获取默认 slot 的内容,然后根据这个去获取组件

  2. keep-alive 只对第一个组件有效,所以获取第一个子组件。

  3. 和 keep-alive 搭配使用的一般有:动态组件 和 router-view


render () {  //  function getFirstComponentChild (children: ?Array<VNode>): ?VNode {    if (Array.isArray(children)) {  for (let i = 0; i < children.length; i++) {    const c = children[i]    if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {      return c    }  }  }  }  const slot = this.$slots.default // 获取默认插槽  const vnode: VNode = getFirstComponentChild(slot)// 获取第一个子组件  const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 组件参数  if (componentOptions) { // 是否有组件参数    // check pattern    const name: ?string = getComponentName(componentOptions) // 获取组件名    const { include, exclude } = this    if (      // not included      (include && (!name || !matches(include, name))) ||      // excluded      (exclude && name && matches(exclude, name))    ) {      // 如果不匹配当前组件的名字和include以及exclude      // 那么直接返回组件的实例      return vnode    }
const { cache, keys } = this
// 获取这个组件的key const key: ?string = vnode.key == null // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key
if (cache[key]) { // LRU缓存策略执行 vnode.componentInstance = cache[key].componentInstance // 组件初次渲染的时候componentInstance为undefined
// make current key freshest remove(keys, key) keys.push(key) // 根据LRU缓存策略执行,将key从原来的位置移除,然后将这个key值放到最后面 } else { // 在缓存列表里面没有的话,则加入,同时判断当前加入之后,是否超过了max所设定的范围,如果是,则去除 // 使用时间间隔最长的一个 cache[key] = vnode keys.push(key) // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } // 将组件的keepAlive属性设置为true vnode.data.keepAlive = true // 作用:判断是否要执行组件的created、mounted生命周期函数 } return vnode || (slot && slot[0])}
复制代码


keep-alive 具体是通过 cache 数组缓存所有组件的 vnode 实例。当 cache 内原有组件被使用时会将该组件 key 从 keys 数组中删除,然后 push 到 keys 数组最后,以便清除最不常用组件。


实现步骤:


  1. 获取 keep-alive 下第一个子组件的实例对象,通过他去获取这个组件的组件名

  2. 通过当前组件名去匹配原来 include 和 exclude,判断当前组件是否需要缓存,不需要缓存,直接返回当前组件的实例 vNode

  3. 需要缓存,判断他当前是否在缓存数组里面:


  • 存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU)

  • 不存在,将组件 key 放入数组,然后判断当前 key 数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key


  1. 最后将这个组件的 keepAlive 设置为 true


(3)keep-alive 本身的创建过程和 patch 过程


缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefined) 和 keepAlive 属性判断不会执行组件的 created、mounted 等钩子函数,而是对缓存的组件执行 patch 过程∶ 直接把缓存的 DOM 对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。


首次渲染


  • 组件的首次渲染∶判断组件的 abstract 属性,才往父组件里面挂载 DOM


// core/instance/lifecyclefunction initLifecycle (vm: Component) {  const options = vm.$options
// locate first non-abstract parent let parent = options.parent if (parent && !options.abstract) { // 判断组件的abstract属性,才往父组件里面挂载DOM while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) }
vm.$parent = parent vm.$root = parent ? parent.$root : vm
vm.$children = [] vm.$refs = {}
vm._watcher = null vm._inactive = null vm._directInactive = false vm._isMounted = false vm._isDestroyed = false vm._isBeingDestroyed = false}
复制代码


  • 判断当前 keepAlive 和 componentInstance 是否存在来判断是否要执行组件 prepatch 还是执行创建 componentlnstance


// core/vdom/create-componentinit (vnode: VNodeWithData, hydrating: boolean): ?boolean {    if (      vnode.componentInstance &&      !vnode.componentInstance._isDestroyed &&      vnode.data.keepAlive    ) { // componentInstance在初次是undefined!!!      // kept-alive components, treat as a patch      const mountedNode: any = vnode // work around flow      componentVNodeHooks.prepatch(mountedNode, mountedNode) // prepatch函数执行的是组件更新的过程    } else {      const child = vnode.componentInstance = createComponentInstanceForVnode(        vnode,        activeInstance      )      child.$mount(hydrating ? vnode.elm : undefined, hydrating)    }  },
复制代码


prepatch 操作就不会在执行组件的 mounted 和 created 生命周期函数,而是直接将 DOM 插入


(4)LRU (least recently used)缓存策略


LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 **"如果数据最近被访问过,那么将来被访问的几率也更高"**。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下∶


  • 新数据插入到链表头部

  • 每当缓存命中(即缓存数据被访问),则将数据移到链表头部

  • 链表满的时候,将链表尾部的数据丢弃。

Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?

不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。


如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。

Vue 中 key 的作用

vue 中 key 值的作用可以分为两种情况来考虑:


  • 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。

  • 第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。


key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速


  • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

  • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快

Vuex 有哪几种属性?

有五种,分别是 State、 Getter、Mutation 、Action、 Module


  • state => 基本数据(数据源存放地)

  • getters => 从基本数据派生出来的数据

  • mutations => 提交更改数据的方法,同步

  • actions => 像一个装饰器,包裹 mutations,使之可以异步。

  • modules => 模块化 Vuex

用过 pinia 吗?有什么优点?

1. pinia 是什么?


  • Vue3中,可以使用传统的Vuex来实现状态管理,也可以使用最新的pinia来实现状态管理,我们来看看官网如何解释pinia的:PiniaVue 的存储库,它允许您跨组件/页面共享状态。

  • 实际上,pinia就是Vuex的升级版,官网也说过,为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家可以直接将pinia比作为Vue3Vuex


2. 为什么要使用 pinia?


  • Vue2Vue3都支持,这让我们同时使用Vue2Vue3的小伙伴都能很快上手。

  • pinia中只有stategetteraction,抛弃了Vuex中的MutationVuexmutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑减少了我们工作量。

  • piniaaction支持同步和异步,Vuex不支持

  • 良好的Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了

  • 无需再创建各个模块嵌套了,Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。

  • 体积非常小,只有1KB左右。

  • pinia支持插件来扩展自身功能。

  • 支持服务端渲染


3. pinna 使用


pinna文档(opens new window)


  1. 准备工作


我们这里搭建一个最新的Vue3 + TS + Vite项目


npm create vite@latest my-vite-app --template vue-ts
复制代码


  1. pinia基础使用


yarn add pinia
复制代码


// main.tsimport { createApp } from "vue";import App from "./App.vue";import { createPinia } from "pinia";const pinia = createPinia();
const app = createApp(App);app.use(pinia);app.mount("#app");
复制代码


2.1 创建store


//sbinsrc/store/user.tsimport { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 idexport const useUsersStore = defineStore('users', { // 其它配置项})
复制代码


创建store很简单,调用 pinia中的defineStore函数即可,该函数接收两个参数:


  • name:一个字符串,必传项,该store的唯一id

  • options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。


我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的


2.2 使用store


<!-- src/App.vue --><script setup lang="ts">import { useUsersStore } from "../src/store/user";const store = useUsersStore();console.log(store);</script>
复制代码


2.3 添加state


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },});
复制代码


2.4 读取state数据


<template>  <img alt="Vue logo" src="./assets/logo.png" />  <p>姓名:{{ name }}</p>  <p>年龄:{{ age }}</p>  <p>性别:{{ sex }}</p></template><script setup lang="ts">import { ref } from "vue";import { useUsersStore } from "../src/store/user";const store = useUsersStore();const name = ref<string>(store.name);const age = ref<number>(store.age);const sex = ref<string>(store.sex);</script>
复制代码


上段代码中我们直接通过store.age等方式获取到了store存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点


import { useUsersStore, storeToRefs } from "../src/store/user";const store = useUsersStore();const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的
复制代码


2.5 修改state数据


<template>  <img alt="Vue logo" src="./assets/logo.png" />  <p>姓名:{{ name }}</p>  <p>年龄:{{ age }}</p>  <p>性别:{{ sex }}</p>  <button @click="changeName">更改姓名</button></template><script setup lang="ts">import child from './child.vue';import { useUsersStore, storeToRefs } from "../src/store/user";const store = useUsersStore();const { name, age, sex } = storeToRefs(store);const changeName = () => {  store.name = "张三";  console.log(store);};</script>
复制代码


2.6 重置state


  • 有时候我们修改了state数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。

  • 此时,我们直接调用store$reset()方法即可,继续使用我们的例子,添加一个重置按钮


<button @click="reset">重置store</button>// 重置storeconst reset = () => {  store.$reset();};
复制代码


当我们点击重置按钮时,store中的数据会变为初始状态,页面也会更新


2.7 批量更改state数据


如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store$patch方法,修改app.vue代码,添加一个批量更改数据的方法


<button @click="patchStore">批量修改数据</button>// 批量修改数据const patchStore = () => {  store.$patch({    name: "张三",    age: 100,    sex: "女",  });};
复制代码


  • 有经验的小伙伴可能发现了,我们采用这种批量更改的方式似乎代价有一点大,假如我们state中有些字段无需更改,但是按照上段代码的写法,我们必须要将 state 中的所有字段例举出了。

  • 为了解决该问题,pinia提供的$patch方法还可以接收一个回调函数,它的用法有点像我们的数组循环回调函数了。


store.$patch((state) => {  state.items.push({ name: 'shoes', quantity: 1 })  state.hasChanged = true})
复制代码


2.8 直接替换整个state


pinia提供了方法让我们直接替换整个state对象,使用store$state方法


store.$state = { counter: 666, name: '张三' }
复制代码


上段代码会将我们提前声明的state替换为新的对象,可能这种场景用得比较少


  1. getters属性


  • gettersdefineStore参数配置项里面的另一个属性

  • 可以把getter想象成Vue中的计算属性,它的作用就是返回一个新的结果,既然它和Vue中的计算属性类似,那么它肯定也是会被缓存的,就和computed一样


3.1 添加getter


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 10,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return state.age + 100;    },  },})
复制代码


上段代码中我们在配置项参数中添加了getter属性,该属性对象中定义了一个getAddAge方法,该方法会默认接收一个state参数,也就是state对象,然后该方法返回的是一个新的数据


3.2 使用getter


<template>  <p>新年龄:{{ store.getAddAge }}</p>  <button @click="patchStore">批量修改数据</button></template><script setup lang="ts">import { useUsersStore } from "../src/store/user";const store = useUsersStore();// 批量修改数据const patchStore = () => {  store.$patch({    name: "张三",    age: 100,    sex: "女",  });};</script>
复制代码


上段代码中我们直接在标签上使用了store.gettAddAge方法,这样可以保证响应式,其实我们state中的name等属性也可以以此种方式直接在标签上使用,也可以保持响应式


3.3 getter中调用其它getter


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return state.age + 100;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },});
复制代码


3.3 getter传参


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return (num: number) => state.age + num;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },});
复制代码


<p>新年龄:{{ store.getAddAge(1100) }}</p>
复制代码


  1. actions属性


  • 前面我们提到的stategetters 属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data数据和computed计算属性一样。

  • 那么,如果我们有业务代码的话,最好就是卸载actions属性里面,该属性就和我们组件代码中的methods相似,用来放置一些处理业务逻辑的方法。

  • actions属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法


4.1 添加actions


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return (num: number) => state.age + num;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },  actions: {    // 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store    saveName(name: string) {      this.name = name;    },  },});
复制代码


4.2 使用actions


使用actions中的方法也非常简单,比如我们在App.vue中想要调用该方法


const saveName = () => {  store.saveName("poetries");};
复制代码


总结


pinia的知识点很少,如果你有 Vuex 基础,那么学起来更是易如反掌


pinia 无非就是以下 3 个大点:


  • state

  • getters

  • actions

Vue 是如何收集依赖的?

在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶


function defieneReactive (obj, key, val){  const dep = new Dep();  ...  Object.defineProperty(obj, key, {    ...    get: function reactiveGetter () {      if(Dep.target){        dep.depend();        ...      }      return val    }    ...  })}
复制代码


以上只保留了关键代码,主要就是 const dep = new Dep()实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend() 进行依赖收集。 (1)Dep Dep 是整个依赖收集的核心,其关键代码如下:


class Dep {  static target;  subs;
constructor () { ... this.subs = []; } addSub (sub) { this.subs.push(sub) } removeSub (sub) { remove(this.sub, sub) } depend () { if(Dep.target){ Dep.target.addDep(this) } } notify () { const subs = this.subds.slice(); for(let i = 0;i < subs.length; i++){ subs[i].update() } }}
复制代码


Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶


(2)Watcher


class Watcher {  getter;  ...  constructor (vm, expression){    ...    this.getter = expression;    this.get();  }  get () {    pushTarget(this);    value = this.getter.call(vm, vm)    ...    return value  }  addDep (dep){        ...    dep.addSub(this)  }  ...}function pushTarget (_target) {  Dep.target = _target}
复制代码


Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。


(3)过程


在实例化 Vue 时,依赖收集的相关过程如下∶初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,


updateComponent = () => {  vm._update(vm._render())}new Watcher(vm, updateComponent)
复制代码


get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。


this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 方法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。刚才 Dep.target 已经被赋值为 watcher,于是便会执行 addDep 方法,然后走到 dep.addSub() 方法,便将当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便已经完成了一个依赖收集的过程。

Vue 为什么没有类似于 React 中 shouldComponentUpdate 的生命周期?

考点: Vue 的变化侦测原理


前置知识: 依赖收集、虚拟 DOM、响应式系统


根本原因是 Vue 与 React 的变化侦测方式有所不同


React 是 pull 的方式侦测变化,当 React 知道发生变化后,会使用 Virtual Dom Diff 进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要用 shouldComponentUpdate 进行手动操作来减少 diff,从而提高程序整体的性能.


Vue 是 pull+push 的方式侦测变化的,在一开始就知道那个组件发生了变化,因此在 push 的阶段并不需要手动控制 diff,而组件内部采用的 diff 方式实际上是可以引入类似于 shouldComponentUpdate 相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前 Vue 并没有考虑引入 shouldComponentUpdate 这种手动优化的生命周期.

Vue 中封装的数组方法有哪些,其如何实现页面更新

在 Vue 中,对响应式处理利用的是 Object.defineProperty 对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行 hack,让 Vue 能监听到其中的变化。 那 Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是 Vue 中对这些方法的封装:


// 缓存数组原型const arrayProto = Array.prototype;// 实现 arrayMethods.__proto__ === Array.prototypeexport const arrayMethods = Object.create(arrayProto);// 需要进行功能拓展的方法const methodsToPatch = [  "push",  "pop",  "shift",  "unshift",  "splice",  "sort",  "reverse"];
/** * Intercept mutating methods and emit events */methodsToPatch.forEach(function(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) { // push、unshift会新增索引,所以要手动observer case "push": case "unshift": inserted = args; break; // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。 case "splice": inserted = args.slice(2); break; } // if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听 // notify change ob.dep.notify();// 通知依赖更新 // 返回原生数组方法的执行结果 return result; });});
复制代码


简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用 notify,通知渲染 watcher,执行 update。

v-if 和 v-show 的区别

  • 手段:v-if 是动态的向 DOM 树内添加或者删除 DOM 元素;v-show 是通过设置 DOM 元素的 display 样式属性控制显隐;

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

  • 编译条件:v-if 是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show 是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且 DOM 元素保留;

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

  • 使用场景:v-if 适合运营条件不大可能改变;v-show 适合频繁切换。


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
进阶vue面试题总结_Vue_bb_xiaxia1998_InfoQ写作社区