vue3-compiler(四) transform特殊指令v-if、组件

199 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情

这次讲一下怎么 transform 几个特殊的指令: v-if, v-for v-model,还有组件

v-for

示例:

<div v-for="(item, index) in items">{{item + index}}</div>

div 会根据 items 的数量而去渲染相应数量的 div。而 items 变量来源于 runtime,因此 v-for 指令不能只靠编译完成,需要 runtime 配合。

编译目标:

h(
  Fragment,
  null,
  renderList(items, (item, index) => h('div', null, item + index))
);

其中 h('div', null, item + index)就是节点删掉 for属性后正常编译的结果

实现

// 遍历节点时,代理一层
export function traverseNode(node, parent) {
  switch (node.type) {
。。。
    case NodeTypes.ELEMENT:
          // 替换createElementVNode
      return resolveElementASTNode(node, parent);
function resolveElementASTNode(node, parent) {  
// 判断是否是v-for节点
  const forNode = pluck(node.directives, 'for');
  if (forNode) {
    const { exp } = forNode;
    // 以 in 或者 of 分隔开,得到 v-for="(item, index) in items" 中的 (item, index) 和 items
    //   renderList(items, (item, index) => h('div', null, item + index))
    const [args, source] = exp.content.split(/\sin\s|\sof\s/);
    return `h(Fragment, null, renderList(${source.trim()}, ${args.trim()} => ${resolveElementASTNode(
      node
    )}))`;
  }
    //如果正常就正常解析咯, <div>{{item + index}}</div> 解析成 h('div', null, item + index)
    return createElementVNode(node);
}
// 判断是否是特殊节点,且删除掉这个属性,for属性不像id,不能往下传
function pluck(directives, name, remove = true) {
  const index = directives.findIndex((dir) => dir.name === name);
  const dir = directives[index];
  if (remove && index > -1) {
    directives.splice(index, 1);
  }
  return dir;
}
# runtime
// source在这边能获取到ctx的值,就能得到items的长度,从而知道该分成多少个子节点,用fragment包
export function renderList(source, renderItem) {
  const vnodes = [];
  if (isNumber(source)) {
    // items为数字时,如果是10,则是1到10
    for (let i = 0; i < source; i++) {
      vnodes.push(renderItem(i + 1, i));
    }
    // 数组和字符串都是一样
  } else if (isString(source) || isArray(source)) {
    for (let i = 0; i < source.length; i++) {
      vnodes.push(renderItem(source[i], i));
    }
  } else if (isObject(source)) {
    // 对象时,三个参数 value,key,index in items
    const keys = Object.keys(source);
    keys.forEach((key, index) => {
      vnodes.push(renderItem(source[key], key, index));
    });
  }
  return vnodes;
}

v-if

vue-next-template-explorer.netlify.app/

每个 if 语句都包函三个部分:condition, consequent, alternate 组成 condition ? consequent : alternate

<div v-if='ok'>Counter</div> ok? <div>Counter</div> : null

 <div v-if='ok'>Counter</div>
 ——————本地实现
 ok ? h('div', null, 'Counter') : h(Text, null, '');
 ——————源
 return (_openBlock(), _createElementBlock(_Fragment, null, [
      ok
        ? (_openBlock(), _createElementBlock("div", { key: 0 }, "Counter"))
        : _createCommentVNode("v-if", true), //注释节点

实现单纯 v-if

function resolveElementASTNode(node, parent) {
  const ifNode = pluck(node.directives, 'if')
  if (ifNode) {
       const consequent = resolveElementASTNode(node);
      const { exp } = ifNode;
    return `${exp.content} ? ${consequent} : ${createTextVNode()}`;
  }

v-else

<div v-if='ok'>Counter</div>
<h2 v-else></h2>
 ——————本地实现
[
  ok ? h("h1", null, 'Counter') : h("h2")
]
——————源
  ok
        ? (_openBlock(), _createElementBlock("div", { key: 0 }, "Counter"))
        : (_openBlock(), _createElementBlock("h2", { key: 1 })),

还有 其他情况

<h1 v-if="ok"></h1>
<h2 v-else-if="ok2"></h2>
<h3 v-else-if="ok3"></h3>
ok
  ? h('h1')
  : ok2
    ? h('h2')
    : ok3
      ? h('j3')
      : h(Text, null, '');

<h1 v-if="ok"></h1>
<h2 v-else-if="ok2"></h2>
<h3 v-else></h3>
ok
  ? h('h1')
  : ok2
    ? h('h2')
    : h('h3');

实现完整

  • 为了知道下个节点是否是v-else,需要从父节点的children里获取,所以改一下,获取到parent

export function traverseNode(node, parent) {
  switch (node.type) {
    case NodeTypes.ROOT:
      if (node.children.length === 1) {
          // 这里传入根父节点
        return traverseNode(node.children[0], node);
      }
      const result = traverseChildren(node);
      return node.children.length > 1 ? `[${result}]` : result;
    case NodeTypes.ELEMENT:
          // traverseNode在 traverseChildren 里调用,很容易 拿到 parent 传给他。
      return resolveElementASTNode(node, parent);
  	。。。
  }
function resolveElementASTNode(node, parent) {
  const ifNode =
    pluck(node.directives, 'if') || pluck(node.directives, 'else-if');

  if (ifNode) {
    // 递归必须用resolveElementASTNode,因为一个元素可能有多个指令
    // 所以处理指令时,移除当下指令也是必须的
    const consequent = resolveElementASTNode(node, parent);
    let alternate;

    // 如果有ifNode,则需要看它的下一个元素节点是否有else-if或else
    const { children } = parent;
    let i = children.findIndex((child) => child === node) + 1;
    for (; i < children.length; i++) {
      const sibling = children[i];

      // <div v-if="ok"/> <p v-else-if="no"/> <span v-else/>
      // 为了处理上面的例子,需要将空节点删除
      // 也因此,才需要用上for循环
      if (sibling.type === NodeTypes.TEXT && !sibling.content.trim()) {
        children.splice(i, 1);
        i--;
        continue;
      }
// 下个else节点
      if (
        sibling.type === NodeTypes.ELEMENT &&
        (pluck(sibling.directives, 'else') ||
          // else-if 既是上一个条件语句的 alternate,又是新语句的 condition
          // 因此pluck时不删除指令,下一次循环时当作ifNode处理,第三个参数false就是不删
         // v-else-if的处理和v-if一样,所以不要删了else-if,用if的逻辑去做
          pluck(sibling.directives, 'else-if', false))
      ) {
        alternate = resolveElementASTNode(sibling, parent);
          // 用完就要删了节点
        children.splice(i, 1);
      }
      // 只用向前寻找一个相临的元素,因此for循环到这里可以立即退出
      break;
    }

    const { exp } = ifNode;
    return `${exp.content} ? ${consequent} : ${alternate || createTextVNode()}`;
  }

举个例子:

<h1 v-if="ok"></h1>
<h2 v-else-if="ok2"></h2>
<h3 v-else></h3>
ok
  ? h('h1')
  : ok2
    ? h('h2')
    : h('h3');

找到v-if时,就要去找后面有没有v-else-if,如果有,用v-if的逻辑去处理它,处理结果当成 ok?a:b里的b就行了,说难不难,说简单也不简单

v-if 和 v-for 的优先级 不建议 v-ifv-for 一起使用,因为会造成语义混乱,谁应该优先呢? 但它两在一起又确实是合法的。有意思的是 vue2v-for 优先,vue3v-if 优先。

v-model

cn.vuejs.org/guide/essen…

最早期的 vue 是这样描述 v-model 的:v-model 本质上是一个语法糖。 如下代码 <input v-model="test"> 本质上是 <input :value="test" @input="test = $event.target.value">

但现在的 v-model 有着完全不同的实现。它是利用 vue 的自定义指令实现的。 为了图简便,我们沿用以前的设定去实现,直接将 vModel 改为两个指令。

const vModel = pluck(node.directives, 'model');
if (vModel) {
  node.directives.push(
    {
      type: NodeTypes.DIRECTIVE,
      name: 'bind',
      exp: vModel.exp, // 表达式节点
      arg: {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: 'value',
        isStatic: true,
      }, // 表达式节点
    },
    {
      type: NodeTypes.DIRECTIVE,
      name: 'on',
      exp: {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: `($event) => ${vModel.exp.content} = $event.target.value`,
        isStatic: false,
      }, // 表达式节点
      arg: {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: 'input',
        isStatic: true,
      }, // 表达式节点
    }
  );
}

但是这种方法只对 input 凑效。v-model 有着非常多的使用场合,每种场合效果也不尽相同,甚至还能用于组件上,因此完整的实现还是很麻烦的。还有 radio, checkbox 的。

比如
<input v-model="test" @input='foo'>
这样按照我们的写法会出现两个input事件冲突
不展开了

组件的引入

组件的使用方法:需在 createApp 里申明 components(只支持全局组件)

createApp({
  components: { TreeItem },
});

————使用
<ul id="demo">
  <tree-item class="item" :model="treeData"></tree-item>
</ul>
  const TreeItem = {
    template: '#item-template',
  。。。
  }
<script type="text/x-template" id="item-template">。。。</script>

runtime 里使用 resolveComponent 配合,

  • codegen.js

得到 h(resolveComponent(TreeItem))

function createElementVNode(node) {
  const { children, directives } = node;

  const tag =
    node.tagType === ElementTypes.ELEMENT
      ? `"${node.tag}"`
      : `resolveComponent("${node.tag}")`;
...
	return `h(${tag}, ${propStr}, ${childrenStr})`;
}
  • createApp.js

let components;
export function createApp(rootComponent) {
// 拿到{ Foo },
  components = rootComponent.components || {};
  ...
 }
export function resolveComponent(name) {
  return (
    components &&
    (components[name] ||
      components[camelize(name)] ||
      components[capitalize(camelize(name))])
  );
  // tree-item,treeItem,TreeItem  如果组件名 tree-item找不到就找 treeItem,TreeItem
}

得到

h({
    template: '#item-template',
  。。。
  })

怎么解析看这个 :juejin.cn/post/713576…