Vue

# Vue基础

# computed、watch与methods

computed

  1. 支持缓存,计算属性是基于响应式依赖进行缓存的,也就是data中或父组件传过来的props中的数据,只有在响应式数据发生变化才会重新计算
  2. 不支持异步,当computed中有异步操作时,无法监听数据的变化
  3. 计算属性中函数的返回值就是属性的属性值
  4. 应用场景:需要进行数值计算,且依赖于其他数据,可以利用computed的缓存特性,避免每次获取值都要重新计算。

watch:

  1. 不支持缓存,只要监听的数据变化,就会触发,监听数据必须是响应式数据
  2. 支持异步监听,监听函数接收两个值:新值和旧值
  3. 应用场景:需要在数据变化时执行异步或开销较大的操作,watch可以限制执行该操作的频率,并在得到最终结果前设置中间状态,这是计算属性无法做到的。

可以将同一函数定义为 method 或 computed,对于最终的结果,两种方式都是相同的。但是计算属性可以缓存,而method调用总会执行该函数。

# v-if、v-show与v-html

v-if:

  1. v-if是惰性的,如果初始条件为假,则渲染时会忽略对应节点;只有在条件第一次变为真时才开始局部编译
  2. v-if切换有一个局部编译/卸载的过程,切换过程中会销毁和重建内部的事件监听和子组件
  3. v-if有更高的切换消耗,适合条件不容易被改变的情况

v-show

  1. 无论首次条件是否为真,都被编译,然后缓存,而且DOM元素保留
  2. v-show是通过设置DOM元素的display样式属性控制显隐的
  3. v-show有更高的初始渲染消耗,适合需要频繁切换的场景

v-html:会先移除节点下的所有子节点,调用html方法,通过addProp给节点设置innerHTML为v-html的值。

# v-if和v-for的优先级问题

  • 在Vue2.x中,v-for优先于v-if,如果同时出现,每次渲染都会执行循环再判断条件,这样渲染就不可避免,浪费性能。
  • 在Vue3.x中,v-if优先于v-for。

由于语法上存在歧义,建议避免在同一元素上同时使用两者。更好的办法是通过创建计算属性筛选出列表,一次创建可见元素。

# v-model语法糖

# data为什么是一个函数而不是对象

Vue组件可能存在多个实例,如果使用对象形式定义data,则会导致它们共用一个data对象,那么状态变更将会影响所有组件实例,这是不合理的;采用函数形式定义,在initData时会将其作为工厂函数返回全新data对象,有效规避多实例之间状态污染问题。

而在Vue根实例创建过程中则不存在该限制,也是因为根实例只能有一个,不需要担心这种情况。

# slot插槽

# 对keep-alive组件的理解

keep-alive是Vue提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。这也可以使用v-show来达到目的,但是v-show在初始渲染时会全部渲染,而keep-alive只有在被包裹的组件第一次切换时才被渲染并保存在内存中。

keep-alive的生命周期

如果为一个组件包裹了keep-alive,它会多出两个生命周期: activateddeactivated

  • 当组件被切换时,会被缓存到内存中、触发 deactivated 生命周期;
  • 当组件被切换回来时,再去缓存里找这个组件、触发 activated 钩子函数。

彻底揭秘keep-alive原理 (opens new window)

# $nextTick的原理及作用

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

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

# vm.$set()

在Vue2.x,如果直接给vue中data的对象添加属性,或直接设置数组的某项值,会发现视图并未刷新。这是因为defineProperty的限制(Vue3的proxy可以完美解决这些问题),如果在Vue实例创建时没有声明,就不会被Vue转换成响应式属性,自然就不会触发视图的更新,这个时候就要使用Vue的全局api $set()

this.$set(要改变的数组/对象, 索引/key, value);

this.$set(this.arr, 0, "1"); // 改变数组
this.$set(this.obj, "c", "o"); // 改变对象
1
2
3
4

vm.$set()实现原理

  • 如果目标是数组,直接使用数组的splice方法触发响应式(该方法已被vue重写)
  • 如果目标是对象,会先判断属性是否存在,对象是否为响应式。如果要对属性进行响应式处理,则通过调用defineReactive方法(该方式就是Vue在初始化对象时,给对象属性采用Object.defineProperty动态添加getter和setter功能所调用的方法)

