vue綜合講解

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)映射了:

image.png
_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ì)算有了一定了解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容