見證奇蹟-Vue原始碼全面揭祕

安穩.發表於2019-03-21

Created By JishuBao on 2019-03-21 19:38:22
Recently revised in 2019-03-21 19:38:22

開篇題外話:

  本文是根據某網的視訊教程“轉譯”過來的文字版,附上自己的原始碼理解與具體程式碼實現,偷偷告訴大家,價值好幾百哦!自己也是為了記錄下來方便日後自己學習,希望被發現了不要告我侵權啥的,人家還是個孩子嘛!

目錄

1. 準備工作

一、準備工作

1.文章總覽

  在深入瞭解這篇文章之前,我更希望您熟練使用Vue開發過至少3個以上的專案且JS基礎較好,而不是零基礎學習,因為這篇文章是原始碼解析,如果您沒有使用過甚至瞭解過,可以說對您的幫助不是很大!

  這一章問我們將介紹一些Vue的前置知識如flow原始碼目錄構建方式編譯入口等...

  新建VueSecret資料夾,準備大刀闊斧的接受Vue的薰陶吧!

見證奇蹟-Vue原始碼全面揭祕

2.認識flow

什麼是flow

  Flow 是 facebook 出品的 JavaScript 靜態型別檢查工具。Vue.js 的原始碼利用了 Flow 做了靜態型別檢查,所以瞭解 Flow 有助於我們閱讀原始碼。

為什麼用flow

  JavaScript 是動態型別語言,它的靈活性有目共睹,但是過於靈活的副作用是很容易就寫出非常隱蔽的隱患程式碼,在編譯期甚至看上去都不會報錯,但在執行階段就可能出現各種奇怪的 bug。

  型別檢查是當前動態型別語言的發展趨勢,所謂型別檢查,就是在編譯期儘早發現(由型別錯誤引起的)bug,又不影響程式碼執行(不需要執行時動態檢查型別),使編寫 JavaScript 具有和編寫 Java 等強型別語言相近的體驗。

  專案越複雜就越需要通過工具的手段來保證專案的維護性和增強程式碼的可讀性。 Vue.js 在做 2.0 重構的時候,在 ES2015 的基礎上,除了 ESLint 保證程式碼風格之外,也引入了 Flow 做靜態型別檢查。之所以選擇 Flow,主要是因為 Babel 和 ESLint 都有對應的 Flow 外掛以支援語法,可以完全沿用現有的構建配置,非常小成本的改動就可以擁有靜態型別檢查的能力。

flow的工作方式

  VueSecret資料夾下新建chart-one資料夾存放章節一的原始碼!

見證奇蹟-Vue原始碼全面揭祕

  chart-one資料夾下新建flow資料夾存放章節一的原始碼!

見證奇蹟-Vue原始碼全面揭祕

  請確保你的電腦上安裝過node,並且可以使用npm命令!因為我們需要使用node上的flow相關模組,故執行命令初始化專案!作者本人喜歡使用yarn安裝(npm相同),如有對yarn不瞭解的,歡迎看我的另外一篇文章yarn介紹,安裝好yarn以後執行命令全域性安裝flow

npm install -g flow-bin
複製程式碼

  允許flow命令會發現報錯顯示缺少flow配置檔案

Could not find a .flowconfig in . or any of its parent directories.
See "flow init --help" for more info
複製程式碼

  執行命令生成flow配置檔案

flow init
複製程式碼

見證奇蹟-Vue原始碼全面揭祕

通常型別檢查分為2種方式:型別推斷型別註釋

  • 型別判斷:通過變數的使用上下文來推斷出變數型別,然後根據這些推斷來檢查型別。

它不需要任何程式碼修改即可進行型別檢查,最小化開發者的工作量。它不會強制你改變開發習慣,因為它會自動推斷出變數的型別。這就是所謂的型別推斷,Flow 最重要的特性之一。

  flow目錄下新建index.js檔案寫下以下程式碼!

需要注意的是頭部需要使用/*@flow*/進行標記,否則他會跳過檢查

/*@flow*/

function split(str){
    return str.split(' ');
}

split(11)
複製程式碼

  flow目錄執行flow會報錯,因為函式 split 期待的引數是字串,而我們輸入了數字。

Error ---------------------------------------------------------------------------------------------------- index.js:4:12

Cannot call `str.split` because property `split` is missing in `Number` [1].

   index.js:4:12
   4|     return str.split(' ');
                 ^^^^^^^^^^^^^^

References:
   index.js:7:7
   7| split(11)
            ^^ [1]



