博客 / 詳情

返回

通過Proxy和Reflect實現vue的響應式原理

vue3通過Proxy+Reflect實現響應式,vue2通過defineProperty來實現

Proxy

Proxy是什麼

Proxy是ES6中增加的類,表示代理。
如果我們想要監聽對象的操作過程,可以先創建一個代理對象,之後所有對於對象的操作,都由代理對象來完成,代理對象可以監聽到我們對於原對象進行了哪些操作。

Proxy怎麼使用

Proxy是一個類,通過new關鍵字創建對象,傳入原對象和處理監聽的捕獲器

const user = {
  name: 'alice'
}

const proxy = new Proxy(user, {})

console.log(proxy)
proxy.name = 'kiki'
console.log(proxy.name)
console.log(user.name)

對代理所作的操作,同樣會作用於原對象

什麼是捕獲器

捕獲器就是用來監聽對於對象操作的方法

const user = {
  name: 'alice',
  age: 18
}
const proxy = new Proxy(user, {
  get: function(target, key){
    console.log(`調用${key}屬性的讀取操作`)
    return target[key]
  },
  set: function(target, key, value){
    console.log(`調用${key}屬性的設置操作`)
    target[key] = value
  }
})
proxy.name = 'kiki'
console.log(proxy.age)

以上get、set是捕獲器中常用的兩種,分別用於對象數據的“讀取”操作和“設置”操作

有哪些捕獲器

Proxy裏對應捕獲器與普通對象的操作和定義是一一對應的

  1. handler.getPrototypeOf()
    Object.getPrototypeOf 方法的捕捉器。
  2. handler.setPrototypeOf()
    Object.setPrototypeOf 方法的捕捉器。
  3. handler.isExtensible()
    Object.isExtensible 方法的捕捉器。
  4. handler.preventExtensions()
    Object.preventExtensions 方法的捕捉器。
  5. handler.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor 方法的捕捉器。
  6. handler.defineProperty()
    Object.defineProperty 方法的捕捉器。
  7. handler.has()
    in 操作符的捕捉器。
  8. handler.get()
    屬性讀取操作的捕捉器。
  9. handler.set()
    屬性設置操作的捕捉器。
  10. handler.deleteProperty()
    delete 操作符的捕捉器。
  11. handler.ownKeys()
    Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
  12. handler.apply()
    函數調用操作的捕捉器。
  13. handler.construct()
    new 操作符的捕捉器。

將這些捕獲器以及對應的對象操作寫在了以下示例中

const user = {
  name: "alice",
  age: 18
};
const proxy = new Proxy(user, {
  get(target, key) {
    console.log("執行了get方法");
    return target[key];
  },
  set(target, key, value) {
    target[key] = value;
    console.log("執行了set方法");
  },
  has(target, key) {
    return key in target;
  },
  deleteProperty(target, key){
    console.log('執行了delete的捕獲器')
    delete target[key]
  },
  ownKeys(target){
    console.log('ownKeys')
    return Object.keys(target)
  },
  defineProperty(target, key, value){
    console.log('defineProperty', target, key, value)
    return true
  },
  getOwnPropertyDescriptor(target, key){
    return Object.getOwnPropertyDescriptor(target, key)
  },
  preventExtensions(target){
    console.log('preventExtensions')
    return Object.preventExtensions(target)
  },
  isExtensible(target){
    return Object.isExtensible(target)
  },
  getPrototypeOf(target){
    return Object.getPrototypeOf(target)
  },
  setPrototypeOf(target, prototype){
    console.log('setPrototypeOf', target, prototype)
    return false
  }
});

console.log(proxy.name);
proxy.name = "kiki";
console.log("name" in proxy);
delete proxy.age
console.log(Object.keys(proxy))
Object.defineProperty(proxy, 'name', {
  value: 'alice'
})
console.log(Object.getOwnPropertyDescriptor(proxy, 'name'))
console.log(Object.preventExtensions(proxy))
console.log(Object.isExtensible(proxy))
console.log(Object.getPrototypeOf(proxy))
Reflect.setPrototypeOf(proxy, {})

通過捕獲器去監聽對象的修改、查詢、刪除等操作

以上捕獲器中只有apply和constructor是屬於函數對象的

function foo(){}

const proxy = new Proxy(foo, {
  apply: function(target, thisArg, args){
    console.log('執行proxy的apply方法', target, thisArg, args)
    return target.apply(thisArg, args)
  },
  construct: function(target, argArray){
    console.log('執行proxy的construct方法',target, argArray)
    return new target()
  }
})

proxy.apply({}, [1,2])
new proxy('alice', 18)

apply方法執行apply捕獲器,new操作執行constructor捕獲器

Reflect

Reflect是什麼

Reflect也是ES6新增的一個API,表示反射。它是一個對象,提供了很多操作對象的方法,類似於Object中的方法,比如 Reflect.getPrototypeOf 和 Object.getPrototypeOf。

早期操作對象的方法都是定義在Object上,但Object作為構造函數,直接放在它身上並不合適,所以新增Reflect對象來統一操作,並且轉換了對象中in、delete這樣的操作符

Reflect中有哪些方法

Reflect中的方法與Proxy中是一一對應的

  1. Reflect.apply(target, thisArgument, argumentsList)
    對一個函數進行調用操作,同時可以傳入一個數組作為調用參數。和 Function.prototype.apply() 功能類似。
  2. Reflect.construct(target, argumentsList[, newTarget])
    對構造函數進行 new 操作,相當於執行 new target(...args)。
  3. Reflect.defineProperty(target, propertyKey, attributes)和 Object.defineProperty() 類似。如果設置成功就會返回 true
  4. Reflect.deleteProperty(target, propertyKey)
    作為函數的delete操作符,相當於執行 delete target[name]。
  5. Reflect.get(target, propertyKey[, receiver])
    獲取對象身上某個屬性的值,類似於 target[name]。
  6. Reflect.getOwnPropertyDescriptor(target, propertyKey)
    類似於 Object.getOwnPropertyDescriptor()。如果對象中存在該屬性,則返回對應的屬性描述符,  否則返回 undefined.
  7. Reflect.getPrototypeOf(target)
    類似於 Object.getPrototypeOf()。
  8. Reflect.has(target, propertyKey)
    判斷一個對象是否存在某個屬性,和 in 運算符 的功能完全相同。
  9. Reflect.isExtensible(target)
    類似於 Object.isExtensible().
  10. Reflect.ownKeys(target)
    返回一個包含所有自身屬性(不包含繼承屬性)的數組。(類似於 Object.keys(), 但不會受enumerable影響).
  11. Reflect.preventExtensions(target)
    類似於 Object.preventExtensions()。返回一個Boolean。
  12. Reflect.set(target, propertyKey, value[, receiver])
    將值分配給屬性的函數。返回一個Boolean,如果更新成功,則返回true。
  13. Reflect.setPrototypeOf(target, prototype)
    設置對象原型的函數. 返回一個 Boolean, 如果更新成功,則返回true。

將之前通過Proxy設置代理的對象操作全都變為Reflect

const user = {
  name: "alice",
  age: 18
};
const proxy = new Proxy(user, {
  get(target, key) {
    console.log("執行了get方法");
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    Reflect.set(target, key, value)
    console.log("執行了set方法");
  },
  has(target, key) {
    return Reflect.has(target, key)
  },
  deleteProperty(target, key){
    Reflect.deleteProperty(target, key)
  },
  ownKeys(target){
    console.log('ownKeys')
    return Reflect.ownKeys(target)
  },
  defineProperty(target, key, value){
    console.log('defineProperty', target, key, value)
    return true
  },
  getOwnPropertyDescriptor(target, key){
    return Reflect.getOwnPropertyDescriptor(target, key)
  },
  preventExtensions(target){
    console.log('preventExtensions')
    return Reflect.preventExtensions(target)
  },
  isExtensible(target){
    return Reflect.isExtensible(target)
  },
  getPrototypeOf(target){
    return Reflect.getPrototypeOf(target)
  },
  setPrototypeOf(target, prototype){
    console.log('setPrototypeOf', target, prototype)
    return false
  }
});
console.log(proxy.name);
proxy.name = "kiki";
console.log(Reflect.has(proxy, 'name'));
delete proxy.age
console.log(Reflect.ownKeys(proxy))
Reflect.defineProperty(proxy, 'name', {
  value: 'alice'
})
console.log(Reflect.getOwnPropertyDescriptor(proxy, 'name'))
console.log(Reflect.preventExtensions(proxy))
console.log(Reflect.isExtensible(proxy))
console.log(Reflect.getPrototypeOf(proxy))
Reflect.setPrototypeOf(proxy, {})

實現的效果是完全一致的

Reflect的receiver

Reflect中在進行get/set捕獲器操作的時候,還有一個入參是receiver,指的是代理對象,用於改變this指向

const user = {
  _name: 'alice',
  get name(){
    return this._name
  },
  set name(value){
    this._name = value
  }
}
const proxy = new Proxy(user, {
  get: function(target, key, receiver){
    console.log('get操作', key)
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, receiver){
    console.log('set操作', key)
    return Reflect.set(target, key, receiver)
  },
})
console.log(proxy.name)
proxy.name = 'kiki'

