上一節總結了Vue的響應式數據原理,下面總結一下Vue中模板編譯。模板編譯情景衆多,複雜多變,如今只學習了普通標籤的解析,編譯,未能對組件,指令,事件等多種狀況進行深刻學習總結。html
解析模板代碼生成AST語法樹,主要依賴正則。node
將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("靜態節點")))
}
複製代碼
(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("靜態節點")))
}
})
複製代碼
代碼位置 complier 中的 parser.js
複製代碼
主要依賴正則解析(我正則很渣,看懂都很難,之後再深刻學習吧,直接照搬珠峯架構姜文老師)數組
實現步驟bash
先解析開始標籤 如<div id='app'> ={ tagName:'div',attrs:[{id:app}]}
架構
方法:parseStartTag [1:< 2:div 3:id='app' 4:>] 四個部分 獲得 tag,attr 而後進入 start 方法,建立ast節點。app
解析子節點標籤(遞歸)dom
解析到結束標籤 注意:解析玩開始節點後將節點入棧,解析到結束節點後而後將開始節點出棧,此時棧的最後一點就是當前節點的父節點。函數
例如: [div,p]
解析到 </p>
此時出棧[div]
獲得p
,取棧尾 將p
插入到div
的子節點。學習
import {extend} from '../util/index.js'
// 字母a-zA-Z_ - . 數組小寫字母 大寫字母
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 標籤名
// ?:匹配不捕獲 <aaa:aaa>
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// startTagOpen 能夠匹配到開始標籤 正則捕獲到的內容是 (標籤名)
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 標籤開頭的正則 捕獲的內容是標籤名
// 閉合標籤 </xxxxxxx>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配標籤結尾的 </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*(\/?)>/; // 匹配標籤結束的 >
// 匹配動態變量的 +? 儘量少匹配 {{}}
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, //標籤
attrs, //屬性
children:[], //子節點
attrsMap: makeAttrsMap(attrs),
parent:null, //父節點
type:ELEMENT_NDOE //節點類型
}
}
// console.log(html)
function start(tagName, attrs) {
//建立跟節點
let element=createASTElement(tagName,attrs);
if(!root)
{
root=element;
}
currentParent=element;//最新解析的元素
//processFor(element);
elementStack.push(element); //元素入棧 //能夠保證 後一個是的parent 是他的前一個
}
function end(tagName) { // 結束標籤
//最後一個元素出棧
let element=elementStack.pop();
let parent=elementStack[elementStack.length-1];
//節點先後不一致,拋出異常
if(element.tag!==tagName)
{
throw new TypeError(`html tag is error ${tagName}`);
}
if(parent)
{
//子元素的parent 指向
element.parent=parent;
//將子元素添進去
parent.children.push(element);
}
}
/**
* 解析到文本
* @param {*} text
*/
function chars(text) { // 文本
//解析到文本
text=text.replace(/\s/g,'');
//將文本加入到當前元素
currentParent.children.push({
type:TEXT_NODE,
text
})
}
// 根據 html 解析成樹結構 </span></div>
while (html) {
//若是是html 標籤
let textEnd = html.indexOf('<');
if (textEnd == 0) {
const startTageMatch = parseStartTag();
if (startTageMatch) {
// 開始標籤
start(startTageMatch.tagName,startTageMatch.attrs)
}
const endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1])
}
// 結束標籤
}
// 若是不是0 說明是文本
let text;
if (textEnd > 0) {
text = html.substring(0, textEnd); // 是文本就把文本內容進行截取
chars(text);
}
if (text) {
advance(text.length); // 刪除文本內容
}
}
function advance(n) {
html = html.substring(n);
}
/**
* 解析開始標籤
* <div id='app'> ={ tagName:'div',attrs:[{id:app}]}
*/
function parseStartTag() {
const start = html.match(startTagOpen); // 匹配開始標籤
if (start) {
const match = {
tagName: start[1], // 匹配到的標籤名
attrs: []
}
advance(start[0].length);
let end, attr;
//開始匹配屬性 若是沒有匹配到標籤的閉合 而且比配到標籤的 屬性
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] })
};
//匹配到閉合標籤
if (end) {
advance(end[0].length);
return match;
}
}
}
return root;
}
複製代碼
如:return _c("div",{id:"app"},_c("div",{class:"content"},_v("名稱:"+_s(name)),_c("h5",undefined,_v("年齡:"+_s(age)))),_c("p",{style:{"color":"red"}},_v("靜態節點")
其中:_c 是建立普通節點,_v 是建立文本幾點,_s 是待變從數據取值(處理模板中{{XXX}})
最後返回的是字符串代碼。
每個普通節點都會生成 _c('標籤名',{屬性},子(_v文本,_c(普通子節點))) 因爲是樹行結構,因此須要遞歸嵌套
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){
//仍是元素節點
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))}`);
//添加匹配的結果
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)})`
}
}
}
//三部分 標籤,屬性,子
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;
}
複製代碼
let astStr=generate(ast);
let renderFnStr = `with(this){ \r\nreturn ${astStr} \r\n}`;
let render=new Function(renderFnStr);
return render;
複製代碼
基本流程
// 代碼位置 render.js
import {createElement,createNodeText} from './vdom/create-element.js'
export function renderMixin(Vue){
//建立節點
Vue.prototype._c=function(){
return createElement(...arguments);
}
//建立文本節點
Vue.prototype._v=function(text){
return createNodeText(text);
}
Vue.prototype._s=function(val){
return val===null?"":(typeof val==='object'?JSON.stringify(val):val);
}
// 生成虛擬節點的方法
Vue.prototype._render=function(){
const vm=this;
//這就是上一部分生成的 render 函數
const {render}=vm.$options;
//執行
let node=render.call(vm);
console.log(node);
return node;
}
}
// 代碼位置 vom/create-element.js
/**
* 建立節點
* @param {*} param0
*/
export function createElement(tag,data={},...children){
return vNode(tag,data,data.key,children,undefined);
}
/**
* 文本節點
* @param {*} text
*/
export function createNodeText(text){
console.log(text);
return vNode(undefined,undefined,undefined,undefined,text)
}
/**
* 虛擬節點
*/
function vNode(tag,data,key,children,text){
return {
tag,
data,
key,
children,
text
}
}
複製代碼
數據代理
咱們發如今 生成的render 函數中有with(this){todo XXX}
with 語句的本來用意是爲逐級的對象訪問提供命名空間式的速寫方式. 也就是在指定的代碼區域, 直接經過節點名稱調用對象。 在 with中的 this也就是 Vue的實例vm。可是上一節中咱們獲得的響應式數據都在vm._data 中,因此咱們須要實現 vm.age能夠取得 vm._data.age,因此須要代理。 實現代理有兩種方案
Object.defineProperty
(源碼採用)__defineGetter__ 和 __defineSetter__
// state.js 中
function initData(vm){
const options=vm.$options;
if(options.data)
{
// 若是 data 是函數獲得函數執行的返回值
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;
}
})
}
複製代碼
patch.js
/**
* 創建元素
* @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]);
}
}
}
複製代碼