编译优化
PatchFlags优化
Diff算法无法避免新旧虚拟DOM中无用的比较操作,通过patchFlags来标记动态内容,可以实现快速diff算法
<div>
<h1>Hello Jiang</h1>
<span>{{name}}</span>
</div>
此template经过模板编译会变成以下代码:
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("h1", null, "Hello Jiang"),
_createTextVNode(),
_createElementVNode("span", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
]))
}
创建虚拟节点
const VueComponent = {
setup(){
let state = reactive({name:'jw'});
setTimeout(() => {
state.name = 'zf'
}, 1000);
return {
...toRefs(state)
}
},
render(_ctx){
return (openBlock(),createElementBlock('div',null,[
createElementVNode("h1", null, "Hello Jiang"),
createElementVNode("span", null, toDisplayString(_ctx.name), 1 /* TEXT */)
]))
}
}
render(h(VueComponent),app)
生成的虚拟DOM是:
{
type: "div",
__v_isVNode: true,
children:[
{type: 'h1', props: null, key: null, …}
{type: Symbol(), props: null, key: null, …}
{type: 'span', props: null, key: null, …}
],
dynamicChildren:[{type: 'span', children: _ctx.name, patchFlag: 1}]
}
此时生成的虚拟节点多出一个dynamicChildren属性。这个就是block的作用,block可以收集所有后代动态节点。这样后续更新时可以直接跳过静态节点,实现靶向更新
动态标识
export const enum PatchFlags {
TEXT = 1, // 动态文本节点
CLASS = 1 << 1, // 动态class
STYLE = 1 << 2, // 动态style
PROPS = 1 << 3, // 除了class\style动态属性
FULL_PROPS = 1 << 4, // 有key,需要完整diff
HYDRATE_EVENTS = 1 << 5, // 挂载过事件的
STABLE_FRAGMENT = 1 << 6, // 稳定序列,子节点顺序不会发生变化
KEYED_FRAGMENT = 1 << 7, // 子节点有key的fragment
UNKEYED_FRAGMENT = 1 << 8, // 子节点没有key的fragment
NEED_PATCH = 1 << 9, // 进行非props比较, ref比较
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
DEV_ROOT_FRAGMENT = 1 << 11,
HOISTED = -1, // 表示静态节点,内容变化,不比较儿子
BAIL = -2 // 表示diff算法应该结束
}
靶向更新实现
export { createVnode as createElementVNode }
let currentBlock = null
export function openBlock(){ // 创建block
currentBlock = []
}
export function closeBlock(){ //关闭block
currentBlock = null;
}
export function createElementBlock(type,props?,children?,patchFlag?){ // 创建block元素
return setupBlock(createVNode(type,props,children,patchFlag))// 将动态元素挂载到block节点上
}
export function setupBlock(vnode){
vnode.dynamicChildren = currentBlock;
closeBlock();
return vnode;
}
export function createTextVNode(text: ' ', flag = 0) { // 创建文本虚拟节点
return createVNode(Text, null, text, flag)
}
export function toDisplayString(val){ // 就是JSON.stringify
return isString(val)? val : val == null ? '' :isObject(val)? JSON.stringify(val): String(val);
}
export const createVNode = (type,props,children = null,patchFlag =0)=>{
// ...
if(currentBlock && vnode.patchFlag > 0){
currentBlock.push(vnode);
}
return vnode;
}
靶向更新
const patchElement = (n1,n2) =>{ // 比较两个元素的差异
let el = (n2.el = n1.el);
const oldProps = n1.props || {}
const newProps = n2.props || {}
let {patchFlag} = n2;
if(patchFlag){ // 单独处理标识属性
if(patchFlag & PatchFlags.CLASS){
if(oldProps.class !== newProps.class){
hostPatchProp(el,'class',null,newProps.class);
}
}
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children)
}
}
}else{ // 处理所有属性
patchProps(oldProps,newProps,el);
}
if(n2.dynamicChildren){ // 比较动态节点
patchBlockChildren(n1,n2);
}else{
patchChildren(n1,n2,el);
}
}
function patchBlockChildren(n1,n2){
for(let i = 0 ; i < n2.dynamicChildren.length;i++){
patchElement(n1.dynamicChildren[i],n2.dynamicChildren[i]);
}
}
由此可以看出性能被大幅度提升,从tree级别的比对,变成了线性结构比对。
BlockTree
为什么我们还要提出blockTree的概念? 只有block不就挺好的么? 问题出在block在收集动态节点时是忽略虚拟DOM树层级的。
<div>
<p v-if="flag">
<span>{{a}}</span>
</p>
<div v-else>
<span>{{a}}</span>
</div>
</div>
这里我们知道默认根节点是一个block节点,如果要是按照之前的套路来搞,这时候切换flag的状态将无法从p标签切换到div标签。 解决方案:就是将不稳定的结构也作为block来进行处理
不稳定结构
所谓的不稳结构就是DOM树的结构可能会发生变化。不稳定结构有哪些呢? (v-if/v-for/Fragment)
v-if
<div>
<div v-if="flag">
<span>{{a}}</span>
</div>
<div v-else>
<p><span>{{a}}</span></p>
</div>
</div>
编译后的结果:
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
(_ctx.flag)
? (_openBlock(), _createElementBlock("div", { key: 0 }, [
_createElementVNode("span", null, _toDisplayString(_ctx.a), 1 /* TEXT */)
]))
: (_openBlock(), _createElementBlock("div", { key: 1 }, [
_createElementVNode("p", null, [
_createElementVNode("span", null, _toDisplayString(_ctx.a), 1 /* TEXT */)
])
]))
]))
}
Block(div)
Blcok(div,{key:0})
Block(div,{key:1})
父节点除了会收集动态节点之外,也会收集子block。 更新时因key值不同会进行删除重新创建
v-for
随着v-for变量的变化也会导致虚拟DOM树变得不稳定
<div>
<div v-for="item in fruits">{{item}}</div>
</div>
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.fruits, (item) => {
return (_openBlock(), _createElementBlock("div", null, _toDisplayString(item), 1 /* TEXT */))
}), 256 /* UNKEYED_FRAGMENT */))
}
可以试想一下,如果不增加这个block,前后元素不一致是无法做到靶向更新的。因为dynamicChildren中还有可能有其他层级的元素。同时这里还生成了一个Fragment,因为前后元素个数不一致,所以称之为不稳定序列。
稳定Fragment
这里是可以靶向更新的, 因为稳定则有参照物
<div>
<div v-for="item in 3">{{item}}</div>
</div>
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(), _createElementBlock(_Fragment, null, _renderList(3, (item) => {
return _createElementVNode("div", null, _toDisplayString(item), 1 /* TEXT */)
}), 64 /* STABLE_FRAGMENT */))
]))
}
静态提升
<div>
<span>hello</span>
<span a=1 b=2>{{name}}</span>
<a><span>{{age}}</span></a>
</div>
我们把模板直接转化成render函数是这个酱紫的,那么问题就是每次调用render函数都要重新创建虚拟节点。
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "hello"),
_createElementVNode("span", {
a: "1",
b: "2"
}, _toDisplayString(_ctx.name), 1 /* TEXT */),
_createElementVNode("a", null, [
_createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
])
]))
}
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello", -1 /* HOISTED */)
const _hoisted_2 = {
a: "1",
b: "2"
}
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createElementVNode("span", _hoisted_2, _toDisplayString(_ctx.name), 1 /* TEXT */),
_createElementVNode("a", null, [
_createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
])
]))
}
静态提升则是将静态的节点或者属性提升出去。静态提升是以树为单位。也就是说树中节点有动态的不会进行提升。
预字符串化
静态提升的节点都是静态的,我们可以将提升出来的节点字符串化。 当连续静态节点超过20个时,会将静态节点序列化为字符串。
<div>
<span></span>
...
...
<span></span>
</div>
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>....</span>", 20)
缓存函数
<div @click="e=>v=e.target.value"></div>
每次调用render的时都要创建新函数
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", {
onClick: e=>_ctx.v=e.target.value
}, null, 8 /* PROPS */, ["onClick"]))
}
开启函数缓存后,函数会被缓存起来,后续可以直接使用
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", {
onClick: _cache[0] || (_cache[0] = e=>_ctx.v=e.target.value)
}))
}
模版转化AST语法树
模板编译 Vue中对template属性会编译成render方法。在线模板编译器
增添新的包compiler-core/package.json
{
"name": "@vue/compiler-core",
"version": "1.0.0",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
"buildOptions": {
"name": "VueCompilerCore",
"compat": true,
"formats": [
"esm-bundler",
"cjs"
]
}
}
我们将开发环境下的打包入口改为 compile-core,这里我们先提供所需要ast的节点类型
export function compile(template){
// 1.将模板转化成ast语法树
const ast = baseParse(template);
// 2.对ast语法树进行转化
transform(ast);
// 3.生成代码
return generate(ast)
}
生成ast语法树
准备语法树相关type
export const enum NodeTypes {
ROOT, // 根节点
ELEMENT, // 元素
TEXT, // 文本
COMMENT, // 注释
SIMPLE_EXPRESSION, // 简单表达式
INTERPOLATION, // 模板表达式
ATTRIBUTE,
DIRECTIVE,
// containers
COMPOUND_EXPRESSION, // 复合表达式
IF,
IF_BRANCH,
FOR,
TEXT_CALL, // 文本调用
// codegen
VNODE_CALL, // 元素调用
JS_CALL_EXPRESSION, // js调用表达式
}
创建解析上下文
创建解析上下文,并且根据类型做不同的处理解析。 ast转化
function createParserContext(content) {
return {
line: 1,
column: 1,
offset: 0,
source: content, // source会不停的被截取
originalSource: content // 原始内容
}
}
function isEnd(context) {
const source = context.source;
return !source;
}
function parseChildren(context) {
const nodes = [];
while (!isEnd(context)) {
const s = context.source;
let node;
if (s.startsWith('{{')){ // 处理表达式类型
}else if(s[0] === '<'){ // 标签的开头
if(/[a-z]/i.test(s[1])){} // 开始标签
}
if(!node){ // 文本的处理
}
nodes.push(node);
}
return nodes;
}
function baseParse(template){
const context = createParserContext(template);
return parseChildren(context);
}
处理文本节点
采用假设法获取文本结束位置
function parseText(context) { // 123123{{name}}</div>
const endTokens = ['<', '{{'];
let endIndex = context.source.length; // 文本的总长度
// 假设遇到 < 就是文本的结尾 。 在假设遇到{{ 是文本结尾。 最后找离的近的
// 假设法
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i], 1);
if (index !== -1 && endIndex > index) {
endIndex = index;
}
}
}
处理文本内容,删除匹配到的结果,计算最新上下文位置信息
function parseText(context) {
// ...
let start = getCursor(context); // 1.获取文本开始位置
const content = parseTextData(context, endIndex); // 2.处理文本数据
return {
type: NodeTypes.TEXT,
content,
loc: getSelection(context, start) // 3.获取全部信息
}
}
function getCursor(context) { // 获取当前位置
let { line, column, offset } = context;
return { line, column, offset }
}
function parseTextData(context, endIndex) {
const rawText = context.source.slice(0, endIndex);
advanceBy(context, endIndex); // 截取内容
return rawText
}
function advanceBy(context, endIndex) {
let s = context.source;
advancePositionWithMutation(context, s, endIndex) // 更改位置信息
context.source = s.slice(endIndex);
}
function advancePositionWithMutation(context, s, endIndex) { // 更新最新上下文信息
let linesCount = 0; // 计算行数
let linePos = -1; // 计算其实行开始位置
for (let i = 0; i < endIndex; i++) {
if (s.charCodeAt(i) === 10) { // 遇到\n就增加一行
linesCount++;
linePos = i; // 记录换行后的字节位置
}
}
context.offset += endIndex; // 累加偏移量
context.line += linesCount; // 累加行数
// 计算列数,如果无换行,则直接在原列基础 + 文本末尾位置,否则 总位置减去换行后的字节位置
context.column = linePos == -1 ? context.column + endIndex : endIndex - linePos
}
function getSelection(context,start){
const end = getCursor(context);
return {
start,
end,
source:context.originalSource.slice(start.offset,end.offset)
}
}
转化成最终ast节点结果,标记ast节点类型
处理表达式节点
获取表达式中的变量,计算表达式的位置信息
function parseInterpolation(context) {
const start = getCursor(context); // 获取表达式的开头位置
const closeIndex = context.source.indexOf('}}', '{{'); // 找到结束位置
advanceBy(context, 2); // 去掉 {{
const innerStart = getCursor(context); // 计算里面开始和结束
const innerEnd = getCursor(context);
const rawContentLength = closeIndex - 2; // 拿到内容
const preTrimContent = parseTextData(context, rawContentLength);
const content = preTrimContent.trim();
const startOffest = preTrimContent.indexOf(content);
if (startOffest > 0) { // 有空格
advancePositionWithMutation(innerStart, preTrimContent, startOffest); // 计算表达式开始位置
}
const endOffset = content.length + startOffest;
advancePositionWithMutation(innerEnd, preTrimContent, endOffset)
advanceBy(context, 2);
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
content,
loc: getSelection(context, innerStart, innerEnd) // 需要修改getSelection方法
},
loc: getSelection(context, start)
}
}
处理元素节点
处理标签
获取标签名称,更新标签位置信息
function advanceSpaces(context){
const match = /^[ \t\r\n]+/.exec(context.source);
if(match){
advanceBy(context,match[0].length);
}
}
function parseTag(context){
const start = getCursor(context); // 获取开始位置
const match = /^</?([a-z][^ \t\r\n/>]*)/.exec(context.source); // 匹配标签名
const tag = match[1];
advanceBy(context,match[0].length); // 删除标签
advanceSpaces(context); // 删除空格
const isSelfClosing = context.source.startsWith('/>'); // 是否是自闭合
advanceBy(context,isSelfClosing?2:1); // 删除闭合 /> >
return {
type:NodeTypes.ELEMENT,
tag,
isSelfClosing,
loc:getSelection(context,start)
}
}
function parseElement(context) {
// 1.解析标签名
let ele = parseTag(context);
if(context.source.startsWith('</')){
parseTag(context); // 解析标签,标签没有儿子,则直接更新标签信息的结束位置
}
ele.loc = getSelection(context,ele.loc.start); // 更新最终位置
return ele;
}
处理子节点
递归处理子节点元素
function isEnd(context) {
const source = context.source;
if(context.source.startsWith('</')){ // 如果遇到结束标签说明没有子节点
return true;
}
return !source;
}
function parseElement(context) {
let ele = parseTag(context);
const children = parseChildren(context); // 因为结尾标签, 会再次触发parseElement,这里如果是结尾需要停止
if(context.source.startsWith('</')){
parseTag(context);
}
ele.loc = getSelection(context,ele.loc.start); // 更新最终位置
(ele as any).children = children; // 添加children
return ele;
}
处理属性
在处理标签后处理属性
function parseTag(context){
const start = getCursor(context);
const match = /^</?([a-z][^ \t\r\n/>]*)/.exec(context.source);
const tag = match[1];
advanceBy(context,match[0].length);
advanceBySpaces(context);
let props = parseAttributes(context); // 处理属性
// ......
return {
type:NodeTypes.ELEMENT,
tag,
isSelfClosing,
loc:getSelection(context,start),
props
}
}
function parseAttributes(context) {
const props: any = [];
while (context.source.length > 0 && !context.source.startsWith('>')) {
const attr = parseAttribute(context)
props.push(attr);
advanceSpaces(context); // 解析一个去空格一个
}
return props
}
function parseAttribute(context) {
const start = getCursor(context);
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
const name = match[0]; // 捕获到属性名
advanceBy(context, name.length); // 删除属性名
let value
if (/^[\t\r\n\f ]*=/.test(context.source)) { // 删除空格 等号
advanceSpaces(context);
advanceBy(context, 1);
advanceSpaces(context);
value = parseAttributeValue(context); // 解析属性值
}
const loc = getSelection(context, start)
return {
type: NodeTypes.ATTRIBUTE,
name,
value: {
type: NodeTypes.TEXT,
content: value.content,
loc: value.loc
},
loc
}
}
function parseAttributeValue(context) {
const start = getCursor(context);
const quote = context.source[0];
let content
const isQuoteed = quote === '"' || quote === "'";
if (isQuoteed) {
advanceBy(context, 1);
const endIndex = context.source.indexOf(quote);
content = parseTextData(context, endIndex); // 解析引号中间的值
advanceBy(context, 1);
}
return { content, loc: getSelection(context, start) }
}
处理空节点
function parseChildren(context) {
const nodes: any = [];
while (!isEnd(context)) {
//....
}
for(let i = 0 ;i < nodes.length; i++){
const node = nodes[i];
if(node.type == NodeTypes.TEXT){ // 如果是文本 删除空白文本,其他的空格变为一个
if(!/[^\t\r\n\f ]/.test(node.content)){
nodes[i] = null
}else{
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
}
}
}
return nodes.filter(Boolean)
}
创建根节点
将解析出的节点,再次进行包裹,这样可以支持模板下多个根节点的情况, 也是我们常说的 Fragment
export function createRoot(children,loc){
return {
type:NodeTypes.ROOT,
children,
loc
}
}
function baseParse(template) {
// 标识节点的信息 行 列 偏移量
const context = createParserContext(template);
const start = getCursor(context);
return createRoot(
parseChildren(context),
getSelection(context,start)
)
}
遍历AST语法树
我们需要遍历ast语法树,访问树中节点进行语法树的转化
function transformElement(node){
console.log('元素处理',node)
}
function transformText(node){
console.log('文本处理',node)
}
function transformExpression(node){
console.log('表达式')
}
function traverseNode(node,context){
context.currentNode = node;
const transforms = context.nodeTransforms;
for(let i = 0; i < transforms.length;i++){
transforms[i](node,context); // 调用转化方法进行转化
if(!context.currentNode) return
}
switch(node.type){
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
for(let i = 0; i < node.children.length;i++){
context.parent = node;
traverseNode(node.children[i],context);
}
}
}
function createTransformContext(root){
const context = {
currentNode:root, // 当前转化节点
parent:null, // 当前转化节点的父节点
nodeTransforms:[ // 转化方法
transformElement,
transformText,
transformExpression
],
helpers: new Map(), // 创建帮助映射表,记录调用方法次数
helper(name){
const count = context.helpers.get(name) || 0;
context.helpers.set(name,count+1)
return name
}
}
return context
}
function transform(root){
// 创建转化的上下文, 记录转化方法及当前转化节点
let context = createTransformContext(root)
// 递归遍历
traverseNode(root,context)
}
export function compile(template){
const ast = baseParse(template);
transform(ast);
}
退出函数
表达式不需要退出函数,直接处理即可。元素需要在遍历完所有子节点在进行处理
function transformExpression(node){
if(node.type == NodeTypes.INTERPOLATION){
console.log('表达式')
}
}
function transformElement(node){
if(node.type === NodeTypes.ELEMENT ){
return function postTransformElement(){ // 元素处理的退出函数
// 如果这个元素
console.log('元素',node)
}
}
}
function transformText(node){
if(node.type === NodeTypes.ELEMENT || node.type === NodeTypes.ROOT){
return ()=>{
console.log('元素/root',node)
}
}
}
function traverseNode(node,context){
// ...
for(let i = 0; i < transforms.length;i++){
let onExit = transforms[i](node,context); // 调用转化方法进行转化
if(onExit){
exitsFns.push(onExit)
}
if(!context.currentNode) return
}
// ...
// 最终context.currentNode 是最里面的
context.currentNode = node; // 修正currentNode;
let i = exitsFns.length
while (i--) {
exitsFns[i]()
}
}
转化表达式
runtimeHelpers.ts
export const TO_DISPLAY_STRING = Symbol(`toDisplayString`);
export const helperNameMap = {
[TO_DISPLAY_STRING]: "toDisplayString",
}
switch(node.type){
case NodeTypes.INTERPOLATION:
context.helper(TO_DISPLAY_STRING); // 用于JSON.stringify
break
// ...
}
最后生成的代码我们取值时需要从上下文中进行取值。
export function transformExpression(node){
if(node.type == NodeTypes.INTERPOLATION){
node.content.content = `_ctx.${node.content.content}`; // 修改content信息
}
}
转化文本元素
文本元素的转化我们需要将多个文本连接起来,如果是动态文本添加patchFlags标识,最终生成文本调用逻辑
function isText(node) {
return node.type == NodeTypes.INTERPOLATION || node.type == NodeTypes.TEXT;
}
export function transformText(node,context){
if(node.type === NodeTypes.ELEMENT || node.type === NodeTypes.ROOT){
return ()=>{
// 如果这个元素
let hasText = false;
const children = node.children;
let currentContainer = undefined // 合并儿子
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (isText(child)) {
hasText = true;
for (let j = i + 1; j < children.length; j++) {
const next = children[j];
if(isText(next)){
if(!currentContainer){
currentContainer = children[i] = { // 合并表达式
type:NodeTypes.COMPOUND_EXPRESSION,
loc:child.loc,
children:[child]
}
}
currentContainer.children.push(` + `,next);
children.splice(j,1);
j--;
}else{
currentContainer = undefined;
break;
}
}
}
}
if (!hasText || children.length == 1) { // 一个元素不用管,可以执行innerHTML
return
}
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (isText(child) || child.type === NodeTypes.COMPOUND_EXPRESSION) {
const callArgs = []
callArgs.push(child)
if (child.type !== NodeTypes.TEXT) { // 如果不是文本
callArgs.push(PatchFlags.TEXT + '')
}
// 全部格式话成文本调用
children[i] = {
type: NodeTypes.TEXT_CALL, // 最终需要变成createTextVnode() 增加patchFlag
content: child,
loc: child.loc,
codegenNode: createCallExpression(context,callArgs) // 创建表达式调用
}
}
}
}
}
}
runtimeHelpers.ts
export const CREATE_TEXT = Symbol(`createTextVNode`)
export const helperNameMap = {
[CREATE_TEXT]:`createTextVNode`
}
ast.ts
export function createCallExpression(context,args){
let callee = context.helper(CREATE_TEXT); // 生成代码时需要createTextVNode方法
return {
callee,
type: NodeTypes.JS_CALL_EXPRESSION,
arguments: args
}
}
转化元素
export function createObjectExpression(properties){
return {
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties
}
}
export function transformElement(node,context){
if(node.type === NodeTypes.ELEMENT ){
return function postTransformElement(){ // 元素处理的退出函数
let vnodeTag = `'${node.tag}'`;
let properties = [];
let props = node.props
for(let i = 0 ; i< props.length; i++){ // 这里属性其实应该在codegen里在处理
properties.push({
key:props[i].name,
value:props[i].value.content
})
}
const propsExpression = props.length > 0 ? createObjectExpression(properties):null
let vnodeChildren = null;
if (node.children.length === 1) {
// 只有一个孩子节点 ,那么当生成 render 函数的时候就不用 [] 包裹
const child = node.children[0];
vnodeChildren = child;
}else{
if(node.children.length > 0){ // 处理儿子节点
vnodeChildren = node.children
}
}
// 代码生成
node.codegenNode = createVNodeCall(context,vnodeTag,propsExpression,vnodeChildren);
}
}
}
runtimeHelpers.ts
export const CREATE_ELEMENT_VNODE = Symbol("createElementVNode");
export const helperNameMap = {
[CREATE_ELEMENT_VNODE]: "createElementVNode", // 创建元素节点标识
}
export function createVNodeCall(context,tag,props,children){
context.helper(CREATE_ELEMENT_VNODE);
return {
type:NodeTypes.VNODE_CALL,
tag,
props,
children
}
}
转化根节点
转化根节点需要添加block节点
export const FRAGMENT = Symbol("FRAGMENT");
export const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock`)
export const OPEN_BLOCK = Symbol(`openBlock`)
export const helperNameMap = {
[FRAGMENT]: "Fragment",
[OPEN_BLOCK]: `openBlock`, // block处理
[CREATE_ELEMENT_BLOCK]: `createElementBlock`
}
function createTransformContext(root){
const context = {
removeHelper(name){
const count = context.helpers.get(name);
if(count){
const currentCount = count - 1;
if(!currentCount){
context.helpers.delete(name);
}else{
context.helpers.set(name,currentCount)
}
}
},
// ...
}
return context
}
function createRootCodegen(root,context){
let {children } = root
if(children.length == 1){
const child = children[0];
if (child.type === NodeTypes.ELEMENT && child.codegenNode) {
const codegenNode = child.codegenNode;
root.codegenNode = codegenNode;
context.removeHelper(CREATE_ELEMENT_VNODE); // 不要创建元素
context.helper(OPEN_BLOCK)
context.helper(CREATE_ELEMENT_BLOCK); // 创建元素block就好了
root.codegenNode.isBlock = true; // 只有一个元素节点,那么他就是block节点
} else {
root.codegenNode = child; // 直接用里面的节点换掉
}
}else{
root.codegenNode = createVNodeCall(context,context.helper(FRAGMENT),undefined,root.children)
context.helper(OPEN_BLOCK)
context.helper(CREATE_ELEMENT_BLOCK)
root.codegenNode.isBlock = true; // 增加block fragment
}
}
export function transform(root){
// 创建转化的上下文, 记录转化方法及当前转化节点
let context = createTransformContext(root)
// 递归遍历
traverseNode(root,context)
createRootCodegen(root,context); // 生成根节点的codegen
root.helpers = [...context.helpers.keys()]
}
整个transform的流程,就是给ast节点添加codegenNode属性,用于方便生成对应的代码,并且收集生成代码所需的方法。
创建生成上下文
生成代码时我们需要将生成的代码拼接成字符串,同时添加换行缩进等。
function createCodegeContext(){
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
}
function generate(ast){
const context = createCodegeContext();
}
export function compile(template){
// 1.将模板转化成ast语法树
const ast = baseParse(template);
// 2.对ast语法树进行转化
transform(ast);
// // 3.生成代码
return generate(ast)
}
生成文本代码
let r = compile('hello, jw')
1
如果仅仅是文本直接返回即可~
function genText(node,context){
context.push(JSON.stringify(node.content)) // 添加文本代码
}
function genFunctionPreamble(ast,context){ // 生成函数
const {push,newline} = context
if(ast.helpers.length > 0){ // 生成导入语句
push(`const {${ast.helpers.map((s)=>`${helperNameMap[s]}:_${helperNameMap[s]}`).join(', ')}} = Vue`)
}
newline()
push(`return `)
}
function genNode(node,context){
switch(node.type){
case NodeTypes.TEXT:
genText(node,context)
break;
}
}
function generate(ast){
const context = createCodegeContext();
const {push,indent} = context
genFunctionPreamble(ast,context);
const functionName = 'render';
const args = ['_ctx','$props'];
push(`function ${functionName}(${args.join(', ')}){`)
indent();
push(`return `)
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push(`null`)
}
return context.code
}
生成表达式代码
let r = compile('{{age}}')
function genExpression(node,context){
const { content } = node
context.push(content)
}
function genInterpolation(node,context){
const { push, helper } = context
push(`${helper(TO_DISPLAY_STRING)}(`)
genNode(node.content, context)
push(`)`)
}
function genNode(node,context){
switch(node.type){
case NodeTypes.TEXT:
genText(node,context)
break;
case NodeTypes.INTERPOLATION: // 生成表达式
genInterpolation(node,context)
break;
case NodeTypes.SIMPLE_EXPRESSION: // 简单表达式的处理
genExpression(node, context)
break
}
}
生成元素表达式
let r = compile(`<div a='1' b='2'>123</div>`)
function genNodeList(nodes,context){ // 生成节点列表,用","分割
const { push } = context;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isString(node)) {
push(`${node}`); // 如果是字符串直接放入
}else if(Array.isArray(node)){
genNodeList(node, context)
} else {
genNode(node, context);
}
if (i < nodes.length - 1) {
push(", ");
}
}
}
function genVNodeCall(node,context){
const {push,helper} = context;
const {tag,props,children,isBlock} = node
if(isBlock){
push(`(${helper(OPEN_BLOCK)}(),`)
}
// 生成createElementBlock或者createElementVnode
const callHelper = isBlock ? CREATE_ELEMENT_BLOCK: CREATE_ELEMENT_VNODE;
push(helper(callHelper));
push('(');
genNodeList([tag, props, children].map(item=>item || 'null'), context);
push(`)`)
if(isBlock){
push(`)`)
}
}
function genNode(node,context){
switch(node.type){
case NodeTypes.VNODE_CALL: // 元素调用
genVNodeCall(node,context);
break;
}
}
生成元素属性
function genObjectExpression(node, context) {
const { push, newline } = context
const { properties } = node
if (!properties.length) {
push(`{}`)
return
}
push('{')
for (let i = 0; i < properties.length; i++) {
const { key, value } = properties[i]
// key
push(key);
push(`: `)
push(JSON.stringify(value));
// value
if (i < properties.length - 1) {
push(`,`)
}
}
push('}')
}
function genNode(node,context){
switch(node.type){
case NodeTypes.JS_OBJECT_EXPRESSION: // 元素属性
genObjectExpression(node, context)
break
}
}
处理Fragment情况
如果是Fragment,则将字符串和symbol直接放入代码中,对元素再次进行递归处理
let r = compile(`<div>123</div><div>123</div>`)
function genNode(node,context){
if (isString(node)) {
context.push(node)
return
}
if (isSymbol(node)) {
context.push(context.helper(node))
return
}
switch(node.type){
// ...
case NodeTypes.ELEMENT:
genNode(node.codegenNode,context)
}
}
处理复合表达式
let r = compile(`<div>123 {{abc}}</div>`)
function genCompoundExpression(node,context) {
for (let i = 0; i < node.children!.length; i++) {
const child = node.children![i]
if (isString(child)) {
context.push(child)
} else {
genNode(child, context)
}
}
}
function genNode(node,context){
switch(node.type){
// ...
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context)
break
}
}
复合表达式+元素处理#
let r = compile(`<div>123 {{name}} <span>{{name}}</span></div>`)
function genCallExpression(node, context) {
const { push, helper } = context
const callee = helper(node.callee)
push(callee + `(`, node)
genNodeList(node.arguments, context)
push(`)`)
}
function genNode(node,context){
switch(node.type){
// ...
case NodeTypes.TEXT_CALL: // 对文本处理
genNode(node.codegenNode, context)
break
case NodeTypes.JS_CALL_EXPRESSION: // 表达式处理
genCallExpression(node, context)
break
}
}