- 把模板转化成render函数
- 调用render函数产生虚拟节点,将虚拟节点插入到真实节点上
let oldTemplate = `<div>{{message}}</div>`;
let vm1 = new Vue({data:{message:'hello world'}});
const render1 = compileToFunction(oldTemplate);
const oldVnode = render1.call(vm1);//虚拟dom
document.body.appendChild(createElm(oldVnode));
生成render函数方法:compileToFunction
//parse.js
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; //属性正则
const unicodeRegExp =
/a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
const dynamicArgAttribute =
/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`; //标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //用来获取标签名的,match后索引为1的
const startTagOpen = new RegExp(`^<${qnameCapture}`); //开始标签
const startTagClose = /^\s*(\/?)>/; //<div/>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); //闭合标签
const doctype = /^<!DOCTYPE [^>]+>/i;
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!\--/;
const conditionalComment = /^<!\[/;
export function parseHtml(html) {
let root = null;
let stack = [];
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;
} else {
//说明不是开始标签
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) {
charts(text);
advance(text.length);
}
} //html字符串解析成对应的脚本来触发 tokens <div id="app"></div>
function createAstElement(tagName, attrs) {
return {
tag: tagName,
children: [],
attrs,
type: 1,
parent: null,
};
}
function start(tagName, attributes) {
let parent = stack[stack.length - 1];
let element = createAstElement(tagName, attributes);
if (!root) {
root = element;
}
if (parent) {
element.parent = parent; //当放入栈时,记录父亲是谁
parent.children.push(element);
}
stack.push(element);
}
function end(tagName) {
let last = stack.pop();
if (last.tag != tagName) {
throw new Error("标签闭合有误");
}
}
function charts(text) {
text = text.replace(/\s/g, "");
let parent = stack[stack.length - 1];
if (text) {
parent.children.push({
type: 3,
text,
});
}
}
return root;
}
//generate.js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
function getProps(el){//{[name:'id',value:app]}
let str = ''
el.forEach(attr=>{
//修改style前_c('div'),{d:"app",class:"app",style:"color:red;background:blue"}
if (attr.name == "style") {
let styleObj = {};
attr.value.replace(/([^:;]+):([^:;]+)/g, function () {
styleObj[arguments[1]] = arguments[2];
});
attr.value = styleObj;
}
//修改style之后_c('div'),{d:"app",class:"app",style:{"color":"red","background":"blue"}}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
})
return `{${str.slice(0,-1)}}`
}
function gen(el){
if(el.type == 1){
return generate(el);
}else{
let text = el.text;
if(!defaultTagRE.test(text)){
return `_v("${text}")`
}else{
// 'hello' + arr + 'world' hello{{arr}}world
let tokens = [];
let match;
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 generateChildren(el){
let children = el.children
if(children){
return children.map(c=>gen(c)).join(',')
}
return false
}
export function generate(el){
//遍历树,将树拼接成字符串
let children = generateChildren(el);
let str = el.attrs.length>0?getProps(el.attrs):'undefined';
let code = `_c('${el.tag}',${str}${children?`,${children}`:''})`;
return code
}
//index.js
import { generate } from "./generate";
import { parseHtml } from "./parse";
export function compileToFunction(el) {
const root = parseHtml(el);
//生成代码 _c('div',{id:'app',a:1},[text])
let code = generate(root);
let render = new Function (`with(this){return ${code}}`)
return render
//html->ast(只能描述语法,语法不存在的属性无法描述)->render函数->虚拟dom(增减额外的属性)->生成真实的dom
}
创建虚拟节点方法:createElm
export function patch(oldVnode,vnode){//新的虚拟节点替换掉老的节点,先插入,后删除
if(!oldVnode){
return createElm(vnode)
}
if(oldVnode.nodeType == 1){
const parent = oldVnode.parentNode;
const newElm = createElm(vnode);
parent.insertBefore(newElm, oldVnode.nextSibling);
parent.removeChild(oldVnode);
return newElm;//首次渲染时将老节点删除掉,会导致再次更新数据,获取不到老节点,所以没办法插入,所以每次更新老节点
}
}
function createComponent(vnode){
let i = vnode.data;
if((i = i.hook) && (i = i.init)){
i(vnode);//调用init方法
}
if (vnode.componentInstance) {
//有属性说明子组件new完毕了,并且组件的真实dom挂载到了vnode。componentInstance
return true;
}
}
export function createElm(vnode){
let {vm,tag,data,children,text} = vnode;
if(typeof tag === 'string'){
//判断是否是组件
if( createComponent(vnode)){
//返回组件对应的真实节点
console.log(vnode.componentInstance.$el);
return vnode.componentInstance.$el
}
vnode.el = document.createElement(tag);
if(children.length){
children.forEach(child=>{
vnode.el.appendChild(createElm(child));
})
}
}else{
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
网友评论