Vue3源码学习8——编译器compile进阶

91 阅读7分钟

上一篇Vue3源码编译器compile基础已经处理了编译器的基础情况,可以把模板编译成render函数从而进行渲染

但是对于响应性数据多层级节点指令v-if等),目前还没有做处理

响应性数据的编译

响应性数据在Vue中用的是插值表达式{{}},所以我们需要识别出这一部分,总共也是走三个步骤

  • 生成AST对象
  • AST对象转换为JavaScript AST(添加codegen属性)
  • 拼接生成render函数字符串

AST对象生成

响应性数据的AST对象生成主要就是识别出插值表达式,这一部分之前在parseChildren中留了一个位置

function parseChildren(context: ParserContext, ancestors) {
  const nodes = [];

  while (!isEnd(context, ancestors)) {
    const s = context.source;

    let node;

    if (startsWith(s, "{{")) {
      // 模板语法
      node = parseInterpolation(context);
    } else if (s[0] === "<") {
      ......
    }

    ......
  }

  return nodes;
}

parseInterpolation这个方法主要就是取出插值表达式大括号内的变量名字,并让光标右移

function parseInterpolation(context: ParserContext) {
  // 模板表达式以{{ XX }}格式呈现
  const [open, close] = ["{{", "}}"];

  // 光标先右移到开始位置之后
  advanceBy(context, open.length);

  // 获取到变量名,并去除前后的空格
  const closeIndex = context.source.indexOf(close, open.length);
  const preTrimContext = parseTextData(context, closeIndex);
  const content = preTrimContext.trim();

  // 光标右移到插值表达式之后
  advanceBy(context, close.length);

  return {
    type: NodeTypes.INTERPOLATION,
    content: {
      type: NodeTypes.SIMPLE_EXPRESSION,
      isStatic: false,
      content,
    },
  };
}

AST对象转换成JavaScript AST

之前我们处理文本节点的时候,已经埋好了插值表达式普通文本拼接逻辑

现在需要做的,是让最后拼接render函数字符串的时候,能给插值表达式添加上_toDisplayString方法

这样一来,做法就明确了,直接在traverseNode中给插值表达式这种情况添加helper方法即可

function traverseNode(node, context: TransformContext) {
  ......

  switch (node.type) {
    // 处理子节点
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context);
      break;
    // 插值表达式处理:添加上toDisplayString方法
    case NodeTypes.INTERPOLATION:
      context.helper(TO_DISPLAY_STRING);
      break;
  }

  ......
}

render函数字符串拼接

可以先观察Vue3源码中最后拼接出来的render函数字符串

const _Vue = Vue

return function render(_ctx, _cache) {
  with(_ctx) {
    const { toDisplayString: _toDisplayString, createElementBlock: _createElementBlock } = _Vue
    return _createElementBlock("div", null, "hello " + _toDisplayString(msg))
  }
}

和之前已经完成的部分,差异有三

  • render函数中有一个with函数包裹
  • 添加了_toDisplayString方法
  • 子节点的_toDisplayString方法中的变量名是msgwith的作用使得方法可以找到msg

所以这里的处理就围绕这三点差异展开

with方法的包裹

with可以扩展一个语句的作用域链,但不推荐使用

MDN with语法说明

这个处理起来比较简单,直接在拼接方法中添加上with就行

function generate(ast) {
  ......

  push(`with (_ctx) {`);
  indent();

  ......

  // 结束后要退回缩进并再添加一个结束的括号
  deindent();
  push("}");

  return {
    ast,
    code: context.code,
  };
}

但是在实际使用最后生成的render函数的时候,会有一个问题:因为msg_ctxmsg,但是现在_ctx还没有值,所以我们在render函数中应该补充传入data作为_ctx

function renderComponentRoot(instance) {
  // 给data添加默认值
  const { vnode, render, data = {} } = instance;

  let result;

  try {
    if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
      // 第一个参数改this,第二个参数是为了ctx改变作用域的
      result = normalizeVNode(render!.call(data, data));
    }
  } catch (err) {
    console.error(err);
  }

  return result;
}

toDisplayString方法的添加

这里需要区分几种情况

  • 简单表达式(插值表达式的变量名
  • 插值表达式
  • 复合表达式(插值表达式+文本的组合)

这几种情况的区分放在genNode里面

function genNode(node, context) {
  switch (node.type) {
    ......
    // 简单表达式(插值表达式的变量名)
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context);
      break;
    // 插值表达式
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context);
      break;
    // 复合表达式(即简单+插值)
    case NodeTypes.COMPOUND_EXPRESSION:
      genCompoundExpression(node, context);
      break;
  }
}
简单表达式的处理

用的genExpression处理,其实就是直接推入render函数字符串

