Vue
響應式解析一個vue
的小demo
javascript
<template> <div>price: {{price}}</div> <div>total: {{price * quantity}}</div> <div>totalPriceWithSale: {{totalPriceWithSale}}</div> </template> <script> var vue = new Vue({ el: '#app', data: { price: 5, quantity: 10, sum: 0 }, computed: { totalPriceWithSale() { return this.price * 0.8 } }, watch: { price (val) { this.sum += val } } }) </script>
在vue
中當改變data
中的price
, quantity
, sum
中的值,其依賴這三個字段的地方就會觸發更新,這就是響應式,那麼vue
具體是怎麼實現的呢?vue
因爲vue
中的涉及的功能較多,因此咱們先脫離vue
,從最簡單的開始瞭解java
> let price = 5 > let quantity = 10 > let sum = 0 > let totalPriceWithSale = price * 0.8 > let total = price * quantity > totalPriceWithSale 4 > total 50 > price = 10 // 修改 price 的值爲10 10 > totalPriceWithSale // 未改變 4 > total // 未改變 50
將vue
中的代碼改寫成上面最原始的樣子,當咱們修改price
的值爲10
,打印其依賴price
的變量totalPriceWithSale
與total
發現其值並無改變,然而,在響應式中當price
改變其依賴該屬性的也會發生改變,那進一步優化一下吧, 將每次值更新須要從新執行的代碼放在一個函數中,而後放在一個數組中保存起來,值更新後就從新執行函數react
let price = 5 let quantity = 10 let sum = 0 let total = 0 let target = null // 當前須要執行的依賴函數 let storage = [] target = () => { total = price * quantity } function record() { storage.push(target) } record() // 保存值更新時須要執行的函數 target() // 初始化執行設置 total 值 // 遍歷執行函數 function replay() { storage.forEach(run => run()) } price = 20 console.log(total) // 50 replay() // 執行依賴函數 console.log(total) // 200:值更新
上面的代碼中,price
的值更新後執行replay
函數,其依賴price
的total
變量的值就進行了更新。還能夠將上面的代碼中對依賴的收集以及執行使用一個類來進行優化git
class Dep{ constructor() { this.subs = [] } depend() { if (target && !this.subs.includes(target)) { this.susb.push(target) } } notify() { this.subs.forEach(run => run()) } }
使用上面的類對以上的代碼來進行優化github
let price = 5 let quantity = 10 let sum = 10 let total = 0 const dep = new Dep() let target = null target = () => { total = price * quantity } dep.depend() // 收集依賴 target() console.log(total) // 50 price = 10 // 修改 price 的值 dep.notify() // 更新其依賴 price 的值 console.log(total) // 100
在vue
中對data
中每個屬性都進行了Dep
的實例,用來保存對其的依賴,在依賴的屬性改變的時候更新值.如今對依賴收集進行了優化,那麼可否對須要target
進行優化使其複用呢數組
target = () => { total = price * quantity } dep.depend() // 收集依賴 target()
上面的這段代碼,若是監聽依賴price
的另外一個變量,則須要進行重寫,不能友好的複用app
watcher(() => { total = price * quantity }) watcher(() => { sum = price + quantity }) function wathcer(myFun) { target = myFun dep.depend() target() target = null } console.log(total, sum) // 50 15 price = 10 dep.notify() console.log(total, sum) // 100 20
使用watcher
可以很好的進行復用,在watcher
中收集依賴函數,添加依賴。那麼又有一個問題,每次修改值都須要手動的去執行dep.notify()
函數,能不能當值改變的時候自動執行notify
函數呢。在vue
中,數據是存放在data
屬性中,該屬性返回的是一個對象,能夠對對象中的每個屬性進行監聽,當獲取或者設置值的時候調用相關的函數函數
let price = 5 let quantity = 10 let sum = 10 let total = 0 let salePrice = 0 // const dep = new Dep() let target = null let data = { price: 5, quantity: 10 } Object.keys(data).forEach(ele => { const dep = new Dep() let interVal = data[ele] Object.defineProperty(data, ele, { get: () => { dep.depend() console.log(ele, dep.subs) // 其中的屬性以及subs, price的subs中有3個函數,quantity只有兩個 // price, Array(1) [] // quantity, Array(1) [] // price, Array(2) [, ] // quantity, Array(2) [, ] // price, Array(3) [, , ] // price, Array(3) [, , ] // quantity, Array(2) [, ] return interVal }, set: (val) => { interVal = val dep.notify() } }) }) function watcher(myFun) { target = myFun target() target = null } watcher(() => { total = data.price * data.quantity }) watcher(() => { sum = data.price + data.quantity }) watcher(() => { salePrice = data.price * 0.7 })
上面就是響應式的基本原理,如今data
中的值都爲基本的類型值,很容易處理,若是其屬性值仍然爲對象呢?性能
值仍然爲對象時,那麼須要進行遞歸監聽,將Object.defineProperty
的監聽操做進行相應的封裝,使用observe
函數進行值額類型判斷
若是其值爲對象,則對其中的每一個屬性設置getter
與setter
, 而且在設置getter
與setter
以前判斷其值的類型,值爲對象則繼續添加getter
與setter
let data = { // price: 5, // quantity: 10, obj: { a: 1, b: 2, c: { d: 3 } } } function isObject(val) { return Object.prototype.toString.call(val) === '[object Object]' } function observe(val) { if (isObject(val)) { const keys = Object.keys(val) keys.forEach(key => { defineProperty(val, key) }) } } function defineProperty(obj, key) { const dep = new Dep() let internalVal = obj[key] observe(internalVal) Object.defineProperty(obj, key, { get: function () { // TODO 外層對象改變,內部對象也應該更新依賴 dep.depend() // target 函數爲 target = () => { console.log('render obj', data.obj, data.obj.a, data.obj.b, data.obj.c, data.obj.c.d) } // obj 被訪問5次,向其中添加了5次 watcher 中的 target 函數 // obj 中的 c 屬性,被訪問兩次,則其對應的 subs 中有兩個 watcher 中的 target 函數 // 爲了不向 subs 中添加劇復的 target 函數,須要判斷是否存在 this.subs.includes(target) console.log(`get key: ${key} dep: ${dep} subs: ${dep.subs}`) return internalVal }, set: function(val) { internalVal = val dep.notify() } }) } observe(data) function watcher(myFun) { target = myFun target() target = null } watcher(() => { total = data.obj.a + data.obj.b + data.obj.c.d // console.log('render obj', data.obj, data.obj.a, data.obj.b, data.obj.c, data.obj.c.d) }) // 修改其中的每個屬性的值,total 的值 total // 6 data.obj.a = 2 total // 7 data.obj.b = 3 total // 8 data.obj.c.d = 4 total // 9
上面對值爲對象時進行了循環遞歸添加,那麼當值爲數組時怎麼處理呢?
let data = { arr: [1, 2, 3, {a: 4}, [5, 6]] } function observer(val) { if (isObject(val)) { const keys = Object.keys(val) keys.forEach(key => defineProperty(val, key)) } else if(Array.isArray(val)) { // 對數組中的每一項綁定 val.forEach(ele => defineProperty(val,)) } }
上面的代碼中對數組中的每一項都會進行監聽,而後在數組的每一項改變的時候都會進行依賴更新,因爲數組的數據量可能會很大,這樣會比較影響性能(我暫時是這麼認爲的)
將上面的代碼優化一下,當數組的中的值是基本類型時,不進行監聽,引用類型是再進行監聽
let data = { arr: [1, 2, 3, [4, 5], {a: 7} } function observer(val) { if (isObject(val) || Array.isArray(val)) { addObserver(val) } } function addObserver(val) { if(isObject(val)) { const keys = Object.keys(val) keys.forEach(key => defineProperty(val, key)) } else if(Array.isArray(val)) { val.forEach(ele => { observer(ele) }) } } watcher(() => { total = data.arr[0] + data.arr[1] + data.arr[2] + data.arr[3][0] + data.arr[3][1] + data.arr[4].a }) total // 22 data.arr[0] = 2 // 修改其中某個依賴的值 total // 22 未更新 data.arr[3][0] = 5 // 修改數組中的數組中的值 total // 22 未更新 data.arr[4].a = 8 // 修改數組中的對象的某個屬性值 total // 25 更新
上面的方法沒有監聽數組中值爲基本類型的值,但出現了一個問題,數組內嵌套數組不會被監聽到,這也就是在vue
中直接修改數組內的值或者數組中嵌套的數組中的值不會觸發視圖更新的緣由,可是vue
給咱們提供了一些改變數組值的方法能夠觸發更新或者$set
方法
那麼vue
中是怎麼作到能夠使用數組的方法修改數組以此來觸發視圖更新的呢?
// 用Array原型中的方法建立一個新對象 const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) // 須要進行重寫的方法 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(ele => { const original = arrayProto[ele] Object.defineProperty(arrayMethods, ele, { value: function(...args) { const result = original.apply(this, args) console.log(`調用 ${ele} 方法`) return result }, enumerable: false, writable: true, configurable: true }) }) function protoAugment(target, proto) { targte.__proto__ = proto } function addObserver(val) { if (isObject(val)) { const keys = Object.keys(val) keys.forEach(key => defineProperty(val, key)) } else if(Array.isArray(val)) { protoAugment(val, arrayMethods) val.forEach(ele => observe(ele)) } }
上面的代碼執行後,調用data.arr
數組的push
, pop
, shift
, unshift
, splice
, sort
, reverse
都會打印出對應的提示,這樣在調用相應的方法時均可以進行依賴分發。那麼,問題又來了,怎麼能夠拿到依賴的dep
呢?當咱們調用這些方法時,是經過data.arr.push(3)
這樣的方式進行調用的,那麼在push
方法內部就能夠經過this
拿到data.arr
這個數組,因此咱們能夠在這個數組中保存所須要的依賴dep
,而後進行分發便可
let data = { arr: [1, 2, 3] } function observe(val) { if (isObject(val) || Array.isArray(val)) { return addObserver(val) } } function addObserver(val) { const dep = new Dep() Object.defineProperty(val, '__dep__', { value: dep, enumerable: false, writable: true, configurable: true }) if (isObject(val)) { const keys = Object.keys(val) keys.forEach(key => defineProperty(val, key)) } else if(Array.isArray(val)) { val.forEach(ele => observe(ele)) } return val } function defineProperty(val, key) { const dep = new Dep() const internalVal = val[key] const childOb = observe(internalVal) // 獲取到值爲引用類型的 __dep__ Object.defineProperty(val, key, { get: function() { dep.depend() if (childOb) { childOb.__dep__.depend() // 添加依賴,當調用 push 等方法時進行依賴分發 } }, set: function(newVal) { internalVal = newVal dep.notify() } }) } methodsToPatch.forEach(ele => { const original = arrayProto[ele] Object.defineProperty(arrayMethods, ele, { value: function(...args) { const result = original.apply(this, args) const dep = this.__dep__ console.log(`調用 ${ele} 方法`) dep.notity() return result }, enumerable: false, writable: true, configurable: true }) }) total // 6 data.arr.push(4) total // 10 值進行了更新
以上內容爲本人理解,若有不對處,請指正
倉庫地址 歡迎 star
參考: