跳到主要内容

模拟实现

2024年03月13日
柏拉文
越努力,越幸运

一、packages/vue-compat/src/index.ts 导出 compile


import { compile } from '@vue/compiler-dom';

function compileToFunction(template, options?) {
const { code } = compile(template, options);

const render = new Function(code)();
return render;
}

export { compileToFunction as compile };

二、packages/compiler-core/src/compile.ts


import { extend } from '@vue/shared';
import { baseParse } from './parse';
import { transform } from './transform';
import { transformText } from './transforms/transformText';
import { transformElement } from './transforms/transformElement';
import { generate } from './codegen';
import { transformIf } from './transforms/vif';

export function baseCompile(template, options = {}) {
const ast = baseParse(template);

transform(
ast,
extend(options, {
nodeTransforms: [transformElement, transformText, transformIf]
})
);

return generate(ast);
}

三、packages/compiler-core/src/parse.ts


import { ElementTypes, NodeTypes } from './ast';

export enum TextModes {
DATA,
RCDATA,
RAWTEXT,
CDATA
}

export enum TagType {
Start,
End
}

function createParserContext(content, options?) {
return {
source: content
};
}

export function baseParse(content, options?) {
const context = createParserContext(content, options);
return createRoot(parseChildren(context, TextModes.DATA, []));
}

export function createRoot(children) {
return {
children,
loc: {},
type: NodeTypes.ROOT
};
}

export function parseChildren(context, mode, ancestors) {
const nodes = [];
while (!isEnd(context, mode, ancestors)) {
const s = context.source;
let node;
if (startsWith(s, '{{')) {
node = parseInterpolation(context);
} else if (s[0] === '<') {
if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors);
}
}

if (!node) {
node = parseText(context);
}
pushNode(nodes, node);
}
return nodes;
}

export function isEnd(context, mode, ancestors) {
const s = context.source;

switch (mode) {
case TextModes.DATA:
if (startsWith(s, '</')) {
for (let i = ancestors.length - 1; i >= 0; i--) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true;
}
}
}
break;
}

return !s;
}

export function startsWith(source, searchString) {
return source.startsWith(searchString);
}

export function startsWithEndTagOpen(source, tag) {
return startsWith(source, '</');
}

export function parseElement(context, ancestors) {
const element = parseTag(context, TagType.Start);

ancestors.push(element);
const children = parseChildren(context, TextModes.DATA, ancestors);
ancestors.pop();
element.children = children;

if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End);
}

return element;
}

export function parseInterpolation(context) {
const [open, close] = ['{{', '}}'];
advanceBy(context, open.length);

const closeIndex = context.source.indexOf(close, open.length);
const preTrimContent = parseTextData(context, closeIndex);
const content = preTrimContent.trim();
advanceBy(context, close.length);

return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
content
}
};
}

export function pushNode(nodes, node) {
nodes.push(node);
}

export function parseText(context) {
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;
}
}
const content = parseTextData(context, endIndex);
return {
content,
type: NodeTypes.TEXT
};
}

export function parseTag(context, type) {
const match = /^<\/?([a-z][^\r\n\t\f />]*)/i.exec(context.source)!;
const tag = match[1];

advanceBy(context, match[0].length);

// 属性和指令的处理
advanceSpaces(context);
let props = parseAttributes(context, type);

let isSelfClosing = startsWith(context.source, '/>');
advanceBy(context, isSelfClosing ? 2 : 1);

return {
tag,
props,
children: [],
type: NodeTypes.ELEMENT,
tagType: ElementTypes.ELEMENT
};
}

export function advanceBy(context, numberOfCharacters) {
const { source } = context;
context.source = source.slice(numberOfCharacters);
}

export function parseTextData(context, length) {
const rawText = context.source.slice(0, length);
advanceBy(context, length);
return rawText;
}

export function advanceSpaces(context) {
const match = /^[\t\r\n\f\s]+/.exec(context.source);
if (match) {
advanceBy(context, match[0].length);
}
}

export function parseAttributes(context, type) {
const props: any[] = [];
const attributeNames = new Set();

while (
context.source.length > 0 &&
!startsWith(context.source, '>') &&
!startsWith(context.source, '/>')
) {
const attr = parseAttribute(context, attributeNames);
if (type === TagType.Start) {
props.push(attr);
}
advanceSpaces(context);
}

return props;
}

export function parseAttribute(context, nameSet) {
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!;
const name = match[0];
nameSet.add(name);
advanceBy(context, name.length);
let value: any = undefined;
if (/^[\t\r\n\f ]*=/.test(context.source)) {
advanceSpaces(context);
advanceBy(context, 1);
advanceSpaces(context);
value = parseAttributeValue(context);
}
// v- 指令
if (/^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) {
const match =
/(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
name
)!;
let dirName = match[1];
return {
type: NodeTypes.DIRECTIVE,
name: dirName,
exp: value && {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
loc: {}
},
art: undefined,
modifiers: undefined
};
}

return {
type: NodeTypes.ATTRIBUTE,
name,
value: value && {
type: NodeTypes.TEXT,
content: value.content,
loc: {}
},
loc: {}
};
}

export function parseAttributeValue(context) {
let content = '';

const quote = context.source[0];
const isQuoted = quote === `"` || quote === `'`;
if (isQuoted) {
advanceBy(context, 1);
const endIndex = context.source.indexOf(quote);
if (endIndex === -1) {
content = parseTextData(context, context.source.length);
} else {
content = parseTextData(context, endIndex);
advanceBy(context, 1);
}
}

return {
content,
loc: {},
isQuoted
};
}

四、packages/compiler-core/src/transform.ts


import { isArray, isString } from '@vue/shared';
import { NodeTypes } from './ast';
import { TO_DISPLAY_STRING } from './runtimeHelper';
import { isSingleElementRoot } from './transforms/hoistStatic';

export function transform(root, options) {
const context = createTransformContext(root, options);
traverseNode(root, context);

createRootCodegen(root);

root.helpers = [...context.helpers.keys()];
root.components = [];
root.directives = [];
root.imports = [];
root.hoists = [];
root.temps = [];
root.cached = [];
}

export function createTransformContext(root, { nodeTransforms }) {
const context: { [key: string]: any } = {
nodeTransforms,
root,
helpers: new Map(),
currentNode: root,
parent: null,
childIndex: 0,
helper(name) {
const count = context.helpers.get(name) || 0;
context.helpers.set(name, count + 1);
return name;
},
replaceNode(node) {
context.parent.children[context.childIndex] = context.currentNode = node;
}
};

return context;
}

export function traverseNode(node, context) {
context.currentNode = node;
const { nodeTransforms } = context;
const exitFns: any[] = [];

for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context);
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit);
} else {
exitFns.push(onExit);
}
}

if (!context.currentNode) {
return;
} else {
node = context.currentNode;
}
}

switch (node.type) {
case NodeTypes.IF_BRANCH:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context);
break;
case NodeTypes.INTERPOLATION:
context.helper(TO_DISPLAY_STRING);
break;
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context);
}
break;
}

context.currentNode = node;

let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}

export function traverseChildren(parent, context) {
parent.children.forEach((node, index) => {
context.parent = parent;
context.childIndex = index;
traverseNode(node, context);
});
}

export function createRootCodegen(root) {
const { children } = root;

if (children.length === 1) {
const child = children[0];
if (isSingleElementRoot(root, child)) {
root.codegenNode = child.codegenNode;
}
}
}

export function createStructuralDirectiveTransform(name, fn) {
const matches = isString(name) ? n => n === name : n => name.test(n);
return (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
const { props } = node;
const exitFns: any[] = [];
for (let i = 0; i < props.length; i++) {
const prop = props[i];
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
props.splice(i, 1);
i--;
const onExit = fn(node, prop, context);
if (onExit) {
exitFns.push(onExit);
}
}
}
return exitFns;
}
};
}

五、packages/compiler-core/src/transforms/vif.ts


import { isString } from '@vue/shared';
import {
NodeTypes,
createCallExpression,
createConditionalExpression,
createObjectExpression,
createObjectProperty,
createSimpleExpression
} from '../ast';
import { createStructuralDirectiveTransform } from '../transform';
import { getMemoedVNodeCall, injectProp } from '../utils';
import { CREATE_COMMENT } from '../runtimeHelper';

export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
let key = 0;

return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(branch, key, context);
}
};
});
}
);

export function processIf(node, dir, context, processCodegen) {
if (dir.name === 'if') {
const branch = createIfBranch(node, dir);
const ifNode = {
type: NodeTypes.IF,
loc: {},
branches: [branch]
};

context.replaceNode(ifNode);

if (processCodegen) {
return processCodegen(ifNode, branch, true);
}
}
}

function createIfBranch(node, dir) {
return {
type: NodeTypes.IF_BRANCH,
loc: {},
condition: dir.exp,
children: [node]
};
}

export function createCodegenNodeForBranch(branch, keyIndex, context) {
if (branch.condition) {
return createConditionalExpression(
branch.condition,
createChildrenCodegenNode(branch, keyIndex),
createCallExpression(context.helper(CREATE_COMMENT), ['"v-if"', 'true'])
);
} else {
return createChildrenCodegenNode(branch, keyIndex);
}
}

function createChildrenCodegenNode(branch, keyIndex) {
const keyProperty = createObjectProperty(
`key`,
createSimpleExpression(`${keyIndex}`, false)
);

const { children } = branch;
const firstChild = children[0];
const ret = firstChild.codegenNode;
const vnodeCall = getMemoedVNodeCall(ret);

injectProp(vnodeCall, keyProperty);
return ret;
}

六、packages/compiler-core/src/codegen.ts


import { isArray, isString } from '@vue/shared';
import { NodeTypes, getVNodeHelper } from './ast';
import { TO_DISPLAY_STRING, helperNameMap } from './runtimeHelper';

const aliasHelper = s => `${helperNameMap[s]}: _${helperNameMap[s]}`;

