乐趣区

关于vue.js:进阶vue面试题总结

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

依据过滤器的名称,过滤器是用来过滤数据的,在 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. 须要缓存,判断他以后是否在缓存数组外面:
  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 很简略,调用 p inia中的 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>
// 重置 store
const 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属性
  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

<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属性
  2. 后面咱们提到的 stategetter s 属性都次要是数据层面的,并没有具体的业务逻辑代码,它们两个就和咱们组件代码中的 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 适宜频繁切换。
退出移动版