# Vue如何收集依赖

参考 (opens new window)

# 单页面应用与多页面应用

  • SPA单页面应用:只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
    • 优点:
      1. 用户体验好、内容改变不需要重新加载整个页面
      2. 避免了不必要的重复渲染,SPA相对对服务器压力小
    • 缺点:
      1. 初次加载耗时长
      2. 前进后退路由管理:SPA在一个页面中显示所有内容,所以不能使用浏览器的前进后退功能,所有页面切换需要自己建立堆栈管理
      3. 不利于SEO,所有内容都在一个页面中动态替换显示
  • MPA多页面应用:有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。

image-20220309161624614

# 预渲染

如果仅希望改善网站的一些推广页面 (例如//about/contact等) 的SEO,预渲染比SSR更加合适。预渲染可以在构建时为指定的路由生成静态HTML文件,通俗来讲就是把希望被爬虫抓取的页面通过预渲染插件(webpack中的prerender-spa-plugin插件)来提前渲染出来,好让爬虫抓取这些内容,改善SEO。

# SSR

SSR即服务端渲染:将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后把html直接返回给客户端。它通常是由内容呈现时间对应用的重要程度决定的,SSR可以实现最佳的初始加载性能。

优点

  • 更好的SEO,因为搜索引擎爬虫会直接读取完整的渲染出来的页面。
  • 更快的内容呈现,尤其是网络连接缓慢或设备运行速度缓慢的时候。服务端标记不需要等待所有的 JavaScript 都被下载并执行之后才显示,所以用户可以更快看到完整的渲染好的内容。

缺点

  • 开发条件会受限,浏览器特有的代码只能在特定的生命周期钩子中使用;一些外部的库在服务端渲染应用中可能需要经过特殊处理。SSR只支持调用beforeCreatecreated两个钩子,其他只会在客户端执行。
  • 不同于一个完全静态的 SPA 可以部署在任意的静态文件服务器,服务端渲染应用需要一个能够运行 Node.js 服务器的环境。
  • 更多的服务端负载。在 Node.js 中渲染一个完整的应用会比仅供应静态文件产生更密集的 CPU 运算。所以如果流量很高,需要能够承担相应负载的服务器并采取缓存策略。

# 生命周期

从Vue实例创建、运行、到销毁期间,会运行一些叫生命周期钩子的函数。主要的生命周期分类:

  1. 创建期间:beforeCreate、created、beforeMount、mounted
    • beforeCreate:在实例初始化之后,data的响应式追踪、event/watcher之前被调用,也就是说不能访问到data、computed、watch、methods上的数据和方法。
    • created:实例创建完成之后被调用,实例上配置的options包括data、computed、watch、methods等都配置完成了,但是此时渲染节点还未挂载到DOM,所以不能访问到与实例绑定的 $el 属性对应的节点,如果想要与DOM进行交互,可以通过vm.$nextTick来访问DOM。
    • beforeMount:在挂载前调用,相关的render函数首次被调用。完成以下配置:编译模板,把data里的数据和模板生成html。此时还没有挂载到html页面上。
    • mounted:在挂载后调用,此时真实的DOM已经挂载完毕,数据已经被监测,可以访问DOM节点。
  2. 运行期间:beforeUpdate、updated
    • beforeUpdate:响应式数据更新时调用,发生在虚拟DOM重新渲染和打补丁(patch)之前。这里可以进一步变更状态,不会触发附加的重渲染过程。
    • updated:发生在DOM更新完成之后,这期间避免更改数据,可能导致更新无限循环。
  3. 销毁期间:beforeDestroy、destroyed(beforeUnmount、unmounted)
    • beforeDestroy/beforeUnmount:实例销毁前调用,这里可以进行善后首尾工作,如清除定时器。
    • destroyed/unmounted:实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

异步请求可以在 created、beforeMount、mounted 中进行,这里data可以访问,且可以将服务器返回的数据进行赋值。如果异步请求不依赖DOM,推荐在created中调用,有以下优点:

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

Vue3的生命周期销毁阶段的钩子函数有了些变动,且新添了一些其他的生命周期函数 (opens new window)。下图就是Vue3的生命周期图:

实例的生命周期

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

  • 加载渲染过程:父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
  • 子组件更新过程:父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
  • 父组件更新过程:父 beforeUpdate->父 updated
  • 销毁过程:父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

# 组件通信

# props/$emit

父组件通过 props 向子组件传递数据,子组件通过 $emit 和父组件通信。

父组件向子组件传值

  1. props可以为字符串数组或对象(键值分别为名称和数据类型)
  2. 基本使用:父组件中使用子组件的时候给子组件添加要传入的属性,子组件通过props来接收父组件传入的属性
  3. 单向数据流:props只能是父组件向子组件传值,反过来不行
  4. 子组件避免修改prop:js中对象和数组是通过引用传入的,所以在子组件中应该避免修改任何prop,防止应用的数据流向难以理解
  5. 传递静态or动态的prop:如果使用子组件时传入的属性是静态值,可以不适用 v-bind ;如果是动态的值,则使用 v-bind 绑定
  6. props 属性命名规则:在 props 中使用驼峰形式,DOM模板中需要使用短横线的形式。如果使用字符串模板,就没有这个限制了
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // 或任何其他构造函数
}

