vue 響應式解析

Vue響應式解析

一個vue的小demojavascript

<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的變量totalPriceWithSaletotal發現其值並無改變,然而,在響應式中當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函數,其依賴pricetotal變量的值就進行了更新。還能夠將上面的代碼中對依賴的收集以及執行使用一個類來進行優化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函數進行值額類型判斷
若是其值爲對象,則對其中的每一個屬性設置gettersetter, 而且在設置gettersetter以前判斷其值的類型,值爲對象則繼續添加gettersetter

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

參考:

相關文章
相關標籤/搜索