这一节的目标是,用户提供template,生成render函数,并且运行生成的函数,最后渲染出dom。
组件结构:
export const App = {
name: 'App',
template: `<div>hi,{{count}}</div>`,
setup() {
const count = (window.count = ref(1));
return {
count
};
}
};
首先,compiler-core需要一个入口函数,它将依次调用parse->transform->generate,完成编译全流程,返回generate的返回值,是一个对象,含有code属性。
export function baseCompile(template) {
const ast = baseParse(template);
transform(ast, {
nodeTransforms: [transformExpression, transformText, transformElement]
});
return generate(ast);
}
不难想到,现在要做的事,就是跑到runtime-core里,找到instance.render = Component.render之前,判断Component是否含有template属性,如果有,就调用baseCompile生成render。
这种做法暂且不说别的,首先违背了Vue包的依赖关系。依赖关系图如下:
上面的做法,相当于runtime-core直接依赖于compiler-core。
所以,需要把相关逻辑提升到最顶层,即Vue中。
在runtime-core下的component中,设置全局变量complier,并暴露修改它的函数:
let compiler;
export function registerRuntimeCompiler(_compiler) {
compiler = _compiler;
}
在最顶层的index.ts中,调用该函数,设置compiler:
function compileToFunction(template) {
const { code } = baseCompile(template);
// code:
// const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString } = Vue
// return function render(_ctx, _cache){return _createElementVNode("div", null, "hi, " + _toDisplayString(_ctx.message))}"
// new Function有点类似于eval。最后一个参数是函数体字符串,前面是参数
// runtime-core作为Vue,暴露createElementVNode和toDisplayString函数
const render = new Function('Vue', code)(runtimeDom);
return render;
}
registerRuntimeCompiler(compileToFunction);
之后在运行时处理组件时,compiler已被赋值,就能解析并调用render函数了。
function finishComponentSetup(instance: any) {
const Component = instance.type;
// 优先使用用户提供的render函数
if (!Component.render && compiler) {
if (Component.template) {
// compiler就是compileToFunction
Component.render = compiler(Component.template);
}
}
instance.render = Component.render;
}
由于生成的render函数中,插值的变量是从_ctx上寻找,而不是this,但是本质上都是从proxy。_ctx是render函数的第一个参数,所以call需要把proxy作为第一个参数传入。以后想要在插值语句中使用setup暴露的数据,在模板里,this也不用写了。
const subTree = (instance.subTree = instance.render.call(
proxy, // this
proxy // 第一个参数,_ctx
));
然后,提供toDisplayString,并且导出。
export function toDisplayString(value) {
return String(value);
}
最后,由于render中使用的函数名叫“createElementVNode”,和我们runtime-core中实现的函数名不一致,还需要用as导出一次。
// 导出为了让render使用
export { createVNode as createElementVNode };
// 之前的导出不变,因为其他地方也用到过createVNode
export function createVNode(type, props?, children?) {
// ...
}
现在template就能渲染成dom了,mini-vue基本大功告成!