vue3-compiler(二) parse

650 阅读4分钟

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

这次讲compiler模块的parse,用例子和简单的代码实现来看看是怎么将模板转化成ast节点树的。

parse

简单说下过程

生成根节点,传入的作为children解析

从左到右遍历探查,< 开头则解析元素,{{则解析插值节点,其他的归为文本节点,一直到 </或者字符结束

工具函数

advanceBy 吃掉一段字符

就是用这个方法,解析完的字符要吃掉,最后吃光光

function advanceBy(context, numberOfCharacters) {
  const { source } = context;
  context.source = source.slice(numberOfCharacters);
}

parseTextData 吃掉的文本

解析文本时,拿取文本尾部索引,吃掉文本并且返回文本内容

function parseTextData(context, length) {
  const rawText = context.source.slice(0, length);
  advanceBy(context, length);
  return rawText;
}

吃掉空白区域

比如 id="foo" v-if 中间就有空格,吃掉,方便下个属性的解析

function advanceSpaces(context) {
  const match = /^[\t\r\n\f ]+/.exec(context.source);
  if (match) {
    advanceBy(context, match[0].length);
  }
}

辨别自闭合标签、元素和组件

const HTML_TAGS =
  'html,body,base,head,link,meta,style,title,address,article,aside,footer,' +
  'header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,' +
  'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,' +
  'data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,' +
  'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,' +
  'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,' +
  'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,' +
  'option,output,progress,select,textarea,details,dialog,menu,' +
  'summary,template,blockquote,iframe,tfoot';
​
const VOID_TAGS =
  'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr';
​
function makeMap(str) {
  const map = str
    .split(',')
    .reduce((map, item) => ((map[item] = true), map), Object.create(null));
  return (val) => !!map[val];
}
​
export const isVoidTag = makeMap(VOID_TAGS);
export const isNativeTag = makeMap(HTML_TAGS);

横杆转驼峰

// my-class =》myClass
const camelizeRE = /-(\w)/g;
export function camelize(str) {
  // _:-c,c:c
  return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
}
// 源码:在vue3中,加了闭包缓存
const cacheStringFunction$1 = (fn) => {
    const cache = Object.create(null);
    return ((str) => {
        const hit = cache[str];
        return hit || (cache[str] = fn(str));
    });
};
const camelize = cacheStringFunction$1((str) => {
    return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
});

节点类型

export const NodeTypes = {
  ROOT: 'ROOT',
  ELEMENT: 'ELEMENT',
  TEXT: 'TEXT',
  SIMPLE_EXPRESSION: 'SIMPLE_EXPRESSION',
  INTERPOLATION: 'INTERPOLATION',
  ATTRIBUTE: 'ATTRIBUTE',
  DIRECTIVE: 'DIRECTIVE',
};
​
export const ElementTypes = {
  ELEMENT: 'ELEMENT',
  COMPONENT: 'COMPONENT',
};

添加根节点

export function createRoot(children) {
  return {
    type: NodeTypes.ROOT,
    children,
  };
}
​
export function parse(content) {
  // 添加配置
  const context = createParserContext(content);
  // 加根节点,开始解析
  return createRoot(parseChildren(context));
}
​
function createParserContext(content) {
  return {
    options: {
      delimiters: ['{{', '}}'], // 插值节点起始和终点
      isVoidTag, // 放这是为了跨平台支持,作用是为了区分是否是自闭和标签元素
      isNativeTag, // 区分是否是html标签,为了区别是元素还是组件
    },
    source: content,
  };
}

解析

例子:<div id="foo" v-if="ok">hello {{name}}</div>

function parseChildren(context) {
  const nodes = [];
  // 没结束一直循环
  while (!isEnd(context)) {
    const s = context.source;
    let node;
    // 插值节点
    if (s.startsWith(context.options.delimiters[0])) {
      // '{{'
      node = parseInterpolation(context);
    } else if (s[0] === '<') {
      // 元素节点
      node = parseElement(context);
    } else {
      // 文本节点
      node = parseText(context);
    }
    nodes.push(node);
  }
​
  return nodes
}

元素节点

// id="foo" v-if="ok">
function parseElement(context) {
  // Start tag. 解析标签,解析到 > 或者 />
  const element = parseTag(context);
  // 只用 /> 判断是否是自闭合不够,虽然 hr 是自闭和 但是<hr/> 和 <hr> 两种写法都被允许
  // 所以还用 isVoidTag 枚举判断 是否是 自闭和标签
  if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
    return element;
  }
​
  // Children.  其他的内容当children  hello {{name}}</div>
  element.children = parseChildren(context);
​
  // End tag. 删闭合 '/>' 或者 >
  parseTag(context);
​
  return element;
}
function parseTag(context) {
  // Tag open.
  // <div > : < 开头、或者</ 
  // 接小写字母,后面接上非空格等元素
  // 匹配 <div ,match = [<,div] ,也匹配 </div>
  const match = /^</?([a-z][^\t\r\n\f />]*)/i.exec(context.source);
  const tag = match[1];
​
  advanceBy(context, match[0].length); //删掉 <div 
  advanceSpaces(context); //去掉div后面的空格
​
  // Attributes.  吃掉属性 删掉字符
  const { props, directives } = parseAttributes(context);
​
  // Tag close. 闭合标签,看是否是自闭和,自闭和删两个,正常闭删1个
  const isSelfClosing = context.source.startsWith('/>');
​
  advanceBy(context, isSelfClosing ? 2 : 1);
  // 枚举标签名,得出是否是组件
  const tagType = isComponent(tag, context)
    ? ElementTypes.COMPONENT
    : ElementTypes.ELEMENT;
​
  return {
    type: NodeTypes.ELEMENT,
    tag, //标签名 div
    tagType, // 组件还是标签, element
    props, // [id]
    directives, // [v-if]
    isSelfClosing, // 是否自闭和,false
    children: [],
  };
}
​
// 枚举标签名,得出是否是组件
function isComponent(tag, context) {
  const { options } = context;
  return !options.isNativeTag(tag);
}
// 解析属性
function parseAttributes(context) {
  const props = [];
  const directives = [];
  // 当字符还有且没结束时,循环
  while (
    context.source.length &&
    !context.source.startsWith('>') &&
    !context.source.startsWith('/>')
  ) {
    const attr = parseAttribute(context);
    if (attr.type === NodeTypes.ATTRIBUTE) {
      props.push(attr);
    } else {
      directives.push(attr);
    }
  }
  return { props, directives };
}
function parseAttribute(context) {
  // Name. v-bind:class='abc'
  // name判断很宽除了下述几个字符外都支持 非空格开头,后接非等于号空格以外的字符
  const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source);
  const name = match[0];
  // 取到名字 v-bind:class 后删掉
  advanceBy(context, name.length);
  advanceSpaces(context);
​
  // Value  
  let value;
  // 有可能是 chcked  若有等于号,删掉,去除等号两边空格,解析内容
  if (context.source[0] === '=') {
    advanceBy(context, 1);
    advanceSpaces(context);
    // 拿到 值
    value = parseAttributeValue(context);
    advanceSpaces(context);
  }
  // 如果是v-|:|@)开头就是指令节点,否则是属性节点
  // Directive  name : v-bind:class
  if (/^(v-|:|@)/.test(name)) {
    let dirName, argContent;
    if (name[0] === ':') {
      dirName = 'bind';
      argContent = name.slice(1);
    } else if (name[0] === '@') {
      dirName = 'on';
      argContent = name.slice(1);
    } else if (name.startsWith('v-')) {
      // v-bind:class   =》 [bind,class]
      [dirName, argContent] = name.slice(2).split(':');
    }
​
    return {
      type: NodeTypes.DIRECTIVE,
      name: dirName, // bind
      exp: value && { // abc
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: value.content,
        isStatic: false,
      },
      arg: argContent && { // class ,v-if则是空值
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: camelize(argContent),
        isStatic: true,
      }
    };
  }
​
  // Attribute  
  return {
    type: NodeTypes.ATTRIBUTE,
    name,
    value: value && {
      type: NodeTypes.TEXT,
      content: value.content,
    },
  };
}
​
function parseAttributeValue(context) {
  // 不考虑没有引号的情况
  // 取到 '
  const quote = context.source[0];
  advanceBy(context, 1);
  // 找到后引号,拿出里面的值
  const endIndex = context.source.indexOf(quote);
  const content = parseTextData(context, endIndex);
​
  advanceBy(context, 1);
​
  return { content };
}

