Vue 自检

975 阅读14分钟

这些知识最好的文档就是官方文档

生命周期

官网

所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法
例如 created: () => this.fetchTodos()。这是因为箭头函数绑定了父上下文,因此 this 与你期待的 Vue 实例不同,this.fetchTodos 的行为未定义。

  1. beforeCreate 在数据观测和初始化事件(event/watcher)还未开始之前

  2. created 完成数据观测,属性和方法的运算。但 $el 属性还不可用

  3. beforeMount 在挂载前调用,相关的 render 函数第一次调用。实例已完成编译模板,把 data 的数据和模板生成 html。但还未挂载 html 到页面上。
    该钩子在服务器端渲染期间不被调用。

  4. mounted 实例被挂载后调用,这时 el 被新创建的 vm.el替换了。可以在mounted内部使用vm.el 替换了。可以在 mounted 内部使用 vm.nextTick。
    该钩子在服务器端渲染期间不被调用。

  5. beforeUpdate 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
    该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务端进行。

  6. updated 虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
    该钩子在服务器端渲染期间不被调用。

  7. activated 被 keep-alive 缓存的组件激活时调用。
    该钩子在服务器端渲染期间不被调用。

  8. deactivated 被 keep-alive 缓存的组件停用时调用。
    该钩子在服务器端渲染期间不被调用。

  9. beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
    该钩子在服务器端渲染期间不被调用。

  10. destroyed 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
    该钩子在服务器端渲染期间不被调用。

总计: data 在 created 可获得。 el 在 mounted 获得, $nextTick beforeCreated 和 created 可在服务端渲染使用,其余都不可以。 每个生命周期都不能用箭头函数

父子组件生命周期

加载渲染过程
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

更新过程
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

销毁过程
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

keep-alive 缓存组件

  1. keep-alive 是 vue 内置的一个组件,可以是被包含的组件保留状态,避免重新渲染

  2. 一般结合路由和动态组件一起使用,用于缓存组件

  3. 有三个属性。

    • include 只有匹配的组件会被缓存。
    • exclude 任何名称匹配的都不会被缓存,ex 比 in 优先级高。
    • max 缓存组件的最大个数
  4. 和路由配合使用时,可设置 router 的元信息 meta 来决定要不要缓存

  5. 对应两个生命周期 activated , deactivated。当组件激活时,触发钩子函数 activated,当组件被移除时,触发 deactivated

keep-alive 缓存的是 Vnode

juejin.cn/post/684490…

v-for / v-if 唯一 key

key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。key 不同就会被重新渲染。 Vue 会最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。 有相同父元素的子元素(即同层元素)必须有独特的 key。重复的 key 会造成渲染错误。(因为 diff算法 同层比较原则)

  • key 会用在虚拟DOM 算法(diff 算法)中,用来辨别新旧节点。SameVnode 判断节点是否相同,第一步就是先判断 key

  • 不带 key 的时候会最大限度减少元素的变动,尽可能用相同元素。(就地复用)

  • 带 key 的时候,会基于相同的key来进行排列。(相同的复用)

  • 带 key 还能触发过渡效果,以及触发组件的生命周期

参考文章 vue中的key

不要用 index 做 key index 为什么不要做 key

在 v-for 里,如果是中间插入数据,会导致期之后的 index 全部变化,都要重新渲染,开销很大。而且可能会发生两个不一样的东西却被误以为是一样的了,因为用了一样的 index。推荐使用唯一 id 做 key

v-show v-if

v-show 的元素始终会被渲染并保留在 DOM 中,只是简单的基于CSS进行切换。而 v-if 是在销毁和重建中的。

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

v-if 中也会遇到 key 的问题
cn.vuejs.org/v2/guide/co…

Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。

在项目遇到这样的情形,很多表单,用v-if v-else 区分不同状态下的显隐。因为不同状态用的模板一样,但状态改变时,输入框前的 label 改变了,但是输入框内的文字依然保留之前的文字。如果加了表达检验的话,这些校验都错乱了。

这就是因为这些表单并没有重新渲染,我们就要加上唯一key,让vue认为他们都是完全独立的。这又可以引出 diff算法了。

data 为什么是函数

cn.vuejs.org/v2/guide/co…

一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。如果是对象的话,一个组件被多次引用,就会被多个地方更改数据,这是不合理的。

watch 和 computed 和 methods

重点, computed 当且仅当其依赖的数据改变时,会重新计算。

computed 和 methods 对比 同一函数也可定义为一个方法。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。而方法是每次都要重新计算。 计算属性是基于响应性依赖缓存的,方式没有。 调用时方式必须是函数,计算属性是属性,并可以定义成 get/set变成可读写属性

