02vue2源码之vue编译过程($mount)

109 阅读2分钟

初始化init时候挂载

  Vue.prototype._init = function(options) {
        // el,data
       ---------
        if(vm.$options.el){
            // 将数据挂载到这个模板上
            vm.$mount(vm.$options.el);
        }
    }

原型上的挂载方法

  • compileToFunction方法将模板转换为render函数
 Vue.prototype.$mount = function (el) {
        const vm = this;
        const options = vm.$options
        el = document.querySelector(el);
        vm.$el = el;
        // 把模板转化成 对应的渲染函数 =》 虚拟dom概念 vnode =》 diff算法 更新虚拟dom =》 产生真实节点,更新
        if(!options.render){ // 没有render用template,目前没render
            let template = options.template;
            if(!template && el){ // 用户也没有传递template 就取el的内容作为模板
                template = el.outerHTML;
                let render = compileToFunction(template);
                options.render = render;
            }
        }
        // options.render 就是渲染函数
        // 调用render方法 渲染成真实dom 替换掉页面的内容
        mountComponent(vm,el); // 组件的挂载流程
}

1.compileToFunction方法实现编译流程

let root = parserHTML(template) 
// 生成代码 
let code = generate(root)
//生成render函数
let render = new Function(`with(this){return ${code}}`); // code 中会用到数据 数据在vm上

问题核心:如何将template转换成render函数 ?

  • 1.将template模板转换成 ast 语法树 - parserHTML
  • 2.对静态语法做静态标记 - markUp
  • 3.重新生成代码 - codeGen

2.parserHTML,将template模块转换成ast树

  • 将html传入,查看要解析的内容是否存在,如果存在就不停的解析
  • 将我们的html =》 词法解析(开始标签,结束标签,属性,文本)
  • ast语法树 用来描述html语法的 使用栈来表示stack = []

1.先定义好匹配标签和属性等正则规则

const ncname = `[a-zA-Z_][\-\.0-9_a-zA-Z]*`; // 标签名 
const qnameCapture = `((?:${ncname}\:)?${ncname})`; //  用来获取的标签名的 match后的索引为1的
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配开始标签的 
const endTag = new RegExp(`^<\/${qnameCapture}[^>]*>`); // 匹配闭合标签的
//           aa  =   "  xxx "  | '  xxxx '  | xxx
const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // a=b  a="b"  a='b' //用来匹配属性的
const startTagClose = /^\s*(/?)>/; //     />   <div/>
const defaultTagRE = /{{((?:.|\r?\n)+?)}}/g; // {{aaaaa}}
// 将解析后的结果 组装成一个树结构,这是ast树的结构
function createAstElement(tagName, attrs) {
    return {
        tag: tagName,
        type: 1,
        children: [],
        parent: null,
        attrs
    }
}

2.查看解析内容,不停的解析

 //初始化进入 textEnd == 0
 //  <div id="app" a=1 style="color:red;background:blue">
 //       hello {{arr}} world
 //   </div>

//开始标签解析完成后,这时候先解析文本 textEnd != 0
//"
//        hello {{arr}} world
//    </div>"

//当解析到最后的标签时
//"</div>" textEnd == 0

//最后 html 为 "" 返回对应的ast树

function parserHTML(html){
      while (html) { // 看要解析的内容是否存在,如果存在就不停的解析
        let textEnd = html.indexOf('<'); // 当前解析的开头  
        if (textEnd == 0) {
            const startTagMatch = parseStartTag(html); // 解析开始标签
            //开头匹配到的match
            //startTagMatch = [tagName:'div',attrs:[{name:'id',value:'app',name:'a',value,'1'}]]
            if (startTagMatch) {
                start(startTagMatch.tagName, startTagMatch.attrs)//等到开始标签解析完成后,解析结束标签
                continue; 
            }
            const endTagMatch = html.match(endTag);
            if (endTagMatch) {
                end(endTagMatch[1]); //推出栈中元素
                advance(endTagMatch[0].length); //删除结束标签
                continue;
            }
        }
        
        let text; 
        if (textEnd > 0) {
            text = html.substring(0, textEnd) //  hello {{arr}} world
        }
        if (text) {
            chars(text); //解析文本节点,将文本节点,推入
            advance(text.length); //在删除html中的文本节点
        }
    }
}

3.解析开始标签

//解析开头
function parseStartTag() { //解析开头
        const start = html.match(startTagOpen); //判断是否是开头标签
        if (start) { //如果是开始标签
            const match = {
                tagName: start[1], //div
                attrs: []
            }
            //start[0] <div
            advance(start[0].length); 
            let end;
            // 如果没有遇到标签结尾就不停的解析
            let attr;
          	//判断遇到结束标签没有,如果没有并且有属性 例如 id = app a=1 等属性,则把属性也删除掉
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                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;
        }
        return false; // 不是开始标签
    }

4.将开始解析的标签推入到stack中

//tagName :div attrs:[{name:'id',value:'app',name:'a',value,'1'}]
function start(tagName, attributes) {
    let parent = stack[stack.length - 1];
    let element = createAstElement(tagName, attributes); //创建到ast树结构中
    if (!root) {
        root = element;
    }
    if(parent){ //判断有没有父亲
        element.parent = parent;// 当放入栈中时 继续父亲是谁
        parent.children.push(element)
    }
    stack.push(element);
}

