vue3.0 響應式原理(超詳細)

在這裏插入圖片描述

一 基於proxy的Observer

1 什麼是proxy

Proxy 對象用於定義基本操做的自定義行爲(如屬性查找、賦值、枚舉、函數調用等)。html

proxy是es6新特性,爲了對目標的做用主要是經過handler對象中的攔截方法攔截目標對象target的某些行爲(如屬性查找、賦值、枚舉、函數調用等)。vue

/* target: 目標對象,待要使用 Proxy 包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。 */
/* handler: 一個一般以函數做爲屬性的對象,各屬性中的函數分別定義了在執行各類操做時代理 proxy 的行爲。 */ 
const proxy = new Proxy(target, handler);

2 爲何要用proxy,改用proxy以後的利與弊

** 3.0 將帶來一個基於 Proxy 的 observer 實現,它能夠提供覆蓋語言 (JavaScript——譯註) 全範圍的響應式能力,消除了當前 Vue 2 系列中基於 Object.defineProperty 所存在的一些侷限,這些侷限包括:1 對屬性的添加、刪除動做的監測; 2 對數組基於下標的修改、對於 .length 修改的監測; 3 對 Map、Set、WeakMap 和 WeakSet 的支持;;

vue2.0 用 Object.defineProperty 做爲響應式原理的實現,可是會有它的侷限性,好比 沒法監聽數組基於下標的修改,不支持 Map、Set、WeakMap 和 WeakSet等缺陷 ,因此改用了proxy解決了這些問題,這也意味着vue3.0將放棄對低版本瀏覽器的兼容(兼容版本ie11以上)。node

3 proxy中hander對象的基本用法

vue3.0 響應式用到的捕獲器(接下來會重點介紹)react

handler.has() -> in 操做符 的捕捉器。 (vue3.0 用到)
handler.get() -> 屬性讀取 操做的捕捉器。 (vue3.0 用到)
handler.set() -> 屬性設置* 操做的捕捉器。 (vue3.0 用到)
handler.deleteProperty() -> delete 操做符 的捕捉器。(vue3.0 用到)
handler.ownKeys() -> Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。(vue3.0 用到)es6

vue3.0 響應式沒用到的捕獲器(有興趣的同窗能夠研究一下api

handler.getPrototypeOf() -> Object.getPrototypeOf 方法的捕捉器。
handler.setPrototypeOf() -> Object.setPrototypeOf 方法的捕捉器。
handler.isExtensible() -> Object.isExtensible 方法的捕捉器。
handler.preventExtensions() -> Object.preventExtensions 方法的捕捉器。
handler.getOwnPropertyDescriptor() -> Object.getOwnPropertyDescriptor 方法的捕捉器。
handler.defineProperty() -> Object.defineProperty 方法的捕捉器。
handler.apply() -> 函數調用操做 的捕捉器。
handler.construct() -> new 操做符 的捕捉器。數組

① has捕獲器

has(target, propKey)瀏覽器

target:目標對象微信

propKey:待攔截屬性名app

做用: 攔截判斷target對象是否含有屬性propKey的操做

攔截操做: propKey in proxy; 不包含for...in循環

對應Reflect: Reflect.has(target, propKey)

🌰例子:

const handler = {
    has(target, propKey){
        /*
        * 作你的操做
        */
        return propKey in target
    }
}
const proxy = new Proxy(target, handler)

② get捕獲器

get(target, propKey, receiver)

target:目標對象

propKey:待攔截屬性名

receiver: proxy實例

返回: 返回讀取的屬性

做用:攔截對象屬性的讀取

攔截操做:proxy[propKey]或者點運算符

對應Reflect: Reflect.get(target, propertyKey[, receiver])

🌰例子:

const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : '沒有此水果';
    }
}

const foot = new Proxy({}, handler)
foot.apple = '蘋果'
foot.banana = '香蕉';

console.log(foot.apple, foot.banana);    /* 蘋果 香蕉 */
console.log('pig' in foot, foot.pig);    /* false 沒有此水果 */

特殊狀況

const person = {};
Object.defineProperty(person, 'age', {
  value: 18, 
  writable: false,
  configurable: false
})
const proxPerson = new Proxy(person, {
  get(target,propKey) {
    return 20
    //應該return 18;不能返回其餘值,不然報錯
  }
})
console.log( proxPerson.age ) /* 會報錯 */

③ set捕獲器

set(target,propKey, value,receiver)

target:目標對象

propKey:待攔截屬性名

value:新設置的屬性值

receiver: proxy實例

返回:嚴格模式下返回true操做成功;不然失敗,報錯

做用: 攔截對象的屬性賦值操做

攔截操做: proxy[propkey] = value

對應Reflect: Reflect.set(obj, prop, value, receiver)

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) { /* 若是年齡不是整數 */
        throw new TypeError('The age is not an integer')
      }
      if (value > 200) {  /* 超出正常的年齡範圍 */
        throw new RangeError('The age seems invalid')
      }
    }
    obj[prop] = value
    // 表示成功
    return true
  }
}
let person = new Proxy({}, validator)
person.age = 100
console.log(person.age)  // 100
person.age = 'young'     // 拋出異常: Uncaught TypeError: The age is not an integer
person.age = 300         // 拋出異常: Uncaught RangeError: The age seems invalid

當對象的屬性writable爲false時,該屬性不能在攔截器中被修改

const person = {};
Object.defineProperty(person, 'age', {
    value: 18,
    writable: false,
    configurable: true,
});

const handler = {
    set: function(obj, prop, value, receiver) {
        return Reflect.set(...arguments);
    },
};
const proxy = new Proxy(person, handler);
proxy.age = 20;
console.log(person) // {age: 18} 說明修改失敗

④ deleteProperty 捕獲器

deleteProperty(target, propKey)

target:目標對象

propKey:待攔截屬性名

返回:嚴格模式下只有返回true, 不然報錯

做用: 攔截刪除target對象的propKey屬性的操做

攔截操做: delete proxy[propKey]

對應Reflect: Reflect.delete(obj, prop)

var foot = { apple: '蘋果' , banana:'香蕉'  }
var proxy = new Proxy(foot, {
  deleteProperty(target, prop) {
    console.log('當前刪除水果 :',target[prop])
    return delete target[prop]
  }
});
delete proxy.apple
console.log(foot)

/*
運行結果:
'當前刪除水果 : 蘋果'
{  banana:'香蕉'  }
*/

特殊狀況: 屬性是不可配置屬性時,不能刪除

var foot = {  apple: '蘋果' }
Object.defineProperty(foot, 'banana', {
   value: '香蕉', 
   configurable: false
})
var proxy = new Proxy(foot, {
  deleteProperty(target, prop) {
    return delete target[prop];
  }
})
delete proxy.banana /* 沒有效果 */
console.log(foot)

⑤ownKeys 捕獲器

ownKeys(target)

target:目標對象

返回: 數組(數組元素必須是字符或者Symbol,其餘類型報錯)

做用: 攔截獲取鍵值的操做

攔截操做:

1 Object.getOwnPropertyNames(proxy)

2 Object.getOwnPropertySymbols(proxy)

3 Object.keys(proxy)

4 for...in...循環

對應Reflect:Reflect.ownKeys()

var obj = { a: 10, [Symbol.for('foo')]: 2 };
Object.defineProperty(obj, 'c', {
   value: 3, 
   enumerable: false
})
var p = new Proxy(obj, {
 ownKeys(target) {
   return [...Reflect.ownKeys(target), 'b', Symbol.for('bar')]
 }
})
const keys = Object.keys(p)  // ['a']
// 自動過濾掉Symbol/非自身/不可遍歷的屬性

/* 和Object.keys()過濾性質同樣,只返回target自己的可遍歷屬性 */
for(let prop in p) { 
 console.log('prop-',prop) /* prop-a */
}

/* 只返回攔截器返回的非Symbol的屬性,無論是否是target上的屬性 */
const ownNames = Object.getOwnPropertyNames(p)  /* ['a', 'c', 'b'] */

