# 1.Vue 生命周期

  • 生命周期的定义:是指 Vue 实例对象从创建之初到销毁的过程,

  • 生命周期的作用:Vue 的所有功能都是围绕生命周期进行的,在生命周期的不同阶段调用不同的钩子函数来实现组建的数据管理和DOM渲染。

总共分为 8 个阶段创建前/后,载入前/后,更新前/后,销毁前/后

# 1、在 beforeCreate 和 created 钩子函数之间的生命周期

 在这个生命周期之间,进行初始化事件,进行数据的观测,可以看到在 created 的时候数据已经和 data 属性进行绑定(放在 data 中的属性当值发生改变的同时,视图也会改变)

  注意:此时还是没有 el 选项

 创建前/后: 在 beforeCreate 阶段,vue 实例的挂载元素 el 和数据对象 data 都为 undefined,还未初始化。在 created 阶段,vue 实例的数据对象 data 有了,el 还没有

# 2、created 钩子函数和 beforeMount 间的生命周期

 首先会判断对象是否有 el 选项 如果有的话就继续向下编译,如果没有 el 选项,则停止编译,也就意味着停止了生命周期,直到在该 vue 实例上调用 vm.$mount(el)

 template 参数选项的有无对生命周期的影响

 (1)如果 vue 实例对象中有 template 参数选项,则将其作为模板编译成 render 函数。
 (2)如果没有 template 选项,则将外部 HTML 作为模板编译。
 (3)可以看到 template 中的模板优先级要高于 outer HTML 的优先级。

 在 vue 对象中还有一个 render 函数,它是以 createElement 作为参数,然后做渲染操作,而且我们可以直接嵌入 JSX.

 所以综合排名优先级:
  render 函数选项 > template 选项 > outer HTML.

载入前/后:在 beforeMount 阶段,vue 实例的$el 和 data 都初始化了,但还是挂载之前为虚拟的 dom 节点,data.message 还未替换。在 mounted 阶段,vue 实例挂载完成,data.message 成功渲染。

# 3、beforeMount 和 mounted 钩子函数间的生命周期

  可以看到此时是给 vue 实例对象添加$el 成员,并且替换掉挂在的 DOM 元素。因为在之前 console 中打印的结果可以看到 beforeMount 之前 el 上还是 undefined。

# 4、mounted

  在 mounted 之前,此时还有挂在到页面上,还是 JavaScript 中的虚拟 DOM 形式存在的。在 mounted 之后可以看到 h1 中的内容发生了变化。

# 5、beforeUpdate 钩子函数和 updated 钩子函数间的生命周期

 当 vue 发现 data 中的数据发生了改变,会触发对应组件的重新渲染,先后调用 beforeUpdate 和 updated 钩子函数

 我们在 console 中输入:vm.message = '触发组件更新'
 发现触发了组件的更新:
 更新前/后:当 data 变化时,会触发 beforeUpdate 和 updated 方法

# 6、beforeDestroy 和 destroyed 钩子函数间的生命周期

 beforeDestroy 钩子函数在实例销毁之前调用。在这一步,实例仍然完全可用。
 destroyed 钩子函数在 Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

 销毁前/后:在执行 destroy 方法后,对 data 的改变不会再触发周期函数,说明此时 vue 实例已经解除了事件监听以及和 dom 的绑定,但是 dom 结构依然存在

# 2.Vue 数据双向绑定

# 数据双向绑定

在 vue 上使用 v-model 指令执行数据的双向绑定

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

要实现 mvvm 的双向绑定,就必须要实现以下几点:

1、实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个 Watcher,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm 入口函数,整合以上三者

# 实现数据绑定的做法有大致如下几种

  • 发布者-订阅者模式(backbone.js)
  • 脏值检查(angular.js)
  • 数据劫持(vue.js)

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

# 整理了一下,要实现 mvvm 的双向绑定,就必须要实现以下几点

1、实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个 Watcher,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm 入口函数,整合以上三者