watch的使用场景是:当在data中的某个数据发生变化时, 我们需要做一些操作, 或者当需要在数据变化时执行异步或开销较大的操作时. 我们就可以使用watch来进行监听。

watch 和 computed的区别是:

相同点:他们两者都是观察页面数据变化的。

不同点:computed 只有当依赖的数据变化时才会计算, 当数据没有变化时, 它会读取缓存数据。 watch 没有缓存,每次都需要执行函数。当需要在数据变化时执行异步或开销较大的操作时,watch 最有用的。

vue 响应式原理,依赖收集

Dep 对象用于依赖收集,它实现了一个发布订阅模式,完成了数据 Data 和渲染视图 Watcher 的订阅

Vue 采用数据劫持和发布订阅模式

  1. Vue2 在初始化时,首先通过 Object.defineProperty 对 Data 每个属性绑定 get 和 set ,监听所有的 Data 中的数据变化。同时 Observer模块 会创建 Dep 用来搜集使用该 Data 的 Watcher。Dep 对象基于发布订阅模式实现的,用于依赖收集。Watcher 是在 beforeMount生命周期 创建 (并将 Dep.target 标识为当前 Watcher。) Watcher 创建时会执行 render 方法,最终将 Vue 代码渲染成真实的 DOM。

  2. 编译模板时,如果使用到了 Data 中的数据,就会触发 Data 的 get 方法,然后触发 Dep 的 depend 方法,最终触发 Dep 的 addSub 将当前的 Watcher 对象加入到依赖收集池 Dep 中。

  3. 数据更新时,会触发 Data 的 set 方法,继而触发 Dep 的 notify方法,notify方法会通知所有使用到该 Data 的 Watcher对象 调用其 update 方法去更新试图,所有使用到这个 Data 的 Watcher 会加入一个队列,并开启一个异步队列进行更新,最终执行 _render 方法完成页面更新。

响应式原理 juejin.cn/post/685766…

3.0 之前是使用 Object,defineProperty 劫持数据,3.0 使用 Proxy

object.defineProperty 缺点

  1. 不能监听数组;因为数组没有 getter 和 setter,因为数组的长度不确定,太长性能负担很大

  2. 只能监听属性,而不是整个对象,需要遍历对象。

  3. 只能监听属性的变化,不能监听属性的删减。也正因如此,vue文档要求,给数组或对象新增属性时,需要用 vm.$set 才能保证新增的属性也是响应式的。

proxy

优点

  1. 可以监听数组

  2. 可以监听整个对象,而不是属性

  3. 13种拦截方法,强大很多

  4. 返回新对象,而不是直接修改原对象。

Reflect 是内置对象,可以简单化内部操作 一些语言内置的方法 [[get]] [[set]] [[delete]] 不方便调用,用 Reflect 就可以,还是函数返回

缺点

不兼容 IE,且目前无法用 polyfill 磨平

发布订阅模式

class Observer {
  constructor() {
    this.caches = {}
  }

  on(event, fn) {
    this.caches[event] = this.caches[event] || [];
    this.caches[event].push(fn);
  }

  emit(event, data) {
    if (this.caches[event]) {
      this.caches[event].forEach(fn => fn(data))
    }
  }

  off(event, fn) {
    if (this.caches[event]) {
      const newCaches = fn ? this.caches[event].filter(e => e !== fn) : [];
      this.caches[event] = newCaches;
    }
  }
}

组件传值问题

1. 父子组件传值

1 最常用 props 2 子组件用 $parent 获取父组件 3 v-bind 配合 .sync

2. 子父组件传值

1 子组件 emit触发自定义事件,父组件von监听2父组件emit 触发自定义事件,父组件 v-on 监听 2 父组件 children 获取子组件 3 父组件 $refs 获取子组件

3. 祖先组件

1 attrsattrs 和 listeners 情形:A 是 B 父组件,B 是 C 父组件。A 给 C 传值

<C v-bind="$attrs" v-on="$listeners"></C> 利用B组件为中介,B 中调用 C 组件时,使用 v-on 绑定 listeners,vbind绑定listeners, v-bind 绑定 attrs; 这样 C 组件就可以获取 A组件调用B组件写的属性和事件了

listeners包含的是事件,listeners 包含的是事件, attrs 是属性 C 组件使用: attrs.name或者触发事件了this.attrs.name 或者触发事件了 this.emit('funA')

举例 segmentfault.com/a/119000002…

2 provide 和 inject 父组件通过 provide 提供属性,不论子组件多深,都可调用 inject 获取数据

// 父组件
provide: {
  for: 'test';
},
data() {
  return ...
}