文本节点

hello{{}}

// 不支持文本节点中带有'<'符号  hello{{}}
function parseText(context) {
  const endTokens = ['<', context.options.delimiters[0]];
// 找在 < 前面 的 {{ ,找不到就用 < 结尾
  // 寻找text最近的endIndex。因为遇到'<'或'{{'都可能结束
  let endIndex = context.source.length;
  for (let i = 0; i < endTokens.length; i++) {
    const index = context.source.indexOf(endTokens[i], 1);
    if (index !== -1 && endIndex > index) {
      endIndex = index;
    }
  }
​
  const content = parseTextData(context, endIndex);
​
  return {
    type: NodeTypes.TEXT,
    content,
  };
}

插值节点

{{name}}</div>

function parseInterpolation(context) {
  const [open, close] = context.options.delimiters;
​
  advanceBy(context, open.length);
  const closeIndex = context.source.indexOf(close);
    // trim() 去掉name两边可能存在的空格 
  const content = parseTextData(context, closeIndex).trim();
  advanceBy(context, close.length);
​
  return {
    type: NodeTypes.INTERPOLATION,
    content: {
      type: NodeTypes.SIMPLE_EXPRESSION,
      isStatic: false,
      content,
    },
  };
}

删除空白

vue-next-template-explorer.netlify.app/#eyJzcmMiOi…

