Vue.js原始碼解析-Vue初始化流程

駱三瘋發表於2021-06-17

前言

距離上一篇博文的更新時間已經過了一個月,一則是除了開發自己的專案之外,還臨時接手了其他同事的開發任務,大家都懂得。二則是寫這篇博文需要準備的東西著實不少,除錯、畫圖、總結、整理筆記,需要花不少時間和精力。當然本著擼穿原始碼的目標,還是堅持整理完成,就先個自己打個call吧,O(∩_∩)O哈哈~。

1. 初始化流程概述圖、程式碼流程圖

1.1 初始化流程概述

通過debug的方式(如何準備原始碼除錯環境,大家可以參考我之前寫的 這篇文章)總結了Vue從初始化到遞迴建立元素的大致流程:定義Vue建構函式/全域性API/例項的屬性方法 -> new Vue() -> init() -> mountComponent -> render -> patch -> createElm-> createComponent -> createChildren -> insert,整理的流程概述圖如下。

1.2 初始化程式碼執行流程圖

下圖程式碼的執行邏輯,表示Vue從初始化到DOM渲染的一個流程。之前看原始碼的時候,主要是用到什麼API,再去看與這個API相關的邏輯。但是這樣看程式碼缺少系統性,不利於總結和複習。所以就一邊寫demo,一邊斷點,畫出大概的程式碼執行流程,雖然不是很完善,但至少能有個匯流排。等到要看其他功能程式碼的時候,可以在此基礎上進行擴充套件,同時也便於程式碼定位和邏輯的梳理。

2. 初始化相關程式碼分析

2.1 initGlobalAPI(Vue) 初始化Vue的全域性靜態API

平時開發通過 new Vue({...}) 去建立了根例項,當然在此之前,Vue已經做了一些前期的準備工作。Vue 的核心程式碼都在 src/core 目錄中,我們先來看看 core/index.js 這個入口檔案,這部分程式碼邏輯很簡單。

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 初始化全域性API
initGlobalAPI(Vue)

// 下面程式碼是服務端ssr渲染使用,web端可以忽略
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

// 新增 vue 版本號這個靜態變數
Vue.version = '__VERSION__'

export default Vue

我們主要關注的 initGlobalAPI(Vue) 這個函式,它定義在 core/global-api/index.js 檔案中,主要給建構函式,新增諸如 Vue.set/delete/use/mixin/extend/component/directive/filter 這些靜態方法。

/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  // 這個是給 Vue 設定的 config 屬性,不要手動的去替換這個物件,
  // 如果替換,vue 會給 warn 提示
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)
  
  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  // Vue的靜態方法: Vue.set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
  
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
  // 用於標識 Weex 多例項場景中,通過“base”標識普通物件元件的建構函式。
  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue
  
  extend(Vue.options.components, builtInComponents)
  // Vue的靜態方法: Vue.use/mixin/extend
  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  // Vue的靜態屬性方法:Vue.component/directive/filter
  initAssetRegisters(Vue)
}

其中 initAssetRegisters(Vue),通過靜態變數陣列 [ 'component', 'directive','filter'] 遍歷建立了Vue.component/directive/filter 這三個靜態屬性方法。 靜態變數配置在 src/shared/constants.js 檔案中,方法定義在 core/global-api/assets.js 檔案中。

export const SSR_ATTR = 'data-server-rendered'
// 註冊全域性API時候使用
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
// 生命週期函式使用
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]
/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => {
    // Vue.comoponent/directive/filter 靜態方法的繫結
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

2.2 定義Vue建構函式、例項方法

Vue 這個建構函式,定義在 core/instance/index.js 檔案中。從程式碼中可以看到,用工廠模式,執行不同的混入函式,對 Vue.prototype 原型進行加工,給例項新增對應的屬性方法。

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'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 建構函式中執行 Vue.prototype._init 方法
  this._init(options)  
}

// 例項初始化方法: Vue.prototype._init 
initMixin(Vue) 