Found 1 error
複製程式碼
  • 型別註釋:事先註釋好我們期待的型別,Flow 會基於這些註釋來判斷。

  如上所述,型別推斷是 Flow 最有用的特性之一,不需要編寫型別註釋就能獲取有用的反饋。但在某些特定的場景下,新增型別註釋可以提供更好更明確的檢查依據。

  新建index2.js檔案

function split(str){
    return str.split(' ');
}

split(11)
複製程式碼

  Flow 檢查上述程式碼時檢查不出任何錯誤,因為從語法層面考慮, + 即可以用在字串上,也可以用在數字上,我們並沒有明確指出 add() 的引數必須為數字

  在這種情況下,我們可以藉助型別註釋來指明期望的型別。型別註釋是以冒號 : 開頭,可以在函式引數,返回值,變數宣告中使用。

  新建index3.js檔案

/*@flow*/

function add(x: number, y: number): number {
    return x + y
}
add('Hello',11);
複製程式碼

  flow目錄執行flow會報錯,因為函式引數的期待型別為數字,而我們提供了字串。

  上面的例子是針對函式的型別註釋。接下來我們來看看 Flow 能支援的一些常見的型別註釋。

  • 陣列
/*@flow*/

var arr: Array<number> = [1, 2, 3]

arr.push('Hello')
複製程式碼

  陣列型別註釋的格式是 Array,T 表示陣列中每項的資料型別。在上述程式碼中,arr 是每項均為數字的陣列。如果我們給這個陣列新增了一個字串,Flow 能檢查出錯誤。

  • 類和物件
/*@flow*/

class Bar {
    x: string;           // x 是字串
    y: string | number;  // y 可以是字串或者數字
    z: boolean;
  
    constructor(x: string, y: string | number) {
      this.x = x
      this.y = y
      this.z = false
    }
  }
  
  var bar: Bar = new Bar('hello', 4)

  var obj: { a: string, b: number, c: Array<string>, d: Bar } = {
    a: 'hello',
    b: 11,
    c: ['hello', 'world'],
    d: new Bar('hello', 3)
  }hello', 4)
複製程式碼

  類的型別註釋格式如上,可以對類自身的屬性做型別檢查,也可以對建構函式的引數做型別檢查。這裡需要注意的是,屬性 y 的型別中間用 | 做間隔,表示 y 的型別即可以是字串也可以是數字。

  物件的註釋型別類似於類,需要指定物件屬性的型別。

  • Null

  若想任意型別 T 可以為 null 或者 undefined,只需類似如下寫成 ?T 的格式即可。

/*@flow*/

var foo: ?string = null
複製程式碼

  此時,foo 可以為字串,也可以為 null。

  • 更多型別見官方文件

flow在vue.js原始碼中的應用

  有時候我們想引用第三方庫,或者自定義一些型別,但 Flow 並不認識,因此檢查的時候會報錯。為了解決這類問題,Flow 提出了一個 libdef 的概念,可以用來識別這些第三方庫或者是自定義型別,而 Vue.js 也利用了這一特性。

  在 Vue.js 的主目錄下有 .flowconfig 檔案, 它是 Flow 的配置檔案,感興趣的同學可以看官方文件。這其中的 [libs] 部分用來描述包含指定庫定義的目錄,預設是名為 flow-typed 的目錄。

  這裡 [libs] 配置的是 flow,表示指定的庫定義都在 flow 資料夾內。我們開啟這個目錄,會發現檔案如下:

flow
├── compiler.js        # 編譯相關
├── component.js       # 元件資料結構
├── global-api.js      # Global API 結構
├── modules.js         # 第三方庫定義
├── options.js         # 選項相關
├── ssr.js             # 服務端渲染相關
├── vnode.js           # 虛擬 node 相關
複製程式碼

  可以看到,Vue.js 有很多自定義型別的定義,在閱讀原始碼的時候,如果遇到某個型別並想了解它完整的資料結構的時候,可以回來翻閱這些資料結構的定義。

  通過對 Flow 的認識,有助於我們閱讀 Vue 的原始碼,並且這種靜態型別檢查的方式非常有利於大型專案原始碼的開發和維護。類似 Flow 的工具還有如 TypeScript,感興趣的同學也可以自行去了解一下。

3.原始碼目錄設計

Vue.js 原始碼目錄設計

Vue.js 的原始碼都在 src 目錄下,其目錄結構如下。

src
├── compiler        # 編譯相關 
├── core            # 核心程式碼 
├── platforms       # 不同平臺的支援
├── server          # 服務端渲染
├── sfc             # .vue 檔案解析
├── shared          # 共享程式碼
複製程式碼

