Promise
所謂Promise,簡單說就是一個(gè)容器,里面保存著某個(gè)未來才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果。從語法上說,Promise 是一個(gè)對(duì)象,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進(jìn)行處理。
—— ECMAScript 6 入門 阮一峰
場景
最近正在做的一個(gè)項(xiàng)目,包含"前臺(tái)"與"后臺(tái)", 前臺(tái)是對(duì)數(shù)據(jù)的展示,后臺(tái)是對(duì)用戶等相關(guān)權(quán)限的管理,大到頁面級(jí)別的權(quán)限,小到數(shù)據(jù)接口與菜單權(quán)限。整體是基于Oauth2.0做的一整套權(quán)限系統(tǒng)。
用戶登錄成功后,返回權(quán)限列表,一級(jí)權(quán)限包含當(dāng)前用戶可以訪問到的子菜單:如下圖:
管理員權(quán)限下,7個(gè)子系統(tǒng)圖標(biāo)會(huì)全部返回,實(shí)際情況返回多少菜單取決于當(dāng)前用戶權(quán)限。例如普通用戶只能看到查看兩個(gè)子系統(tǒng),因?yàn)槭莿?dòng)態(tài)可配置的,考慮到后期可能會(huì)增加子系統(tǒng),所以子系統(tǒng)路由、名稱、以及圖像全部都來源于后臺(tái),通過后臺(tái)管理進(jìn)行配置。
按照正常的思路,后臺(tái)獲取菜單信息,然后循環(huán)渲染:
//Home.vue
data() {
return: {
list:[]
}
},
mounted() {
this.GetMenuList();
},
methods:{
GetMenuList() {
...
this.list = res.result //后臺(tái)獲取菜單列表
}
}
<div class='container'>
<div class="col_item" v-for="(item,index) in list" :key="index">
<div class="content_img">
<img :src="item.src" alt class="img_item"
@click="routerLink(item.url)"
>
</div>
<div class="content_title">{{ item.descritpion }}</div>
</div>
</div
正常的公司網(wǎng)速下,頁面加載效果如下圖:
因?yàn)槭莿?dòng)態(tài)獲取子系統(tǒng)圖標(biāo),并且頁面響應(yīng)式,也就是任何屏幕都會(huì)一屏顯示,所以未知圖像的width,height,寬高會(huì)自動(dòng)計(jì)算。
谷歌開發(fā)工具調(diào)制3G網(wǎng)速下,仔細(xì)看:
為了模擬網(wǎng)速慢的情況,使用谷歌瀏覽器network設(shè)置了3G網(wǎng)速,可以看到頁面加載時(shí),背景的container是一個(gè)最小高度(動(dòng)態(tài)計(jì)算),當(dāng)圖片加載成功并且循環(huán)顯示后,塊的高度撐搞,并且圖片也是從0高度到實(shí)際高度,在弱網(wǎng)絡(luò)情況下,體驗(yàn)一般。
考慮過通過固定寬高來解決,但是用不能滿足響應(yīng)式的一屏幕顯示。
問題原因
因?yàn)閠emplate循環(huán)里,動(dòng)態(tài)賦值圖片src,所以此時(shí)的邏輯是先請(qǐng)求所有的數(shù)據(jù)列表(其中包含圖像src字段),然后動(dòng)態(tài)綁定:src,然后圖片根據(jù)src才能加載,也就造成了弱網(wǎng)下的“延遲”問題。
根據(jù)原因分析,container容器預(yù)先寫在template標(biāo)簽里,高度由圖像撐開,container內(nèi)部循環(huán)完成后,基本的骨架已經(jīng)渲染完成,然后圖像加載完成,container被撐高。
因此,如果先加載完成全部圖片,再進(jìn)行渲染是不是可以解決?
制定方案:
- 1.通過image對(duì)象,用代碼初始化load所有圖像。
- 2.通過appedChild一次性將內(nèi)容插入container容器內(nèi)
1.利用Promise,異步加載圖像
修改代碼如下:
//Home.vue
<script>
export default {
data () {
return {
list: [], //存放后臺(tái)返回的數(shù)據(jù)
}
},
mounted() {
this.GetMenuList();
},
methods:{
GetMenuList() {
...
this.list = res.result //后臺(tái)獲取菜單列表,不負(fù)責(zé)渲染,只存儲(chǔ)數(shù)據(jù)
this.loadImages();
},
//圖像加載方法
loadItemImage(img) {
return new Promise(resolve => {
const image = new Image();//通過new Image對(duì)象 加載圖像,本質(zhì)是一個(gè)object
image.src = img.imgUrl; //指定src
image.url = img.url; //自定義添加一些字段暴漏到外部
image.id = img.id; //自定義添加一些字段暴漏到外部
image.name = img.name;
image.descritpion = img.descritpion; //自定義添加一些字段暴漏到外部
image.onload = () => resolve(image); //加載圖像
image.onerror = () => resolve(image);
image.onclick = () => { //添加跳轉(zhuǎn)點(diǎn)擊事件
this.routerLink(img.url, img.name, img.id);//跳轉(zhuǎn)函數(shù)
};
image.className = "home_container_img_item"; //添加class屬性
});
},
//圖像處理函數(shù)
loadImages() {
Promise.all(
this.list.map(img => {
return this.loadItemImage(img);
})
).then(imgs => {
//imgs是list.map后生成的新數(shù)組,其中包含了n個(gè)image對(duì)象
//得到已經(jīng)加載完成的image數(shù)組,準(zhǔn)備append到container容器
});
}
}
}
</script>
解讀代碼:首先后臺(tái)返回?cái)?shù)據(jù)后調(diào)用loadImages,對(duì)list進(jìn)行map操作,每一項(xiàng)執(zhí)行l(wèi)oadItemImage方法。
loadItemImage方法:return 了一個(gè)Promise實(shí)例,實(shí)例內(nèi)部通過代碼new Image,創(chuàng)建了一個(gè)image實(shí)例,其中src、onclick、onload、className是image原型上的屬性和事件,因?yàn)閷?shí)際需要點(diǎn)擊圖像跳轉(zhuǎn),所以在image上新增了一些自定義屬性,供跳轉(zhuǎn)使用。當(dāng)圖像load成功后resolve。循環(huán)操作后,map返回一個(gè)由image對(duì)象組成的新數(shù)組:
Promise.all當(dāng)所有的圖像加載完成后,準(zhǔn)備進(jìn)行append到container節(jié)點(diǎn)。
如何append?
參考了vue的思想,通過虛擬Dom操作映射到真實(shí)的Dom下,避免直接循環(huán)append操作dom,數(shù)據(jù)驅(qū)動(dòng)視圖:
新建virtualDom.js
//聲明Dom類,用工廠方法進(jìn)行封裝:
class Dom {
constructor(tags, attribute, children, event) {
this.tags = tags; // html標(biāo)簽字段,div p input..
this.attribute = attribute; //class style 等html屬性
this.children = children; //子節(jié)點(diǎn)數(shù)組
this.event = event; //事件對(duì)象
}
};
// 創(chuàng)建虛擬DOM,返回虛擬節(jié)點(diǎn)(object)
export function createElement(tags, attribute, children, event = {}) {
return new DOM(tags, attribute, children, event);
//少數(shù)dom元素可能存在事件,如點(diǎn)擊事件等,應(yīng)對(duì)多數(shù)情況,設(shè)置event默認(rèn)值{}
}
新建render.js :
// render方法虛擬DOM映射到真實(shí)DOM
export function render(dom) {
// 根據(jù)標(biāo)簽創(chuàng)建元素
let el = document.createElement(dom.tags);
// 遍歷添加屬性
for (let key in dom.attribute) {
// 設(shè)置屬性的方法
setAttr(el, key, dom.attribute[key]);
}
//添加事件
for (let key in dom.event) {
// 添加事件的方法
AddEvent(el, key, dom.event[key]);
}
//針對(duì)于與大多數(shù)情況做了判斷,本項(xiàng)目的上下文環(huán)境中有三種情況
dom.children.forEach(child => {
if (child instanceof Dom) //如果子節(jié)點(diǎn)是Dom類,那么就繼續(xù)向下遞歸
child = render(child)
else if (typeof child == 'string') //如果是文本那么就是文本節(jié)點(diǎn)
child = document.createTextNode(child);
else
child = child; //其他html元素,本項(xiàng)目中是<img>元素
// 添加到對(duì)應(yīng)元素內(nèi)
el.appendChild(child); //插入元素
});
return el;
}
// 設(shè)置屬性
export function setAttr(node, key, value) {
switch (key) {
case 'value':
// node是一個(gè)input或者textarea就直接設(shè)置其value即可
if (node.tagName.toLowerCase() === 'input' ||
node.tagName.toLowerCase() === 'textarea') {
node.value = value;
} else {
node.setAttribute(key, value);
}
break;
case 'style':
// 直接賦值行內(nèi)樣式
node.style.cssText = value;
break;
default:
node.setAttribute(key, value);
break;
}
}
//添加事件
function AddEvent(el, key, funcEvent) {
switch (key) {
case 'click': //單擊
el.onclick = funcEvent;
break;
case 'dbClick':
//..
break;
//...根據(jù)情況添加
}
}
// 將元素插入到頁面內(nèi)
export function renderDom(el, target) {
target.appendChild(el);
}
準(zhǔn)備映射虛擬dom:
映射: Object to dom.
或者說是:
js to html.
本項(xiàng)目中的dom結(jié)果是這樣的:
接著上面的代碼
//Home.vue
import { createElement} from "common/utils/virtualDom.js";
import { render, renderDom} from "common/utils/render.js";
...
//圖像處理函數(shù)
loadImages() {
Promise.all(
this.list.map(img => {
return this.loadItemImage(img);
})
).then(imgs => { //imgs是list.map后生成的新數(shù)組,其中包含了n個(gè)image對(duì)象
//得到已經(jīng)加載完成的image數(shù)組,準(zhǔn)備append到container容器
let element = []
for(let img of imgs) {
element.push(createElement('div', {class: 'home_container_col_item'},
[ //創(chuàng)建div,子元素img就是每一個(gè)img對(duì)象
createElement('div',{class: 'home_container_content_img'},[img]),
createElement(//創(chuàng)建文本字,添加點(diǎn)擊事件
'div',
{class:'home_container_content_title'},
[img.descritpion],
{click:()=>{this.routerLink(img.url, img.name, img.id)}}),
]
))
}
//createElement的四個(gè)參數(shù)依次寫入
//上邊的樹就是下面這種結(jié)構(gòu)
// {
// tags: "div",
// attribute: {
// class: "home_container_col_item"
// },
// children: [
// {
// tags: "div",
// attribute: {
// class: "home_container_content_img"
// },
// children: [image]
// },
// {
// tags: "div",
// attribute: {
// class: "home_container_content_title"
// },
// children: ['xxx子系統(tǒng)']
// },
// ]
// };
//然后循環(huán)push到數(shù)組里。
//...接上
//container
let virtualDom = createElement(
"div",
{ class: "home_container_menu_row" },
element
);
let el = render(virtualDom); // 渲染虛擬DOM得到真實(shí)的DOM結(jié)構(gòu)
renderDom(el, document.getElementById("home_container_center"));//掛載dom
});
}
上面的dom結(jié)構(gòu)只是一個(gè)例子,實(shí)際情況要根據(jù)自己的結(jié)構(gòu)編寫。
看實(shí)際項(xiàng)目效果:
菜單動(dòng)態(tài)獲取,container并沒有高度被撐開的情況,頁面加載,container為空,后臺(tái)返回?cái)?shù)據(jù)后,container呈現(xiàn)。
參考文章: