vue3-compiler(三) transform、codegen

254 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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对象的PIcos和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));
 }