// 例項資料狀態相關方法: Vue.prototype.$data/$props/$set/$delete,$watch
stateMixin(Vue)

// 例項事件相關方法: Vue.prototype.$on/$once/$off/$emit
eventsMixin(Vue)

// 例項生命週期相關方法:Vue.prototype._update/$forceUpdate/$destory
lifecycleMixin(Vue)

// 例項渲染相關方法:Vue.prototype.$nextTick/_render
renderMixin(Vue)

export default Vue

2.3 new Vue(options)

執行 new Vue() 建立元件例項,同時 this._init(options) 初始化方法被執行,合併使用者配置、初始化週期、事件、資料、屬性等。

new Vue({
    data: {...},
    props: {...},
    methods: {...},
    computed: {...}
    ...
})

這部分處理邏輯在 core/instance/indexjs 檔案中,與 _init() 相關的主要看 initMixin 這個函式。

/* @flow */

import config from '../config'
import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { mark, measure } from '../util/perf'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
import { extend, mergeOptions, formatComponentName } from '../util/index'

let uid = 0

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge 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)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self  // 丟擲vue例項本身
    vm._self = vm
    
    // 初始化屬性:vm.$parent/$root/$children/$refs
    initLifecycle(vm)
    
    // 初始化父元件傳入的 _parentListeners 事件。
    initEvents(vm)
    
    // 初始化render相關:vm.$slot/scopedSlots/_c/$createElement
    initRender(vm)
    
    // 呼叫生命鉤子 beforeCreate
    callHook(vm, 'beforeCreate')
    
    // 在data/props之前解析注入
    initInjections(vm) // resolve injections before data/props 
    
    // 初始化相關使用者配置的資料響應式:vm._props/_data, 以及computed、watch、methods
    initState(vm)
    
    // 在 data/props 之後提供資料
    initProvide(vm) // resolve provide after data/props 
    
    // 呼叫生命鉤子 created
    callHook(vm, 'created') 

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
......

2.4 執行 $mount 進行掛載

執行 $mount 掛載,目錄是為了生成 vnode,進而轉換為真實DOM執行更新。 $mount 方法在 web 端相關兩個 src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js 構建檔案中都有定義。我們這裡分析 entry-runtime-with-compiler.js 帶 compiler 版本的入口檔案。關於 Vue scripts 指令碼構建相關的內容,大家可以參考我之前寫的 這篇文章 的第2章節。

entry-runtime-with-compiler.js 版本,是在 src/platform/web/runtime/index.js 版本的基礎上,加 compiler 相關的功能邏輯。它首先儲存 runtime 版本的 mount = Vue.prototype.$mount 方法。再重寫 Vue.prototype.$mount 方法。如果使用者傳入 template 模板,就通過編譯,轉換成 render 函式。最後通過先前儲存的 mount 方法進行掛載。下面我們在再來複習一下這個 $mount 實現邏輯。

......
// 1. 儲存 runtime 版本 Vue.prototype 上的 $mount 方法
const mount = Vue.prototype.$mount

// 2. 重寫 Vue.prototype 上的 $mount(加上 compiler 相關功能邏輯) 
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  // 處理 options 配置
  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }

    // 3. 存在 template 選項內容,就進行編譯。
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      // 編譯獲取 render 函式
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  
  // 4. 編譯結束,呼叫 runtime 版本的 $mount 方法進行掛載
  return mount.call(this, el, hydrating)
}
......

最後,程式碼執行 mount.call(this, el, hydrating)。實際上覆用了 runtime/index.js 中的定義的 $mount 公共方法,程式碼註釋如下。

/* @flow */

import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'

import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

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

// 定義了公共的 $mount 方法
// public mount method 
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// devtools global hook
/* istanbul ignore next */

....

export default Vue

公共 $mount 方法實際上呼叫了 mountComponent 函式,它 core/instance/lifecycle.js 檔案中定義,在mountComponent 函式中,例項化一個渲染Watcher,此時 Watcher 內部邏輯中呼叫定義的 updateComponent 函式。updateComponent 被呼叫, vm._render 執行生成 vnode,最終呼叫 _update 將 vnode 更新成 DOM,程式碼註釋如下。

