阅读 122

vue-模板编译

在创建vue实例时,会传入el或者template模板,在vue内部会将模板编译成一个渲染函数render。模板编译的过程如下:

compileToFunction将模板转为render函数

function compileToFunction(template) {
  let root = parseHTML(template)
  let code = generate(root)
  let renderFn = new Function(`with(this){return ${code}}`)
  return renderFn
}
复制代码

生成AST语法树

AST语法树,是源代码抽象语法结构的树状表现形式。树上的每个节点都表示源代码中的一种结构。

parseHTML

const ncname = '[a-zA-Z_][\\w\\-\\.]*'; // 匹配abc-aaa
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; // <aa:aac>
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则,捕获的内容是标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);// 匹配标签结尾
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;// 匹配属性
const startTagClose = /^\s*(\/?)>/;// 匹配标签结束的>
let root = null; // ast的树根
let curParent; // 标识当前父级
let stack = [];
const ELEMENT_TYPE = 1
const TEXT_TYPE = 3;
function parseHTML(html) {
  while(html) { // 循环匹配
    let textEnd = html.indexOf('<');
    if(textEnd == 0) { // 如果是开头 可以匹配<xx m=n>或者</ xx>
      let startTagMatch = parseStartTag(); // 1. 匹配<xx m=n
      if (startTagMatch) {
        start(startTagMatch.tagName, startTagMatch.attrs);
        continue;
      }
      let endTagMatch = html.match(endTag) // 2.匹配</xx>
      if (endTagMatch) {
        advance(endTagMatch[0].length);
        end(endTagMatch[1]); 
        continue;
      }
    }
    let text;
    if (textEnd >= 0) { // 3.匹配文本
      text = html.substring(0, textEnd)
    }
    if (text) {
      advance(text.length);
      
      chars(text)
    }
  }
  // 从html上删除字符串
  function advance(n) { 
    html = html.substring(n)
  }
  // 解析开始标签
  function parseStartTag() { 
    let start = html.match(startTagOpen); // 匹配 <xx
    if(start){
      const match = {
        tagName: start[1],
        attrs: []
      }
      advance(start[0].length);
      let end, attr;
      while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { // 匹配m=n
        advance(attr[0].length);
        match.attrs.push({
          name: attr[1],
          value: attr[3] || attr[4] || attr[5]
        })
      }
      if (end) { // 匹配>
        advance(end[0].length);
        return match
      }
    }
  }
  return root
}
复制代码

start + createASTElement处理tagName和attrs

// 创建ast语法树
function createASTElement(tagname, attrs) { 
  return {
    tag: tagname,
    type: ELEMENT_TYPE,
    children: [],
    attrs,
    parent: null
  }
}

function start(tagName, attrs) {
  let element = createASTElement(tagName, attrs);
  if (!root) {
    root = element
  }
  curParent = element;
  stack.push(element)
}
复制代码

chars处理文本

function chars(text){
  text = text.replace(/\s/g, '');
  if(text) {
    curParent.children.push({
      text,
      type: TEXT_TYPE
    })
  }

}
复制代码

end处理父子关系

  • 定义全局变量stack和cuParent;
  • 当start时,保存curParent = element和stack.push(element)
  • 当end时,通过pop获取stack最后一项,它为element;它的父级为新stack的最后一项;建立双向父子关系
function end(tagName) {
  let element = stack.pop();
  curParent = stack[stack.length - 1]
  if (curParent) {
    element.parent = curParent;
    curParent.children.push(element)
  }
}
复制代码

将AST语法树generate为模板code--模板引擎

转为render函数是一个字符串拼接的过程。如果是元素,用_c包裹;如果是文本,用_v包裹;如果是变量,用_s包裹

generate:将AST转为模板字符串

function generate(el){
  const {tag, attrs, children} = el
  let newChildren = genChildren(el)
  // _c创建元素
  let code = `_c('${tag}', ${
    attrs.length > 0 ? genProps(attrs) : 'undefined'
  }, ${
    newChildren ? newChildren : ''
  })`;
  console.log(code, 'code')
  return code
}
复制代码

genProps:将属性数组转为属性子符串

function genProps(attrs){
  let str = '';
  for(let i = 0; i < attrs.length; i++) {
    let attr = attrs[i];
    if (attr.name === 'style') {
      
      let obj = {};
      attr.value.split(';').forEach(item => {
        let [key, value] = item.split(':')
        obj[key] = value
      });
      attr.value = JSON.stringify(obj);
      str += `${attr.name}:${attr.value},`
    } else {
      str += `${attr.name}:'${attr.value}',`
    }
  }
  return `{${str.slice(0, -1)}}`
}
复制代码

genChildren:将children转为字符串children

function genChildren(el) {
  let {children} = el;
  if(children && children.length) {
    return `${children.map(c => genC(c)).join(',')}`
  } else {
    return false
  }
}
复制代码

genC:根据child类型不同,进行不同转译

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{任意字符|换行}}
function genC(node) {
  if (node.type === 1) { //元素时,递归调用genenrate
    return generate(node)
  } else if (node.type === 3) {//文本时,区分普通文本和变量
    let text = node.text;
   let tokens = [];
   let match, index;
   let lastIndex = defaultTagRE.lastIndex = 0;
   while(match = defaultTagRE.exec(text)) {
     index = match.index
     if (index >= lastIndex) {
      tokens.push(JSON.stringify(text.slice(lastIndex, index)))
      tokens.push(`_s(${match[1].trim()})`) // 变量时
      lastIndex = index + match[0].length
     }
   }
   if (lastIndex < text.length){
     tokens.push(JSON.stringify(text.slice(lastIndex)))
   }

   return `_v(${tokens.join('+')})`
  }
}
复制代码

生成render

  • 通过with改变作用域
  • 通过newFunction生成新的function
let renderFn = new Function(`with(this){return ${code}}`)
复制代码