vue2.0和1.0模板渲染的區(qū)別
Vue 2.0 中模板渲染與 Vue 1.0 完全不同,1.0 中采用的 DocumentFragment (想了解可以觀看這篇文章),而 2.0 中借鑒 React 的 Virtual DOM?;?Virtual DOM,2.0 還可以支持服務(wù)端渲染(SSR),也支持 JSX 語法。
真實(shí)DOM存在什么問題,為什么要用虛擬DOM
我們?yōu)槭裁床恢苯邮褂迷?DOM 元素,而是使用真實(shí) DOM 元素的簡(jiǎn)化版 VNode,最大的原因就是 document.createElement 這個(gè)方法創(chuàng)建的真實(shí) DOM 元素會(huì)帶來性能上的損失。我們來看一個(gè) document.createElement 方法的例子
let div = document.createElement('div');
for(let k in div) {
console.log(k);
}
打開 console 運(yùn)行一下上面的代碼,會(huì)發(fā)現(xiàn)打印出來的屬性多達(dá) 228 個(gè),而這些屬性有 90% 多對(duì)我們來說都是無用的。VNode 就是簡(jiǎn)化版的真實(shí) DOM 元素,關(guān)聯(lián)著真實(shí)的dom,比如屬性elm,只包括我們需要的屬性,并新增了一些在 diff 過程中需要使用的屬性,例如 isStatic。
DOM的操作很慢,但是JS確很快的,DOM 樹上的結(jié)構(gòu)、屬性信息我們都可以很容易地用 JavaScript 對(duì)象表示出來,既然我們可以用JS對(duì)象表示DOM結(jié)構(gòu),那么當(dāng)數(shù)據(jù)狀態(tài)發(fā)生變化而需要改變DOM結(jié)構(gòu)時(shí),我們先通過JS對(duì)象表示的虛擬DOM計(jì)算出實(shí)際DOM需要做的最小變動(dòng),反過來,就可以根據(jù)這個(gè)用 JavaScript 對(duì)象表示的樹結(jié)構(gòu)來構(gòu)建一棵真正的DOM樹,操作實(shí)際DOM更新了, 從而避免了粗放式的DOM操作帶來的性能問題。
Virtual DOM算法,簡(jiǎn)單總結(jié)下包括幾個(gè)步驟:
1用JS對(duì)象描述出DOM樹的結(jié)構(gòu),然后在初始化構(gòu)建中,用這個(gè)描述樹去構(gòu)建真正的DOM,并實(shí)際展現(xiàn)到頁面中
2當(dāng)有數(shù)據(jù)狀態(tài)變更時(shí),重新構(gòu)建一個(gè)新的JS的DOM樹,通過新舊對(duì)比DOM數(shù)的變化diff,并記錄兩棵樹差異
3把步驟2中對(duì)應(yīng)的差異通過步驟1重新構(gòu)建真正的DOM,并重新渲染到頁面中,這樣整個(gè)虛擬DOM的操作就完成了,視圖也就更新了
我們看一下 Vue 2.0 源碼中 AST 數(shù)據(jù)結(jié)構(gòu)(其實(shí)就是構(gòu)建vnode的標(biāo)準(zhǔn)) 的定義:
declare type ASTNode = ASTElement | ASTText | ASTExpression
declare type ASTElement = { // 有關(guān)元素的一些定義
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 區(qū)分。
VNode數(shù)據(jù)結(jié)構(gòu)
下面是 Vue 2.0 源碼中 VNode 數(shù)據(jù)結(jié)構(gòu) 的定義 (帶注釋的跟下面介紹的內(nèi)容有關(guān)):
constructor {
this.tag = tag //元素標(biāo)簽
this.data = data //屬性
this.children = children //子元素列表
this.text = text
this.elm = elm //對(duì)應(yīng)的真實(shí) 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 //是否被標(biāo)記為靜態(tài)節(jié)點(diǎn)
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
}
isStatic是否被標(biāo)記為靜態(tài)節(jié)點(diǎn)很重要下面會(huì)講到。
render函數(shù)
這個(gè)函數(shù)是通過編譯模板文件得到的,其運(yùn)行結(jié)果是 VNode。render 函數(shù) 與 JSX 類似,Vue 2.0 中除了 Template 也支持 JSX 的寫法。大家可以使用 Vue.compile(template)方法編譯下面這段模板。
div id="app">
header>
h1>I am a template!/h1>
/header>
p v-if="message">
{{ message }}
/p>
p v-else>
No message.
/p>
/div>
方法會(huì)返回一個(gè)對(duì)象,對(duì)象中有 render 和 staticRenderFns 兩個(gè)值??匆幌律傻?render函數(shù)
(function() {
with(this){
return _c('div',{ //創(chuàng)建一個(gè) div 元素
attrs:{"id":"app"} //div 添加屬性 id
},[
_m(0), //靜態(tài)節(jié)點(diǎn) header,此處對(duì)應(yīng) staticRenderFns 數(shù)組索引為 0 的 render 函數(shù)
_v(" "), //空的文本節(jié)點(diǎn)
(message) //三元表達(dá)式,判斷 message 是否存在
//如果存在,創(chuàng)建 p 元素,元素里面有文本,值為 toString(message)
?_c('p',[_v("\n "+_s(message)+"\n ")])
//如果不存在,創(chuàng)建 p 元素,元素里面有文本,值為 No message.
:_c('p',[_v("\n No message.\n ")])
]
)
}
})
我們可以看到,通過上面的函數(shù)我們將一段html通過函數(shù)生成了,類似jsx語法。
_m(0)是啥意思,可能不好理解,我們稍后會(huì)講解。
要看懂上面的 render函數(shù),只需要了解 _c,_m,_v,_s 這幾個(gè)函數(shù)的定義,其中 _c 是 createElement(創(chuàng)建元素),_m 是 renderStatic(渲染靜態(tài)節(jié)點(diǎn)),_v 是 createTextVNode(創(chuàng)建文本dom),_s 是 toString (轉(zhuǎn)換為字符串)
header是靜態(tài)節(jié)點(diǎn),與vue渲染無關(guān),通過_m(renderStatic)渲染的節(jié)點(diǎn)不會(huì)進(jìn)入diff計(jì)算。
除了 render 函數(shù),還有一個(gè) staticRenderFns 數(shù)組,這個(gè)數(shù)組中的函數(shù)與 VDOM 中的 diff 算法優(yōu)化相關(guān),我們會(huì)在編譯階段給后面不會(huì)發(fā)生變化的 VNode 節(jié)點(diǎn)打上 static 為 true 的標(biāo)簽,那些被標(biāo)記為靜態(tài)節(jié)點(diǎn)的 VNode 就會(huì)單獨(dú)生成 staticRenderFns 函數(shù)
(function() { //上面 render 函數(shù) 中的 _m(0) 會(huì)調(diào)用這個(gè)方法
with(this){
return _c('header',[_c('h1',[_v("I'm a template!")])])
}
})
其實(shí)到現(xiàn)在我們已經(jīng)很清楚,給我們?nèi)我庖粋€(gè)模板template都可以將它構(gòu)建成vnode的形式,這樣就很好區(qū)分,通過render字符串的就是有vue指令屬性的html,而staticFns的則是靜態(tài)節(jié)點(diǎn)。
compile 函數(shù)就是將 template 編譯成 render 函數(shù)的字符串形式。
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
/**
* Compile a template.
*/
export function compile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) // 把template轉(zhuǎn)化為抽象語法樹
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
parse方法位于src/compiler/parser/index.js,大家可以自己去學(xué)習(xí)。
我們來看一下generate函數(shù)如何寫的。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): {
render: string,
staticRenderFns: Array<string>
} {
// save previous staticRenderFns so generate calls can be nested
const prevStaticRenderFns: Array<string> = staticRenderFns
const currentStaticRenderFns: Array<string> = staticRenderFns = []
const prevOnceCount = onceCount
onceCount = 0
currentOptions = options
warn = options.warn || baseWarn
transforms = pluckModuleFunction(options.modules, 'transformCode')
dataGenFns = pluckModuleFunction(options.modules, 'genData')
platformDirectives = options.directives || {}
isPlatformReservedTag = options.isReservedTag || no
const code = ast ? genElement(ast) : '_c("div")'
staticRenderFns = prevStaticRenderFns
onceCount = prevOnceCount
return {
render: `with(this){return ${code}}`,
staticRenderFns: currentStaticRenderFns
}
}
現(xiàn)在一個(gè)前端也要逐漸習(xí)慣ts語法了,我們通過genElement(ast)實(shí)現(xiàn)模板渲染的code。
這個(gè)函數(shù)主要有三個(gè)步驟組成:parse,optimize 和 generate,分別輸出一個(gè)包含 AST,staticRenderFns 的對(duì)象和 render函數(shù) 的字符串。
其中 genElement 函數(shù)(src/compiler/codegen/index.js)是會(huì)根據(jù) AST 的屬性調(diào)用不同的方法生成字符串返回。
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
}
}
- parse 函數(shù),主要功能是將 -template字符串解析成 AST。前面定義了ASTElement的數(shù)據(jù)結(jié)構(gòu),parse 函數(shù)就是將template里的結(jié)構(gòu)(指令,屬性,標(biāo)簽等)轉(zhuǎn)換為AST形式存進(jìn)ASTElement中,最后解析生成AST。
- optimize 函數(shù)(src/compiler/optimizer.js)主要功能就是標(biāo)記靜態(tài)節(jié)點(diǎn),為后面 patch 過程中對(duì)比新舊 VNode 樹形結(jié)構(gòu)做優(yōu)化。被標(biāo)記為 static 的節(jié)點(diǎn)在后面的 diff 算法中會(huì)被直接忽略,不做詳細(xì)的比較。
- generate 函數(shù)(src/compiler/codegen/index.js)主要功能就是根據(jù) AST 結(jié)構(gòu)拼接生成 render 函數(shù)的字符串。
講到這里,大概也知道了vue在減少渲染所作的一些東西。
下面在各詳細(xì)的例子把:

