vue.js源碼深入解析


第一、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 源碼構建

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(',')
  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 規范。

以 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 則為 webbase 並不是實際的路徑,它的真實路徑借助了別名的配置,我們來看一下別名配置的代碼,在 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 構建過程,在 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 的神秘面紗。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM