『手写Vue3』template -> render

272 阅读2分钟

这一节的目标是,用户提供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包的依赖关系。依赖关系图如下:

1b74939f43af7a05125d64b974c02876.png

上面的做法,相当于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基本大功告成!

image.png