關於 Vue 的下一個主版本,公佈的不少新特性引起了激烈的討論,但其中有一個特性引發了個人注意:javascript
更良好的可調試能力:咱們能夠精確地追蹤到一個組件發生重渲染的觸發時機和完成時機,及其緣由
在本文中,咱們將討論在 Vue2.x 中如何監測響應式機制,而且將演示一些和性能調優相關的代碼段。前端
若是你的項目比較大,那麼你頗有可能在用 Vuex。你會將 store 分割爲模塊,而且爲了關聯數據的訪問一致性你甚至須要將你的狀態範式化。vue
你可能使用 Vuex 的 getter 來派生狀態,事實上,你還會使用複合的派生數據,即一個 getter 會引用另外一個 getter 派生的數據。java
在 Vue 組件中,你會使用各類分層的模式,固然也包括常常用的 slots。在這樣的組件樹中,確定會有計算屬性(派生出來的數據)。node
當這些發生的時候,從 store 中的狀態到渲染的組件之間的響應式依賴關係將很難理清楚。react
這就是計算屬性樹了,若是不把它弄清楚的話,那麼翻轉一個看似不起眼的布爾值可能會觸發一百個組件的更新。
咱們將學習一些響應式機制的內部工做原理。若是你尚未(比較深地)理解 Dependency 類(譯者注:Dep
— 爲與源碼一致,後文都採用 Dep
)與 Watcher 類之間的關係,能夠考慮學習一下內容豐富、條例清晰的高級 Vue 課程:創建一個響應式系統。android
__ob__
麼?認可吧,當時是否是有點好奇,__ob__
看起來是否是像這樣?ios
這些在 subs
中的 Watcher 將會在這個響應式數據發生改變的時候更新。git
有時候你會在開發者工具中瀏覽一下這些對象,而且找到一些有用的信息,有時候找不到。有時候你會發現 Watcher 遠不止 5 個。github
咱們用一些簡單的代碼說明一下:JSFiddle
這個例子的 store 中的狀態有散列數組 users
和 currentUserId
兩個屬性。還有一個 getter 用來返回當前用戶的信息。另外還有一個 getter 只返回狀態爲活躍的用戶數組。
而後這裏有兩個組件,其中有三個計算屬性:
validCurrentUser
— 若當前用戶是有效用戶則爲 truetotal
— 引用反映當前全部活躍用戶的 getter,將返回活躍用戶數upperCaseName
— 將用戶的姓名映射爲大寫形式但願舉的這個特別的例子,對理解咱們討論的內容有所幫助。
一般,當從一個 Dep 類實例獲取到更新的通知時,響應機制將會觸發對應的 Watcher 函數。當我變動一個被組件渲染所依賴的響應式數據時,將觸發重渲染。
但咱們看看派生的數據,它的狀況有點複雜。首先,計算屬性的值是被緩存起來的,以便在它計算出來以後就一直可用計算後的值,只有當它的緩存失效纔會被從新計算,換句話說,只在其依賴的數據發生改變時它們纔會從新求值。
咱們再來看看以前的例子。currentUserId
狀態被 currentUser
這個 getter 引用了,而後在 validCurrentUser
計算屬性引用了 currentUser
,validCurrentUser
又是根組件 render 函數的 v-if
表達式的一部分。這條引用鏈看起來不錯。
實際上,響應數據的存儲是經過一個 Watcher 的配置選項來處理的。當咱們使用組件中的 Watcher 時,API 文檔中介紹了兩個可選選項(deep
,immediate
),但其實還有一些沒被文檔記錄的選項,我並不推介你使用這些沒被記錄的選項,但理解他們卻頗有益處。其中一個選項是 lazy
,配置它以後 Watcher 將會維護一個 dirty
標誌,若是依賴的響應數據已經更改但這個 Watcher 還未運行時它將爲 true,也就是說,此時緩存已過期。
在咱們的例子中,若是 currentUserId
被改爲 3。任何依賴於它且被設置了 lazy
的 Watcher 都會被標記爲 dirty,但 Watcher 並無運行。currentUser
和 validCurrentUser
都是這個狀態的 lazy Watcher。根渲染函數一樣會依賴於這個狀態,渲染將在下一個 tick 時被觸發。當渲染函數執行時,將會訪問已經被標記爲 dirty 的 validCurrentUser
,它將從新運行它的 getter 函數,進而訪問一樣須要更新的 currentUser
。至此,這個組件將會被正確重渲染,而且相關緩存將被更新。
等等,我彷佛聽見你在問,爲何全部 3 個 Watcher 都是依賴於這個狀態的呢?
難道他們不是相互依賴的麼?計算屬性 watcher 有一個特性就是不只它自身的值是響應式的,並且當計算屬性的 getter 被調用時,若是當前有 Wathcer 在讀取這個計算屬性的話(即 Dep.target
中有值--譯者),全部這個計算屬性的依賴也將會被這個 Wathcer 收集起來。這種依賴收集關係鏈的扁平化對性能表現更優,並且也是個比較簡單的解決方案。
這意味着一個組件將發生更新,即便它所依賴的計算屬性在從新計算後的值並無發生變化,這種更新顯然沒有什麼意義。
其中一些邏輯能夠閱讀一下 watcher 類源碼的優雅實現,代碼量 240 行左右。
__ob__
中咱們能夠獲得哪些關於計算屬性響應式機制的信息呢咱們能夠看到有哪些 Watcher 訂閱(subs
)了響應式數據的更新。記住,響應式機制在下面這些情景下起做用:
最後一個情景頗有可能被忽略,由於在開發者工具中是沒法瀏覽它的 Dep 類實例(譯者注:__ob__
)。由於 Dep 類是在最初響應式化的時候就被實例化的,可是並無在這個對象中的什麼地方把它記錄下來。稍後咱們將回頭討論這個問題,由於我將用一個小技巧來間接拿到它。
然而經過觀察對象和數組的 Watcher 也可讓咱們收穫良多,下面是一個簡單的 Watcher:
將示例跑起來以後打開開發者工具,它應該在頁面所有渲染完成以後暫停運行。你能夠輸入下面的表達式,就能看到跟上面這個圖同樣的狀況了:
this.$store.state.users[2].__ob__.dep.subs[5]
這是一個組件的渲染 Watcher,也是一個對象引用。能看到 dirty
和 lazy
這兩個我以前提到過的標誌位。同時,咱們還能夠知道它不是一個用戶建立的 Watcher(譯者注:user
爲 false)。
有時,試圖找出這個 Watcher 是哪一個組件的渲染 Watcher 是困難的,由於若是這個組件沒有全局註冊,或者這個組件沒有設置 name 屬性,那麼基本能夠說它是匿名的。然而若是你從另外一個組件引用了這個匿名組件的時候,它的 $vnode.tag
屬性一般包含它被引用時所用的名稱。
上面的這個 Watcher 來自於被其父組件定義爲 Comp
的子組件。它與 upperCaseName
計算屬性相關。計算屬性一般有一個在 getter 函數上指明的有意義的名稱,這是由於計算屬性一般被定義爲對象屬性。
一般計算屬性會給出他們的名稱及其所屬的組件,可是 Vuex 的 getter 卻並不如此。currentUser
這個 Watcher 看起來長這樣:
惟一能證實它是 Vuex 中的 getter 的線索是:它的函數體定義在 vuex.min.js 中(譯者注:[[FunctionLocation]]
)。
因此咱們應該怎樣獲取 getter 的名稱呢?在開發者工具中你一般能夠訪問 [[Scopes]]
,你能夠在 [[Scopes]]
中找到它的名稱,然而這並非經過編程的方式來獲取的。
下面是個人一個解決方法,在建立 Vuex 的 store 以後運行:
const watchers = store._vm._computedWatchers; Object.keys(watchers).forEach(key => { watchers[key].watcherName = key; });
第一行可能看起來有點奇怪,但其實 Vuex 的 store 中會維護一個 Vue 的實例,來幫助實現 getter 的功能,實際上,getter 就是一個假裝起來的計算屬性!
如今,當咱們查看 subs
數組中的 Watcher 時,咱們能夠經過獲取 watcherName
來獲取 Vuex 的 getter 的名稱。
上面我提到調試響應式數據時你是看不到對象屬性的 Dep 類實例。
在示例中,每一個 user
對象都有一個 name
屬性,每一個屬性都包含各自的 Watcher,這些 Watcher 將會在屬性發生變動時收到更新通知。
儘管 Dep 實例並不能直接訪問到,可是能夠被監聽他們的 Watcher 訪問到。Watcher 保留有一份它所依賴的全部依賴項的數組。
個人小技巧是給屬性增長一個 Watcher,而後拿到這個 Watcher 的依賴項
可是這並不簡單,我能夠經過 Vue 的 $watch
接口來添加一個 Watcher,可是返回的並非 Watcher 實例。所以我須要從 Vue 實例的內部屬性中獲取到 Watcher 實例。
const tempVm = new Vue(); tempVm.$watch(() => store.state.users[2].name, () => {}); const tempWatch = tempVm._watchers[0]; // now pull the subs from the deps tempWatch.deps.forEach(dep => dep.subs .filter(s => s !== tempWatch) .forEach(s => subs.add(s)));
我已經把這些小的代碼片斷封裝到了一個任何人均可以獲取到的工具庫中:vue-pursue。
能夠看看使用示例。
例子中的 () => this.$store.state.users[2].name
通過 vue-pursue 處理後返回:
{ "computed": [ "currentUser", "validCurrentUser", "Comp.upperCaseName" ], "components": [ "Comp" ], "unrecognised": 1 }
須要注意的是,根組件將會在操做後更新,但由於根組件沒有名稱,因此其顯示爲 unrecognised
。currentUser
這個 Vuex 的 getter 將會更新,且這個更新並不來源於 name
的更新。
經過傳遞一個箭頭函數給 vue-pursue,這個箭頭函數所具備的全部依賴將會被將會被訂閱者考慮在內,這意味着 users
和 users[2]
對象也包括在內。或者,若是咱們傳遞 (this.$store.state.users[2], ‘name’)
,輸出將會是:
{ "computed": [ "validCurrentUser", "Comp.upperCaseName" ], "components": [ "Comp" ], "unrecognised": 1 }
我須要着重強調的是,要謹慎使用任何如下劃線做爲開頭的屬性,由於這不是公共 API 的一部分,它們可能會在沒有任何警告的狀況下被移除。上面介紹的這個功能,一開始就沒打算使用於生產環境,也沒打算使用在運行時環境,這只是一個方便調試的開發者工具。
最終隨着 Vue3.0 的出現,這將會被更全面、更簡單易用、更可靠的替代。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、 iOS、 前端、 後端、 區塊鏈、 產品、 設計、 人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、 官方微博、 知乎專欄。
PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~
另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~