遍历生成的节点,删掉字符之间多余的空格,删掉两个之间有换行的元素中间的空白节点

# 例子1
<div>
  a
    b
</div>
把两个字符中间的换行和空格 换成单纯的一个空格
export function render(_ctx, _cache, $props, $setup, $data, $options) {
    //return (_openBlock(), _createElementBlock("div", null, "\r\n  a\r\n    b\r\n"))
  return (_openBlock(), _createElementBlock("div", null, " a b "))
}
# 例子2
<div>
  <span>a</span>
  <span>b</span> <span>c</span>
</div>
可以看到,两个节点中间有换行,则可以删掉中间的空白节点,如果不是换行,则要保留
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("span", null, "a"),
    _createElementVNode("span", null, "b"),
    _createTextVNode(),
    _createElementVNode("span", null, "c")
  ]))
}

代码

function parseChildren(context) {
  const nodes = [];
。。。
​
  let removedWhitespace = false;
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    if (node.type === NodeTypes.TEXT) {
      // 全是空白的节点
      if (!/[^\t\r\n\f ]/.test(node.content)) {
        const prev = nodes[i - 1];
        const next = nodes[i + 1];
        // 开头和结尾是空白,删掉空白节点
        if (
          !prev ||
          !next ||
          (prev.type === NodeTypes.ELEMENT &&
            next.type === NodeTypes.ELEMENT &&
            /[\r\n]/.test(node.content))
        ) {
          // <span>b</span> 换行 <span>c</span>
          // 不能在这里删,会影响节点遍历,打标志,说明有删除操作
          removedWhitespace = true;
          nodes[i] = null;
        } else {
          // <span>b</span> <span>c</span>
          // Otherwise, the whitespace is condensed into a single space
          node.content = ' ';
        }
      } else {
        // a   b
        node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ');
      }
    }
  }
// 有删除操作 去掉删掉的节点
  return removedWhitespace ? nodes.filter(Boolean) : nodes;
}
​