- 把模板轉(zhuǎn)化成render函數(shù)
- 調(diào)用render函數(shù)產(chǎn)生虛擬節(jié)點,將虛擬節(jié)點插入到真實節(jié)點上
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函數(shù)方法: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) {
//如果是開始標簽桌粉,需要截取掉前面的字符串,并獲取到匹配到的數(shù)據(jù)
const match = {
tagName: start[1],
attrs: [],
};
advance(start[0].length);
let end;
//如果沒有遇到結(jié)束標簽就不停的解析
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) {
//看要解析的內(nèi)容是否存在鳞青,如果存在就不停的解析
let textEnd = html.indexOf("<"); //可能是開始標簽霸饲,也可能是結(jié)束標簽
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字符串解析成對應(yīng)的腳本來觸發(fā) 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函數(shù)->虛擬dom(增減額外的屬性)->生成真實的dom
}
創(chuàng)建虛擬節(jié)點方法:createElm
export function patch(oldVnode,vnode){//新的虛擬節(jié)點替換掉老的節(jié)點胶惰,先插入傻工,后刪除
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;//首次渲染時將老節(jié)點刪除掉,會導(dǎo)致再次更新數(shù)據(jù)孵滞,獲取不到老節(jié)點中捆,所以沒辦法插入,所以每次更新老節(jié)點
}
}
function createComponent(vnode){
let i = vnode.data;
if((i = i.hook) && (i = i.init)){
i(vnode);//調(diào)用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)){
//返回組件對應(yīng)的真實節(jié)點
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;
}