乐趣区

关于前端:图解-Vue-响应式原理

最近部门分享,有同学提到了 Vue 响应式原理,大家在探讨时,发现一些同学对这一常识了解还不够深刻,不能造成一个闭环,为了帮忙大家了解这个问题,我从新过了一下 Vue 源码,并整顿了多张流程图,便于大家了解。

  • Vue 初始化
  • 模板渲染
  • 组件渲染

本文 Vue 源码版本:2.6.11,为了便于了解,均有所删减。

本文将从以下两个方面进行摸索:

  • 从 Vue 初始化,到首次渲染生成 DOM 的流程。
  • 从 Vue 数据批改,到页面更新 DOM 的流程。

Vue 初始化

先从最简略的一段 Vue 代码开始:

<template>
  <div>
    {{message}}
  </div>
</template>
<script>
new Vue({data() {
    return {message: "hello world",};
  },
});
</script>

这段代码很简略,最终会在页面上打印一个 hello world,它是如何实现的呢?

咱们从源头:new Vue 的中央开始剖析。

// 执行 new Vue 时会顺次执行以下办法
// 1. Vue.prototype._init(option)
// 2. initState(vm)
// 3. observe(vm._data)
// 4. new Observer(data)

// 5. 调用 walk 办法,遍历 data 中的每一个属性,监听数据的变动。function walk(obj) {const keys = Object.keys(obj);
  for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i]);
  }
}

// 6. 执行 defineProperty 监听数据读取和设置。function defineReactive(obj, key, val) {
  // 为每个属性创立 Dep(依赖收集的容器,后文会讲)const dep = new Dep();
  // 绑定 get、set
  Object.defineProperty(obj, key, {get() {
      const value = val;
      // 如果有 target 标识,则进行依赖收集
      if (Dep.target) {dep.depend();
      }
      return value;
    },
    set(newVal) {
      val = newVal;
      // 批改数据时,告诉页面从新渲染
      dep.notify();},
  });
}

数据描述符绑定实现后,咱们就能失去以下的流程图:

图中咱们能够看到,Vue 初始化时,进行了数据的 get、set 绑定,并创立了一个 Dep 对象。

对于数据的 get、set 绑定咱们并不生疏,然而 Dep 对象什么呢?

Dep 对象用于依赖收集,它实现了一个公布订阅模式,实现了数据 Data 和渲染视图 Watcher 的订阅,咱们一起来分析一下。

class Dep {
  // 依据 ts 类型提醒,咱们能够得出 Dep.target 是一个 Watcher 类型。static target: ?Watcher;
  // subs 寄存收集到的 Watcher 对象汇合
  subs: Array<Watcher>;
  constructor() {this.subs = [];
  }
  addSub(sub: Watcher) {
    // 收集所有应用到这个 data 的 Watcher 对象。this.subs.push(sub);
  }
  depend() {if (Dep.target) {
      // 收集依赖,最终会调用下面的 addSub 办法
      Dep.target.addDep(this);
    }
  }
  notify() {const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      // 调用对应的 Watcher,更新视图
      subs[i].update();}
  }
}

依据对 Dep 的源码剖析,咱们失去了上面这张逻辑图:

理解 Data 和 Dep 之后,咱们来持续揭开 Watcher 的面纱。

class Watcher {constructor(vm: Component, expOrFn: string | Function) {
    // 将 vm._render 办法赋值给 getter。// 这里的 expOrFn 其实就是 vm._render,后文会讲到。this.getter = expOrFn;
    this.value = this.get();}
  get() {
    // 给 Dep.target 赋值为以后 Watcher 对象
    Dep.target = this;
    // this.getter 其实就是 vm._render
    // vm._render 用来生成虚构 dom、执行 dom-diff、更新实在 dom。const value = this.getter.call(this.vm, this.vm);
    return value;
  }
  addDep(dep: Dep) {
    // 将以后的 Watcher 增加到 Dep 收集池中
    dep.addSub(this);
  }
  update() {
    // 开启异步队列,批量更新 Watcher
    queueWatcher(this);
  }
  run() {
    // 和初始化一样,会调用 get 办法,更新视图
    const value = this.get();}
}

源码中咱们看到,Watcher 实现了渲染办法 _render 和 Dep 的关联,初始化 Watcher 的时候,打上 Dep.target 标识,而后调用 get 办法进行页面渲染。加上上文的 Data,目前 Data、Dep、Watcher 三者的关系如下:

咱们再拉通串一下整个流程:Vue 通过 defineProperty 实现了 Data 中所有数据的代理,当数据触发 get 查问时,会将以后的 Watcher 对象退出到依赖收集池 Dep 中,当数据 Data 变动时,会触发 set 告诉所有应用到这个 Data 的 Watcher 对象去 update 视图。

目前的整体流程如下:

上图的流程中 Data 和 Dep 都是 Vue 初始化时创立的,但当初咱们并不知道 Wacher 是从哪里创立的,带着这个问题,咱们接着往下摸索。

模板渲染

上文中,咱们剖析了初始化 Vue 过程中解决数据的局部,接下来,咱们剖析一下数据渲染的局部。