// 子组件
inject: ['for'],
data() {
  return {
    msg: this.for
  }
}

4. event bus 非父子组件,其实所有组件都可以

使用一个空的 Vue 实例作为中央事件总线,结合 emitemit on 使用

Bus 定义方式

1 抽离为独立模块,组件按需引入 2 将Bus挂载到Vue根实例的原型上 3 将Bus注入到Vue根对象上

// bus.js
import Vue from 'vue';
const Bus = new Vue();
export default Bus;

// 2
import Vue from 'vue';
Vue.prototype.$bus = new Vue();

// 3
import Vue from 'vue';
const Bus = new Vue();
new Vue({
  el: '#app',
  data: {
    Bus
  }
})

// 组件使用时
this.$Bus.$emit() // 触发事件
this.$Bus.$on() // 监听事件
// 注册的Bus要在组件销毁时卸载,否则会多次挂载,造成一次触发多次响应的问题。
beforeDestory() {
  this.$Bus.$off('方法名')
}

5. Vuex 状态管理

Vuex 是 Vue 的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。简单来说就是:应用遇到多个组件共享状态时,使用 vuex。

属性:

  1. state:vuex的基本数据,用来存储变量
  2. getter:state 的读取方法,相当于state的计算属性
  3. mutation:提交更新数据的方法,必须是同步的。它会接受 state 作为第一个参数,提交载荷作为第二个参数。
  4. action:Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。
  5. modules:模块化vuex,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。

vuex 的使用流程:
页面通过 dispatch 派发异步事件到 action。action 通过 commit 把对应参数同步提交到 mutation,mutation 会修改 state 中对应的值。最后通过 getter 把对应值跑出去。

异步代码只有 actions 来处理,不能使用 mutation。换句话说,mutation 必须是同步的,而action 里想干嘛就干嘛。 vuex 用 devtools 追踪状态变化,就是追踪 Mutation,如果 mutation 有异步操作就不能正常追踪了。(异步状态未知)

同步的意义在于每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。

尤雨溪自己说的 www.zhihu.com/question/48…

之前公司实际项目中。一般都是将 接口函数 统一封装起来在一个 api 文件夹下,通过 api字段 暴露出来。然后将会被多个组件公用的请求,写在 action 中。再通过改变 mutation 来改变 state 。组件 dispatch 派发即可。

简单实现:

原文:https://zhuanlan.zhihu.com/p/166087818

class Store {
  constructor(options) {
    this.vm = new Vue({
      data:{
        state: options.state
      }
    })

    let getters = options.getter || {}
    this.getters = {}
    Object.keys(getters).forEach(getterName => {
      Object.defineProperty(this.getters,getterName,{
        get:()=> {
          return getters[getterName](this.state)
        }
      })
    })

    let mutations = options.mutations || {}
    this.mutations = {}
    Object.keys(mutations).forEach(mutationName => {
      this.mutations[mutationName] = (arg) =>  {
        mutations[mutationName](this.state,arg)
      }
    })

    let actions = options.actions
    this.actions = {}
    Object.keys(actions).forEach(actionName => {
      this.actions[actionName] = (arg) => {
        actions[actionName](this,arg)
      }
    })
  }

  dispatch(method,arg){
    this.actions[method](arg)
  }
  // 修改代码
  commit = (method,arg) => {

    this.mutations[method](arg)
  }
  get state(){
    return this.vm.state
  }
}

vuex 与 redux 对比

juejin.cn/post/684490…

虚拟 DOM 与 diff 算法

虚拟 DOM

Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点。 就是一个普通的 JavaScript 对象,包含了 tag、props、children 三个属性。

<div id="app">
  <p class="text">hello world!!!</p>
</div>
// 虚拟 dom
{ tag: 'div', props: { id: 'app' },
	chidren: [{ tag: 'p', props: { className: 'text' },
    		chidren: ['hello world!!!']}]}

如何生成 虚拟 DOM

如果用纯手写的方式把整个页面的 虚拟 DOM 打出来,那是反人类的设计。

主流的虚拟 DOM 库(snabbdom、virtual-dom),通常都有一个 h 函数,也就是 React 中的 React.createElement,以及 Vue 中的 render 方法中的 createElement。(另外 React 是通过 babel 将 jsx 转换为 h 函数渲染的形式,而 Vue 是使用 vue-loader 将模版转为 h 函数渲染的形式。)

通过这些渲染函数可以把页面模板编译成 虚拟 DOM。

Vue template

vue 的模板语法,是一种形象描述视图的标记语法。被 Vue-template-compiler 解析成 reder 函数,通过 VNode 和 diff 算法统一替换为 DOM 生成页面。