compiler

compiler 目錄包含 Vue.js 所有編譯相關的程式碼。它包括把模板解析成 ast 語法樹,ast 語法樹優化,程式碼生成等功能。

編譯的工作可以在構建時做(藉助 webpack、vue-loader 等輔助外掛);也可以在執行時做,使用包含構建功能的 Vue.js。顯然,編譯是一項耗效能的工作,所以更推薦前者——離線編譯。

core

core 目錄包含了 Vue.js 的核心程式碼,包括內建元件、全域性 API 封裝,Vue 例項化、觀察者、虛擬 DOM、工具函式等等。

這裡的程式碼可謂是 Vue.js 的靈魂,也是我們之後需要重點分析的地方。

platform

Vue.js 是一個跨平臺的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 natvie 客戶端上。platform 是 Vue.js 的入口,2 個目錄代表 2 個主要入口,分別打包成執行在 web 上和 weex 上的 Vue.js。

我們會重點分析 web 入口打包後的 Vue.js,對於 weex 入口打包的 Vue.js,感興趣的同學可以自行研究。

server

Vue.js 2.0 支援了服務端渲染,所有服務端渲染相關的邏輯都在這個目錄下。注意:這部分程式碼是跑在服務端的 Node.js,不要和跑在瀏覽器端的 Vue.js 混為一談。

服務端渲染主要的工作是把元件渲染為伺服器端的 HTML 字串,將它們直接傳送到瀏覽器,最後將靜態標記"混合"為客戶端上完全互動的應用程式。

sfc

通常我們開發 Vue.js 都會藉助 webpack 構建, 然後通過 .vue 單檔案的編寫元件。

這個目錄下的程式碼邏輯會把 .vue 檔案內容解析成一個 JavaScript 的物件。

shared

Vue.js 會定義一些工具方法,這裡定義的工具方法都是會被瀏覽器端的 Vue.js 和服務端的 Vue.js 所共享的。

從 Vue.js 的目錄設計可以看到,作者把功能模組拆分的非常清楚,相關的邏輯放在一個獨立的目錄下維護,並且把複用的程式碼也抽成一個獨立目錄。 這樣的目錄設計讓程式碼的閱讀性和可維護性都變強,是非常值得學習和推敲的。

4.Vue.js原始碼構建

Vue.js 原始碼是基於Rollup構建的,它的構建相關配置都在 scripts 目錄下。

構建指令碼

通常一個基於 NPM 託管的專案都會有一個 package.json 檔案,它是對專案的描述檔案,它的內容實際上是一個標準的 JSON 物件。 我們通常會配置 script 欄位作為 NPM 的執行指令碼,Vue.js 原始碼構建的指令碼如下:

{
  "script": {
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build --weex"
  }
}
複製程式碼

這裡總共有 3 條命令,作用都是構建 Vue.js,後面 2 條是在第一條命令的基礎上,新增一些環境引數。

當在命令列執行 npm run build 的時候,實際上就會執行 node scripts/build.js,接下來我們來看看它實際是怎麼構建的。

構建過程

我們對於構建過程分析是基於原始碼的,先開啟構建的入口 JS 檔案,在 scripts/build.js 中:

let builds = require('./config').getAllBuilds()

// filter builds via command line arg
if (process.argv[2]) {
  const filters = process.argv[2].split(',')
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
  })
} else {
  // filter out weex builds by default
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}

build(builds)
複製程式碼

這段程式碼邏輯非常簡單,先從配置檔案讀取配置,再通過命令列引數對構建配置做過濾,這樣就可以構建出不同用途的 Vue.js 了。接下來我們看一下配置檔案,在 scripts/config.js 中:

const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.js'),
    format: 'cjs',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.js'),
    format: 'cjs',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime only (ES Modules). Used by bundlers that support ES Modules,
  // e.g. Rollup & Webpack 2
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // Runtime+compiler CommonJS build (ES Modules)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.min.js'),
    format: 'umd',
    env: 'production',
    banner
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler production build  (Browser)
  'web-full-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
    format: 'umd',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // ...
}
複製程式碼

這裡列舉了一些 Vue.js 構建的配置,關於還有一些服務端渲染 webpack 外掛以及 weex 的打包配置就不列舉了。

對於單個配置,它是遵循 Rollup 的構建規則的。其中 entry 屬性表示構建的入口 JS 檔案地址,dest 屬性表示構建後的 JS 檔案地址。format 屬性表示構建的格式,cjs 表示構建出來的檔案遵循 CommonJS 規範,es 表示構建出來的檔案遵循 ES Module 規範。 umd 表示構建出來的檔案遵循 UMD 規範。

