日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

前言

為什么前端框架Vue能夠做到響應式?當依賴數據發生變化時,會對頁面進行自動更新,其原理還是在于對響應式數據的獲取和設置進行了監聽,一旦監聽到數據發生變化,依賴該數據的函數就會重新執行,達到更新的效果。那么我們如果想監聽對象中的屬性被設置和獲取的過程,可以怎么做呢?

1.Object.defineProperty

在ES6之前,如果想監聽對象屬性的獲取和設置,可以借助Object.defineProperty方法的存取屬性描述符來實現,具體怎么用呢?我們來看一下。

const obj = {
  name: 'curry',
  age: 30
}

// 1.拿到obj所有的key
const keys = Object.keys(obj)

// 2.遍歷obj所有的key,并設置存取屬性描述符
keys.forEach(key => {
  let value = obj[key]

  Object.defineProperty(obj, key, {
    get: function() {
      console.log(`obj對象的${key}屬性被訪問啦!`)
      return value
    },
    set: function(newValue) {
      console.log(`obj對象的${key}屬性被設置啦!`)
      value = newValue
    }
  })
})

// 設置:
obj.name = 'kobe' // obj對象的name屬性被設置啦!
obj.age = 24 // obj對象的age屬性被設置啦!
// 訪問:
console.log(obj.name) // obj對象的name屬性被訪問啦!
console.log(obj.age) // obj對象的age屬性被訪問啦!

在Vue2.x中響應式原理實現的核心就是使用的Object.defineProperty,而在Vue3.x中響應式原理的核心被換成了Proxy,為什么要這樣做呢?主要是Object.defineProperty用來監聽對象屬性變化,有以下缺點:

  • 首先,Object.defineProperty設計的初衷就不是為了去監聽對象屬性的,因為它的主要使用功能就是用來定義對象屬性的;
  • 其次,Object.defineProperty在監聽對象屬性功能上有所缺陷,如果想監聽對象新增屬性、刪除屬性等等,它是無能為力的;

2.Proxy

在ES6中,新增了一個Proxy類,翻譯為代理,它可用于幫助我們創建一個代理對象,之后我們可以在這個代理對象上進行許多的操作。

2.1.Proxy的基本使用

如果希望監聽一個對象的相關操作,當Object.defineProperty不能滿足我們的需求時,那么可以使用Proxy創建一個代理對象,在代理對象上,我們可以監聽對原對象進行了哪些操作。下面將上面的例子用Proxy來實現,看看效果。

基本語法:const p = new Proxy(target, handler)

  • target:需要代理的目標對象;
  • handler:定義的各種操作代理對象的行為(也稱為捕獲器);
const obj = {
  name: 'curry',
  age: 30
}

// 創建obj的代理對象
const objProxy = new Proxy(obj, {
  // 獲取對象屬性值的捕獲器
  get: function(target, key) {
    console.log(`obj對象的${key}屬性被訪問啦!`)
    return target[key]
  },
  // 設置對象屬性值的捕獲器
  set: function(target, key, newValue) {
    console.log(`obj對象的${key}屬性被設置啦!`)
    target[key] = newValue
  }
})

// 之后的操作都是拿代理對象objProxy
// 設置:
objProxy.name = 'kobe' // obj對象的name屬性被設置啦!
objProxy.age = 24 // obj對象的age屬性被設置啦!
// 訪問:
console.log(objProxy.name) // obj對象的name屬性被訪問啦!
console.log(objProxy.age) // obj對象的age屬性被訪問啦!
// 可以發現原對象obj同時發生了改變
console.log(obj) // { name: 'kobe', age: 24 }

2.2.Proxy的set和get捕獲器

在上面的例子中,其實已經使用到了set和get捕獲器,而set和get捕獲器是最為常用的捕獲器,下面具體來看看這兩個捕獲器吧。

(1)set捕獲器

set函數可接收四個參數:

  • target:目標對象(被代理對象);
  • property:將被設置的屬性key;
  • value:設置的新屬性值;
  • receiver:調用的代理對象;

(2)get捕獲器

get函數可接收三個參數:

  • target:目標對象;
  • property:被獲取的屬性key;
  • receiver:調用的代理對象;

2.3.Proxy的Apply和construct捕獲器

上面所講的都是對對象屬性的操作進行監聽,其實Proxy提供了更為強大的功能,可以幫助我們監聽函數的調用方式。

  • apply:監聽函數是否使用apply方式調用。
  • construct:監聽函數是否使用new操作符調用。