function createCodegenContext(ast) {
const context = {
code: '',
runtimeGlobalName: 'Vue',
source: ast.loc.source,
indentLevel: 0,
isSSR: false,
helper(key) {
return `_${helperNameMap[key]}`;
},
push(code) {
context.code += code;
},
newline() {
newline(context.indentLevel);
},
indent() {
newline(++context.indentLevel);
},
deindent() {
newline(--context.indentLevel);
}
};

function newline(n) {
context.code += '\n' + ` `.repeat(n);
}

return context;
}

export function generate(ast, options?) {
const context = createCodegenContext(ast) as any;

const { push, newline, indent, deindent } = context;

genFunctionPreamble(context);

const functionName = `render`;
const args = ['_ctx', '_cache'];
const signature = args.join(', ');
push(`function ${functionName}(${signature}) {`);
indent();

push(`with (_ctx) {`);
indent();

const hasHelpers = ast.helpers.length > 0;
if (hasHelpers) {
push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = _Vue`);
push('\n');
newline();
}

newline();
push(`return `);
if (ast.codegenNode) {
genNode(ast.codegenNode, context);
} else {
push(`null`);
}

deindent();
push('}');

deindent();
push('}');

return {
ast,
code: context.code
};
}

function genFunctionPreamble(context) {
const { push, newline, runtimeGlobalName } = context;
const VueBinding = runtimeGlobalName;
push(`const _Vue = ${VueBinding} \n`);
newline();
push(`return `);
}

function genNode(node, context) {
switch (node.type) {
case NodeTypes.IF:
case NodeTypes.ELEMENT:
genNode(node.codegenNode, context);
break;
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context);
break;
case NodeTypes.TEXT:
genText(node, context);
break;
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context);
break;
case NodeTypes.INTERPOLATION:
genInterpolation(node, context);
break;
case NodeTypes.COMPOUND_EXPRESSION:
genCompundExpression(node, context);
break;
case NodeTypes.ELEMENT:
genNode(node.codegenNode, context);
break;
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context);
break;
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context);
break;
}
}

function genVNodeCall(node, context) {
const { push, helper } = context;
const {
tag,
props,
children,
patchFlag,
dynamicProps,
direcctives,
isBlock,
disableTracking,
isComponent
} = node;

const callHelper = getVNodeHelper(context.isSSR, isComponent);
push(helper(callHelper) + `(`);
const args = genNullableArgs([tag, props, children, patchFlag, dynamicProps]);
genNodeList(args, context);
push(')');
}

function genText(node, context) {
context.push(JSON.stringify(node.content));
}

function genExpression(node, context) {
const { content, isStatic } = node;
context.push(isStatic ? JSON.stringify(content) : content);
}

function genInterpolation(node, context) {
const { push, helper } = context;
push(`${helper(TO_DISPLAY_STRING)}(`);
genNode(node.content, context);
push(')');
}

function genNullableArgs(args) {
let i = args.length;
while (i--) {
if (args[i] != null) {
break;
}
}

return args.slice(0, i + 1).map(arg => arg || 'null');
}

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 (isArray(node)) {
genNodeListArray(node, context);
} else {
genNode(node, context);
}

if (i < nodes.length - 1) {
push(`, `);
}
}
}

function genNodeListArray(nodes, context) {
context.push('[');
genNodeList(nodes, context);
context.push(']');
}

function genCompundExpression(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 genConditionalExpression(node, context) {
const { test, alternate, consequent, newline: needNewLine } = node;
const { push, indent, deindent, newline } = context;
if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
genExpression(test, context);
}
needNewLine && indent();
context.indentLevel++;
needNewLine || push(` `);
push(`?`);
genNode(consequent, context);
context.indentLevel--;
needNewLine && newline();
needNewLine || push(` `);
push(`: `);
const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION;
if (!isNested) {
context.indentLevel++;
}
genNode(alternate, context);
if (!isNested) {
context.indentLevel--;
}
needNewLine && deindent(true);
}

function genCallExpression(node, context) {
const { push, helper } = context;
const callee = isString(node.callee) ? node.callee : helper(node.callee);
push(callee + `(`);
genNodeList(node.arguments, context);
push(`)`);
}

七、packages/compiler-core/src/runtimeHelper.ts


export const CREATE_VNODE = Symbol('createVNode');
export const CREATE_COMMENT = Symbol('createCommentVNode');
export const TO_DISPLAY_STRING = Symbol('to_display_string');
export const CREATE_ELEMENT_VNODE = Symbol('createElementVNode');

export const helperNameMap = {
[CREATE_VNODE]: 'createVNode',
[CREATE_COMMENT]: 'createCommentVNode',
[TO_DISPLAY_STRING]: 'toDisplayString',
[CREATE_ELEMENT_VNODE]: 'createElementVNode'
};

测试用例


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3 Next Mini</title>
<script src="../packages/vue/dist/vue.iife.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h, render, compile } = Vue;
const template = `<div>Hello World <h1 v-if="isShow">你好,世界</h1></div>`;

const renderFn = compile(template);

const component = {
data() {
return {
isShow: false
};
},
render: renderFn,
created() {
setTimeout(() => {
this.isShow = true;
}, 4000);
}
};

const vnode = h(component);
render(vnode, document.querySelector('#app'));
</script>
</body>
</html>