目標:經過demo理解Vue的實例化過程,學習Vue的代碼組織結構html
Vue (讀音 /vjuː/,相似於 view) 是一套用於構建用戶界面(View層)的漸進式框架 (與React相似) vue
Github star - 用戶數量足夠多(足夠成熟,代碼質量有保證) node
nvm use v7
複製代碼
git clone --branch v2.5.17 git@github.com:vuejs/vue.git
複製代碼
Flow 是 facebook 出品的 JavaScript 靜態類型檢查工具。Vue.js 的源碼利用了 Flow 作了靜態類型檢查,因此瞭解 Flow 有助於咱們閱讀源碼webpack
在 Vue.js 的主目錄下有 .flowconfig 文件, 它是 Flow 的配置文件,感興趣的同窗能夠看官方文檔 這其中的 [libs] 部分用來描述包含指定庫定義的目錄,默認是名爲 flow-typed 的目錄git
// .flowconfig
[libs]
flow
複製代碼
這裏 [libs] 配置的是 flow,表示指定的庫定義都在 flow 文件夾內。咱們打開這個目錄,會發現文件以下:github
flow
├── compiler.js # 編譯相關
├── component.js # 組件數據結構
├── global-api.js # Global API 結構
├── modules.js # 第三方庫定義
├── options.js # 選項相關
├── ssr.js # 服務端渲染相關
├── vnode.js # 虛擬 node 相關
├── weex.js # weex 相關
複製代碼
flow主要有幾個比較明顯的問題:web
React也是使用的flow,可是facebook也發現flow的問題也愈來愈明顯,fb的jest重構也選擇了ts,vue3也選擇了ts,不知道後續React是否會有所動做編程
咱們先只看source目錄下的結構json
src
├── compiler # 編譯相關
├── core # 核心代碼
├── platforms # 不一樣平臺的支持
├── server # 服務端渲染
├── sfc # .vue 文件解析
├── shared # 共享代碼
複製代碼
compiler 目錄包含 Vue.js 全部編譯相關的代碼。它包括把模板解析成 ast 語法樹,ast 語法樹優化,代碼生成等功能api
編譯的工做能夠在構建時作(藉助 webpack、vue-loader 等輔助插件);也能夠在運行時作,使用包含構建功能的 Vue.js。顯然,編譯是一項耗性能的工做,因此更推薦前者——離線編譯
core 目錄包含了 Vue.js 的核心代碼,包括內置組件(transition / slot等)、全局 API 封裝($set),Vue 實例化、觀察者、虛擬 DOM、工具函數等等 這裏的代碼可謂是 Vue.js 的靈魂,也是咱們以後須要重點分析的地方
Vue.js 是一個跨平臺的 MVVM 框架,它能夠跑在 web 上,也能夠配合 weex 跑在 native 客戶端上 platform 是 Vue.js 的入口,2 個目錄表明 2 個主要入口,分別打包成運行在 web 上和 weex 上的 Vue.js
咱們會重點分析 web 入口打包後的 Vue.js,對於 weex 入口打包的 Vue.js,感興趣的同窗能夠研究研究,做爲補充分享
Vue.js 2.x 支持服務端渲染,全部服務端渲染相關的邏輯都在這個目錄下 注意:這部分代碼是跑在服務端的 Node.js,不要和跑在瀏覽器端的 Vue.js 混爲一談
服務端渲染主要的工做是把組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後合成爲客戶端的應用程序
一般咱們開發 Vue.js 都會藉助 webpack 構建, 而後經過 .vue 單文件來編寫組件。 這個目錄下的代碼邏輯會把 .vue 文件內容解析成一個 JavaScript 的對象
Vue.js 會定義一些工具方法,這裏定義的工具方法都是會被瀏覽器端的 Vue.js 和服務端的 Vue.js 所共享的
這樣的目錄設計讓代碼的閱讀性和可維護性都變強,是很是值得學習和推敲的
簡單介紹了下Vue的相關內容後,如今開始講講今天的主題——Vue實例化過程
因爲咱們模板編譯部分在後面纔會講到,因此咱們實例化過程當中只使用render函數進行編寫,這和咱們常見的vue編寫方式不太同樣
var vm = new Vue({
el: '.app',
render: function(createElement) {
return createElement(
'div',
{},
'Hello World'
)
}
})
複製代碼
Vue 的設計雖然沒有徹底遵循 MVVM 模型,可是也受到了它的啓發。所以在文檔中常常會使用 vm (ViewModel 的縮寫) 這個變量名錶示 Vue 實例 (好比ref 屬性雖然爲父組件操做子組件大開了方便之門,可是它繞開了 ViewModel 來訪問 View)
(見DEMO 1)
若是隻是簡單的建立一個Hello World的div,咱們用js會怎麼實現:
寫成代碼應該是這個樣子
function createElement(type, options, text) {
// createElement
const element = document.createElement(type)
// set content
element.innerHTML = text
return element
}
function Vue(options) {
const { el, render } = options
// 1. 找到element
const targetDOM = document.querySelector(el)
// 2. 調用函數生成Element
const targetElement = render.call(this, createElement)
// 3. 替換HTML
targetDOM.outerHTML = targetElement.outerHTML
}
new Vue({
el: '.app',
render: function(createElement) {
return createElement(
'div',
{},
'Hello World'
)
}
})
複製代碼
(DEMO 2)
固然,Vue做爲一個視圖框架,並非只提供了一個簡單渲染div的能力。若是咱們但願在上面加一點細節,好比說加上個 vdom ,應該怎麼加
Vue.js 中 Virtual DOM 是借鑑了一個開源庫 snabbdom 的實現,而後加入了一些 Vue.js 特點的東西,相比於Vue的vdom,它更加簡單和純粹
VNode 是對真實 DOM 的一種抽象描述,它的核心定義無非就幾個關鍵屬性,標籤名、數據、子節點、鍵值等,其它屬性都是用來擴展 VNode 的靈活性以及實現一些特殊 feature 的
因爲 VNode 只是用來映射到真實 DOM 的渲染,不須要包含操做 DOM 的方法,所以它是很是輕量和簡單的
下面是snabbdom的官網例子:
var snabbdom = require('snabbdom');var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var container = document.getElementById('container');
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
' and this is just normal text',
h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
]);// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
' and this is still just normal text',
h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
]);// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
複製代碼
咱們觀察後能夠發現,sanbbdom的createElement(h)函數執行後,返回的並非咱們想象的一個真實的targetElement,而是一個 vnode 。再經過 patch 函數掛載到真實的DOM元素上
Vue內的Virtual DOM 的實現基本相似:
咱們先看看Vue是怎麼生成vnode的
Vue.js 的 createElement 方法,定義在 src/core/vdom/create-elemenet.js 中
// wrapper function for providing a more flexible interface
// without getting yelled at by flowexport
function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
// normalizationType 表示子節點規範的類型,類型不一樣規範的方法也就不同,它主要是參考 render 函數是編譯生成的仍是用戶手寫的
return _createElement(context, tag, data, children, normalizationType)
}
複製代碼
這裏實際調用的是_createElement函數,_createElement 方法有 5 個參數:
createElement 函數的主要作了兩個工做,咱們主要分析第二個:
下列代碼摘選自_createElement 方法,定義在 src/core/vdom/create-elemenet.js 中
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
複製代碼
對於 createComponent 建立組件類型的 VNode 的過程,咱們在以後的組件化分享部分會詳細介紹,這裏先直接跳過
咱們已經知道 Vue 是如何建立了一個 VNode,接下來就是要把這個 VNode 渲染成一個真實的 DOM 並渲染出來,這個過程是經過 _update函數 完成的,它被調用的時機有 2 個,一個是首次渲染,一個是數據更新的時候
咱們這一次只分析首次渲染部分,數據更新部分會在以後分析響應式原理的時候涉及接下來分析一下這個過程
_update 方法定義在 src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
// ...
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
// no need for the ref nodes after initial patch
// this prevents keeping a detached DOM tree in memory (#5851)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
// ...
}
複製代碼
_update 的核心就是調用 vm.patch 方法,這個方法實際上在不一樣的平臺,好比 web 和 weex 上的定義是不同的,在瀏覽器非服務端渲染狀況下,他會指向到
src/platforms/web/runtime/patch.js
/* @flow */
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
該方法的定義是調用 createPatchFunction 方法的返回值,這裏傳入了一個對象,包含 nodeOps 參數和 modules 參數
其中,nodeOps 封裝了一系列 DOM 操做的方法,modules 定義了一些模塊的鉤子函數的實現,咱們這裏先不詳細介紹,來看一下 createPatchFunction 的實現,它定義在 src/core/vdom/patch.js 中
export function createPatchFunction (backend) {
// ...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// create diff patch
// ...
}
}
複製代碼
createPatchFunction 內部定義了一系列的輔助方法,最終返回了一個 patch 方法,這個方法就賦值給了 vm._update 函數裏調用的 vm.patch
在介紹 patch 的方法實現以前,咱們能夠思考一下
爲什麼 Vue.js 源碼繞了這麼一大圈,把相關代碼分散到各個目錄
由於前面介紹過,patch 是平臺相關的,在 Web 和 Weex 環境,它們把虛擬 DOM 映射到 「平臺 DOM」 的方法是不一樣的,而且對 「DOM」 包括的屬性模塊建立和更新也不盡相同。所以每一個平臺都有各自的 nodeOps 和 modules,它們的代碼須要託管在 src/platforms 這個大目錄下
而不一樣平臺的 patch 的主要邏輯部分是相同的,因此這部分公共的部分託管在 core 這個大目錄下。差別化部分只須要經過參數來區別,這裏用到了一個函數柯里化的技巧,經過 createPatchFunction 把差別化參數提早固化,這樣不用每次調用 patch 的時候都傳遞 nodeOps 和 modules 了,這種編程技巧也很是值得學習
在計算機科學中,柯里化(英語:Currying),又譯爲卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術
在這裏,nodeOps 表示對 「平臺 DOM」 的一些操做方法,modules 表示平臺的一些模塊,它們會在整個 patch 過程的不一樣階段執行相應的鉤子函數,這些代碼的具體實現會在以後的分享介紹
回到 patch 方法自己,它接收 4個參數:
patch 的邏輯看上去相對複雜,由於它有着很是多的分支邏輯,爲了方便理解,咱們並不會在這裏介紹全部的邏輯,僅會針對咱們以前的例子分析它的執行邏輯
結合咱們的例子,咱們的場景是首次渲染,因此在執行 patch 函數的時候,傳入的 vm.el = vm.patch(vm.$el, vnode, hydrating, false /* removeOnly */)
在__patch__函數內,調用了createElm。 createElm 的做用是經過虛擬節點建立真實的 DOM 並插入到它的父節點中,而後還有一些相似遍歷生成子節點的過程,這裏就不詳細介紹了
最後在patch內,將以前的node替換。那麼至此咱們從主線上把模板和數據如何渲染成最終的 DOM 的過程分析完畢了,那咱們能夠經過代碼實現一遍基本的邏輯
(demo3)
DEBUG DEMO1