Vue

作者: shaokang 时间: February 23, 2022字数:8201

vue

Vue vs React

对 React 和 Vue 的理解,它们的异同 相似之处:
● 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库;
● 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板;
● 都使用了 Virtual DOM(虚拟 DOM)提高重绘性能;
● 都有 props 的概念,允许组件间的数据传递;
● 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性。

不同之处 :
1)数据流
Vue 默认支持数据双向绑定,而 React 一直提倡单向数据流

2)虚拟 DOM
Vue2.x 开始引入”Virtual DOM”,消除了和 React 在这方面的差异,但是在具体的细节还是有各自的特点。
● Vue 宣称可以更快地计算出 Virtual DOM 的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
● 对于 React 而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate 这个生命周期方法来进行控制,但 Vue 将此视为默认的优化。

3)组件化
React 与 Vue 最大的不同是模板的编写。
● Vue 鼓励写近似常规 HTML 的模板。写起来很接近标准 HTML 元素,只是多了一些属性。
● React 推荐你所有的模板通用 JavaScript 的语法扩展——JSX 书写。

具体来讲:React 中 render 函数是支持闭包特性的,所以 import 的组件在 render 中可以直接调用。但是在 Vue 中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。

4)监听数据变化的实现原理不同
● Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能
● React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的 vDOM 的重新渲染。这是因为 Vue 使用的是可变数据,而 React 更强调数据的不可变。

5)高阶组件
react 可以通过高阶组件(HOC)来扩展,而 Vue 需要通过 mixins 来扩展。

高阶组件就是高阶函数,而 React 的组件本身就是纯粹的函数,所以高阶函数对 React 来说易如反掌。相反 Vue.js 使用 HTML 模板创建视图组件,这时模板无法有效的编译,因此 Vue 不能采用 HOC 来实现。

6)构建工具
两者都有自己的构建工具:
● React ==> Create React APP
● Vue ==> vue-cli

7)跨平台
● React ==> React Native
● Vue ==> Weex

Vue2 vs Vue3

Vue3 新特性,vue3 相比 vue2:

  • 速度更快:
    • 重写了虚拟 DOM 的实现、diff 算法优化
    • 编译模板的优化
    • 更高效的组件初始化
    • update 性能提高 1.3-2 倍
    • SSR 速度提高了 2-3 倍
  • 体积减少:
    • tree-shaking
  • 更易维护
    • composition Api
    • 更好的 Typescript 支持
    • 编译器重写
  • 更接近原生
    • 自定义渲染 Api
  • 更易使用
    • 响应式 Api

新增特性:

  • fragments 【支持多个根节点】
  • Teleport 【通过 Teleport,我们可以在组件的逻辑位置写模板代码,然后在 Vue 应用范围之外渲染它】
  • composition Api
  • createRenderer 【能够构建自定义渲染器,将 vue 的开发模型扩展到其他平台】

细节上:

  • 响应式方面 vue3 的响应式是基于 Proxy 来实现的,利用代理来拦截对象的基本操作,配合 Refelect.*方法来完成响应式的操作。
  • 书写方面 提供了 setup 的方式,配合组合式 API,可以建立组合逻辑、创建响应式数据、创建通用函数、注册生命周期钩子等。
  • diff 算法方面: 在 vue2 中使用的是双端 diff 算法:是一种同时比较新旧两组节点的两个端点的算法(比头、比尾、头尾比、尾头比)。一般情况下,先找出变更后的头部,再对剩下的进行双端 diff。 在 vue3 中使用的是快速 diff 算法:它借鉴了文本 diff 算法的预处理思路,先处理新旧两组节点中相同的前置节点和后置节点(头和头比、尾和尾比较,如果不同就退出比较)。当前置节点和后置节点全部处理完毕后,如果无法通过简单的挂载新节点或者卸载已经不存在的节点来更新,则需要根据节点间的索引关系,构造出一个最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。

  • 编译上的优化: vue3 新增了 PatchFlags 来标记节点类型(动态节点收集与补丁标志),会在一个 Block 维度下的 vnode 下收集到对应的 dynamicChildren(动态节点),在执行更新时,忽略 vnode 的 children,去直接找到动态节点数组进行更新,这是一种高效率的靶向更新。 vue3 提供了静态提升方式来优化重复渲染静态节点的问题,结合静态提升,还对静态节点进行预字符串化,减少了虚拟节点的性能开销,降低了内存占用。 vue3 会将内联事件进行缓存,每次渲染函数重新执行时会优先取缓存里的事件

