携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第22天,点击查看活动详情
从 AST
到渲染函数代码,vue
经过了 transform
, codegen
两个步骤。为了节约代码,合一起实现了。
例子
Counter
<div>{{count}}</div>
<button @click="add">click</button>
————————demo 转
[
h(Text,null,'Counter'),
h('div',null,count),
h('button',{ onClick: add }, "click")
]
————————vue3 原
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createTextVNode("Counter\r\n"),
_createElementVNode("div", null, _toDisplayString(_ctx.count), 1 /* TEXT */),
_createTextVNode(),
_createElementVNode("button", { onClick: _ctx.add }, "click", 8 /* PROPS */, ["onClick"])
], 64 /* STABLE_FRAGMENT */))
}
回顾下之前的AST
<div id="foo" v-if="ok">hello {{name}}</div>
{
"type": "ROOT",
"children": [
{
"type": "ELEMENT",
"tag": "div",
"tagType": "ELEMENT",
"props": [
{
"type": "ATTRIBUTE",
"name": "id",
"value": { "type": "TEXT", "content": "foo" }
}
],
"directives": [
{
"type": "DIRECTIVE",
"name": "if",
"exp": {
"type": "SIMPLE_EXPRESSION",
"content": "ok",
"isStatic": false
}
}
],
"isSelfClosing": false,
"children": [
{ "type": "TEXT", "content": "hello " },
{
"type": "INTERPOLATION",
"content": {
"type": "SIMPLE_EXPRESSION",
"isStatic": false,
"content": "name"
}
}
]
}
]
}
transform
开始转化
// node : ast树
export function traverseNode(node) {
switch (node.type) {
case NodeTypes.ROOT:
// 一开始是根节点,如果子节点只有一个就解析一个子节点 用traverseNode
if (node.children.length === 1) {
return traverseNode(node.children[0], node);
}
//如果多个就 解析多个,用traverseChildren
const result = traverseChildren(node);
return node.children.length > 1 ? `[${result}]` : result;
// 解析元素节点,像属性和指令节点都涵盖在里面
case NodeTypes.ELEMENT:
return createElementVNode(node);
// 解析文本节点 'a' h(Text,null,'a')
case NodeTypes.TEXT:
return createTextVNode(node);
// 解析插值节点 {{a}} h(Text,null,a)
case NodeTypes.INTERPOLATION:
return createTextVNode(node.content);
}
}
function traverseChildren(node) {
const { children } = node;
// 如果子节点是单纯的文本节点或者插槽,优化一下,例子:<span>{{x}}</span> =>h(Text,null,x)
if (children.length === 1) {
const child = children[0];
if (child.type === NodeTypes.TEXT) {
return createText(child);
}
if (child.type === NodeTypes.INTERPOLATION) {
return createText(child.content);
}
}
// 如果多个子节点,遍历递归解析,就像patch中的patchchildren
// <span> {{x}}<span>abc</span> </span> => h(Text,null,[h(),h()])
const results = [];
for (let i = 0; i < children.length; i++) {
const child = children[i];
results.push(traverseNode(child, node));
}
return results.join(', ');
}
元素节点
<button @click="add">click</button> =》ast =》 h('button',{ onClick: add }, "click")
function createElementVNode(node) {
const { children, directives } = node;
const tag =
node.tagType === ElementTypes.ELEMENT
? `"${node.tag}"`
: `resolveComponent("${node.tag}")`;
// 属性节点、指令节点都会作为prop,v-html='c' =>{innerHTML:'c'}, :a=b => {a:b} @click="add" => { onClick: add }
const propArr = createPropArr(node);
// 属性 要 转成 对象键值对
let propStr = propArr.length ? `{ ${propArr.join(', ')} }` : 'null';
// 如果没有孩子
if (!children.length) {
if (propStr === 'null') {
return `h(${tag})`;
}
return `h(${tag}, ${propStr})`;
}
// 如果有孩子,解析孩子
let childrenStr = traverseChildren(node);
// 如果有很多孩子,转成数组
if (children[0].type === NodeTypes.ELEMENT) {
childrenStr = `[${childrenStr}]`;
}
return `h(${tag}, ${propStr}, ${childrenStr})`;
}
function createPropArr(node) {
//属性节点、指令节点
const { props, directives } = node;
return [
...props.map((prop) => `${prop.name}: ${createText(prop.value)}`),
...directives.map((dir) => {
const content = dir.arg?.content;
switch (dir.name) {
case 'bind': //:a=b => {a:b}
return `${content}: ${createText(dir.exp)}`;
case 'on':
//@click="add" => { onClick: add }
// @click="add()" => { onClick: $event => (${add()}) }
// capitalize: 首字母大写工具函数
const eventName = `on${capitalize(content)}`;
let exp = dir.exp.content;
// 有@click ='foo',也有 ='foo()'的 这边判断有括号就转成 $event => (${exp})
// 以括号结尾,并且不含'=>'的情况,如 @click="foo()"
// 当然,判断很不严谨,比如不支持 @click="i++" ()=>foo()这个例子也不给支持
// 以( 开头 ,中间非 ) ,) 结尾,不贪婪
if (/\([^)]*?\)$/.test(exp) && !exp.includes('=>')) {
exp = `$event => (${exp})`;
}
return `${eventName}: ${exp}`;
case 'html': // v-html='c' =>{innerHTML:'c'},
return `innerHTML: ${createText(dir.exp)}`;
default: // v-for/v-if-else 下次讲
return `${dir.name}: ${createText(dir.exp)}`;
}
}),
];
}
// capitalize: 首字母大写工具函数
export function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}
文本、插值节点
// 解析文本节点 'a' h(Text,null,'a')
case NodeTypes.TEXT:
return createTextVNode(node);
// 解析插值节点 {{a}} h(Text,null,a)
case NodeTypes.INTERPOLATION:
return createTextVNode(node.content);
// 可以看到 差不多,只是一个有引号,一个没有
// node只接收text和simpleExpresstion 单纯的文本 和 {{ 变量 }}
function createTextVNode(node) {
const child = createText(node);
return `h(Text, null, ${child})`;
}
// 如果是文本,则返回 双引号的文本,如果不是,返回变量 通过isStatic来判断,其中插值isStatic为false
function createText({ content = '', isStatic = true } = {}) {
return isStatic ? JSON.stringify(content) : content;
}
到这里就完成了
Counter
<div>{{count}}</div>
<button @click="add">click</button>
————————demo 转
[
h(Text,null,'Counter'),
h('div',null,count),
h('button',{ onClick: add }, "click")
]
组装函数
new Function
developer.mozilla.org/en-US/docs/…
函数前面的参数是参数,最后一个是函数体
// Create a function that takes two arguments, and returns the sum of those arguments
const adder = new Function('a', 'b', 'return a + b');
// Call the function
adder(2, 6);
// 8
两种方案取变量,ctx.
和 with
with
developer.mozilla.org/zh-CN/docs/…
JavaScript 查找某个未使用命名空间的变量时,会通过作用域链来查找,作用域链是跟执行代码的 context 或者包含这个变量的函数有关。'with'语句将某个对象添加到作用域链的顶部,如果在 statement 中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出ReferenceError
异常。 不推荐使用with
,在 ECMAScript 5 严格模式中该标签已被禁止。推荐的替代方案是声明一个临时变量来承载你所需要的属性。有弊端,语义不明,不确定cos是不是取Math里的;像r需要先在Math里面找,性能消耗;
例子:下面的with
语句指定Math
对象作为默认对象。with
语句里面的变量,分別指向Math
对象的PI
、cos和
sin
函数,不用在前面添加命名空间。后续所有引用都指向Math
对象。
var a, x, y;
var r = 10;
with (Math) {
a = PI * r * r;
x = r * cos(PI);
y = r * sin(PI / 2);
}
# runtime/component.js
export function mountComponent(vnode, container, anchor, patch) {
if (!Component.render && Component.template) {
let { template } = Component;
Component.render = new Function('ctx', compile(template));
}
export function compile(template) {
const ast = parse(template);
return generate(ast);
}
export function generate(ast) {
const returns = traverseNode(ast);
const code = `
with (ctx) {
const { h, Text, Fragment, renderList, resolveComponent, withModel } = MiniVue
return ${returns}
}`;
return code;
}
结果
createApp({
template:`
Counter
<div>{{count}}</div>
<button @click="add">click</button>
`,
setup() {
const count = ref(0);
const add = () => count.value++;
return {
count,
add,
};
},
}).mount('#app');
—————————— 得到
render = new Function(ctx,with (ctx) {
const { h, Text, Fragment, renderList, resolveComponent, withModel } = MiniVue
return [
h('div', null, count),
h(
'button',
{
onClick: add,
},
'add'
),
];
})
也就是
render(ctx) {
return [
h('div', null, ctx.count),
h(
'button',
{
onClick: ctx.add,
},
'add'
),
];
},
}
渲染时:Component.render(instance.ctx) 这样变量就传过去了
另一种方案
在变量前加 ctx.
render(ctx) {
return [
h('div', null, ctx.count),
h(
'button',
{
onClick: ctx.add,
},
'add'
),
];
},
}
这种 v-for
里的变量前不能加 .ctx
,用到了babel
去转,比较复杂
template加在root里
<div id='app'>
Counter
<div>{{count}}</div>
<button @click="add">click</button>
</div>
export function createApp(rootComponent) {
components = rootComponent.components || {};
const app = {
mount(rootContainer) {
// 那么要取到里边的值赋值给 rootComponent
if (!isFunction(rootComponent.render) && !rootComponent.template) {
rootComponent.template = rootContainer.innerHTML;
}
rootContainer.innerHTML = '';
template加在script里
<script type='text/template' id='template'>
Counter
<div>{{count}}</div>
<button @click="add">click</button>
</script>
createApp({template:'#template'})
export function mountComponent(vnode, container, anchor, patch) {
if (!Component.render && Component.template) {
let { template } = Component;
if (template[0] === '#') {
const el = document.querySelector(template);
template = el ? el.innerHTML : '';
}
Component.render = new Function('ctx', compile(template));
}