<!-- 子组件 -->
<template>
  <div id="son">
    <p>{{postTitle}}</p>
    <p>{{msg}}</p>
    <button @click="fn">按钮</button>
  </div>
</template>
<script>
  export default {
    name: "son",
    <!-- props使用驼峰 -->
    props: ["postTitle", "msg", "fn"];
  };
</script>


<!-- 父组件 -->
<template>
  <div id="father">
    <!-- 传递静态or动态属性,模板中使用横线分隔方式 -->
    <son post-title="hello!" :msg="msgData" :fn="myFunction"></son>
  </div>
</template>
<script>
  import son from "./son.vue";
  export default {
    name: father,
    data() {
      msgData: "父组件数据";
    },
    methods: {
      myFunction() {
        console.log("vue");
      }
    },
    components: {
      son
    }
  };
</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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

子组件向父组件传值

  1. 在父组件定义函数,在子组件通过$emit('函数名', 要传的参数)调用
<template>
  <div id="example">
    <advice-component v-on:advise="showAdvice"></advice-component>
  </div>
</template>
<script>
  import adviceComponent from './text/adviceComponent.vue'
  // 父组件实例
  const app = createApp({
    methods: {
      showAdvice(advice) {
        alert(advice)
      }
    }
  })

  // 注册子组件
  app.component('adviceComponent', {
    emits: ['advise'], // vue3提供的,与props类似,定义一个组件可以向其父组件触发的事件
    data() {
      return {
        adviceText: 'Some advice'
      }
    },
    // 
    template: `
      <div>
        <input type="text" v-model="adviceText">
        <button v-on:click="$emit('advise', adviceText)">
          Click me for sending advice
    		</button>
    	</div>
    `
  })

  app.mount('#example')
</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
30
31
32
33
34
35
36
37

props传入函数与$emit调用的区别

# eventBus事件总线($emit/$on)

eventBus适用于父子组件非父子组件等之间的通信,一个组件通过$emit('事件名', 传递的参数对象)发出事件,另一个组件通过$on('相同的事件名', function(传递过来的参数对象){...})。

Vue3.x已经移除了 $on,$off(移除事件总线监听器) 和 $once 方法。绝大多数情况下不建议使用eventBus在组件间通信,长期看它很难维护,可以通过其他方式来解决,具体看事件总线 (opens new window)

# 依赖注入(provide/inject)

该方法用于父子or祖孙组件之间通信层数很深的情况下可以用这种方式传值。provide/inject 是Vue提供的两个钩子,和data、methods是同级的。

  • provide 钩子用来让父组件发送数据或方法
  • inject 钩子用来给子组件接收数据或方法

注:默认情况下依赖注入所提供的属性是非响应式的,可以通过computed、ref或reactive来解决。

// 祖父组件
provide() {
 return {
    num: Vue.computed(() => this.num) 
   // 让num变成响应式的
   // 这里的num是父组件data中的数据,provide必须是一个函数,如果是一个对象就不能传递data中的数据
  };
}
// 子孙组件
inject: ['num'] // 在子组件中通过this.num来调用


// 访问父组件中的所有属性
provide() {
 return {
    app: this // 直接返回父组件实例
  };
}
data() {
 return {
    num: 1
  };
}
// 子孙组件
inject: ['app']
console.log(this.app.num)
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

# ref/$refs

实现父子组件之间的通信。

ref被用来给元素或子组件注册引用信息。引用信息会被注册在父组件的$refs对象上。

  • 如果在普通的DOM元素上使用,引用指向的就是这个DOM元素
  • 如果用在子组件上,引用就指向组件实例

注:因为 ref 本身是作为渲染函数的结果而创建的,在初始渲染时你不能访问它们——它们还不存在!$refs 也是非响应式的,因此不应该试图用它在模板中做数据绑定。

<!-- vm.$refs.p 会是 DOM 节点 -->
<p ref="p">hello</p>

<!-- vm.$refs.child 会是子组件实例 -->
<child-component ref="child"></child-component>

<!-- 当动态绑定时,我们可以将 ref 定义为回调函数,显式地传递元素或组件实例 -->
<child-component :ref="(el) => child = el"></child-component>
1
2
3
4
5
6
7
8

# $parent/$children

  • $parent:可以让组件访问上一级父组件的属性和方法(是一个对象)
  • $root:访问根组件的实例
  • $children:可以让组件访问所有直接子组件实例(是一个数组),但是并不能保证顺序且访问的数据也不是响应式的,在Vue3中已被移除,可以使用 $refs 替换

# $attrs/$listeners

Vue引入了$attrs / $listeners,实现组件之间的跨代通信。如A是B组件的父组件,B是C组件的父组件,可以使用该方法实现组件A给组件C传递数据。

  • inheritAttrs:默认为true,继承父组件除 props 之外的所有属性;如果设为false,只继承class属性
  • $attrs:
    • vue2.x:继承所有父组件属性(除prop传递的属性、class和style),一般用在子组件的子元素上
    • vue3.x:继承所有父组件属性,包括class和style,还有监听的事件!如果在子组件的子元素使用v-bind绑定了$attrs,会将class和style一起应用到该元素上,可能会造成视觉效果的破坏
  • $listeners:
    • vue2.x:该属性是一个对象,包含作用在这个组件上的所有监听器,使用v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)
    • vue3.x:该对象已被移除,事件监听器成为了$attrs的一部分,可以直接通过$attrs调用
<!-- A组件 -->
<template>
  <div id="app">
    <!-- 此处监听了两个事件,可以在B组件或者C组件中直接触发 -->
    <child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
  </div>
</template>
<script>
  import Child1 from './Child1.vue';
  export default {
    components: { Child1 },
    methods: {
      onTest1() {
        console.log('test1 running');
      },
      onTest2() {
        console.log('test2 running');
      }
    }
  };
</script>

<!-- B组件 -->
<template>
  <div class="child-1">
    <p>props: {{pChild1}}</p>
    <p>$attrs: {{$attrs}}</p>
    <child2 v-bind="$attrs" v-on="$listeners"></child2>
  </div>
</template>
<script>
  import Child2 from './Child2.vue';
  export default {
    props: ['pChild1'],
    components: { Child2 },
    inheritAttrs: false,
    mounted() {
      this.$emit('test1'); // 触发APP.vue中的test1方法
    }
  };
</script>

<!-- C组件 -->
<template>
  <div class="child-2">
    <p>props: {{pChild2}}</p>
    <p>$attrs: {{$attrs}}</p>
  </div>
</template>
<script>
  export default {
    props: ['pChild2'],
    inheritAttrs: false,
    mounted() {
      this.$emit('test2');// 触发APP.vue中的test2方法
    }
  };