/* 只返回攔截器返回的Symbol的屬性,無論是否是target上的屬性*/
const ownSymbols = Object.getOwnPropertySymbols(p)// [Symbol(foo), Symbol(bar)]

/*返回攔截器返回的全部值*/
const ownKeys = Reflect.ownKeys(p)
// ['a','c',Symbol(foo),'b',Symbol(bar)]

二 vue3.0 如何創建響應式

vue3.0 創建響應式的方法有兩種:
第一個就是運用composition-api中的reactive直接構建響應式,composition-api的出現咱們能夠在.vue文件中,直接用setup()函數來處理以前的大部分邏輯,也就是說咱們沒有必要在 export default{ } 中在聲明生命週期 , data(){} 函數,watch{} , computed{} 等 ,取而代之的是咱們在setup函數中,用vue3.0 reactive watch 生命週期api來到達一樣的效果,這樣就像react-hooks同樣提高代碼的複用率,邏輯性更強。

第二個就是用傳統的 data(){ return{} } 形式 ,vue3.0沒有放棄對vue2.0寫法的支持,而是對vue2.0的寫法是徹底兼容的,提供了applyOptions 來處理options形式的vue組件。可是options裏面的data , watch , computed等處理邏輯,仍是用了composition-api中的API對應處理。

1 composition-api reactive

Reactive 至關於當前的 Vue.observable () API,通過reactive處理後的函數能變成響應式的數據,相似於option api裏面的vue處理data函數的返回值。

咱們用一個todoList的demo試着嚐嚐鮮。

const { reactive , onMounted } = Vue
setup(){
    const state = reactive({
        count:0,
        todoList:[]
    })
    /* 生命週期mounted */
    onMounted(() => {
       console.log('mounted')
    })
    /* 增長count數量 */
    function add(){
        state.count++
    } 
    /* 減小count數量 */
    function del(){
        state.count--
    }
    /* 添加代辦事項 */
    function addTodo(id,title,content){
        state.todoList.push({
            id,
            title,
            content,
            done:false
        })
    }
    /* 完成代辦事項 */
    function complete(id){
        for(let i = 0; i< state.todoList.length; i++){
            const currentTodo = state.todoList[i] 
            if(id === currentTodo.id){
                state.todoList[i] = {
                    ...currentTodo,
                    done:true
                } 
                break
            }
        }
    }
    return {
        state,
        add,
        del,
        addTodo,
        complete
    }
}

2 options data

options形式的和vue2.0並無什麼區別

export default {
    data(){
        return{
            count:0,
            todoList:[] 
        }
    },
    mounted(){
        console.log('mounted')
    }
    methods:{
        add(){
            this.count++
        },
        del(){
            this.count--
        },
        addTodo(id,title,content){
           this.todoList.push({
               id,
               title,
               content,
               done:false
           })
        },
        complete(id){
            for(let i = 0; i< this.todoList.length; i++){
                const currentTodo = this.todoList[i] 
                if(id === currentTodo.id){
                    this.todoList[i] = {
                        ...currentTodo,
                        done:true
                    } 
                    break
                }
            }
        }
    }
}

三 響應式原理初探

不一樣類型的Reactive

vue3.0能夠根據業務需求引進不一樣的API方法。這裏須要

① reactive

創建響應式reactive,返回proxy對象,這個reactive能夠深層次遞歸,也就是若是發現展開的屬性值是引用類型的並且被引用,還會用reactive遞歸處理。並且屬性是能夠被修改的。

② shallowReactive

創建響應式shallowReactive,返回proxy對象。和reactive的區別是隻創建一層的響應式,也就是說若是發現展開屬性是引用類型也不會遞歸

③ readonly

返回的proxy處理的對象,能夠展開遞歸處理,可是屬性是隻讀的,不能修改。能夠作props傳遞給子組件使用。

④ shallowReadonly

返回通過處理的proxy對象,可是創建響應式屬性是隻讀的,不展開引用也不遞歸轉換,能夠這用於爲有狀態組件建立props代理對象。