function genExpression(node, context) {
  const { content, isStatic } = node;
  context.push(isStatic ? JSON.stringify(content) : content);
}
插值表达式的处理

插值表达式用了genInterpolation处理,核心就是把变量名用_toDisplayString包裹一下

至于里面的变量名用genNode递归,交给genExpression处理

function genInterpolation(node, context) {
  const { push, helper } = context;
  push(`${helper(TO_DISPLAY_STRING)}(`);
  genNode(node.content, context);
  push(`)`);
}
复合表达式的处理

复合表达式处理用的genCompoundExpression,核心就是把其中的子节点再挨个判断,递归到genNode去判断(文本/插值

function genCompoundExpression(node, context) {
  for (let i = 0; i < node.children.length; i++) {
    const child = node.children[i];
    // +,直接推入
    if (isString(child)) {
      context.push(child);
    }
    // 文本/插值表达式的处理在genNode中,需要递归
    else {
      genNode(child, context);
    }
  }
}

多层级节点的编译

多层级节点指的就是节点嵌套,例如下面这个

<div>
  <h1>hello world</h1>
</div>

这种嵌套节点编译器出来的render函数应该也是嵌套的

处理思路也简单,只要在逻辑上保证让这种类型的节点可以递归处理就好了

function genNode(node, context) {
  switch (node.type) {
    ......
    // 多层级节点
    case NodeTypes.ELEMENT:
      genNode(node.codegenNode, context);
      break;
  }
}

指令的编译

指令是Vue中的特色,Vue的内部封装好了若干指令,例如v-ifv-forv-on等等

而对于指令的编译,其实也是要经历核心的三个步骤,即:

  • AST生成
  • AST转换成JavaScript AST
  • render函数拼接

阅读源码可以知道,v-if指令其实是在render函数中创建了一个三元表达式,动态根据v-if的变量的值,决定是渲染v-if节点,还是一个v-if注释节点

(插入源码的分析,即render函数)

下面以v-if的指令编译为例,说明Vue中的指令编译过程

指令的AST生成

我们知道,v-if是写在标签之后,像属性一样用空格分隔开

<div v-if="test">hello world</div>

所以对它的解析其实也是对属性的一种解析,只是我们要正确识别出v-if指令即可

属性的解析是在标签之后的,所以放在了标签解析parseTag方法里面

function parseTag(context: ParserContext, type: TagType) {
  ......

  // 根据tag的名称长度,右移source位置(<+tag名字)
  advanceBy(context, match[0].length);

  // 属性和指令的处理
  // 先把tag和属性之间的空格给去掉,光标右移到属性开始的位置
  advanceSpaces(context);
  // 对属性做解析
  let props = parseAttributes(context, type);

  ......

  return {
    type: NodeTypes.ELEMENT,
    tag,
    tagType: ElementTypes.ELEMENT,
    props,
    children: [],
  };
}

// 把tag和属性之间的空格去掉,右移到属性开始的位置
function advanceSpaces(context: ParserContext): void {
  const match = /^[\t\r\n\f ]+/.exec(context.source);
  if (match) {
    advanceBy(context, match[0].length);
  }
}

属性的解析

对于属性来说,都是密密麻麻一串放在tag标签名字之后的,我们借着指令的识别,把所有的属性都给识别出来,同时用特定的正则去单独处理指令

属性列表的解析处理用parseAttributes方法,核心就是循环识别各个属性,拿到属性名属性值,存放起来最后一并挂在节点的props属性中

function parseAttributes(context, type) {
  const props: any = [];

  // 属性名的存放
  const attributeNames = new Set<string>();

  // 当源码长度不为0,且剩下的内容不是标签结束的时候,说明还有属性,要循环处理
  while (
    context.source.length > 0 &&
    !startsWith(context.source, ">") &&
    !startsWith(context.source, "/>")
  ) {
    // 拿到属性名字和值的AST
    const attr = parseAttribute(context, attributeNames);
    // 只有是开始标签,才去存放属性
    if (type === TagType.Start) {
      props.push(attr);
    }
    // 两个属性之间有空格,要右移到下一个属性/结尾位置
    advanceSpaces(context);
  }

  return props;
}

单个属性用parseAttribute做解析,基本逻辑就是识别出属性名属性值,对于v-指令普通属性分开处理(用正则识别出来v-指令)

function parseAttribute(context: ParserContext, nameSet: Set<string>) {
  // 拿到属性名
  const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!;
  const name = match[0];

  nameSet.add(name);

  // 右移继续准备拿到属性值
  advanceBy(context, name.length);

  let value: any = undefined;

  // 如果能有=出现,后面就是属性值了
  if (/^[^\t\r\n\f ]*=/.test(context.source)) {
    advanceSpaces(context);
    advanceBy(context, 1);
    advanceSpaces(context);
    // 获取属性值
    value = parseAttributeValue(context);
  }

  // v-指令处理
  if (/^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) {
    // 拿到v-指令的名字(例如if/for等等)
    const match =
      /(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
        name
      )!;

    let dirName = match[1];

    return {
      type: NodeTypes.DIRECTIVE,
      name: dirName,
      // 指令绑定的值
      exp: value && {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: value.content,
        isStatic: false,
        loc: {},
      },
      art: undefined,
      modifiers: undefined,
      loc: {},
    };
  }

  // 普通属性处理
  return {
    type: NodeTypes.ATTRIBUTE,
    name,
    value: value && {
      type: NodeTypes.TEXT,
      content: value.content,
      loc: {},
    },
    loc: {},
  };
}

// 单个属性值的处理
function parseAttributeValue(context: ParserContext) {
  // 属性内容
  let content = "";

  // 第一位是引号(单双不确定,所以要拿到它,后面对应去匹配)
  const quote = context.source[0];
  // 右移引号宽度
  advanceBy(context, 1);
  const endIndex = context.source.indexOf(quote);
  // 没有找到结束引号,则后面内容都是属性值
  if (endIndex === -1) {
    content = parseTextData(context, context.source.length);
  } else {
    content = parseTextData(context, endIndex);
    // 右移引号宽度
    advanceBy(context, 1);
  }

  return { content, isQuoted: true, loc: {} };
}

指令AST到JavaScript AST的转化

JavaScript AST主要还是codegenNode属性的变化

首先,封装一个通用处理指令逻辑createStructuralDirectiveTransform方法,这样后续所有指令可以通过这个方法,匹配指令名后,再返回对应的处理逻辑

function createStructuralDirectiveTransform(name: string | RegExp, fn) {
  // 检测指令是否匹配
  const matches = isString(name)
    ? (n: string) => n === name
    : (n: string) => name.test(n);

  return (node, context) => {
    // 因为所有的指令都绑定在ELEMENT节点上,所以只处理ELEMENT节点
    if (node.type === NodeTypes.ELEMENT) {
      const { props } = node;
      const exitFns: any = [];

      // 因为指令实际存在属性中,因此遍历属性,找到其中的指令
      for (let i = 0; i < props.length; i++) {
        const prop = props[i];

        if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
          // 删除这个属性(因为它本来就不是属性,而是因为位置在属性上)
          props.splice(i, 1);
          i--;

          // 执行传入的方法,当有返回值的时候,推入这个exitFns列表(traverseNode的逻辑)
          const onExit = fn(node, prop, context);
          if (onExit) exitFns.push(onExit);
        }
      }

      return exitFns;
    }
  };
}

