Vue2核心原理(简易) - 模板编译(笔记)

712 阅读2分钟

前言

html模板 -> ast树结构 -> render函数 = 代码字符串 + with + new Function -> 虚拟dom(vnode) -> 生产真实dom
本章项目地址

预期

将下面模板进行编译

<div id="app" style="font-size:17px; background:gray;">
    <span>hello {{ arr }} world</span>
</div>

<script>
    var vm = new Vue({
        data: {
            message: 'hello Vue'
        }
    })

    vm.$mount('#app')
</script>

编译各个阶段呈现

  • ast树结构(语法层面的描述 js css html)

  • render函数(用来生成虚拟dom)

  • vnode

正题

compileToFunction方法(编译初始化)

import { parserHTML } from './parser'
import { generate } from './generate'

/**
 * @description 编译成render函数
 * @description html模板 -> ast树结构 -> render函数 = 代码字符串 + with + new Function -> 虚拟dom(vnode) -> 生产真实dom
 */
export function compileToFunction(template) {
    /** ast树结构 */
    let root = parserHTML(template)
    
    /** render字符串 */
    let code = generate(root)

    /** render函数 */
    let render = new Function(`with(this){return ${code}}`)

    return render
}

parserHTML文件(ast语法层面的描述 js css html -> vdom)

/**
 * @description ast语法层面的描述 js css html -> vdom
 */

/** 标签名  */
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
/** 用来获取的标签名的 match后的索引为1 */
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
/** 匹配开始标签 */
const startTagOpen = new RegExp(`^<${qnameCapture}`);
/** 匹配闭合标签 */
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);

/** 匹配标签属性attribute 如 a=b a='b' a="b" */
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
/** 匹配开始标签的关闭字符 如 > /> */
const startTagClose = /^\s*(\/?)>/;

export function parserHTML(html) {
    /** 组装ast树结构 */
    function createAstElement(tagName, attrs) {
      return {
          tag: tagName,
          type: 1,
          children: [],
          parent: null,
          attrs
      }
    }

    /** 核心方法 */
    // 要返回的ast树结构
    let root = null;
    // 用于查找父标签 当该标签编译结束时删除掉
    let stack = [];
    
    /** 标签开始时组装树结构 */
    function start(tagName, attributes) {
      let parent = stack[stack.length - 1];
      let element = createAstElement(tagName, attributes);
      if (!root) {
          root = element;
      }
      if(parent){
          element.parent = parent.tag
          parent.children.push(element)
      }
      stack.push(element);
    }

    /** 文本时 */
    function chars(text) {
      text = text.replace(/\s/g, "");
      let parent = stack[stack.length - 1];
      if (text) {
          parent.children.push({
              type: 3,
              text
          })
      }
    }

    /** 标签结束时 */
    function end(tagName) {
      let last = stack.pop();
      if (last.tag !== tagName) {
          throw new Error('标签有误');
      }
    }

    /** 截取掉已经编译过的字符串 */
    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))) {
                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
    }

    while (html) {
        let textEnd = html.indexOf('<')
        // 标签开始时
        if (textEnd == 0) {
            const startTagMatch = parseStartTag(html); // 解析开始标签
            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) }
        if (text) {
            chars(text)
            advance(text.length)
        }
    }

    return root
}

generate文件 (render函数字符串)

/** 匹配 {{aaaa}} */
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g

/**
 * @description 编译属性 [{name:'xxx',value:'xxx'},{name:'xxx',value:'xxx'}]
 * @description 期望 { xxx: 'xxx' }
 */
function genProps(attrs) {
    let str = '';
    for (let i = 0; i < attrs.length; i++) {
        let attr = attrs[i];
        if (attr.name === 'style') {
            let styleObj = {};
            attr.value.replace(/([^;:]+)\:([^;:]+)/g, function() {
                styleObj[arguments[1]] = arguments[2]
            })
            attr.value = styleObj
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},`
    }
    return `{${str.slice(0,-1)}}`
}

/** 核心方法 */
/**
 * @description 递归生成 与 检索文本
 */
function gen(el) {
    if (el.type == 1) {
        return generate(el);
    } else {
        let text = el.text;
        if (!defaultTagRE.test(text)) {
            return `_v('${text}')`;
        } else {
            let tokens = [];
            let match;
            // 正则的g与exec发生冲突 将其置0 不然之检索一次
            let lastIndex = defaultTagRE.lastIndex = 0;
            while (match = defaultTagRE.exec(text)) {
                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(el) {
    let children = el.children
    if (children) {
        return children.map(c => gen(c)).join(',')
    }
    return false
}

/**
 * @description 将ast树结构 转换成 render函数字符串
 * @description _c('div',{id:'app',a:1},_c('span',{},'world'),_v())
 */
export function generate(el) {
    let children = genChildren(el)
    let code = `_c('${el.tag}',${ el.attrs.length? genProps(el.attrs): 'undefined'}${children? `,${children}`:''})`
    return code
}

render函数vnode

import { createElement, createTextElement } from './vdom/index'

export function renderMixin(Vue){
    Vue.prototype._c = function() {
        return createElement(this,...arguments)
    }

    Vue.prototype._v = function(text) {
        return createTextElement(this,text)
    }

    Vue.prototype._s = function(val) {
        if(typeof val == 'object') return JSON.stringify(val)
        return val;
    }

    Vue.prototype._render = function(){
       const vm = this
       let render = vm.$options.render
       let vnode = render.call(vm)
       return vnode
    }
}
import { isObject, isReservedTag } from '../utils'

/**
 * @description 创建标签的vnode
 */
export function createElement(vm, tag, data = {}, ...children) {
    // tag 是不是组件
    if (isReservedTag(tag)) {
        return vnode(vm, tag, data, data.key, children, undefined)
    } else {
        const Ctor = vm.$options.components[tag]
        return createComponent(vm, tag, data, data.key, children, Ctor)
    }

}

/**
 * @description 创建组件的vnode
 */
function createComponent(vm, tag, data, key, children, Ctor) {
    // 组件是不是构造函数
    if (isObject(Ctor)) {
        Ctor = vm.$options._base.extend(Ctor)
    }

    data.hook = {
        init(vnode) {
            let vm = vnode.componentInstance = new Ctor({_isComponent: true})
            vm.$mount()
        }
    }

    return vnode(vm, `vue-component-${tag}`, data, key, undefined, undefined, {Ctor, children})
}

/**
 * @description 创建文本的vnode
 */
export function createTextElement(vm, text) {
    return vnode(vm, undefined, undefined, undefined, undefined, text)
}

/** 核心方法 */
/**
 * @description 套装vnode
 */
function vnode(vm, tag, data, key, children, text, componentOptions) {
    return {
        vm,
        tag,
        data,
        key,
        children,
        text,
        componentOptions
        // .....
    }
}
/**
 * @description 是不是对象
 */
export function isObject(data) {
    return typeof data === 'object' && data !== null
}

/**
 * @description 是否是原生标签
 */
export function isReservedTag(str) {
    let reservedTag = 'a,div,span,ul,li,p,img,button'
    return reservedTag.includes(str)
}

下一章 Vue2核心原理(简易) - 视图更新(初次渲染)笔记