儲存對象與proxy

上文中咱們說起到。用Reactive處理過並返回的對象是一個proxy對象,假設存在不少組件,或者在一個組件中被屢次reactive,就會有不少對proxy對象和它代理的原對象。爲了能把proxy對象和原對象創建關係,vue3.0採用了WeakMap去儲存這些對象關係。WeakMaps 保持了對鍵名所引用的對象的弱引用,即垃圾回收機制不將該引用考慮在內。只要所引用的對象的其餘引用都被清除,垃圾回收機制就會釋放該對象所佔用的內存。也就是說,一旦再也不須要,WeakMap 裏面的鍵名對象和所對應的鍵值對會自動消失,不用手動刪除引用。

const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>() /* 只讀的 */
const readonlyToRaw = new WeakMap<any, any>() /* 只讀的 */

vue3.0 用readonly來設置被攔截器攔截的對象可否被修改,能夠知足以前的props不能被修改的單向數據流場景。
咱們接下來重點講一下接下來的四個weakMap的儲存關係。

rawToReactive

鍵值對 : { [targetObject] : obseved }

target(鍵):目標對象值(這裏能夠理解爲reactive的第一個參數。)
obsered(值):通過proxy代理以後的proxy對象。

reactiveToRaw
reactiveToRaw 儲存的恰好與 rawToReactive的鍵值對是相反的。
鍵值對 { [obseved] : targetObject }

rawToReadonly

鍵值對 : { [target] : obseved }

target(鍵):目標對象。
obsered(值):通過proxy代理以後的只讀屬性的proxy對象。

readonlyToRaw
儲存狀態與rawToReadonly恰好相反。

reactive入口解析

接下來咱們重點從reactive開始講。

reactive({ ...object }) 入口

/* TODO: */
export function reactive(target: object) {
  if (readonlyToRaw.has(target)) {
    return target
  }
  return createReactiveObject(
    target,                   /* 目標對象 */
    rawToReactive,            /* { [targetObject] : obseved  }   */
    reactiveToRaw,            /* { [obseved] : targetObject }  */
    mutableHandlers,          /* 處理 基本數據類型 和 引用數據類型 */
    mutableCollectionHandlers /* 用於處理 Set, Map, WeakMap, WeakSet 類型 */
  )
}

reactive函數的做用就是經過createReactiveObject方法產生一個proxy,並且針對不一樣的數據類型給定了不一樣的處理方法。

createReactiveObject

以前說到的createReactiveObject,咱們接下來看看createReactiveObject發生了什麼。

const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  /* 判斷目標對象是否被effect */
  /* observed 爲通過 new Proxy代理的函數 */
  let observed = toProxy.get(target) /* { [target] : obseved  } */
  if (observed !== void 0) { /* 若是目標對象已經被響應式處理,那麼直接返回proxy的observed對象 */
    return observed
  }
  if (toRaw.has(target)) { /* { [observed] : target  } */
    return target
  }
  /* 若是目標對象是 Set, Map, WeakMap, WeakSet 類型,那麼 hander函數是 collectionHandlers 否側目標函數是baseHandlers */
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
   /* TODO: 建立響應式對象  */
  observed = new Proxy(target, handlers)
  /* target 和 observed 創建關聯 */
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  /* 返回observed對象 */
  return observed
}

經過上面源碼建立proxy對象的大體流程是這樣的:
①首先判斷目標對象有沒有被proxy響應式代理過,若是是那麼直接返回對象。
②而後經過判斷目標對象是不是[ Set, Map, WeakMap, WeakSet ]數據類型來選擇是用collectionHandlers , 仍是baseHandlers->就是reactive傳進來的mutableHandlers做爲proxy的hander對象。
③最後經過真正使用new proxy來建立一個observed ,而後經過rawToReactive reactiveToRaw 保存 target和observed鍵值對。

大體流程圖:

在這裏插入圖片描述

四 攔截器對象baseHandlers -> mutableHandlers

以前咱們介紹過baseHandlers就是調用reactive方法createReactiveObject傳進來的mutableHandlers對象。
咱們先來看一下mutableHandlers對象

mutableHandlers

攔截器的做用域

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

vue3.0 用到了以上幾個攔截器,咱們在上節已經介紹了這幾個攔截器的基本用法,首先咱們對幾個基本用到的攔截器在作一下回顧。

①get,對數據的讀取屬性進行攔截,包括 target.點語法 和 target[]

②set,對數據的存入屬性進行攔截 。

③deleteProperty delete操做符進行攔截。

vue2.0不能對對象的delete操做符進行屬性攔截。

例子🌰:

delete object.a

是沒法監測到的。

vue3.0proxy中deleteProperty 能夠攔截 delete 操做符,這就表述vue3.0響應式能夠監聽到屬性的刪除操做。

④has,對 in 操做符進行屬性攔截。

vue2.0不能對對象的in操做符進行屬性攔截。

例子

a in object

has 是爲了解決如上問題。這就表示了vue3.0能夠對 in 操做符 進行攔截。

⑤ownKeys Object.keys(proxy) ,for...in...循環 Object.getOwnPropertySymbols(proxy)Object.getOwnPropertyNames(proxy) 攔截器

例子

Object.keys(object)

說明vue3.0能夠對以上這些方法進行攔截。

五 組件初始化階段

若是咱們想要弄明白整個響應式原理。那麼組件初始化,到初始化過程當中composition-api的reactive處理data,以及編譯階段對data屬性進行依賴收集是分不開的。vue3.0提供了一套從初始化,到render過程當中依賴收集,到組件更新,到組件銷燬完整響應式體系,咱們很難從一個角度把東西講明白,因此在正式講攔截器對象如何收集依賴,派發更新以前,咱們看看effect作了些什麼操做。

1 effect -> 新的渲染watcher

vue3.0用effect反作用鉤子來代替vue2.0watcher。咱們都知道在vue2.0中,有渲染watcher專門負責數據變化後的重新渲染視圖。vue3.0改用effect來代替watcher達到一樣的效果。

咱們先簡單介紹一下mountComponent流程,後面的文章會詳細介紹mount階段的

1 mountComponent 初始化mountComponent

// 初始化組件
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 第一步: 建立component 實例   */
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    /* 第二步 : TODO:初始化 初始化組件,創建proxy , 根據字符竄模版獲得 */
    setupComponent(instance)
    /* 第三步:創建一個渲染effect,執行effect */
    setupRenderEffect(
      instance,     // 組件實例
      initialVNode, //vnode  
      container,    // 容器元素
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )   
  }

上面是整個mountComponent的主要分爲了三步,咱們這裏分別介紹一下每一個步驟幹了什麼:
① 第一步: 建立component 實例 。
② 第二步:初始化組件,創建proxy ,根據字符竄模版獲得render函數。生命週期鉤子函數處理等等
③ 第三步:創建一個渲染effect,執行effect。

從如上方法中咱們能夠看到,在setupComponent已經構建了響應式對象,可是尚未初始化收集依賴

2 setupRenderEffect 構建渲染effect

const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 建立一個渲染 effect */
    instance.update = effect(function componentEffect() {
      //...省去的內容後面會講到
    },{ scheduler: queueJob })
  }

爲了讓你們更清楚的明白響應式原理,我這隻保留了和響應式原理有關係的部分代碼。

setupRenderEffect的做用

① 建立一個effect,並把它賦值給組件實例的update方法,做爲渲染更新視圖用。
② componentEffect做爲回調函數形式傳遞給effect做爲第一個參數

3 effect作了些什麼

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  const effect = createReactiveEffect(fn, options)
  /* 若是不是懶加載 當即執行 effect函數 */
  if (!options.lazy) {
    effect()
  }
  return effect
}

effect做用以下

① 首先調用。createReactiveEffect
② 若是不是懶加載 當即執行 由createReactiveEffect建立出來的ReactiveEffect函數

4 ReactiveEffect

