4 月 17 日,尤大在微博上宣佈 Vue 3.0 beta 版本正式發佈。javascript
在尤大發布的《 Vue3 設計過程》文章中提到之因此重構 Vue 一個考量就是JavaScript新的語言特性在主流瀏覽器中的支持程度,其中最值得一提的就是Proxy,它爲框架提供了攔截對於object的操做的能力。Vue 的一項核心能力就是監聽用戶定義的狀態變化並響應式刷新DOM。Vue 2是經過替換狀態對象屬性的getter和setter來實現這一特性的。改成Proxy後,能夠突破Vue當前的限制,好比沒法監聽新增屬性,還能提供更好的性能表現。前端
Two key considerations led us to the new major version (and rewrite) of Vue: First, the general availability of new JavaScript language features in mainstream browsers. Second, design and architectural issues in the current codebase that had been exposed over time.vue
做爲一名高級前端猿,咱們要知其然,更要知其因此然,那就讓咱們來看一下到底什麼是 Proxy?java
Proxy 這個詞翻譯過來就是「代理」,用在這裏表示由它來「代理」某些操做。 Proxy 會在目標對象以前架設一層「攔截」,外界對該對象的訪問,都必須先經過這層攔截,所以提供了一種機制,可 以對外界的訪問進行過濾和改寫。react
先來看下 proxy 的基本語法api
const proxy = new Proxy(target, handler)
複製代碼
咱們看一個簡單的例子:數組
const person = {
name: 'muyao',
age: 27
};
const proxyPerson = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.name // 35
proxy.age // 35
proxy.sex // 35 不存在的屬性一樣起做用
person.name // muyao 原對象未改變
複製代碼
上面代碼中,配置對象有一個get方法,用來攔截對目標對象屬性的訪問請求。get方法的兩個參數分別是目標對象和所要訪問的屬性。能夠看到,因爲攔截函數老是返回35,因此訪問任何屬性都獲得35瀏覽器
注意,Proxy 並無改變原有對象 而是生成一個新的對象,要使得 Proxy 起做用,必須針對 Proxy 實例(上例是 proxyPerson)進行操做,而不是針對目標對象(上例是 person)進行操做bash
Proxy 支持的攔截操做一共 13 種:app
proxy.foo
和 proxy['foo']
。proxy.foo = v
或 proxy['foo'] = v
, 返回一個布爾值。propKey in proxy
的操做,返回一個布爾值。delete proxy[propKey]
的操做,返回一個布爾值。Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循環,返回一個數組。該方法返回目標對象全部自身的屬性的屬性名。Object.getOwnPropertyDescriptor(proxy, propKey)
,返回屬性的描述對象。Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一個布爾值。Object.preventExtensions(proxy)
,返回一個布爾值。Object.getPrototypeOf(proxy)
,返回一個對象。Object.isExtensible(proxy)
,返回一個布爾值。Object.setPrototypeOf(proxy, proto)
,返回一個布爾值。proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。new proxy(...args)
。Vue2 中是遞歸遍歷 data 中的全部的 property,並使用 Object.defineProperty 把這些 property 所有轉爲 getter/setter,在getter 中作數據依賴收集處理,在 setter 中 監聽數據的變化,並通知訂閱當前數據的地方。
// 對 data中的數據進行深度遍歷,給對象的每一個屬性添加響應式
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 進行依賴收集
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
// 是數組則須要對每個成員都進行依賴收集,若是數組的成員仍是數組,則遞歸。
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 新的值須要從新進行observe,保證數據響應式
childOb = !shallow && observe(newVal)
// 將數據變化通知全部的觀察者
dep.notify()
}
})
複製代碼
但因爲 JavaScript 的限制,這種實現有幾個問題:
Vue3 進行了全新改進,使用 Proxy 代理的做爲全新的變動檢測,再也不使用 Object.defineProperty
在 Vue3 中,可使用 reactive() 建立一個響應狀態
import { reactive } from 'vue'
// reactive state
const state = reactive({
desc: 'Hello Vue 3!',
count: 0
});
複製代碼
咱們在源碼 vue-next/packages/reactivity/src/reactive.ts
文件中看到了以下的實現:
//reactive f => createReactiveObject()
function createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
...
// 設置攔截器
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers;
observed = new Proxy(target, handlers);
...
return observed;
}
複製代碼
下面咱們看下 state 通過處理後的狀況
能夠看到被代理的目標對象 state 設置了 get()、set()、deleteProperty()、has()、ownKeys()這 5 個 handler,一塊兒來看下它們都作了什麼
get() 會自動讀取響應數據,並進行 track 調用
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
...
// 恢復默認行爲
const res = Reflect.get(target, key, receiver)
...
// 調用 track
!isReadonly && track(target, TrackOpTypes.GET, key)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
複製代碼
目標對象上不存在的屬性設置值時,進行 「添加」 操做,而且會觸發 trigger() 來通知響應系統的更新。解決了 Vue 2.x 中沒法檢測到對象屬性的添加的問題
function createSetter(shallow = false) {
return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean {
...
const hadKey = hasOwn(target, key)
// 恢復默認行爲
const result = Reflect.set(target, key, value, receiver)
// 若是目標對象在原型鏈上,不要 trigger
if (target === toRaw(receiver)) {
// 若是設置的屬性不在目標對象上 就進行 Add 這就解決了 Vue 2.x 中沒法檢測到對象屬性的添加或刪除的問題
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
複製代碼
關聯 delete 操做,當目標對象上的屬性被刪除時,會觸發 trigger() 來通知響應系統的更新。這也解決了 Vue 2.x 中沒法檢測到對象屬性的刪除的問題
function deleteProperty(target, key) {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
// 存在屬性刪除時觸發 trigger
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
複製代碼
這兩個 handler 並無修改默認行爲,可是它們都調用 track() 函數,回顧上文能夠知道has() 影響 in 操做的,ownKeys() 影響 for...in 及循環
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
track(target, TrackOpTypes.HAS, key)
return result
}
function ownKeys(target: object): (string | number | symbol)[] {
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.ownKeys(target)
}
複製代碼
經過上面的分析,咱們能夠看到,Vue3 藉助 Proxy 的幾個 Handler 攔截操做,收集依賴,實現了響應系統核心。
咱們已經看到了 Proxy 在 Vue3 中的應用場景,其實在使用了Proxy後,對象的行爲基本上都是可控的,因此咱們能拿來作一些以前實現起來比較複雜的事情。
let api = {
getUser: function(userId) {
/* ... */
},
setUser: function(userId, config) {
/* ... */
}
};
// 打日誌
function log(timestamp, method) {
console.log(`${timestamp} - Logging ${method} request.`);
}
api = new Proxy(api, {
get: function(target, key, proxy) {
var value = target[key];
return function(...arguments) {
log(new Date(), key); // 打日誌
return Reflect.apply(value, target, arguments);
};
}
});
api.getUsers();
複製代碼
let numObj = { count: 0, amount: 1234, total: 14 };
numObj = new Proxy(numObj, {
set(target, key, value, proxy) {
if (typeof value !== 'number') {
throw Error('Properties in numObj can only be numbers');
}
return Reflect.set(target, key, value, proxy);
}
});
// 拋出錯誤,由於 "foo" 不是數值
numObj.count = 'foo';
// 賦值成功
numObj.count = 333;
複製代碼
能夠看到 Proxy 能夠有不少有趣的應用,你們快快去探索吧!
本文首發於公衆號-前端瑣話