因爲文章篇幅限制,因此將 Vue 源碼解讀(8)—— 編譯器 之 解析 拆成了兩篇文章,本篇是對 Vue 源碼解讀(8)—— 編譯器 之 解析(上) 的一個補充,因此在閱讀時請同時打開 Vue 源碼解讀(8)—— 編譯器 之 解析(上) 一塊兒閱讀。javascript
/src/compiler/parser/index.jshtml
/** * 處理元素上的全部屬性: * v-bind 指令變成:el.attrs 或 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...], * 或者是必須使用 props 的屬性,變成了 el.props = [{ name, value, start, end, dynamic }, ...] * v-on 指令變成:el.events 或 el.nativeEvents = { name: [{ value, start, end, modifiers, dynamic }, ...] } * 其它指令:el.directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...] * 原生屬性:el.attrs = [{ name, value, start, end }],或者一些必須使用 props 的屬性,變成了: * el.props = [{ name, value: true, start, end, dynamic }] */
function processAttrs(el) {
// list = [{ name, value, start, end }, ...]
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
// 屬性名
name = rawName = list[i].name
// 屬性值
value = list[i].value
if (dirRE.test(name)) {
// 說明該屬性是一個指令
// 元素上存在指令,將元素標記動態元素
// mark element as dynamic
el.hasBindings = true
// modifiers,在屬性名上解析修飾符,好比 xx.lazy
modifiers = parseModifiers(name.replace(dirRE, ''))
// support .foo shorthand syntax for the .prop modifier
if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
// 爲 .props 修飾符支持 .foo 速記寫法
(modifiers || (modifiers = {})).prop = true
name = `.` + name.slice(1).replace(modifierRE, '')
} else if (modifiers) {
// 屬性中的修飾符去掉,獲得一個乾淨的屬性名
name = name.replace(modifierRE, '')
}
if (bindRE.test(name)) { // v-bind, <div :id="test"></div>
// 處理 v-bind 指令屬性,最後獲得 el.attrs 或者 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...]
// 屬性名,好比:id
name = name.replace(bindRE, '')
// 屬性值,好比:test
value = parseFilters(value)
// 是否爲動態屬性 <div :[id]="test"></div>
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
// 若是是動態屬性,則去掉屬性兩側的方括號 []
name = name.slice(1, -1)
}
// 提示,動態屬性值不能爲空字符串
if (
process.env.NODE_ENV !== 'production' &&
value.trim().length === 0
) {
warn(
`The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"`
)
}
// 存在修飾符
if (modifiers) {
if (modifiers.prop && !isDynamic) {
name = camelize(name)
if (name === 'innerHtml') name = 'innerHTML'
}
if (modifiers.camel && !isDynamic) {
name = camelize(name)
}
// 處理 sync 修飾符
if (modifiers.sync) {
syncGen = genAssignmentCode(value, `$event`)
if (!isDynamic) {
addHandler(
el,
`update:${camelize(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
if (hyphenate(name) !== camelize(name)) {
addHandler(
el,
`update:${hyphenate(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
}
} else {
// handler w/ dynamic event name
addHandler(
el,
`"update:"+(${name})`,
syncGen,
null,
false,
warn,
list[i],
true // dynamic
)
}
}
}
if ((modifiers && modifiers.prop) || (
!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
)) {
// 將屬性對象添加到 el.props 數組中,表示這些屬性必須經過 props 設置
// el.props = [{ name, value, start, end, dynamic }, ...]
addProp(el, name, value, list[i], isDynamic)
} else {
// 將屬性添加到 el.attrs 數組或者 el.dynamicAttrs 數組
addAttr(el, name, value, list[i], isDynamic)
}
} else if (onRE.test(name)) { // v-on, 處理事件,<div @click="test"></div>
// 屬性名,即事件名
name = name.replace(onRE, '')
// 是否爲動態屬性
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
// 動態屬性,則獲取 [] 中的屬性名
name = name.slice(1, -1)
}
// 處理事件屬性,將屬性的信息添加到 el.events 或者 el.nativeEvents 對象上,格式:
// el.events = [{ value, start, end, modifiers, dynamic }, ...]
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
} else { // normal directives,其它的普通指令
// 獲得 el.directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...]
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
isDynamic = false
if (arg) {
name = name.slice(0, -(arg.length + 1))
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1)
isDynamic = true
}
}
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
if (process.env.NODE_ENV !== 'production' && name === 'model') {
checkForAliasModel(el, value)
}
}
} else {
// 當前屬性不是指令
// literal attribute
if (process.env.NODE_ENV !== 'production') {
const res = parseText(value, delimiters)
if (res) {
warn(
`${name}="${value}": ` +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div id="{{ val }}">, use <div :id="val">.',
list[i]
)
}
}
// 將屬性對象放到 el.attrs 數組中,el.attrs = [{ name, value, start, end }]
addAttr(el, name, JSON.stringify(value), list[i])
// #6887 firefox doesn't update muted state if set via attribute
// even immediately after element creation
if (!el.component &&
name === 'muted' &&
platformMustUseProp(el.tag, el.attrsMap.type, name)) {
addProp(el, name, 'true', list[i])
}
}
}
}
複製代碼
/src/compiler/helpers.jsjava
/** * 處理事件屬性,將事件屬性添加到 el.events 對象或者 el.nativeEvents 對象中,格式: * el.events[name] = [{ value, start, end, modifiers, dynamic }, ...] * 其中用了大量的篇幅在處理 name 屬性帶修飾符 (modifier) 的狀況 * @param {*} el ast 對象 * @param {*} name 屬性名,即事件名 * @param {*} value 屬性值,即事件回調函數名 * @param {*} modifiers 修飾符 * @param {*} important * @param {*} warn 日誌 * @param {*} range * @param {*} dynamic 屬性名是否爲動態屬性 */
export function addHandler ( el: ASTElement, name: string, value: string, modifiers: ?ASTModifiers, important?: boolean, warn?: ?Function, range?: Range, dynamic?: boolean ) {
// modifiers 是一個對象,若是傳遞的參數爲空,則給一個凍結的空對象
modifiers = modifiers || emptyObject
// 提示:prevent 和 passive 修飾符不能一塊兒使用
// warn prevent and passive modifier
/* istanbul ignore if */
if (
process.env.NODE_ENV !== 'production' && warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.',
range
)
}
// 標準化 click.right 和 click.middle,它們實際上不會被真正的觸發,從技術講他們是它們
// 是特定於瀏覽器的,但至少目前位置只有瀏覽器才具備右鍵和中間鍵的點擊
// normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks.
if (modifiers.right) {
// 右鍵
if (dynamic) {
// 動態屬性
name = `(${name})==='click'?'contextmenu':(${name})`
} else if (name === 'click') {
// 非動態屬性,name = contextmenu
name = 'contextmenu'
// 刪除修飾符中的 right 屬性
delete modifiers.right
}
} else if (modifiers.middle) {
// 中間鍵
if (dynamic) {
// 動態屬性,name => mouseup 或者 ${name}
name = `(${name})==='click'?'mouseup':(${name})`
} else if (name === 'click') {
// 非動態屬性,mouseup
name = 'mouseup'
}
}
/** * 處理 capture、once、passive 這三個修飾符,經過給 name 添加不一樣的標記來標記這些修飾符 */
// check capture modifier
if (modifiers.capture) {
delete modifiers.capture
// 給帶有 capture 修飾符的屬性,加上 ! 標記
name = prependModifierMarker('!', name, dynamic)
}
if (modifiers.once) {
delete modifiers.once
// once 修飾符加 ~ 標記
name = prependModifierMarker('~', name, dynamic)
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive
// passive 修飾符加 & 標記
name = prependModifierMarker('&', name, dynamic)
}
let events
if (modifiers.native) {
// native 修飾符, 監聽組件根元素的原生事件,將事件信息存放到 el.nativeEvents 對象中
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
// 說明有修飾符,將修飾符對象放到 newHandler 對象上
// { value, dynamic, start, end, modifiers }
newHandler.modifiers = modifiers
}
// 將配置對象放到 events[name] = [newHander, handler, ...]
const handlers = events[name]
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
複製代碼
/src/compiler/parser/index.jsnode
/** * 將傳遞進來的條件對象放進 el.ifConditions 數組中 */
export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}
複製代碼
/src/compiler/parser/index.jsweb
/** * 若是元素上存在 v-pre 指令,則設置 el.pre = true */
function processPre(el) {
if (getAndRemoveAttr(el, 'v-pre') != null) {
el.pre = true
}
}
複製代碼
/src/compiler/parser/index.js面試
/** * 設置 el.attrs 數組對象,每一個元素都是一個屬性對象 { name: attrName, value: attrVal, start, end } */
function processRawAttrs(el) {
const list = el.attrsList
const len = list.length
if (len) {
const attrs: Array<ASTAttr> = el.attrs = new Array(len)
for (let i = 0; i < len; i++) {
attrs[i] = {
name: list[i].name,
value: JSON.stringify(list[i].value)
}
if (list[i].start != null) {
attrs[i].start = list[i].start
attrs[i].end = list[i].end
}
}
} else if (!el.pre) {
// non root node in pre blocks with no attributes
el.plain = true
}
}
複製代碼
/src/compiler/parser/index.js正則表達式
/** * 處理 v-if、v-else-if、v-else * 獲得 el.if = "exp",el.elseif = exp, el.else = true * v-if 屬性會額外在 el.ifConditions 數組中添加 { exp, block } 對象 */
function processIf(el) {
// 獲取 v-if 屬性的值,好比 <div v-if="test"></div>
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
// el.if = "test"
el.if = exp
// 在 el.ifConditions 數組中添加 { exp, block }
addIfCondition(el, {
exp: exp,
block: el
})
} else {
// 處理 v-else,獲得 el.else = true
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
// 處理 v-else-if,獲得 el.elseif = exp
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
複製代碼
/src/compiler/parser/index.jsexpress
/** * 處理 v-once 指令,獲得 el.once = true * @param {*} el */
function processOnce(el) {
const once = getAndRemoveAttr(el, 'v-once')
if (once != null) {
el.once = true
}
}
複製代碼
/src/compiler/parser/index.js數組
/** * 檢查根元素: * 不能使用 slot 和 template 標籤做爲組件的根元素 * 不能在有狀態組件的 根元素 上使用 v-for 指令,由於它會渲染出多個元素 * @param {*} el */
function checkRootConstraints(el) {
// 不能使用 slot 和 template 標籤做爲組件的根元素
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.',
{ start: el.start }
)
}
// 不能在有狀態組件的 根元素 上使用 v-for,由於它會渲染出多個元素
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.',
el.rawAttrsMap['v-for']
)
}
}
複製代碼
/src/compiler/parser/index.js瀏覽器
/** * 主要作了 3 件事: * 一、若是元素沒有被處理過,即 el.processed 爲 false,則調用 processElement 方法處理節點上的衆多屬性 * 二、讓本身和父元素產生關係,將本身放到父元素的 children 數組中,並設置本身的 parent 屬性爲 currentParent * 三、設置本身的子元素,將本身全部非插槽的子元素放到本身的 children 數組中 */
function closeElement(element) {
// 移除節點末尾的空格,當前 pre 標籤內的元素除外
trimEndingWhitespace(element)
// 當前元素再也不 pre 節點內,而且也沒有被處理過
if (!inVPre && !element.processed) {
// 分別處理元素節點的 key、ref、插槽、自閉合的 slot 標籤、動態組件、class、style、v-bind、v-on、其它指令和一些原生屬性
element = processElement(element, options)
}
// 處理根節點上存在 v-if、v-else-if、v-else 指令的狀況
// 若是根節點存在 v-if 指令,則必須還提供一個具備 v-else-if 或者 v-else 的同級別節點,防止根元素不存在
// tree management
if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
if (process.env.NODE_ENV !== 'production') {
// 檢查根元素
checkRootConstraints(element)
}
// 給根元素設置 ifConditions 屬性,root.ifConditions = [{ exp: element.elseif, block: element }, ...]
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production') {
// 提示,表示不該該在 根元素 上只使用 v-if,應該將 v-if、v-else-if 一塊兒使用,保證組件只有一個根元素
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`,
{ start: element.start }
)
}
}
// 讓本身和父元素產生關係
// 將本身放到父元素的 children 數組中,而後設置本身的 parent 屬性爲 currentParent
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
const name = element.slotTarget || '"default"'
; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
}
currentParent.children.push(element)
element.parent = currentParent
}
}
// 設置本身的子元素
// 將本身的全部非插槽的子元素設置到 element.children 數組中
// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c => !(c: any).slotScope)
// remove trailing whitespace node again
trimEndingWhitespace(element)
// check pre state
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
// 分別爲 element 執行 model、class、style 三個模塊的 postTransform 方法
// 可是 web 平臺沒有提供該方法
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
複製代碼
/src/compiler/parser/index.js
/** * 刪除元素中空白的文本節點,好比:<div> </div>,刪除 div 元素中的空白節點,將其從元素的 children 屬性中移出去 */
function trimEndingWhitespace(el) {
if (!inPre) {
let lastNode
while (
(lastNode = el.children[el.children.length - 1]) &&
lastNode.type === 3 &&
lastNode.text === ' '
) {
el.children.pop()
}
}
}
複製代碼
/src/compiler/parser/index.js
function processIfConditions(el, parent) {
// 找到 parent.children 中的最後一個元素節點
const prev = findPrevElement(parent.children)
if (prev && prev.if) {
addIfCondition(prev, {
exp: el.elseif,
block: el
})
} else if (process.env.NODE_ENV !== 'production') {
warn(
`v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
`used on element <${el.tag}> without corresponding v-if.`,
el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
)
}
}
複製代碼
/src/compiler/parser/index.js
/** * 找到 children 中的最後一個元素節點 */
function findPrevElement(children: Array<any>): ASTElement | void {
let i = children.length
while (i--) {
if (children[i].type === 1) {
return children[i]
} else {
if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') {
warn(
`text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
`will be ignored.`,
children[i]
)
}
children.pop()
}
}
}
複製代碼
到這裏編譯器的解析部分就結束了,相信不少人看的是雲裏霧裏的,即便多看幾遍可能也沒有那麼清晰。
不要着急,這個很正常,編譯器這塊兒的代碼量確實是比較大。可是內容自己其實不復雜,複雜的是它要處理東西實在是太多了,這才致使這部分的代碼量巨大,相對應的,就會產生比較難的感受。確實不簡單,至少我以爲它是整個框架最複雜最難的地方了。
對照着視頻和文章你們能夠多看幾遍,不明白的地方寫一些示例代碼輔助調試,編寫詳細的註釋。仍是那句話,書讀百遍,其義自現。
閱讀的過程當中,你們須要抓住編譯器解析部分的本質:將類 HTML 字符串模版解析成 AST 對象。
因此這麼多代碼都在作一件事情,就是解析字符串模版,將整個模版用 AST 對象來表示和記錄。因此,你們閱讀的時候,能夠將解析過程當中生成的 AST 對象記錄下來,幫助閱讀和理解,這樣在讀完之後不至於那麼迷茫,也有助於你們理解。
這是我在閱讀的時候的一個簡單記錄:
const element = {
type: 1,
tag,
attrsList: [{ name: attrName, value: attrVal, start, end }],
attrsMap: { attrName: attrVal, },
rawAttrsMap: { attrName: attrVal, type: checkbox },
// v-if
ifConditions: [{ exp, block }],
// v-for
for: iterator,
alias: 別名,
// :key
key: xx,
// ref
ref: xx,
refInFor: boolean,
// 插槽
slotTarget: slotName,
slotTargetDynamic: boolean,
slotScope: 做用域插槽的表達式,
scopeSlot: {
name: {
slotTarget: slotName,
slotTargetDynamic: boolean,
children: {
parent: container,
otherProperty,
}
},
slotScope: 做用域插槽的表達式,
},
slotName: xx,
// 動態組件
component: compName,
inlineTemplate: boolean,
// class
staticClass: className,
classBinding: xx,
// style
staticStyle: xx,
styleBinding: xx,
// attr
hasBindings: boolean,
nativeEvents: {同 evetns},
events: {
name: [{ value, dynamic, start, end, modifiers }]
},
props: [{ name, value, dynamic, start, end }],
dynamicAttrs: [同 attrs],
attrs: [{ name, value, dynamic, start, end }],
directives: [{ name, rawName, value, arg, isDynamicArg, modifiers, start, end }],
// v-pre
pre: true,
// v-once
once: true,
parent,
children: [],
plain: boolean,
}
複製代碼
面試官 問:簡單說一下 Vue 的編譯器都作了什麼?
答:
Vue 的編譯器作了三件事情:
將組件的 html 模版解析成 AST 對象
優化,遍歷 AST,爲每一個節點作靜態標記,標記其是否爲靜態節點,而後進一步標記出靜態根節點,這樣在後續更新的過程當中就能夠跳過這些靜態節點了;標記靜態根用於生成渲染函數階段,生成靜態根節點的渲染函數
從 AST 生成運行時的渲染函數,即你們說的 render,其實還有一個,就是 staticRenderFns 數組,裏面存放了全部的靜態節點的渲染函數
面試官 問:詳細說一說編譯器的解析過程,它是怎麼將 html 字符串模版變成 AST 對象的?
答:
遍歷 HTML 模版字符串,經過正則表達式匹配 "<"
跳過某些不須要處理的標籤,好比:註釋標籤、條件註釋標籤、Doctype。
備註:整個解析過程的核心是處理開始標籤和結束標籤
解析開始標籤
獲得一個對象,包括 標籤名(tagName)、全部的屬性(attrs)、標籤在 html 模版字符串中的索引位置
進一步處理上一步獲得的 attrs 屬性,將其變成 [{ name: attrName, value: attrVal, start: xx, end: xx }, ...] 的形式
經過標籤名、屬性對象和當前元素的父元素生成 AST 對象,其實就是一個 普通的 JS 對象,經過 key、value 的形式記錄了該元素的一些信息
接下來進一步處理開始標籤上的一些指令,好比 v-pre、v-for、v-if、v-once,並將處理結果放到 AST 對象上
處理結束將 ast 對象存放到 stack 數組
處理完成後會截斷 html 字符串,將已經處理掉的字符串截掉
解析閉合標籤
若是匹配到結束標籤,就從 stack 數組中拿出最後一個元素,它和當前匹配到的結束標籤是一對。
再次處理開始標籤上的屬性,這些屬性和前面處理的不同,好比:key、ref、scopedSlot、樣式等,並將處理結果放到元素的 AST 對象上
備註 視頻中說這塊兒有誤,回頭看了下,沒有問題,不須要改,確實是這樣
而後將當前元素和父元素產生聯繫,給當前元素的 ast 對象設置 parent 屬性,而後將本身放到父元素的 ast 對象的 children 數組中
最後遍歷完整個 html 模版字符串之後,返回 ast 對象
歡迎你們關注個人 掘金帳號 和 B站,若是內容有幫到你,歡迎你們點贊、收藏 + 關注