其实 new Vue 执行到最初,会调用 mount 办法,将 Vue 实例渲染成 dom。

// new Vue 执行流程。// 1. Vue.prototype._init(option)
// 2. vm.$mount(vm.$options.el)
// 3. render = compileToFunctions(template),编译 Vue 中的 template 模板,生成 render 办法。// 4. Vue.prototype.$mount 调用下面的 render 办法挂载 dom。// 5. mountComponent

// 6. 创立 Watcher 实例
const updateComponent = () => {vm._update(vm._render());
};
// 联合上文,咱们就能得出,updateComponent 就是传入 Watcher 外部的 getter 办法。new Watcher(vm, updateComponent);

// 7. new Watcher 会执行 Watcher.get 办法
// 8. Watcher.get 会执行 this.getter.call(vm, vm),也就是执行 updateComponent 办法
// 9. updateComponent 会执行 vm._update(vm._render())

// 10. 调用 vm._render 生成虚构 dom
Vue.prototype._render = function (): VNode {
  const vm: Component = this;
  const {render} = vm.$options;
  let vnode = render.call(vm._renderProxy, vm.$createElement);
  return vnode;
};
// 11. 调用 vm._update(vnode) 渲染虚构 dom
Vue.prototype._update = function (vnode: VNode) {
  const vm: Component = this;
  if (!prevVnode) {
    // 首次渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
  } else {
    // 更新
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
};
// 12. vm.__patch__ 办法就是做的 dom diff 比拟,而后更新 dom,这里就不开展了。

看完 Vue 模板渲染的过程,咱们能够失去如下的流程图:

到这里,咱们就晓得了 Watcher 其实是在 Vue 初始化的阶段创立的,属于生命周期中 beforeMount 的地位创立的,创立 Watcher 时会执行 render 办法,最终将 Vue 代码渲染成实在的 DOM。

咱们再将之前的流程整合一下,就能失去以下的流程:

上图剖析了 Vue 初始化到渲染 DOM 的整个过程,最初咱们再剖析一下,当数据变动时,Vue 又是怎么进行更新的?

其实,在上图也能看出,在 Data 变动时,会调用 Dep.notify 办法,随即调用 Watcher 外部的 update 办法,此办法会将所有应用到这个 Data 的 Watcher 退出一个队列,并开启一个异步队列进行更新,最终执行 _render 办法实现页面更新。

整体的流程如下:

好了,摸索到这里,Vue 的响应式原理,曾经被咱们剖析透彻了,如果你还没有明确,无妨再细品一下上图。

组件渲染

原本摸索到下面的流程图就完结了,但好奇的我又想到了一个问题 ????。

Vue 组件又是怎么渲染的呢?

带着这个问题,我持续查阅了源码。

// 从模板编译开始,当发现一个自定义组件时,会执行以下函数
// 1. compileToFunctions(template)
// 2. compile(template, options);
// 3. const ast = parse(template.trim(), options)
// 4. const code = generate(ast, options)
// 5. createElement

// 6. createComponent
export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // $options._base 其实就是全局 Vue 构造函数,在初始化时 initGlobalAPI 中定义的:Vue.options._base = Vue
  const baseCtor = context.$options._base;
  // Ctor 就是 Vue 组件中 <script> 标签下 export 出的对象
  if (isObject(Ctor)) {
    // 将组件中 export 出的对象,继承自 Vue,失去一个构造函数
    // 相当于 Vue.extend(YourComponent)
    Ctor = baseCtor.extend(Ctor);
  }
  const vnode = new VNode(`vue-component-${Ctor.cid}xxx`);
  return vnode;
}

// 7. 实现组件继承 Vue,并调用 Vue._init 办法,进行初始化
Vue.extend = function (extendOptions: Object): Function {
  const Super = this;
  const Sub = function VueComponent(options) {
    // 调用 Vue.prototype._init,之后的流程就和首次加载保持一致
    this._init(options);
  };
  // 原型继承,相当于:Component extends Vue
  Sub.prototype = Object.create(Super.prototype);
  Sub.prototype.constructor = Sub;
  return Sub;
};

看完组件渲染的源码后,联合上文,重新整理了一张流程图,图中的蓝色局部就是渲染组件的过程。

好了,当初是真的完结了,最终的流程图就是下面的这一张图。

问个问题,当初你了解 Vue 响应式原理了吗?

如果仍感觉不好了解,我这里还筹备了一张带标注的简图 ????

思考与总结

本文从源码的角度,介绍了 Vue 响应式原理,来简略回顾一下吧。

  1. 从 new Vue 开始,首先通过 get、set 监听 Data 中的数据变动,同时创立 Dep 用来收集应用该 Data 的 Watcher。
  2. 编译模板,创立 Watcher,并将 Dep.target 标识为以后 Watcher。
  3. 编译模板时,如果应用到了 Data 中的数据,就会触发 Data 的 get 办法,而后调用 Dep.addSub 将 Watcher 收集起来。
  4. 数据更新时,会触发 Data 的 set 办法,而后调用 Dep.notify 告诉所有应用到该 Data 的 Watcher 去更新 DOM。

最初,如果你对此有任何想法,欢送留言评论!

退出移动版