.vue 文件常用到的 template 标签是声明 虚拟DOM 的标签模板,是模板占位符,包裹元素。但是不会被渲染到页面上。

Vue template 到 render 的过程

简单以 vue 为例,介绍下这个过程

过程主要如下 template -> ast -> render 函数

  1. 调用 parse 方法通过正则表达式将 template 转化为 ast抽象语法树

     ast树节点有三种类型,type 1 普通元素;type 2 表达式;type 3 纯文本
    
  2. 对静态节点进行优化

     分析那些是静态节点,做标记,静态节点的 DOM 永远不会改变,后续更新渲染可直接跳过。
     这对更新模板有极大的优化作用
     
    
  3. 生成 render 函数

     将 ast 抽象语法树 编译成 render 字符串,最后通过 new Function(redner) 生成 render 函数
    

有了 render 渲染函数,就可以生成 虚拟DOM。就可以被各平台渲染出页面。 图片出处: 详解Vue中的虚拟DOM

虚拟DOM 用处

  1. 相当于在 js 和真实 dom 中间加了一层缓存,利用 dom diff 算法避免没有必要的 dom 操作,从而提高性能(JS 线程和 操作DOM 是两个线程,频繁的交互影响性能)。当然算法有时并不是最优解,因为它需要兼容很多实际中可能发生的情况,比如后续会讲到两个节点的 dom 树移动。

  2. 虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM

参考大佬回答:Vue采用虚拟DOM的目的是什么?

diff 算法

目的是什么?
diff 算法是为了减少 DOM 操作的性能开销,因为浏览器生成新 DOM 会占用很多资源。所以我们要尽可能的复用 DOM 元素。diff 算法就是帮助我们判断出是否有节点需要移动,应该如何移动,找出需要添加或删除的节点

diff 算法是一种通过同层的树节点进行比较,而非对树进行逐层搜索遍历的高效算法,降低时间复杂度为O(n)。
diff 在很多场景下都有用,比如 vue 虚拟DOM 渲染成真实DOM 的新旧 VNode 比较更新。

diff算法的特点

  1. 只同级比较,不会跨层级比较

  2. diff 比较循环两边,并往中间收拢。

     因为对节点的操作一般都发生在头和尾。
    

我们先看下 Vue 中 diff算法的使用吧。

Vue 中的 diff算法

从源码分析

  1. 必要性,lifecycle.js - mountComponent()

    Vue 中每生成一个组件都有一个对应的 Watcher,两者为一一对应的,为了降低 Watcher 的粒度,每个组件只有一个 Watcher。但是问题来了,组件可能存在多个 data 变化,有多个 key 变化。这就需要 diff算法 精确的找到发生改变的节点。

  2. 执行方式,patch.js - patchVode()

    diff算法 执行的地方。执行策略:深度优先,同层比较

在比较同层节点时,有几种情况:

1. 旧节点有孩子节点,新节点无孩子节点。则直接删除旧节点的孩子节点
2. 旧节点无孩子节点,新节点有孩子节点。则直接添加新节点的孩子节点
3. 都有文本节点,则选择新文本节点
4. 双方都有孩子节点,则执行 updateChildren 函数比较孩子节点。这是最重要的,算法核心优化在此

3. 高效性,patch.js - updateChildren() 这是 diff算法 优化的核心。

1. 创建四个指针,分别指向新旧VNode 的头和尾。依次比较。

2. 以上四步都不是相同节点,则在旧VNode 中遍历查找是否有 新VNode 的头指针指向的节点。有的话则把节点移动至旧VNode 头指针前。如果没有则在旧VNode 头指针前插入该节点。

3. 第一个节点操作完成后,指针后移,重复操作。**结果为:新增,删除,移动**

总结:

  1. diff算法 是虚拟DOM 技术的必然产物,通过新旧虚拟DOM 的对比,将变化的地方更新在真实DOM 上。也需要 diff算法高效的对比过程,降低时间复杂度为O(n)

  2. vue2 中为了降低 Watcher 的粒度,每个组件只对应一个 Watcher,只有引入 diff算法 才能精确的找到发生变化的地方。

  3. vue 中 diff 执行的时刻是组件实例执行其更新函数时,它会对比上一次渲染结果 oldVnode 和新的渲染结果 newVnode ,此过程称为 patch。

  4. diff 策略是:深度优先,同层比较。两个节点之间会根据是否拥有子节点或者文本节点做不同操作(上面有介绍)。比较两组子节点是重点(上面有介绍)。patch 过程更高效了。

参考文章:
Vue原理之虚拟DOM和render函数
Vue 虚拟dom diff原理详解