Vue.jsのreactive systemがどのように実装されているか追ってみた. 初回はコンパイラの挙動を確認.
- Vuejs v2.6.10
src/compiler/index.js
compilerのentrypointとなるのがsrc/compiler/index.js.
(Vue.jsはflowを用いてtype checkを行っている. compiler周りの型はflow/compiler.jsに記載.)
parse(template, options)でASTを生成し,generate(ast, options)でcodeを生成する.
Virtual DOMのpatch/mergeを効率よく行うため,返り値のCompiledResultにcodeだけでなくASTを持たせている.
staticRenderFnsについてはparser部分で説明する.
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
src/compiler/parser/index.js
.vueファイルからASTを生成する部分. parse関数内部で関数定義してて長いので,重要なところだけピックアップする.
VueのテンプレートはHTMLのDOMをベースにしていて,特に
- Mustache
- Directive の2点を拡張記法としてもっている.
そのためパース自体はhtml-parseを用いており,parseの主な仕事はHTML DOMのASTを返すのとパース時のエラー出力となっている.
/**
* Convert HTML string to AST.
*/
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
warn = options.warn || baseWarn
/* omit */
parseHTML(template, {
/* omit */,
start (tag, attrs, unary, start, end) { /* omit */ },
})
return root // ASTのroot Node
}
関係ないけど階段状にインポートしてるの好き.
import {
addProp,
addAttr,
baseWarn,
addHandler,
addDirective,
getBindingAttr,
getAndRemoveAttr,
getRawBindingAttr,
pluckModuleFunction,
getAndRemoveAttrByRegex
} from '../helpers'
Abstract Syntax Tree
generateが受け取るastの型ASTElementはflow/compiler.jsに定義されている.
ASTは以下の3種類に分類され,ASTNodeがそれぞれのtypeを元に判別している.
ASTElement: type: 1ASTText: type: 2ASTExpression: type: 3
type ASTNode = ASTElement | ASTText | ASTExpression
ASTElementを例にとると以下のようになっている.
declare type ASTElement = {
type: 1;
tag: string;
attrsList: Array<ASTAttr>;
attrsMap: { [key: string]: any };
rawAttrsMap: { [key: string]: ASTAttr };
parent: ASTElement | void;
children: Array<ASTNode>;
/* omit */
staticRoot?: boolean;
text?: string;
component?: string;
if?: string;
for?: string;
transition?: string | true;
/* omit */
model?: {
value: string;
callback: string;
expression: string;
};
}
ASTの構造はASTElementがrootとなり,branchにASTNodeを再帰的に持つ構造になる.
staticRoot?やcomponent?等のパラメータは,そのASTNode以下がstaticであるかdynamicであるか,等レンダリングの最適化の際に参照される.
この最適化機構はsrc/compiler/optimizer.jsのmarkStatic及びmarkStaticRootが担う.
src/compiler/codegen/index.js
generateはASTからrenderとstaticRenderFnsの2つを生成する.
renderがVueのDynamic Renderingを担当し,staticRenderFnsは静的な(変更のない)DOMを生成する.
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
}
}
genElementはASTElementのstaticRoot?やfor?等を確認し,それぞれのディレクティブ・タグに対応するElement, Componentを生成する.
各々の生成はgenStatic, genIfのようにそれぞれのジェネレータで行う.
各々のジェネレータは受け取ったASTを処理した後に再帰的にgenElementを呼び出す.
最終的にgenElementは最小単位であるElementの生成を行う.
Elementは<tag data>children</tag>からなるDOMである.
以下がその生成部分.
data = genData(el, state)
/* omit */
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
Elementのdata部分はgenDataが担当する. data部分に含まれるのはclass, directive, model.
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// directives first.
// directives may mutate the el's other properties before they are generated.
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
/* omit */
// component v-model
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
/* omit */
return data
}
最終的にCompiledResultが生成される.
declare type CompiledResult = {
ast: ?ASTElement;
render: string;
staticRenderFns: Array<string>;
stringRenderFns?: Array<string>;
errors?: Array<string | WarningMessage>;
tips?: Array<string | WarningMessage>;
};
Summary
- Vueのコンパイラは
parserとgeneratorからなる. parserはhtml-parseを元にASTを生成する.generatorはASTを元に動的部分と静的部分のrender群を生成する
Next
次回はsrc/core/observerを読む.
(名前からVueのreactive systemがObserver patternベースなんだろうな)
Comments