Vue的渲染機制指的是Vue怎麼將單文件組件中的template轉換爲AST(語法樹),再將AST轉換成render函數,最後生成虛擬dom節點(包含建立元素節點的一切信息的JavaScript對象),並建立元素節點掛載到頁面上,基本過程以下圖: 本節先介紹模板編譯生成render函數的過程。html
模板編譯成渲染函數經歷了三個階段: 將模板解析成AST、遍歷AST標記靜態節點以及靜態根節點和使用AST生成render函數。 如下面模板爲例:node
<div id="app">{{ message }}</div>
複製代碼
首先獲取組件的模板內容express
var template = options.template;
if (template) {
// 針對字符串模板和選擇符匹配模板
if (typeof template === 'string') {
// 選擇符匹配模板,以'#'爲前綴的選擇符
if (template.charAt(0) === '#') {
// 獲取匹配元素的innerHTML
template = idToTemplate(template);
}
} else if (template.nodeType) {
// 針對DOM元素匹配,獲取匹配元素的innerHTML
template = template.innerHTML;
} else {
{
warn('invalid template option:' + template, this);
}
return this
}
} else if (el) {
// 若是沒有傳入template模板,則默認以el元素所屬的根節點做爲基礎模板
template = getOuterHTML(el);
}
複製代碼
獲取模板後處理的核心過程以下:數組
compileToFunctions(template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
...
var compiled = compile(template, options);
...
var compiled = baseCompile(template.trim(), finalOptions);
複製代碼
上面的代碼是在建立編譯器,真正的編譯過程: 解析、優化以及生成render函數,代碼以下:bash
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
複製代碼
真正的解析函數是parseHTML,它的參數是template,和一個options對象,這個對象包含了start、end、chars以及comment對標籤處理的函數:app
parseHTML(template, {
warn: warn$2,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
// 處理起始標籤
start: function start (tag, attrs, unary, start$1, end) {
...
var element = createASTElement(tag, attrs, currentParent);
...
},
// 用來處理結束標籤
end: function end (tag, start, end$1) {
var element = stack[stack.length - 1];
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (options.outputSourceRange) {
element.end = end$1;
}
closeElement(element);
},
// 用來處理文本
chars: function chars (text, start, end) {
...
},
// 處理評論內容
comment: function comment (text, start, end) {
// adding anyting as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
var child = {
type: 3,
text: text,
isComment: true
};
if (options.outputSourceRange) {
child.start = start;
child.end = end;
}
currentParent.children.push(child);
}
}
});
return root
}
複製代碼
parseHTML函數核心內容爲:dom
while (html) {
last = html;
// Make sure we're not in a plaintext content element like script/style // 父元素爲正常元素 if (!lastTag || !isPlainTextElement(lastTag)) { var textEnd = html.indexOf('<'); // html以標籤開頭 if (textEnd === 0) { // Comment: if (comment.test(html)) { var commentEnd = html.indexOf('-->'); if (commentEnd >= 0) { if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3); } advance(commentEnd + 3); continue } } // Doctype: var doctypeMatch = html.match(doctype); if (doctypeMatch) { advance(doctypeMatch[0].length); continue } // End tag: 處理結束標籤 ... // Start tag: // 解析起始標籤 ... } ... } else { // 父元素爲script、style、textarea的處理邏輯 ... } if (html === last) { options.chars && options.chars(html); if (!stack.length && options.warn) { options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), { start: index + html.length }); } break } } 複製代碼
基本過程以下:async
html=<div id="app">{{ message }}</div>
首先獲取textEnd === 0,接下來可判斷html是以div標籤起始的,進行parseStartTag處理var startTagMatch = parseStartTag();
if (startTagMatch) {
// 對獲取起始標籤的屬性,生成鍵值對
handleStartTag(startTagMatch);
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1);
}
continue
}
複製代碼
返回對象爲函數
{
attrs: [" id="app"", "id", "=", "app", undefined, undefined, index: 0, input: " id="app">{{ message }}<button @click="update">更新</button></div>", groups: undefined, start: 4, end: 13],
end: 14,
start: 0,
tagName: "div",
unarySlash: ""
}
複製代碼
其中unarySlash表示是不是閉合標籤。在通過handleStartTag函數處理後調用start函數優化
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs,start: match.start, end: match.end });
lastTag = tagName;
}
// 上面根據parseStartTag返回的對象生成attrs
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end);
}
複製代碼
options.start函數處理後生成
{attrsList: [{
end: 13
name: "id"
start: 5
value: "app"
}],
attrsMap: {id: "app"},
children: [],
end: 14,
parent: undefined,
rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}},
start: 0,
tag: "div",
type: 1}
複製代碼
先判斷標籤是不是閉合標籤,若是是的話直接closeElement,不是的話,更新currentParent而且將當前元素推入stack棧。
if (!unary) {
currentParent = element;
stack.push(element);
} else {
closeElement(element);
}
複製代碼
須要注意的是: stack棧的做用是維護DOM的層級,防止HTML標籤的不匹配。 2. 這個while的第一次循環結束,html被截取爲{{ message }}</div>
,此時計算textEnd爲13,此時處理的是文本元素
// 爲文本節點
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) {
rest = html.slice(textEnd);
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1);
if (next < 0) { break }
textEnd += next;
rest = html.slice(textEnd);
}
text = html.substring(0, textEnd);
}
if (textEnd < 0) {
text = html;
}
if (text) {
advance(text.length);
}
if (options.chars && text) {
options.chars(text, index - text.length, index);
}
複製代碼
此時rest = </div>
, text = {{ message }}
,由 options.chars函數處理文本,具體代碼爲
...
var children = currentParent.children;
...
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE$1, ' ');
}
var res;
var child;
// 帶變量的文本節點, type = 2
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text: text
};
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
// 不帶變量的文本節點,type = 3
child = {
type: 3,
text: text
};
}
if (child) {
if (options.outputSourceRange) {
child.start = start;
child.end = end;
}
children.push(child);
}
}
複製代碼
經由parseText函數
function parseText (
text,
delimiters
) {
// 匹配 {{ message }}
var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
if (!tagRE.test(text)) {
return
}
var tokens = [];
var rawTokens = [];
var lastIndex = tagRE.lastIndex = 0;
var match, index, tokenValue;
while ((match = tagRE.exec(text))) {
index = match.index;
// push text token
// 把{{左邊的文本內容添加到tokens中
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index));
tokens.push(JSON.stringify(tokenValue));
}
// tag token
// 將 {{ message }} 轉成 _s(message) 添加到數組
var exp = parseFilters(match[1].trim());
tokens.push(("_s(" + exp + ")"));
rawTokens.push({ '@binding': exp });
lastIndex = index + match[0].length;
}
// 把{{右邊的文本內容添加到tokens中
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex));
tokens.push(JSON.stringify(tokenValue));
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
複製代碼
處理後返回
{
expression: "_s(message)",
tokens: [{@binding: "message"}]
}
複製代碼
將該節點push進children,生成
[
{
end: 27,
expression: "_s(message)",
start: 14,
text: "{{ message }}",
tokens: [{@binding: "message"}],
type: 2
}
]
複製代碼
</div>
,計算的textEnd = 0,匹配到結束標籤var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
複製代碼
endTagMatch爲
[
"</div>", "div", groups: undefined, index: 0, input: "</div>"
]
複製代碼
棧進行遍歷,尋找與當前結束標籤匹配的起始標籤,options.end函數處理
var element = stack[stack.length - 1];
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (options.outputSourceRange) {
element.end = end$1;
}
closeElement(element);
複製代碼
將棧中的元素pop出,currentParent爲棧的頂層第一個元素element, 更新元素element的end屬性,在進行closeElement主要代碼爲:
// 對元素的屬性進行處理,如ref,slot,is,attrs
if (!inVPre && !element.processed) {
element = processElement(element, options);
}
...
// 將父子關係確認好
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.
var name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
}
// 將當前元素推入到當前父節點的children數組中,更新當前元素的父元素
currentParent.children.push(element);
element.parent = currentParent;
}
}
複製代碼
生成的AST爲
{
attrs: [{
dynamic: undefined
end: 13
name: "id"
start: 5
value: ""app""
}]
attrsList: [{name: "id", value: "app", start: 5, end: 13}]
attrsMap: {id: "app"}
children: [{
end: 27,
expression: "_s(message)",
start: 14,
text: "{{ message }}",
tokens: [{@binding: "message"}],
type: 2
}]
end: 33
parent: undefined
plain: false
rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}
start: 0
tag: "div"
type: 1
}
複製代碼
最後再更新stack和lastTag,stack=[],lastTag='div',循環結束,返回root這個生成的ast。
接下來進入優化階段,
optimize(ast, options);
// 生成靜態節點以及靜態根節點
function optimize (root, options) {
if (!root) { return }
isStaticKey = genStaticKeysCached(options.staticKeys || '');
isPlatformReservedTag = options.isReservedTag || no;
// first pass: mark all non-static nodes.
markStatic$1(root);
// second pass: mark static roots.
markStaticRoots(root, false);
}
複製代碼
首先給ast標記非靜態節點
function markStatic$1 (node) {
// 先判斷該節點是不是靜態節點
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 (var i = 0, l = node.children.length; i < l; i++) {
var child = node.children[i];
// 遞歸子節點標記靜態節點
markStatic$1(child);
// 若是子節點打完標記後,判斷子節點是不是靜態節點,若是不是,則父節點node不多是靜態節點,此時需將父節點設置static = false
if (!child.static) {
node.static = false;
}
}
...
}
}
複製代碼
判斷是不是靜態節點的函數
function isStatic (node) {
if (node.type === 2) { // expression,表達式
return false
}
if (node.type === 3) { // text 文本節點
return true
}
// 若是元素節點沒有v-pre,必須同時知足
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 內置標籤,如slot、component
isPlatformReservedTag(node.tag) && // not a component,必須是保留標籤,<list></list>就不是保留標籤
!isDirectChildOfTemplateFor(node) && // 當前節點的父節點不能是帶v-for指令的template標籤
Object.keys(node).every(isStaticKey) //
))
}
複製代碼
接着找出全部的靜態根節點並標記
// second pass: mark static roots.
markStaticRoots(root, false);
複製代碼
具體爲
function markStaticRoots (node, isInFor) {
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 (var i = 0, l = node.children.length; i < l; i++) { markStaticRoots(node.children[i], isInFor || !!node.for); } } ... } } 複製代碼
返回的AST爲:
{
attrs: [{
dynamic: undefined
end: 13
name: "id"
start: 5
value: ""app""
}]
attrsList: [{name: "id", value: "app", start: 5, end: 13}]
attrsMap: {id: "app"}
children: [{
end: 27,
expression: "_s(message)",
start: 14,
static: false,
text: "{{ message }}",
tokens: [{@binding: "message"}],
type: 2
}]
end: 33
parent: undefined
plain: false,
rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}
start: 0,
static: false,
staticRoot: false,
tag: "div",
type: 1
}
複製代碼
做用將ast生成render渲染函數,不一樣的節點生成的方式不同,具體代碼以下:
function generate (
ast,
options
) {
var state = new CodegenState(options);
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
複製代碼
先判斷ast是否存在,不存在默認是_c('div'),_c表示
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
複製代碼
存在則對ast具體處理
function genElement (el, state) {
if (el.parent) {
el.pre = el.pre || el.parent.pre;
}
// 處理靜態節點
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
// 處理v-once指令
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
// 處理v-for指令
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
// 處理v-if指令
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
// 處理template標籤
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
// 處理slot內置組件
return genSlot(el, state)
} else {
// component or element
var code;
if (el.component) {
// 處理組件
code = genComponent(el.component, el, state);
} else {
var data;
// 處理元素
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData$2(el, state);
}
// 生成子節點的render函數部分
var children = el.inlineTemplate ? null : genChildren(el, state, true);
code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
// module transforms
for (var i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code
}
}
複製代碼
進入genData$2函數爲
function genData$2 (el, state) {
var data = '{';
// directives first.
// directives may mutate the el's other properties before they are generated. var dirs = genDirectives(el, state); if (dirs) { data += dirs + ','; } // key if (el.key) { data += "key:" + (el.key) + ","; } // ref if (el.ref) { data += "ref:" + (el.ref) + ","; } if (el.refInFor) { data += "refInFor:true,"; } // pre if (el.pre) { data += "pre:true,"; } // record original tag name for components using "is" attribute if (el.component) { data += "tag:\"" + (el.tag) + "\","; } // module data generation functions for (var i = 0; i < state.dataGenFns.length; i++) { data += state.dataGenFns[i](el); } // attributes,更新attrs的屬性形式 if (el.attrs) { data += "attrs:" + (genProps(el.attrs)) + ","; } // DOM props if (el.props) { data += "domProps:" + (genProps(el.props)) + ","; } // event handlers if (el.events) { data += (genHandlers(el.events, false)) + ","; } if (el.nativeEvents) { data += (genHandlers(el.nativeEvents, true)) + ","; } // slot target // only for non-scoped slots if (el.slotTarget && !el.slotScope) { data += "slot:" + (el.slotTarget) + ","; } // scoped slots if (el.scopedSlots) { data += (genScopedSlots(el, el.scopedSlots, state)) + ","; } // component v-model if (el.model) { data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},"; } // inline-template if (el.inlineTemplate) { var inlineTemplate = genInlineTemplate(el, state); if (inlineTemplate) { data += inlineTemplate + ","; } } data = data.replace(/,$/, '') + '}'; // v-bind dynamic argument wrap // v-bind with dynamic arguments must be applied using the same v-bind object // merge helper so that class/style/mustUseProp attrs are handled correctly. if (el.dynamicAttrs) { data = "_b(" + data + ",\"" + (el.tag) + "\"," + (genProps(el.dynamicAttrs)) + ")"; } // v-bind data wrap if (el.wrapData) { data = el.wrapData(data); } // v-on data wrap if (el.wrapListeners) { data = el.wrapListeners(data); } return data } 複製代碼
其功能就是拼接字符串,先給data賦值一個'{', 而後發現節點有哪些屬性就將其拼接到data,最後加上一個'}',最後返回一個完整得data:
"{attrs:{"id":"app"}}"
複製代碼
接着對元素節點的子節點進行處理
genChildren(el, state, true);
複製代碼
具體函數爲:
function genChildren (
el,
state,
checkSkip,
altGenElement,
altGenNode
) {
var children = el.children;
if (children.length) {
...
var gen = altGenNode || genNode;
return ("[" + (children.map(function (c) { return gen(c, state); }).join(',')) + "]" + (normalizationType$1 ? ("," + normalizationType$1) : ''))
}
}
複製代碼
根據不一樣子節點類型生成不一樣的節點字符串將其拼接在一塊兒,genNode函數爲:
function genNode (node, state) {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
複製代碼
遞歸子節點來生成子節點的子節點,最後拼接到一塊兒返回。文本節點的處理
function genText (text) {
return ("_v(" + (text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))) + ")")
}
複製代碼
動態文本使用express表達式,靜態文本用text,把文本放在_v中做爲參數,生成的code即render函數爲:
"_c('div',{attrs:{"id":"app"}},[_v(_s(message))])"
複製代碼
最後由
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
複製代碼
中的vm._render()函數調用生成VNode,代碼爲
vnode = render.call(vm._renderProxy, vm.$createElement);
複製代碼
調用生成的render函數,指向vm._renderProxy,with語句的做用是將代碼的做用域設置到一個特定的做用域this中,調用後進入以下:
with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message))])}
複製代碼
其中_s表示toString(),生成的VNode爲
{
asyncFactory: undefined
asyncMeta: undefined
children: [
{
asyncFactory: undefined,
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: undefined,
context: undefined,
data: undefined,
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined
ns: undefined,
parent: undefined,
raw: false,
tag: undefined,
text: "Hello Wolrd",
child: undefined
}],
componentInstance: undefined
componentOptions: undefined
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
data: {attrs: {id: 'app'}}
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "div",
text: undefined,
child: undefined
}
複製代碼
此時編譯過程結束,下篇介紹VNode的渲染過程。