function fn(x, y) {
  return x + y
}

const fnProxy = new Proxy(fn, {
  /*
    target: 目標函數(fn)
    thisArg: 指定的this對象,也就是被調用時的上下文對象({ name: 'curry' })
    argumentsList: 被調用時傳遞的參數列表([1, 2])
  */
  apply: function(target, thisArg, argumentsList) {
    console.log('fn函數使用apply進行了調用')
    return target.apply(thisArg, argumentsList)
  },
  /*
    target: 目標函數(fn)
    argumentsList: 被調用時傳遞的參數列表
    newTarget: 最初被調用的構造函數(fnProxy)
  */
  construct: function(target, argumentsList, newTarget) {
    console.log('fn函數使用new進行了調用')
    return new target(...argumentsList)
  }
})

fnProxy.apply({ name: 'curry' }, [1, 2]) // fn函數使用apply進行了調用
new fnProxy() // fn函數使用new進行了調用

2.4.Proxy所有的捕獲器

除了上面提到的4種捕獲器,Proxy還給我們提供了其它9種捕獲器,一共是13個捕獲器,下面對這13個捕獲器進行簡單總結,下面表格的捕獲器分別對應對象上的一些操作方法。

捕獲器handler

捕獲對象

get()

屬性讀取操作

set()

屬性設置操作

has()

in操作符

deleteProperty()

delete操作符

apply()

函數調用操作

construct()

new操作符

getPrototypeOf()

Object.getPrototypeOf()

setPrototypeOf()

Object.setPrototypeOf()

isExtensible()

Object.isExtensible()

preventExtensions()

Object.perventExtensions()

getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor()

defineProperty()

Object.defineProperty()

ownKeys()

Object.getOwnPropertySymbols()

Proxy捕獲器具體用法可查閱MDN:https://developer.mozilla.org/zh-CN/docs/Web/JAVAScript/Reference/Global_Objects/Proxy

3.Reflect

在ES6中,還新增了一個API為Reflect,翻譯為反射,為一個內置對象,一般用于搭配Proxy進行使用。

3.1.Reflect有什么作用呢?

可能會有人疑惑,為什么在這里提到Reflect,它具體有什么作用呢?怎么搭配Proxy進行使用呢?

  • Reflect上提供了很多操作JavaScript對象的方法,類似于Object上操作對象的方法;
  • 比如:Reflect.getPrototypeOf()類似于Object.getPrototypeOf(),Reflect.defineProperty()類似于Object.defineProperty();
  • 既然Object已經提供了這些方法,為什么還提出Reflect這個API呢?這里涉及到早期ECMA規范問題,Object本是作為一個構造函數用于創建對象,然而卻將這么多方法放到Object上,本就是不合適的;所以,ES6為了讓Object職責單一化,新增了Reflect,將Object上這些操作對象的方法添加到Reflect上,且Reflect不能作為構造函數進行new調用

3.2.Reflect的基本使用

在上述Proxy中,操作對象的方法都可以換成對應的Reflect上的方法,基本使用如下:

const obj = {
  name: 'curry',
  age: 30
}

// 創建obj的代理對象
const objProxy = new Proxy(obj, {
  // 獲取對象屬性值的捕獲器
  get: function(target, key) {
    console.log(`obj對象的${key}屬性被訪問啦!`)
    return Reflect.get(target, key)
  },
  // 設置對象屬性值的捕獲器
  set: function(target, key, newValue) {
    console.log(`obj對象的${key}屬性被設置啦!`)
    Reflect.set(target, key, newValue)
  },
  // 刪除對象屬性的捕獲器
  deleteProperty: function(target, key) {
    console.log(`obj對象的${key}屬性被刪除啦!`)
    Reflect.deleteProperty(target, key)
  }
})

// 設置:
objProxy.name = 'kobe' // obj對象的name屬性被設置啦!
objProxy.age = 24 // obj對象的age屬性被設置啦!
// 訪問:
console.log(objProxy.name) // obj對象的name屬性被訪問啦!
console.log(objProxy.age) // obj對象的age屬性被訪問啦!
// 刪除:
delete objProxy.name // obj對象的name屬性被刪除啦!

3.3.Reflect上常見的方法

對比Object,我們來看一下Reflect上常見的操作對象的方法(靜態方法):

