問題場景
我使用vue 做前端開發,現在我有一個腳本樹,想要只是搜索功能,這個搜索是在前端完成的,但是當腳本樹的數據和層次結構較深時,輸入搜索後,可能需要1-2秒的時間響應,這時就希望在搜索開始的時候加入loading效果,結束後取消loading。
- 腳本樹長這樣:
- 點擊搜索後希望使用iview 的 Spin 組件, 期望結果如下:
也就是加個遮罩層,有個Loaing,按照iview的組件文檔只需要給Spin 組件綁定一個布爾類型的值,通過值的改變就能顯示或者隱藏Spin組件, 前端代碼如下:
<Spin size="large" fix v-show="dataLoading"></Spin>
點擊搜索後我的搜索方法如下;
onSearchTreeData (searchContent) {
// this.searchText = searchContent
this.dataLoading = true
// 搜索邏輯
this.userScriptTreeMap.forEach((treeItem, menuTag) => {
let isSearchMatch = false
if (!searchContent) {
isSearchMatch = false
} else {
isSearchMatch = treeItem.text.indexOf(searchContent) !== -1
}
// 各個子節點會觀測$isSearchMatch 屬性值
this.$set(treeItem, '$isSearchMatch', isSearchMatch)
if (isSearchMatch) {
while (treeItem.parent) {
if (!treeItem.parent.opened) {
treeItem.parent.opened = treeItem
}
treeItem = treeItem.parent
}
}
})
// 設置組件結束搜索
this.dataLoading = false
console.info('3333')
},```’
其實這段代碼很簡單,不用關注搜索邏輯的話,也就是三步,第一步:搜索之前把 dataLoading設置爲true,這時候希望Spin組件顯示,出現loading效果。第二步: 執行搜索邏輯;第三步搜索完成後:把dataLoading設置爲false,loading效果結束,然而測試時,發現不會出現loading效果,也就是vue組件綁定的值變化並沒有引起界面的變化。思考原因:一開始覺得搜索很快就完成了,就是dataLoading的值變化的太快,Spin快速的顯示和隱藏,視覺上感覺不到而已。然後我加了個空的for循環,時間足夠長,這次輸入後明顯感覺頁面卡了,但是也沒出現loading效果。
問題思考及解決辦法
從代碼邏輯上來看好像沒有啥錯誤,但是爲什麼數據值改變了,vue沒有重新渲染界面了?這好像和MVVM理念不一樣啊。思考了一會想起關鍵一句話:js 是單線程的。vue只是Js框架而已,數據值的改變,重新渲染都需要js線程去執行,那麼我執行搜索方法時是同步的,雖然值改變了,但是vue需要等整個search方法結束後纔會把變化的值渲染到組件UI,然而等這整個search方法執行完後,dataLoading相當於沒變,自然就不會有Loading效果了。那麼關鍵點就是希望能讓搜索的邏輯能夠異步執行。js 要咋樣異步了?反正想到一個函數setTimeout 貌似是異步的,於是改成如下代碼:
// 第一步頁面Loading
this.dataLoading = true
// 第二步搜索
setTimeout(() => {
this.userScriptTreeMap.forEach((treeItem, menuTag) => {
this.$set(treeItem, '$isSearchMatch', false)
treeItem.opened = false
})
this.userScriptTreeMap.forEach((treeItem, menuTag) => {
let isSearchMatch = false
if (!searchContent) {
isSearchMatch = false
} else {
isSearchMatch = treeItem.text.indexOf(searchContent) !== -1
}
// 各個子節點會觀測$isSearchMatch 屬性值
this.$set(treeItem, '$isSearchMatch', isSearchMatch)
if (isSearchMatch) {
while (treeItem.parent) {
if (!treeItem.parent.opened) {
treeItem.parent.opened = treeItem
}
treeItem = treeItem.parent
}
}
})
// 設置組件結束搜索
this.dataLoading = false
}, 100)
這裏看到我的執行邏輯是在timeout裏面執行的。跑一下效果,果然出現了loading效果。這裏可以看到timeout是異步的,即 第一步dataLoading值發生改變並且vue已經監測到值重新渲染UI後,在執行setTimeout裏面的搜索邏輯,搜索結束後dataLoading設置爲false,loading結束,搜索值出現。
好的,問題解決了,提交代碼完工了!開心😊!
解決辦法後面更深的原因
但是這裏有個疑惑,爲啥setTimeout可以呢?setTimeout 到底做了啥?是不是有其他的辦法?帶着疑問問了下百度粑粑,發現引出js event loop 知識點。
參考大神門的文章如下:
總的來說需要清楚js 事件循環的機制才能明白解決辦法和方案。
後續嘗試
看完js 事件循環後,發現Promise 的then 方法回調其實也是異步的,只不過它比setTimeout的時間要早,於是想能不能把搜索方法放到then裏面去執行:嘗試如下:
onSearchTreeData (searchContent) {
// this.searchText = searchContent
this.dataLoading = true
new Promise(resolve => {
console.info('1111')
resolve()
}).then(() => {
console.info('22222')
// 選重置狀態
for (let i = 0; i < 9000000000; i++) {
}
this.userScriptTreeMap.forEach((treeItem, menuTag) => {
this.$set(treeItem, '$isSearchMatch', false)
treeItem.opened = false
})
this.userScriptTreeMap.forEach((treeItem, menuTag) => {
let isSearchMatch = false
if (!searchContent) {
isSearchMatch = false
} else {
isSearchMatch = treeItem.text.indexOf(searchContent) !== -1
}
// 各個子節點會觀測$isSearchMatch 屬性值
this.$set(treeItem, '$isSearchMatch', isSearchMatch)
if (isSearchMatch) {
while (treeItem.parent) {
if (!treeItem.parent.opened) {
treeItem.parent.opened = treeItem
}
treeItem = treeItem.parent
}
}
})
// 設置組件結束搜索
this.dataLoading = false
})
console.info('3333')
結果發現是不行的,這裏看到我打印了執行順序,執行順序是 “1111”-》“3333”-》“2222”,從執行順序來說符合事件循環模型,但是不成功的原因依然是vue js需要監聽值的變化,then 被回調的時間太短,vue還未監聽執行渲染邏輯,then 邏輯就被執行,js單線程的原因導致必須等then方法執行完後,在執行vue的監聽。這個只是我個人推測。