vue

Vue原理

响应式原理、VitralDOM

Posted by page on April 18, 2022

Vue原理

前言

为什么会产生 Vue、React 等这些框架并且被前端开发者所使用?

问题的产生:UI 与状态同步

早期前端开发主要内容仅数据展示,提交表单

业务复杂 => 数据复杂性,深度交互 => 频繁编写根据数据操作 DOM 的逻辑

缓解: 将用户界面更新操作独立

mvc: model 数据模型 view 视图 control 控制器

问题:频繁操作 DOM 性能低下(如果没有注意性能的话);中间步骤过多,易产生 bug 且不易维护。

解决:MVVM 的核心 VM 视图数据层,存在于视图层(view)之上,使我们不用关注 view 的更新,只需关注视图数据层的逻辑,因为 view 已经被视图数据层直接控制,我们只需专注视图数据层的逻辑即可;

结果:产生了 Vue、React 等这种 MVVM 模型 框架/库

Vue 数据劫持、Angular 的脏检测还是 React 的组件级 reRender 都是解决 ui 与状态同步问题的不同实现;

成为主流原由:同类中的是佼佼者,不管是数据流管理架构还是成熟的前端框架自带的生态解决方案。后续各种UI 库出现巩固了前端开发中地位;

总结:提高开发效率

Vue实例生命周期

生命周期概括了vue在所有情况下的操作逻辑

init vue 实例:生命周期逻辑

beforeCreated

inject 与 data 响应式(通过 defineProperty 为 data 设置set,get;数据劫持是后续响应式和依赖收集的基础)

created

是否存在 el 选项,存在则立即将调用 $mount 方法;否则等待手动调用 vm.$mount()(调用挂载)

编译:是否存在 template 选项,存在(即运行时编译)则编译 template 为 jsx 在 render 函数生成虚拟节点;否则直接将挂载元素放入 render 中(编译即语法解析,生成AST语法树得到渲染函数)

beforeMounted

创建 vm.$el(真实 DOM 树) 替换 el(执行渲染函数得到虚拟DOM,此过程完成了依赖收集;有了虚拟DOM,解析它替换到真实DOM即完成挂载)

mounted

监听到响应数据修改(set触发响应式)

beforeUpdate

调用 re-render, diff 对比后更新(patch)真实 DOM

updated

beforeDestroy

清除 watcher 监听、子组件、事件监听器

destroyed

Vue内部

new Vue()发生了什么

  1. 初始化及调用挂载

    最重要的是通过 Object.defineProperty 设置 被设置的对象的 setter 与 getter 函数,这是实现「响应式」以及「依赖收集」的前置条件

  2. 编译

    parse、optimize 与 generate 三阶段

    generate 是将 AST 转化成 render function 字符串(jsx)的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。

    在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。

  3. 响应式

    render function 渲染时生成虚拟 DOM,解析虚拟 DOM 替换真实 DOM

    render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「依赖收集」,「依赖收集」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中

    示意图

    在修改对象的值的时候,会触发对应的 setter, setter 通知之前「依赖收集」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch 的过程以及使用队列来异步更新的策略

  4. Virtual DOM

    用 js 对象属性来描述节点,类 DOM 树的数据结构对真实 DOM 树的抽象,实际上它只是一层对真实 DOM 的抽象。

    JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

    更新视图(真实 DOM)都基于 Virtual DOM 的更新,update 会再次执行 render function, 得到一个新的 VNode 节点

    解析这个新的 VNode 节点,然后用 innerHTML 直接全部渲染到真实 DOM 中。可行但性能待考虑,因为我们只对其中的一小块内容进行了修改。「patch」:将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们虚拟节点的「差异」。根据虚拟节点的差异对真实 DOM 进行修改

实践

响应式

基于 Object.defineProperty 的响应式

此处位于流程的初始化(init)阶段中,目的是观察数据模型(数据被读取/修改时具备自执行操作的能力)。

依赖收集

只有响应式无法处理多实例(视图)依赖多数据模型的各个属性情况;

订阅者 Dep ,它的主要作用是用来存放Watcher观察者对象,且通知Watcher更新视图;

观察者 Watch,它的主要作用是存放当前视图更新操作,且能够在视图依赖数据模型的某个属性时,在属性的getter中能被属性的dep(订阅者)收集;

依赖收集,new Vue实例执行observer时,规定属性的getter中将视图的Watch收集到属性的dep(订阅器),setter中调用dep.notify;