双向数据绑定

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 变更的双向绑定效果。

Computed 和 Watch 的区别

对于 Computed:
● 它支持缓存,只有依赖的数据发生了变化,才会重新计算
● 不支持异步,当 Computed 中有异步操作时,无法监听数据的变化
● computed 的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 声明过,或者父组件传递过来的 props 中的数据进行计算的。
● 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed
● 如果 computed 属性的属性值是函数,那么默认使用 get 方法,函数的返回值就是属性的属性值;在 computed 中,属性有一个 get 方法和一个 set 方法,当数据发生变化时,会调用 set 方法。

对于 Watch:
● 它不支持缓存,数据变化时,它就会触发相应的操作
● 支持异步监听
● 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
● 当一个属性发生变化时,就需要执行相应的操作
● 监听数据必须是 data 中声明的或者父组件传递过来的 props 中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
○ immediate:组件加载立即触发回调函数
○ deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep 无法监听到数组和对象内部的变化。
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用 watch。

slot

slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。
● 默认插槽:又名匿名插槽,当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
● 具名插槽:带有具体名字的插槽,也就是带有 name 属性的 slot,一个组件可以出现多个具名插槽。
● 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。

实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在 vm.$slot中,默认插槽为vm.$slot.default,具名插槽为 vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot 中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

Vue template 到 render 的过程

vue 的模版编译过程主要如下:template -> ast -> render 函数

(1)调用 parse 方法将 template 转化为 ast(抽象语法树)
● parse 的目标:把 tamplate 转换为 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。
● 解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造 AST 树的目的。
AST 元素节点总共三种类型:type 为 1 表示普通元素、2 为表达式、3 为纯文本

(2)对静态节点做优化
这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化

深度遍历 AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的 DOM 永远不会改变,这对运行时模板更新起到了极大的优化作用

(3)生成代码
const code = generate(ast, options)
generate 将 ast 抽象语法树编译成 render 字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function( render) 生成 render 函数。

Diff 算法

patch 过程

  1. 首先判断新节点 vnode 是否存在,如果不存在的话,则把旧节点整个删除;
  2. 如果新节点 vnode 存在的话,再判断旧节点 oldVnode 是否存在,如果不存在的话,则新增新节点 vnode;
  3. 如果 vnode 和 oldVnode 都存在的话,判断两者是不是相同节点,如果是的话,这调用 patchVnode 方法,对两个节点进行详细比较判断;
  4. 如果两者不是相同节点的话,这种情况一般就是初始化页面,此时 oldVnode 其实是真实 Dom,这是只需要将 vnode 转换为真实 Dom 然后替换掉 oldVnode。

patchNode 过程

  1. 首先判断两个虚拟 Dom 是不是全等,即没有任何变动;是的话直接结束函数,否则继续执行;
  2. 其次更新节点的属性;
  3. 接着判断 vnode.text 是否存在,即 vnode 是不是文本节点。是的话,只需要更新节点文本既可,否则的话,这继续比较;
  4. 判断 vnode 和 oldVnode 是否有孩子节点:
    • 如果两者都有孩子节点的话,执行 updateChildren()方法,进行比较更新孩子节点;
    • 如果 vnode 有孩子节点而 oldVnode 没有的话,则直接新增所有孩子节点,并将该节点文本属性设为空;
    • 如果 oldVnode 有孩子节点而 vnode 没有的话,则直接删除所有孩子节点;
    • 如果两者都没有孩子节点,就判断 oldVnode.text 是否有内容,有的话清空内容既可。

