vue将模板字符串渲染为DOM的过程

一、模板编译

  1. parse方法,将 template 中的代码解析成AST抽象语法树;(本文主要实现parse方法)
  2. optimize 方法,优化AST抽象语法树,防止重复渲染;
  3. generate函数,将AST抽象语法树生成render函数字符串(h函数);

    二、渲染DOM

  4. h函数,生成虚拟节点

  5. patch方法,diff算法,并渲染为真实DOM

vue源码之AST抽象语法树的实现 - 图1

实现简版parse方法,将模板字符串转换为AST

简版不考虑以下情况:

  1. 自结束标签(有兴趣的伙伴可以自行尝试编写)
  2. 文本节点位于结束标签与开始标签之间,比如:

    文本节点

    最终效果:

    vue源码之AST抽象语法树的实现 - 图2

    实现思路:

  3. 遍历模板字符串,利用栈的思想

  4. 如果是开始标签,处理属性信息,向栈中推入该标签的信息,如:{ tag:’div’, attrs:[], type:1, children: [] }
  5. 如果是文本节点,向栈顶元素的children属性中推入文本节点
  6. 如果是结束标签,弹栈,并将弹栈的结果,推入到当前栈顶元素的children属性中,如果弹栈后,栈空了,说明已经遍历完一个根元素了

    实现代码:

    parse.js 解析模板字符串

    1. import parseAttr from "./parseAttr"
    2. // parse函数
    3. export default function (template) {
    4. let i = 0
    5. let lastTem = '' // 记录剩下的模板字符串
    6. let stack = [] // 栈
    7. // 匹配收集开始标签,并收集除标签名以外的attr
    8. let startReg = /^<([a-z]+[1-6]?)(\s.*?)?>/
    9. // 匹配收集结束标签
    10. let endReg = /^<\/([a-z]+[1-6]?)>/
    11. // 匹配收集文字
    12. let wordReg = /^>(.*?)<\//
    13. // 记录结果,模板字符串可以不止一个根标签,都搜集到children中
    14. let res = { tag: 'template', children: [] }
    15. while (i < template.length) {
    16. lastTem = template.substring(i) // 获取剩下的模板字符串
    17. if (startReg.test(lastTem)) { // 如果是开始标签
    18. let match = lastTem.match(startReg)
    19. let startTag = match[1] // 开始标签
    20. let attrs = match[2] // 属性字符串
    21. let attrsArr = [] // 收集属性的数组
    22. if (attrs) {
    23. attrsArr = parseAttr(attrs) // 将属性字符串变为数组
    24. }
    25. // 将标签信息推入栈数组
    26. stack.push({ tag: startTag, attrs: attrsArr, type: 1, children: [] })
    27. i += match[0].length - 1 // length-1,是为了能收集到标签后的文字
    28. } else if (endReg.test(lastTem)) { // 如果是结束标签
    29. let endTag = lastTem.match(endReg)[1] // 结束标签
    30. if (endTag == stack[stack.length - 1].tag) {
    31. let top = stack.pop() // 栈顶元素
    32. if (stack.length == 0) {
    33. // 栈中没有元素时,表示遍历完了一个根元素
    34. res.children.push(top)
    35. } else {
    36. // 将栈顶元素,推入上一个元素的children属性中
    37. stack[stack.length - 1].children.push(top)
    38. }
    39. } else {
    40. // 收集的结束标签与栈顶元素的tag不相等
    41. throw new Error(`${stack[stack.length - 1].tag}没有结束标签`)
    42. }
    43. i += endTag.length + 3
    44. } else if (wordReg.test(lastTem)) { // 文本节点
    45. let word = lastTem.match(wordReg)[1]
    46. // 将文本节点推入栈顶元素的children中
    47. stack[stack.length - 1].children.push({ text: word, type: 3 })
    48. i += word.length
    49. } else { // 如果是其他情况(比如是标签间空格),就不做任何处理
    50. i++
    51. }
    52. }
    53. return res
    54. }

    parseAttr.js 解析属性字符串

    思路:
  7. 遍历属性字符串,使用一个变量记录是否在引号内

  8. 如果遇到在空格,且不在引号内的空格,从当前位置切断并存储到数组中
  9. 利用”=”将数组中每个字符串转换为{name:xxx,value:xxx}的对象格式
    1. // parseAttr函数 解析attr
    2. export default function (attrStr) {
    3. let attrs = attrStr.trim() // 去除前后空格
    4. let res = [] // 记录结果
    5. let spaceIn = false // 记录是否在引号内,默认不在引号内
    6. let pos = 0 // 记录断点
    7. for (let i = 0; i < attrs.length; i++) {
    8. let char = attrs[i]
    9. if (char == '"') {
    10. spaceIn = !spaceIn // 遇到引号,spaceIn取反
    11. } else if (char == ' ' && !spaceIn) { // 遇到空格,且不在引号内
    12. // 从引号处切断,并存储到res数组中
    13. res.push(attrs.substring(pos, i).trim())
    14. pos = i // 将断点设置为当前索引
    15. }
    16. }
    17. // 将最后一项推到结果数组中
    18. res.push(attrs.substring(pos).trim())
    19. // 使用map方法,将数组中的每一项变为{name:xxx,value:xxx}的格式
    20. return res.map(item => {
    21. let o = item.match(/(.*?)="(.*?)"/)
    22. return {
    23. name: o[1],
    24. value: o[2]
    25. }
    26. })
    27. }

    index.js 测试代码

    1. import parse from "./parse";
    2. // 两个同级的ul标签
    3. let template = `
    4. <ul class="hide box" id="box">
    5. <li>A</li>
    6. <li>B</li>
    7. </ul>
    8. <ul class="show box" id="box2">
    9. <li>C</li>
    10. <li>D</li>
    11. </ul>
    12. `
    13. console.log(parse(template))
    结果:
    image.png