小白看源碼之Vue篇-1:new Vue()幹了什麼?

前言

距離本小白接觸前端開發也已經快將近一年了,從開始的連閉包的原理都須要瞭解一個晚上(雖然目前深刻其原理我也說不明白)到如今可以本身獨立完成一個簡單的單頁webapp,回顧近一年的時間,也是按照前人留下的足跡,一步一個腳印的走,從html到css再到js再到jQuery庫,再到webpack,直到Vue。一路走來較爲順利,單也有磕絆,也有疑惑。開始接觸Vue的時候,甚爲驚豔!居然有如此之寫法,簡潔,方便。可是使用過程固然難免充滿了疑惑,爲何是這樣的?爲何能這樣寫。在通過近一年的打磨以後,本小白也決定開始研究、研讀Vue源碼,但願在Vue3.0來臨之際,將2.6.X源碼研究一遍,爲日後3.0打基礎,而且將這一年來心中的很多疑惑悉數解開!也但願本文對一樣在Vue道路上的朋友有所幫助~javascript

1、準備工做

磨刀不誤砍柴工,再開始研讀以前,咱們須要瞭解一下Vue的文件的架構以及Vue源碼編寫使用到的靜態類型檢查器FLOW。css

文件架構

咱們從github上面下載最新的vue的源碼後,能夠看到vue的源碼項目文件仍是很是龐大的,和通常的webpack相似,咱們Vue源碼也存在src文件當中。 html

compiler文件夾存放的代碼是與模板解析相關的後續的文章也會專門講到。

core文件就是咱們這一篇要講得的核心也是Vue實例初始化的核心所在。前端

platform文件夾存放的是咱們的vue如何進入到運行時的,區分是經過cli打包的文件仍是直接在html中引用的狀況等。vue

server文件中存放生成render模板編譯的相關代碼,例如咱們的template是如何編譯成render函數,邏輯在這個文件夾中。java

FLOW

FLOW是一個靜態類型檢查器,因爲Vue中存在大量的類,引入flow靜態檢查器來確保類屬性不出錯,用法相似於TypeScript,這裏就很少展開介紹了(實際上是不會)。node

2、new Vue()

一、Class Vue

正文開始了!首先,咱們找到Vue類定義的位置src/core/instance/index.jswebpack

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
複製代碼

在這裏看到Vue類的定義很是"簡單"= =;作了一個判斷防止調用Vue函數,而後就是一個初始化this._init(options)。而_init 就是在initMixin(Vue)是混入到類中的。options就是咱們常常main.js寫的那個傳入的對象:git

new Vue({
  render: h => h(App),
}).$mount('#app')
複製代碼

那麼,接下來 咱們就要看一下initMixin到底對Vue作了什麼,給這個類添加了什麼功能,以及_init()函數到底作了哪些事情。github

二、initMixin()

從上面代碼能夠看到initMixin在同級目錄下init.js文件中,咱們來看一下initMixin到底幹了什麼。

export function initMixin (Vue: Class<Component>) {
    Vue.prototype._init = function (options?: Object) { 
    //some codes
    }
}
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  //some codes
}

export function resolveConstructorOptions (Ctor: Class<Component>) {
  //some codes
}

function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  //some codes
}

複製代碼

好吧整個文件去掉邏輯部分仍是很清晰的,暴露四個方法,能夠看到initMixin就是隻爲Vue添加了一個_init方法,那麼接下來咱們就重點來看一下_init方法,在Vue實例化的過程當中,作了什麼吧~

_init()

首先,先把源碼貼上~

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
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) 
    initProvide(vm) // resolve provide after data/props
    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) //實例的首次掛載
    }
  }
複製代碼

一、mergeOptions

咱們一點點來說,首先判斷咱們傳入的options,根據是否存在options._isComponent來判斷如何處理options。Vue的初始化邏輯固然是走下面的,經過mergeOptions()合併了父類(這裏是指Vue)options和當前options。那麼好奇的童鞋確定要問了,Vue哪來的options??!!這裏賣個關子,後面會講到的~仍是比較重要的一塊,因此留個懸念,也算是加深印象~