...
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* 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')

  let updateComponent
  /* istanbul ignore if */  // web端可以忽略
  if (process.env.NODE_ENV !== 'production' && 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方法,渲染 watcher 內部會呼叫。
    // 如果 updateComponent 被呼叫,render 方法先執行,生成 vnode。
    // 最後執行 _update 方法,進行DOM更新,new Vue() 走的是建立DOM邏輯。
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // 初始化渲染 watcher,內部邏輯會呼叫 updateComponent。
  // 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
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  
  // 如果 vm.$vnode === null 當前 vm 的父 vnode 為null。
  // 即判斷 vm 當前例項為 Vue 的根例項.
  // vm.$vnode 在上面的 updateChildComponent 方法中有的定義 vm.$vnode = parentVnode 
  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true  // 標記該Vue根例項掛載結束
    callHook(vm, 'mounted')  // 執行鉤子 mounted。
  }
  return vm
}
...

2.5 執行 _render 生成 vnode

vm._render 方法在之前的內容中有提到,它定義 instance/index.js 檔案中,它是在 Vue 建構函式定義的時候,給Vue新增的例項方法。

具體邏輯在 src/core/instance/render.js 檔案中。其他程式碼邏輯可以先不關注,主要關注,vnode = render.call(vm._renderProxy, vm.$createElement) 這部分呼叫。

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  // 給例項初始化render方法
  Vue.prototype._render = function (): VNode {
    ...  
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      // 呼叫使用者定義 render 函式生成vnode
      vnode = render.call(vm._renderProxy, vm.$createElement)  
    } 
    ...
    return vnode
  }
}

render.call 執行,傳入了 vm.$createElement,這裡就是使用者可以通過手寫 render 函式,用來生成 vnode 的實現。示例如下,其中 h 就是 vm.$createElement。

<div id="app">
  {{title}}
</div>

<script>
  window.app = new Vue({
    data: {
      title: 'vue render'
    },
    // 手寫 render 函式,h === vm.$createElement
    render(h) {
      return h(
        'div',
        {
          attrs: {
            id: 'demo'
          }
        },
        this.title
      );
    }
  }).$mount('#app');
</script>

vm.$createElement 方法會在例項 _init() 初始化階段,通過執行 initRender 函式新增。

initRender 方法定義在 src/core/instance/render.js 檔案中,可以看到 vm._cvm.$createElement 方法最終都是執行 createElement 來生成 vnode。vm._c 是 例項內部方法來建立 vnode,vm.$createElement 是使用者手寫 render 函式來建立 vnode,程式碼註釋如下。

export function initRender (vm: Component) {
  ...  
  // vue 內部 render 函式呼叫
  // 它就是 template 編譯生成 render 函式中使用的 vm._c
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  
  // normalization is always applied for the public version, used in
  // user-written render functions. 
  // 使用者手寫的 render 函式呼叫(上面例子中的 h 函式會被執行)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  ...
}

2.5 執行update將vnode轉化為真實DOM

上節內容中介紹了 Vue 在 $mount 方法執行掛載的時候,vm._update 方法中的 vm.render() 執行生成 vnode,下面繼續分析這個 vm._update 方法。

vm._update 這個方法也定義在 src/core/instance/lifecycle.js 檔案中,內部通過 prevVnode 這個條件判斷,執行不同引數的 patch 方法,來選擇是初始化操作或還是更新操作。本章內容是執行初始化,所以 vm.\(el = vm.__patch__(vm.\)el, vnode, hydrating, false /* removeOnly */) 這個方法會被執行,建立DOM。

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    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
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }
  ...
}

關於 update 後面的流程,簡單來說,就是通過遍歷子vnode,遞迴建立DOM子節點,再插入到父節點的邏輯,它實現方式也蠻有意思的,我會在下一篇博文中對這部分程式碼做分析。