封装了指令通用的处理方法后,接下来写一个v-if指令专门的处理方法transformIf

其中,第二个参数又被封装了一层,而这个参数实际上是traverseNode中的onExit函数,将会在最后处理v-if指令创建codegenNode属性

const transformIf = createStructuralDirectiveTransform(
  /^(if|else|else-if)$/,
  // 第二个onExit的参数如下
  // 第一个参数是node自己
  // 第二个参数实际上是v-if的prop属性
  // 第三个参数是上下文
  // 第四个参数是一个匿名函数,真实目的是往这个ifNode上面创建codegenNode属性
  (node, dir, context) => {
    return processIf(node, dir, context, (ifNode, branch, isRoot) => {
      let key = 0;

      return () => {
        if (isRoot) {
          ifNode.codegenNode = createCodegenNodeForBranch(branch, key, context);
        }
      };
    });
  }
);

function processIf(
  node,
  dir,
  context: TransformContext,
  processCodegen?: (node, branch, isRoot: boolean) => () => void
) {
  // 只有if指令才处理
  if (dir.name === "if") {
    // 创建一个branch属性
    const branch = createIfBranch(node, dir);

    const ifNode = {
      type: NodeTypes.IF,
      loc: {},
      branches: [branch],
    };

    // 上下文指向的node改成当前的ifNode
    context.replaceNode(ifNode);

    if (processCodegen) {
      return processCodegen(ifNode, branch, true);
    }
  }
}

function createIfBranch(node, dir) {
  return {
    type: NodeTypes.IF_BRANCH,
    loc: {},
    condition: dir.exp,
    children: [node],
  };
}

processCodegen就是针对v-if预创建的codegenNode属性,目的就是在最后拼接render函数字符串的时候,如果有条件语句,根据条件语句创建三元表达式

