一文了解Vue 3 的 generate 是這樣生成 render 函數(shù)的
前言
在之前的 面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令? 文章中講了transform
階段處理完v-for、v-model等指令后,會生成一棵javascript AST抽象語法樹。這篇文章我們來接著講generate
階段是如何根據(jù)這棵javascript AST抽象語法樹生成render函數(shù)字符串的,本文中使用的vue版本為3.4.19
。
看個demo
還是一樣的套路,我們通過debug一個demo來搞清楚render函數(shù)字符串是如何生成的。demo代碼如下:
<template> <p>{{ msg }}</p> </template> <script setup lang="ts"> import { ref } from "vue"; const msg = ref("hello world"); </script>
上面這個demo很簡單,使用p標簽渲染一個msg響應(yīng)式變量,變量的值為"hello world"。我們在瀏覽器中來看看這個demo生成的render函數(shù)是什么樣的,代碼如下:
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=23bfe016"; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return _openBlock(), _createElementBlock( "p", null, _toDisplayString($setup.msg), 1 /* TEXT */ ); }
上面的render函數(shù)中使用了兩個函數(shù):openBlock
和createElementBlock
。在之前的 vue3早已具備拋棄虛擬DOM的能力了文章中我們已經(jīng)講過了這兩個函數(shù):
openBlock
的作用為初始化一個全局變量currentBlock
數(shù)組,用于收集dom樹中的所有動態(tài)節(jié)點。createElementBlock
的作用為生成根節(jié)點p標簽的虛擬DOM,然后將收集到的動態(tài)節(jié)點數(shù)組currentBlock
塞到根節(jié)點p標簽的dynamicChildren
屬性上。
render函數(shù)的生成其實很簡單,經(jīng)過transform
階段處理后會生成一棵javascript AST抽象語法樹
,這棵樹的結(jié)構(gòu)和要生成的render函數(shù)結(jié)構(gòu)是一模一樣的。所以在generate
函數(shù)中只需要遞歸遍歷這棵樹,進行字符串拼接就可以生成render函數(shù)啦!
加我微信heavenyjj0012回復(fù)「666」,免費領(lǐng)取歐陽研究vue源碼過程中收集的源碼資料,歐陽寫文章有時也會參考這些資料。同時讓你的朋友圈多一位對vue有深入理解的人。
generate
函數(shù)
首先給generate
函數(shù)打個斷點,generate
函數(shù)在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。
然后啟動一個debug終端,在終端中執(zhí)行yarn dev
(這里是以vite舉例)。在瀏覽器中訪問 http://localhost:5173/ ,此時斷點就會走到generate
函數(shù)中了。在我們這個場景中簡化后的generate
函數(shù)是下面這樣的:
function generate(ast) { const context = createCodegenContext(); const { push, indent, deindent } = context; const preambleContext = context; genModulePreamble(ast, preambleContext); const functionName = `render`; const args = ["_ctx", "_cache"]; args.push("$props", "$setup", "$data", "$options"); const signature = args.join(", "); push(`function ${functionName}(${signature}) {`); indent(); push(`return `); genNode(ast.codegenNode, context); deindent(); push(`}`); return { ast, code: context.code, }; }
generate
中主要分為四部分:
- 生成
context
上下文對象。 - 執(zhí)行
genModulePreamble
函數(shù)生成:import { xxx } from "vue";
- 生成render函數(shù)中的函數(shù)名稱和參數(shù),也就是
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
- 生成render函數(shù)中return的內(nèi)容
context
上下文對象
context
上下文對象是執(zhí)行createCodegenContext
函數(shù)生成的,將斷點走進createCodegenContext
函數(shù)。簡化后的代碼如下:
function createCodegenContext() { const context = { code: ``, indentLevel: 0, helper(key) { return `_${helperNameMap[key]}`; }, push(code) { context.code += code; }, indent() { newline(++context.indentLevel); }, deindent(withoutNewLine = false) { if (withoutNewLine) { --context.indentLevel; } else { newline(--context.indentLevel); } }, newline() { newline(context.indentLevel); }, }; function newline(n) { context.push("\n" + ` `.repeat(n)); } return context; }
為了代碼具有較強的可讀性,我們一般都會使用換行和鎖進。context
上下文中的這些屬性和方法作用就是為了生成具有較強可讀性的render函數(shù)。
code
屬性:當(dāng)前生成的render函數(shù)字符串。
indentLevel
屬性:當(dāng)前的鎖進級別,每個級別對應(yīng)兩個空格的鎖進。helper
方法:返回render函數(shù)中使用到的vue包中export導(dǎo)出的函數(shù)名稱,比如返回openBlock
、createElementBlock
等函數(shù)push
方法:向當(dāng)前的render函數(shù)字符串后插入字符串code。indent
方法:插入換行符,并且增加一個鎖進。deindent
方法:減少一個鎖進,或者插入一個換行符并且減少一個鎖進。newline
方法:插入換行符。
生成import {xxx} from "vue"
我們接著來看generate
函數(shù)中的第二部分,生成import {xxx} from "vue"
。將斷點走進genModulePreamble
函數(shù),在我們這個場景中簡化后的genModulePreamble
函數(shù)代碼如下:
function genModulePreamble(ast, context) { const { push, newline, runtimeModuleName } = context; if (ast.helpers.size) { const helpers = Array.from(ast.helpers); push( `import { ${helpers .map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`) .join(", ")} } from ${JSON.stringify(runtimeModuleName)} `, -1 /* End */ ); } genHoists(ast.hoists, context); newline(); push(`export `); }
其中的ast.helpers
是在transform
階段收集的需要從vue中import導(dǎo)入的函數(shù),無需將vue中所有的函數(shù)都import導(dǎo)入。在debug終端看看helpers
數(shù)組中的值如下圖:
從上圖中可以看到需要從vue中import導(dǎo)入toDisplayString
、openBlock
、createElementBlock
這三個函數(shù)。
在執(zhí)行push
方法之前我們先來看看此時的render函數(shù)字符串是什么樣的,如下圖:
從上圖中可以看到此時生成的render函數(shù)字符串還是一個空字符串,執(zhí)行完push方法后,我們來看看此時的render函數(shù)字符串是什么樣的,如下圖:
從上圖中可以看到此時的render函數(shù)中已經(jīng)有了import {xxx} from "vue"
了。
這里執(zhí)行的genHoists
函數(shù)就是前面 搞懂 Vue 3 編譯優(yōu)化:靜態(tài)提升的秘密文章中講過的靜態(tài)提升的入口。
生成render函數(shù)中的函數(shù)名稱和參數(shù)
執(zhí)行完genModulePreamble
函數(shù)后,已經(jīng)生成了一條import {xxx} from "vue"
了。我們接著來看generate
函數(shù)中render函數(shù)的函數(shù)名稱和參數(shù)是如何生成的,代碼如下:
const functionName = `render`; const args = ["_ctx", "_cache"]; args.push("$props", "$setup", "$data", "$options"); const signature = args.join(", "); push(`function ${functionName}(${signature}) {`);
上面的代碼很簡單,都是執(zhí)行push
方法向render函數(shù)中添加code字符串,其中args
數(shù)組就是render函數(shù)中的參數(shù)。我們在來看看執(zhí)行完上面這塊代碼后的render函數(shù)字符串是什么樣的,如下圖:
從上圖中可以看到此時已經(jīng)生成了render函數(shù)中的函數(shù)名稱和參數(shù)了。
生成render函數(shù)中return的內(nèi)容
接著來看generate
函數(shù)中最后一塊代碼,如下:
indent(); push(`return `); genNode(ast.codegenNode, context);
首先調(diào)用indent
方法插入一個換行符并且增加一個鎖進,然后執(zhí)行push
方法添加一個return
字符串。
接著以根節(jié)點的codegenNode
屬性為參數(shù)執(zhí)行genNode
函數(shù)生成return中的內(nèi)容,在我們這個場景中genNode
函數(shù)簡化后的代碼如下:
function genNode(node, context) { switch (node.type) { case NodeTypes.SIMPLE_EXPRESSION: genExpression(node, context) break case NodeTypes.INTERPOLATION: genInterpolation(node, context); break; case NodeTypes.VNODE_CALL: genVNodeCall(node, context); break; } }
這里涉及到SIMPLE_EXPRESSION
、INTERPOLATION
和VNODE_CALL
三種AST抽象語法樹node節(jié)點類型:
INTERPOLATION
:表示當(dāng)前節(jié)點是雙大括號節(jié)點,我們這個demo中就是:{{msg}}
這個文本節(jié)點。SIMPLE_EXPRESSION
:表示當(dāng)前節(jié)點是簡單表達式節(jié)點,在我們這個demo中就是雙大括號節(jié)點{{msg}}
中的更里層節(jié)點msg
VNODE_CALL
:表示當(dāng)前節(jié)點是虛擬節(jié)點,比如我們這里第一次調(diào)用genNode
函數(shù)傳入的ast.codegenNode
(根節(jié)點的codegenNode
屬性)就是虛擬節(jié)點。
genVNodeCall
函數(shù)
由于當(dāng)前節(jié)點是虛擬節(jié)點,第一次進入genNode
函數(shù)時會執(zhí)行genVNodeCall
函數(shù)。在我們這個場景中簡化后的genVNodeCall
函數(shù)代碼如下:
const OPEN_BLOCK = Symbol(`openBlock`); const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock`); function genVNodeCall(node, context) { const { push, helper } = context; const { tag, props, children, patchFlag, dynamicProps, isBlock } = node; if (isBlock) { push(`(${helper(OPEN_BLOCK)}(${``}), `); } const callHelper = CREATE_ELEMENT_BLOCK; push(helper(callHelper) + `(`, -2 /* None */, node); genNodeList( // 將參數(shù)中的undefined轉(zhuǎn)換成null genNullableArgs([tag, props, children, patchFlag, dynamicProps]), context ); push(`)`); if (isBlock) { push(`)`); } }
首先判斷當(dāng)前節(jié)點是不是block節(jié)點,由于此時的node為根節(jié)點,所以isBlock
為true。將斷點走進helper
方法,我們來看看helper(OPEN_BLOCK)
返回值是什么。helper
方法的代碼如下:
const helperNameMap = { [OPEN_BLOCK]: `openBlock`, [CREATE_ELEMENT_BLOCK]: `createElementBlock`, [TO_DISPLAY_STRING]: `toDisplayString`, // ...省略 }; helper(key) { return `_${helperNameMap[key]}`; }
helper
方法中的代碼很簡單,這里的helper(OPEN_BLOCK)
返回的就是_openBlock
。
將斷點走到第一個push
方法,代碼如下:
push(`(${helper(OPEN_BLOCK)}(${``}), `);
執(zhí)行完這個push
方法后在debug終端看看此時的render函數(shù)字符串是什么樣的,如下圖:
從上圖中可以看到,此時render函數(shù)中增加了一個_openBlock
函數(shù)的調(diào)用。
將斷點走到第二個push
方法,代碼如下:
const callHelper = CREATE_ELEMENT_BLOCK; push(helper(callHelper) + `(`, -2 /* None */, node);
同理helper(callHelper)
方法返回的是_createElementBlock
,執(zhí)行完這個push
方法后在debug終端看看此時的render函數(shù)字符串是什么樣的,如下圖:
從上圖中可以看到,此時render函數(shù)中增加了一個_createElementBlock
函數(shù)的調(diào)用。
繼續(xù)將斷點走到genNodeList
部分,代碼如下:
genNodeList( genNullableArgs([tag, props, children, patchFlag, dynamicProps]), context );
其中的genNullableArgs
函數(shù)功能很簡單,將參數(shù)中的undefined
轉(zhuǎn)換成null
。比如此時的props
就是undefined
,經(jīng)過genNullableArgs
函數(shù)處理后傳給genNodeList
函數(shù)的props
就是null
。
genNodeList
函數(shù)
繼續(xù)將斷點走進genNodeList
函數(shù),在我們這個場景中簡化后的代碼如下:
function genNodeList(nodes, context, multilines = false, comma = true) { const { push } = context; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (shared.isString(node)) { push(node); } else { genNode(node, context); } if (i < nodes.length - 1) { comma && push(", "); } } }
我們先來看看此時的nodes
參數(shù),如下圖:
這里的nodes
就是調(diào)用genNodeList
函數(shù)時傳的數(shù)組:[tag, props, children, patchFlag, dynamicProps]
,只是將數(shù)組中的undefined
轉(zhuǎn)換成了null
。
nodes
數(shù)組中的第一項為字符串p,表示當(dāng)前節(jié)點是p標簽。- 由于當(dāng)前p標簽沒有props,所以第二項為null的字符串。
- 第三項為p標簽子節(jié)點:{{msg}}
- 第四項也是一個字符串,標記當(dāng)前節(jié)點是否是動態(tài)節(jié)點。
在講genNodeList
函數(shù)之前,我們先來看一下如何使用h
函數(shù)生成一個<p>{{ msg }}</p>
標簽的虛擬DOM節(jié)點。根據(jù)vue官網(wǎng)的介紹,h
函數(shù)定義如下:
// 完整參數(shù)簽名 function h( type: string | Component, props?: object | null, children?: Children | Slot | Slots ): VNode
h
函數(shù)接收的第一個參數(shù)是標簽名稱或者一個組件,第二個參數(shù)是props對象或者null,第三個參數(shù)是子節(jié)點。
所以我們要使用h
函數(shù)生成demo中的p標簽虛擬DOM節(jié)點代碼如下:
h("p", null, msg)
h
函數(shù)生成虛擬DOM實際就是調(diào)用的createBaseVNode
函數(shù),而我們這里的createElementBlock
函數(shù)生成虛擬DOM也是調(diào)用的createBaseVNode
函數(shù)。兩者的區(qū)別是createElementBlock
函數(shù)多接收一些參數(shù),比如patchFlag
和dynamicProps
。
現(xiàn)在我想你應(yīng)該已經(jīng)反應(yīng)過來了,為什么調(diào)用genNodeList
函數(shù)時傳入的第一個參數(shù)nodes
為:[tag, props, children, patchFlag, dynamicProps]
。這個數(shù)組的順序就是調(diào)用createElementBlock
函數(shù)時傳入的參數(shù)順序。
所以在genNodeList
中會遍歷nodes
數(shù)組生成調(diào)用createElementBlock
函數(shù)需要傳入的參數(shù)。
先來看第一個參數(shù)tag
,這里tag
的值為字符串"p"。所以在for循環(huán)中會執(zhí)行push(node)
,生成調(diào)用createElementBlock
函數(shù)的第一個參數(shù)"p"。在debug終端看看此時的render函數(shù),如下圖:
從上圖中可以看到createElementBlock
函數(shù)的第一個參數(shù)"p"
接著來看nodes
數(shù)組中的第二個參數(shù):props
,由于p標簽中沒有props
屬性。所以第二個參數(shù)props
的值為字符串"null",在for循環(huán)中同樣會執(zhí)行push(node)
,生成調(diào)用createElementBlock
函數(shù)的第二個參數(shù)"null"。在debug終端看看此時的render函數(shù),如下圖:
從上圖中可以看到createElementBlock
函數(shù)的第二個參數(shù)null
接著來看nodes
數(shù)組中的第三個參數(shù):children
,由于children
是一個對象,所以以當(dāng)前children節(jié)點作為參數(shù)執(zhí)行genNode
函數(shù)。
這個genNode
函數(shù)前面已經(jīng)執(zhí)行過一次了,當(dāng)時是以根節(jié)點的codegenNode
屬性作為參數(shù)執(zhí)行的?;仡櫼幌?code>genNode函數(shù)的代碼,如下:
function genNode(node, context) { switch (node.type) { case NodeTypes.SIMPLE_EXPRESSION: genExpression(node, context) break case NodeTypes.INTERPOLATION: genInterpolation(node, context); break; case NodeTypes.VNODE_CALL: genVNodeCall(node, context); break; } }
前面我們講過了NodeTypes.INTERPOLATION
類型表示當(dāng)前節(jié)點是雙大括號節(jié)點,而我們這次執(zhí)行genNode
函數(shù)傳入的p標簽children,剛好就是{{msg}}雙大括號節(jié)點。所以代碼會走到genInterpolation
函數(shù)中。
genInterpolation
函數(shù)
將斷點走進genInterpolation
函數(shù)中,genInterpolation
代碼如下:
function genInterpolation(node, context) { const { push, helper } = context; push(`${helper(TO_DISPLAY_STRING)}(`); genNode(node.content, context); push(`)`); }
首先會執(zhí)行push
方法向render函數(shù)中插入一個_toDisplayString
函數(shù)調(diào)用,在debug終端看看執(zhí)行完這個push
方法后的render函數(shù),如下圖:
從上圖中可以看到此時createElementBlock
函數(shù)的第三個參數(shù)只生成了一半,調(diào)用_toDisplayString
函數(shù)傳入的參數(shù)還沒生成。
接著會以node.content
作為參數(shù)執(zhí)行genNode(node.content, context);
生成_toDisplayString
函數(shù)的參數(shù),此時代碼又走回了genNode
函數(shù)。
將斷點再次走進genNode
函數(shù),看看此時的node是什么樣的,如下圖:
從上圖中可以看到此時的node節(jié)點是一個簡單表達式節(jié)點,表達式為:$setup.msg
。所以代碼會走進genExpression
函數(shù)。
genExpression
函數(shù)
接著將斷點走進genExpression
函數(shù)中,genExpression
函數(shù)中的代碼如下:
function genExpression(node, context) { const { content, isStatic } = node; context.push( isStatic ? JSON.stringify(content) : content, -3 /* Unknown */, node ); }
由于當(dāng)前的msg
變量是一個ref
響應(yīng)式變量,所以isStatic
為false
。所以會執(zhí)行push
方法,將$setup.msg
插入到render函數(shù)中。
執(zhí)行完push
方法后,在debug終端看看此時的render函數(shù)字符串是什么樣的,如下圖:
從上圖中可以看到此時的render函數(shù)基本已經(jīng)生成了,剩下的就是調(diào)用push
方法生成各個函數(shù)的右括號")"和右花括號"}"。將斷點逐層走出,直到generate
函數(shù)中。代碼如下:
function generate(ast) { // ...省略 genNode(ast.codegenNode, context); deindent(); push(`}`); return { ast, code: context.code, }; }
執(zhí)行完最后一個 push
方法后,在debug終端看看此時的render函數(shù)字符串是什么樣的,如下圖:
從上圖中可以看到此時的render函數(shù)終于生成啦!
總結(jié)
這是我畫的我們這個場景中generate
生成render函數(shù)的流程圖:
- 執(zhí)行
genModulePreamble
函數(shù)生成:import { xxx } from "vue";
- 簡單字符串拼接生成render函數(shù)中的函數(shù)名稱和參數(shù),也就是
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
- 以根節(jié)點的
codegenNode
屬性為參數(shù)調(diào)用genNode
函數(shù)生成render函數(shù)中return的內(nèi)容。- 此時傳入的是虛擬節(jié)點,執(zhí)行
genVNodeCall
函數(shù)生成return _openBlock(), _createElementBlock(
和調(diào)用genNodeList
函數(shù),生成createElementBlock
函數(shù)的參數(shù)。 - 處理p標簽的
tag
標簽名和props
,生成createElementBlock
函數(shù)的第一個和第二個參數(shù)。此時render函數(shù)return的內(nèi)容為:return _openBlock(), _createElementBlock("p", null
- 處理p標簽的children也就是
{{msg}}
節(jié)點,再次調(diào)用genNode
函數(shù)。此時node節(jié)點類型為雙大括號節(jié)點,調(diào)用genInterpolation
函數(shù)。 - 在
genInterpolation
函數(shù)中會先調(diào)用push
方法,此時的render函數(shù)return的內(nèi)容為:return _openBlock(), _createElementBlock("p", null, _toDisplayString(
。然后以node.content
為參數(shù)再次調(diào)用genNode
函數(shù)。 node.content
為$setup.msg
,是一個簡單表達式節(jié)點,所以在genNode
函數(shù)中會調(diào)用genExpression
函數(shù)。執(zhí)行完genExpression
函數(shù)后,此時的render函數(shù)return的內(nèi)容為:return _openBlock(), _createElementBlock("p", null, _toDisplayString($setup.msg
- 調(diào)用push方法生成各個函數(shù)的右括號")"和右花括號"}",生成最終的render函數(shù)
- 此時傳入的是虛擬節(jié)點,執(zhí)行
到此這篇關(guān)于原來 Vue 3 的 generate 是這樣生成 render 函數(shù)的的文章就介紹到這了,更多相關(guān)原來vue3中template使用ref無需.value是因為這個內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue項目導(dǎo)入導(dǎo)出文件功能以及導(dǎo)出文件后亂碼問題及解決
這篇文章主要介紹了Vue項目導(dǎo)入導(dǎo)出文件功能以及導(dǎo)出文件后亂碼問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09vue.js 解決v-model讓select默認選中不生效的問題
這篇文章主要介紹了vue.js 解決v-model讓select默認選中不生效的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07談?wù)剬ue響應(yīng)式數(shù)據(jù)更新的誤解
本篇文章主要介紹了談?wù)剬ue響應(yīng)式數(shù)據(jù)更新的誤解,深入了解了vue響應(yīng)式數(shù)據(jù),有興趣的可以了解一下2017-08-08vue中Npm run build 根據(jù)環(huán)境傳遞參數(shù)方法來打包不同域名
這篇文章主要介紹了vue項目中Npm run build 根據(jù)環(huán)境傳遞參數(shù)方法來打包不同域名,使用npm run build --xxx,根據(jù)傳遞參數(shù)xxx來判定不同的環(huán)境,給出不同的域名配置,具體內(nèi)容詳情大家參考下本文2018-03-03vue-openlayers實現(xiàn)地圖坐標彈框效果
這篇文章主要為大家詳細介紹了vue-openlayers實現(xiàn)地圖坐標彈框效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-09-09