前言
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)
}