(1) 如果沒有receiver,那麼當修改name屬性時,objProxy先執行key為name時的get操作
(2) 然後代理到obj裏的get方法,讀取this的_name屬性,此時的this是obj,會直接修改
obj._name,不會再經過objProxy
(3) 增加了receiver之後,執行obj的get方法,讀取this的_name屬性,此時this是proxy
對象,所以會再次到get的捕獲器中

set操作同理

Reflect中的constructor

用於改變this的指向

function Person(){
}
function Student(name, age){
  this.name = name;
  this.age = age
}
const student = Reflect.construct(Student, ['aclie', 18], Person)

console.log(student)
console.log(student.__proto__ === Person.prototype)

此時創建的student對象雖然擁有Student的屬性和方法,但是它的this指向Person

vue的響應式

通過以下步驟一步步實現響應式

1、定義一個數組收集所有的依賴

  • 定義全局變量reactiveFns用來保存所有的函數
  • 定義方法watchFn,入參為函數,代碼體為將入參保存進全局變量中
  • 修改對象的值,遍歷全局變量,執行每一個函數
let user = {
  name: "alice",
};
let reactiveFns = [];
function watchFn(fn) {
  reactiveFns.push(fn);
}
watchFn(function () {
  console.log("哈哈哈");
});
watchFn(function () {
  console.log("hello world");
});
function foo(){
  console.log('普通函數')
}
user.name = "kiki";
reactiveFns.forEach((fn) => {
  fn();
});

此時通過響應式函數 watchFn 將所有需要執行的函數收集進了數組中,然後當變量的值發生變化時,手動遍歷執行所有的函數

2.收集依賴類的封裝

以上只有一個數組來收集對象的執行函數,真實情況下,不止一個對象需要對操作狀態進行監聽,需要監聽多個對象就可以使用類。

  • 定義Depend類,給每個實例對象提供addDepend方法用於添加綁定的方法, notify方法用於執行該實例的reactiveFns屬性上添加的所有方法
  • 封裝響應式函數watchFn,函數裏調用實例對象的appDepend方法
  • 修改對象的值,調用實例對象的notify方法
class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
let user = {
  name: "alice",
  age: 18,
};
const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn);
}
watchFn(function(){
  console.log('啦啦啦啦啦啦')
})
watchFn(function(){
  console.log('hello hello')
})
function foo(){
  console.log('foo')
}
user.name = 'kiki'
depend.notify()

將收集操作和依次執行函數的方法都定義在類中

3.自動監聽對象的變化

以上仍然是我們自己手動調用執行函數的方法,以下自動監聽

  • 在通過類收集依賴的基礎上,增加Proxy來定義對象,Reflect執行對象的方法
  • 在set方法中執行實例對象depend的notify方法
  • 修改代理proxy屬性的值
class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
let user = {
  name: "alice",
  age: 18,
};
const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn);
}
const proxy = new Proxy(user, {
  get: function (target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    depend.notify();
  },
});
watchFn(function () {
  console.log("name變化執行的函數");
});
watchFn(function () {
  console.log("age變化執行的函數");
});
function foo() {
  console.log("foo");
}
proxy.name = "kiki";
proxy.age = 20;

此時的問題是,修改了對象的任一屬性,所有的函數都會調用,沒有按照一一對應的關係來保存對象的屬性和對應的函數

4、收集依賴的管理

  • 定義weakMap用來管理對象,[對象名, map],定義map保存 [對象的屬性名, 實例對象depend]
  • 定義獲取depend實例對象的方法getDepend,先從weakMap中獲取map,如果沒有就new 一個,再從map中獲取depend對象,如果沒有再new一個
  • 在set方法中獲取depend對象,調用notify方法
