內(nèi)心深處對(duì)游戲的熱愛(ài),讓我一直渴望能自己制作一些電子游戲。幾個(gè)月前我開(kāi)始將這種夢(mèng)想變?yōu)楝F(xiàn)實(shí),并第一次參加了全球游戲大賽(Global Game Jam)。我和我的團(tuán)隊(duì)使用 Vue.js 構(gòu)建了一個(gè)名為“ ZeroDaysLeft ”的游戲,其形式是 Web 端的單頁(yè)面應(yīng)用程序。這款游戲的主題是環(huán)境保護(hù),我們考慮到商業(yè)活動(dòng)對(duì)地球環(huán)境的影響,希望就這個(gè)話(huà)題做一些有益的探討。使用 Vue.js 制作的游戲并不多。我的團(tuán)隊(duì)遲到了一天,然后用猜拳的方式選擇了我們要用的框架;我們飛快地寫(xiě)完了代碼,并在周末結(jié)束時(shí)做出了游戲的可運(yùn)行版本。在本地測(cè)試時(shí)一切都很順利。自然,我們?yōu)樽约旱谝淮螌?xiě)出來(lái)的游戲作品感到自豪,并希望與世界分享它。
可是問(wèn)題出現(xiàn)了——當(dāng)我們構(gòu)建好應(yīng)用并開(kāi)始查詢(xún)域時(shí),內(nèi)存占用爆表了。它幾乎沒(méi)法正常運(yùn)行,不管換什么機(jī)器都會(huì)卡住不動(dòng),即使在強(qiáng)大的基于 Intel i7 處理器的系統(tǒng)上程序也會(huì)崩潰。游戲大賽的時(shí)間限制把我們拉回了現(xiàn)實(shí),我們決定擱置生產(chǎn)性能問(wèn)題,這樣起碼我們能做出一款能在自己的設(shè)備上運(yùn)行的完整游戲。就像大部分的“已完成”項(xiàng)目一樣,第二天我們就把它拋在腦后了。
但我自己沒(méi)法釋?xiě)?。它一直困擾著我。問(wèn)題是出在 Vue.js 上嗎?是 Netlify 嗎?還是因?yàn)槲覀兊娜∏纱a?我必須找出答案。
調(diào)查性能下降的原因
我首先使用 Lighthouse 進(jìn)行了快速測(cè)試。所幸 Firefox 為此提供了一個(gè)瀏覽器插件。下面就是我得到的結(jié)果。
89%的數(shù)字挺不錯(cuò)的。實(shí)際上,與許多流行的網(wǎng)站相比,這個(gè)表現(xiàn)相當(dāng)出色。這個(gè)測(cè)試指出了一些潛在問(wèn)題,例如速度指數(shù)和第一次有意義且有內(nèi)容的繪制步驟等。從理論上講,解決這些問(wèn)題會(huì)進(jìn)一步提高分?jǐn)?shù),但不一定能解決應(yīng)用面臨的嚴(yán)重性能問(wèn)題。
我們的游戲中有一些圖像和音頻素材資源,但是兩者都不至于讓游戲卡死在那里。我們也可以對(duì)這些已經(jīng)優(yōu)化過(guò)的資源再過(guò)度優(yōu)化一遍,但這可能根本就無(wú)濟(jì)于事。
這個(gè)測(cè)試無(wú)法讓我們真正找出可能導(dǎo)致這一性能問(wèn)題的原因。于是我開(kāi)始想:“該不會(huì)是 Vue 的問(wèn)題吧?”這種想法會(huì)冒出來(lái)也沒(méi)什么理由,但要是不檢查一下就是蠢了。我檢查了已部署站點(diǎn)的控制臺(tái),結(jié)果空白一片。但警告往往不會(huì)在生產(chǎn)中顯示。當(dāng)我在本地進(jìn)行相同操作時(shí),一堆 Vue 警告讓我吃了一驚。
像大多數(shù)開(kāi)發(fā)人員一樣,我對(duì)控制臺(tái)警告沒(méi)那么在意,覺(jué)得它們只是警告,而不代表錯(cuò)誤;所以我一般會(huì)把注意力集中在其他地方?;蛟S消除這些警告可以解決我的生產(chǎn)問(wèn)題,我決定深入研究每個(gè)問(wèn)題并修復(fù)它們。
所有這些警告均來(lái)自我創(chuàng)建的、用來(lái)顯示名為 Cards.vue 選項(xiàng)的組件,因此這個(gè)組件可能需要大量重寫(xiě)。
我決定按順序解決這些控制臺(tái)警告。
> [Vue warn]: Avoid using non-primitive value as key, use string/number value instead.
found in
---> <Cards> at src/components/Cards.vue
Vue.js 有很多指令,讓我們能更直觀地使用框架,比如說(shuō) v-for 就可以快速將數(shù)組渲染為列表。使用它時(shí),我們需要一個(gè) :key 才能有效地重渲染組件。但我們將一個(gè)對(duì)象用作了一個(gè)鍵,這是非原始值,因此導(dǎo)致了這個(gè)錯(cuò)誤。我決定將 index.description 用作一個(gè)新鍵,因?yàn)樗且粋€(gè)字符串,并且在值發(fā)生更改時(shí)可以更好地重新渲染。
> [Vue warn]: Duplicate keys detected: '[object Object]'. This may cause an update error.
found in
---> <Cards> at src/components/Cards.vue
將 :key 更改為一個(gè)字符串(index.description)來(lái)解決上一個(gè)錯(cuò)誤,就能解決這個(gè)重復(fù)鍵的錯(cuò)誤。我們只能將字符串類(lèi)型寫(xiě)入 DOM,因此當(dāng)我們傳遞一個(gè)要渲染的對(duì)象時(shí),該對(duì)象將轉(zhuǎn)換為等效的字符串(即 [object Object]);并且因?yàn)檫@以前是我們的鍵,所以每個(gè)對(duì)象都將轉(zhuǎn)換為 [object Object](除非對(duì)象有不同的值),進(jìn)而會(huì)出現(xiàn)重復(fù)鍵警告?,F(xiàn)在既然鍵不是對(duì)象,警告就會(huì)消失,效率也會(huì)提升。
> [Vue warn]: You may have an infinite update loop in a component render function.
found in
---> <Cards> at src/components/Cards.vue
就一個(gè)非常模糊的警告來(lái)說(shuō),這個(gè)警告似乎是最重要的:無(wú)限循環(huán)意味著內(nèi)存消耗。這條消息并沒(méi)有告訴我們可能出了什么問(wèn)題,但它確實(shí)暗示了問(wèn)題與組件中的 render 函數(shù)有關(guān)。也許是因?yàn)槲覀儗?xiě)的代碼比較取巧,因此觸發(fā)了不間斷的更新,并占用了大量的計(jì)算能力,以至于使瀏覽器和設(shè)備崩潰。
這條警告至少告訴我們要檢查 Cards.vue,所以我的第一個(gè)想法是檢查組件中的反應(yīng)屬性,因?yàn)檫@可能會(huì)導(dǎo)致錯(cuò)誤。反應(yīng)屬性在更改后會(huì)觸發(fā)重新渲染。
我們正在顯示 index.days 和 index.description 中的數(shù)據(jù)。但我們不會(huì)更改這些數(shù)據(jù),我們從 cardInfo 數(shù)組獲得 index。
我們使用這段代碼對(duì)數(shù)組中的元素進(jìn)行隨機(jī)排序,然后將前四個(gè)元素顯示為玩家選擇的選項(xiàng)。當(dāng)用戶(hù)單擊一個(gè)選項(xiàng)時(shí)將調(diào)用 effects() 函數(shù),它除了會(huì)計(jì)算一個(gè)動(dòng)作如何影響游戲狀態(tài)外,還使用 cardInfo 上的拼接原型刪除前四個(gè)元素。
在 Vue 這種使用虛擬 DOM 的框架里,用上諸如 cardInfo 之類(lèi)的反應(yīng)屬性后,每當(dāng)數(shù)據(jù)屬性的值更改時(shí)都會(huì)觸發(fā)重新渲染。在我們的應(yīng)用里,我們會(huì)直接使用 sort() 原型來(lái)更改它,然后刪除元素來(lái)重新排序。所有這些都會(huì)觸發(fā)“無(wú)限”的重新渲染,從而引發(fā)警告。
我決定更改數(shù)據(jù)過(guò)濾的邏輯,并停止對(duì)反應(yīng)屬性 cardInfo 的多次更改。我安裝了 lodash.shuffle 并定義了一個(gè)計(jì)算屬性 shuffledList(),它將創(chuàng)建一個(gè)名為 list 的 cardInfo 副本。我對(duì)其應(yīng)用了隨機(jī)排序操作,并返回了一個(gè)“frozen”結(jié)果,然后拆分開(kāi)來(lái)顯示四張卡片。我們使用了 Object.freeze(),它將使我們返回的對(duì)象不可變,從而完全停止了所有重新渲染操作。
至此,問(wèn)題解決了。
掉進(jìn)框架的坑
老實(shí)說(shuō),當(dāng)我剛開(kāi)始調(diào)查性能下降原因的時(shí)候,還覺(jué)得我肯定要優(yōu)化很多資源才能解決問(wèn)題。最后這個(gè)結(jié)果說(shuō)明,在使用許多框架抽象時(shí)我們都必須非常小心——特別是在 Vue 中更是如此,只有在必要時(shí)才使用某條指令,而且用法一定不能出錯(cuò),因?yàn)樗鼈兘^對(duì)有自己的代價(jià)。
這還讓我開(kāi)始思考自己做過(guò)的其他工作,其中應(yīng)用程序可能會(huì)因?yàn)榭蚣芏霈F(xiàn)不必要的性能問(wèn)題。大多數(shù)現(xiàn)代的前端框架都有很多抽象,使我們能更輕松地為 Web 制作應(yīng)用程序。但我們應(yīng)該牢記一點(diǎn),那就是使用這些東西可能會(huì)引發(fā)潛在的性能問(wèn)題。
我經(jīng)常使用 Vue.js,所以決定探索一些我以前用過(guò)的指令,以前我用這些指令的時(shí)候完全沒(méi)考慮過(guò)它們可能對(duì)應(yīng)用程序帶來(lái)的性能影響。其中有三條非常流行的指令進(jìn)入了我的視線(xiàn)。
這兩條指令都是用來(lái)有條件地渲染元素的,但是它們背后的工作機(jī)制卻大不相同,因此用法也大相徑庭。v-if 一開(kāi)始不會(huì)渲染組件,而只在條件為真時(shí)才渲染組件。這意味著當(dāng)你多次切換組件的可見(jiàn)性時(shí),就會(huì)不斷重新渲染。如果你要多次更改組件的可見(jiàn)性,那就不要使用這個(gè)功能。這會(huì)影響你的性能。
v-show 是一個(gè)很好的替代品。不管你是否啟用 CSS 都會(huì)渲染你的組件,但是只會(huì)根據(jù)條件是 true 還是 false 來(lái)決定組件是否可見(jiàn)。這種方法確實(shí)有其缺點(diǎn),因?yàn)樗粫?huì)將非必要組件的渲染推遲到你需要它們?cè)谄聊簧蠈?shí)際出現(xiàn)的時(shí)候。如果你的初始渲染沒(méi)那么復(fù)雜,那么它就很合適。
這條指令通常用來(lái)從數(shù)組中渲染列表。它有一個(gè)特殊的語(yǔ)法,形式為 item in list,其中 list 是源數(shù)據(jù)數(shù)組,而 item 是要迭代的數(shù)組元素的別名。默認(rèn)情況下,Vue 在源數(shù)據(jù)數(shù)組上添加 watchers,每當(dāng)發(fā)生更改時(shí)它就會(huì)觸發(fā)重新渲染。這種持續(xù)的重新渲染可能會(huì)對(duì)應(yīng)用程序性能產(chǎn)生不利影響。如果你只想可視化對(duì)象,那么 Object.freeze() 是一個(gè)很好的解決方案,可以大大提高性能。但是請(qǐng)務(wù)必記住,你將無(wú)法更新組件或編輯對(duì)象數(shù)據(jù)。
在這個(gè)研究過(guò)程中我還意識(shí)到,Lighthouse 可能檢查的是以更直接的方式影響用戶(hù)體驗(yàn)的應(yīng)用性能指標(biāo),所以接下來(lái)我的疑問(wèn)就是如何跟蹤服務(wù)器上的應(yīng)用程序性能。
我們是不是太依賴(lài)直覺(jué),是不是在假設(shè)開(kāi)發(fā)人員知道自己在做什么,假設(shè)他們遵循的是最佳實(shí)踐?不管怎樣,這次經(jīng)歷讓我對(duì)單頁(yè)應(yīng)用程序的性能產(chǎn)生了不同的看法。大家可以在 GitHub 上查看上述項(xiàng)目的存儲(chǔ)庫(kù),也歡迎大家在 Twitter 上和我打招呼。
JavaScript 中,Vue 是一種流行的前端框架,它具有簡(jiǎn)潔的語(yǔ)法、高效的渲染能力和rich的組件庫(kù)。Vue 的設(shè)計(jì)目標(biāo)是為開(kāi)發(fā)者提供一種簡(jiǎn)單易用的方式來(lái)構(gòu)建用戶(hù)界面,同時(shí)保持高性能和可擴(kuò)展性。
Vue 的核心庫(kù)是極小的,只關(guān)注視圖層,而且可以與其他庫(kù)或框架很好地集成。這使得 Vue 適合用于構(gòu)建單頁(yè)應(yīng)用程序(SPA)和Progressive Web Apps(PWA)。
Vue 的組件系統(tǒng)是它的核心特性之一,允許開(kāi)發(fā)者使用小型、獨(dú)立且易于重用的代碼塊來(lái)構(gòu)建用戶(hù)界面。Vue 組件可以傳遞數(shù)據(jù)和方法,同時(shí)還支持兩向數(shù)據(jù)綁定和異步組件加載。
Vue 還提供了一套豐富的直接指令,例如v-if、v-for和v-model,使得開(kāi)發(fā)者能夠方便地操作 DOM 元素。
Vue 的響應(yīng)式系統(tǒng)是它的另一個(gè)重要特性,允許開(kāi)發(fā)者根據(jù)數(shù)據(jù)的變化來(lái)實(shí)時(shí)更新 DOM,從而達(dá)到高效的數(shù)據(jù)驅(qū)動(dòng)開(kāi)發(fā)。
Vue 還提供了一個(gè)強(qiáng)大的工具鏈,包括官方的命令行界面(CLI)、開(kāi)發(fā)工具(DevTools)和豐富的社區(qū)支持。
總之,Vue 是一個(gè)優(yōu)秀的前端框架,適合于構(gòu)建富互動(dòng)的用戶(hù)界面,同時(shí)具有簡(jiǎn)單易學(xué)的特點(diǎn),并且擁有強(qiáng)大的社區(qū)和文檔支持。
ue的使用相信大家都很熟練了,使用起來(lái)簡(jiǎn)單。但是大部分人不知道其內(nèi)部的原理是怎么樣的,今天我們就來(lái)一起實(shí)現(xiàn)一個(gè)簡(jiǎn)單的vue
Object.defineProperty()
實(shí)現(xiàn)之前我們得先看一下Object.defineProperty的實(shí)現(xiàn),因?yàn)関ue主要是通過(guò)數(shù)據(jù)劫持來(lái)實(shí)現(xiàn)的,通過(guò)get、set來(lái)完成數(shù)據(jù)的讀取和更新。
var obj = {name:'wclimb'} var age = 24 Object.defineProperty(obj,'age',{ enumerable: true, // 可枚舉 configurable: false, // 不能再define get () { return age }, set (newVal) { console.log('我改變了',age +' -> '+newVal); age = newVal } }) > obj.age > 24 > obj.age = 25; > 我改變了 24 -> 25 > 25
從上面可以看到通過(guò)get獲取數(shù)據(jù),通過(guò)set監(jiān)聽(tīng)到數(shù)據(jù)變化執(zhí)行相應(yīng)操作,還是不明白的話(huà)可以去看看Object.defineProperty文檔。
流程圖
image
html代碼結(jié)構(gòu)<div id="wrap"> <p v-html="test"></p> <input type="text" v-model="form"> <input type="text" v-model="form"> <button @click="changeValue">改變值</button> {{form}} </div>
js調(diào)用
new Vue({ el: '#wrap', data:{ form: '這是form的值', test: '<strong>我是粗體</strong>', }, methods:{ changeValue(){ console.log(this.form) this.form = '值被我改變了,氣不氣?' } } })
Vue結(jié)構(gòu)
class Vue{ constructor(){} proxyData(){} observer(){} compile(){} compileText(){} } class Watcher{ constructor(){} update(){} }
Vue constructor 初始化
class Vue{ constructor(options = {}){ this.$el = document.querySelector(options.el); let data = this.data = options.data; // 代理data,使其能直接this.xxx的方式訪(fǎng)問(wèn)data,正常的話(huà)需要this.data.xxx Object.keys(data).forEach((key)=> { this.proxyData(key); }); this.methods = options.methods // 事件方法 this.watcherTask = {}; // 需要監(jiān)聽(tīng)的任務(wù)列表 this.observer(data); // 初始化劫持監(jiān)聽(tīng)所有數(shù)據(jù) this.compile(this.$el); // 解析dom } }
上面主要是初始化操作,針對(duì)傳過(guò)來(lái)的數(shù)據(jù)進(jìn)行處理
proxyData 代理data
class Vue{ constructor(options = {}){ ...... } proxyData(key){ let that = this; Object.defineProperty(that, key, { configurable: false, enumerable: true, get () { return that.data[key]; }, set (newVal) { that.data[key] = newVal; } }); } }
上面主要是代理data到最上層,this.xxx的方式直接訪(fǎng)問(wèn)data
observer 劫持監(jiān)聽(tīng)
class Vue{ constructor(options = {}){ ...... } proxyData(key){ ...... } observer(data){ let that = this Object.keys(data).forEach(key=>{ let value = data[key] this.watcherTask[key] = [] Object.defineProperty(data,key,{ configurable: false, enumerable: true, get(){ return value }, set(newValue){ if(newValue !== value){ value = newValue that.watcherTask[key].forEach(task => { task.update() }) } } }) }) } }
同樣是使用Object.defineProperty來(lái)監(jiān)聽(tīng)數(shù)據(jù),初始化需要訂閱的數(shù)據(jù)。
把需要訂閱的數(shù)據(jù)到push到watcherTask里,等到時(shí)候需要更新的時(shí)候就可以批量更新數(shù)據(jù)了。下面就是;
遍歷訂閱池,批量更新視圖。
set(newValue){ if(newValue !== value){ value = newValue // 批量更新視圖 that.watcherTask[key].forEach(task => { task.update() }) } }
compile 解析dom
class Vue{ constructor(options = {}){ ...... } proxyData(key){ ...... } observer(data){ ...... } compile(el){ var nodes = el.childNodes; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if(node.nodeType === 3){ var text = node.textContent.trim(); if (!text) continue; this.compileText(node,'textContent') }else if(node.nodeType === 1){ if(node.childNodes.length > 0){ this.compile(node) } if(node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')){ node.addEventListener('input',(()=>{ let attrVal = node.getAttribute('v-model') this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'value')) node.removeAttribute('v-model') return () => { this.data[attrVal] = node.value } })()) } if(node.hasAttribute('v-html')){ let attrVal = node.getAttribute('v-html'); this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'innerHTML')) node.removeAttribute('v-html') } this.compileText(node,'innerHTML') if(node.hasAttribute('@click')){ let attrVal = node.getAttribute('@click') node.removeAttribute('@click') node.addEventListener('click',e => { this.methods[attrVal] && this.methods[attrVal].bind(this)() }) } } } }, compileText(node,type){ let reg = /\{\{(.*?)\}\}/g, txt = node.textContent; if(reg.test(txt)){ node.textContent = txt.replace(reg,(matched,value)=>{ let tpl = this.watcherTask[value] || [] tpl.push(new Watcher(node,this,value,type)) if(value.split('.').length > 1){ let v = null value.split('.').forEach((val,i)=>{ v = !v ? this[val] : v[val] }) return v }else{ return this[value] } }) } } }
這里代碼比較多,我們拆分看你就會(huì)覺(jué)得很簡(jiǎn)單了
首先我們先遍歷el元素下面的所有子節(jié)點(diǎn),node.nodeType === 3 的意思是當(dāng)前元素是文本節(jié)點(diǎn),node.nodeType === 1 的意思是當(dāng)前元素是元素節(jié)點(diǎn)。因?yàn)榭赡苡械氖羌兾谋镜男问剑缂冸p花括號(hào)就是純文本的文本節(jié)點(diǎn),然后通過(guò)判斷元素節(jié)點(diǎn)是否還存在子節(jié)點(diǎn),如果有的話(huà)就遞歸調(diào)用compile方法。下面重頭戲來(lái)了,我們拆開(kāi)看:
if(node.hasAttribute('v-html')){ let attrVal = node.getAttribute('v-html'); this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'innerHTML')) node.removeAttribute('v-html') }
上面這個(gè)首先判斷node節(jié)點(diǎn)上是否有v-html這種指令,如果存在的話(huà),我們就發(fā)布訂閱,怎么發(fā)布訂閱呢?只需要把當(dāng)前需要訂閱的數(shù)據(jù)push到watcherTask里面,然后到時(shí)候在設(shè)置值的時(shí)候就可以批量更新了,實(shí)現(xiàn)雙向數(shù)據(jù)綁定,也就是下面的操作
that.watcherTask[key].forEach(task => { task.update() })
然后push的值是一個(gè)Watcher的實(shí)例,首先他new的時(shí)候會(huì)先執(zhí)行一次,執(zhí)行的操作就是去把純雙花括號(hào) -> 1,也就是說(shuō)把我們寫(xiě)好的模板數(shù)據(jù)更新到模板視圖上。
最后把當(dāng)前元素屬性剔除出去,我們用Vue的時(shí)候也是看不到這種指令的,不剔除也不影響
至于Watcher是什么,看下面就知道了
Watcher
class Watcher{ constructor(el,vm,value,type){ this.el = el; this.vm = vm; this.value = value; this.type = type; this.update() } update(){ this.el[this.type] = this.vm.data[this.value] } }
之前發(fā)布訂閱之后走了這里面的操作,意思就是把當(dāng)前元素如:node.innerHTML = '這是data里面的值'、node.value = '這個(gè)是表單的數(shù)據(jù)'
那么我們?yōu)槭裁床恢苯尤ジ履?,還需要update做什么,不是多此一舉嗎?
其實(shí)update記得嗎?我們?cè)谟嗛喅乩锩嫘枰扛?,就是通過(guò)調(diào)用Watcher原型上的update方法。
以下是總結(jié)出來(lái)最全前端框架視頻,包含: javascript/vue/react/angualrde/express/koa/webpack 等學(xué)習(xí)資料。
*請(qǐng)認(rèn)真填寫(xiě)需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。