vue2源码学习--02-1模板编译(template到ast语法树)

回顾上一篇

上一篇文章手写了响应式原理,目前达成效果当读取数据会走get里的方法,数据修改会走set方法,数组调用改变数组的方法会触发我们新写的方法 当前目录结构以及效果

好废话不多说直接开始

1、生成render函数
1、$mount

众所周知 vue是需要传进去一个根节点的即el,我们正是要把template模板的内容挂到el,这个el是必传的,但是方法不唯一也可以 m o u n t ( e l ) , 传到 o p t i o n s 里的 e l 也是走 mount(el),传到options里的el也是走 mount(el),传到options里的el也是走mount方法。 以下是实现过程 init.js里

js 复制代码
export function initMixin(Vue) {
    Vue.prototype._init = function(options) {
        const vm = this;
        vm.$options = options
        initState(vm);

        if(options.el) {
            vm.$mount(options.el)
        }
    }
    Vue.prototype.$mount = function(el) {
      const vm = this;
      el = document.querySelector(el);
      let ops = vm.$options
      if (!ops.render) {
          let template;
          if(!ops.template && el) { // 没写模板但是写了el
              template = el.outerHTML
          } else {
              if(el) {
                  template = ops.template
              }
          }
          if(template) {
              const render = compileToFunction(template)
              ops.render = render;
          }
      }
      //  mountComponent(vm, el) // 组件挂载
  }
}

这块template也是options选项里的内容,如果传了就用传进来的没有会拿el节点的xml内容,最终我们总会拿到template,然后我们需要根据拿到的模板生成render函数,即compileToFunction

2、生成render函数第一步:生成ast语法树

src下新建compiler文件夹,新建index.js

js 复制代码
export function compileToFunction(template) {
    // 将template 转换成ast语法书
    // 生成render方法 render方法执行后返回结果是 虚拟dom
    let ast = parseHTML(template)
    // 核心就是生成render函数
    // let render = ''
    // return render
}

对模板的处理用的是正则匹配,为维护方便,新建一个parse.js。因为只是学习原理不会写完所有正则匹配,只写几个常用的,这块真的太磨人了。 以下是用到的正则

js 复制代码
// // 识别合法的xml标签
const ncname = '[a-zA-Z_][\\-\\.0-9_a-zA-Z]*';

// // 复用拼接
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;

// // 匹配注释
// var comment =/^<!--/;

// // 匹配<!DOCTYPE> 声明标签
// var doctype = /^<!DOCTYPE [^>]+>/i;

// // 匹配条件注释
// // var conditionalComment =/^<![/;

// // 匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`);

// // 匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);

// // 匹配单标签
const startTagClose = /^\s*(\/?)>/;

// // 匹配属性,例如 id、class
// 第一分组是key value是 3/4/5 分组
const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

// {{ ... }}
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g

下边的跟上边是同一个文件,为了加一些说明,所以拆开

js 复制代码
export function parseHTML(html) { 
    // 标签type=1 文本=3 只是一个标识
    const ELEMENT_TYPE = 1
    const TEXT_TYPE = 3
    ...
    while (html) {
        // 首次传进来 <div></div>
        let textEnd = html.indexOf('<');
        // 如果<是第一个 再去用正则判断是不是起始标签的开始例:<div> 如果是则对标签内容进行处理
        if (textEnd === 0) {
            // 对开始标签处理
            const startTagMatch = parseStartTag()
            if(startTagMatch) {
            // 生成ast语法对象
                start(startTagMatch.tagName, startTagMatch.attrs)
                continue
            }
            // 判断是不结束标签的开始例如</div>
            let endTagMacth = html.match(endTag)
            if(endTagMacth) {
                end(endTagMacth[1])
                advance(endTagMacth[0].length)
                continue
            }
        }
        // <尖角号不是开始而是在后边判断为文本内容处理 如 123</div> 对123进行处理
        if(textEnd > 0) {
            let text = html.substring(0,textEnd)
            if(text) {
                chars(text)
                advance(text.length)
            }
        }
    }
}

以上是判断出模板内容的几种情况,然后根据不同情况进行不同的处理

下面开始具体处理方法

1、对开始标签的处理

js 复制代码
// 处理过的内容进行删除
function advance(n) {
    html = html.substring(n)
}
function parseStartTag() {
    const start = html.match(startTagOpen)
    if (start) {
        const match = {
            tagName: start[1],
            attrs: []
        }
        // 对内容处理完后就删掉
        advance(start[0].length)
        let attr, end;
        // 如果不是开始标签的结束(这里匹配到结束符号即>会给end赋值)
        while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
            advance(attr[0].length)
            match.attrs.push({name: attr[1], value: attr[3] || attr[4] || attr[5] || true})
        }
        // 如果end有值了 则进行删除操作
        if (end) {
            advance(end[0].length)
        }
        // 结果举例:{tagName:'div', attrs:[{name:'id', value:'1'}] }
        return match
    }
    // 如果不是开始标签的结束 循环match属性

    return false
}

生成ast语法

js 复制代码
// 创建一个栈,读取到开始标签就把当前标签生成的ast对象放进去
// 标签可能是多层级的例如 <div>  <div></div> <p><p> </div>
// 当我们读取到开始标签就入栈读取到结束标签就把该ast语法对象出栈,永远保证不出错
const stack =[];
// 父标签
let currentParent;
// 根节点
let root;
function start(tag, attrs) {
    let node = createASTElement(tag, attrs)
    if(!root) {
        root = node
    }
    // 有父元素 就把正在处理的parent指向父元素, 父元素的children push进当前处理的元素
    if(currentParent) {
        node.parent = currentParent
        currentParent.children.push(node)
    }
    stack.push(node)
    // 把当前元素置为父元素
    currentParent = node
}
function createASTElement(tag, attrs) {
    return {
        tag,
        type: ELEMENT_TYPE,
        children: [],
        attrs,
        parent: null
    }
}

2、结束标签的开始 的处理

js 复制代码
    // 结束就是 stack的当前处理标签的出栈 以及当前父元素重新获取当前栈的最后一个
    function end(tag) {
        stack.pop()
        currentParent = stack[stack.length -1]
    }

3、文本内容的处理

js 复制代码
function chars(text) {
    text = text.replace(/\s/g, '')
    text && currentParent.children.push({
        type: TEXT_TYPE,
        text,
        parent: currentParent
    })
}

4、完整版

js 复制代码
// // 识别合法的xml标签
const ncname = '[a-zA-Z_][\\-\\.0-9_a-zA-Z]*';

// // 复用拼接,这在我们项目中完成可以学起来
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;

// // 匹配注释
// var comment =/^<!--/;

// // 匹配<!DOCTYPE> 声明标签
// var doctype = /^<!DOCTYPE [^>]+>/i;

// // 匹配条件注释
// // var conditionalComment =/^<![/;

// // 匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`);

// // 匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);

// // 匹配单标签
const startTagClose = /^\s*(\/?)>/;

// // 匹配属性,例如 id、class
// 第一分组是key value是 3/4/5 分组
const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

// {{ ... }}
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g

// // 匹配动态属性,例如 v-if、v-else
// var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)[[^=]+][^\s"'<>/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

export function parseHTML(html) { //html 以<开始
    const ELEMENT_TYPE = 1
    const TEXT_TYPE = 3
    const stack =[]
    let currentParent ;
    let root;

    function createASTElement(tag, attrs) {
        return {
            tag,
            type: ELEMENT_TYPE,
            children: [],
            attrs,
            parent: null
        }
    }
    function start(tag, attrs) {
        let node = createASTElement(tag, attrs)
        if(!root) {
            root = node
        }
        if(currentParent) {
            node.parent = currentParent
            currentParent.children.push(node)
        }
        stack.push(node)
        currentParent = node
    }
    function chars(text) {
        text = text.replace(/\s/g, '')
        text && currentParent.children.push({
            type: TEXT_TYPE,
            text,
            parent: currentParent
        })
    }
    function end(tag) {
        stack.pop()
        currentParent = stack[stack.length -1]
    }
    function advance(n) {
        html = html.substring(n)
    }
    function parseStartTag() {
        const start = html.match(startTagOpen)
        if (start) {
            const match = {
                tagName: start[1],
                attrs: []
            }
            advance(start[0].length)
            let attr, end;
            // 如果不是开始标签的结束
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                advance(attr[0].length)
                match.attrs.push({name: attr[1], value: attr[3] || attr[4] || attr[5] || true})
            }
            if (end) {
                advance(end[0].length)
            }
            return match
        }
        // 如果不是开始标签的结束 循环match属性

        return false
    }
    while (html) {
        // 首次传进来 <div></div>
        let textEnd = html.indexOf('<');
        // 如果<是第一个 再去用正则判断是不是起始标签的开始例:<div> 如果是则对标签内容进行处理
        if (textEnd === 0) {
            const startTagMatch = parseStartTag()
            if(startTagMatch) {
                start(startTagMatch.tagName, startTagMatch.attrs)
                continue
            }
            // 判断是不结束标签的开始例如</div>
            let endTagMacth = html.match(endTag)
            if(endTagMacth) {
                end(endTagMacth[1])
                advance(endTagMacth[0].length)
                continue
            }
        }
        // <尖角号不是开始而是在后边判断为文本内容处理 如 123</div> 对123进行处理
        if(textEnd > 0) {
            let text = html.substring(0,textEnd)
            if(text) {
                chars(text)
                advance(text.length)
            }
        }
    }
    return root
}

6、检验成果

以上模板转ast语法就完成了 文章太长了,下一篇 从ast语法树到render函数,再到真实dom,dom diff算法再依赖收集和通知更新写完后再写

相关推荐
做前端的娜娜子2 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅2 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni3 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学4 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
赵庆明老师4 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim
wc884 小时前
微软EDGE浏览器功能学习
前端·学习·edge
Csvn5 小时前
🎯 原生 `<dialog>` 元素:终于可以扔掉一半的自定义弹窗组件了?
前端
Csvn5 小时前
🎨 CSS @layer:用「层叠优先级」终结样式打架的世纪难题
前端
濮水大叔6 小时前
不必把 Vue3 写成“麻花”:从状态碎片到对象协作,重新理解 Zova 的前端心智模型
前端·typescript·vue3·ioc·tsx·zova