class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
let user = {
  name: "alice",
  age: 18,
};
const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn);
}
const weakMap = new WeakMap()
function getDepend(obj, key){
  let map = weakMap.get(obj)
  if(!map){
    map = new Map()
    weakMap.set(obj, map)
  }
  let depend = map.get(key)
  if(!depend){
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}
const proxy = new Proxy(user, {
  get: function (target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    const depend = getDepend(target, key)
    depend.notify();
  },
});
watchFn(function () {
  console.log("name變化執行的函數");
});
watchFn(function () {
  console.log("age變化執行的函數");
});
function foo() {
  console.log("foo");
}
proxy.name = "kiki";
proxy.age = 20;

此時proxy對應的depend是沒有值的,所以此時沒有任何打印的數據

5、正確管理收集的依賴

  • 全局定義變量activeReactiveFn指向watchFn傳入的方法,執行傳入的方法,再將 activeReactiveFn指向null
  • watchFn傳入時便會被執行一次,用於代碼對象的get方法中收集依賴
  • Depend類中使用addDepend方法無需傳參,直接使用全局的activeReactiveFn
  • get方法中通過getDepend獲取depend,並使用addDepend方法,收集依賴
class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
let user = {
  name: "alice",
  age: 18,
};
let activeReactiveFn = null;
function watchFn(fn) {
  activeReactiveFn = fn;
  fn();
  activeReactiveFn = null;
}
const weakMap = new WeakMap();
function getDepend(obj, key) {
  let map = weakMap.get(obj);
  if (!map) {
    map = new Map();
    weakMap.set(obj, map);
  }
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}
const proxy = new Proxy(user, {
  get: function (target, key, receiver) {
    const depend = getDepend(target, key)
    depend.addDepend(activeReactiveFn)
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    const depend = getDepend(target, key);
    depend.notify();
  },
});
watchFn(function () {
  console.log(proxy.name, "name變化執行的函數");
  console.log(proxy.name, "name變化執行的函數 again");
});
watchFn(function () {
  console.log(proxy.age, "age變化執行的函數");
});
function foo() {
  console.log("foo");
}
proxy.name = "kiki";

此時已經能夠根據屬性值的變化而執行對應的函數了,但同一個函數會執行兩次

6、重構

  • 當函數中調用兩次proxy的屬性時,會將同一個函數添加到數組中兩次,所以將 reactiveFn數據結構由數組變成set
  • 定義reactive函數用來收集處理需要代理及響應式的對象
let activeReactiveFn = null;
class Depend {
  constructor() {
    this.reactiveFns = new Set();
  }
  addDepend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn);
    }
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
function watchFn(fn) {
  activeReactiveFn = fn;
  fn();
  activeReactiveFn = null;
}
const weakMap = new WeakMap();
function getDepend(obj, key) {
  let map = weakMap.get(obj);
  if (!map) {
    map = new Map();
    weakMap.set(obj, map);
  }
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}
function reactive(obj){
  return new Proxy(obj, {
    get: function (target, key, receiver) {
      const depend = getDepend(target, key);
      depend.addDepend();
      return Reflect.get(target, key, receiver);
    },
    set: function (target, key, value, receiver) {
      Reflect.set(target, key, value, receiver);
      const depend = getDepend(target, key);
      depend.notify();
    },
  });
}
const user = reactive({
  name: "alice",
  age: 18,
})
const info = reactive({
  message: 'hello'
})
watchFn(function () {
  console.log(user.name, "name變化執行的函數");
  console.log(user.name, "name變化執行的函數 again");
});
watchFn(function () {
  console.log(user.age, "age變化執行的函數");
});
watchFn(function(){
  console.log(info.message, "message發生變化了")
})
function foo() {
  console.log("foo");
}
user.name = "kiki";
user.age = 20;
info.message = 'ooo'

此時已實現vue3的響應式~

vue2的實現原理

vue2的實現就是將Proxy和Reflect替換成了Object.defineProperty和Object本身的一些方法

let activeReactiveFn = null;
class Depend {
  constructor() {
    this.reactiveFns = new Set();
  }
  addDepend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn);
    }
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
function watchFn(fn) {
  activeReactiveFn = fn;
  fn();
  activeReactiveFn = null;
}
const weakMap = new WeakMap();
function getDepend(obj, key) {
  let map = weakMap.get(obj);
  if (!map) {
    map = new Map();
    weakMap.set(obj, map);
  }
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}
function reactive(obj){
  Object.keys(obj).forEach(key=>{
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get: function () {
        const depend = getDepend(obj, key);
        depend.addDepend();
        return value
      },
      set: function (newValue) {
        value = newValue
        const depend = getDepend(obj, key);
        depend.notify();
      },
    })
  })
  return obj
}
const user = reactive({
  name: "alice",
  age: 18,
})
const info = reactive({
  message: 'hello'
})
watchFn(function () {
  console.log(user.name, "name變化執行的函數");
  console.log(user.name, "name變化執行的函數 again");
});
watchFn(function () {
  console.log(user.age, "age變化執行的函數");
});
watchFn(function(){
  console.log(info.message, "message發生變化了")
})
function foo() {
  console.log("foo");
}
user.name = "kiki";
user.age = 20;
info.message = 'ooo'

和上面實現的效果是一致的

以上就是通過Proxy和Reflect實現vue的響應式原理,關於js高級,還有很多需要開發者掌握的地方,可以看看我寫的其他博文,持續更新中~

user avatar joe_sky 頭像 thepoy 頭像 xiaohaiqianduan 頭像 sky124380729 頭像
4 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.