Vue2源码【2】Vue MVVM 的工作流程

整个 Vue 的运作其实始于 import Vue from 'vue' 执行的那一刻,一开始只是执行一些准备工作,然后到 new Vue 才开始执行核心的流程,也就是开始构建 MVVM。

在上一篇博客有讲到, MVVM 其实就可以划分为2个阶段,“首次构建”+“重新渲染”。之所以会有“重新渲染”,是因为利用了响应式原理,当数据变化时需要重新渲染,还有强制重新渲染也是类似的。

运行时的首次构建会走这个逻辑:

new Vue ==> init ==> $mount ==> compile(如果是带编译的版本会走这步;而如果你直接写render函数则会忽略这步) ==> render(得到vnode) ==> patch(根据vnode操作DOM)

下面这个过程,经过我多番勘正,应该是正确的。即便看不懂也没关系,之后我会拎出几个点来说一下。

我们以最简单的案例为例:【尚无使用组件、显式写render函数】 来分析首次构建的流程,其他情况只是稍有区别:

  • import Vue 的时候发生 【预定义】
    • initMixin函数:预先定义Vue.prototype._init。【_init和vm实例一对一】
    • 其他函数的执行,主要是用于预定义vm等
  • ------------ 在new Vue的时候执行: (src/core/instance/init.js) -----------
  • _init () 【在刚刚创建vm时,各模块给vm预设一些属性或者功能】
    • 合并配置:merge options 赋值给vm.$options
    • initLifecycle
    • initEvents 事件中心
    • initRender: vm.$createElement就是它定义的
    • initState 初始化 data、props、computed、watcher 等等
      • initData
        • getData简单调用 用户data函数.call(vm, vm)。data的结果赋值给vm._data
        • 然后就是用数据劫持,让this[‘某属性’]代理vm._data[‘某属性’]
        • 然后observe data,也就是《响应式的原理 之 观察者模式》
  • 调用vm.$mount:利用观察者设计模式:Watcher 在首次构建和数据变化的时候调用vm._update(vm._render())
    • vm._render():得到vnode
      • vm._render其中比较关键节点的是 $createElement 和 createComponent 函数,分别针对不同的类型
    • vm._update(vm._render()):根据vnode更新vm.$el,并完成真实dom更新。其中比较关键的函数是 vm.__patch__
src/core/instance/init.js

import Vue的时候就会执行下面代码。而_init是在执行new Vue的时候执行的。

function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

vm.$mount

vm.$mount 的执行,是构建整个 MVVM 的核心。

// 实现形式:自己包装自己
const mount = Vue.prototype.$mount // 【子组件的挂载】被包装vm.$mount,约等于 mountComponent 函数,来自src/platforms/web/runtime/index.js
Vue.prototype.$mount = function () {
  // 【铺垫变量,略】option.render/option.template等等 ==> template变量 ==> compileToFunctions函数(得出vm.$options.render和vm.$options.staticRenderFns
  return mount.call(this, el, hydrating)
}

综上所述,vm.$mount 约等于 调用 mountComponent 函数 约等于Watcher结合updateComponent回调 + 调用 beforeMount 和 mounted 钩子 。 而 updateComponent 回调 的核心就是 vm._update(vm._render())(外层函数即 vm.__patch__) 也就是一个观察者模式,会首次调用1次回调,(不考虑优化)然后观察每当“数据改变需要驱动视图更新”又会调用1次回调,即在初始化和update阶段调用vm._update(vm._render())

  • updateComponent = () => { vm._update(vm._render())} 回调,用于订阅观察者模式,在稍后发布事件时调用。
    • 【核心】vm._update(vm._render())。其中,vm._render 约等于 vm.$options.render+vm.$createElement回调 【生成vnode】
    • vm._update 用于执行一次真实的DOM更新。主要通过vm.__patch__ 【根据传进来的vnode更新vm.$el,并且完成真实dom更新】

updateComponent负责逻辑,而new Watcher负责控制执行它的时机:

  • 【略】(callHook 调用beforeMount还有 mounted生命周期钩子。之后mark那些函数都是性能埋点)
  • new Watcher(vm, updateComponent, noop, { 一个服务于“渲染”的观察者模式 决定了什么时候调用updateComponent回调
    • Watcher 的效果:一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数 后者就是一个“响应式原理”
    • 只要是能用上的组件,都会执行其vm.$mount,那么就会执行new Watcher,这就是为什么“视图组件实例基本上都配备有渲染watcher”

几个要点:

vdom / vnode

其实 VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。

vm.$createElement

一句话总结:vm.$createElement:返回vnode,其间规范化children,其间处理了一堆它责任范围内的edge case。其核心是createElement也就是_createElement。_createElement,也就是children 的规范化以及 VNode 的创建。

$createElement 之 createComponent

一句话总结,也是返回vnode:【1】构造子类构造函数 【2】安装组件钩子函数 【3】实例化 vnode

$createElement 的时候,如果发现是组件类型,就直接跑 createComponent 的逻辑,以取代 $createElement 其本身。

和普通元素节点的 vnode 不同,组件的 vnode 是没有 children 的。

可以看到,createComponent 的逻辑也会有一些复杂,但是分析源码比较推荐的是只分析核心流程,分支流程可以之后针对性的看,所以这里针对组件渲染这个 case 主要就 3 个关键步骤:

  • 【1】构造子类构造函数
  • 【2】安装组件钩子函数
  • 【3】实例化 vnode
文章目录
  1. src/core/instance/init.js
  • vm.$mount
    1. vdom / vnode
    2. vm.$createElement
    3. $createElement 之 createComponent