loading

Vue 2.x 知识点总结

# Vue 跟 React 的异同点

相同点:

  • 1.都使用了虚拟 dom
  • 2.组件化开发
  • 3.都是单向数据流(父子组件之间,不建议子修改父传下来的数据)
  • 4.都支持服务端渲染

不同点:

  • 1.React 的 JSX,Vue 的 template
  • 2.数据变化,React 手动(setState),Vue 自动(初始化已响应式处理,Object.defineProperty)
  • 3.React 单向绑定,Vue 双向绑定
  • 4.React 的 Redux,Vue 的 Vuex

# MVVM 和 MVC

MVC

  • Model(模型):负责从数据库中取数据
  • View(视图):负责展示数据的地方
  • Controller(控制器):用户交互的地方,例如点击事件等等 思想:Controller 将 Model 的数据展示在 View 上

MVVM

  • VM:也就是 View-Model,做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
  • 思想:实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应 Vue 数据驱动的思想)

Vue 是 MVVM 框架,但是不是严格符合 MVVM,因为 MVVM 规定 Model 和 View 不能直接通信,而 Vue 的 ref 可以做到这点

# 为什么 data 是个函数并且返回一个对象呢?

data 之所以只一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行 data 函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染。

# 组件之间的传值方式

  • 父组件传值给子组件,子组件使用 props 进行接收
  • 子组件传值给父组件,子组件使用$emit+事件对父组件进行传值
  • 组件中可以使用$parent和$children 获取到父组件实例和子组件实例,进而获取数据
  • 使用$attrs和$listeners,在对一些组件进行二次封装时可以方便传值,例如 A->B->C
  • 使用$refs 获取组件实例,进而获取数据
  • 使用 Vuex 进行状态管理
  • 使用 eventBus 进行跨组件触发事件,进而传递数据
  • 祖孙组件:使用 provide 和 inject
  • 使用浏览器本地缓存,例如 localStorage
  • 父组件获取子组件数据:作用域插槽

# v-on 监听多个方法

<input type="text" v-on="{ input:onInput,focus:onFocus,blur:onBlur, }">
1

# Vue 的生命周期方法

  • beforeCreate(支持服务端渲染) 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
  • created(支持服务端渲染) 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el,如果非要想与 Dom 进行交互,可以通过 vm.$nextTick 来访问 Dom
  • beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
  • beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
  • updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
  • beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
  • destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
  • activated keep-alive 专属,组件被激活时调用
  • deactivated keep-alive 专属,组件被销毁时调用
  • errorCaptured(支持服务端渲染) 当捕获来自子孙组件的错误时被调用,这个钩子有三个参数:错误对象,发生错误的组件实例,错误来源信息的字符串;可以返回 false 以防止错误继续向上传递

异步请求在哪一步发起?

可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。 如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

# 父子组件生命周期钩子函数执行顺序

加载渲染过程

  • 父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted

子组件更新过程

  • 父 beforeUpdate->子 beforeUpdate->子 updated->父 updated

父组件更新过程

  • 父 beforeUpdate->父 updated

销毁过程

  • 父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

# v-if 和 v-show

  • 1.v-if 是通过控制 dom 元素的删除和生成来实现显隐,每一次显隐都会使组件重新跑一遍生命周期,因为显隐决定了组件的生成和销毁,v-show 由 false 变为 true 的时候不会触发组件的生命周期。 v-if 由 false 变为 true 则会触发组件的 beforeCreate、create、beforeMount、mounted 钩子,由 true 变为 false 会触发组件的 beforeDestory、destoryed 方法。
  • 2.v-show 是通过控制 dom 元素的 css 样式来实现显隐,不会销毁(display: none)
  • 3.频繁或者大数量显隐使用 v-show,否则使用 v-if

# this.xxx 读取顺序

props -> methods -> data -> computed

# 组件中 name 的作用

  • 项目使用 keep-alive 时,可搭配组件 name 进行缓存过滤
  • DOM 做递归组件时需要调用自身 name
  • vue-devtools 调试工具里显示的组件名称是由 vue 中组件 name 决定的

# computed 和 watch

# computed

