vue2源码---模板编译

113 阅读1分钟

1、第一步将模板解析成ast语法树

// 模板编译原理

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;  
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 >


 
export  function parserHTML(html) {
  // 可以不停的截取模板, 直到把模板全部解析完毕
  let stack = []
  let root = null;
  // 构建父子关系
  function createASTElement(tag,attrs,parent) {
    return {
      tag,
      type: 1, // 元素
      children: [],
      parent,
      attrs
    }
  }
  function start (tag,attrs) { // [div,p]
    // 遇到开始标签 就取栈中的最后一个作为节点 
    let parent = stack[stack.length-1]
    let element =  createASTElement(tag,attrs,parent)
    if (root === null) { // 说明当前节点就是根节点
      root = element
    }
    if (parent) {
      element.parent = parent // 更新p的parent属性 指向parent
      parent.children.push(element)
    }
    stack.push(element)
  }
  function end (tagName) {
    let endTag = stack.pop()
    if (endTag.tag !== tagName) {
      console.log('标签出错')
    }
  }

  function text (chars) {
    let parent = stack[stack.length -1]
    chars = chars.replace(/\s/g,"")
    if (chars) {
      parent.children.push({
        type: 2,
        text:chars
      })
    }
  }
  function advance(len) {
    html = html.substring(len)
  }
  function parseStartTag() {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1],
        attrs: []
      }
      advance(start[0].length)
  
      let end;
      let attr;
      while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { // 1、要有属性 2、不是开始标签的结束标签
        match.attrs.push({ name: attr[1],value: attr[3] || attr[4] || attr[5]})
        advance(attr[0].length)
      }
      if (end) {
        advance(end[0].length)
      }
      return match
    } else {
      return false
    }
  }
  while(html) {
    // 解析标签和文本
    let index = html.indexOf('<')
    debugger
    if (index === 0) { 
      // 解析开始标签 并且把属性也解析出来
      const startTagMatch = parseStartTag()
      if (startTagMatch) { // 开始标签
        start(startTagMatch.tagName, startTagMatch.attrs)
        continue;
      }
      let endTagMatch;
      if (endTagMatch = html.match(endTag)) { // 结束标签
        end(endTagMatch[1])
        advance(endTagMatch[0].length)
        continue;
      }
    }
    if (index > 0) { // 文本
      let chars = html.substring(0,index) 
      text(chars)
      advance(chars.length)
    }
  }
  // console.log('root',root)
  return root
}

2、第二步根据生成的ast树生成代码

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
function genProps(attrs) {
  let str = '';
  for(let i = 0; i < attrs.length; i++) {
    let attr = attrs[i]
    if (attr.name === 'style') {
      let styles = {}
      attr.value.replace(/([^;:]+):([^;:]+)/g,function () {
        styles[arguments[1]] = arguments[2]
      })
      attr.value = styles
    }
    str += `${attr.name}: ${JSON.stringify(attr.value)},`
  }
  
  return `{${str.slice(0,-1)}}`
}
function gen(el) {
  if (el.type === 1) {
    return generate(el) // 如果是元素就递归生成
  } else {
    let text = el.text
    if (!defaultTagRE.test(text)) return `_v('${text}')` // 说明就是普通文本

    // 说明有表达式 我需要做一个表达式和普通值的拼接['aaaa',_s(name),'bbbb'].join('+')
    // _v('aaa'+_s(name)+'bbb')
    let lastIndex = defaultTagRE.lastIndex = 0 // 每次都需要重置为0
    let tokens = []
    let match;
    while(match = defaultTagRE.exec(text)) { // 如果正则+ g配合exec就会有一个问题lastIndex的问题 每次匹配完lastIndex会向前递进
      let 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('+')})`
  }
}
function genChildren(ast) {
  let children = ast.children
  if (children) {
    return `${children.map(c=>gen(c)).join(',')}`
  }
  return false
}
export function generate(ast) {
  let children = genChildren(ast)
  let code = `_c('${ast.tag}',${
    ast.attrs.length ? genProps(ast.attrs) : 'undefined'
  }${
    children ? `,${children}`: ''
  })`   
  return code
}

注意正则 + g的配合使用造成的lastIndex问题,需要将lastIndex手动置为0

3、第三步根据生成的code 利用 new Function + with 生成render函数

export function compileToFunction (template) {
  // 1、将模板变成ast语法树
  let ast = parserHTML(template)
  

  // 2、代码生成
  let code = generate(ast)
  let render = `with(this){return ${code}}`;
  let renderFn = new Function(render);
  return renderFn
}