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

回顾上一篇

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

好废话不多说直接开始

1、生成render函数
1、$mount

众所周知 vue是需要传进去一个根节点的即el,我们正是要把template模板的内容挂到el,这个el是必传的,但是方法不唯一也可以 <math xmlns="http://www.w3.org/1998/Math/MathML"> m o u n t ( e l ) , 传到 o p t i o n s 里的 e l 也是走 mount(el),传到options里的el也是走 </math>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算法再依赖收集和通知更新写完后再写

相关推荐
Martin -Tang36 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发37 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁2 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂2 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐3 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成5 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽5 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新6 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_7 小时前
【Linux】多线程(概念,控制)
linux·运维·前端