// vue.js
// 订阅者
class Dep {
  constructor() {
    this.subs = [];
  }
  static target = null;
  addSub(watcher) {
    this.subs.push(watcher);
  }
  notify() {
    this.subs.forEach((watcher) => watcher.update());
  }
}
// 监听者
class Watcher {
  constructor(options) {
    Dep.target = this;
    this._update = options.update;
  }
  update() {
    console.log("update");
    this._update();
  }
}
// 对象响应化(可观察)
function observer(obj) {
  if (!(obj && typeof obj === "object")) return;
  Object.keys(obj).forEach((attr) => {
    const dep = new Dep();
    let val = obj[attr];
    Object.defineProperty(obj, attr, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter() {
        // 依赖收集
        if (Dep.target) dep.addSub(Dep.target);
        console.log(attr, dep);
        return val;
      },
      set: function reactiveSetter(newVal) {
        if (newVal === val) return;
        // 更新视图
        val = newVal;
        dep.notify();
      }
    });
  });
  return { value: obj, dep: new Dep() };
}
// Vue 类
class Vue {
  constructor(options = {}) {
    this._data = typeof options.data === "function" ? options.data() : options.data;
    options.template && (this._template = options.template);
    // 1.  声明 get 方法
    if (!this._data.__ob__) this._data.__ob__ = observer(this._data);
    // 2.  新建一个 Watcher 对象
    new Watcher({ update: () => this._update() });
    // 3.  触发 get 方法
    options.el && this.$mount(options.el);
    Dep.target = null;
  }
  $mount(el) {
    this.$el = document.querySelector(el);
    // 挂载替换$el...
    let innerHTML = this._template;
    Object.keys(this._data).forEach((attr) => {
      const value = this._data[attr];
      innerHTML = innerHTML.replaceAll(`}`, value);
    });
    this.$el.innerHTML = innerHTML;
  }
  // 视图更新
  _update() {
    console.log(`更新视图成功!`);
    let innerHTML = this._template;
    Object.keys(this._data).forEach((attr) => {
      const value = this._data[attr];
      innerHTML = innerHTML.replaceAll(`}`, value);
    });
    this.$el.innerHTML = innerHTML;
  }
}
<div id="app"></div>
<div id="h5"></div>
<script>
    const model = {
      text: "",
      time: new Date()
    };
    const App = new Vue({
      el: "#app",
      data: model,
      template: `
        <input value="" />
        <div>内容:</div>
        <div>时间:</div>`
    });
    const H5 = new Vue({
      el: "#h5",
      data: model,
      template: `
        <div>vm2内容:</div>
        <div>vm2时间:</div>`
    });
    console.log(App);
    console.log(H5);
</script>

VNode

Virtural DOM由虚拟节点(VNode)组成,vue的createElement接口实现VNode创建;虚拟DOM是编译生成的render函数的执行结果;

VNode由标签、节点描述Attr(class,style…)、子节点三部分组成;

VNode中节点描述引用vue实例的数据模型(data/props);数据模型修改即VNode数据修改,修改后无需其它操作可作为更新视图的依据;

Compile

vue支持template模板语法,所以内部存在编译将源代码转为抽象语法结构的树状表现形式;

3阶段:parseoptimizegenerate

parse

用正则等方式将 template 模板中进行字符串解析,得到指令、class、style等数据,形成 AST (抽象语法树);

optimize

为静态节点添加标记,方便patch 时直接跳过这些被标记的节点的比对,实现优化;

generate

generate 是将 AST 转化成 render function 字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串;

Diff Patch

适配层

Virtural DOM以JS对象来描述节点树,这使其具备跨平台能力;适配层将不同平台的 API 封装在内,以同样的接口对外提供视图操作;

diff

  1. 对虚拟DOM树上同层的树节点进行比较,新旧节点存在/不存在,增删新旧节点;
  2. 若有新旧节点相同(sameVnode),则进入patchVnode(比对);
  3. 对虚拟节点子节点进行比较,新旧节点的子节点存在/不存在,增删新旧节点的子节点;
  4. 若有新旧子节点都存在,则进入updateChildren(子节点比对);
  5. 存在oldStartIdxnewStartIdxoldEndIdx 以及 newEndIdx;从新节点索引项开始向旧的比对,对旧节点调整位置,更新新旧索引;
  6. 若新旧起始结束节点都不相同,则对维护的旧节点key索引映射中查找对应的新节点,找到且sameVnode则旧节点调整位置,更新新旧索引;未找到sameVnode则将新节点插入起始/结束索引,更新新旧索引;、
  7. 当任一起始索引贴近,则相同项比对完毕,此时对多余旧节点remove或多余新节点添加至旧节点;