vue 原始碼學習(二) 例項初始化和掛載過程

黃呼呼發表於2019-01-08

vue 入口

從vue的構建過程可以知道,web環境下,入口檔案在 src/platforms/web/entry-runtime-with-compiler.js(以Runtime + Compiler模式構建,vue直接執行在瀏覽器進行編譯工作)

import Vue from './runtime/index'
複製程式碼

下一步,找到./runtime/index,發現:

import Vue from 'core/index'
複製程式碼

下一步,找到core/index,發現:

import Vue from './instance/index'
複製程式碼

按照這個思路找,最後發現:Vue是在'core/index'下定義的

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')
  }
  this._init(options)
}

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

export default Vue
複製程式碼

引入方法,用function定義了Vue類,再以Vue為引數,呼叫了5個方法,最後匯出了vue

可以進入這5個檔案檢視相關方法,主要就是在Vue原型上掛載方法,可以看到,Vue 是把這5個方法按功能放入不同的模組中,這很利於程式碼的維護和管理

initGlobalAPI

回到core/index.js, 看到除了引入已經在原型上掛載方法後的 Vue 外,還匯入initGlobalAPI 、 isServerRendering、FunctionalRenderContext,執行initGlobalAPI(Vue),在vue.prototype上掛載$isServer、$ssrContext、FunctionalRenderContext,在vue 上掛載 version 屬性,

看到initGlobalAPI的定義,主要是往vue.config、vue.util等上掛載全域性靜態屬性和靜態方法(可直接通過Vue呼叫,而不是例項呼叫),再把builtInComponents 內建元件擴充套件到Vue.options.components下。此處大致瞭解下它是做什麼的即可,後面用到再做具體分析。

new Vue()

一般我們用vue都採用模板語法來宣告:

<div id="app">
  {{ message }}
</div>
複製程式碼
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
複製程式碼

當new Vue()時,vue做了哪些處理?

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')
  }
  this._init(options)
}
複製程式碼

看到vue只能通過new例項化,否則報錯。例項化vue後,執行了this._init(),該方法在通過initMixin(Vue)掛載在Vue原型上的,找到定義檔案core/instance/init.js 檢視該方法。

_init()

一開始在this物件上定義_uid、_isVue,判斷options._isComponent,此次先不考慮options._isComponenttrue的情況,走else,合併options,接著安裝proxy, 初始化生命週期,初始化事件、初始化渲染、初始化data、鉤子函式等,最後判斷有vm.$options.el則執行vm.$mount(),即是把el渲染成最終的DOM

初始化data 資料繫結

_init()中通過initState()來繫結資料到vm上,看下initState的定義:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

複製程式碼