UpdateChildren 过程

  1. 首先 oldStartVnode 和 newStartVnode 进行比较,如果比较相同的话,我们就可以执行 patchVnode 语句,并且移动 oldStartIdx 和 newStartIdx。
  2. oldEndVnode 和 newEndVnode 进行比较,操作同上。
  3. oldStartVnode 和 newEndVnode 做比较,如果相同 patchVnode 更新节点的内容,并且需要移动节点的位置。
  4. oldEndVnode 和 newStartVnode 做比较,操作同上。
  5. 如果存在 key 值,通过 oldCh 的 key->index 的映射表,找到对应旧孩子节点的下标 index。如果下标为空,新增节点。 如果下标不为空,比较两个节点,如果是相同节点的话,调用 patchVnode()函数,并移动节点位置。如果不同节点,直接创建新的一个节点。
  6. 头指针大于尾指针时就结束循环。

keep-alive

如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。

主要流程

  1. 判断组件 name ,不在 include 或者在 exclude 中,直接返回 vnode,说明该组件不被缓存。
  2. 获取组件实例 key ,如果有获取实例的 key,否则重新生成。
  3. key 生成规则,cid +”∶∶”+ tag ,仅靠 cid 是不够的,因为相同的构造函数可以注册为不同的本地组件。
  4. 如果缓存对象内存在,则直接从缓存对象中获取组件实例给 vnode ,不存在则添加到缓存对象中。
  5. 最大缓存数量,当缓存组件数量超过 max 值时,清除 keys 数组内第一个组件。

实现步骤:

  1. 获取 keep-alive 下第一个子组件的实例对象,通过他去获取这个组件的组件名
  2. 通过当前组件名去匹配原来 include 和 exclude,判断当前组件是否需要缓存,不需要缓存,直接返回当前组件的实例 vNode
  3. 需要缓存,判断他当前是否在缓存数组里面:
    ● 存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU)
    ● 不存在,将组件 key 放入数组,然后判断当前 key 数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key
  4. 最后将这个组件的 keepAlive 设置为 true

LRU (least recently used)缓存策略
LRU 缓存策略 ∶ 从内存中找出最久未使用的数据并置换新的数据。
LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是”如果数据最近被访问过,那么将来被访问的几率也更高”。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下 ∶
● 新数据插入到链表头部
● 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
● 链表满的时候,将链表尾部的数据丢弃。

$nextTick 原理及作用

Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。

nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout 的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。

状态管理

Vuex

Vuex 是一个用于 Vue.js 应用的状态管理模式 + 库。它充当应用程序中所有组件的集中存储,规则确保状态只能以可预测的方式进行更改。

用法示例:

import { createStore } from 'vuex';

const counterstore = createStore({
    state() {
        return {
            todoListItems: [],
        };
    },
    getters: {
        countList(state) {
            return state.todoListItems.length();
        },
    },
    actions: {
        addNewToList({ commit }, item) {
            commit('createNewItem', item);
        },
    },
    mutations: {
        createNewItem(state, item) {
            state.todoListItems.push(item);
        },
    },
});

更多见官方文档:https://vuex.vuejs.org/

Pinia

Pinia 是 Vue 的存储库,它允许你在组件/页面之间共享状态。如果您熟悉组合式 API,您可能会认为您已经可以通过简单的导出 const state = reactive({}) 共享全局状态。这对于单页应用程序来说是正确的,但如果应用程序是服务器端呈现的,则会使应用程序暴露于安全漏洞。但是,即使在小型单页应用程序中。

// stores/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
    state: () => {
        return { count: 0 };
    },
    // could also be defined as
    // state: () => ({ count: 0 })
    actions: {
        increment() {
            this.count++;
        },
    },
});

与 Vuex 的比较:官方介绍

总结就是:

  • 轻量,体积小
  • 更完整的 TypeScript 支持
  • 没有 mutation,不需要考虑创建规则来管理状态
  • 更直接的 API,不需要记住用 commit 还是 dispatch,不再需要使用魔法字符串来调用操作
  • 支持多个 Store,不像 Vuex 需要多模块嵌套