從零實(shí)現(xiàn)一個(gè)vue文件解析器
如何從 0 處理一個(gè) vue 文件并實(shí)現(xiàn)簡(jiǎn)單的響應(yīng)式?
在現(xiàn)在的前端工程化中,打包工具是不可或缺的,其中webpack
無(wú)疑是占據(jù)了主導(dǎo)地位,當(dāng)然也有尤大搞的vite
,但是論生態(tài)和使用人數(shù),至少在目前webpack
還是更勝一籌。
打包工具能幫助我們打包前端文件,在webpack
中,不同后綴的文件通過(guò)不同loader
來(lái)處理。
本文就討論下怎么實(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
文件通過(guò) vue-loader
處理。
實(shí)現(xiàn)一個(gè)簡(jiǎn)易的vue-loader
,通過(guò)一系列正則,最終一個(gè).vue
文件的內(nèi)容會(huì)被包裝到一個(gè)對(duì)象中
比方說(shuō)我現(xiàn)在的.vue 文件寫(xiě)了下面這些內(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)過(guò) 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í)候,我們就能通過(guò)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)的掛載
接下來(lái)就一步步實(shí)現(xiàn)createApp
,首先,我們先來(lái)定義一個(gè) vm,一會(huì)兒所有的屬性都可以放在 vm 上,同時(shí)把vue-loader
解析過(guò)的文件對(duì)象中的內(nèi)容給解構(gòu)出來(lái)
function createApp(component) { const vm = {}; const { template, methods, data } = component; }
template 解析
在上面經(jīng)過(guò)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 接下來(lái)就是對(duì) js 的操作
data 響應(yīng)式處理
vue 的核心就在于響應(yīng)式,vue2 通過(guò)Object.defineProperty
實(shí)現(xiàn)響應(yīng)式,我們來(lái)實(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)我放到后面再說(shuō) update(vm, key); }, }); }
這樣,我們就監(jiān)聽(tīng)了data
中每個(gè)屬性的get
和set
,實(shí)現(xiàn)了數(shù)據(jù)的響應(yīng)式處理
初始化數(shù)據(jù)池
在上面的 template 解析中,我們已經(jīng)拿到了template
轉(zhuǎn)換過(guò)后的節(jié)點(diǎn),但是有個(gè)問(wèn)題,節(jié)點(diǎn)的內(nèi)容沒(méi)有經(jīng)過(guò)任何處理,如{{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ǔ)法,如果有的話(huà)存入節(jié)點(diǎn)數(shù)據(jù)池中
const allNodes = $node.querySelectorAll('*'); allNodes.forEach((node) => { // 這里獲取到的textContent是原原始的沒(méi)經(jīng)過(guò)任何處理的節(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) : //無(wú)參函數(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ù)池和事件池,接下來(lái)我們就要拿節(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:事件類(lèi)型 handler:事件處理函數(shù) let { type, handler } = info; //在vue中,是用this.function 去訪(fǎng)問(wèn)方法,所以方法要被綁定到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è)問(wèn)題
- with 是干啥用的?
- 為什么_render 要抽離出來(lái)?
首先先來(lái)介紹下 with
with 的作用是用來(lái)改變標(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è)完整的不通過(guò)任何第三方插件解析 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)文章
Element Carousel 走馬燈的具體實(shí)現(xiàn)
這篇文章主要介紹了Element Carousel 走馬燈的具體實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07Vue Cli 3項(xiàng)目使用融云IM實(shí)現(xiàn)聊天功能的方法
這篇文章主要介紹了Vue Cli 3項(xiàng)目 使用融云IM實(shí)現(xiàn)聊天功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-04-04vue項(xiàng)目base64轉(zhuǎn)img方式
這篇文章主要介紹了vue項(xiàng)目base64轉(zhuǎn)img方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04vue2項(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ù),本文通過(guò)一些示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-12-12vue.js實(shí)現(xiàn)圖書(shū)管理功能
這篇文章主要為大家詳細(xì)介紹了vue.js實(shí)現(xiàn)圖書(shū)管理功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09VUE3使用Element-Plus時(shí)如何修改ElMessage中的文字大小
在使用Element-plus的Elmessage時(shí)使用默認(rèn)的size無(wú)法滿(mǎn)足我們的需求時(shí),我們可以自定義字體的大小,但是直接重寫(xiě)樣式后,并沒(méi)有起作用,甚至使用::v-deep深度選擇器也沒(méi)有效果,本文介紹VUE3使用Element-Plus時(shí)如何修改ElMessage中的文字大小,感興趣的朋友一起看看吧2023-09-09