function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T, /**回調函數 */
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    try {
        enableTracking()
        effectStack.push(effect) //往effect數組中裏放入當前 effect
        activeEffect = effect //TODO: effect 賦值給當前的 activeEffect
        return fn(...args) //TODO:    fn 爲effect傳進來 componentEffect
      } finally {
        effectStack.pop() //完成依賴收集後從effect數組刪掉這個 effect
        resetTracking() 
        /* 將activeEffect還原到以前的effect */
        activeEffect = effectStack[effectStack.length - 1]
    }
  } as ReactiveEffect
  /* 配置一下初始化參數 */
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = [] /* TODO:用於收集相關依賴 */
  effect.options = options
  return effect
}

createReactiveEffect

createReactiveEffect的做用主要是配置了一些初始化的參數,而後包裝了以前傳進來的fn,重要的一點是把當前的effect賦值給了activeEffect,這一點很是重要,和收集依賴有着直接的關係

在這裏留下了一個疑點,

①爲何要用effectStack數組來存放這裏effect

總結

咱們這裏個響應式初始化階段進行總結

① setupComponent建立組件,調用composition-api,處理options(構建響應式)獲得Observer對象。

② 建立一個渲染effect,裏面包裝了真正的渲染方法componentEffect,添加一些effect初始化屬性。

③ 而後當即執行effect,而後將當前渲染effect賦值給activeEffect

最後咱們用一張圖來解釋一下整個流程。

在這裏插入圖片描述

六 依賴收集,get作了些什麼?

1 迴歸mutableHandlers中的get方法

1 不一樣類型的get

/* 深度get */
const get = /*#__PURE__*/ createGetter()
/* 淺get */
const shallowGet = /*#__PURE__*/ createGetter(false, true)
/* 只讀的get */
const readonlyGet = /*#__PURE__*/ createGetter(true)
/* 只讀的淺get */
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

上面咱們能夠知道,對於以前講的四種不一樣的創建響應式方法,對應了四種不一樣的get,下面是一一對應關係。

reactive ---------> get

shallowReactive --------> shallowGet

readonly ----------> readonlyGet

shallowReadonly ---------------> shallowReadonlyGet

四種方法都是調用了createGetter方法,只不過是參數的配置不一樣,咱們這裏那第一個get方法作參考,接下來探索一下createGetter。

createGetter

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    const res = Reflect.get(target, key, receiver)
    /* 淺邏輯 */
    if (shallow) {
      !isReadonly && track(target, TrackOpTypes.GET, key)
      return res
    }
    /* 數據綁定 */
    !isReadonly && track(target, TrackOpTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ?
          /* 只讀屬性 */
          readonly(res)
          /*  */
        : reactive(res)
      : res
  }
}

這就是createGetter主要流程,特殊的數據類型ref咱們暫時先不考慮。
這裏用了一些流程判斷,咱們用流程圖來講明一下這個函數主要作了什麼?

在這裏插入圖片描述

咱們能夠得出結論:
在vue2.0的時候。響應式是在初始化的時候就深層次遞歸處理了
可是

與vue2.0不一樣的是,即使是深度響應式咱們也只能在獲取上一級get以後才能觸發下一級的深度響應式。
好比

setup(){
 const state = reactive({ a:{ b:{} } })
 return {
     state
 }
}

在初始化的時候,只有a的一層級創建了響應式,b並無創建響應式,而當咱們用state.a的時候,纔會真正的將b也作響應式處理,也就是說咱們訪問了上一級屬性後,下一代屬性纔會真正意義上創建響應式

這樣作好處是,
1 初始化的時候不用遞歸去處理對象,形成了沒必要要的性能開銷。
*2 有一些沒有用上的state,這裏就不須要在深層次響應式處理。

2 track->依賴收集器

咱們先來看看track源碼:

track作了些什麼

