初衷
在使用vue2.0的過程,有時看API很難理解vue作者的思想,這促使我想要去深入瞭解vue底層的思想,瞭解完底層的一些思想,才能更好的用活框架,雖然網上已經有很多原始碼解析的文件,但我覺得只有自己動手了,才能更加深印象。
vue2.0和1.0模板渲染的區別
Vue 2.0 中模板渲染與 Vue 1.0 完全不同,1.0 中採用的 DocumentFragment (想了解可以觀看這篇文章),而 2.0 中借鑑 React 的 Virtual DOM。基於 Virtual DOM,2.0 還可以支援服務端渲染(SSR),也支援 JSX 語法。
知識普及
在開始閱讀原始碼之前,先了解一些相關的知識:AST 資料結構,VNode 資料結構,createElement 的問題,render函式。
AST 資料結構
AST 的全稱是 Abstract Syntax Tree(抽象語法樹),是原始碼的抽象語法結構的樹狀表現形式,計算機學科中編譯原理的概念。而vue就是將模板程式碼對映為AST資料結構,進行語法解析。
我們看一下 Vue 2.0 原始碼中 AST 資料結構 的定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
declare type ASTNode = ASTElement | ASTText | ASTExpression declare type ASTElement = { // 有關元素的一些定義 type: 1; tag: string; attrsList: Array{ name: string; value: string }>; attrsMap: { [key: string]: string | null }; parent: ASTElement | void; children: ArrayASTNode>; //...... } declare type ASTExpression = { type: 2; expression: string; text: string; static?: boolean; } declare type ASTText = { type: 3; text: string; static?: boolean; } |
我們看到 ASTNode 有三種形式:ASTElement,ASTText,ASTExpression。用屬性 type 區分。
VNode資料結構
下面是 Vue 2.0 原始碼中 VNode 資料結構 的定義 (帶註釋的跟下面介紹的內容有關):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
constructor { this.tag = tag //元素標籤 this.data = data //屬性 this.children = children //子元素列表 this.text = text this.elm = elm //對應的真實 DOM 元素 this.ns = undefined this.context = context this.functionalContext = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false //是否被標記為靜態節點 this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false } |
真實DOM存在什麼問題,為什麼要用虛擬DOM
我們為什麼不直接使用原生 DOM 元素,而是使用真實 DOM 元素的簡化版 VNode,最大的原因就是 document.createElement
這個方法建立的真實 DOM 元素會帶來效能上的損失。我們來看一個 document.createElement 方法的例子
1 2 3 4 |
let div = document.createElement('div'); for(let k in div) { console.log(k); } |
開啟 console 執行一下上面的程式碼,會發現列印出來的屬性多達 228
個,而這些屬性有 90% 多對我們來說都是無用的。VNode 就是簡化版的真實 DOM 元素,關聯著真實的dom,比如屬性elm,只包括我們需要的屬性,並新增了一些在 diff 過程中需要使用的屬性,例如 isStatic。
render函式
這個函式是通過編譯模板檔案得到的,其執行結果是 VNode。render 函式 與 JSX 類似,Vue 2.0 中除了 Template 也支援 JSX 的寫法。大家可以使用 Vue.compile(template)方法編譯下面這段模板。
1 2 3 4 5 6 7 8 9 10 11 |
div id="app"> header> h1>I am a template!/h1> /header> p v-if="message"> {{ message }} /p> p v-else> No message. /p> /div> |
方法會返回一個物件,物件中有 render 和 staticRenderFns 兩個值。看一下生成的 render函式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(function() { with(this){ return _c('div',{ //建立一個 div 元素 attrs:{"id":"app"} //div 新增屬性 id },[ _m(0), //靜態節點 header,此處對應 staticRenderFns 陣列索引為 0 的 render 函式 _v(" "), //空的文字節點 (message) //三元表示式,判斷 message 是否存在 //如果存在,建立 p 元素,元素裡面有文字,值為 toString(message) ?_c('p',[_v("\n "+_s(message)+"\n ")]) //如果不存在,建立 p 元素,元素裡面有文字,值為 No message. :_c('p',[_v("\n No message.\n ")]) ] ) } }) |
要看懂上面的 render函式,只需要瞭解 _c,_m,_v,_s 這幾個函式的定義,其中 _c 是 createElement
(建立元素),_m 是 renderStatic
(渲染靜態節點),_v 是 createTextVNode
(建立文字dom),_s 是 toString
(轉換為字串)
除了 render 函式,還有一個 staticRenderFns 陣列,這個陣列中的函式與 VDOM 中的 diff 演算法優化相關,我們會在編譯階段給後面不會發生變化的 VNode 節點打上 static 為 true 的標籤,那些被標記為靜態節點的 VNode 就會單獨生成 staticRenderFns 函式
1 2 3 4 5 |
(function() { //上面 render 函式 中的 _m(0) 會呼叫這個方法 with(this){ return _c('header',[_c('h1',[_v("I'm a template!")])]) } }) |
模板渲染過程(重要的函式介紹)
$mount 函式,主要是獲取 template,然後進入 compileToFunctions 函式。
compileToFunctions 函式,主要將 template 編譯成 render 函式。首先讀快取,沒有快取就呼叫 compile 方法拿到 render 函式 的字串形式,再通過 new Function 的方式生成 render 函式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 有快取的話就直接在快取裡面拿 const key = options && options.delimiters ? String(options.delimiters) + template : template if (cache[key]) { return cache[key] } const res = {} const compiled = compile(template, options) // compile 後面會詳細講 res.render = makeFunction(compiled.render) //通過 new Function 的方式生成 render 函式並快取 const l = compiled.staticRenderFns.length res.staticRenderFns = new Array(l) for (let i = 0; i l; i++) { res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i]) } ...... } return (cache[key] = res) // 記錄至快取中 |
compile 函式就是將 template 編譯成 render 函式的字串形式,後面一小節我們會詳細講到。
完成render方法的生成後,會進入 _mount 中進行DOM更新。該方法的核心邏輯如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 觸發 beforeMount 生命週期鉤子 callHook(vm, 'beforeMount') // 重點:新建一個 Watcher 並賦值給 vm._watcher vm._watcher = new Watcher(vm, function updateComponent () { vm._update(vm._render(), hydrating) }, noop) 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 |
首先會new一個watcher物件(主要是將模板與資料建立聯絡),在watcher物件建立後,會執行傳入的方法 vm._update(vm._render(), hydrating)
。其中的vm._render()主要作用就是執行前面compiler生成的render方法,並返回一個vNode物件。vm.update() 則會對比新的 vdom 和當前 vdom,並把差異的部分渲染到真正的 DOM 樹上。
推薦個圖,響應式工程流程
(想深入瞭解watcher的背後實現原理的,可以觀看這篇文章 Vue2.0 原始碼閱讀:響應式原理)
compile
上文中提到 compile 函式就是將 template 編譯成 render 函式 的字串形式。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export function compile ( template: string, options: CompilerOptions ): CompiledResult { const AST = parse(template.trim(), options) //1. parse optimize(AST, options) //2.optimize const code = generate(AST, options) //3.generate return { AST, render: code.render, staticRenderFns: code.staticRenderFns } } |
這個函式主要有三個步驟組成:parse,optimize 和 generate,分別輸出一個包含 AST,staticRenderFns 的物件和 render函式 的字串。
parse 函式,主要功能是將 template字串解析成 AST。前面定義了ASTElement的資料結構,parse 函式就是將template裡的結構(指令,屬性,標籤等)轉換為AST形式存進ASTElement中,最後解析生成AST。
optimize 函式(src/compiler/optimizer.js)主要功能就是標記靜態節點,為後面 patch 過程中對比新舊 VNode 樹形結構做優化。被標記為 static 的節點在後面的 diff 演算法中會被直接忽略,不做詳細的比較。
generate 函式(src/compiler/codegen/index.js)主要功能就是根據 AST 結構拼接生成 render 函式的字串。
1 2 3 4 5 6 7 |
const code = AST ? genElement(AST) : '_c("div")' staticRenderFns = prevStaticRenderFns onceCount = prevOnceCount return { render: `with(this){return ${code}}`, //最外層包一個 with(this) 之後返回 staticRenderFns: currentStaticRenderFns } |
其中 genElement 函式(src/compiler/codegen/index.js)是會根據 AST 的屬性呼叫不同的方法生成字串返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function genElement (el: ASTElement): string { if (el.staticRoot && !el.staticProcessed) { return genStatic(el) } else if (el.once && !el.onceProcessed) { return genOnce(el) } else if (el.for && !el.forProcessed) { return genFor(el) } else if (el.if && !el.ifProcessed) { return genIf(el) } else if (el.tag === 'template' && !el.slotTarget) { return genChildren(el) || 'void 0' } else if (el.tag === 'slot') { } return code } } |
以上就是 compile 函式中三個核心步驟的介紹,compile 之後我們得到了 render 函式 的字串形式,後面通過 new Function 得到真正的渲染函式。資料發現變化後,會執行 Watcher 中的 _update 函式(src/core/instance/lifecycle.js),_update 函式會執行這個渲染函式,輸出一個新的 VNode 樹形結構的資料。然後在呼叫 patch 函式,拿這個新的 VNode 與舊的 VNode 進行對比,只有發生了變化的節點才會被更新到真實 DOM 樹上。
patch
patch.js 就是新舊 VNode 對比的 diff 函式,主要是為了優化dom,通過演算法使操作dom的行為降到最低,diff 演算法來源於 snabbdom
,是 VDOM 思想的核心。snabbdom 的演算法為了 DOM 操作跨層級增刪節點較少的這一目標進行優化,它只會在同層級進行, 不會跨層級比較。
想更加深入VNode diff演算法原理的,可以觀看(解析vue2.0的diff演算法)
總結
- compile 函式主要是將 template 轉換為 AST,優化 AST,再將 AST 轉換為 render函式;
- render函式 與資料通過 Watcher 產生關聯;
- 在資料發生變化時呼叫 patch 函式,執行此 render 函式,生成新 VNode,與舊 VNode 進行 diff,最終更新 DOM 樹。