二、一系列的initXXX()

能夠看到接下來就是一系列的init初始化,咱們本章大體介紹一下每一個init都是幹啥的,具體展開討論會放到後面專門的文中介紹~首先是initLifecycle:它主要是針對生命週期作一些初始化;initEvents:對vue的監聽事件的初始化;initRender:初始化Vue的render方法,本文會花必定篇幅展開;callhook:beforeCreate:調起預先設定的beforeCreate鉤子函數(沒錯生命週期鉤子函數就在這裏慢慢開始了);initInjections(vm):後代組件injection相關初始化;initState(vm):初始化data、props、methods等屬性,數據雙向綁定的核心所在,以後文章會詳細展開。initProvide(vm):後代組件initProvide相關初始化;callHook(vm, 'created'):調起預先設定的created鉤子函數。

三、vm.$mount(vm.$options.el)

最後一步就是調用vm.$mount()方法進行實例的掛載。咱們能夠看到,若是咱們傳入的屬性若是有el屬性,纔會調用,否則是不會調用的。因此咱們在new Vue是會有兩種狀況:

new Vue({
  render: h => h(App),
}).$mount('#app')//不走_init()的mount本身調用

new Vue({
  el:'#app'//_init直接調用mount
  render: h => h(App),
})
複製代碼

到這裏,_init(options)的流程也就結束了,雖而後面的文章咱們還會無數次提到他。。。接下來咱們就來一塊兒看一下vm.$mount()到底幹了些什麼,以及具體他是如何掛載咱們的Vue實例的。

三、vm.$mount(el)

vm.$mount(el)定義在src/platforms/web/index.js咱們一塊兒來看一下代碼。

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

看來只是一個封裝,咱們繼續去找mountComponent(this, el, hydrating),代碼在src/core/instance/lifecycle.js裏面。咱們看一下代碼:

export function mountComponent ( //vue實例掛載的方法 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
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  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 = () => {
      vm._update(vm._render(), hydrating) //掛載實例,調用
    }
  }

  new Watcher(vm, updateComponent, noop, { 
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */) 
  hydrating = false

  // 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
    callHook(vm, 'mounted')
  }
  return vm
}
複製代碼

咱們一步步看他的邏輯來看他的邏輯。一、!vm.$options.render判斷若是實例不存在render函數,則建立一個空的vnode。二、callHook(vm, 'beforeMount')原來這個鉤子函數在這兒~三、調用了_update()裏面兩個參數,vm._render()建立了一個node節點,接下來咱們會重點展開。

let updateComponent () => {
      vm._update(vm._render(), hydrating) 
    }
複製代碼

四、new Watcher()添加了一個觀察者,這是一個render Watcher監聽數據變化後展開節點渲染。(後面介紹watcher的時候會重點講)目前咱們只要知道watcher在初始化會調用傳入的updateComponent函數,從而觸發渲染和實例掛載。五、callHook(vm, 'mounted'):mounted鉤子函數都不陌生。到這裏vm.$mount函數也就走完了!

3、總結

原本打算把vm._update(vm._render(), hydrating)也放在這裏面講的,可是一想,若是倉促展開,可能有些細節回顧及不到並且vm._update()vm._render()也是比較重要的兩塊一塊涉及patch(確定就會涉及到diff算法)另外一塊涉及到Vnode建立,如今本小白也處於比較沒有思緒的狀況,不知如何展開你們會比較容易接受,須要再整理整理大綱在接着往下來寫~因此就先到這兒吧!在這裏也貼一下目前本小白學習整理的大綱:Vue源碼解讀

最後的最後打個小小的廣告~

有大佬們須要短信業務,或者號碼認證的能力的話,能夠看看這裏!中國聯通創新能力平臺 運營商官方平臺!沒有中間商賺差價~

相關文章
相關標籤/搜索