3. 程式碼除錯

demo示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>init</title>
    <script src="../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <div>{{title}}</div>
      文字
    </div>
    <script>
      window.app = new Vue({
        // el: "#app",
        data: {
          title: '初始化'
        }
      })
      .$mount('#app');
    </script>
  </body>
</html>

debug:找到斷點入口

當vue.js 被載入。dev 環境下,通過 --sourcemap 配置,生成vue.js.map 檔案對原始碼進行定位,通過入口檔案entry-runtime-with-compiler.js,知道不同 index.js 入口檔案的引用關係如下。
entry-runtime-with-compiler.js 
⬆
web/runtime/index.js 
⬆
src/core/index.js 
⬆
core/instance/index.js。 // 該檔案較上面三個檔案,被最先解析執行

確定斷點位置如下。

debug:新增例項屬性、方法。

在 core/instance/index.js 檔案中斷點如下,在 initMixin(Vue) 之前,Vue 只是一個單純的建構函式。

繼續斷點執行,可以看到Vue.prototype 上新增了相應的例項方法。

debug:新增全域性靜態屬性方法

斷點 core/index.js 檔案執行,可以看到給Vue新增的全域性靜態屬性方法。

debug:new Vue()、_init 初始化

斷點到demo檔案,開始例項化。

step into 進入呼叫的建構函式。斷點 this._init(options) 處。

step into 進入,可以看到此時 vm 例項上還沒有相應的屬性。

執行斷點如下,可以看vm例項上,已經初始化 \(parent、\)slots、_c 等屬性方法。

step over 一直單步執行,直到斷點 $mout 處進行掛載。

debug:$mount 執行掛載

斷點至 entry-runtime-with-compiler.js 檔案的如下位置。此時需要關注 render 函式。通過 compileToFunctions 已經將 template 模板,編譯成了 render 函式,賦值給 this.$options 。 並且通過 return mount.call(this, el, hydrating),將當前例項的 this 引用,通過引數的方式進行傳遞。

生成的 render 函式,可以點選 [[FunctionLocation]] 進行檢視,截圖如下。

單步執行,進入 呼叫 mountComponent。

step into 進入函式呼叫,並且打上斷點。

繼續單步執行可以看到定義了 updateComponent 這個方法。

繼續單步執行到 new Watcher,斷點進入。

debug:例項化渲染watcher

斷點到 this.get() 處,watcher 的依賴收集等其他程式碼邏輯這裡先不關注,主要關注這個this.get() 執行,內部呼叫 this.getter.call(vm, vm),進而執行先前定義的 updateComponent 方法。

step into 進入 updateComponent,打上斷點標記。

debug:render執行生成vnode

如何生成 render 函式的編譯邏輯這裡先不關注,之後的博文中會對compiler內容進行程式碼分析。step over 單步執行一下,讓 vm._render() 執行結束,返回 vnode 作為引數傳遞給 vm._update。

update 執行生成真實DOM

step into 進入 vm._update(vm._render(), hydrating) 方法,它將傳入的 vnode 作為當前例項的_vnode 私有屬性。

step over 單步往下走,執行完 update 之後,可以看到介面中的DOM已近替換完成。

總結

  • Vue 在例項化之前,給原型物件 Vue.prototype 擴充套件了例項的屬性和方法,同時給 Vue 建構函式,擴充套件全域性靜態屬性和方法。
  • 當執行 new Vue() 建立例項,建構函式內部執行 _init 初始化邏輯,給當前例項新增諸如 \(parent、\)slots、_c 等屬性方法。
  • 初始化結束之後,執行 $mount 進行掛載,最終是通過 mountComponent 方法來實現的。
  • mountComponent 重點是給當前例項建立一個渲染Watcher,在 Watcher 的 this.get() 邏輯中會呼叫先前定義的 updateComponent 方法,開始更新。
  • updateComponent 先執行 vm._render 方法生成 vnode,最終呼叫 vm._update 將 vnode 轉化成真實DOM更新檢視。

相關文章