最近在看一些底層方面的知識。因此想作個系列嘗試去聊聊這些比較複雜又很重要的知識點。學習就比如是座大山,只有本身去爬山,才能看到不同的風景,體會更加深入。今天咱們就來聊聊Vue中比較重要的響應式原理以及依賴收集。html
Object.defineProperty() 和 Proxy 對象,均可以用來對數據的劫持操做。何爲數據劫持呢?就是在咱們訪問或者修改某個對象的某個屬性的時候,經過一段代碼進行攔截,而後進行額外的操做,返回結果。vue中雙向數據綁定就是一個典型的應用。vue
Vue2.x 是使用 Object.defindProperty(),來實現對對象的監聽。react
Vue3.x 版本以後就改用Proxy實現。數組
在MDN中是這樣定義:bash
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。閉包
Object.defineProperty(obj, prop, descriptor)mvvm
const data = {}
const name = 'zhangsan'
Object.defineProperty(data, 'name', {
writable: true,
configurable: true,
get: function () {
console.log('get')
return name
},
set: function (newVal) {
console.log('set')
name = newVal
}
})
複製代碼
當把一個普通的 JavaScript 對象傳入 Vue 實例做爲 data 選項,Vue 將遍歷此對象全部的 property,並使用 Object.defineProperty 把這些 property 所有轉爲 getter/setter。簡單理解就是在data和用戶之間作了一層代理中間層,在vue initData的時候,將_data上面的數據代理到vm上,經過observer類將全部的data變成可觀察的,及對data定義的每個屬性進行getter\setter操做,這就是Vue實現響應式的基礎。ide
Vue數據響應式變化主要涉及 Observer, Watcher , Dep 這三個主要的類。所以要弄清Vue響應式變化須要明白這個三個類之間是如何運做聯繫的;以及它們的原理,負責的邏輯操做。Observer類是將每一個目標對象(即data)的鍵值轉換成getter/setter形式,用於進行依賴收集以及調度更新。那麼在vue這個類是如何實現的:函數
// 監聽對象屬性Observer類
class Observer {
constructor(value) {
this.value = value
if (!value || (typeof value !== 'object')) {
return
} else {
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
複製代碼
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return val
},
set: function reactiveSetter(newVal) {
// 注意:value一直在閉包中,此處設置完以後,再get時也是會獲得最新的值
if (newVal === val) return
updateView()
}
})
}
function updateView() {
console.log('視圖更新了')
}
const data = {
name: 'zhangsan',
age: 20
}
new Observer(data)
data.name = 'lisi' // 打印‘視圖更新了’
複製代碼
這就是簡單的一個Observer類,這也是vue響應式的基本原理。但咱們都知道 object.defineproperty的存在一些缺點:性能
一、對於複雜的對象須要深度監聽,遞歸到底,一次性計算量大
二、沒法監聽新增屬性/刪除屬性(Vue.set Vue.delete)
三、沒法監聽數組,需特殊處理,也就是上面說的變異方法
這也就是vue3改進的一方面,後文咱們也會着重講解vue3 proxy如何作響應式的。
上圖中咱們看到data中的一級目錄name、age在值改變的時候,會出發視圖更新,但在咱們實際開發過程當中,data可能會是比較複雜的對象,嵌套了好幾層:
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京'
}
}
data.info.address = '上海' // 並無執行。
複製代碼
形成這種緣由是,代碼中defineReactive接收到的val是一個對象,爲了不這種複雜的對象vue採用遞歸的思想在defineReactive函數中在執行一次observer函數就行,遞歸將對象在遍歷一次獲取key/value值,new Observer(val)。一樣在設置值的時候可能會把name也設置成一個對象,所以在data值更新的時候也須要進行判斷深度監聽
function defineReactive(obj, key, val) {
new Observer(val) // 深度監聽
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return val
},
set: function reactiveSetter(newVal) {
// 注意:value一直在閉包中,此處設置完以後,再get時也是會獲得最新的值
if (newVal === val) return
new Observer(val) // 深度監聽
updateView()
}
})
}
複製代碼
object.defineproperty對數組是不起做用的,那麼在vue中又是如何去監聽數組的變化,其實Vue 將被偵聽的數組的變動方法進行了包裹。接下來將用簡單代碼演示:
// 防止全局污染,從新定義數組原型
const oldArrayProperty = Array.prototype
// 建立新對象,原型指向oldArrayProperty
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () { // 在定義數組的方法
updateView()
oldArrayProperty[methodName].call(this, ...arguments) // 實際執行數組的方法
}
})
// 在Observer函數中對數組進行處理
if (Array.isArray(value)) {
value.__proto__ = arrProto
}
複製代碼
從代碼中看到,在Observer函數有一層對數組進行攔截,將數組的__proto__指向了一個arrProto,arrProto是一個對象,這個對象指向數組的原型,所以arrProto擁有了數組原型上的方法,而後在這對象上從新自定義了數組的7中方法將其包裹,但又不會影響數組原型的方法,這就是變異,再將數組的每一個成員進行observe,使之成響應式數據。
咱們如今有這麼一個Vue對象
new Vue({
template:
`<div>
<span>text1:</span> {{text1}}
<div>`,
data: {
text1: 'text1',
text2: 'text2'
}
})
複製代碼
咱們能夠從以上代碼看出,data中text2並無被模板實際用到,爲了提升代碼執行效率,咱們沒有必要對其進行響應式處理,所以,依賴收集簡單理解就是收集只在實際頁面中用到的data數據,那麼Vue是如何進行依賴收集的,這也就是下面要講的Watcher、Dep類了。
被Observer的data在觸發 getter 時,Dep 就會收集依賴,而後打上標記,這裏就是標記爲Dep.target
Watcher是一個觀察者對象。依賴收集之後的watcher對象被保存在Dep的subs中,數據變更的時候Dep會通知watcher實例,而後由watcher實例回調cb進行視圖更新。
Watcher能夠接受多個訂閱者的訂閱,當有data變更時,就會經過 Dep 給 Watcher 發通知進行更新。
咱們能夠用一些簡單的代碼去實現這個過程。
class Observer {
constructor(value) {
this.value = value
if (!value || (typeof value !== 'object')) {
return
} else {
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
// 訂閱者Dep,存放觀察者對象
class Dep {
constructor() {
this.subs = []
}
/*添加一個觀察者對象*/
addSub (sub) {
this.subs.push(sub)
}
/*依賴收集,當存在Dep.target的時候添加觀察者對象*/
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知全部watcher對象更新視圖
notify () {
this.subs.forEach((sub) => {
sub.update()
})
}
}
class Watcher {
constructor() {
/* 在new一個Watcher對象時將該對象賦值給Dep.target,在get中會用到 */
Dep.target = this;
}
update () {
console.log('視圖更新啦')
}
/*添加一個依賴關係到Deps集合中*/
addDep (dep) {
dep.addSub(this)
}
}
function defineReactive (obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
dep.depend() /*進行依賴收集*/
return val
},
set: function reactiveSetter (newVal) {
if (newVal === val) return
dep.notify()
}
})
}
class Vue {
constructor (options) {
this._data = options.data
new Observer(this._data) // 全部data變成可觀察的
new Watcher() // 建立一個觀察者實例
console.log('render~', this._data.test)
}
}
let o = new Vue({
data: {
test: 'hello vue.'
}
})
o._data.test = 'hello mvvm!'
Dep.target = null
複製代碼
總結
Proxy能夠理解成在目標對象前架設一個攔截層,外界對該對象的出發必須先經過這層攔截層,所以提供了一種機制能夠對外界的訪問進行過濾和改寫。
function reactive(value = {}) {
if (!value || (typeof value !== 'object')) {
return
}
// 代理配置
const proxyConf = {
get(target, key,receiver) {
// 只處理非原型的屬性
let ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key)
}
const result = Reflect.get(target, key, receiver)
// 深度監聽
// 性能如何提高? 何時用何時遞歸
return reactive(result)
},
set(target, key, val, receiver) {
// 重複的數據不處理
const oldVal = target[key]
if (val === oldVal) return true
const ownKey = Reflect.ownKeys(target)
if (ownKeys.include(key)) {
console.log('已有的key', key)
} else {
console.log('新增的key', key)
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
return result
}
}
// 生成代理對象
const observed = new Proxy(value, proxyConf)
return observed
}
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京'
},
num: [1, 2, 3]
}
const proxyData = reactive(data)
proxyData.name ='lisi' // set name lisi
複製代碼
proxy深度監聽的性能提高,在proxy中對於複雜的對象,只會geter()的時候對當前層的監聽,好比說在info中
info: {
address: '北京',
a: {
b: {
c: {
d: 2
}
}
}
}
複製代碼
修改proxyData.info.a並不會把後面b、c、d遞歸出來,避免了object.defineProperty一次性所有遞歸計算完成。因爲proxy原生對數組就能監聽,因此也是對object.defineProperty缺點的一個改進。而且從代碼中能夠看出,在增長/刪除時proxy也同樣能夠監聽到,這就是proxy的優點。
reflect對象的方法和proxy對象的方法一一對應,只要是proxy對象的方法,就能在reflect對象找到對應的方法。這就使得proxy對象能夠方便的調用對應的reflect方法來完成默認的行爲,做爲修改行爲的基礎。
Reflect有實際上是對Object對象的規範化吧,將Object對象的一些明顯屬於語言內部的方法(好比Object.defineProperty)放到Reflect對象上。
Reflect.get(target, name, receiver): 查到並返回target對象上的name屬性,沒有該屬性會返回undefined
Reflect.set(target, name, value, receiver): 設置target對象的name屬性等於value
Reflect.has(object, name): 判斷對象上是否有name屬性
Reflect.ownKeys(target): 返回對象的全部屬性
// 觀察者模式指的是函數自動觀察數據對象的模式,一旦數據有變化,數據就會自動執行
const queuedObservers = new Set()
const observe = fn => queuedObservers.add(fn)
const observable = obj => new Proxy(obj, {set})
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
queuedObservers.forEach(observe => observe())
return result
}
const person = observable({ // 觀察對象
name: '張三',
age: 20
})
function print() { // 觀察者
console.log(`${person.name}, ${person.age}`)
}
observe(print)
person.name = '李四'
複製代碼