件遵循 ES Module 規範。 umd 表示構建出來的檔案遵循 UMD 規範。 以 web-runtime-cjs 配置為例,它的 entry 是 resolve('web/entry-runtime.js'),先來看一下 resolve 函式的定義。 原始碼目錄:scripts/config.js

const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}
複製程式碼

這裡的 resolve 函式實現非常簡單,它先把 resolve 函式傳入的引數 p 通過 / 做了分割成陣列,然後取陣列第一個元素設定為 base。在我們這個例子中,引數 p 是 web/entry-runtime.js,那麼 base 則為 web。base 並不是實際的路徑,它的真實路徑藉助了別名的配置,我們來看一下別名配置的程式碼,在 scripts/alias 中:

const path = require('path')

module.exports = {
  vue: path.resolve(__dirname, '../src/platforms/web/entry-runtime-with-compiler'),
  compiler: path.resolve(__dirname, '../src/compiler'),
  core: path.resolve(__dirname, '../src/core'),
  shared: path.resolve(__dirname, '../src/shared'),
  web: path.resolve(__dirname, '../src/platforms/web'),
  weex: path.resolve(__dirname, '../src/platforms/weex'),
  server: path.resolve(__dirname, '../src/server'),
  entries: path.resolve(__dirname, '../src/entries'),
  sfc: path.resolve(__dirname, '../src/sfc')
}
複製程式碼

很顯然,這裡 web 對應的真實的路徑是 path.resolve(__dirname, '../src/platforms/web'),這個路徑就找到了 Vue.js 原始碼的 web 目錄。然後 resolve 函式通過 path.resolve(aliases[base], p.slice(base.length + 1)) 找到了最終路徑,它就是 Vue.js 原始碼 web 目錄下的 entry-runtime.js。因此,web-runtime-cjs 配置對應的入口檔案就找到了。

它經過 Rollup 的構建打包後,最終會在 dist 目錄下生成 vue.runtime.common.js。

Runtime Only VS Runtime+Compiler

通常我們利用 vue-cli 去初始化我們的 Vue.js 專案的時候會詢問我們用 Runtime Only 版本的還是 Runtime+Compiler 版本。下面我們來對比這兩個版本。

  • Runtime Only

我們在使用 Runtime Only 版本的 Vue.js 的時候,通常需要藉助如 webpack 的 vue-loader 工具把 .vue 檔案編譯成 JavaScript,因為是在編譯階段做的,所以它只包含執行時的 Vue.js 程式碼,因此程式碼體積也會更輕量。

  • Runtime+Compiler

我們如果沒有對程式碼做預編譯,但又使用了 Vue 的 template 屬性並傳入一個字串,則需要在客戶端編譯模板,如下所示:

// 需要編譯器的版本
new Vue({
  template: '<div>{{ hi }}</div>'
})

// 這種情況不需要
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})
複製程式碼

因為在 Vue.js 2.0 中,最終渲染都是通過 render 函式,如果寫 template 屬性,則需要編譯成 render 函式,那麼這個編譯過程會發生執行時,所以需要帶有編譯器的版本。

很顯然,這個編譯過程對效能會有一定損耗,所以通常我們更推薦使用 Runtime-Only 的 Vue.js。

通過這一節的分析,我們可以瞭解到 Vue.js 的構建打包過程,也知道了不同作用和功能的 Vue.js 它們對應的入口以及最終編譯生成的 JS 檔案。儘管在實際開發過程中我們會用 Runtime Only 版本開發比較多,但為了分析 Vue 的編譯過程,我們這門課重點分析的原始碼是 Runtime+Compiler 的 Vue.js。

5.從入口開始

我們之前提到過 Vue.js 構建過程,在 web 應用下,我們來分析 Runtime + Compiler 構建出來的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js:

/* @flow */

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
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
  }

  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)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        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')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

export default Vue
複製程式碼

那麼,當我們的程式碼執行 import Vue from 'vue' 的時候,就是從這個入口執行程式碼來初始化 Vue,

那麼 Vue 到底是什麼,它是怎麼初始化的,我們來一探究竟。

Vue的入口

在這個入口 JS 的上方我們可以找到 Vue 的來源:import Vue from './runtime/index',我們先來看一下這塊兒的實現,它定義在 src/platforms/web/runtime/index.js 中:

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, isChrome } 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

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

// ...

export default Vue
複製程式碼

這裡關鍵的程式碼是 import Vue from 'core/index',之後的邏輯都是對 Vue 這個物件做一些擴充套件,可以先不用看,我們來看一下真正初始化 Vue 的地方,在 src/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'