獲取options,初始化props、methods、data、計算屬性、watch繫結到vm上,先來看下initData()是如何把繫結data的:

  • 先判斷data是不是function型別,是則呼叫getData,返回data的自呼叫,不是則直接返回data,並將data賦值到vm._data上

  • 對data、props、methods,作個校驗,防止出現重複的key,因為它們最終都會掛載到vm上,都是通過vm.key來呼叫

  • 通過proxy(vm, `_data`, key)把每個key都掛載在vm

    export function proxy (target: Object, sourceKey: string, key: string) {
      sharedPropertyDefinition.get = function proxyGetter () {
        return this[sourceKey][key]
      }
      sharedPropertyDefinition.set = function proxySetter (val) {
        this[sourceKey][key] = val
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    複製程式碼

    proxy() 定義了一個get/set函式,再通過Object.defineProperty定義\修改屬性(不瞭解Object.defineProperty()的同學可以先看下文件,通過Object.defineProperty()定義的屬性,通過描述符的設定可以進行更精準的控制物件屬性),將對target的key訪問加了一層get/set,即當訪問vm.key時,實際上是呼叫了sharedPropertyDefinition.get,返回this._data.key,這樣就實現了通過vm.key來呼叫vm._data上的屬性

  • 最後,observe(data, true /* asRootData */) 觀察者,對資料作響應式處理,這也是vue的核心之一,此處先不分析

$mount() 例項掛載

Vue的核心思想之一是資料驅動,在vue下,我們不會直接操作DOM,而是通過js修改資料,所有邏輯只需要考慮對資料的修改,最後再把資料渲染成DOM。其中,$mount()就是負責把資料掛載到vm,再渲染成最終DOM

接下來將會分析下vue是如何把javaScript物件渲染成dom元素的,和之前一樣,主要分析主線程式碼

預處理

還是從src/platform/web/entry-runtime-with-compiler.js 檔案入手,

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  ···
}
複製程式碼

首先將原先原型上的$mount方法快取起來,再重新定義$mount

  • 先判斷 elel 不能是 body, html ,因為渲染出來的 DOM最後是會替換掉el
  • 判斷render方法, 有的話直接呼叫mount.call(this, el, hydrating)
  • 沒有render方法時:
  1. 判斷有沒有template ,有則用compileToFunctions將其編譯成render方法
  2. 沒有template時,則檢視有沒有el,有轉換成template,再用compileToFunctions將其編譯成render方法
  3. render掛載到options下
  4. 最後呼叫 mount.call(this, el, hydrating),即是呼叫原先原型上的mount方法

我們發現這一系列呼叫都是為了生成render函式,說明在vue中,所有的元件渲染最終都需要render方法(不管是單檔案.vue還是el\template),vue 文件裡也提到:

Vue 選項中的 render 函式若存在,則 Vue 建構函式不會從 template 選項或通過 el 選項指定的掛載元素中提取出的 HTML 模板編譯渲染函式。

原先原型上的mount方法

找到原先原型上的mount方法,在src/platform/web/runtime/index.js中:

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
複製程式碼

這個是公用的$mount方法,這麼設計使得這個方法可以被 runtime onlyruntime+compiler 版本共同使用

$mount 第一個引數el, 表示掛載的元素,在瀏覽器環境會通過query(el)獲取到dom物件,第二個引數和服務端渲染相關,不進行深入分析,此處不傳。接著呼叫mountComponent()

看下query(),比較簡單,當elstring時,找到該選擇器返回dom物件,否則新建立個div dom物件,el是dom物件直接返回el.

mountComponent

mountComponent定義在src/core/instance/lifecycle.js中,傳入vm,el,

  • el快取在vm.$el

  • 判斷有沒有render方法,沒有則直接把createEmptyVNode作為render函式

  • 開發環境警告(沒有Render但有el/template不能使用runtime-only版本、rendertemplate必須要有一個)

  • 掛載beforeMount鉤子

  • 定義 updateComponent , 渲染相關

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    複製程式碼
  • new Watcher() 例項化一個渲染watcher,簡單看下定義, this.getter = expOrFnupdateComponent掛載到this.getterthis.value = this.lazy ? undefined : this.get()

    get () {
      pushTarget(this)
      let value
      const vm = this.vm
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {...}
      return value
    }
    複製程式碼

    執行this.get(),則執行了this.getter,即updateComponent,所以new Watcher()時會執行updateComponent,也就會執行到vm._update、vm._render方法。

    因為之後不止初始化時需要渲染頁面,資料發生變化時也是要更新到dom上的,例項watcher可以實現對資料進行監聽以及隨後的更新dom處理,watcher會在初始化執行回撥,也會在資料變化時執行回撥,此處先簡單介紹為什麼要使用watcher,不深入分析watcher實現原理。

  • 最後判斷有無根節點,無則表示首次掛載,新增mounted鉤子函式 ,返回vm

總結

例項初始化:new Vue()->掛載方法屬性->this._init->初始化data->$mount

掛載過程:(在complier版本,生成render函式)對el作處理,執行mountComponent,mountComponent中定義了updateComponent,通過例項化watcher的回撥執行updateComponent,執行updateComponent,即呼叫了vm._update、vm._render真實渲染成dom物件。

相關文章