Reflect方法

類似于

get(target, propertyKey [, receiver])

獲取對象某個屬性值,target[name]

set(target, propertyKey, value [, receiver])

將值分配給屬性的函數,返回一個boolean

has(target, propertyKey)

判斷一個對象是否存在某個屬性,和in運算符功能相同

deleteProperty(target, propertyKey)

delete操作符,相當于執行delete target[name]

apply(target, thisArgument, argumentsList)

對一個函數進行調用操作,可以傳入一個數組作為調用參數,Function.prototype.apply()

construct(target, argumentsList [, newTarget])

對構造函數進行new操作,new target(...args)

getPrototypeOf(target)

Object.getPrototype()

setPrototypeOf(target, prototype)

設置對象原型的函數,返回一個boolean

isExtensible(target)

Object.isExtensible()

preventExtensions(target)

Object.preventExtensions(),返回一個boolean

getOwnPropertyDescriptor(target, propertyKey)


Object.getOwnPropertyDescriptor(),如果對象中存在該屬性,則返回對應屬性描述符,否則返回undefined

defineProperty(target, propertyKey, attributes)

Object.defineProperty(),設置成功返回true

ownKeys(target)

返回一個包含所有自身屬性(不包含繼承屬性)的數組,類似于Object.keys(),但是不會受enumerable影響

具體Reflect和Object對象之間的關系和使用方法,可以參考MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

3.4.Reflect的construct方法

construct方法有什么作用呢?具體的應用場景是什么?這里提一個需求,就明白construct方法的作用了。

需求:創建Person和Student兩個構造函數,最終的實例對象執行的是Person中的代碼,帶上實例對象的類型是Student。

construct可接收的參數:

  • target:被運行的目標構造函數(Person);
  • argumentsList:類數組對象,參數列表;
  • newTarget:作為新創建對象原型對象的constructor屬性(Student);
function Person(name, age) {
  this.name = name
  this.age = age
}

function Student() {}

const stu = Reflect.construct(Person, ['curry', 30], Student)
console.log(stu)
console.log(stu.__proto__ === Student.prototype)

打印結果:實例對象的類型為Student,并且實例對象原型指向Student構造函數的原型。

由淺入深,帶你用JavaScript實現響應式原理

 

Reflect的construct方法就可以用于類繼承的實現,可在babel工具中查看ES6轉ES5后的代碼,就是使用的Reflect的construct方法:

由淺入深,帶你用JavaScript實現響應式原理

 

4.receiver的作用

在介紹Proxy的set和get捕獲器的時候,其中有個參數叫receiver,具體什么是調用的代理對象呢?它的作用是什么?

如果原對象(需要被代理的對象)它有自己的getter和setter服務器屬性時,那么就可以通過receiver來改變里面的this。

// 假設obj的age為私有屬性,需要通過getter和setter來訪問和設置
const obj = {
  name: 'curry',
  _age: 30,
  get age() {
    return this._age
  },
  set age(newValue) {
    this._age = newValue
  }
}

const objProxy = new Proxy(obj, {
  get: function(target, key, reveiver) {
    console.log(`obj對象的${key}屬性被訪問啦!`)
    return Reflect.get(target, key)
  },
  set: function(target, key, newValue, reveiver) {
    console.log(`obj對象的${key}屬性被設置啦!`)
    Reflect.set(target, key, newValue)
  }
})

// 設置:
objProxy.name = 'kobe'
objProxy.age = 24
// 訪問:
console.log(objProxy.name)
console.log(objProxy.age)

在沒有使用receiver的情況下的打印結果為:name和age屬性都被訪問一次和設置一次。

由淺入深,帶你用JavaScript實現響應式原理

 

但是由于原對象obj中對age進行了攔截操作,我們看一下age具體的訪問步驟

  • 首先,打印objProxy.age會被代理對象objProxy中的get捕獲器所捕獲;
  • 緊接著Reflect.get(target, key)對obj中的age進行了訪問,又會被obj中的get訪問器所攔截,返回this._age;
  • 很顯然在執行this._age的時候_age在這里是被訪問了的,而這里的this指向的原對象obj;
  • 一般地,通過this._age的時候,應該也是要被代理對象的get捕獲器所捕獲的,那么就需要將這里的this修改成objProxy,相當于objProxy._age,在代理對象objProxy中就可以被get捕獲到了;
  • receiver的作用就在這里,把原對象中this改成其代理對象,同理age被設置也是一樣的,訪問和設置信息都需要被打印兩次;