/* target 對象自己 ,key屬性值  type 爲 'GET' */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  /* 當打印或者獲取屬性的時候 console.log(this.a) 是沒有activeEffect的 當前返回值爲0  */
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    /*  target -map-> depsMap  */
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    /* key : dep dep觀察者 */
    depsMap.set(key, (dep = new Set()))
  }
   /* 當前activeEffect */
  if (!dep.has(activeEffect)) {
    /* dep添加 activeEffect */
    dep.add(activeEffect)
    /* 每一個 activeEffect的deps 存放當前的dep */
    activeEffect.deps.push(dep)
  }
}

裏面主要引入了兩個概念 targetMapdepsMap

targetMap
鍵值對 proxy : depsMap
proxy : 爲reactive代理後的 Observer對象 。
depsMap :爲存放依賴dep的 map 映射。

depsMap
鍵值對:key : deps
key 爲當前get訪問的屬性名,
deps 存放effect的set數據類型。

咱們知道track做用大體是,首先根據 proxy對象,獲取存放deps的depsMap,而後經過訪問的屬性名key獲取對應的dep,而後將當前激活的effect存入當前dep收集依賴。

主要做用
①找到與當前proxy 和 key對應的dep。
②dep與當前activeEffect創建聯繫,收集依賴。

爲了方便理解,targetMapdepsMap的關係,下面咱們用一個例子來講明:
例子:
父組件A

<div id="app" >
  <span>{{ state.a }}</span>
  <span>{{ state.b }}</span>
<div>
<script>
const { createApp, reactive } = Vue

/* 子組件 */
const Children ={
    template="<div> <span>{{ state.c }}</span> </div>",
    setup(){
       const state = reactive({
          c:1
       })
       return {
           state
       }
    }
}
/* 父組件 */
createApp({
   component:{
       Children
   } 
   setup(){
       const state = reactive({
           a:1,
           b:2
       })
       return {
           state
       }
   }
})mount('#app')

</script>

咱們用一幅圖表示如上關係:

在這裏插入圖片描述

渲染effect函數如何觸發get

咱們在前面說過,建立一個渲染renderEffect,而後把賦值給activeEffect,最後執行renderEffect ,在這個期間是怎麼作依賴收集的呢,讓咱們一塊兒來看看,update函數中作了什麼,咱們回到以前講的componentEffect邏輯上來

function componentEffect() {
    if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, a, parent } = instance
        /* TODO: 觸發instance.render函數,造成樹結構 */
        const subTree = (instance.subTree = renderComponentRoot(instance))
        if (bm) {
          //觸發 beforeMount聲明週期鉤子
          invokeArrayFns(bm)
        }
        patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
        )
        /* 觸發聲明週期 mounted鉤子 */
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        instance.isMounted = true
      } else {
        // 更新組件邏輯
        // ......
      }
}

這邊代碼大體首先會經過renderComponentRoot方法造成樹結構,這裏要注意的是,咱們在最初mountComponent的setupComponent方法中,已經經過編譯方法compile編譯了template模版的內容,state.a state.b等抽象語法樹,最終返回的render函數在這個階段會被觸發,在render函數中在模版中的表達式 state.a state.b 點語法會被替換成data中真實的屬性,這時候就進行了真正的依賴收集,觸發了get方法。接下來就是觸發生命週期 beforeMount ,而後對整個樹結構從新patch,patch完畢後,調用mounted鉤子

依賴收集流程總結

① 首先執行renderEffect ,賦值給activeEffect ,調用renderComponentRoot方法,而後觸發render函數。

② 根據render函數,解析通過compile,語法樹處理事後的模版表達式,訪問真實的data屬性,觸發get。

③ get方法首先通過以前不一樣的reactive,經過track方法進行依賴收集。

④ track方法經過當前proxy對象target,和訪問的屬性名key來找到對應的dep。

⑤ 將dep與當前的activeEffect創建起聯繫。將activeEffect壓入dep數組中,(此時的dep中已經含有當前組件的渲染effect,這就是響應式的根本緣由)若是咱們觸發set,就能在數組中找到對應的effect,依次執行。

最後咱們用一個流程圖來表達一下依賴收集的流程。
在這裏插入圖片描述

七 set 派發更新

