Vue.js設計與實現之權衡的藝術
1.寫在前面
本文便帶領大家進入《Vue.js設計與實現》描述的宇宙,開啟探索框架設計的思想的旅程。
2.框架設計里到處都體現了權衡的藝術
作者在文章中寫到『框架設計里到處都體現了權衡的藝術』,的確在進行設計模式和技術選型的時候,我們都會去綜合考慮性能和開發效率,去權衡各方面因素從而得到盡可能完善的框架。
框架是由各個模塊組成的,彼此關聯又相互獨立,要做到實現當前的功能,又要考慮到后續的模塊拆分和拓展。作為框架的設計者,需要站在全局的角度去思考和設計,需要對整體的設計思路有著清晰的掌控。實現細節是在設計的時候不用太過于在意的視點,不要囿于高山的霧層,畢竟它只是整個框架的冰山一角。
在Vue框架的設計中,最能體現這種權衡思想的可能是『命令式和聲明式』、『編譯時和運行時』等之間的權衡,需要了解彼此的差異、汲取兩者的優點。
3.命令式和聲明式
正如你所知道的,在計算機編程范式中有三種:命令式編程,聲明式編程和函數式編程。
命令式編程:是關注計算機執行的步驟,即一步一步告訴計算機先做什么再做什么。
聲明式編程:以數據結構的形式來表達程序執行的邏輯。它的主要思想是告訴計算機應該做什么,但不指定具體要怎么做。
函數式編程:是與聲明式編程關聯的,只關注做什么而不是怎么做。但函數式編程不僅僅局限于聲明式編程。
命令式
對于前端開發從業者而言,JQuery框架并不會陌生,它其實就是最經典的命令式的框架設計,它關注計算機執行的步驟,即關注過程。命令式編程其實就是寫給計算機看的,讓我們的自然語言能與代碼進行一一對應,更符合我們做事邏輯。
$("#app")//獲取id為app的標簽元素
.text("hello pingping")//設置標簽的文本內容
.on("click",()=>console.log("hello onechuan"));//給id為app的便簽綁定事件
等價于原生js的代碼:
const div = document.querySelector("#app");
div.innerText = "hello pingping";
div.addEventListener("click",()=>console.log("hello onechuan"))
聲明式
而聲明式更關注實現結果,具體的實現過程并不是使用者所在意的,這也很大程度地降低了認知成本,關注表層邏輯提升使用效率。
事實上,Vue.js的設計并不是簡單使用純粹的命令式或是聲明式編程,而是結合兩者的優點。在內部實現使用命令式告知計算機如何運行,對外暴露的API等則是采用的聲明式編程,能夠用人話讓使用者讀懂結果。
<div @click="()=>console.log('hello onechuan')">hello pingping<div>
性能和可維護性
在《編譯原理》書中,了解到命令式代碼的性能優于聲明式代碼,這是因為聲明式代碼需要經過編譯成計算機能夠讀懂的命令式代碼。但是呢,聲明式代碼更像是人類能夠讀懂的人話,在盡可能犧牲少量性能的同時降低代碼的維護成本。
Vue.js框架就是結合兩者的優點,對命令式代碼進行了封裝,對使用者提供可維護性更高的聲明式代碼。
4.真實DOM和虛擬DOM
對于聲明式代碼的更新性能消耗而言:聲明式代碼的更新性能消耗 = 找出差異的性能消耗 + 直接修改的性能消耗,如果我們找到能夠讓找出差異的性能消耗最小化的算法,那么就能夠將聲明式代碼的性能消耗無限趨近于命令式代碼性能消耗。
我們分別從創建頁面和更新頁面兩方面,對真實DOM和虛擬DOM操作的性能消耗進行分析:
狀態 | 虛擬DOM(純JS創建VNODE) | 真實DOM(渲染HTML字符串) |
創建頁面 | 新建所有的DOM對象 | 新建所有的DOM對象 |
更新頁面 | 必要的DOM更新 | -銷毀所有的舊DOM,新建所有的新DOM |
關于性能:真實DOM<虛擬DOM<原生JS。
此處簡要的進行總結,后續文章將會有更詳細的數據分析。
5.編譯時和運行時
在框架設計時還要考慮是選擇:純運行時、純編譯時還是運行時+編譯時,這需要結合你所期望的待設計框架的特征做出合適的決策。
運行時
所謂運行時,就是計算機所運行時的代碼,不需要經歷額外的處理,便能夠實現我們所期許的結果。
例如,我們需要將提供的樹形結構的數據對象,渲染到渲染成dom樹,那么我們需要設計一個Render函數直接進行渲染,這樣就能得到我們想要的結果:
const obj = {
tag:"div",
children:[{
tag:"span",
children:"hello world"
}]
}
Render(obj, document.body)
function Render(obj, root){
const el = document.createElement(obj.tag);
if(typeof obj.children === "string"){
const text = document.createTextNode(obj.children);
el.appendChild(text)
}else if(obj.children){
// 如果是數組,就進行遞歸調用render,使用el作為root參數
obj.children.forEach(child=>Render(child, el))
}
// 最后將元素添加到根元素
root.appendChild(el)
}
瀏覽器顯示如下:
編譯時
那么,編譯就是一種轉換技術,將高級語言轉換低級語言,Vue.js將HTML標簽通過編譯轉換成樹形結構的數據對象。
這樣我們需要編寫一個Compiler函數,用于將HTML標簽通過編譯換成樹形結構的數據對象。如下:
const html = `
<div>
<span>hello world</span>
</div>`
const obj = compoler(html)
Render(obj, document.body)
這樣就能將:
<div>
<span>hello pingping</span>
</div>
編譯成:
const obj = {
tag: 'div',
children: [
{tag: 'span', children: 'hello world'}
]
}
結合Render函數進行渲染,這樣我們就初步設計了一個運行時+編譯時的框架了。
運行時+編譯時
所謂在Vue.js是運行時+編譯時框架,其實指的是:
支持運行時:使用者可以直接提供樹形結構的數據對象而無需編譯;
支持編譯時:使用者可以提供HTML字符串,將其編譯成樹形結構的數據對象后再交給運行時處理。
為什么Vue.js要設計成運行時+編譯時框架?
這所以這樣設計也是開源團隊進行權衡的結果,運行時無法分析用戶提供的內容,而加入編譯后就可以對用戶內容進行分析和編譯。在編譯的時候提取這些用戶內容的信息,再通過Render函數進行渲染。
當然,將框架設計成純編譯時,可以分析用戶內容直接編譯成可執行的JS代碼,在保證性能的同時犧牲了框架的靈活性和可維護性,對用戶而言必須對內容編譯后才能使用。
對此,Vue.js的設計是綜合考量,才用的運行時+編譯時的框架設計,在保留運行時的靈活性的同時,盡可能不犧牲性能。
6.寫在最后
在本文中,了解到開源團隊對于命令式和聲明式、真實DOM和虛擬DOM、運行時和編譯時的權衡選擇,在盡可能減少性能損耗的同時提供最好的用戶體驗和可維護性、靈活性。