// 假設obj的age為私有屬性,需要通過getter和setter來訪問和設置
const obj = {
  name: 'curry',
  _age: 30,
  get age() {
    return this._age
  },
  set age(newValue) {
    this._age = newValue
  }
}

const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    console.log(`obj對象的${key}屬性被訪問啦!`)
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    console.log(`obj對象的${key}屬性被設置啦!`)
    Reflect.set(target, key, newValue, receiver)
  }
})

// 設置:
objProxy.name = 'kobe'
objProxy.age = 24
// 訪問:
console.log(objProxy.name)
console.log(objProxy.age)

再來看一下打印結果:

由淺入深,帶你用JavaScript實現響應式原理

 

也可以打印receiver,在瀏覽器中進行查看,其實就是這里的objProxy:

由淺入深,帶你用JavaScript實現響應式原理

 

5.響應式原理的實現

5.1.什么是響應式呢?

當某個變量值發生變化時,會自動去執行某一些代碼。如下代碼,當變量num發生變化時,對num有所依賴的代碼可以自動執行。

let num = 30

console.log(num) // 當num方式變化時,這段代碼能自動執行
console.log(num * 30) // 當num方式變化時,這段代碼能自動執行

num = 1
  • 像上面這一種自動響應數據變化的代碼機制,就稱之為響應式;
  • 在開發中,一般都是監聽某一個對象中屬性的變化,然后自動去執行某一些代碼塊,而這些代碼塊一般都存放在一個函數中,因為函數可以方便我們再次執行這些代碼,只需再次調用函數即可;

5.2.收集響應式函數的實現

在響應式中,需要執行的代碼可能不止一行,而且也不可能一行行去執行,所以可以將這些代碼放到一個函數中,當數據發生變化,自動去執行某一個函數。但是在開發中有那么多函數,怎么判斷哪些函數需要響應式?哪些又不需要呢?

  • 封裝一個watchFn的函數,將需要響應式的函數傳入;
  • watchFn的主要職責就是將這些需要響應式的函數收集起來,存放到一個數組reactiveFns中;
const obj = {
  name: 'curry',
  age: 30
}

// 定義一個存放響應式函數的數組
const reactiveFns = []
// 封裝一個用于收集響應式函數的函數
function watchFn(fn) {
  reactiveFns.push(fn)
}

watchFn(function() {
  let newName = obj.name
  console.log(newName)
  console.log('1:' + obj.name)
})

watchFn(function() {
  console.log('2:' + obj.name)
})

obj.name = 'kobe'
// 當obj中的屬性值發送變化時,遍歷執行那些收集的響應式函數
reactiveFns.forEach(fn => {
  fn()
})
由淺入深,帶你用JavaScript實現響應式原理

 

5.3.收集響應式函數的優化

上面實現的收集響應式函數,目前是存放到一個數組中來保存的,而且只是對name屬性的的依賴進行了收集,如果age屬性也需要收集,不可能都存放到一個數組里面,而且屬性值改變后,還需要通過手動去遍歷調用,顯而易見是很麻煩的,下面做一些優化。

  • 封裝一個類,專門用于收集這些響應式函數;
  • 類中添加一個notify的方法,用于遍歷調用這些響應式函數;
  • 對于不同的屬性,就分別去實例化這個類,那么每個屬性就可以對應一個對象,并且對象中有一個存放它的響應式數組的屬性reactiveFns;
class Depend {
  constructor() {
    // 用于存放響應式函數
    this.reactiveFns = []
  }

  // 用戶添加響應式函數
  addDependFn(fn) {
    this.reactiveFns.push(fn)
  }

  // 用于執行響應式函數
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

const obj = {
  name: 'curry',
  age: 30
}

const dep = new Depend()
// 在watchFn中使用dep的addDependFn來收集
function watchFn(fn) {
  dep.addDependFn(fn)
}

watchFn(function() {
  let newName = obj.name
  console.log(newName)
  console.log('1:' + obj.name)
})

watchFn(function() {
  console.log('2:' + obj.name)
})

obj.name = 'kobe'
// name屬性發生改變,直接調用notify
dep.notify()

5.4.自動監聽對象的變化

在修改對象屬性值后,還是需要手動去調用其notify函數來通知響應式函數執行,其實可以做到自動監聽對象屬性的變化,來自動調用notify函數,這個想必就很容易了,在前面做了那么多功課,就是為了這里,不管是用Object.defineProperty還是Proxy都可以實現對象的監聽,這里我使用功能更加強大的Proxy,并結合Reflect來實現。

class Depend {
  constructor() {
    // 用于存放響應式函數
    this.reactiveFns = []
  }

