在$mount
的時候,當遇到 Vue 實例傳入的參數不包含 render,而是 template 或者 el 的時候,就會執行編譯的過程,將另外兩個轉變爲 render 函數。vue
在編譯的過程當中,有三個階段:node
本文只針對其中的 optimize 階段進行重點闡述。web
編譯過程首先就是對模板作解析,生成 AST,它是一種抽象語法樹,是對源代碼的抽象語法結構的樹狀表現形式。在不少編譯技術中,如 babel 編譯 ES6 的代碼都會先生成 AST。正則表達式
生成的 AST 是一個樹狀結構,每個節點都是一個 ast element
,除了它自身的一些屬性,還維護了它的父子關係,如 parent
指向它的父節點,children
指向它的全部子節點。算法
parse
的目標是把 template
模板字符串轉換成 AST 樹,它是一種用 JavaScript 對象的形式來描述整個模板。那麼整個 parse
的過程是利用正則表達式順序解析模板,當解析到開始標籤、閉合標籤、文本的時候都會分別執行對應的回調函數,來達到構造 AST 樹的目的。express
AST 元素節點總共有 3 種類型:數組
過 parse
過程後,會輸出生成 AST 樹,那麼接下來咱們須要對這顆樹作優化。爲何須要作優化呢?緩存
由於 Vue 是數據驅動,是響應式的。可是咱們的模板中,並非全部的數據都是響應式的,也有不少的數據在首次渲染以後就永遠不會變化了。既然如此,在咱們執行 patch
的時候就能夠跳過這些非響應式的比對。babel
簡單來講:整個 optimize
的過程實際上就幹 2 件事情,markStatic(root)
標記靜態節點 ,markStaticRoots(root, false)
標記靜態根節點。異步
/** * Goal of the optimizer: walk the generated template AST tree * and detect sub-trees that are purely static, i.e. parts of * the DOM that never needs to change. * * Once we detect these sub-trees, we can: * * 1. Hoist them into constants, so that we no longer need to * create fresh nodes for them on each re-render; * 2. Completely skip them in the patching process. */
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
複製代碼
經過代碼來看,能夠更好解析標記靜態節點的邏輯:
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
複製代碼
代碼解讀:
isStatic()
中咱們看到,isBuiltInTag
(即tag
爲component
和slot
)的節點不會被標註爲靜態節點,isPlatformReservedTag
(即平臺原生標籤,web 端如 h1 、div標籤等)也不會被標註爲靜態節點。children
,執行遞歸的markStatic
。node.ifConditions
表示的實際上是包含有elseif
和 else
子節點,它們都不在children
中,所以對這些子節點也執行遞歸的markStatic
。static
的狀況,那麼父節點的static
屬性就會變爲 false。標記靜態節點的做用是什麼呢?實際上是爲了下面的標記靜態根節點服務的。
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
複製代碼
代碼解讀:
markStaticRoots()
方法針對的都是普通標籤節點。表達式節點與純文本節點都不在考慮範圍內。markStatic()
得出的static
屬性,在該方法中用上了。將每一個節點都判斷了一遍static
屬性以後,就能夠更快地肯定靜態根節點:經過判斷對應節點是不是靜態節點 且 內部有子元素 且 單一子節點的元素類型不是文本類型。注意:只有純文本子節點時,他是靜態節點,但不是靜態根節點。靜態根節點是 optimize 優化的條件,沒有靜態根節點,說明這部分不會被優化。
而 Vue 官方說明是,若是子節點只有一個純文本節點,若進行優化,帶來的成本就比好處多了。所以這種狀況下,就不進行優化。
首先來分析一下,之因此在 optimize 過程當中作這個靜態根節點的優化,目的是什麼,成本是什麼?
目的:在 patch 過程當中,減小沒必要要的比對過程,加速更新。
目的很好理解。那麼成本呢?
成本:a. 須要維護靜態模板的存儲對象。b. 多層render函數調用.
詳細解釋這兩個成本背後的細節:
一開始的時候,全部的靜態根節點 都會被解析生成 VNode,而且被存在一個緩存對象中,就在 Vue.proto._staticTree 中。
隨着靜態根節點的增長,這個存儲對象也會愈來愈大,那麼佔用的內存就會愈來愈多
勢必要減小一些沒必要要的存儲,全部只有純文本的靜態根節點就被排除了
這個過程涉及到實際操做更新的過程。在實際render 的過程當中,針對靜態節點的操做也須要調用對應的靜態節點渲染函數,作必定的判斷邏輯。這裏須要必定的消耗。
若是純文本節點不作優化,那麼就是須要在更新的時候比對這部分純文本節點咯?這麼作的代價是什麼呢?只是須要比對字符串是否相等而已。簡直不要太簡單,消耗簡直不要過小。
既然如此,那麼還須要維護多一個靜態模板緩存麼?在 render 操做過程當中也不須要額外對該類型的靜態節點進行處理。
staticRoot
屬性會在咱們編譯過程的第三個階段generate
階段--生成 render 函數代碼階段--起到做用。generate
函數定義在src/compiler/codegen/index.js
中,咱們詳細來看:
export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
複製代碼
generate
函數首先經過 genElement(ast, state)
生成 code
,再把 code
用 with(this){return ${code}}}
包裹起來。這裏的genElement
代碼以下:
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
const data = el.plain ? undefined : genData(el, state)
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
複製代碼
其中,首個判斷條件就用到了節點的staticRoot
屬性:
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
}
複製代碼
進入genStatic
:
// hoist static sub-trees out
function genStatic (el: ASTElement, state: CodegenState): string {
el.staticProcessed = true
// Some elements (templates) need to behave differently inside of a v-pre
// node. All pre nodes are static roots, so we can use this as a location to
// wrap a state change and reset it upon exiting the pre node.
const originalPreState = state.pre
if (el.pre) {
state.pre = el.pre
}
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
state.pre = originalPreState
return `_m(${ state.staticRenderFns.length - 1 }${ el.staticInFor ? ',true' : '' })`
}
複製代碼
能夠看到,genStatic
函數最終將對應的代碼邏輯塞入到了state.staticRenderFns
中,而且返回了一個帶有_m
函數的字符串,這個_m
是處理靜態節點函數的縮寫,爲了方便生成的 render 函數字符串不要過於冗長。其具體的含義在src/core/instance/render-helpers/index.js
中:
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
/** * Runtime helper for rendering static trees. */
export function renderStatic ( index: number, isInFor: boolean ): VNode | Array<VNode> {
const cached = this._staticTrees || (this._staticTrees = [])
let tree = cached[index]
// if has already-rendered static tree and not inside v-for,
// we can reuse the same tree.
if (tree && !isInFor) {
return tree
}
// otherwise, render a fresh tree.
tree = cached[index] = this.$options.staticRenderFns[index].call(
this._renderProxy,
null,
this // for render fns generated for functional component templates
)
markStatic(tree, `__static__${index}`, false)
return tree
}
複製代碼
在具體執行 render 函數的過程當中,會執行_m
函數,其實執行的就是上面代碼中的renderStatic
函數。靜態節點的渲染邏輯是這樣的:
_staticTrees
屬性是否有對應的index
緩存值,如有,則直接使用。$options.staticRenderFns
對應的函數,結合genStatic
的代碼,可知其對應執行的函數詳細。在本文中,咱們詳細分析了 Vue 編譯過程當中的 optimize 過程。這個過程主要作了兩個事情:標記靜態節點markStatic
與標記靜態根節點markStaticRoot
。同時,咱們也分析了標記靜態根節點markStaticRoot
在接下來的 generate 階段的做用。
但願對讀者有必定的幫助!如有理解不足之處,望指出!
vue源碼解讀文章目錄:
Vue 更多系列: