【Vue】new Vue()做了什么?

1,134 阅读17分钟

前言

Vue 这个框架,我们使用的很是熟练了,那么我们有人想过new Vue()帮我们做了什么?

const app = new Vue({
    el:"#app",
    // 对象格式
    data:{
        foo:"foo"
    },
    // 函数格式
    data(){
        return {
             foo:"foo"
        }
    }
})

本文涉及源码版本:2.7.14

一、new Vue() 做了什么?

首先,我们可以先了解一下,new Vue 涉及了以下五个方面,所以我们可以从这五个方面进行分析:

  • init初始化
  • mount挂载
  • compiler编译
  • render渲染
  • patcch补丁

1.1 init初始化

执行 init 操作,包括且不限制 initLifecycle、initState等。

通过入口文件,将配置项传入,调用 _init 方法进行初始化

Vue方法的源码如下:vue-2.7.14\src\core\instance\index.ts

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
import type { GlobalAPI } from 'types/global-api'

function Vue(options) {
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

//@ts-expect-error Vue has function type
initMixin(Vue)
//@ts-expect-error Vue has function type
stateMixin(Vue)
//@ts-expect-error Vue has function type
eventsMixin(Vue)
//@ts-expect-error Vue has function type
lifecycleMixin(Vue)
//@ts-expect-error Vue has function type
renderMixin(Vue)

export default Vue as unknown as GlobalAPI

通过上面的源码我们可以了解到,new Vue()在不出意外的情况下,执行了方法this._init()

this._init()方法主要步骤:

  1. 合并配置项
  2. 初始化生命周期、初始化事件中心、初始化渲染、调用beforCreate钩子,初始化injectionstate/data/props/computed...provide、调用create钩子;
  3. 最后通过判断有无vm.$options.el进行vm.$mount

this._init()源码:vue-2.7.14\src\core\instance\init.ts

export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    // 性能检测
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // 一个标志,将其标记为 vue 实例,而无需执行实例
    vm._isVue = true
    // 避免实例添加observer
    vm.__v_skip = true
    // 影响范围(作用域)
    vm._scope = new EffectScope(true /* detached */)
    vm._scope._vm = true
    // 合并配置:业务逻辑以及组件的一些特性全都放到了 vm.$options 中
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options as any)
    } else { // 顶层的vm
      vm.$options = mergeOptions(
        // 一些内置组件(keep-alive...)和指令(show、model...)
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    // 初始化vm._renderProxy 为后期渲染做准备
    // 开发环境并支持proxy vm._renderProxy = new Proxy(vm)
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化生命周期
    initLifecycle(vm)
    // 初始化事件
    initEvents(vm)
    // 初始化渲染
    initRender(vm)
    // 调用 beforeCreate 回调
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    // 初始化 injections
    initInjections(vm) // resolve injections before data/props
    // 初始化 state
    initState(vm)
    // 初始化 provide
    initProvide(vm) // resolve provide after data/props
    // 调用 created 回调
    callHook(vm, 'created')

    /* istanbul ignore if */
    // 性能检测
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    // el存在就进入下一步操作 - 挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

1.2 mount挂载

通过 init 源码我们可以了解到在进行一系列初始化操作后,下一步操作就是挂载

image.png

挂载过程中完成了最重要的两件事:

  • 初始化
  • 建立更新机制

1.2.1 生成 render 函数 -- vm.prototype.$mount()

vm.$mount()方法主要作用是若配置项中没有 render 方法则将 template 作为编译函数的参数生成该方法,最后调用 runtime 中的 mount 方法

mount源码:vue-2.7.14\src\platforms\web\runtime-with-compiler.ts

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  // 1. 不允许挂载在 html 和 body 上
  if (el === document.body || el === document.documentElement) {
    __DEV__ &&
      warn(
        `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
      )
    return this
  }

  // 获得配置项
  const options = this.$options
  // resolve template/el and convert to render function
  // render 不存在
  if (!options.render) {
    let template = options.template
    // 2. 初始化 template
    if (template) {
      if (typeof template === 'string') {
        // 如果模板开头是#,说明是一个ID选择器,通过idToTemplate获取相应的innerHtml
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (__DEV__ && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) { // 如果是node对象,直接获取innerHtml
        template = template.innerHTML
      } else { // 不合法配置项
        if (__DEV__) {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // @ts-expect-error
      template = getOuterHTML(el)
    }
    // 3. 将模板转化为 render 函数
    if (template) {
      /* istanbul ignore if */
      // 性能检测
      if (__DEV__ && config.performance && mark) {
        mark('compile')
      }

      // 3.1 将模板转换为 render 函数,得到 ast 抽象树获得的render函数
      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          outputSourceRange: __DEV__,
          shouldDecodeNewlines,
          shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        },
        this
      )
      // 3.2保存渲染函数 with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_v(\"\\n……
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      // 性能检测
      if (__DEV__ && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  // 4. 调用$mount方法
  return mount.call(this, el, hydrating)
}

1.2.2 完成挂载——mountComponent

这是runtime时的mount方法。最后直接调用了mountComponent方法。所以直接看mountComponent方法吧。

源码:vue-2.7.14\src\platforms\web\runtime\index.ts

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent方法就是 vue 从 beforeMountmounted 的过程。主要步骤如下:

  1. render 函数是生成 DOM 的关键,所以在一开始会先判断是否存在,如果不存在创建一个空的虚拟DOM
  2. 调用生命周期钩子 beforeMounts
  3. 生成 updateComponent 方法,用于触发页面的更新
  4. 生成 watcherOptions 配置,里面 before 方法会触发 beforeUpdate 钩子,将会在触发 updateComponent 前调用
  5. 生成组件更新的 watcher, 前面两部分是为了这部分做准备。new Watcher() 后触发 updateComponent 方法的调用,生成页面虚拟DOM,将 watcher 加入到影响页面变化 data 的依赖收集器中,这样当 data 发送变化时,就会触发页面更新,最终进行 dom diff,生成真实dom
  6. 调用生命周期钩子 mounted

mountComponent方法源码如下:vue-2.7.14\src\core\instance\lifecycle.ts

export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  vm.$el = el
  // render 不存在 - 警告
  if (!vm.$options.render) {
    // @ts-expect-error invalid type
    vm.$options.render = createEmptyVNode
    if (__DEV__) {
      /* istanbul ignore if */
      if (
        (vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el ||
        el
      ) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
            'compiler is not available. Either pre-compile the templates into ' +
            'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  // 调用钩子函数 beforeMount
  callHook(vm, 'beforeMount')

  // 初始化 updateComponent
  let updateComponent
  /* istanbul ignore if */
  if (__DEV__ && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      // vm._render() 得到Vnode
      // vm._update() 更新页面
      vm._update(vm._render(), hydrating)
    }
  }

  // 页面更新前会调用 before 函数,触发钩子函数 beforeUpdate
  const watcherOptions: WatcherOptions = {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }

  if (__DEV__) {
    watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
    watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  // 创建组件更新的 watcher,后面会加入到影响页面变化 data 的 deps
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
  hydrating = false

  // flush buffer for flush: "pre" watchers queued in setup()
  const preWatchers = vm._preWatchers
  if (preWatchers) {
    for (let i = 0; i < preWatchers.length; i++) {
      preWatchers[i].run()
    }
  }

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  // 挂载完成,触发狗仔函数 mounted
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

1.2.3 更新机制形成 - new Watcher()

这里主要分析上面 new Watcher() 后,如何触发 updateComponent 方法的调用。因此只涉及部分 Watcher 类的代码。

new Watcher() 构造方法里会初始化一些属性,最重要的是将 this.getter = expOrFn,将 updateComponent 方法赋给了 this.getter。最后初始化 this.value 时,调用了 this.get()

new watcher() 构造方法源码如下:vue-2.7.14\src\core\observer\watcher.ts

constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
) {
    recordEffectScope(
      this,
      // if the active effect scope is manually created (not a component scope),
      // prioritize it
      activeEffectScope && !activeEffectScope._vm
        ? activeEffectScope
        : vm
        ? vm._scope
        : undefined
    )
    // 将 watcher 挂载到 vm._watcher 上
    if ((this.vm = vm) && isRenderWatcher) {
      vm._watcher = this
    }
    // options
    // 根据 options 初始化一些属性
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
      if (__DEV__) {
        this.onTrack = options.onTrack
        this.onTrigger = options.onTrigger
      }
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.post = false
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = __DEV__ ? expOrFn.toString() : ''
    // parse expression for getter
    // render 类型 expOrFn 肯定是一个方法,所以 this.getter 就是页面更新方法了
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        __DEV__ &&
          warn(
            `Failed watching path: "${expOrFn}" ` +
              'Watcher only accepts simple dot-delimited paths. ' +
              'For full control, use a function instead.',
            vm
          )
      }
    }
    // 因为 render 类型的 watcher lazy 值不会是 true(只有computed才会是),所以接下来会调用 get 方法
    this.value = this.lazy ? undefined : this.get()
}

get 方法中,可以看到这行代码 value = this.getter.call(vm, vm)。就是这里调用了 updateComponent 方法。调用了 updateComponent 方法会触发 vm._update(vm.render(), hydrating)。所以接下来分析 vm._render 方法。

get() 方法如下:vue-2.7.14\src\core\observer\watcher.ts

/**
 * Evaluate the getter, and re-collect dependencies.
 */
get() {
  // 相当于 Dep.target = this
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 调用 this.getter 方法,即调用了 updateComponent 方法
    value = this.getter.call(vm, vm)
  } catch (e: any) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

1.3 生成虚拟DOM - vm._render()

1.3.1 用户自定义 render 函数

这种情况是用户自定义 render 函数。上面调用 render 方法处,传了一个 vm.$createElement 函数作为参数,所以 hvm.$createElement

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

找到 vm.$createElement 源码发现最终调用的是 createElement 方法。

vm.$createElement 源码:vue-2.7.14\src\core\instance\render.ts

// 这段代码在initRender中

// 用于用户自己手写的 render 函数
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

1.3.2 编译生成的 render 函数

这段html代码通过编译后生成的 render 函数如下:

<div id="app">
    {{num1}}
    {{hello}}
    <button @click="changeNum">num1</button>
    <button @click="changeNum2">num2</button>
    <div v-for="(item,idx) of arr" :key="item">
        {{item}}
    </div>
    <comp :msg="msg" @log-msg="logMsg"></comp>
</div>

d2eea221ede54a60814c2bc97d44b5be~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.webp

代码中,使用了 with(this){} 说明 {} 所有引用都指向 this 对象(即vm)。所以代码中的 _c = vm._c,因此最终其实也是调用 createElement 函数生成虚拟DOM

在执行这段代码时,会读取到 data 中的属性,比如 num1、arr等等,读取时会调用这些属性的 getter 方法,在 getter 方法中会判断到 Dep.target 中存在值,会将该值存入依赖收集器,从而 完成依赖收齐。

属性发生改变时,触发 setter 方法,通过 depwatcher 最终会调用 updateComponent 方法进行页面的更新。

vm._c 源码:vue-2.7.14\src\core\instance\render.ts

// src\core\instance\render.ts
// 这段代码在initRender中

// 用于编译后产生的render函数
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

1.3.3 createElement

createElement源码如下:vue-2.7.14\src\core\vdom\create-element.ts

/**
 * SIMPLE_NORMALIZE: 浅扁平
 * ALWAYS_NORMALIZE:深扁平
*/
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement(
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any, // 使用哪种扁平化方式
  alwaysNormalize: boolean // 当render 函数是手写的时候为true
): VNode | Array<VNode> {
  // 实际上是平判断 data 是否存在,如果不存在参数向前进一个
  if (isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

_createElement方法是创建Vnode的核心:

  • 规范 childrenchildren 按照 normalizationType 类型进行扁平化处理,这个目的是规范 children ,对于同一深度层次的元素,不管是单个元素还是该元素在数组中,只要是属于同一深度层次,都扁平化一个一维数组中
  • 生成 Vnode:如果是普通标签,直接 new Vnode()。Vue组件则需要通过 createComponent 生成 Vnode

_createElement 源码如下:vue-2.7.14\src\core\vdom\create-element.ts

/**
 * 生成虚拟DOM
 * @param context: vm
 * @param tag: 标签(div、p、ul)
 * @param data: data标签上的属性
 * @param children: 子节点
 * @param normalizationType
 * @returns Vnode: 虚拟dom
*/
export function _createElement(
  context: Component,
  tag?: string | Component | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // Vnode data 不能为响应式对象
  // 有 __ob__ 代表这个对象为响应式对象
  if (isDef(data) && isDef((data as any).__ob__)) {
    __DEV__ &&
      warn(
        `Avoid using observed data object as vnode data: ${JSON.stringify(
          data
        )}\n` + 'Always create fresh vnode data objects in each render!',
        context
      )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  // <Component :is=""/>
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
    warn(
      'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
      context
    )
  }
  // support single function children as default scoped slot
  if (isArray(children) && isFunction(children[0])) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 根据 normalizationType 对应不同扁平化处理方式
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 深层扁平化
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 浅层扁平化
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // 标签为字符串类型
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (
        __DEV__ &&
        isDef(data) &&
        isDef(data.nativeOn) &&
        data.tag !== 'component'
      ) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      // 创建普通标签的Vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag),
        data,
        children,
        undefined,
        undefined,
        context
      )
    } else if (
      (!data || !data.pre) &&
      isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
    ) {
      // component
      // 为组件,调用 createComponent 创建 Vnode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      // 直接创建 Vnode
      vnode = new VNode(tag, data, children, undefined, undefined, context)
    }
  } else {
    // direct component options / constructor
    // tag为组件构造函数或组件选项,创建Vue组件
    vnode = createComponent(tag as any, data, context, children)
  }
  if (isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

1.3.4 创建Vue组件 - createComponent

createComponent作用是返回 Vue 组件的虚拟DOM。同时在这过程中,会构造子类构造函数(这里会调用 _init 方法完成组件初始化)、安装组件钩子函数

所谓的组件化就是把页面拆分成多个组件,组件吧、内部包含自己的 HTML、CSS、JavaScript,这样可以拼成一个模块,并且组件可以复用、拼接,等同于积木一样,一个大的页面可以由很多小的组件拼接而成,接下来我们就用一个例子来看 vue 的组件内部是如何工作的

import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

const app = new Vue({
  render: (h) => h(App),
}).$mount("#app");

console.log(app);

这段代码是通过 render 函数去渲染的,render 函数调用 createElementcreateElement 根据 tag 的不同调用不同的方法生成 Vnode

在例子中,我们可以看到传入的是一个名为 App 的对象,所以继续执行 createComponent

createComponent在这里主要做了三件事:

  1. 把传入的组件对象构造成 vue 的子类
  2. 安装组件钩子函数
  3. 实例化 Vnode 并返回

createComponent源码如下:vue-2.7.14\src\core\vdom\create-component.ts

export function createComponent(
  Ctor: typeof Component | Function | ComponentOptions | void,
  data: VNodeData | undefined,
  context: Component,
  children?: Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  // 根实例
  // 在 initGlobalAPI 中定义的 vue.options._base = Vue
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    // 1.
    // 通过 Vue 的 extend 方法,生成子类构造函数,使得子类也有 Vue 根实例的一些方法
    // 其实就是构造Vue的子类
    // src/core/global-api/extend.js
    Ctor = baseCtor.extend(Ctor as typeof Component)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (__DEV__) {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  // 异步组件
  let asyncFactory
  // @ts-expect-error
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  // 解析构造函数选项,如果全局混合后应用
  // 创建组件构造函数
  resolveConstructorOptions(Ctor as typeof Component)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    // @ts-expect-error
    transformModel(Ctor.options, data)
  }

  // extract props
  // @ts-expect-error
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  // @ts-expect-error
  // 函数式组件
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(
      Ctor as typeof Component,
      propsData,
      data,
      context,
      children
    )
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  // @ts-expect-error
  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  // 2. 安装组件钩子函数
  installComponentHooks(data)

  // return a placeholder vnode
  // @ts-expect-error
  // 3.实例化 Vnode 并返回。需要注意组件Vnode没有children,这点在之后的patch在分析
  const name = getComponentName(Ctor.options) || tag
  const vnode = new VNode(
    // @ts-expect-error
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    // @ts-expect-error
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

1.4 生成真实DOM - vm._update(vnode)

_update 源码:vue-2.7.14\src\core\instance\lifecycle.ts

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  // 存储在前面的el
  const prevEl = vm.$el
  // 存储在前面的Vnode
  const prevVnode = vm._vnode
  // 存储活动的实例
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render 首次渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates 更新
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // update __vue__ reference
  // 更新指向
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  let wrapper: Component | undefined = vm
  while (
    wrapper &&
    wrapper.$vnode &&
    wrapper.$parent &&
    wrapper.$vnode === wrapper.$parent._vnode
  ) {
    wrapper.$parent.$el = wrapper.$el
    wrapper = wrapper.$parent
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

通过上面源码,我们可以发现,不管是首次渲染,还是更新都执行了方法__patch____patch__方法的主要作用是创建、渲染和返回节点

1.4.1 __patch__

__patch__的源码:vue-2.7.14\src\platforms\web\runtime\index.ts

import { patch } from './patch'

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

__patch__是来自于 patch 方法的赋值,该方法源码:vue-2.7.14\src\platforms\web\runtime\patch.ts

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

createPatchFunction方法才是__patch__的核心所在

1.4.2 createPatchFunction

源码:vue-2.7.14\src\core\vdom\patch.ts

提示:由于该函数的代码量较大, 所以调整了一下代码结构,方便阅读和理解

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

/**
 * 工厂函数,注入平台特有的一些功能操作,并定义一些方法,然后返回 patch 函数
 */
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  /**
   * modules: { ref, directives, 平台特有的一些操纵,比如 attr、class、style 等 }
   * nodeOps: { 对元素的增删改查 API }
   */
  const { modules, nodeOps } = backend

  /**
   * hooks = ['create', 'activate', 'update', 'remove', 'destroy']
   * 遍历这些钩子,然后从 modules 的各个模块中找到相应的方法,比如:directives 中的 create、update、destroy 方法
   * 让这些方法放到 cb[hook] = [hook 方法] 中,比如: cb.create = [fn1, fn2, ...]
   * 然后在合适的时间调用相应的钩子方法完成对应的操作
   */
  for (i = 0; i < hooks.length; ++i) {
    // 比如 cbs.create = []
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // 遍历各个 modules,找出各个 module 中的 create 方法,然后添加到 cbs.create 数组中
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  /**
   * vm.__patch__
   *   1、新节点不存在,老节点存在,调用 destroy,销毁老节点
   *   2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
   *   3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
   */
  function patch(oldVnode, vnode, hydrating, removeOnly) {
    // 如果新节点不存在,老节点存在,则调用 destroy,销毁老节点
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // 新的 VNode 存在,老的 VNode 不存在,这种情况会在一个组件初次渲染的时候出现,比如:
      // <div id="app"><comp></comp></div>
      // 这里的 comp 组件初次渲染时就会走这儿
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 判断 oldVnode 是否为真实元素
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // 不是真实元素,但是老节点和新节点是同一个节点,则是更新阶段,执行 patch 更新节点
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 是真实元素,则表示初次渲染
        if (isRealElement) {
          // 挂载到真实元素以及处理服务端渲染的情况
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // 走到这儿说明不是服务端渲染,或者 hydration 失败,则根据 oldVnode 创建一个 vnode 节点
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // 拿到老节点的真实元素
        const oldElm = oldVnode.elm
        // 获取老节点的父元素,即 body
        const parentElm = nodeOps.parentNode(oldElm)

        // 基于新 vnode 创建整棵 DOM 树并插入到 body 元素下
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // 递归更新父占位符节点元素
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // 移除老节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

1 销毁节点 - invokeDestroyHook

/**
 * 销毁节点:
 *   执行组件的 destroy 钩子,即执行 $destroy 方法 
 *   执行组件各个模块(style、class、directive 等)的 destroy 方法
 *   如果 vnode 还存在子节点,则递归调用 invokeDestroyHook
 */
function invokeDestroyHook(vnode) {
  let i, j
  const data = vnode.data
  if (isDef(data)) {
    if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode)
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  }
  if (isDef((i = vnode.children))) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j])
    }
  }
}

2 判断两个节点是否相同 - sameVnode

function sameVnode(a, b) {
  return (
    // key 必须相同,需要注意的是 undefined === undefined => true
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory &&
    ((
      // 标签相同
      a.tag === b.tag &&
      // 都是注释节点
      a.isComment === b.isComment &&
      // 都有 data 属性
      isDef(a.data) === isDef(b.data) &&
      // input 标签的情况
      sameInputType(a, b)) ||
      // 异步占位符节点
      (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
  )
}

3 创建空 VNode - emptyNodeAt

/**
 * 为元素(elm)创建一个空的 vnode
 */
function emptyNodeAt(elm) {
  return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}

通过上面的源码我们可以了解到,createPatchFunction中,在新旧VNode经过diff算法,得到最新的VNode,将其渲染到真实DOM上

4 基于 vnode 创建整棵 DOM 树,并插入到父节点上 - createElm

/**
 * 基于 vnode 创建整棵 DOM 树,并插入到父节点上
 */
function createElm(
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  /**
   * 重点
   * 1、如果 vnode 是一个组件,则执行 init 钩子,创建组件实例并挂载,
   *   然后为组件执行各个模块的 create 钩子
   *   如果组件被 keep-alive 包裹,则激活组件
   * 2、如果是一个普通元素,则什么也不错
   */
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  // 获取 data 对象
  const data = vnode.data
  // 所有的孩子节点
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    // 未知标签
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }

    // 创建新节点
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    // 递归创建所有子节点(普通元素、组件)
    createChildren(vnode, children, insertedVnodeQueue)
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    // 将节点插入父节点
    insert(parentElm, vnode.elm, refElm)

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
    // 注释节点,创建注释节点并插入父节点
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    // 文本节点,创建文本节点并插入父节点
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

5 创建组件实例 - createComponent

/**
 * 如果 vnode 是一个组件,则执行 init 钩子,创建组件实例,并挂载
 * 然后为组件执行各个模块的 create 方法
 * @param {*} vnode 组件新的 vnode
 * @param {*} insertedVnodeQueue 数组
 * @param {*} parentElm oldVnode 的父节点
 * @param {*} refElm oldVnode 的下一个兄弟节点
 * @returns 如果 vnode 是一个组件并且组件创建成功,则返回 true,否则返回 undefined
 */
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  // 获取 vnode.data 对象
  let i = vnode.data
  if (isDef(i)) {
    // 验证组件实例是否已经存在 && 被 keep-alive 包裹
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    // 执行 vnode.data.init 钩子函数,该函数在讲 render helper 时讲过
    // 如果是被 keep-alive 包裹的组件:则再执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
    // 如果是组件没有被 keep-alive 包裹或者首次渲染,则初始化组件,并进入挂载阶段
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child
    // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
      // 如果 vnode 是一个子组件,则调用 init 钩子之后会创建一个组件实例,并挂载
      // 这时候就可以给组件执行各个模块的的 create 钩子了
      initComponent(vnode, insertedVnodeQueue)
      // 将组件的 DOM 节点插入到父节点内
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        // 组件被 keep-alive 包裹的情况,激活组件
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

6 向父节点插入节点 - insert

/**
 * 向父节点插入节点 
 */
function insert(parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}

7 移除节点 - removeVnodes

/**
 * 移除指定索引范围(startIdx —— endIdx)内的节点 
 */
function removeVnodes(vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx]
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        removeAndInvokeRemoveHook(ch)
        invokeDestroyHook(ch)
      } else { // Text node
        removeNode(ch.elm)
      }
    }
  }
}

8 更新节点 - patchVnode

/**
 * 更新节点
 *   全量的属性更新
 *   如果新老节点都有孩子,则递归执行 diff
 *   如果新节点有孩子,老节点没孩子,则新增新节点的这些孩子节点
 *   如果老节点有孩子,新节点没孩子,则删除老节点的这些孩子
 *   更新文本节点
 */
function patchVnode(
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 老节点和新节点相同,直接返回
  if (oldVnode === vnode) {
    return
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  const elm = vnode.elm = oldVnode.elm

  // 异步占位符节点
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }

  // 跳过静态节点的更新
  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    // 新旧节点都是静态的而且两个节点的 key 一样,并且新节点被 clone 了 或者 新节点有 v-once指令,则重用这部分节点
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  // 执行组件的 prepatch 钩子
  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }

  // 老节点的孩子
  const oldCh = oldVnode.children
  // 新节点的孩子
  const ch = vnode.children
  // 全量更新新节点的属性,Vue 3.0 在这里做了很多的优化
  if (isDef(data) && isPatchable(vnode)) {
    // 执行新节点所有的属性更新
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  if (isUndef(vnode.text)) {
    // 新节点不是文本节点
    if (isDef(oldCh) && isDef(ch)) {
      // 如果新老节点都有孩子,则递归执行 diff 过程
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      // 老孩子不存在,新孩子存在,则创建这些新孩子节点
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      // 老孩子存在,新孩子不存在,则移除这些老孩子节点
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      // 老节点是文本节点,则将文本内容置空
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    // 新节点是文本节点,则更新文本节点
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}

9 更新子节点 - updateChildren

/**
 * diff 过程:
 *   diff 优化:做了四种假设,假设新老节点开头结尾有相同节点的情况,一旦命中假设,就避免了一次循环,以提高执行效率
 *             如果不幸没有命中假设,则执行遍历,从老节点中找到新开始节点
 *             找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
 *   如果老节点先于新节点遍历结束,则剩余的新节点执行新增节点操作
 *   如果新节点先于老节点遍历结束,则剩余的老节点执行删除操作,移除这些老节点
 */
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  // 老节点的开始索引
  let oldStartIdx = 0
  // 新节点的开始索引
  let newStartIdx = 0
  // 老节点的结束索引
  let oldEndIdx = oldCh.length - 1
  // 第一个老节点
  let oldStartVnode = oldCh[0]
  // 最后一个老节点
  let oldEndVnode = oldCh[oldEndIdx]
  // 新节点的结束索引
  let newEndIdx = newCh.length - 1
  // 第一个新节点
  let newStartVnode = newCh[0]
  // 最后一个新节点
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // removeOnly是一个特殊的标志,仅由 <transition-group> 使用,以确保被移除的元素在离开转换期间保持在正确的相对位置
  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    // 检查新节点的 key 是否重复
    checkDuplicateKeys(newCh)
  }

  // 遍历新老两组节点,只要有一组遍历完(开始索引超过结束索引)则跳出循环
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      // 如果节点被移动,在当前索引上可能不存在,检测这种情况,如果节点不存在则调整索引
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 老开始节点和新开始节点是同一个节点,执行 patch
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      // patch 结束后老开始和新开始的索引分别加 1
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 老结束和新结束是同一个节点,执行 patch
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      // patch 结束后老结束和新结束的索引分别减 1
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      // 老开始和新结束是同一个节点,执行 patch
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      // 处理被 transtion-group 包裹的组件时使用
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      // patch 结束后老开始索引加 1,新结束索引减 1
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      // 老结束和新开始是同一个节点,执行 patch
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      // patch 结束后,老结束的索引减 1,新开始的索引加 1
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      // 如果上面的四种假设都不成立,则通过遍历找到新开始节点在老节点中的位置索引

      // 找到老节点中每个节点 key 和 索引之间的关系映射 => oldKeyToIdx = { key1: idx1, ... }
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      // 在映射中找到新开始节点在老节点中的位置索引
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        // 在老节点中没找到新开始节点,则说明是新创建的元素,执行创建
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        // 在老节点中找到新开始节点了
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          // 如果这两个节点是同一个,则执行 patch
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          // patch 结束后将该老节点置为 undefined
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // 最后这种情况是,找到节点了,但是发现两个节点不是同一个节点,则视为新元素,执行创建
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      // 老节点向后移动一个
      newStartVnode = newCh[++newStartIdx]
    }
  }
  // 走到这里,说明老姐节点或者新节点被遍历完了
  if (oldStartIdx > oldEndIdx) {
    // 说明老节点被遍历完了,新节点有剩余,则说明这部分剩余的节点是新增的节点,然后添加这些节点
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
    // 说明新节点被遍历完了,老节点有剩余,说明这部分的节点被删掉了,则移除这些节点
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}

10 检查key是否重复 - checkDuplicateKeys

/**
 * 检查一组元素的 key 是否重复 
 */
function checkDuplicateKeys(children) {
  const seenKeys = {}
  for (let i = 0; i < children.length; i++) {
    const vnode = children[i]
    const key = vnode.key
    if (isDef(key)) {
      if (seenKeys[key]) {
        warn(
          `Duplicate keys detected: '${key}'. This may cause an update error.`,
          vnode.context
        )
      } else {
        seenKeys[key] = true
      }
    }
  }
}

11 新增节点 - addVnodes

/**
 * 在指定索引范围(startIdx —— endIdx)内添加节点
 */
function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
  for (; startIdx <= endIdx; ++startIdx) {
    createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
  }
}

12 创建指定节点的关系映射 - createKeyToOldIdx

/**
 * 得到指定范围(beginIdx —— endIdx)内节点的 key 和 索引之间的关系映射 => { key1: idx1, ... }
 */
function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

13 找节点位置索引 - findIdxInOld

/**
  * 找到新节点(vnode)在老节点(oldCh)中的位置索引 
  */
function findIdxInOld(node, oldCh, start, end) {
  for (let i = start; i < end; i++) {
    const c = oldCh[i]
    if (isDef(c) && sameVnode(node, c)) return i
  }
}

14 调用 各个模块的 create 方法 - invokeCreateHooks

/**
 * 调用 各个模块的 create 方法,比如创建属性的、创建样式的、指令的等等 ,然后执行组件的 mounted 生命周期方法
 */
function invokeCreateHooks(vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  // 组件钩子
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    // 组件好像没有 create 钩子
    if (isDef(i.create)) i.create(emptyNode, vnode)
    // 调用组件的 insert 钩子,执行组件的 mounted 生命周期方法
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

15 创建子节点们 - createChildren

/**
 * 创建所有子节点,并将子节点插入父节点,形成一棵 DOM 树
 */
function createChildren(vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    // children 是数组,表示是一组节点
    if (process.env.NODE_ENV !== 'production') {
      // 检测这组节点的 key 是否重复
      checkDuplicateKeys(children)
    }
    // 遍历这组节点,依次创建这些节点然后插入父节点,形成一棵 DOM 树
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    // 说明是文本节点,创建文本节点,并插入父节点
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

二、总结

综上所述,new Vue() 的整体过程大致可以划分为四部分,分别是初始化、挂载、生成虚拟节点,生成真实节点,最后调用 mounted 钩子

  1. 初始化:合并配置项,初始化生命周期、事件、datacomputedwatch等等,完成了 beforeCreatecreated 的整个过程
  2. 挂载:如果 options.render 不存在,将 template 编译成 render 函数。接下来就是建立更新机制,创建 watcher,通过 watcher 中的 get 方法,调用组件更新 updateComponent 方法
  3. 生成虚拟DOM:调用更新函数,会调用 vm._render 方法,这个方法会调用 vm.$options.render 方法用于生成虚拟DOM。简单讲,就是执行render函数,生成VNode
  4. 生成真实DOM:通过 vm._render 方法得到虚拟DOM后,会作为 vm._update() 方法的参数去生成 Vnode 相应的真实DOM。简单讲,就是新旧VNode经过diff算法后,渲染到真实DOM上
    • Vue 的 patch 算法的作用:负责首次渲染和后续更新或者销毁组件
      • 如果老的 VNode 是真实元素,则表示首次渲染,创建整棵 DOM 树,并插入 body,然后移除老的模板节点

      • 如果老的 VNode 不是真实元素,并且新的 VNode 也存在,则表示更新阶段,执行 patchVnode

        1)首先是全量更新所有的属性

        2)如果新老 VNode 都有孩子,则递归执行 updateChildren,进行 diff 过程

        针对前端操作 DOM 节点的特点进行如下优化:

        • 同层比较(降低时间复杂度)深度优先(递归)
        • 而且前端很少有完全打乱节点顺序的情况,所以做了四种假设,假设新老 VNode 的开头结尾存在相同节点,一旦命中假设,就避免了一次循环,降低了 diff 的时间复杂度,提高执行效率。如果不幸没有命中假设,则执行遍历,从老的 VNode 中找到新的 VNode 的开始节点
        • 找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
        • 如果老的 VNode 先于新的 VNode 遍历结束,则剩余的新的 VNode 执行新增节点操作
        • 如果新的 VNode 先于老的 VNode 遍历结束,则剩余的老的 VNode 执行删除操纵,移除这些老节点

        3)如果新的 VNode 有孩子,老的 VNode 没孩子,则新增这些新孩子节点

        4)如果老的 VNode 有孩子,新的 VNode 没孩子,则删除这些老孩子节点

        5)剩下一种就是更新文本节点

      • 如果新的 VNode 不存在,老的 VNode 存在,则调用 destroy,销毁老节点

  • 大致流程图

cc7927cf3323476fbceafd7224faa0b9_tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.webp

  • 详细流程图

new Vue() 做了什么?.jpg

资料来源