进阶vue面试题总结

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

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

使用场景:

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

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

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

  • 商品价格:{{item.price | filterPrice}}
  • 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 = [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 {
        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. 需要缓存,判断他当前是否在缓存数组里面:
    4. 存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU)
    • 不存在,将组件 key 放入数组,然后判断当前 key数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key
    • 最后将这个组件的 keepAlive 设置为 true

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

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

    首次渲染

    • 组件的首次渲染∶判断组件的 abstract 属性,才往父组件里面挂载 DOM
    // core/instance/lifecycle
    function 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-component
    init (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 [email protected] my-vite-app --template vue-ts
    1. pinia基础使用
    yarn add pinia
    // main.ts
    import { 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.ts
    import { defineStore } from 'pinia'
    
    // 第一个参数是应用程序中 store 的唯一 id
    export const useUsersStore = defineStore('users', {
      // 其它配置项
    })

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

    • name:一个字符串,必传项,该store的唯一id
    • options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。

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

    2.2 使用store

    
    

    2.3 添加state

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

    2.4 读取state数据

    
    

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

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

    2.5 修改state数据

    
    

    2.6 重置state

    • 有时候我们修改了state数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。
    • 此时,我们直接调用store$reset()方法即可,继续使用我们的例子,添加一个重置按钮
    
    // 重置store
    const reset = () => {
      store.$reset();
    };

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

    2.7 批量更改state数据

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

    
    // 批量修改数据
    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属性
    2. gettersdefineStore参数配置项里面的另一个属性
    3. 可以把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

    
    

    上段代码中我们直接在标签上使用了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
        },
      },
    });

    新年龄:{{ store.getAddAge(1100) }}

    1. actions属性
    2. 前面我们提到的stategetters属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data数据和computed计算属性一样。
    3. 那么,如果我们有业务代码的话,最好就是卸载actions属性里面,该属性就和我们组件代码中的methods相似,用来放置一些处理业务逻辑的方法。
    4. 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.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) {
      // 缓存原生数组方法
      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适合频繁切换。

    你可能感兴趣的:(vue.js)