Vue3 發佈後,有一個重要的有關於響應式機制的改動,html
Vue2 的時候,採用的是 Object.defineProperty
方式,重構數據的 set
、get
方法,來達到監聽數據變動的方法,vue
可是在 Vue3 發佈後,就再也不使用 Object.defineProperty
了,而是使用了 ES6 中的 Proxy
來對數據進行一個封裝,起到一箇中間代理的做用來監聽數據的變動,對於 Proxy
不瞭解的小夥伴能夠看這裏:Proxyreact
下面主要是對 Vue3 的響應式機制進行一個簡單的實現,主要包含兩個:ref
、reactive
;git
ref
:是對基礎數據進行封裝監聽,例如:Boolean、Numberes6
reactive
:是對複雜數據進行封裝監聽,例如:github
{
key1: 'Benson',
key2: {
key3: 1007,
key4: [1, 2, 3]
}
}
複製代碼
let activeEffect // 用於保存當先須要依賴的函數
// mini 依賴中心
class Dep {
constructor(){
this.subs = new Set(); // 使用 Set 避免重複收集依賴
}
depend(){
// 收集依賴
if(activeEffect){
this.subs.add(activeEffect)
}
}
notofy(){
// 數據變化,觸發effect執行
this.subs.forEach(effect=>effect())
}
}
function effect(fn){
activeEffect = fn; // 保存當前響應式依賴函數
fn(); // 執行依賴函數
}
const dep = new Dep() // vue3 中就變成一個大的 map
// ref 大概的原理在這了,待會後面能夠看代碼
function ref(val){
let _value = val
// 攔截.value操做
let state = {
get value(){
// 獲取值,收集依賴 track
dep.depend()
return _value
},
set value(newCount){
// 修改,通知dep,執行有這個依賴的effect函數
// 源碼這裏會作判斷,是否真的值發生了變化
_value = newCount
// trigger
dep.notofy()
}
}
return state
}
const state = ref(0)
effect(()=>{
// 這個函數內部,依賴state的變化
console.log(state.value)
})
setInterval(()=>{
state.value++; // 這裏進行響應式數據的值改變,觸發 set 方法
},1000)
複製代碼
上面的案例就是對 ref
的一個簡單實現了,其實已經可以很好的表示 Vue3 在源碼中對 ref
的實現邏輯了。typescript
接下來能夠了解一下源碼是怎麼樣的:segmentfault
ref 在源碼中會對傳入的數據進行類型判斷,若是判斷爲對象數據類型會使用 reactive
去進行響應式分裝的,否者會使用 RefImpl
的 get
,set
方法去監聽,這點相似於 Vue2 的 Object.definePropert。數組
在源碼上爲 Ref 定義了一個 interface
:緩存
// 生成一個惟一key
declare const RefSymbol: unique symbol
export interface Ref<T = any> {
/**
* value值,存放真正的數據的地方
*/
value: T
/**
* Type differentiator only.
* We need this to be in public d.ts but don't want it to show up in IDE
* autocomplete, so we use a private Symbol instead.
* 用此惟一 key,來作 Ref 接口的一個描述符, 讓 isRef 函數作類型判斷
*/
[RefSymbol]: true
/**
* @internal
*/
_shallow?: boolean
}
複製代碼
接下來看看 ref 方法:
// 對於 ref 進行屢次重載
export function ref<T extends object>(
value: T
): T extends Ref ? T : Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value)
}
// 看通常狀況 ref(123),使用最後一個
function createRef(rawValue: unknown, shallow = false) {
// 判斷是否已是響應式 ref 數據了
if (isRef(rawValue)) {
return rawValue
}
// 建立響應式數據
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false) {
// 轉化數據爲響應式數據
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
// track 的代碼在 effect中,能猜到此處就是監聽函數收集依賴的方法
track(toRaw(this), TrackOpTypes.GET, 'value')
// 返回數據
return this._value
}
set value(newVal) {
// 若是數據發生變化
if (hasChanged(toRaw(newVal), this._rawValue)) {
// 更新數據
this._rawValue = newVal
// 轉化數據爲響應式數據
this._value = this._shallow ? newVal : convert(newVal)
// 能猜到此處就是觸發監聽函數執行的方法
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
// 數據類型不合適使用 ref,將採用 reactive
const convert = <T extends unknown>(val: T): T =>
/**
* isObject() 從 @vue/shared 中引入,判斷一個數據是否爲對象
* 若是傳遞的值是個對象(包含數組/Map/Set/WeakMap/WeakSet),則使用 reactive 執行,不然返回原數據
*/
isObject(val) ? reactive(val) : val
// 從@vue/shared中引入,判斷一個數據是否爲對象
// Record<any, any>表明了任意類型key,任意類型value的類型
// 爲何 val is Record<any, any> 而不是 val is object 呢?能夠看下這個回答:
// https://stackoverflow.com/questions/52245366/in-typescript-is-there-a-difference-between-types-object-and-recordany-any
export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object'
複製代碼
以上就是 Vue3 中對 ref 的簡單閱讀,至於 ref 裏面的各個內部方法具體邏輯,能夠了解一下前面的簡單例子就能大概知道了,若是要仔細瞭解的話,就自行一步一步去查看源碼了哈~
Vue3 對於比較複雜的數據,就會採用 reactive
進行響應式的封裝,下面來看看如何實現一個簡易版的響應式邏輯:
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<div id="btn">click</div>
<script src="./vue.js"></script>
<script>
const root = document.getElementById('app')
const btn = document.getElementById('btn')
// 響應式封裝
let obj = reactive({
name: 'Benson',
age: 24,
num: { count: 1 },
})
// 計算屬性
let double = computed(()=>obj.age*2)
// 反作用,依賴函數
effect(()=>{
console.log('數據變了',obj.age)
root.innerHTML = `<h1>${obj.name}今年${obj.age}歲了,雙倍${double.value}, Num: ${obj.num.count}</h1>`
})
btn.addEventListener('click',()=>{
// obj.age+=1;
obj.num.count += 1; // 測試 reactive 遞歸封裝 Proxy 的特色
},false)
</script>
</body>
</html>
複製代碼
在 index.html 中,對一個對象進行 reactive
響應式封裝,而且還生成一個對 obj.age
的計算屬性,這裏其實計算屬性就是一個特殊的依賴函數(反作用函數)
effect
反作用函數傳入的方法會在響應式數據發生變化後執行。反作用函數執行以後,計算屬性根據取值操做,也就是 get
方法會觸發,這時候,就會觸發 computed
的傳入的 Effect 執行獲取到最新值,這一點能夠留意一下,下面的簡易版實現邏輯:
<!--vue.js-->
const effectStack = [] // 這裏存儲當前響應式數據的依賴函數
let targetMap = new WeakMap() // 存儲全部reactive,全部key對應的依賴
// {
// target1: {
// key1: [effect]
// }
// }
// target1 其實就是使用響應式源對象做爲 key,對象中的屬性做爲 key1 ,而後該屬性對應着哪一些反作用函數整合到 [effect] 中
function track(target,key){
// 收集依賴
// reactive可能有多個,一個又有N個屬性key
const effect = effectStack[effectStack.length-1]
if(effect){
let depMap = targetMap.get(target)
if(!depMap){
depMap = new Map() // 相似對象類型,裏面放着響應數據的屬性 key 和對應 dep
targetMap.set(target, depMap)
}
let dep = depMap.get(key)
if(!dep){
dep = new Set() // 這裏使用了 Set 很重要,這裏的 Set 可以防止重複保存依賴函數
depMap.set(key,dep)
}
// 添加依賴
dep.add(effect)
effect.deps.push(dep)
}
}
function trigger(target,key,info){
// 觸發更新
let depMap = targetMap.get(target)
if(!depMap){
return
}
const effects = new Set()
const computedRunners = new Set()
if(key){
let deps = depMap.get(key)
deps.forEach(effect=>{
if(effect.computed){
computedRunners.add(effect)
}else{
effects.add(effect)
}
})
}
// 計算屬性傳入的 `fn` 會依賴 `reactive` 對象的屬性 A
// 因此這個 `fn` 也會在屬性 A 依賴集合 `deps` 進行存儲,屬性 A
// 發生了變化也會執行這個 `fn`
computedRunners.forEach(computed=>computed())
// 這裏會執行通常的函數,這裏就是主要就是執行:root.innerHTML 更新視圖
effects.forEach(effect=>effect())
}
function effect(fn,options={}){
// {lazy:false,computed:false}
// 反作用
// computed是一個特殊的effect
let e = createReactiveEffect(fn,options)
if(!options.lazy){
// lazy決定是否是首次就執行effect
e()
}
return e
}
const baseHandler = {
get(target,key){
const res = Reflect.get(target, key); // reflect更合理的
// 收集依賴
track(target,key)
// 當使用到內部屬性的時候,再進行 Proxy 封裝,
if (typeof res === 'object') {
return reactive(res);
}
return res
},
set(target,key,val){
const info = {oldValue:target[key], newValue:val}
Reflect.set(target, key, val); // Reflect.set
// 觸發更新
trigger(target,key,info)
}
}
function reactive(target){
if (typeof target === 'object') {
/*
if (target instanceof Array) {
// 若是是一個數組,那麼取出來數組中的每個元素
// 判斷每個元素是否又是一個對象,若是又是一個對象,那麼也須要包裝成 Proxy
target.forEach((item, index) => {
if (typeof item === 'object') {
target[index] = reactive(item);
}
});
} else {
// 若是是一個對象,那麼取出對象屬性的值
// 判斷對象屬性的值是否又是一個對象,若是又是一個對象,那麼也須要包裝成 Proxy
for (let key in target) {
const item = target[key];
if (typeof item === 'object') {
target[key] = reactive(item);
}
}
}
*/
// target變成響應式
const observerd = new Proxy(target, baseHandler);
return observerd;
} else {
console.warn('請傳入 Object');
return target;
}
}
function createReactiveEffect(fn,options){
const effect = function _effect(...args){
/* 這裏的 _effect 和 fn 都會由於在 run 函數中保存在 effectStack,
* 而後執行 fn 觸發數據的 get 方法,保存在 targetMap 對應響應式數據屬性 key 的 dep 中,
* 因此 _effect 和 fn 都會一直處於閉包狀態,而不會消失,
* 這時候,設置響應式數據的 set 方法時,就會觸發執行 _effect 方法,
* 而且從新執行 run 和裏面的 fn,這時候 fn執行時,
* 又會觸發響應數據的 get 方法,觸發收集依賴函數,
* 此時就是由於收集依賴的是 new Set(),一所不會致使重複收集相同的依賴,流程就是這樣了
*/
return run(_effect,fn,args)
}
// 爲了後續清理 以及緩存
effect.deps = []
effect.computed = options.computed
effect.lazy = options.lazy
return effect
}
function run(effect,fn,args){
if(effectStack.indexOf(effect)===-1){
try{
/**
* 這裏計算屬性取值的時候,會調用計算屬性的 fn 獲得返回值,若是沒有 if (!effect.computed) 這個條件,
* 那麼計算屬性中所依賴的屬性好比:age 就會綁定上 fn 這個依賴,而不是綁定上 root.innerHTML 這個依賴
* 會致使更新 age 值,沒法刷新視圖,由於對於這種狀況:
* effect(()=>{
* root.innerHTML = `<h1>雙倍${double.value}</h1>`
* })
* 沒有取值 obj.age,只作了 double.value 的取值的話,就沒法讓計算屬性中的 age 綁定正確的更新函數了
* 固然 vue3 源碼中也並非這樣作的,這裏只是簡單了一下,待更新中...
*/
if (!effect.computed) effectStack.push(effect)
return fn(...args)
}finally{
effectStack.pop()
}
}
}
function computed(fn){
// 特殊的effect
const runner = effect(fn, {computed:true,lazy:true})
return{
effect:runner,
get value(){
return runner() // 這裏計算屬性取值的時候,會執行這個 runner 從而獲得最新的值,這個值是依賴於計算屬性傳入的 fn 而來的
}
}
}
複製代碼
上訴案例就是簡單的 reactive
實現,裏面還有一個特殊的計算屬性的響應式實現,基本流程作了什麼,都在註釋上進行標識了。
有一點注意的是: reactive 會進行嵌套封裝 Proxy ,但它又不是一次性的,須要用到內部屬性的時候會去給內部屬性也封裝 Proxy,這樣返回的數據進行變動的時候,也能進行代理。
// 源碼
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
複製代碼
除了 reactive
、ref
外,還有兩個相似的 Api:shallowreactive
、shallowref
,這兩個和前兩個的區別就是不執行嵌套對象的深度響應式轉換,只封裝第一層 Proxy。
代碼中使用到了 ES6 的 Proxy 和 Reflect,不懂的小夥伴還得須要去了解一下這幾個知識點滴~