本质是一个惰性求值的观察者computed watcher。其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,computed watcher 通过 this.dep.subs.length 判断有没有订阅者, 有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。) 没有的话,仅仅把 this.dirty = true (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

# watch

watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开 deep:true 选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听

watch: {
  firstName: {
    handler(newName, oldName) {
      this.fullName = newName + ' ' + this.lastName;
    },
    // 代表在wacth里声明了firstName这个方法之后立即执行handler方法
    immediate: true
  }
}

watch: {
  obj: {
    handler(newName, oldName) {
      console.log('obj.a changed');
    },
    immediate: true,
    deep: true
  }
}

// 字符串的形式监听
watch: {
  'obj.a': {
    handler(newName, oldName) {
      console.log('obj.a changed');
    },
    immediate: true,
    // deep: true
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

注意:Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcheruser watcher)三种

# Vue 的数据频繁变化但只会更新一次

  • 检测到数据变化
  • 开启一个队列
  • 在同一事件循环中缓冲所有数据改变
  • 如果同一个 watcher (watcherId 相同)被多次触发,只会被推入到队列中一次

不优化,每一个数据变化都会执行: setter->Dep->Watcher->update->run

优化后:执行顺序 update -> queueWatcher -> 维护观察者队列(重复 id 的 Watcher 处理) -> waiting 标志位处理 -> 处理$nextTick(在为微任务或者宏任务中异步更新 DOM)

# 为什么访问 data 属性不需要带 data

访问属性代理 this.data.xxx 转换 this.xxx 的实现

    /** 将 某一个对象的属性 访问 映射到 对象的某一个属性成员上 */
    function proxy( target, prop, key ) {
      Object.defineProperty( target, key, {
        enumerable: true,
        configurable: true,
        get () {
          return target[ prop ][ key ];
        },
        set ( newVal ) {
          target[ prop ][ key ] = newVal;
        }
      } );
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

# Vue 中的 key 到底有什么用

key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 注意:在没有 key 的情况下,会更快。 引用官网的话:key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)

更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。

# 为什么不能用 index 做 key

如果 v-for 中的列表数据顺序被改变,在 diff 比较中将错误地复用旧节点,导致不必要的重新渲染步骤

<div v-for="(item, index) in list" :key="index">{{item.name}}</div>

list: [
    { name: '小明', id: '123' },
    { name: '小红', id: '124' },
    { name: '小花', id: '125' }
]

渲染为
<div key="0">小明</div>
<div key="1">小红</div>
<div key="2">小花</div>

现在我执行 list.unshift({ name: '小林', id: '122' })

渲染为
<div key="0">小林</div>
<div key="1">小明</div>
<div key="2">小红</div>
<div key="3">小花</div>


新旧对比

<div key="0">小明</div>  <div key="0">小林</div>
<div key="1">小红</div>  <div key="1">小明</div>
<div key="2">小花</div>  <div key="2">小红</div>
                         <div key="3">小花</div>

可以看出,如果用index做key的话,其实是更新了原有的三项,并新增了小花,虽然达到了渲染目的,但是损耗性能

现在我们使用id来做key,渲染为

<div key="123">小明</div>
<div key="124">小红</div>
<div key="125">小花</div>

现在我执行 list.unshift({ name: '小林', id: '122' }),渲染为

<div key="122">小林</div>
<div key="123">小明</div>
<div key="124">小红</div>
<div key="125">小花</div>

新旧对比

                           <div key="122">小林</div>
<div key="123">小明</div>  <div key="123">小明</div>
<div key="124">小红</div>  <div key="124">小红</div>
<div key="125">小花</div>  <div key="125">小花</div>

可以看出,原有的三项都不变,只是新增了小林这个人,这才是最理想的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# vue-router 路由模式

默认值: "hash" (浏览器环境) | "abstract" (Node.js 环境) 可选值: "hash" | "history" | "abstract" 配置路由模式:

hash:

  • 基于 location.hash 来实现的,location.hash 的值就是 URL 中#后面的内容。其实现原理就是监听#后面的内容来发起 Ajax 请求来进行局部更新,而不需要刷新整个页面。
  • 使用 hashchange 事件来监听 URL 的变化,以下这几种情况改变 URL 都会触发 hashchange 事件:浏览器前进后退改变 URL、a 标签改变 URL、window.location 改变 URL。

history:

  • history 提供了 pushState 和 replaceState 两个方法来记录路由状态,通过这两个 API 可以改变 url 地址且不会发送请求。
  • history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:通过浏览器前进或者后退改变 URL 时会触发 popstate 事件,通过 pushState/replaceState 或 a 标签改变 URL 不会触发 popstate 事件。可以拦截 pushState/replaceState 的调用和 a 标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。
  • pushState(state, title, url) 和 replaceState(state, title, url)都可以接受三个相同的参数。
  • 用了 HTML5 的实现,单页路由的 url 就不会多出一个 # ,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。

abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

# params 和 query 的区别

  • url 地址显示:query 在浏览器地址栏中显示参数,params 则不显示
  • query 刷新不会丢失 query 里面的数据、params 刷新会丢失 params 里面的数据。
//query语法:
this.$router.push({path:"地址",query:{id:"123"}}); //这是传递参数
this.$route.query.id; //这是接受参数
//params语法:
this.$router.push({name:"地址",params:{id:"123"}}); //这是传递参数
this.$route.params.id; //这是接受参数
1
2
3
4
5
6

# 动态组件

动态组件通过 is 特性实现。适用于根据数据、动态渲染的场景,即组件类型不确定。

<template>
  <div v-for="val in componentsData" :key="val.id">
    <component :is="val.type">
  </div>
</template>
<script>
import CustomTitle from './CustomTitle'
import CustomText from './CustomText'
import CustomImage from './CustomImage'

export default {
  data() {
    return {
      componentsData: [{
        id: 1,
        type: 'CustomTitle'
      },{
        id: 2,
        type: 'CustomText'
      },{
        id: 3
        type: 'CustomImage'
      }]
    }
  }
}
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 路由钩子函数执行顺序

# 在失活的组件里调用 beforeRouteLeave 组件守卫。

  //A.vue
  beforeRouteLeave(to, from) {
    console.log('🚀🚀~ beforeRouteLeave');
  },
1
2
3
4

# 调用全局的 beforeEach 全局守卫

router.beforeEach((to, from, next) => {
  console.log('🚀🚀~ to:', to);
  console.log('🚀🚀~ from:', from);
  next();
})
1
2
3
4
5

# 在重用的组件里调用 beforeRouteUpdate 组件守卫。

  //A.vue
  beforeRouteUpdate(to, from) {
    console.log('🚀🚀~ beforeRouteUpdate');
  },
1
2
3
4

# 在路由配置里调用 beforeEnter 路由守卫

//index.js
{
  path: '/a',
  component: () => import('../components/A.vue'),
  beforeEnter: (to, from) => {
   console.log('🚀🚀~ beforeEnter ');
  },
},
1
2
3
4
5
6
7
8

# 解析异步路由组件

# 在被激活的组件里调用 beforeRouteEnter 组件守卫

  //A.vue
  beforeRouteEnter(to, from,next) {
    console.log('🚀🚀~ beforeRouteEnter');
  },
1
2
3
4

# 调用全局的 beforeResolve 全局守卫

router.beforeResolve((to, from, next) => {
  next();
})
1
2
3

# 调用全局的 afterEach 全局守卫

router.afterEach((to, from) => {
  console.log('🚀🚀~ afterEach:');
})
1
2
3

# 触发 DOM 更新

# v-if 和 v-for 不建议用在同一标签

在 Vue2 中,v-for 优先级是高于 v-if 的

<div v-for="item in [1, 2, 3, 4, 5, 6, 7]" v-if="item !== 3">
    {{item}}
</div>
1
2
3

上面的写法是 v-for 和 v-if 同时存在,会先把 7 个元素都遍历出来,然后再一个个判断是否为 3,并把 3 给隐藏掉,这样的坏处就是,渲染了无用的 3 节点,增加无用的 dom 操作,建议使用 computed 来解决

<div v-for="item in list">
    {{item}}
</div>

computed() {
    list() {
        return [1, 2, 3, 4, 5, 6, 7].filter(item => item !== 3)
    }
  }
1
2
3
4
5
6
7
8
9

# 设置动态 class,动态 style

动态class对象:<div :class="{ 'is-active': true, 'red': isRed }"></div>
动态class数组:<div :class="['is-active', isRed ? 'red' : '' ]"></div>
动态style对象:<div :style="{ color: textColor, fontSize: '18px' }"></div>
动态style数组:<div :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"></div>
1
2
3
4

# 不需要响应式的数据

数据量大的死数据,如果都进行响应式处理,那会消耗大量性能

// 方法一:将数据定义在data之外
data () {
    this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    return {}
 }

// 方法二:Object.freeze()
data () {
    return {
        list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
    }
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 对象新属性无法更新视图,删除属性无法更新视图

  • 原因:Object.defineProperty 没有对对象的新属性进行属性劫持
  • 对象新属性无法更新视图:使用 Vue.$set(obj, key, value),组件中this.$set(obj, key, value)
  • 删除属性无法更新视图:使用 Vue.$delete(obj, key),组件中this.$delete(obj, key)

# 直接 arr[index] = xxx 无法更新视图

  • 原因:Vue 没有对数组进行 Object.defineProperty 的属性劫持,所以直接 arr[index] = xxx 是无法更新视图的
  • 使用数组的 splice 方法,arr.splice(index, 1, item)
  • 使用 Vue.$set(arr, index, value)

# 插槽

# 单个插槽

//子组件 : (假设名为:child)
<template>
  <div class= 'child'>
      <slot>子组件默认内容,父组件不在子组件写数据就会默认显示这个</slot>
  </div>
</template>

//父组件:(引用子组件 child)
<template>
  <div class= 'app'>
     <child>
        林三心
     </child>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 具名插槽 (子组件多个对应插入内容)

//子组件 : (假设名为:child)
<template>
  <div class= 'child'>
      <slot name='one'> 这就是默认值1</slot>
      <slot name='two'> 这就是默认值2 </slot>
      <slot name='three'> 这就是默认值3 </slot>
  </div>
</template>

//父组件:(引用子组件 child)
<template>
  <div class= 'app'>
     <child>
        <template v-slot:"one"> 这是插入到one插槽的内容 </template>
        <template v-slot:"two"> 这是插入到two插槽的内容 </template>
        <template v-slot:"three"> 这是插入到three插槽的内容 </template>
     </child>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 作用域插槽 (父组件在子组件处使用子组件 data)

//子组件 : (假设名为:child)
<template>
  <div class= 'child'>
      <slot name= 'one' :value1='child1'> 这就是默认值1</slot>    //绑定child1的数据
      <slot :value2='child2'> 这就是默认值2 </slot>  //绑定child2的数据,这里我没有命名slot
  </div>
</template>

new Vue({
  el:'child',
  data:{
    child1:'数据1',
    child2:'数据2'
  }
})

//父组件:(引用子组件 child)
<template>
  <div class='app'>
     <child>
        <template v-slot:one='slotone'>
           {{ slotone.value1 }}    // 通过v-slot的语法 将子组件的value1值赋值给slotone
        </template>
        <template v-slot:default='slotde'>
           {{ slotde.value2 }}  // 同上,由于子组件没有给slot命名,默认值就为default
        </template>
     </child>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# nextTick 的用处

  • 响应式数据更新后,想要立即拿到页面上对应的 DOM 数据,需要用到 nextTick
  • Vue 采用的是异步更新的策略,通俗点说就是,同一事件循环内多次修改,会统一进行一次视图更新,这样才能节省性能
  • .Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发 生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick 方法会在队列中加入一个回调函数,确保该函数在前面的 dom 操作完成后才调用。
  • microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕;
  • 因为兼容性问题,vue 不得不做了 microtask 向 macrotask 的降级方案:Promise、MutationObserver、setImmediate、setTimeout.

# keep-alive

keep-alive组件接受三个属性参数:includeexcludemax

  • include 指定需要缓存的组件name集合,参数格式支持String, RegExp, Array。当为字符串的时候,多个组件名称以逗号隔开。
  • exclude 指定不需要缓存的组件name集合,参数格式和include一样。
  • max 指定最多可缓存组件的数量,超过数量删除第一个。参数格式支持StringNumber

# 原理

keep-alive 实例会缓存对应组件的 VNode,如果命中缓存,直接从缓存对象返回对应 VNode LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。(墨菲定律:越担心的事情越会发生)

# vuex 的属性

State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。 Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。 Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。 Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。 Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

# vuex 中的数据在页面刷新后数据消失

用 sessionstorage 或者 localstorage 存储数据,或者用 vuex-persist 插件

# Vuex 的严格模式

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

在 Vuex.Store 构造器选项中开启,如下

const store = new Vuex.Store({
    strict:true,
})
1
2
3

# 组件中使用 Vuex 的 getter 和 mutation

// 使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中
import {mapGetters} from 'vuex'
export default{
    computed:{
        ...mapGetters(['total','discountTotal'])
    }
}
1
2
3
4
5
6
7
import { mapMutations } from 'vuex'
methods:{
    ...mapMutations({
        setNumber:'SET_NUMBER',
    })
}
然后调用this.setNumber(10)相当调用this.$store.commit('SET_NUMBER',10)
1
2
3
4
5
6
7

# mutation 和 action

  • action 提交的是 mutation,而不是直接变更状态。mutation 可以直接变更状态
  • action 可以包含任意异步操作。mutation 只能是同步操作
  • 提交方式不同
action 是用this.store.dispatch('ACTION_NAME',data)来提交。
mutation是用this.$store.commit('SET_NUMBER',10)来提交
1
2

接收参数不同,mutation 第一个参数是 state,而 action 第一个参数是 context,其包含了

{
    state,      // 等同于 `store.state`,若在模块中则为局部状态
    rootState,  // 等同于 `store.state`,只存在于模块中
    commit,     // 等同于 `store.commit`
    dispatch,   // 等同于 `store.dispatch`
    getters,    // 等同于 `store.getters`
    rootGetters // 等同于 `store.getters`,只存在于模块中
}

1
2
3
4
5
6
7
8
9

# Vue 的 SSR

  • SSR 就是服务端渲染
  • 基于 nodejs serve 服务环境开发,所有 html 代码在服务端渲染
  • 数据返回给前端,然后前端进行“激活”,即可成为浏览器识别的 html 代码
  • SSR 首次加载更快,有更好的用户体验,有更好的 seo 优化,因为爬虫能看到整个页面的内容,如果是 vue 项目,由于数据还要经过解析,这就造成爬虫并不会等待你的数据加载完成,所以其实 Vue 项目的 seo 体验并不是很好

# Vue 的响应式原理

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:

  • 1、需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
  • 2、compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
  • 3、Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: ① 在自身实例化时往属性订阅器(dep)里面添加自己 ② 自身必须有一个 update()方法 ③ 待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功成身退。
  • 4、MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

# 为什么只对对象劫持,而要对数组进行方法重写

因为对象最多也就几十个属性,拦截起来数量不多,但是数组可能会有几百几千项,拦截起来非常耗性能,所以直接重写数组原型上的方法,是比较节省性能的方案

# vue 渲染过程

  • 1.调用 compile 函数,生成 render 函数字符串 ,编译过程如下:

  • parse 使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST。模板 -> AST (最消耗性能)

  • optimize 遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 的性能

  • generate 将最终的 AST 转化为 render 函数字符串

  • 2.调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
  • 3.调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

# prop 验证,和默认值

props: {
    visible: {
        default: true,
        type: Boolean,
        required: true
    },
},
1
2
3
4
5
6
7

# vue 中怎么重置 data

  • 使用 Object.assign(),vm.data 可以获取当前状态下的 data,vm.data 可以获取当前状态下的 data
  • vm.options.data(this)可以获取到组件初始化状态下的 data。
Object.assign(this.$data, this.$options.data(this)) // 注意加this,不然取不到

data(){ a: this.methodA } 中的this.methodA。

1
2
3
4

# Vue.set 方法的原理

function set(target, key, val) {
    // 判断是否是数组
    if (Array.isArray(target)) {
        // 判断谁大谁小
        target.length = Math.max(target.length, key)
        // 执行splice
        target.splice(key, 1, val)
        return val
    }

    const ob = target.__ob__

    // 如果此对象没有不是响应式对象,直接设置并返回
    if (key in target && !(key in target.prototype) || !ob) {
        target[key] = val
        return val
    }

    // 否则,新增属性,并响应式处理
    defineReactive(target, key, val)
    return val
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# Vue.delete 方法的原理

function del (target, key) {
    // 判断是否为数组
    if (Array.isArray(target)) {
        // 执行splice
        target.splice(key, 1)
        return
    }

    const ob = target.__ob__

    // 对象本身就没有这个属性,直接返回
    if (!(key in target)) return


    // 否则,删除这个属性
    delete target[key]

    // 判断是否是响应式对象,不是的话,直接返回
    if (!ob) return
    // 是的话,删除后要通知视图更新
    ob.dep.notify()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 冷门的知识点

# 子组件改变 props 里的数据

  • 如果修改的是基本类型,则会报错
  • 改变的 props 数据是引用类型,不报错,并且父级数据会跟着变

# props 怎么自定义验证

props: {
    num: {
      default: 1,
      validator: function (value) {
          // 返回值为true则验证不通过,报错
          return [
            1, 2, 3, 4, 5
          ].indexOf(value) !== -1
    }
    }
  }
1
2
3
4
5
6
7
8
9
10
11

# watch 的 immediate 属性有什么用

比如平时 created 时要请求一次数据,并且当搜索值改变,也要请求数据

一般写法

created(){
  this.getList()
},
watch: {
  searchInputValue(){
    this.getList()
  }
}
1
2
3
4
5
6
7
8

使用 immediate 可以这么写,当它为 true 时,会初始执行一次

watch: {
  searchInputValue:{
    handler: 'getList',
    immediate: true
  }
}
1
2
3
4
5
6

# watch 监听一个对象时,如何排除某些属性的监听

下面代码是,params 发生改变就重新请求数据,无论是 a,b,c,d 属性改变

data() {
    return {
      params: {
        a: 1,
        b: 2,
        c: 3,
        d: 4
      },
    };
  },
watch: {
    params: {
      deep: true,
      handler() {
        this.getList;
      },
    },
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

只想要 a,b 改变时重新请求,c,d 改变时不重新请求

mounted() {
    Object.keys(this.params)
      .filter((_) => !["c", "d"].includes(_)) // 排除对c,d属性的监听
      .forEach((_) => {
        this.$watch((vm) => vm.params[_], handler, {
          deep: true,
        });
      });
  },
data() {
    return {
      params: {
        a: 1,
        b: 2,
        c: 3,
        d: 4
      },
    };
  },
watch: {
    params: {
      deep: true,
      handler() {
        this.getList;
      },
    },
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 审查元素时发现 data-v-xxxxx

这是在标记 vue 文件中 css 时使用 scoped 标记产生的,因为要保证各文件中的 css 不相互影响,给每个 component 都做了唯一的标记,所以每引入一个 component 就会出现一个新的'data-v-xxx'标记

# computed 如何实现传参

// html
<div>{{ total(3) }}

// js
computed: {
    total() {
      return function(n) {
          return n * this.num
         }
    },
  }

1
2
3
4
5
6
7
8
9
10
11
12

# vue 的 hook 的使用

常用的使用定时器的方式

export default{
  data(){
    timer:null
  },
  mounted(){
      this.timer = setInterval(()=>{
      //具体执行内容
      console.log('1');
    },1000);
  }
  beforeDestory(){
    clearInterval(this.timer);
    this.timer = null;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

不好的地方在于:得全局多定义一个 timer 变量,可以使用 hook 这么做:

export default{
  methods:{
    fn(){
      const timer = setInterval(()=>{
        //具体执行代码
        console.log('1');
      },1000);
      this.$once('hook:beforeDestroy',()=>{
        clearInterval(timer);
        timer = null;
      })
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

子组件需要在 mounted 时触发父组件的某一个函数,平时都会这么写:

//父组件
<rl-child @childMounted="childMountedHandle"
/>
method () {
  childMountedHandle() {
  // do something...
  }
},

// 子组件
mounted () {
  this.$emit('childMounted')
},
1
2
3
4
5
6
7
8
9
10
11
12
13

使用 hook

//父组件
<rl-child @hook:mounted="childMountedHandle"
/>
method () {
  childMountedHandle() {
  // do something...
  }
},
1
2
3
4
5
6
7
8

# provide 和 inject 是响应式的吗

// 祖先组件
provide(){
    return {
   // keyName: { name: this.name }, // value 是对象才能实现响应式,也就是引用类型
      keyName: this.changeValue // 通过函数的方式也可以[注意,这里是把函数作为value,而不是this.changeValue()]
   // keyName: 'test' value 如果是基本类型,就无法实现响应式
    }
  },
data(){
  return {
	name:'张三'
}
  },
  methods: {
  	changeValue(){
  		this.name = '改变后的名字-李四'
  	}
  }

  // 后代组件
  inject:['keyName']
  create(){
	console.log(this.keyName) // 改变后的名字-李四
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# mixin 数据冲突

  • 当组件 data 选项与混入 data 选项冲突时以组件 data 优先
  • 当组件和 mixin 同时定义生命周期选项,两个都会触发,而且 mixin 会先触发.
  • 组件和 mixin 同时定义相同方法,会使用组件方法,会覆盖 mixin.

# Vue 的 el 属性和$mount 优先级

new Vue({
  router,
  store,
  el: '#app',
  render: h => h(App)
}).$mount('#ggg')

// el优先级 > $mount
1
2
3
4
5
6
7
8

# 动态指令和参数

<template>
    ...
    <aButton @[someEvent]="handleSomeEvent()" :[someProps]="1000" />...
</template>
<script>
  ...
  data(){
    return{
      ...
      someEvent: someCondition ? "click" : "dbclick",
      someProps: someCondition ? "num" : "price"
    }
  },
  methods: {
    handleSomeEvent(){
      // handle some event
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 相同的路由组件如何重新渲染

经常遇到的情况是,多个路由解析为同一个 Vue 组件。问题是,Vue 出于性能原因,默认情况下共享组件将不会重新渲染,如果你尝试在使用相同组件的路由之间进行切换,则不会发生任何变化。

const routes = [
  {
    path: "/a",
    component: MyComponent
  },
  {
    path: "/b",
    component: MyComponent
  },
];
1
2
3
4
5
6
7
8
9
10

如果依然想重新渲染,可以使用 key

<template>
    <router-view :key="$route.path"></router-view>
</template>
1
2
3

# 自定义 v-model

默认情况下,v-model 是 @input 事件侦听器和 :value 属性上的语法糖。但是,可以在 Vue 组件中指定一个模型属性来定义使用什么事件和 value 属性

export default: {
  model: {
    event: 'change',
    prop: 'checked'
  }
}
1
2
3
4
5
6

# 获取 data 中某一个数据的初始状态

data() {
    return {
      num: 10
  },
mounted() {
    this.num = 1000
  },
methods: {
    howMuch() {
        // 计算出num增加了多少,那就是1000 - 初始值
        // 可以通过this.$options.data().xxx来获取初始值
        console.log(1000 - this.$options.data().num)
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 计算变量时,methods 和 computed 哪个好

<div>
    <div>{{howMuch1()}}</div>
    <div>{{howMuch2()}}</div>
    <div>{{index}}</div>
</div>

data: () {
    return {
         index: 0
       }
     }
methods: {
    howMuch1() {
        return this.num + this.price
    }
  }
computed() {
    howMuch2() {
        return this.num + this.price
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

computed 会好一些,因为 computed 会有缓存。例如 index 由 0 变成 1 ,那么会触发视图更新,这时候 methods 会重新执行一次,而 computed 不会,因为 computed 依赖的两个变量 num 和 price 都没变。

# 父组件如何监听子组件生命周期

$emit 实现

// 父组件
<template>
  <div class="parent">
    <Child @mounted="doSomething"/>
  </div>
</template>
<script>
export default {
  methods: {
    doSomething() {
      console.log('父组件监听到子组件 mounted 钩子函数')
    }
  }
}
</script>
//子组件
<template>
  <div class="child">
  </div>
</template>
<script>
export default {
  mounted() {
    console.log('触发mounted事件...')
    this.$emit("mounted")
  }
}
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

@hook 实现

// 父组件
<template>
  <div class="parent">
    <Child @hook:mounted="doSomething"/>
  </div>
</template>
<script>
export default {
  methods: {
    doSomething() {
      console.log('父组件监听到子组件 mounted 钩子函数')
    }
  }
}
</script>

//子组件
<template>
  <div class="child">
  </div>
</template>
<script>
export default {
  mounted() {
    console.log('触发mounted事件...')
  }
}
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 函数式组件

  • 无状态
  • 无法实例化
  • 内部没有任何生命周期处理函数
  • 轻量,渲染性能高,适合只依赖于外部数据传递而变化的组件(展示组件,无逻辑和状态修改)
  • 在 template 标签里标明 functional
  • 只接受 props 值
  • 不需要 script 标签
<template functional>
      <div>
            <p v-for="(item,index) in props.items" :key="index" @click="props.itemClick(item)" />
      </div>
</template>
1
2
3
4
5

# 监听 Vuex 里面的数据的变更

  • .vue 文件中可以直接用 watch 来监听
  • 那么如何在 js 或者其他文件中监听?有两种方法

第一种:store.subscribe();subscribe 是 vuex 的一个实例方法,它通过订阅 store 的 mutation,接收一个函数作为参数,会在每个 mutation 完成后调用

  • 明显的缺点就是由于每个 mutation 方法完成后都会调用,所以如果只想订阅某个特定的数据需要写逻辑来判断

import store from "../src/store/index";

const subFun = store.subscribe((mutation, state) => {
    console.log("rd: state", state);
    console.log("rd: mutation", mutation);
    console.log("rd: mutation.type", mutation.type); // type就是我们当前执行的mutation方法的方法名
    console.log("rd: mutation.payload", mutation.payload); // payload就是我们当次执行mutation所传入的值
});


setTimeout(() => {
    subFun(); // 调用返回值可以取消订阅
}, 10000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

第二种方法:store.watch(); 相比于上一种方法可以监听特定的数据

import store from "../store/index";

const watchFun = store.watch(
    state => state.pathName,
    (newValue, oldValue) => {
        console.log("search string is changing");
        console.log("rd: newValue", newValue);
        console.log("rd: oldValue", oldValue);
    }
);

setTimeout(() => {
    watchFun();
}, 10000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 在相应的 js 文件引入 store
  • 调用 store 的 watch 实例方法,第一个函数参数返回一个需要监听的 state 中的值(比如我想监听 vuex 中的 pathName 的变化情况,就返回该值)
  • 第二个参数同 vue 的 watch,接收 2 个参数代表新旧值
  • 通过变量 watchFun 接收 watch 的返回值,调用该方法会停止监听

# Vuex 如何知道 State 是通过 Mutation 修改还是外部修改

Vuex 中修改 state 唯一渠道是执行 commit 方法,底层通过执行 this._withCommit(fn),且设置_committing 标识符为 true,才能修改 state,修改完还需要将标识符置为 false。外部修改是无法设置标识位的,所以通过 watch 监听 state 变化,来判断修改的合法性。

# new Vue 后整个的流程

  • initProxy:作用域代理,拦截组件内访问其它组件的数据。
  • initLifecycle:建立父子组件关系,在当前组件实例上添加一些属性和生命周期标识。如[Math Processing Error]parent,parent,refs,$children,_isMounted 等。
  • initEvents:对父组件传入的事件添加监听,事件是谁创建谁监听,子组件创建事件子组件监听
  • initRender:声明[Math Processing Error]slots 和 slots 和 createElement()等。
  • initInjections:注入数据,初始化 inject,一般用于组件更深层次之间的通信。
  • initState:重要)数据响应式:初始化状态。很多选项初始化的汇总:data,methods,props,computed 和 watch。
  • initProvide:提供数据注入。
最近更新时间: 2022/09/28 16:26:36
最近更新
01
2023/07/03 00:00:00
02
2023/04/22 00:00:00
03
2023/02/16 00:00:00