開始
上一節(jié)總結(jié)了Vue的響應(yīng)式數(shù)據(jù)原理,下面總結(jié)一下Vue中模板編譯。模板編譯情景眾多,復(fù)雜多變,現(xiàn)在只學(xué)習(xí)了普通標(biāo)簽的解析,編譯,未能對組件,指令,事件等多種情況進(jìn)行深入學(xué)習(xí)總結(jié)。
模板編譯
基本流程
-
解析模板代碼生成AST語法樹,主要依賴正則。
image 將ast 語法樹生成代碼。
with(this){
return _c("div",{id:"app"},_c("div",{class:"content"},_v("名稱:"+_s(name)),_c("h5",undefined,_v("年齡:"+_s(age)))),_c("p",{style:{"color":"red"}},_v("靜態(tài)節(jié)點")))
}
- 生成可執(zhí)行的 render 函數(shù)
(function anonymous( ) {
with(this){
return _c("div",{id:"app"},_c("div",{class:"content"},_v("名稱:"+_s(name)),_c("h5",undefined,_v("年齡:"+_s(age)))),_c("p",{style:{"color":"red"}},_v("靜態(tài)節(jié)點")))
}
})
生成 AST 語法樹
代碼位置 complier 中的 parser.js
主要依賴正則解析(我正則很渣,看懂都很難,以后再深入學(xué)習(xí)吧,直接照搬珠峰架構(gòu)姜文老師)
實現(xiàn)步驟
-
先解析開始標(biāo)簽 如
<div id='app'> ={ tagName:'div',attrs:[{id:app}]}方法:parseStartTag [1:< 2:div 3:id='app' 4:>] 四個部分 得到 tag,attr 然后進(jìn)入 start 方法,創(chuàng)建ast節(jié)點。
解析子節(jié)點標(biāo)簽(遞歸)
-
解析到結(jié)束標(biāo)簽
注意:解析玩開始節(jié)點后將節(jié)點入棧,解析到結(jié)束節(jié)點后然后將開始節(jié)點出棧,此時棧的最后一點就是當(dāng)前節(jié)點的父節(jié)點。例如:
[div,p]解析到</p>此時出棧[div]得到p,取棧尾 將p插入到div的子節(jié)點。
import {extend} from '../util/index.js'
// 字母a-zA-Z_ - . 數(shù)組小寫字母 大寫字母
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 標(biāo)簽名
// ?:匹配不捕獲 <aaa:aaa>
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// startTagOpen 可以匹配到開始標(biāo)簽 正則捕獲到的內(nèi)容是 (標(biāo)簽名)
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 標(biāo)簽開頭的正則 捕獲的內(nèi)容是標(biāo)簽名
// 閉合標(biāo)簽 </xxxxxxx>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配標(biāo)簽結(jié)尾的 </div>
// <div aa = "123" bb=123 cc='123'
// 捕獲到的是 屬性名 和 屬性值 arguments[1] || arguments[2] || arguments[2]
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配屬性的
// <div > <br/>
const startTagClose = /^\s*(\/?)>/; // 匹配標(biāo)簽結(jié)束的 >
// 匹配動態(tài)變量的 +? 盡可能少匹配 {{}}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
const stripParensRE = /^\(|\)$/g;
const ELEMENT_NDOE='1';
const TEXT_NODE='3'
export function parseHTML(html) {
console.log(html)
// ast 樹 表示html的語法
let root; // 樹根
let currentParent;
let elementStack = []; //
/**
* ast 語法元素
* @param {*} tagName
* @param {*} attrs
*/
function createASTElement(tagName,attrs){
return {
tag:tagName, //標(biāo)簽
attrs, //屬性
children:[], //子節(jié)點
attrsMap: makeAttrsMap(attrs),
parent:null, //父節(jié)點
type:ELEMENT_NDOE //節(jié)點類型
}
}
// console.log(html)
function start(tagName, attrs) {
//創(chuàng)建跟節(jié)點
let element=createASTElement(tagName,attrs);
if(!root)
{
root=element;
}
currentParent=element;//最新解析的元素
//processFor(element);
elementStack.push(element); //元素入棧 //可以保證 后一個是的parent 是他的前一個
}
function end(tagName) { // 結(jié)束標(biāo)簽
//最后一個元素出棧
let element=elementStack.pop();
let parent=elementStack[elementStack.length-1];
//節(jié)點前后不一致,拋出異常
if(element.tag!==tagName)
{
throw new TypeError(`html tag is error ${tagName}`);
}
if(parent)
{
//子元素的parent 指向
element.parent=parent;
//將子元素添進(jìn)去
parent.children.push(element);
}
}
/**
* 解析到文本
* @param {*} text
*/
function chars(text) { // 文本
//解析到文本
text=text.replace(/\s/g,'');
//將文本加入到當(dāng)前元素
currentParent.children.push({
type:TEXT_NODE,
text
})
}
// 根據(jù) html 解析成樹結(jié)構(gòu) </span></div>
while (html) {
//如果是html 標(biāo)簽
let textEnd = html.indexOf('<');
if (textEnd == 0) {
const startTageMatch = parseStartTag();
if (startTageMatch) {
// 開始標(biāo)簽
start(startTageMatch.tagName,startTageMatch.attrs)
}
const endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1])
}
// 結(jié)束標(biāo)簽
}
// 如果不是0 說明是文本
let text;
if (textEnd > 0) {
text = html.substring(0, textEnd); // 是文本就把文本內(nèi)容進(jìn)行截取
chars(text);
}
if (text) {
advance(text.length); // 刪除文本內(nèi)容
}
}
function advance(n) {
html = html.substring(n);
}
/**
* 解析開始標(biāo)簽
* <div id='app'> ={ tagName:'div',attrs:[{id:app}]}
*/
function parseStartTag() {
const start = html.match(startTagOpen); // 匹配開始標(biāo)簽
if (start) {
const match = {
tagName: start[1], // 匹配到的標(biāo)簽名
attrs: []
}
advance(start[0].length);
let end, attr;
//開始匹配屬性 如果沒有匹配到標(biāo)簽的閉合 并且比配到標(biāo)簽的 屬性
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length);
match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] })
};
//匹配到閉合標(biāo)簽
if (end) {
advance(end[0].length);
return match;
}
}
}
return root;
}
將AST 語法樹轉(zhuǎn)換為代碼
如:return _c("div",{id:"app"},_c("div",{class:"content"},_v("名稱:"+_s(name)),_c("h5",undefined,_v("年齡:"+_s(age)))),_c("p",{style:{"color":"red"}},_v("靜態(tài)節(jié)點")
其中:_c 是創(chuàng)建普通節(jié)點,_v 是創(chuàng)建文本幾點,_s 是待變從數(shù)據(jù)取值(處理模板中{{XXX}})
最后返回的是字符串代碼。
每一個普通節(jié)點都會生成 _c('標(biāo)簽名',{屬性},子(_v文本,_c(普通子節(jié)點)))
由于是樹行結(jié)構(gòu),所以需要遞歸嵌套
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g //匹配 {{}}
/**
* 屬性
* @param {*} attrs
*/
function genProps(attrs){
let str='';
for(let i=0;i<attrs.length;i++)
{
let attr=attrs[i];
//目前暫時處理 style 特殊情況 例如 @click v-model 都得特殊處理
// {
// name:'style',
// value:'color:red;border:1px'
// }
if(attr.name==='style')
{
let obj={};
attr.value.split(';').forEach(element => {
let [key='',value='']= element.split(':');
obj[key]=value;
});
attr.value=obj;
}
str+=`${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0,-1)}}`;
}
function gen(el){
//還是元素節(jié)點
if(el.type==='1')
{
return generate(el);
}
else{
let text=el.text;
if(!text) return;
//一次解析
if(defaultTagRE.test(el.text))
{
defaultTagRE.lastIndex=0
let lastIndex = 0, //上一次的匹配后的索引
index=0,
match=[],
result=[];
while(match=defaultTagRE.exec(text)){
index=match.index;
//先將 bb{{aa}} 中的 bb 添加
result.push(`${JSON.stringify(text.slice(lastIndex,index))}`);
//添加匹配的結(jié)果
result.push(`_s(${match[1].trim()})`);
lastIndex = index + match[0].length;
console.log(lastIndex);
}
//例如:11{{sd}}{{sds}}23 此時 23還未添加
if(lastIndex<text.length)
{
//result.push(`_v(${JSON.stringify(text.slice(lastIndex))})`);
result.push(JSON.stringify(text.slice(lastIndex)));
}
console.log(result);
//返回
return `_v(${result.join('+')})`
}
//沒有變量
else{
return `_v(${JSON.stringify(text)})`
}
}
}
//三部分 標(biāo)簽,屬性,子
export function generate(el){
let children = genChildren(el); // 生成孩子字符串
let result = `_c("${el.tag}",${
el.attrs.length? `${genProps(el.attrs)}` : undefined
}${
children? `,${children}` :undefined
})`;
return result;
}
生成render 函數(shù)
let astStr=generate(ast);
let renderFnStr = `with(this){ \r\nreturn ${astStr} \r\n}`;
let render=new Function(renderFnStr);
return render;
DOM 渲染
基本流程
- 調(diào)用render 函數(shù)生成虛擬dom
- 首次生成真實dom
- 更新dom,通過diff算法實現(xiàn)對dom的更新。(后面整理總結(jié))
生成虛擬DOM
- 在生成render 函數(shù)中有_c(創(chuàng)建普通節(jié)點),_v(創(chuàng)建文本節(jié)點),_s(處理{{xxx}})等方法,這需要在render.js 實現(xiàn)。所有方法都掛載到Vue 的原型上。
// 代碼位置 render.js
import {createElement,createNodeText} from './vdom/create-element.js'
export function renderMixin(Vue){
//創(chuàng)建節(jié)點
Vue.prototype._c=function(){
return createElement(...arguments);
}
//創(chuàng)建文本節(jié)點
Vue.prototype._v=function(text){
return createNodeText(text);
}
Vue.prototype._s=function(val){
return val===null?"":(typeof val==='object'?JSON.stringify(val):val);
}
// 生成虛擬節(jié)點的方法
Vue.prototype._render=function(){
const vm=this;
//這就是上一部分生成的 render 函數(shù)
const {render}=vm.$options;
//執(zhí)行
let node=render.call(vm);
console.log(node);
return node;
}
}
// 代碼位置 vom/create-element.js
/**
* 創(chuàng)建節(jié)點
* @param {*} param0
*/
export function createElement(tag,data={},...children){
return vNode(tag,data,data.key,children,undefined);
}
/**
* 文本節(jié)點
* @param {*} text
*/
export function createNodeText(text){
console.log(text);
return vNode(undefined,undefined,undefined,undefined,text)
}
/**
* 虛擬節(jié)點
*/
function vNode(tag,data,key,children,text){
return {
tag,
data,
key,
children,
text
}
}
-
數(shù)據(jù)代理
我們發(fā)現(xiàn)在 生成的render 函數(shù)中有
with(this){todo XXX}with 語句的原本用意是為逐級的對象訪問提供命名空間式的速寫方式. 也就是在指定的代碼區(qū)域, 直接通過節(jié)點名稱調(diào)用對象。
在 with中的 this也就是 Vue的實例vm。但是上一節(jié)中我們得到的響應(yīng)式數(shù)據(jù)都在vm._data 中,所以我們需要實現(xiàn) vm.age可以取得 vm._data.age,所以需要代理。
實現(xiàn)代理有兩種方案-
Object.defineProperty(源碼采用) __defineGetter__ 和 __defineSetter__
// state.js 中 function initData(vm){ const options=vm.$options; if(options.data) { // 如果 data 是函數(shù)得到函數(shù)執(zhí)行的返回值 let data=typeof options.data==='function'?(options.data).call(vm):options.data; vm._data=data; for(let key in data) { proxy(vm,'_data',key) } observe(data) } } // 代理 function proxy(target,source,key){ Object.defineProperty(target,key,{ get(){ return target[source][key] }, set(newValue){ target[source][key]=newValue; } }) }真實dom的生成
patch.js
/** * 創(chuàng)建元素 * @param {*} vnode */ function createElement(vnode){ let {tag,data,key,children,text}=vnode; if(typeof tag==='string') { vnode.el=document.createElement(tag); updateProps(vnode); children.forEach(child => { if(child instanceof Array) { child.forEach(item=>{ vnode.el.appendChild(createElement(item)); }) } else{ vnode.el.appendChild(createElement(child)); } }); } else{ vnode.el=document.createTextNode(text); } return vnode.el; } /** * jiu * @param {*} vnode * @param {*} oldNode */ function updateProps(vnode,oldProps={}){ let {el,data}=vnode; for(let key in oldProps) { //舊有新無 刪除 if(!data[key]) { el.removeAttribute(key); } } el.style={}; for(let key in data) { if(key==='style') { for(let styleName in data[key]) { el.style[styleName]=data[key][styleName]; } } else{ el.setAttribute(key,data[key]); } } } -