有這麼一個需求,原來的一個後臺須要重構,前端展現爲這樣的: javascript
正如你所看到的,這個有添加有刪除功能,還須要長成這樣。後端給的數據格式通過簡化爲:{
"data": {
"path": "data",
"type": "dict",
"showName": "文言文編輯",
"value": null,
"isNecessary": true,
"subDefine": [
{
"path": "data/title",
"type": "string",
"showName": "標題",
"value": "周亞夫軍細柳",
"isNecessary": true,
"subDefine": null
},
{
"path": "data/book",
"type": "list",
"showName": "課本",
"value": null,
"isNecessary": true,
"subDefine": [
{
"path": "data/book/book_0",
"type": "dict",
"showName": "1",
"value": null,
"isNecessary": false,
"subDefine": [
{
"path": "data/book/book_0/version",
"type": "string",
"showName": "教材",
"value": "人教新版",
"isNecessary": true,
"subDefine": null
}
]
},
{
"path": "data/book/book_1",
"type": "dict",
"showName": "2",
"value": null,
"isNecessary": false,
"subDefine": [
{
"path": "data/book/book_1/version",
"type": "string",
"showName": "教材",
"value": "部編本",
"isNecessary": true,
"subDefine": null
}
]
}
]
}
]
}
}
複製代碼
在看看各個參數的意義:html
path
: 當前路徑type
: 表示類型showName
: 展現的字value
: 輸入框展現的內容isNecessary
: 是不是必須的subDefine
: 子元素,若是有就渲染子元素若是沒有就不渲染 後端怎麼把數據傳給個人,我就須要按這樣的格式傳給他,中間用戶可能修改value
值,而後須要把這些值進行校驗,而且傳給後端。這個後臺是很古老的東西,具體是用jquery
經過字符串拼接的方式,將數據拼接爲想要的html
,在往真實的DOM
中插入這些字符串,因此形成重複代碼不少,並且字符串相對於html
書寫,可讀性更差。爲了加強可維護性,因此準備對其重構如何把數據轉換爲上面的結構圖,這種結構圖該如何繪製,下面記錄個人心理路程~~:前端
jsx
拿到這個需求的時候那時候還不太瞭解嵌套組件這種思路,因此首先想到了可否使用jsx
,由於當時認爲光靠html
是沒法作到這種嵌套結構的。既然這種思路沒法作到,首先想到的能不能經過js
遞歸調用的方式由於js
更加靈活,最後返回一個html
。這樣咱們就用了js
替代了html
來生成這種嵌套結構的html
。那咱們就須要捨棄template
轉而使用咱們本身定義的render
函數。之前沒有用過jsx
,因此先學習了一個下午,準備作點簡單的東西先試水一下,先在這個項目嘗試一些jsx
的代碼,加上之後,發現編譯報錯,說我差一個loader
,報錯以下:vue
vue.config.js
中加入了
chainWebpack: (config) => {
config.module.rules.delete('js');
}
複製代碼
這樣就不會對jsx
進行編譯,可是和公司本身組件庫的設計有衝突,而後須要組件庫成員來修復這個問題,那麼這個項目可能就不能按時交付了。同時須要考慮到維護成本,vue
中不多地方是使用jsx
的,那後面的人維護這個是否是須要增長維護成本呢。那麼是否能夠選擇更優秀的方式來解決這個問題呢。java
懶人有懶人的思考,個人第一反應就是找個插件啥的,啥都不用操心了,傳遞數據完事了。自己後臺也用的是element-ui
,因此第一想法用一下tree
插件,可是tree
組件長這個樣 node
tree
傳入的參數和後端傳給個人參數及其的類似,那是否能從
tree
的實現中獲取經驗呢
tree
組件實現原理原理可參照element-ui
的實現,這裏對其進行簡化jquery
<tree>
<tree-node v-for="item in data">
</tree-node>
</tree>
複製代碼
大概就是這個意思,外面是tree
組件,裏邊是tree-node
,如何實現多層嵌套呢,下面是tree-node
組件的實現:算法
<template>
<div class="tree-node">
<tree-node v-if="data.children" ></tree-node>
</div>
</template>
<script> export default { name: 'tree-node' } </script>
複製代碼
能夠看到這樣就實現了嵌套組件的效果,可是須要注意的是,必須須要聲明當前組件的name
不然在當前文件中使用當前組件。chrome
從tree
的實現中,能夠借鑑這種思路實現當前產品的需求,實現一個子組件,若是children
存在,那麼就調用tree-node
,若是不存在,就不須要渲染當前組件。首先咱們就實現了這種效果,可是這個需求沒有終結,我又遇到了新的問題--性能問題.element-ui
事實上,在咱們寫代碼的時候不多遇到性能問題,可是此次確實發現了當咱們將數據傳給組件的時候,須要大量的時間上面的結構才能渲染出來。因此須要分析是什麼地方形成了這些性能問題。
這裏藉助了chrome
的性能分析工具performance
,進行調用堆棧分析,看最消耗性能的是哪一個部分
scripting
佔據了大量的時間,在這個旁邊有一個
call-tree
能夠看到是哪一個函數佔據的時候最多
在挨個點下去,看看最消耗時間的是哪一個環節,最後發現以下:
這個方法是
element
的
textarea
中用於自適應高度的一個函數,下面就是分析這個函數對性能的影響這麼大
在需求中我是這麼調用的
<el-input
autosize
type="textarea">
</el-input>
複製代碼
autosize
拖垮了頁面的性能,下面來看看autosize
的實現。autosize
最後調用的方式是resizeTextarea
,下面來看看具體的實現
resizeTextarea() {
const { autosize } = this;
const { minRows, maxRows } = autosize;
this.textareaCalcStyle = calcTextareaHeight(this.$refs.input, minRows, maxRows);
}
複製代碼
能夠看到的是最後調用的是calcTextareaHeight
,具體看看他的實現
function calcTextareaHeight(tragetment, minRows, maxRows) {
if (!hiddenTextarea) {
hiddenTextarea = document.createElement('textarea');
document.body.appendChild(hiddenTextarea)
}
const {
paddingSize,
borderSize,
boxSizing,
contextStyle,
// 獲取元素的尺寸信息
} = calculateNodeStyling(targetment)
// 設置隱藏文本域的樣式
hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`)
hiddenTextarea.value = targetElement.value || targetElement.placeholder || '';
let height = hiddenTextarea.scrollHeight; // 包含pading的高度
if (boxSizing === 'border-box') {
height += borderSize;
} else if (boxSizing === 'content-box') {
height -= padingSize
}
if (hiddenTextarea.parentNode) {
hiddenTextarea.parentNode.remove(hiddenTextarea)
}
hiddenTextarea = null
return { height: `${height}px`}
}
複製代碼
分析上面的函數,由於組件庫須要考慮的元素衆多,能夠須要加入一些對於咱們自身業務無關的代碼例如上面的代碼就有幾個地方能夠針對業務進行優化:
calculateNodeStyling
來獲取元素的一些屬性,這對於業務來講徹底是可控的,padding
、border
咱們徹底能夠設置,而不須要用js
獲取,在常說的性能優化中,最重要的就是避免對DOM
的反覆操做,若是節省了這一步操做是否是效率可以獲得極大的提高呢textarea
,而且把如今的textarea
的樣式賦值給隱藏的textarea
從而來計算高度。這對於咱們只須要使用簡單功能的徹底沒有必要的只須要使用height=scrollHeight
。而且在代碼咱們又把這個隱藏textarea
從文檔流中移除,若是在文檔中有1000
個textarea
中,是否是就須要建立textarea
而後將其移除呢,上面提到操做DOM
會形成性能的降低 有兩個緣由因此準備作一個簡單的輸入框知足咱們的需求<textarea ref="textarea" v-model="value">
</textarea>
<script>
export default {
mounted() {
this.textarea = this.$refs.textarea;
},
watch: value() {
textarea.height = 0;
textarea.height = textarea.scrollHeight;
}
}
</script>
複製代碼
這樣就可以 簡單實現輸入框的高度隨內容改變而改變,並且去除了一些沒有必要的操做,使得性能大大的提升。
除了更改上面組件的實現方式,這個需求中咱們是否有其餘的地方能夠進行優化
瞭解Vue
源碼的都知道,當咱們對data
中的值進行set
操做,須要對新賦值的數據進行響應式設置,也就是從新定義數據的set
、get
操做。可是在當前業務中,後端的值是一個不會更改的值,咱們對其進行響應式是否有必要嗎,而且這個數據是很是大的,若是對這個數據遞歸進行從新定義get
和set
操做是否是自己就是一種消耗的性能,因此咱們並不須要對其進行以來收集,使用object.freeze
就不會讓vue
對這些數據進行從新定義setter
和getter
this.data = Object.freeze(data);
複製代碼
Vue
自己的原理決定了父子建立時生命週期子的前後順序爲:
父beforeCreated => 父created => 父beforeMount => 子beforeCreated
=> 子created => 子beforeMount => 子mounted => 父mounted
複製代碼
當數據更新的時候父子週期的前後順序爲:
 父beforeUpdate->子beforeUpdate->子updated->父updated
複製代碼
爲何渲染這麼慢呢,就是由於整個組件須要等內部的子元素都渲染完成之後,才把整個父組件掛載到真實DOM
,可是對於整個部分沒有太好的解決辦法
由於在數據處理的時候,咱們對後端給的數據每條數據都進行了遍歷,在上面代碼中爲了給某條數據加一個required
屬性,對數組進行了深度遍歷,這樣是爲了讓template
中的表達式更加簡單。後端返回給咱們的數據可能及其龐大,進行遞歸可能就會影響性能,原則是能不算就不算。因此轉而在template
中使用表達式來書寫判斷條件,可能這個表達式寫的很長,可是節約了性能。
在需求中咱們可能須要對一個元素進行子類擴展或者刪除,那麼該如何實現呢。
刪除簡單,點擊刪除實際把該元素的父親的subDefine
刪除最後一個元素,也就是把父元素的trees
刪除最後一個元素,代碼以下:
trees.pop()
複製代碼
這樣就能刪除最後一個元素了
後端在傳給前端的時候,除了一個data
,還有一個minData
, 這個minData
的數據格式和data
相同,不一樣的是每一項的value
都是空的,若是該項能夠擴展,意思是說可以往subDefine
中添加子元素,這個subDefine
是不爲空的,可是隻有一個元素。這個數據在添加子元素的時候極爲的有用,好比說如今當前的元素的subDefine
是個空的,當咱們向其中添加元素的時候,那這時候這個新元素的數據結構應該是怎麼樣的。這時就須要經過找到minData
中哪個元素的path
和當前的path
是相同的。先前想過循環遍歷找到相同的,可是瞬間被本身否認了,雖然我們對算法沒什麼研究,可是也不能使用這麼low
的想法吧。因此首先對mindData
進行處理,在先前提到每一個元素的path
都是不一樣的,那是否是能夠從新建立一個對象,其中的key
就是每條數據的path
,value
就是該條數據。這時候但咱們須要添加一個新元素的時候,只須要知道對應的path
,而後從minData
中取出key
等於path
的那條數據,而後取出那條數據的subDefine
的第一條數據就好了。
下面是minData
的數據處理函數:
constructPathObj(subDefine, res = {}) {
subDefine.forEach((value) => {
res[value.path] = value;
if (value.subDefine) {
this.constructPathObj(value.subDefine, res);
}
});
return res;
}
minData = constructPathObj(data)
複製代碼
這樣就獲得了一個已path
爲key
,數據爲value
的一個對象。這裏還須要注意一點就是由於前面提到path
是惟一的,因此在添加新元素的時候不可以讓path
重複。例如如今subDefine
中有一個元素的path
爲data/book_1
,後端要求新添加的元素path
爲data/book_2
,因此有了如下代碼
const { subDefine } = item;
let index;
if (subDefine.length === 0) {
// 根據path找到子元素
index = 0;
} else {
index = subDefine.length;
}
const temp = this.dealData(minData[information.path].subDefine[0], index);
subDefine.push(temp);
function dealData(data, index) {
const temp = {};
if (data.subDefine) {
temp.subDefine = [];
data.subDefine.forEach((val) => {
// 先對傳給後面的數據path進行處理
val.path = val.path.replace(/(.*)_[0-9]/, `$1_${data.showName}`);
temp.subDefine.push(this.dealData(val));
});
}
if (data.type === 'dict') {
temp.showName = index + 1;
temp.path = data.path.replace(/(.*)_[0-9]/, `$1_${index}`);
}
return {
...data,
...temp,
};
}
複製代碼
這樣就會對生成的每一個元素的path
進行規範,也就到達了添加一個新元素的一個效果。
由於是項目重構,因此須要現有的一個接口,那麼接口的內容就不能變更。那就須要在原有的數據結構上進行修改,把這些數據處理爲咱們想要的數據格式。在實現項目的過程當中遇到了一些性能問題,並對其分析產生這些的緣由並進行解決,加深了對需求的理解,也提升對性能優化這塊的重視。