(關注福利,關注本公衆號回覆[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導)
前端
Vue進階系列彙總以下,歡迎閱讀,歡迎加高級前端進階羣一塊兒學習(文末)。vue
Vue 進階系列(二)之插件原理及實現github
Reactivity表示一個狀態改變以後,如何動態改變整個系統,在實際項目應用場景中即數據如何動態改變Dom。前端工程師
如今有一個需求,有a和b兩個變量,要求b一直是a的10倍,怎麼作?閉包
let a = 3; let b = a * 10; console.log(b); // 30
乍一看好像知足要求,但此時b的值是固定的,無論怎麼修改a,b並不會跟着一塊兒改變。也就是說b並無和a保持數據上的同步。只有在a變化以後從新定義b的值,b纔會變化。app
a = 4; console.log(a); // 4 console.log(b); // 30 b = a * 10; console.log(b); // 40
將a和b的關係定義在函數內,那麼在改變a以後執行這個函數,b的值就會改變。僞代碼以下。函數
onAChanged(() => { b = a * 10; })
因此如今的問題就變成了如何實現onAChanged
函數,當a改變以後自動執行onAChanged
,請看後續。post
如今把a、b和view頁面相結合,此時a對應於數據,b對應於頁面。業務場景很簡單,改變數據a以後就改變頁面b。
<span class="cell b"></span> document .querySelector('.cell.b') .textContent = state.a * 10
如今創建數據a和頁面b的關係,用函數包裹以後創建如下關係。
<span class="cell b"></span> onStateChanged(() => { document .querySelector(‘.cell.b’) .textContent = state.a * 10 })
再次抽象以後以下所示。
<span class="cell b"> {{ state.a * 10 }} </span> onStateChanged(() => { view = render(state) })
view = render(state)
是全部的頁面渲染的高級抽象。這裏暫不考慮view = render(state)
的實現,由於須要涉及到DOM結構及其實現等一系列技術細節。這邊須要的是onStateChanged
的實現。
實現方式是經過Object.defineProperty
中的getter
和setter
方法。具體使用方法參考以下連接。
MDN之Object.defineProperty
須要注意的是get
和set
函數是存取描述符,value
和writable
函數是數據描述符。描述符必須是這兩種形式之一,但兩者不能共存,否則會出現異常。
convert()
函數要求以下:
obj
做爲參數Object.defineProperty
轉換對象的全部屬性示例:
const obj = { foo: 123 } convert(obj) obj.foo // 輸出 getting key "foo": 123 obj.foo = 234 // 輸出 setting key "foo" to 234 obj.foo // 輸出 getting key "foo": 234
在瞭解Object.defineProperty
中getter
和setter
的使用方法以後,經過修改get
和set
函數就能夠實現onAChanged
和onStateChanged
。
實現:
function convert (obj) { // 迭代對象的全部屬性 // 並使用Object.defineProperty()轉換成getter/setters Object.keys(obj).forEach(key => { // 保存原始值 let internalValue = obj[key] Object.defineProperty(obj, key, { get () { console.log(`getting key "${key}": ${internalValue}`) return internalValue }, set (newValue) { console.log(`setting key "${key}" to: ${newValue}`) internalValue = newValue } }) }) }
Dep
類要求以下:
Dep
類,包含兩個方法:depend
和notify
autorun
函數,傳入一個update
函數做爲參數update
函數中調用dep.depend()
,顯式依賴於Dep
實例dep.notify()
觸發update
函數從新運行示例:
const dep = new Dep() autorun(() => { dep.depend() console.log('updated') }) // 註冊訂閱者,輸出 updated dep.notify() // 通知改變,輸出 updated
首先須要定義autorun
函數,接收update
函數做爲參數。由於調用autorun
時要在Dep
中註冊訂閱者,同時調用dep.notify()
時要從新執行update
函數,因此Dep
中必須持有update
引用,這裏使用變量activeUpdate
表示包裹update的函數。
實現代碼以下。
let activeUpdate = null function autorun (update) { const wrappedUpdate = () => { activeUpdate = wrappedUpdate // 引用賦值給activeUpdate update() // 調用update,即調用內部的dep.depend activeUpdate = null // 綁定成功以後清除引用 } wrappedUpdate() // 調用 }
wrappedUpdate
本質是一個閉包,update
函數內部能夠獲取到activeUpdate
變量,同理dep.depend()
內部也能夠獲取到activeUpdate
變量,因此Dep
的實現就很簡單了。
實現代碼以下。
class Dep { // 初始化 constructor () { this.subscribers = new Set() } // 訂閱update函數列表 depend () { if (activeUpdate) { this.subscribers.add(activeUpdate) } } // 全部update函數從新運行 notify () { this.subscribers.forEach(sub => sub()) } }
結合上面兩部分就是完整實現。
要求以下:
convert()
重命名爲觀察者observe()
observe()
轉換對象的屬性使之響應式,對於每一個轉換後的屬性,它會被分配一個Dep
實例,該實例跟蹤訂閱update
函數列表,並在調用setter
時觸發它們從新運行autorun()
接收update
函數做爲參數,並在update
函數訂閱的屬性發生變化時從新運行。示例:
const state = { count: 0 } observe(state) autorun(() => { console.log(state.count) }) // 輸出 count is: 0 state.count++ // 輸出 count is: 1
結合實例1和實例2以後就能夠實現上述要求,observe
中修改obj
屬性的同時分配Dep
的實例,並在get
中註冊訂閱者,在set
中通知改變。autorun
函數保存不變。
實現以下:
class Dep { // 初始化 constructor () { this.subscribers = new Set() } // 訂閱update函數列表 depend () { if (activeUpdate) { this.subscribers.add(activeUpdate) } } // 全部update函數從新運行 notify () { this.subscribers.forEach(sub => sub()) } } function observe (obj) { // 迭代對象的全部屬性 // 並使用Object.defineProperty()轉換成getter/setters Object.keys(obj).forEach(key => { let internalValue = obj[key] // 每一個屬性分配一個Dep實例 const dep = new Dep() Object.defineProperty(obj, key, { // getter負責註冊訂閱者 get () { dep.depend() return internalValue }, // setter負責通知改變 set (newVal) { const changed = internalValue !== newVal internalValue = newVal // 觸發後從新計算 if (changed) { dep.notify() } } }) }) return obj } let activeUpdate = null function autorun (update) { // 包裹update函數到"wrappedUpdate"函數中, // "wrappedUpdate"函數執行時註冊和註銷自身 const wrappedUpdate = () => { activeUpdate = wrappedUpdate update() activeUpdate = null } wrappedUpdate() }
結合Vue文檔裏的流程圖就更加清晰了。
Job Done!!!
本文內容參考自VUE做者尤大的付費視頻
本人Github連接以下,歡迎各位Star
http://github.com/yygmind/blog
我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!
若是你想加羣討論每期面試知識點,公衆號回覆[加羣]便可