</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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
  • C组件中能直接触发test的原因在于B组件调用C组件时 使用 v-on 绑定了$listeners 属性(子组件继承父组件的事件)
  • 在B组件中通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)

# 总结

父子组件间通信

  • 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
  • 通过 ref 属性给子组件设置一个名字。父组件通过 $refs 组件名来获得子组件,子组件通过 $parent 获得父组件,这样也可以实现通信。
  • 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。

兄弟组件间通信

  • (不推荐)使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
  • 通过 $parent/$refs 来获取到兄弟组件,也可以进行通信。

任意组件之间

  • (不推荐)使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。
  • 使用Vuex

# 路由

# 路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。

component (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。

// 非懒加载
import List from '@/components/list.vue'
const router = new VueRouter({
  routes: [
    { path: '/list', component: List }
  ]
})

// 1. 箭头函数 + import动态加载
const List = () => import('@/components/list.vue')
const router = new VueRouter({
  routes: [
    { path: '/list', component: List }
  ]
})
// 2. 箭头函数 + require动态加载
const router = new Router({
  routes: [
   {
     path: '/list',
     component: resolve => require(['@/components/list'], resolve)
   }
  ]
})
// 3. webpack的require.ensure(按模块划分懒加载)
// 后面的'list'为ChunkName,多个路由指定相同的chunkName,会合并打包成一个js文件。
const List = resolve => require.ensure([], () => resolve(require('@/components/list')), 'list');
const router = new Router({
  routes: [
  {
    path: '/list',
    component: List, 
    name: 'list'
  }
 ]
}))
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

# 路由的hash和history模式

# hash模式(默认的路由模式)

hash模式是开发中默认的模式,location.hash值就是url中#后面的一串东西。

特点

  1. hash虽然出现在URL里,但是不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面。
  2. 这种模式浏览器支持度很好,低版本的IE浏览器也支持。
  3. 兼容性好但不美观。

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

window.onhashchange = function(event){
	console.log(event.oldURL, event.newURL);
	let hash = location.hash.slice(1);
}
1
2
3
4
  • 在页面hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应代码。
  • hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。

# history模式

history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。

特点:虽然看着更美观了,但是history模式需要后台配置支持,如果后台没有正确配置(没有相应的路由或资源),访问时会返回404。

修改历史状态:利用了HTML5 History Interface 中新增的 pushState()replaceState()方法。

这两个方法应用于浏览器的历史记录栈,提供对历史记录进行修改的功能。但调用他们进行修改时,虽然修改了URL,但浏览器不会立即向后端发送请求,这样就可以实现“更新视图但不重新请求页面”的效果。

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

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

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})
1
2
3
4

# 两种模式对比

调用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变化

  1. 监听$route的变化

    // 监听,当路由发生变化的时候执行
    watch: {
      $route: {
        handler: function(val, oldVal){
          console.log(val);
        },
        // 深度观察监听
        deep: true
      }
    },
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  2. window.location.hash读取#值

    window.location.hash的值可读可写,读取来判断状态是否改变;写入可以在不重载网页的前提下,添加一条历史访问记录。

# $route和$router的区别

  • $route是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
  • $router 是“路由实例对象”,包括了路由的跳转方法,钩子函数等。

# 路由传参

需求:把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。

# params

  1. 配置路由:

    {
    	path: '/user/:userId',
    	name: 'user',
    	component: user
    }
    
    1
    2
    3
    4
    5
  2. 传入参数

    // 1
    <router-link :to="'/user/'+userId">用户</router-link>
    // 2
    // 这里的name为路由配置中的name,params为要传入的参数对象
    <router-link :to="{name: 'user', params: {userId: userId}}">用户</router-link>
    // 3
    this.$router.push('/user/'+userId)
    // 4
    this.$router.push({name: 'user', params: {userId: userId}})
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

  • 解析后的地址为/user/1这样的形式
  • 获取参数:this.$route.params.userId
  • params对象可以传入多个参数,但是每个参数都要在路由中进行配置。如果未配置,参数会在刷新后丢失
  • params传参,只能用路由的"name"访问,如果用"path",params不起作用

# query

query不用配置路由,直接传参就行。可以使用

  1. <router-link :to="...">按钮</router-link>
  2. this.$router.push(...)