接下來咱們set部分邏輯。

const set = /*#__PURE__*/ createSetter()
/* 淺邏輯 */
const shallowSet = /*#__PURE__*/ createSetter(true)

set也是分兩個邏輯,set和shallowSet,兩種方法都是由createSetter產生,咱們這裏主要以set進行剖析。

createSetter建立set

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    /* shallowSet邏輯 */

    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    /* 判斷當前對象,和存在reactiveToRaw 裏面是否相等 */
    if (target === toRaw(receiver)) {
      if (!hadKey) { /* 新建屬性 */
        /*  TriggerOpTypes.ADD -> add */
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        /* 改變原有屬性 */
        /*  TriggerOpTypes.SET -> set */
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

createSetter的流程大體是這樣的

① 首先經過toRaw判斷當前的proxy對象和創建響應式存入reactiveToRaw的proxy對象是否相等。
② 判斷target有沒有當前key,若是存在的話,改變屬性,執行trigger(target, TriggerOpTypes.SET, key, value, oldValue)。
③ 若是當前key不存在,說明是賦值新屬性,執行trigger(target, TriggerOpTypes.ADD, key, value)。

trigger

/* 根據value值的改變,從effect和computer拿出對應的callback ,而後依次執行 */
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  /* 獲取depssMap */
  const depsMap = targetMap.get(target)
  /* 沒有通過依賴收集的 ,直接返回 */
  if (!depsMap) {
    return
  }
  const effects = new Set<ReactiveEffect>()        /* effect鉤子隊列 */
  const computedRunners = new Set<ReactiveEffect>() /* 計算屬性隊列 */
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || !shouldTrack) {
          if (effect.options.computed) { /* 處理computed邏輯 */
            computedRunners.add(effect)  /* 儲存對應的dep */
          } else {
            effects.add(effect)  /* 儲存對應的dep */
          }
        }
      })
    }
  }

  add(depsMap.get(key))

  const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) { /* 放進 scheduler 調度*/
      effect.options.scheduler(effect)
    } else {
      effect() /* 不存在調度狀況,直接執行effect */
    }
  }

  //TODO: 必須首先運行計算屬性的更新,以便計算的getter
  //在任何依賴於它們的正常更新effect運行以前,均可能失效。

  computedRunners.forEach(run) /* 依次執行computedRunners 回調*/
  effects.forEach(run) /* 依次執行 effect 回調( TODO: 裏面包括渲染effect )*/
}

咱們這裏保留了trigger的核心邏輯

① 首先從targetMap中,根據當前proxy找到與之對應的depsMap。
② 根據key找到depsMap中對應的deps,而後經過add方法分離出對應的effect回調函數和computed回調函數。
③ 依次執行computedRunners 和 effects 隊列裏面的回調函數,若是發現須要調度處理,放進scheduler事件調度

值得注意的的是:

此時的effect隊列中有咱們上述負責渲染的renderEffect,還有經過effectAPI創建的effect,以及經過watch造成的effect。咱們這裏只考慮到渲染effect。至於後面的狀況會在接下來的文章中和你們一塊兒分享。

咱們用一幅流程圖說明一下set過程。

在這裏插入圖片描述

八 總結

咱們總結一下整個數據綁定創建響應式大體分爲三個階段

1 初始化階段: 初始化階段經過組件初始化方法造成對應的proxy對象,而後造成一個負責渲染的effect。

2 get依賴收集階段:經過解析template,替換真實data屬性,來觸發get,而後經過stack方法,經過proxy對象和key造成對應的deps,將負責渲染的effect存入deps。(這個過程還有其餘的effect,好比watchEffect存入deps中 )。

3 set派發更新階段:當咱們 this[key] = value 改變屬性的時候,首先經過trigger方法,經過proxy對象和key找到對應的deps,而後給deps分類分紅computedRunners和effect,而後依次執行,若是須要調度的,直接放入調度。

微信掃碼關注公衆號,按期分享技術文章

在這裏插入圖片描述

相關文章
相關標籤/搜索