從零實(shí)現(xiàn)一個(gè)vue文件解析器
如何從 0 處理一個(gè) vue 文件并實(shí)現(xiàn)簡(jiǎ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)簡(jiǎn)單的響應(yīng)式。
源碼地址 gezhicui/vue-webpack
webpack 部分
首先進(jìn)行 webpack 打包,把.vue 文件通過 vue-loader 處理。
實(shí)現(xiàn)一個(gè)簡(jiǎn)易的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)行處理,掛載到頁(yè)面上
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è)簡(jiǎn)單的響應(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)語(yǔ)法進(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)語(yǔ)法,如果有的話存入節(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)語(yǔ)法
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)語(yǔ)法,我們還需要處理@click這樣的事件語(yǔ)法,首先,我們創(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 頁(yè)面
執(zhí)行完上面的內(nèi)容,我們就到了最后一步 render 頁(yè)面了,我們只要把節(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頁(yè)面元素,這時(shí)候就也會(huì)用到render函數(shù),那就簡(jiǎn)單實(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)簡(jiǎn)單的響應(yīng)式處理了!!
到此這篇關(guān)于實(shí)現(xiàn)一個(gè)vue文件解析器的文章就介紹到這了,更多相關(guān)vue文件解析器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue元素樣式實(shí)現(xiàn)動(dòng)態(tài)改變方法介紹
vue通過js動(dòng)態(tài)修改元素的樣式,如果是固定的幾個(gè)樣式,我常用的是綁定元素的calss,給不同的class寫好需要的樣式,js控制是否使用這個(gè)class2022-09-09
Vue 實(shí)現(xiàn)點(diǎn)擊空白處隱藏某節(jié)點(diǎn)的三種方式(指令、普通、遮罩)
最近小編接到這樣的需求:彈出框(或Popover)在 show 后,點(diǎn)擊空白處可以將其 hide。針對(duì)這個(gè)需求,小編整理了三種實(shí)現(xiàn)方式,如果大家對(duì)vue 點(diǎn)擊空白隱藏節(jié)點(diǎn)問題感興趣的朋友跟隨小編一起看看吧2019-10-10
Vue實(shí)現(xiàn)Echarts圖表寬高自適應(yīng)的實(shí)踐
本文主要介紹了Vue實(shí)現(xiàn)Echarts圖表寬高自適應(yīng)的實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11
Vue.js仿Metronic高級(jí)表格(一)靜態(tài)設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了Vue.js仿Metronic高級(jí)表格的靜態(tài)設(shè)計(jì),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04
vue中使用iview自定義驗(yàn)證關(guān)鍵詞輸入框問題及解決方法
這篇文章主要介紹了vue中使用iview自定義驗(yàn)證關(guān)鍵詞輸入框問題及解決方法,本文通過實(shí)例結(jié)合代碼的形式給大家介紹解決方法,需要的朋友可以參考下2018-03-03
基于vue cli重構(gòu)多頁(yè)面腳手架過程詳解
本文分步驟給大家介紹了基于vue cli重構(gòu)多頁(yè)面腳手架過程,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下2018-01-01