上述流程如图所示:


# 实现 Compile

compile 主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示:


因为遍历解析的过程有多次操作 dom 节点,为提高性能和效率,会先将跟节点 el 转换成文档碎片 fragment 进行解析编译操作,解析完成,再将 fragment 添加回原来的真实 dom 节点中

# 实现 Watcher

Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个 update()方法
3、待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功成身退。

# 实现 MVVM

MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

# 3.v-model是如何实现双向绑定的

  • vue 2.0 v-model是⽤来在表单控件或者组件上创建双向绑定的,他的本质是 v-bindv-on 的语法糖,在 ⼀个组件上使⽤ v-model ,默认会为组件绑定名为 valueprop 和名为 input 的事件。

  • Vue3.0 在 3.x 中,⾃定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件

# 4.Vue 非父子通信方式

# 1、EventBus (实例化一个空Vue作为事件中心管理)

// bus.js
import Vue from 'vue'
export default new Vue()
1
2
3
import Bus from 'bus'

// 监听事件
Bus.$on('updateData', value => { })

// 触发事件
Bus.$emit('updateData', )

// 注销事件
Bus.$off('updateData')
1
2
3
4
5
6
7
8
9
10

# 2、$attrs/$listeners

Vue2.4 提供适用于多层嵌套组件通信的方法

$attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 interitAttrs 选项一起使用。

$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

# 3、Observable (Vue2.6提供的新API)

// 创建 store
import Vue from 'vue'

// 通过Vue.observable创建一个可响应的对象
export const store = Vue.observable({
  userInfo: {},
  roleIds: []
})