對(duì)應(yīng)的結(jié)構(gòu)是這樣的,這個(gè)可以其實(shí)就是真實(shí)DOM樹的一個(gè)結(jié)構(gòu)映射了:

_v(_s(answer)): {{answer}} 模板語法自制文本。
domProps對(duì)應(yīng)的是:value = 'input'
on:{'input',update} input促發(fā)事件update
數(shù)據(jù)發(fā)現(xiàn)變化后,會(huì)執(zhí)行 Watcher 中的 _update 函數(shù)(src/core/instance/lifecycle.js),_update 函數(shù)會(huì)執(zhí)行這個(gè)渲染函數(shù),輸出一個(gè)新的 VNode 樹形結(jié)構(gòu)的數(shù)據(jù)。然后在調(diào)用 patch 函數(shù),拿這個(gè)新的 VNode 與舊的 VNode 進(jìn)行對(duì)比,只有發(fā)生了變化的節(jié)點(diǎn)才會(huì)被更新到真實(shí) DOM 樹上。
virtualdom 比較
react的diff其實(shí)和vue的diff大同小異。所以這張圖能很好的解釋過程。比較只會(huì)在同層級(jí)進(jìn)行, 不會(huì)跨層級(jí)比較。

diff的過程就是調(diào)用patch函數(shù),就像打補(bǔ)丁一樣修改真實(shí)dom。
function patch (oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
createEle(vnode)
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el)
oldVnode = null
}
}
return vnode
}
patch函數(shù)有兩個(gè)參數(shù),vnode和oldVnode,也就是新舊兩個(gè)虛擬節(jié)點(diǎn)。在這之前,我們先了解完整的vnode都有什么屬性,舉個(gè)一個(gè)簡(jiǎn)單的例子:
// body下的 <div id="v" class="classA"><div> 對(duì)應(yīng)的 oldVnode 就是
{
el: div //對(duì)真實(shí)的節(jié)點(diǎn)的引用,本例中就是document.querySelector('#id.classA')
tagName: 'DIV', //節(jié)點(diǎn)的標(biāo)簽
sel: 'div#v.classA' //節(jié)點(diǎn)的選擇器
data: null, // 一個(gè)存儲(chǔ)節(jié)點(diǎn)屬性的對(duì)象,對(duì)應(yīng)節(jié)點(diǎn)的el[prop]屬性,例如onclick , style
children: [], //存儲(chǔ)子節(jié)點(diǎn)的數(shù)組,每個(gè)子節(jié)點(diǎn)也是vnode結(jié)構(gòu)
text: null, //如果是文本節(jié)點(diǎn),對(duì)應(yīng)文本節(jié)點(diǎn)的textContent,否則為null
}
sameVnode函數(shù)就是看這兩個(gè)節(jié)點(diǎn)是否值得比較,代碼相當(dāng)簡(jiǎn)單:
function sameVnode(oldVnode, vnode){
return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
}
兩個(gè)vnode的key和sel相同才去比較它們,比如p和span,div.classA和div.classB都被認(rèn)為是不同結(jié)構(gòu)而不去比較它們。
當(dāng)節(jié)點(diǎn)不值得比較,進(jìn)入else中
else {
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
createEle(vnode)
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el)
oldVnode = null
}
}
過程如下:
- 取得oldvnode.el的父節(jié)點(diǎn),parentEle是真實(shí)dom
- createEle(vnode)會(huì)為vnode創(chuàng)建它的真實(shí)dom,令vnode.el =真實(shí)dom
- parentEle將新的dom插入,移除舊的dom當(dāng)不值得比較時(shí),新節(jié)點(diǎn)直接把老節(jié)點(diǎn)整個(gè)替換了
最后return node
patch最后會(huì)返回vnode,vnode和進(jìn)入patch之前的不同在哪?
沒錯(cuò),就是vnode.el,唯一的改變就是之前vnode.el = null, 而現(xiàn)在它引用的是對(duì)應(yīng)的真實(shí)dom。
patchVnode
兩個(gè)節(jié)點(diǎn)值得比較時(shí),會(huì)調(diào)用patchVnode函數(shù)
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
1.const el = vnode.el = oldVnode.el 這是很重要的一步,讓vnode.el引用到現(xiàn)在的真實(shí)dom,當(dāng)el修改時(shí),vnode.el會(huì)同步變化。
節(jié)點(diǎn)的比較有5種情況
if (oldVnode === vnode),他們的引用一致,可以認(rèn)為沒有變化。
2.if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本節(jié)點(diǎn)的比較,需要修改,則會(huì)調(diào)用Node.textContent = vnode.text。
3.if( oldCh && ch && oldCh !== ch ), 兩個(gè)節(jié)點(diǎn)都有子節(jié)點(diǎn),而且它們不一樣,這樣我們會(huì)調(diào)用updateChildren函數(shù)比較子節(jié)點(diǎn),這是diff的核心,后邊會(huì)講到。
4.else if (ch),只有新的節(jié)點(diǎn)有子節(jié)點(diǎn),調(diào)用createEle(vnode),vnode.el已經(jīng)引用了老的dom節(jié)點(diǎn),createEle函數(shù)會(huì)在老dom節(jié)點(diǎn)上添加子節(jié)點(diǎn)。
5.else if (oldCh),新節(jié)點(diǎn)沒有子節(jié)點(diǎn),老節(jié)點(diǎn)有子節(jié)點(diǎn),直接刪除老節(jié)點(diǎn)。
今天的講解就到這,相信大家對(duì)vue的模板渲染機(jī)制和vnode diff計(jì)算有了一定了解。