function createCodegenNodeForBranch(
  branch,
  keyIndex,
  context: TransformContext
) {
  // 如果v-if后面有条件,要根据条件创建三元表达式
  if (branch.condition) {
    return createConditionalExpression(
      branch.condition,
      createChildrenCodegenNode(branch, keyIndex),
      createCallExpression(context.helper(CREATE_COMMENT), ['"v-if"', "true"])
    );
  }
  // 没有条件,则只针对子节点创建codegenNode
  else {
    return createChildrenCodegenNode(branch, keyIndex);
  }
}

function createConditionalExpression(
  test,
  consquent,
  alternate,
  newline = true
) {
  return {
    type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
    test,
    consquent,
    alternate,
    newline,
    loc: {},
  };
}

// 创建子节点的codegenNode属性
function createChildrenCodegenNode(branch, keyIndex: number) {
  const keyProperty = createObjectProperty(
    `key`,
    createSimpleExpression(`${keyIndex}`, false)
  );

  const { children } = branch;
  const firstChild = children[0];
  const ret = firstChild.codegenNode;
  // 这个vnodeCall这里比较简单,就是node本身
  const vnodeCall = getMemoedVNodeCall(ret);

  injectProp(vnodeCall, keyProperty);
  return ret;
}

// 把属性注入到node的props中
function injectProp(node, prop) {
  let propsWithInjection;

  let props =
    node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2];

  if (props === null || isString(props)) {
    propsWithInjection = createObjectExpression([prop]);
  }

  if (node.type === NodeTypes.VNODE_CALL) {
    node.props = propsWithInjection;
  }
}

function createObjectExpression(properties) {
  return {
    type: NodeTypes.JS_OBJECT_EXPRESSION,
    loc: {},
    properties,
  };
}

处理完这些,最后在traverseNode方法中,添加对于if类型节点的处理逻辑

function traverseNode(node, context: TransformContext) {
  ......

  switch (node.type) {
    // 处理子节点
    case NodeTypes.IF_BRANCH:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context);
      break;
    ......
    case NodeTypes.IF:
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context);
      }
      break;
  }

  ......
}

render函数字符串拼接

最后render函数的拼接核心就是拼接三元表达式

  • 如果满足条件,渲染
  • 不满足条件,挂载一个v-if的注释节点上去
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue

    
    return _createElementVNode("div", [], ["hello ", isShow
      ? _createElementVNode("h1", null, ["world"])
      : _createCommentVNode("v-if", true)
    ])
  }
}

三元表达式的部分主要是由genConditionalExpression处理的

此外,对于v-if判定为false的情况,渲染注释节点的逻辑由genCallExpression处理

function genConditionalExpression(node, context) {
  const { test, alternate, consequent, newline: needNewLine } = node;
  const { push, newline, indent, deindent } = context;

  // 添加v-if的条件值
  if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
    genExpression(test, context);
  }

  needNewLine && indent();

  context.indentLevel++;

  // 问号(准备填充v-if判定为true时候的渲染内容)
  needNewLine || push(` `);
  push(`? `);

  // v-if为true的时候的展示内容
  genNode(consequent, context);

  context.indentLevel--;
  needNewLine && newline();
  needNewLine || push(` `);

  // 冒号(准备填充v-if判定false时候的渲染内容)
  push(`: `);

  const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION;

  if (!isNested) {
    context.indentLevel++;
  }
  // v-if为false的时候的处理内容(渲染v-if的注释节点)
  genNode(alternate, context);

  if (!isNested) {
    context.indentLevel--;
  }
  needNewLine && deindent(true);
}

// 用来处理v-if判定是false时候的渲染(执行方法的方法名+参数)
function genCallExpression(node, context) {
  const { push, helper } = context;
  const callee = isString(node.callee) ? node.callee : helper(node.callee);
  push(callee + `(`, node);
  genNodeList(node.arguments, context);
  push(`)`);
}

另外需要补充一下创建注释节点的方法createCommentVNode,并在全局导出以供render函数识别使用

function createCommentVNode(text) {
  return createVNode(Comment, null, text);
}

// 类似于createVNode方法,封装Symbol做映射
export const CREATE_COMMENT = Symbol("createCommentVNode");
export const helperNameMap = {
  ......
  [CREATE_COMMENT]: "createCommentVNode",
};

最后,要在genNode上添加两种情况的处理,分别是JS调用表达式v-iffalse时的渲染内容)以及JS条件表达式(渲染三元表达式

function genNode(node, context) {
  switch (node.type) {
    ......
    // JS调用表达式(用来渲染v-if为false时候的内容)
    case NodeTypes.JS_CALL_EXPRESSION:
      genCallExpression(node, context);
      break;
    // JS的条件表达式(用来渲染三元表达式)
    case NodeTypes.JS_CONDITIONAL_EXPRESSION:
      genConditionalExpression(node, context);
      break;
  }
}