  // 用戶添加響應式函數
  addDependFn(fn) {
    this.reactiveFns.push(fn)
  }

  // 用于執行響應式函數
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

const obj = {
  name: 'curry',
  age: 30
}

const dep = new Depend()
// 在watchFn中使用dep的addDependFn來收集
function watchFn(fn) {
  dep.addDependFn(fn)
}

// 創建一個Proxy
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 當set捕獲器捕獲到屬性變化時,自動去調用notify
    dep.notify()
  }
})

watchFn(function() {
  let newName = objProxy.name
  console.log(newName)
  console.log('1:' + objProxy.name)
})

watchFn(function() {
  console.log('2:' + objProxy.name)
})

objProxy.name = 'kobe'
objProxy.name = 'klay'
objProxy.name = 'james'

注意:后面使用到的obj對象,需都換成代理對象objProxy,這樣儲能監聽到屬性值是否被設置了。

打印結果:name屬性修改了三次,對應依賴函數就執行了三次。

由淺入深,帶你用JavaScript實現響應式原理

 

5.5.對象依賴的管理(數據存儲結構設計)

在上面實現響應式過程中,都是基于一個對象的一個屬性,如果有多個對象,這多個對象中有不同或者相同的屬性呢?我們應該這樣去單獨管理不同對象中每個屬性所對應的依賴呢?應該要做到當某一個對象中的某一個屬性發生變化時,只去執行對這個對象中這個屬性有依賴的函數,下面就來講一下怎樣進行數據存儲,能夠達到我們的期望。

在ES16中,給我們新提供了兩個新特性,分別是Map和WeakMap,這兩個類都可以用于存放數據,類似于對象,存放的是鍵值對,但是Map和WeakMap的key可以存放對象,而且WeakMap對對象的引用是弱引用。如果對這兩個類不太熟悉,可以去看看上一篇文章:ES6-ES12簡單知識點總結

  • 將不同的對象存放到WeakMap中作為key,其value存放對應的Map;
  • Map中存放對應對象的屬性作為key,其value存放對應的依賴對象;
  • 依賴對象中存放有該屬性對應響應式函數數組;

如果有以下obj1和obj2兩個對象,來看一下它們大致的存儲形式:

const obj1 = { name: 'curry', age: 30 }
const obj2 = { name: 'kobe', age: 24 }
由淺入深,帶你用JavaScript實現響應式原理

 

5.6.對象依賴管理的實現

已經確定了怎么存儲了,下面就來實現一下吧。

  • 封裝一個getDepend函數,主要用于根據對象和key,來找到對應的dep;
  • 如果沒有找到就先進行創建存儲;
// 1.創建一個WeakMap存儲結構,存放對象
const objWeakMap = new WeakMap()
// 2.封裝一個獲取dep的函數
function getDepend(obj, key) {
  // 2.1.根據對象,獲取對應的map
  let map = objWeakMap.get(obj)
  // 如果是第一次獲取這個map,那么需要先創建一個map
  if (!map) {
    map = new Map()
    // 將map存到objWeakMap中對應key上
    objWeakMap.set(obj, map)
  }

  // 2.2.根據對象的屬性,獲取對應的dep
  let dep = map.get(key)
  // 如果是第一次獲取這個dep,那么需要先創建一個dep
  if (!dep) {
    dep = new Depend()
    // 將dep存到map中對應的key上
    map.set(key, dep)
  }

  // 2.3最終將dep返回出去
  return dep
}

在Proxy的捕獲器中獲取對應的dep:

// 創建一個Proxy
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 根據當前對象target和設置的key,去獲取對應的dep
    const dep = getDepend(target, key)
    console.log(dep)
    // 當set捕獲器捕獲到屬性變化時,自動去調用notify
    dep.notify()
  }
})
由淺入深,帶你用JavaScript實現響應式原理

 

5.7.對象的依賴收集優化

