從零實(shí)現(xiàn)一個(gè)vue文件解析器
如何從 0 處理一個(gè) vue 文件并實(shí)現(xiàn)簡單的響應(yīng)式?
在現(xiàn)在的前端工程化中,打包工具是不可或缺的,其中webpack無疑是占據(jù)了主導(dǎo)地位,當(dāng)然也有尤大搞的vite,但是論生態(tài)和使用人數(shù),至少在目前webpack還是更勝一籌。
打包工具能幫助我們打包前端文件,在webpack中,不同后綴的文件通過不同loader來處理。
本文就討論下怎么實(shí)現(xiàn)一個(gè)處理.vue文件的loader,以及用loader處理完.vue文件怎么把內(nèi)容渲染在瀏覽器上,并實(shí)現(xiàn)簡單的響應(yīng)式。
源碼地址 gezhicui/vue-webpack
webpack 部分
首先進(jìn)行 webpack 打包,把.vue 文件通過 vue-loader 處理。
實(shí)現(xiàn)一個(gè)簡易的vue-loader,通過一系列正則,最終一個(gè).vue 文件的內(nèi)容會(huì)被包裝到一個(gè)對(duì)象中
比方說我現(xiàn)在的.vue 文件寫了下面這些內(nèi)容:
<template>
<div>
<h2>{{ count + 1 }}</h2>
<button @click="plus(1)">+</button>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
count: 0
}
},
methods: {
plus (num) {
this.count += num;
}
}
}
</script>那么經(jīng)過 vue-loader 處理,就會(huì)變成一個(gè)對(duì)象:
{
template:
`<div>
<h2>{{ count + 1 }}</h2>
<button @click="plus(1)">+</button>
</div>`,
name: 'App',
data() {
return { count: 0 }
},
methods: {
plus(num) { this.count += num; },
}
}那么,在瀏覽器執(zhí)行這個(gè)文件的時(shí)候,我們就能通過createApp方法,把這個(gè)對(duì)象使用 createApp 進(jìn)行處理,掛載到頁面上
createApp 實(shí)現(xiàn)部分
在 vue 的main.js文件中,我們通常會(huì)把根組件傳遞給createApp作為入?yún)?,?
import App from './App';
import { createApp } from '../modules/vue';
createApp(App).mount('#app');那我們實(shí)現(xiàn)的重點(diǎn)就在于createApp對(duì)vue 組件的處理,以及在createApp的返回內(nèi)容(就是 vm)中添加mount方法,實(shí)現(xiàn)處理完的節(jié)點(diǎn)的掛載
接下來就一步步實(shí)現(xiàn)createApp,首先,我們先來定義一個(gè) vm,一會(huì)兒所有的屬性都可以放在 vm 上,同時(shí)把vue-loader解析過的文件對(duì)象中的內(nèi)容給解構(gòu)出來
function createApp(component) {
const vm = {};
const { template, methods, data } = component;
}template 解析
在上面經(jīng)過vur-loader處理后,template以字符串形式被放到對(duì)象中,所以我們可以拿到 dom 元素字符串,把他轉(zhuǎn)成 dom 元素
/*
template:
`<div>
<h2>{{ count + 1 }}</h2>
<button @click="plus(1)">+</button>
</div>`,
*/
vm.$node = createNode(template);
function createNode(template) {
const _tempNode = document.createElement('div');
_tempNode.innerHTML = template;
return getFirstChildNode(_tempNode);
}這樣,我們就拿到了 html 接下來就是對(duì) js 的操作
data 響應(yīng)式處理
vue 的核心就在于響應(yīng)式,vue2 通過Object.defineProperty實(shí)現(xiàn)響應(yīng)式,我們來實(shí)現(xiàn)個(gè)簡單的響應(yīng)式處理
首先拿到data,為了創(chuàng)建多個(gè)組件時(shí)data不被互相影響,所以data是一個(gè)函數(shù)
vm.$data = data();
for (let key in vm.$data) {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(newValue) {
vm.$data[key] = newValue;
// update觸發(fā)節(jié)點(diǎn)更新,至于實(shí)現(xiàn)我放到后面再說
update(vm, key);
},
});
}這樣,我們就監(jiān)聽了data中每個(gè)屬性的get和set,實(shí)現(xiàn)了數(shù)據(jù)的響應(yīng)式處理
初始化數(shù)據(jù)池
在上面的 template 解析中,我們已經(jīng)拿到了template轉(zhuǎn)換過后的節(jié)點(diǎn),但是有個(gè)問題,節(jié)點(diǎn)的內(nèi)容沒有經(jīng)過任何處理,如{{count + 1}}會(huì)原封不動(dòng)的展示在瀏覽器中,我們希望的是最終展示的是 count 這個(gè)變量+1 的結(jié)果,所以我們需要對(duì)雙括號(hào)語法進(jìn)行解析
我們先定義一個(gè)正則表達(dá)式,匹配{{}}中的內(nèi)容,以及定義一個(gè)節(jié)點(diǎn)數(shù)據(jù)池
// 節(jié)點(diǎn)數(shù)據(jù)池
const exprPool = new Map();
// 正則獲取雙括號(hào)中內(nèi)容
const regExpr = /\{\{(.+?)\}\}/;然后,從我們剛剛定義的vm.$node中拿到所有節(jié)點(diǎn),并查看該節(jié)點(diǎn)是否有雙括號(hào)語法,如果有的話存入節(jié)點(diǎn)數(shù)據(jù)池中
const allNodes = $node.querySelectorAll('*');
allNodes.forEach((node) => {
// 這里獲取到的textContent是原原始的沒經(jīng)過任何處理的節(jié)點(diǎn)內(nèi)容,如{{count + 1}}
const vExpression = node.textContent;
/* exprMatched:{
0: "{{ count + 1 }}"
1: " count + 1 "
groups: undefined
index: 0
input: "{{ count + 1 }}"
}
*/
const exprMatched = vExpression.match(regExpr);
// 如果有雙括號(hào)語法
if (exprMatched) {
const poolInfo = checkExpressionHasData($data, exprMatched[1].trim());
// 把節(jié)點(diǎn)存入節(jié)點(diǎn)數(shù)據(jù)池
poolInfo && exprPool.set(node, poolInfo);
}
});
function checkExpressionHasData(data, expression) {
for (let key in data) {
if (expression.includes(key) && expression !== key) {
// count + 1,返回{key:count,expression:count+1}
return {
key,
expression,
};
} else if (expression === key) {
// count,返回{key:count,expression:count}
return {
key,
expression: key,
};
} else {
return null;
}
}
}
初始化事件池
處理完雙括號(hào)語法,我們還需要處理@click這樣的事件語法,首先,我們創(chuàng)建一個(gè)事件池,再定義兩個(gè)正則分別匹配函數(shù)
const eventPool = new Map(); // 匹配函數(shù)名 const regStringFn = /(.+?)\((.+?)\)/; // 匹配函數(shù)參數(shù) const regString = /\'(.+?)\'/;
同樣的,我們也需要遍歷所有節(jié)點(diǎn)
const allNodes = $node.querySelectorAll('*');
allNodes.forEach((node) => {
const vClickVal = node.getAttribute(`@click`);
if (vClickVal) {
/*
比如@click='plus(1)',解析完成的fnInfo就是
fnInfo:{
args: [1]
methodName: "plus"
}
*/
const fnInfo = checkFunctionHasArgs(vClickVal);
const handler = fnInfo
? //有參函數(shù)傳入args
methods[fnInfo.methodName].bind(vm, ...fnInfo.args)
: //無參函數(shù)直接綁定
methods[vClickVal].bind(vm);
//存入事件池,節(jié)點(diǎn)為key,事件為value
eventPool.set(node, {
type: vClick,
handler,
});
//刪除dom上的attr,不然瀏覽器查看源代碼就會(huì)顯示自定義事件 這樣不好
node.removeAttribute(`@${vClick}`);
}
});
function checkFunctionHasArgs(str) {
const matched = str.match(regStringFn);
if (matched) {
const argArr = matched[2].split(',');
const args = checkIsString(matched[2])
? argArr // ['1']
: argArr.map((item) => Number(item));
return {
methodName: matched[1],
args,
};
}
}
function checkIsString(str) {
return str.match(regString);
}
這樣,我們有擁有了節(jié)點(diǎn)數(shù)據(jù)池和事件池,接下來我們就要拿節(jié)點(diǎn)數(shù)據(jù)池和事件池做操作了
綁定事件處理
有了事件池,我們就要把事件池中的事件綁定到 dom 元素上去,讓事件能夠觸發(fā)。這步其實(shí)是很容易的,因?yàn)槲覀儼?vue 事件加入事件池中時(shí),key 是 dom 元素,value 是事件處理函數(shù),只要把他們兩個(gè)互相綁定就行
function (vm) {
//node:key info:value
for (let [node, info] of eventPool) {
// type:事件類型 handler:事件處理函數(shù)
let { type, handler } = info;
//在vue中,是用this.function 去訪問方法,所以方法要被綁定到vm上
vm[handler.name] = handler;
//給節(jié)點(diǎn)綁定事件處理函數(shù)
node.addEventListener(type, vm[handler.name], false);
}
}render 頁面
執(zhí)行完上面的內(nèi)容,我們就到了最后一步 render 頁面了,我們只要把節(jié)點(diǎn)數(shù)據(jù)池中的節(jié)點(diǎn)內(nèi)容渲染到瀏覽器上
function render(vm) {
exprPool.forEach((info, node) => {
_render(vm, node, info);
});
}
function _render(vm, node, info) {
//info:{key: 'count',expression 'count + 1'}
const { expression } = info;
//expression是一個(gè)字符串,為了執(zhí)行字符串,所以我們需要new Function
const r = new Function(
'vm',
'node',
`
with (vm) {
node.textContent = ${expression};
}
`
);
r(vm, node);
}在這里,我們先解決兩個(gè)問題
- with 是干啥用的?
- 為什么_render 要抽離出來?
首先先來介紹下 with
with 的作用是用來改變標(biāo)識(shí)符的查找優(yōu)先級(jí),優(yōu)先從 with 指定對(duì)象的屬性中查找。e.g:
var a = 1;
var obj = {
a: 2,
};
with (obj) {
console.log(a); //2
}那為什么_render 要單獨(dú)抽成一個(gè)函數(shù)? 因?yàn)樵谇懊娴?data 響應(yīng)式處理 中,set被觸發(fā)時(shí),我們需要拿到新的數(shù)據(jù)值去update頁面元素,這時(shí)候就也會(huì)用到render函數(shù),那就簡單實(shí)現(xiàn)下上面提到的updata
export function update(vm, key) {
//在節(jié)點(diǎn)數(shù)據(jù)池中查找哪個(gè)節(jié)點(diǎn)的key==當(dāng)前改變的key,找到則重新render
exprPool.forEach((info, node) => {
if (info.key === key) {
_render(vm, node, info);
}
});
}到此為止,就能實(shí)現(xiàn)一個(gè)完整的不通過任何第三方插件解析 vue 文件,并實(shí)現(xiàn)簡單的響應(yīng)式處理了?。?/p>
到此這篇關(guān)于實(shí)現(xiàn)一個(gè)vue文件解析器的文章就介紹到這了,更多相關(guān)vue文件解析器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Element Carousel 走馬燈的具體實(shí)現(xiàn)
這篇文章主要介紹了Element Carousel 走馬燈的具體實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
Vue Cli 3項(xiàng)目使用融云IM實(shí)現(xiàn)聊天功能的方法
這篇文章主要介紹了Vue Cli 3項(xiàng)目 使用融云IM實(shí)現(xiàn)聊天功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-04-04
vue項(xiàng)目base64轉(zhuǎn)img方式
這篇文章主要介紹了vue項(xiàng)目base64轉(zhuǎn)img方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
vue2項(xiàng)目增加eslint配置代碼規(guī)范示例
這篇文章主要為大家介紹了vue2項(xiàng)目增加eslint配置代碼規(guī)范示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
在Vue中延遲執(zhí)行某個(gè)函數(shù)的實(shí)現(xiàn)方式
在Vue中延遲執(zhí)行某個(gè)函數(shù),你可以使用setTimeout()函數(shù)或者Vue提供的生命周期鉤子函數(shù),本文通過一些示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-12-12
VUE3使用Element-Plus時(shí)如何修改ElMessage中的文字大小
在使用Element-plus的Elmessage時(shí)使用默認(rèn)的size無法滿足我們的需求時(shí),我們可以自定義字體的大小,但是直接重寫樣式后,并沒有起作用,甚至使用::v-deep深度選擇器也沒有效果,本文介紹VUE3使用Element-Plus時(shí)如何修改ElMessage中的文字大小,感興趣的朋友一起看看吧2023-09-09

