1 はじめに
前回の記事に続いて、ユーザーが作成したテンプレート文字列を解析段階で解析して、対応する抽象構文ツリーを生成し、最適化段階で静的ノードと静的ルート ノードをマークAST
しAST
、いよいよテンプレート コンパイルの 3 つの段階の最後の段階であるコード生成段階に到達しました。いわゆるコード生成フェーズ、どのようなコードを生成するのか? 回答:render
ファンクション文字列を生成します。
Vue
インスタンスがマウントされると、インスタンスが独自の関数を呼び出して、render
インスタンスにtemplate
対応するオプションを生成することがわかりますVNode
。簡単に言うと、Vue
関数が呼び出されている限りrender
、テンプレートは対応する virtual に変換できますDOM
。関数をVue
呼び出したい場合は、まずこの関数を用意する必要があります。では、この関数はどこから来たのでしょうか? ユーザーが書いたものですか、それとも自分で生成したものですか? 答えはすべて可能です。日常の開発では、コンポーネントのオプションにオプションを手動で記述することができ、その値が関数に対応し、その関数が関数となり、ユーザーが関数を手動で記述すると、ユーザーが記述したこの関数がコンポーネントのマウント時に呼び出されることを知っています。ユーザーが書き込まなかったらどうなるでしょうか? このとき、コンポーネントのマウント時に呼び出す関数をテンプレートの内容に基づいて生成する必要があります。そして、テンプレートの内容に従って関数を生成するプロセスが、この記事で紹介するコード生成段階です。render
render
render
Vue
Vue
render
render
render
Vue
render
Vue
render
Vue
render
いわゆるコード生成とは、実際にはAST
テンプレートに対応する抽象構文ツリーに従って関数を生成することであり、この関数を呼び出すことで、テンプレートに対応する仮想を取得できることがわかりましたDOM
。
2. ASTに従ってレンダリング関数を生成する方法
上記のことから、コード生成フェーズの主な作業は、コンポーネントがマウントされたときに呼び出す既存の関数に基づいて、対応する関数を生成することでAST
あることがわかりますrender
。コンポーネントがこの関数を呼び出す限り、対応する仮想関数をrender
取得できます。では、それに応じて関数を生成するにはどうすればよいでしょうか? これはどのようなプロセスですか? 次に、詳しく分析してみましょう。AST
DOM
VNode
AST
render
次のテンプレートがあるとします。
<div id="NLRX"><p>Hello {
{name}}</p></div>
テンプレートが解析および最適化されると、AST
次のようになります。
ast = {
'type': 1,
'tag': 'div',
'attrsList': [
{
'name':'id',
'value':'NLRX',
}
],
'attrsMap': {
'id': 'NLRX',
},
'static':false,
'parent': undefined,
'plain': false,
'children': [{
'type': 1,
'tag': 'p',
'plain': false,
'static':false,
'children': [
{
'type': 2,
'expression': '"Hello "+_s(name)',
'text': 'Hello {
{name}}',
'static':false,
}
]
}]
}
次に、既存の関数に従って、AST
対応する関数を生成しますrender
。関数を生成するrender
プロセスは実際には再帰的なプロセスであり、各ノードを上から下に順番に再帰しAST
、異なるノード タイプに応じてAST
異なるタイプを作成します。VNode
次に、既存のテンプレートを比較し、AST
実際にrender
関数を生成するプロセスをデモしてみましょう。
-
まず、ルート ノードは
div
要素タイプAST
ノードです。次に、要素タイプを作成する必要がありますVNode
。要素タイプVNode
を作成するメソッドを呼び出します_c(tagName,data,children)
。現時点ではそれが何であるかは気にしません。要素 type を作成する_c()
呼び出しを知る必要があるだけです。次に、次のコードを生成できます。_c()
VNode
_c('div',{ attrs:{ "id":"NLRX"}},[/*子节点列表*/])
-
ルート ノード
div
には子ノードがあり、次に子ノード リストを入力して子children
ノードを走査し、子ノードp
も要素タイプであることを確認します。次に、要素タイプの作成を続けてVNode
、上記のコードのルート ノードの子ノード リストに要素タイプを追加します。_c('div',{ attrs:{ "id":"NLRX"}},[_c('p',{ attrs:{ }},[/*子节点列表*/])])
-
同様に、
p
ノードの子ノードをトラバースし続け、それがテキスト ノードであることを確認し、テキスト タイプを作成してノードの子ノード リストVNode
に挿入します。同様に、次のようにテキスト タイプを作成するメソッドを呼び出します。p
VNode
_v()
_c('div',{ attrs:{ "id":"NLRX"}},[_c('p',{ attrs:{ }},[_v("Hello "+_s(name))])])
-
この時点で、走査全体
AST
が完了したので、取得したコードを次のように再パッケージ化します。` with(this){ reurn _c( 'div', { attrs:{"id":"NLRX"}, }, [ _c( 'p', { attrs:{} }, [ _v("Hello "+_s(name)) ] ) ] ) } `
-
最後に、上記で取得した関数文字列を
createFunction
関数に渡します (この関数については後で紹介します)。createFunction
この関数は、取得した関数文字列を実際の関数に変換し、それをコンポーネント内のオプションに割り当てるのに役立ちます。つまり、関数render
です。render
次のように:res.render = createFunction(compiled.render, fnGenErrors) function createFunction (code, errors) { try { return new Function(code) } catch (err) { errors.push({ err, code }) return noop } }
以上が単純なテンプレートに対応する関数をAST
生成するプロセスですrender
が、理論的なプロセスは理解できましたが、実際にソースコードにどのように実装されるのでしょうか。次に、ソース コードに戻って、その具体的な実装プロセスを分析します。
3. ソースコードに戻る
コード生成ステージのソース コードはsrc/compiler/codegen/index.js
にあります。ソース コードは非常に長いですが、ロジックは複雑ではありません。コア ロジックは次のとおりです。
export function generate (ast,option) {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${
code}}`,
staticRenderFns: state.staticRenderFns
}
}
const code = generate(ast, options)
generate
関数を呼び出して最適化された値を渡しast
、generate
まず関数内で空かどうかを判断しast
、空でない場合はgenElement(ast, state)
関数を呼び出して作成しVNode
、空の場合は空の要素div
型を作成しますVNode
。結果はwith(this){return ${code}}
パッケージで返されます。これが実際に機能する関数であることがわかります。関数の内部genElement
を見ていきましょう。genElement
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
}
}
genElement
関数のロジックは非常に明確で、現在の要素ノードのさまざまな属性に従ってAST
さまざまなコード生成関数を実行します。要素ノードの属性にはたくさんの種類がありますが、実際に最終的に作成されるのはVNode
要素ノード、テキストノード、コメントノードの3種類だけです。render
次に、これら 3 つのノード タイプの機能を生成する方法の分析に焦点を当てます。
3.1 要素ノード
要素型ノードを生成するrender
関数コードは次のとおりです。
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
})`
要素ノードを生成する機能はrender
、一連の関数呼び出しを生成することであり_c()
、前述したように、_c()
この関数は、ノードのラベル名tagName
、ノードの属性data
、およびノードの子ノードのリストの3 つのパラメータを受け取りますchildren
。あとは 3 つの部分をすべて入力するだけです。
-
ノード属性データの取得
まず
plain
属性が であるかどうかを判断し、属性がtrue
ある場合true
はノードに属性がないことを意味して値をdata
代入しundefined
、そうでない場合はtrue
関数を呼び出してgenData
ノードの属性data
データを取得します。genData
関数の定義は次のとおりです。export function genData (el: ASTElement, state: CodegenState): string { let data = '{' const 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,` } // 篇幅所限,省略其他情况的判断 data = data.replace(/,$/, '') + '}' return data }
genData
ソース コードは非常に長いですが、そのロジックは非常に単純で、文字列を結合し、最初にdata
値を代入し{
、次にどの属性データが存在するかを判断し、これらのデータを に結合しdata
、最後に 1 を追加し}
、最後にノードのすべての属性を取得していることがわかりますdata
。 -
子ノードのリストを取得します。
子ノード リストの取得は、実際には、 attribute内の要素
children
を走査し、次に次のように、さまざまな要素属性に従ってさまざまな作成関数呼び出し文字列を生成します。AST
children
VNode
export function genChildren (el): { if (children.length) { return `[${ children.map(c => genNode(c, state)).join(',')}]` } } function genNode (node: ASTNode, state: CodegenState): string { if (node.type === 1) { return genElement(node, state) } if (node.type === 3 && node.isComment) { return genComment(node) } else { return genText(node) } }
-
上記の 2 つの手順が完了すると、
_c()
次のように関数呼び出し文字列が生成されます。code = `_c('${ el.tag}'${ data ? `,${ data}` : '' // data }${ children ? `,${ children}` : '' // children })`
3.2 テキストノード
テキスト タイプは関数VNode
を呼び出すことで作成できるため、_v(text)
テキスト ノードを生成するrender
関数は_v(text)
関数呼び出しの文字列を生成することになります。この関数はテキスト コンテンツをパラメータとして受け取り、テキストが動的テキストの場合は動的テキスト ノードのプロパティ_v()
を使用し、純粋な静的テキストの場合はそのプロパティを使用します。生成されたコードは次のとおりです。AST
expression
text
export function genText (text: ASTText | ASTExpression): string {
return `_v(${
text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))
})`
}
3.3 コメントノード
コメント タイプは関数VNode
を呼び出すことで作成できるため、_e(text)
コメント ノードを生成するrender
関数は_e(text)
関数呼び出しの文字列を生成することになります。_e()
この関数はコメントの内容をパラメータとして受け取り、生成されるコードは次のとおりです。
export function genComment (comment: ASTText): string {
return `_e(${
JSON.stringify(comment.text)})`
}
4. まとめ
この記事では、テンプレート コンパイルの 3 つのフェーズのうちの最後のフェーズであるコード生成フェーズについて説明します。
まず、コード生成フェーズが存在する理由と、コード生成フェーズが主に行うことを紹介します。コード生成とは、実際にはコンポーネントのマウント時に呼び出されるテンプレートに対応する抽象構文ツリーAST
に基づいて関数を生成することであり、この関数を呼び出すことでテンプレートに対応する仮想を取得することができますDOM
。
次に、テンプレートを再帰的に走査し、最終的にrender
単純なテンプレートを通じて関数を生成するプロセスを示します。
最後にソースコードに戻り、render
ソースコードを分析することで生成関数の具体的な実装プロセスを理解します。