// ...里面可以填入以下内容
// 1
{name: 'user', query: {id: id}}
// 2
{path: '/user', query: {id: id}}
// 3
'/user?id='+id
1
2
3
4
5
6
7

  • 解析后的地址为/user?id=1这样的形式
  • 获取参数:this.$route.query.userId
  • query使用"name"或"path"引入都可以

# 两者对比

  1. params是路由的一部分,如果配置了一定要传;query是拼接在url后面的参数,没传也没关系。
  2. params、query不设置也可以传参,但是params不设置时,刷新页面或返回参数会丢失;query不会。

# Vue-router跳转和location.href

  • location.href=url:简单方便,但是会刷新页面
  • history.pushState(url):不会刷新页面,静态跳转
  • router.push(url):引入router,使用该方法来跳转。使用了 diff 算法,实现了按需加载,减少了dom的消耗。使用router方法和使用 history.pushState() 没什么差别,因为vue-router就是用了 history.pushState(),尤其是history模式下。

# 导航守卫

  • 全局守卫/钩子:beforeEach、beforeResolve、afterEach
  • 路由独享守卫:beforeEnter
  • 组件内守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

具体可参考导航守卫 (opens new window)

导航行为被触发到导航完成的整个过程

  1. 导航被触发,此时导航未被确认。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件(如果有)。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫(2.5+),表示解析阶段完成。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 非重用组件,开始组件实例(及keep-alive)的生命周期:beforeCreate、created、beforeMount、(deactivated)、mounted、(activated)。
  12. 触发 DOM 更新。
  13. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
  14. 导航完成。

# 对前端路由的理解

在前端技术早期,一个url对应一个页面,如果想从页面A切换到页面B,必然伴随着页面刷新。用户只有在刷新页面的情况下,才能重新去请求数据。

ajax的出现,让用户可以在不刷新页面的情况下发起请求;也出现了“不刷新页面即可更新页面内容”这种需求。在这种背景下出现了SPA(单页面应用),但是刚出现SPA时,人们并没有考虑到“定位”这个问题:

  • SPA不会“记住”你的操作:在内容切换前后,页面的url都是一样的,SPA不知道当前页面“进展到哪一步”,一刷新页面,一切操作就会被清零。
  • 由于有且仅有一个 URL 给页面做映射,这对 SEO 也不够友好,搜索引擎无法收集全面的信息。

为了解决这个问题,前端路由出现了。它可以帮助我们在仅有一个页面的情况下,“记住”用户当前走到了哪一步,即为 SPA 中的各个视图匹配一个唯一标识。这意味着用户前进、后退触发的新内容,都会映射到不同的 URL 上去。此时即便他刷新页面,因为当前的 URL 可以标识出他所处的位置,因此内容也不会丢失。实现这个目的首先要解决两个问题:

  1. 当用户刷新页面时,浏览器会默认根据当前 URL 对资源进行重新定位(发送请求)。这个动作对 SPA 是不必要的,因为我们的 SPA 作为单页面,无论如何也只会有一个资源与之对应。此时若走正常的请求-刷新流程,反而会使用户的前进后退操作无法被记录。
  2. 单页面应用对服务端来说,就是一个URL、一套资源,那么如何做到用“不同的URL”来映射不同的视图内容呢?

从这两个问题来看,服务端已经完全救不了这个场景了。所以要靠咱们前端自力更生,不然怎么叫“前端路由”呢?作为前端,可以提供这样的解决思路:

  • 拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容。把刷新这个动作完全放到前端逻辑里消化掉。
  • 感知 URL 的变化。这里不是说要改造 URL、凭空制造出 N 个 URL 来。而是说 URL 还是那个 URL,只不过我们可以给它做一些微小的处理——这些处理并不会影响 URL 本身的性质,不会影响服务器对它的识别,只有我们前端感知的到。一旦我们感知到了,我们就根据这些变化、用 JS 去给它生成不同的内容。

# Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,其状态存储是响应式的。每一个Vuex应用的核心就是store(仓库)。