可以發現上面打印的結果中的響應式函數數組全部為空,是因為在前面收集響應式函數是通過watchFn來收集的,而在getDepend中并沒有去收集對應的響應式函數,所以返回的dep對象里面的數組全部就為空了。如果對響應式函數,還需要通過自己一個個去收集,是不太容易的,所以可以監聽響應式函數中依賴了哪一個對象屬性,讓Proxy的get捕獲器去收集就行了。

  • 既然get需要監聽到響應式函數訪問了哪些屬性,那么響應式函數在被添加之前肯定是要執行一次的;
  • 如何在Proxy中拿到當前需要被收集的響應式函數呢?可以借助全局變量;
  • 下面就來對watchFn進行改造;
// 定義一個全局變量,存放當前需要收集的響應式函數
let currentReactiveFn = null
function watchFn(fn) {
  currentReactiveFn = fn
  // 先調用一次函數,提醒Proxy的get捕獲器需要收集響應式函數了
  fn()
  // 收集完成將currentReactiveFn重置
  currentReactiveFn = null
}

Proxy中get捕獲器具體需要執行的操作:

// 創建一個Proxy
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    const dep = getDepend(target, key)
    // 拿到全局的currentReactiveFn進行添加
    dep.addDependFn(currentReactiveFn)
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 根據當前對象target和設置的key,去獲取對應的dep
    const dep = getDepend(target, key)
    console.log(dep)
    // 當set捕獲器捕獲到屬性變化時,自動去調用notify
    dep.notify()
  }
})

下面測試一下看看效果:

watchFn(function() {
  console.log('1:我依賴了name屬性')
  console.log(objProxy.name)
})
watchFn(function() {
  console.log('2:我依賴了name屬性')
  console.log(objProxy.name)
})

watchFn(function() {
  console.log('1:我依賴了age屬性')
  console.log(objProxy.age)
})
watchFn(function() {
  console.log('2:我依賴了age屬性')
  console.log(objProxy.age)
})

console.log('----------以上為初始化執行,以下為修改后執行-------------')

objProxy.name = 'kobe'
objProxy.age = 24
由淺入深,帶你用JavaScript實現響應式原理

 

5.8.Depend類優化

截止到上面,大部分響應式原理已經實現了,但是還存在一些小問題需要優化。

  • 優化一:既然currentReactiveFn可以在全局拿到,何不在Depend類中就對它進行收集呢。改造方法addDependFn
  • 優化二:如果一個響應式函數中多次訪問了某個屬性,就都會去到Proxy的get捕獲器,該響應式函數會被重復收集,在調用時就會調用多次。當屬性發生變化后,依賴這個屬性的響應式函數被調用一次就可以了。改造reactiveFns,將數組改成Set,Set可以避免元素重復,注意添加元素使用add
// 將currentReactiveFn放到Depend之前,方便其拿到
let currentReactiveFn = null

class Depend {
  constructor() {
    // 用于存放響應式函數
    this.reactiveFns = new Set()
  }

  // 用戶添加響應式函數
  addDependFn() {
    // 先判斷一下currentReactiveFn是否有值
    if (currentReactiveFn) {
      this.reactiveFns.add(currentReactiveFn)
    }
  }

  // 用于執行響應式函數
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

Proxy中就不用去收集響應式函數了,直接調用addDependFn即可:

// 創建一個Proxy
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    const dep = getDepend(target, key)
    // 直接調用addDepend方法,讓它去收集
    dep.addDependFn()
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 根據當前對象target和設置的key,去獲取對應的dep
    const dep = getDepend(target, key)
    // 當set捕獲器捕獲到屬性變化時,自動去調用notify
    dep.notify()
  }
})

5.9.多個對象實現響應式

前面都只講了一個對象實現響應式的實現,如果有多個對象需要實現可響應式呢?將Proxy封裝一下,外面套一層函數即可,調用該函數,返回該對象的代理對象。

function reactive(obj) {
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      const dep = getDepend(target, key)
      // 直接調用addDepend方法,讓它去收集
      dep.addDependFn()
      return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      // 根據當前對象target和設置的key,去獲取對應的dep
      const dep = getDepend(target, key)
      // 當set捕獲器捕獲到屬性變化時,自動去調用notify
      dep.notify()
    }
  })
}

看一下具體使用效果:

const obj1 = { name: 'curry', age: 30 }
const obj2 = { weight: '130', height: '180' }

const obj1Proxy = reactive(obj1)
const obj2Proxy = reactive(obj2)