5.解析文本的方法

function chars(text) {
    text = text.replace(/\s/g, ""); //去掉全局的空格
    let parent = stack[stack.length - 1]; //找到第一个推进栈中的元素作为父亲
    if (text) {
        //将文本推入到子节点,并且定义文本的type 3
        parent.children.push({
            type: 3, 
            text
        })
    }
}

6.解析最后的结束标签

function end(tagName) {
    let last = stack.pop(); //将栈中内容推出
    if (last.tag !== tagName) { //判断开始标签是否和结束标签一致
        throw new Error('标签有误');
    }
}

总结:就是将传入的html要素,一个一个匹配解析,最后生成一棵ast语法树,vue中采用的是正则匹配的方式,来进行匹配的

{
    "tag": "div",
    "type": 1,
    "children": [
        {
            "type": 3,
            "text": "hello{{arr}}world"
        }
    ],
    "parent": null,
    "attrs": [
        {
            "name": "id",
            "value": "app"
        },
        {
            "name": "a",
            "value": "1"
        },
        {
            "name": "style",
            "value": "color:red;background:blue"
        }
    ]
}

3.generate生成代码

  • 就是将ast树的代码转换为 _c _v _s所包裹的字符串 //code generate代码生成
  • 最终转换结果:_c('div',{id:"app",a:"1",style:{"color":"red","background":"blue"}},_v("hello"+_s(arr)+"world"))

1.调用generate函数,来生成对应的代码

export function generate(el) { //  _c('div',{id:'app',a:1},_c('span',{},'world'),_v())
    // 遍历树 将树拼接成字符串
    let children = genChildren(el); // _v("hello"+_s(arr)+"world")
    let code = `_c('${el.tag}',${
        el.attrs.length? genProps(el.attrs): 'undefined'
    }${
        children? `,${children}`:''
    })`;
  	//最后生成code  _c('div',{id:"app",a:"1",style:{"color":"red","background":"blue"}},_v("hello"+_s(arr)+"world"))
    return code;
}

2.genChildren先遍历ast树中children的部分,主要是针对于文本部分解析

function genChildren(el) {
    let children = el.children; // 获取儿子 [{"type": 3,"text": "hello{{arr}}world" }]
    if (children) { //在遍历孩子
        return children.map(c => gen(c)).join(',') // c {text:"hello{{arr}}world",type: 3}
    }
    return false;
}
//生成代码
function gen(el) {
    if (el.type == 1) { // element = 1 text = 3
        return generate(el); //如果判断是要素,则重新调用genetate函数
    } else {
        //如果是文本
        let text = el.text;
        if (!defaultTagRE.test(text)) { //判断如果没有 {{ }} 大括号,则直接_v展示文本
            return `_v('${text}')`;
        } else {
            // 'hello' + arr + 'world'    hello {{arr}} {{aa}} world
            let tokens = [];
            let match;
            let lastIndex = defaultTagRE.lastIndex = 0; // 默认初始化的时候为0 CSS-LOADER 原理一样 
            while (match = defaultTagRE.exec(text)) { // 看有没有匹配到
                //match: [0:"{{arr}}":1:"arr",index:5 //表示从5位开始匹配到 ] 匹配到对应的数据
                let index = match.index; // 开始索引
                if (index > lastIndex) { //判断找到的{{}}大于最后一个
                    tokens.push(JSON.stringify(text.slice(lastIndex, index))) //找到前半部分
                }
                // tokens [hello]
                tokens.push(`_s(${match[1].trim()})`); // JSON.stringify()
                // tokens [""hello"", "_s(arr)"] //给{{}}的部分加上_s
                lastIndex = index + match[0].length; //加到后半部分的数字端
            }
            if (lastIndex < text.length) {
                tokens.push(JSON.stringify(text.slice(lastIndex))) //切割后半部分的东西到数组里面
            }
            // tokens [""hello"","_s(arr)",""world""]
            return `_v(${tokens.join('+')})` //最后拼接上_v
        }
    }
}

3.解析attribute属性的拼接

//[{"name": "id","value": "app"},{"name": "a","value": "1"},{"name": "style","value": "color:red;background:blue"}]
function genProps(attrs) { // [{name:'xxx',value:'xxx'},{name:'xxx',value:'xxx'}]
    let str = '';
    for (let i = 0; i < attrs.length; i++) {
        let attr = attrs[i];
        if (attr.name === 'style') { // color:red;background:blue
            let styleObj = {};
            attr.value.replace(/([^;:]+):([^;:]+)/g, function() {
                styleObj[arguments[1]] = arguments[2]
            })
            attr.value = styleObj
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},`;
    }
    // id:"app",a:"1",style:{"color":"red","background":"blue"},
    return `{${str.slice(0,-1)}}`
}

4.通过with语法生成函数

with语句
作用域名–一个可以按序检索的对象列表,通过它可以进行变量名的解析。with语句用于临时拓展作用域链,语法如下:

//调用对象属性和方法的简写with:
let obj = {
    name: '使用with读取对象属性'
}
with(obj) {		//严格模式下将禁用with关键字
    console.log(name)
}

最后通过编辑等到的函数

 {
  with(this){
    return _c('div',
      {
        id:"app",a:"1",style:{"color":"red","background":"blue"}
      },
      _v("hello"+_s(arr)+"world")
    )}
}