vuex

  1. 改变store中的状态唯一途径就是显式提交 mutation,该方法只能进行同步操作,且方法名全局唯一。
  2. Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 提交修改信息, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。
const store = new Vuex.Store({
  // 基本数据
  state: {
    count: 0
  },
	// 从基本数据派生出的数据
  getters: {
    countPlus: state => {
      return state.count + 1
    }
  },
	// 提交更改数据的方法,同步
  mutations: {
    increment: (state, payload) => {
      state.count += payload
    }
  },
  // Action可以包含任意异步操作,提交的是mutation,而不是直接变更状态。
  actions: {
    increment (context) {
      // 写异步代码
      ...
      context.commit('increment')
    }
  }
})
new Vue({
  el: '.app',
  store,
  computed: {
    count: function() {
      return this.$store.state.count
    }
  },
  methods: {
    increment: function() {
      this.$store.commit('increment', 10)
    }
  },
  template: `
        <div>
            {{ count }}
            <button @click='increment'>点我</button>
        </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

# 为什么Vuex的mutation中不能做异步操作?

  1. Vuex中所有的状态更新的唯一途径都是mutation,异步操作通过 Action 来提交 mutation实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。
  2. 每个mutation执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现 time-travel 了。如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。

# vuex和localStorage的区别

  • vuex存储在内存中;localStorage以文件方式存在本地,只能存储字符串类型的数据。读取内存比读取硬盘速度要快。
  • vuex用于组件之间传值,能做到数据的响应式;localStorage是将数据存储到浏览器中,一般是在跨页面传递数据时使用
  • 属性页面时vuex存储的值会丢失,localStorage不会

# Vue3.0

# 响应式

# 双向数据绑定

Vue是采用数据劫持结合发布者-订阅者模式的方式来实现双向数据绑定的。

当Vue实例创建时,Vue会遍历data中的属性,用Object.defineProperty(vue3.0使用proxy)将它们转化为getter/setter,在数据变动时发布消息给订阅者,触发相应的监听回调。每个组件实例都有相应的 watcher 实例。主要分为以下几个步骤:

  1. 需要Observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter/getter,给这个对象的某个值赋值,就会触发setter,这样就可以监听到数据变化。
  2. Compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
  3. Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
    1. 在自身实例化时往订阅器管理员(dep)里面添加自己
    2. 自身必须有一个update()方法,待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退

image-20220308201858759

# Object.defineProperty与Proxy

Object.defineProperty缺点

  1. 无法监听数组的变化,Vue内部通过重写数组的8种方法来检测数组变化。除了这8种,其他数组属性都检测不到,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染。
  2. 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历。

在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。

Proxy的优点

  1. Proxy可以直接劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能都远胜Object.defineProperty。
  2. Proxy可以无压力监听数组的变化。
  3. Proxy有13中拦截方式,是Object.defineProperty不具备的。
  4. 如果对象在vue实例创建后新增属性,一样可以监听到,不需要使用Vue.$setVue.$delete 触发响应式。
  5. 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示。

Proxy 唯一的缺点是兼容性的问题,因为它是 ES6 的语法。

可参考面试官: 实现双向绑定Proxy比defineproperty优劣如何? (opens new window)

# Vue模板编译

# 虚拟DOM

从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。其设计最初的目的就是跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟DOM,因为虚拟DOM本身是js对象。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,使用vue内部封装的diff算法来比较现在的虚拟DOM会与缓存的虚拟DOM,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。

# 为什么要用虚拟DOM?

  1. Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,依然可以给你提供过得去的性能。
  2. 跨平台:Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。

# Diff算法原理

Vnode最大的用途就是在数据变化前后生成真实DOM对应的虚拟DOM节点,然后就可以对比新旧两份VNode,找出差异所在,然后更新有差异的DOM节点,最终达到以最少操作真实DOM更新视图的目的。

在新老虚拟DOM对比时:

  • 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
  • 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
  • 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
  • 匹配时,找到相同的子节点,递归比较子节点

在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

# 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 对象来获取对应节点,比遍历方式更快

diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行对比,从而找到相应的旧节点。

# 为什么不建议用index作为key?

使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。