watchFn(function() {
  console.log('我依賴了obj1的name屬性')
  console.log(obj1Proxy.name)
})
watchFn(function() {
  console.log('我依賴了age屬性')
  console.log(obj1Proxy.age)
})

watchFn(function() {
  console.log('我依賴了obj2的weight屬性')
  console.log(obj2Proxy.weight)
})
watchFn(function() {
  console.log('我依賴了obj2的height屬性')
  console.log(obj2Proxy.height)
})

console.log('----------以上為初始化執行,以下為修改后執行-------------')

obj1Proxy.name = 'kobe'
obj1Proxy.age = 24
obj2Proxy.weight = 100
obj2Proxy.height = 165
由淺入深,帶你用JavaScript實現響應式原理

 

5.10.總結整理

通過上面9步完成了最終響應式原理的實現,下面對其進行整理一下:

  • watchFn函數:傳入該函數的函數都是需要被收集為響應式函數的,對響應式函數進行初始化調用,使Proxy的get捕獲器能捕獲到屬性訪問;
  • function watchFn(fn) { currentReactiveFn = fn // 先調用一次函數,提醒Proxy的get捕獲器需要收集響應式函數了 fn() // 收集完成將currentReactiveFn重置 currentReactiveFn = null }
  • Depend類:reactiveFns用于存放響應式函數,addDependFn方法實現對響應式函數的收集,notify方法實現當屬性值變化時,去調用對應的響應式函數;
  • // 將currentReactiveFn放到Depend之前,方便其拿到 let currentReactiveFn = null class Depend { constructor() { // 用于存放響應式函數 this.reactiveFns = new Set() } // 用戶添加響應式函數 addDependFn() { // 先判斷一下currentReactiveFn是否有值 if (currentReactiveFn) { this.reactiveFns.add(currentReactiveFn) } } // 用于執行響應式函數 notify() { this.reactiveFns.forEach(fn => { fn() }) } }
  • reactive函數:實現將普通對象轉成代理對象,從而將其轉變為可響應式對象;
  • function reactive(obj) { return new Proxy(obj, { get: function(target, key, receiver) { const dep = getDepend(target, key) // 直接調用addDepend方法,讓它去收集 dep.addDependFn() return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) // 根據當前對象target和設置的key,去獲取對應的dep const dep = getDepend(target, key) // 當set捕獲器捕獲到屬性變化時,自動去調用notify dep.notify() } }) }
  • getDepend函數:根據指定的對象和對象屬性(key)去查找對應的dep對象;
  • // 1.創建一個WeakMap存儲結構,存放對象 const objWeakMap = new WeakMap() // 2.封裝一個獲取dep的函數 function getDepend(obj, key) { // 2.1.根據對象,獲取對應的map let map = objWeakMap.get(obj) // 如果是第一次獲取這個map,那么需要先創建一個map if (!map) { map = new Map() // 將map存到objWeakMap中對應key上 objWeakMap.set(obj, map) } // 2.2.根據對象的屬性,獲取對應的dep let dep = map.get(key) // 如果是第一次獲取這個dep,那么需要先創建一個dep if (!dep) { dep = new Depend() // 將dep存到map中對應的key上 map.set(key, dep) } // 2.3最終將dep返回出去 return dep }

總結:以上通過Proxy來監聽對象操作的實現響應式的方法就是Vue3響應式原理了。

6.Vue2響應式原理的實現

Vue3響應式原理已經實現了,那么Vue2只需要將Proxy換成Object.defineProperty就可以了。

  • 將reactive函數改一下即可;
function reactive(obj) {
  // 1.拿到obj所有的key
  const keys = Object.keys(obj)

  // 2.遍歷所有的keys,添加存取屬性描述符
  keys.forEach(key => {
    let value = obj[key]

    Object.defineProperty(obj, key, {
      get: function() {
        const dep = getDepend(obj, key)
        // 直接調用addDepend方法,讓它去收集
        dep.addDependFn()
        return value
      },
      set: function(newValue) {
        value = newValue
        // 根據當前對象設置的key,去獲取對應的dep
        const dep = getDepend(obj, key)
        // 監聽到屬性變化時,自動去調用notify
        dep.notify()
      }
    })
  })

  // 3.將obj返回
  return obj
}


本文來自
https://www.cnblogs.com/MomentYY/p/16065162.html

分享到:
標簽:JavaScript
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定