// 定义 mutations, 修改属性
export const mutations = {
  setUserInfo(userInfo) {
    store.userInfo = userInfo
  },
  setRoleIds(roleIds) {
    store.roleIds = roleIds
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在组件中引用
<template>
  <div>
    {{ userInfo.name }}
  </div>
</template>

<script>
import { store, mutations } from '../store'
export default {
  computed: {
    userInfo() {
      return store.userInfo
    }
  },
  created() {
    mutations.setUserInfo({
      name: '子君'
    })
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 4、Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
Vue 的核心是组件,对于组件的通信一般都是父子组件之间的通信。
那么对于非父子关系的组件通信,可以采用事件传参或者多层嵌套,但是这样就是使得代码臃肿并且难以维护。
Vuex 就是解决这个问题,把组件需要共享的数据抽取出来,交给 store 来处理。

简单来说,这个 store 就是用来存储数据
存储数据方式有很多种,比如变量、数组、cookie、localstorage、session 等等
但是由于 store 是内存机制,而不是缓存机制,当前页面一旦刷新或者通过路由跳转亦或是关闭,都会导致 store 初始化
因此在之前就暂时把 store 看成一个多功能的全局变量

而 Vuex 和单纯的全局对象又有点不同
 1、Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
 2、你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

# State:

1、State 就是数据源存放地
2、State 里面存放的数据是响应式的,Vue 组件从 store 中读取数据,若是 store 中的数据发生改变,依赖这个数据的组件也会发生更新

# Getter:

1、getters 可以对 State 进行计算操作,它就是 Store 的计算属性
2、虽然在组件内也可以做计算属性,但是 getters 可以在多组件之间复用
3、如果一个状态只在一个组件内使用,是可以不用 getters

# Mutation:

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。

# Action 类似于 mutation,不同在于:

Action 提交的是 mutation,而不是直接变更状态。 Action 可以包含任意异步操作。

# Moudle

每个 state 对应一个 moudle

# 5.Vue-router

# vue-router介绍

Vue是单页应用(SPA),只有一个html文件,而vue-router就是vue进行页面替换和更新的组件。
由route, routes, router组成。

# ①、路由之间跳转:

  • 声明式(标签跳转)
  • 编程式(js 跳转)

# ②、原理:

 更新视图但不重新请求页面,是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有 2 种方式,Hash 模式和 History 模式:

(1)利用 URL 中的 hash("#")
(2)利用 History interface 在 HTML5 中新增的方法

# ③、vue-router 有三种导航钩子

第一种:全局导航钩子:router.beforeEach(to,from,next),作用:跳转前进行判断拦截(权限认证)
第二种:组件内的钩子
第三种:单独路由独享组件

# ④、$route 和$router 的区别

  • $router 为 VueRouter 的实例,相当于一个全局的路由对象,里面含有很多属性和子对象,例如 history 对象
  • $route 相当于当前正在跳转的路由对象,属于局部对象,以从里面获取 name,path,params,query 等

# ⑤、active-class

router-link 对应的路由匹配成功,将自动设置 class 属性值 .router-link-active

# vue-router 提供了三种运行模式:

● hash: 使用 URL hash 值来作路由。默认模式。
● history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。
● abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端

Hash模式: hash模式的工作原理是 hashchange 事件,可以在window监听hash的变化。我们在url后面随便添加一个#xx触发这个事件。

window.onhashchange = function(event){
    console.log(event)
}
1
2
3

hash即浏览器url中#后面的内容,包含#。hash是URL中的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会加载相应位置的内容,不会重新加载页面。

  • 即#是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中,不包含#。

  • 每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。

  • 所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。

History模式

HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushStatehistory.replaceState API 来完成 URL 跳转而无须重新加载页面;

由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

有时,history模式下也会出问题:

  • hash模式下:xxx.com/#/id=5 请求地址为 xxx.com,没有问题。
  • history模式下:xxx.com/id=5 请求地址为 xxx.com/id=5,如果后端没有对应的路由处理,就会返回404错误;

为了应对这种情况,需要后台配置支持:
在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

# 动态路由

Vue 动态路由,就是基于 router.addRoutes | router.addRoute 这个 api 的一个功能实现

# Vue路由懒加载3种方式

  • Vue异步组件
component:resolve => require(['@/components/Foo'],resolve)
1
  • ES6的import()
const Foo = () => import('../components/Foo')
1
  • webpack的require.ensure()
component:r=>
    require.ensure([],()=>r(require('@/components/home')),'funcDemo')
1
2

# vue router传参的三种方式

  • 1、params传参
this.$router.push({ name: 'news', params: { type: 1 }})
1

此时浏览器url上是看不到任何参数的,像这样http://localhost:9530/news

对于像path: '/detail/:id'这样的携带参数的动态路由,传参时也应当使用params,动态路由传参是可以再url上看到的,像这样http://localhost:9530/detail/121

  • 2、query传参
this.$router.push({ path: 'news', query: { type: 1  }})
1

此时浏览器url上是可以看得到参数的,像这样http://localhost:9530/news/?type=1 通过query传的参数在页面刷新时不会丢失。

  • 3、props传参
{
  name: 'news',
  path: '/news',
  component: () => import('@/views/news'),
  meta: { title: '新闻详情' },
  props: { type: 1 }
}
1
2
3
4
5
6
7

通过props的方式传参时,在组件内的取值方式也是通过props,像父子组件传值那样

# 6.diff算法和虚拟dom

参考 (opens new window)

# 6-1、虚拟 DOM

要理解 Diff 算法,就得先理解好虚拟 DOM。
虚拟 DOM 说白了其实就只是一个 JavaScript 对象,它抽象地描述了一个真实的 DOM 结构。
可以从 Chrome 的 DevTools 中看到,一个 DOM 结构无非是由很多个 HTML 标签根据父子、兄弟等关系组织起来的,而每个 HTML 标签又包含了各种属性,比如 style、class、src 等。
所以只要知道了真实 DOM 的结构,我们就可以把它抽象成一个对象的形式来描述,这个对象就是虚拟 DOM。
我们可以通过递归的方式将一个 DOM 结构解析成一个虚拟 DOM,也可以通过 document.createElement 把一个虚拟 DOM 还原成一个真实 DOM。

// 真实 DOM 和 虚拟 DOM 是可以相互转化
<div id="container" class="p-container">
  <h1>Real DOM</h1>
  
</div>

const VirtualDOM = {
  tag: 'div',
  data: {
    id: 'container',
    class: 'p-container'
  },
  children: [
    {
      tag: 'h1',
      data: {},
      children: ['Real DOM'],
    },
    
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 6-2、为什么要有虚拟DOM

1、跨平台渲染。借助虚拟 DOM 后 FrontEnd 可以进行移动端、小程序等开发。因为虚拟 DOM 本身只是一个 JavaScript 对象,所以可以先由 FE 们写 UI 并抽象成一个虚拟 DOM,再由安卓、IOS、小程序等原生实现根据虚拟 DOM 去渲染页面(React Native、Weex)。
2、函数式的 UI 编程。将 UI 抽象成对象的形式,相当于可以以编写 JavaScript 的形式来写 UI。
3、网上 Blog 常常会说到虚拟 DOM 会有更好的性说能,因为虚拟 DOM 只会在 Diff 后修改一次真实 DOM,所以不会有大量的重排重绘消耗。并且只更新有变动的部分节点,而非更新整个视图。但我对这句话是存疑的,通过下文的 Diff 算法源码可以发现,Vue2 它的 Diff 是每次比对到匹配到的节点后就去修改真实 DOM 的,并不是等所有 Diff 完后再修改一次真实 DOM 而已。

# 6-3、什么是 Diff 算法

1、将真实 DOM 抽象成虚拟 DOM。
2、数据改变时,将新的真实 DOM 再抽象成另一个新的虚拟 DOM。
3、采用深度优先遍历新旧两个虚拟 DOM,如果两个虚拟 DOM 节点值得比较,就递归比较它们的子节点,否则直接创建新的 DOM 节点。

# 6-4、Vue2 中 Diff 原理

当对新旧两个虚拟 DOM 做 Diff 时,Vue 采用的思想是同级比较深度递归双端遍历

# 5-4-1、同级比较:

同级比较指的是只比对两个相同层级的 VNode,如果两者不一样了,就不再去 Diff 它们的子节点,更不会去跨层级比较,而是直接更新它。
这是因为在我们平时的操作,很少出现将一个 DOM 节点进行跨层级移动,比如将原来的父节点移动到它子节点的位置上。
所以 Diff 算法就没有为这个极少数的情况专门去跨层级 Diff,毕竟为此得不偿失,这也是 Diff 算法时间复杂度能从 O(n^3) 优化到 O(n) 的原因之一。

# 6-4-2、深度递归

深度递归指的是比较两个虚拟 DOM 时采用深度优先的先序遍历策略,先比较完一个子节点后,就去比较这个子节点的子孙节点,都递归完后再来遍历它的兄弟节点。
如下图的一个 DOM 结构,节点的编号就是它们的遍历顺序。

那么为什么要使用深度优先遍历,广度优先遍历不行么?
我的理解是,深度优先遍历使用到的是栈结构,进行深度递归的时候,栈中保存的是当前节点的父元素和祖先元素,栈中存储的最大节点个数就是 DOM 树最大的层级数。
而广度优先遍历使用的是队列结构,进行广度递归的时候,队列中保存的是下一层的节点,队列中存储的最大节点个数就是 DOM 树最大的层级节点数。
而在通常情况下,一个 DOM 树的层级数是会少于它的层级节点数的(比如一个列表信息组件),所以使用深度优先遍历占用的空间和消耗会更小些。

# 6-4-3、双端比较

双端比较指的是在 Diff 新旧子节点时,使用了四个指针分为四种比较方法

oldStartIdx 表示旧 VNode 从左边开始 Diff 的节点,初始值为第一个子节点
oldEndIdx 表示旧 VNode 从右边开始 Diff 的节点,初始值为最后一个子节点
newStartIdx 表示新 VNode 从左边开始 Diff 的节点,初始值为第一个子节点
newEndIdx 表示新 VNode 从右边开始 Diff 的节点,初始值为最后一个子节点

# 6-5、Vue的key(为什么要加key、key唯一、不建议用数组的index作为key)

# 6-5-1、为什么要加上key

Vue官网的解释:
key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。
如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。
而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

# 6-5-2、为什么要求key唯一

Vue的源码中有一个sameVnode的方法,这个方法放回布尔值,用来判断两个节点是否相同。

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 6-5-3、为什么不建议用数组的index作为key

<ul>                      
    <li key="0">a</li>
    <li key="1">b</li>  
    <li key="2">c</li>                
</ul>   
1
2
3
4
5

举个例子,上面是初始数据,然后我在数据前插入一个新数据,变成如下的列表

<ul>            
    <li key="0">xxx</li>          
    <li key="0">a</li>
    <li key="1">b</li>  
    <li key="3">c</li>                
</ul>   
1
2
3
4
5
6

按理说,最理想的结果是:只插入一个li标签新节点,其他都不动,确保操作DOM效率最高。

通过Vue的源码可以看到只有在非production的情况下才会显示key重复的warn提示

if (process.env.NODE_ENV !== 'production') {
  checkDuplicateKeys(ch);
}
1
2
3

# 7.Vue的⽗组件和⼦组件⽣命周期钩⼦执⾏顺序

  • 渲染过程:

⽗组件挂载完成⼀定是等⼦组件都挂载完成后,才算是⽗组件挂载完,所以⽗组件的mounted在⼦组件mouted之后。
⽗beforeCreate -> ⽗created -> ⽗beforeMount -> ⼦beforeCreate -> ⼦created -> ⼦beforeMount -> ⼦mounted -> ⽗mounted

  • ⼦组件更新过程:
  1. 影响到⽗组件: ⽗beforeUpdate -> ⼦beforeUpdate->⼦updated -> ⽗updted
  2. 不影响⽗组件: ⼦beforeUpdate -> ⼦updated
  • ⽗组件更新过程:
  1. 影响到⼦组件: ⽗beforeUpdate -> ⼦beforeUpdate->⼦updated -> ⽗updted
  2. 不影响⼦组件: ⽗beforeUpdate -> ⽗updated
  • 销毁过程: ⽗beforeDestroy -> ⼦beforeDestroy -> ⼦destroyed -> ⽗destroyed

# 8.Vue 中的 computed 是如何实现

  1. 当组件初始化的时候, computed 和 data 会分别建⽴各⾃的响应系统, Observer 遍历 data 中每个属性设置 get/set 数据拦截

  2. 初始化 computed 会调⽤ initComputed 函数
    1、注册⼀个 watcher 实例,并在内实例化⼀个 Dep 消息订阅器⽤作后续收集依赖(⽐如渲染 函数的 watcher 或者其他观察该计算属性变化的 watcher )
    2、调⽤计算属性时会触发其 Object.defineProperty 的 get 访问器函数
    3、调⽤ watcher.depend() ⽅法向⾃身的消息订阅器 dep 的 subs 中添加其他属性的 watcher
    4、调⽤ watcher 的 evaluate ⽅法(进⽽调⽤ watcher 的 get ⽅法)让⾃身成为其他 watcher 的消息订阅器的订阅者,⾸先将 watcher 赋给 Dep.target ,然后执⾏ getter 求值函数,当访问求值函数⾥⾯的属性(⽐如来⾃ data 、 props 或其他 computed )时, 会同样触发它们的 get 访问器函数从⽽将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并 返回求值函数结果。

  3. 当某个属性发⽣变化,触发 set 拦截函数,然后调⽤⾃身消息订阅器 dep 的 notify ⽅法,遍 历当前 dep 中保存着所有订阅者 wathcer 的 subs 数组,并逐个调⽤ watcher 的 update ⽅ 法,完成响应更新。

lastUpdate: 11/3/2022, 9:52:58 AM