initGlobalAPI(Vue)

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.version = '__VERSION__'

export default Vue
複製程式碼

這裡有 2 處關鍵的程式碼,import Vue from './instance/index' 和 initGlobalAPI(Vue),初始化全域性 Vue API(我們稍後介紹),我們先來看第一部分,在 src/core/instance/index.js 中:

Vue 的定義

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 的廬山真面目,它實際上就是一個用 Function 實現的類,我們只能通過 new Vue 去例項化它。

有些同學看到這不禁想問,為何 Vue 不用 ES6 的 Class 去實現呢?我們往後看這裡有很多 xxxMixin 的函式呼叫,並把 Vue 當引數傳入,它們的功能都是給 Vue 的 prototype 上擴充套件一些方法(這裡具體的細節會在之後的文章介紹,這裡不展開),Vue 按功能把這些擴充套件分散到多個模組中去實現,而不是在一個模組裡實現所有,這種方式是用 Class 難以實現的。這麼做的好處是非常方便程式碼的維護和管理,這種程式設計技巧也非常值得我們去學習。

initGlobalAPI

Vue.js 在整個初始化過程中,除了給它的原型 prototype 上擴充套件方法,還會給 Vue 這個物件本身擴充套件全域性的靜態方法,它的定義在 src/core/global-api/index.js 中:

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  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.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // 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)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}
複製程式碼

這裡就是在 Vue 上擴充套件的一些全域性方法的定義,Vue 官網中關於全域性 API 都可以在這裡找到,這裡不會介紹細節,會在之後的章節我們具體介紹到某個 API 的時候會詳細介紹。有一點要注意的是,Vue.util 暴露的方法最好不要依賴,因為它可能經常會發生變化,是不穩定的。

那麼至此,Vue 的初始化過程基本介紹完畢。這一節的目的是讓同學們對 Vue 是什麼有一個直觀的認識,它本質上就是一個用 Function 實現的 Class,然後它的原型 prototype 以及它本身都擴充套件了一系列的方法和屬性,那麼 Vue 能做什麼,它是怎麼做的,我們會在後面的章節一層層幫大家揭開 Vue 的神祕面紗。

二、資料驅動

1.資料驅動

Vue.js 一個核心思想是資料驅動。所謂資料驅動,是指檢視是由資料驅動生成的,我們對檢視的修改,不會直接操作 DOM,而是通過修改資料。它相比我們傳統的前端開發,如使用 jQuery 等前端庫直接修改 DOM,大大簡化了程式碼量。特別是當互動複雜的時候,只關心資料的修改會讓程式碼的邏輯變的非常清晰,因為 DOM 變成了資料的對映,我們所有的邏輯都是對資料的修改,而不用碰觸 DOM,這樣的程式碼非常利於維護。

在 Vue.js 中我們可以採用簡潔的模板語法來宣告式的將資料渲染為 DOM:

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

最終它會在頁面上渲染出 Hello Vue。接下來,我們會從原始碼角度來分析 Vue 是如何實現的,分析過程會以主線程式碼為主,重要的分支邏輯會放在之後單獨分析。資料驅動還有一部分是資料更新驅動檢視變化,這一塊內容我們也會在之後的章節分析,這一章我們的目標是弄清楚模板和資料如何渲染成最終的 DOM。

2.new Vue發生了什麼

從入口程式碼開始分析,我們先來分析 new Vue 背後發生了哪些事情。我們都知道,new 關鍵字在 Javascript 語言中代表例項化是一個物件,而 Vue 實際上是一個類,類在 Javascript 中是用 Function 來實現的,來看一下原始碼,在src/core/instance/index.js 中。

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 關鍵字初始化,然後會呼叫 this._init 方法, 該方法在 src/core/instance/init.js 中定義。

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)
  }
}
複製程式碼

Vue 初始化主要就幹了幾件事情,合併配置,初始化生命週期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。

Vue 的初始化邏輯寫的非常清楚,把不同的功能邏輯拆成一些單獨的函式執行,讓主線邏輯一目瞭然,這樣的程式設計思想是非常值得借鑑和學習的。

由於我們這一章的目標是弄清楚模板和資料如何渲染成最終的 DOM,所以各種初始化邏輯我們先不看。在初始化的最後,檢測到如果有 el 屬性,則呼叫 vm.$mount 方法掛載 vm,掛載的目標就是把模板渲染成最終的 DOM,那麼接下來我們來分析 Vue 的掛載過程。

相關文章