vue3.0 pre-alpha之reactivity源碼解析

上一篇文章中介紹瞭如何調試vue-next。接下來開始解讀vue-nextreactivity模塊vue

vue3.0中比較大的改動之一就是響應式的實現有Object.defineProperty改成Proxy實現。閱讀以前能夠先提早了解下Proxyreact

Object.definePropertyObject偵聽須要遍歷遞歸全部的key。因此在vue2.x中須要偵聽的數據須要先在data中定義,新增響應數據也須要使用$set來添加偵聽。並且對Array的偵聽也存在必定的問題。在vue3.0就能夠不用考慮這些問題。bash

用法

先從單元測試瞭解reactive用法

vue3.0中響應式代碼被放到單獨的模塊,代碼在/packages/reactivity目錄下。每一個模塊的單元測試都放在__tests__文件夾下。找到reactive.spec.ts。代碼以下數據結構

import { reactive, isReactive, toRaw, markNonReactive } from '../src/reactive'
import { mockWarn } from '@vue/runtime-test'

describe('reactivity/reactive', () => {
  mockWarn()

  test('Object', () => {
    const original = { foo: 1 }
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(isReactive(observed)).toBe(true)
    expect(isReactive(original)).toBe(false)
    // get
    expect(observed.foo).toBe(1)
    // has
    expect('foo' in observed).toBe(true)
    // ownKeys
    expect(Object.keys(observed)).toEqual(['foo'])
  })

  test('Array', () => {
    const original: any[] = [{ foo: 1 }]
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(isReactive(observed)).toBe(true)
    expect(isReactive(original)).toBe(false)
    expect(isReactive(observed[0])).toBe(true)
    // get
    expect(observed[0].foo).toBe(1)
    // has
    expect(0 in observed).toBe(true)
    // ownKeys
    expect(Object.keys(observed)).toEqual(['0'])
  })
  // ...
})

複製代碼

能夠大體看到reactive.ts提供了以下方法:app

  • reactive: 將原始數據轉化爲可響應的對象,即Proxy對象。支持原始數據類型:Object|Array|Map|Set|WeakMap|WeakSet
  • isReactive: 判斷是否可響應數據
  • toRaw:講可相應數據轉化爲原始數據。
  • markNonReactive:標記數據爲不可響應。

結合effect使用

常常和reactive結合起來使用的是effect,它是偵聽到數據變化後的回調函數。effect單元測試以下:函數

import {
  reactive,
  effect,
  stop,
  toRaw,
  OperationTypes,
  DebuggerEvent,
  markNonReactive
} from '../src/index'
import { ITERATE_KEY } from '../src/effect'

describe('reactivity/effect', () => {
  it('should run the passed function once (wrapped by a effect)', () => {
    const fnSpy = jest.fn(() => {})
    effect(fnSpy)
    expect(fnSpy).toHaveBeenCalledTimes(1)
  })

  it('should observe basic properties', () => {
    let dummy
    const counter = reactive({ num: 0 })
    effect(() => (dummy = counter.num))

    expect(dummy).toBe(0)
    counter.num = 7
    expect(dummy).toBe(7)
  })

  it('should observe multiple properties', () => {
    let dummy
    const counter = reactive({ num1: 0, num2: 0 })
    effect(() => (dummy = counter.num1 + counter.num1 + counter.num2))

    expect(dummy).toBe(0)
    counter.num1 = counter.num2 = 7
    expect(dummy).toBe(21)
  })
})

複製代碼

可總結出reactive + effect的使用方法:post

import { reactive, effect } from 'dist/reactivity.global.js'
let dummy
<!-- reactive監聽對象 -->
const counter = reactive({ num: 0 })
<!-- 數據變更回調effect -->
effect(() => (dummy = counter.num))
複製代碼

原理

從單元測試中能夠發現,reactive函數和effect分別在reactive.tseffect.ts。接下來咱們從這兩個文件開始着手瞭解reactivity的源碼。單元測試

reactive + effect原理解析

參考下面這個例子,看看裏面都作了什麼。測試

import { reactive, effect } from 'dist/reactivity.global.js'
const counter = reactive({ num: 0, times: 0 })
effect(() => {console.log(counter.num)})
counter.num = 1
複製代碼
  • 調用reactive()會生成一個Proxy對象counter
  • 調用effect()時會默認調用一次內部函數() => {console.log(counter.num)}(下文以fn代替),運行fn時會觸發counter.numget trapget trap觸發track(),會在targetMap中增長num依賴。
// targetMap 存儲依賴關係,相似如下結構,這個結構會在 effect 文件中被用到
// {
//   target: {
//     key: Dep
//   }
// }
// 解釋下三者究竟是什麼:target 就是被 proxy 的對象,key 是對象觸發 get 行爲之後的屬性
// export type Dep = Set<ReactiveEffect>
// export type KeyToDepMap = Map<string | symbol, Dep>
// export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()

// get以後targetMap值
{
    counter: {
        num: [fn]
    }
}

複製代碼
  • counter.num = 1,會觸發counterset trap trap,判斷num的值和oldValue不一致後,觸發trigger(),trigger中在targetMap中找到targetMap.counter.num的回調函數是fn。回調執行fn

思考:若是改變了counter.times的值,回調函數fn:() => {console.log(counter.num)}會不會執行呢?爲何?’ui

再次執行counter.num = 1num的值未改變,fn會不會執行呢?

源代碼解析

reactive函數

reactice中核心代碼是createReactiveObject,做用是建立一個proxy對象

reactive(target: object) {
  // 不是 readonly 就建立一個響應式對象,建立出來的對象和源對象不等
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
複製代碼

createReactiveObject

使用proxy建立一個代理對象。判斷對象的構造函數得出 handlers,集合類和別的類型用到的 handler 不同。collectionTypes的值爲Set, Map, WeakMap, WeakSet使用collectionHandlers。Object和Array使用baseHandlers

function createReactiveObject() {
 // 判斷對象的構造函數得出 handlers,集合類和別的類型用到的 handler 不同
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // 建立 proxy 對象,這裏主要要看 handlers 的處理了
  // 因此咱們去 handlers 的具體實現文件夾吧,先看 baseHandlers 的
  // 另外不熟悉 proxy 用法的,能夠先熟悉下文檔 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
  observed = new Proxy(target, handlers)
  return observed
 }
複製代碼

mutableHandlers(handler)

mutableHandlers: ProxyHandler<any> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}
複製代碼

handler的get方法

使用Reflect.get獲取get的原始值,若是此值是對象,則遞歸返回具體的proxy對象。track()作的事情就是塞依賴到 targetMap 中,用於下次尋找是否有這個依賴,另外就是把 effect 的回調保存起來

function createGetter(isReadonly: boolean) {
  return function get(target: any, key: string | symbol, receiver: any) {
    // 得到結果
    const res = Reflect.get(target, key, receiver)
    // ....
   
    // 這個函數作的事情就是塞依賴到 map 中,用於下次尋找是否有這個依賴
    // 另外就是把 effect 的回調保存起來
    track(target, OperationTypes.GET, key)
    // 判斷get的值是否爲對象,是的話將對象包裝成 proxy(遞歸)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}
複製代碼

handler的set方法

核心邏輯是trigger

function set(
  target: any,
  key: string | symbol,
  value: any,
  receiver: any
): boolean {
  // ...
  const result = Reflect.set(target, key, value, receiver)
  // ...
  // don't trigger if target is something up in the prototype chain of original // set 行爲核心邏輯是 trigger if (!hadKey) { trigger(target, OperationTypes.ADD, key) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key) } return result } 複製代碼

trigger方法

targetMap的數據結構以下,用來存儲依賴關係。 若是修改方式是CLEAR,執行全部的回調。不然執行存儲的回調。另外ADDDELETE會執行某些特殊的回調。

// targetMap 存儲依賴關係,相似如下結構,這個結構會在 effect 文件中被用到
// {
//   target: {
//     key: Dep
//   }
// }
// 解釋下三者究竟是什麼:target 就是被 proxy 的對象,key 是對象觸發 get 行爲之後的屬性
// 好比 counter.num 觸發了 get 行爲,num 就是 key。dep 是回調函數,也就是 effect 中調用了 counter.num 的話
// 這個回調就是 dep,須要收集起來下次使用。
複製代碼
function trigger(
  target: any,
  type: OperationTypes,
  key?: string | symbol,
  extraInfo?: any
) {
  const depsMap = targetMap.get(target)
  // ...
  const effects: Set<ReactiveEffect> = new Set()
  const computedRunners: Set<ReactiveEffect> = new Set()
  if (type === OperationTypes.CLEAR) {
    // collection being cleared, trigger all effects for target
    depsMap.forEach(dep => {
      addRunners(effects, computedRunners, dep)
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    // depsMap.get(key) 取出依賴回調
    if (key !== void 0) {
      // 把依賴回調丟到 effects 中
      addRunners(effects, computedRunners, depsMap.get(key as string | symbol))
    }
    // also run for iteration key on ADD | DELETE
    if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
      const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  const run = (effect: ReactiveEffect) => {
    // 簡單點,就是執行回調函數
    scheduleRun(effect, target, type, key, extraInfo)
  }
  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  computedRunners.forEach(run)
  effects.forEach(run)
}
複製代碼

effect方法

effect在非lazy的狀況下會直接調用effect也就是傳入fn,根據fn生成targetMap依賴。當依賴中的數據發生變化時會回調fn

export function effect(
  fn: Function,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
  // 判斷回調是否已經包裝過
  if ((fn as ReactiveEffect).isEffect) {
    fn = (fn as ReactiveEffect).raw
  }
  // 包裝回調,effect其實就是fn方法,在fn函數身上掛了不少屬性。
  const effect = createReactiveEffect(fn, options)
  // 不是 lazy 的話會直接調用一次。可是lazy狀況下,不調用effect,故而不會生成targetMap依賴。致使不能回調。不知道這是否是一個bug?
  if (!options.lazy) {
    effect()
  }
  // 返回值用以 stop
  return effect
}
複製代碼
相關文章
相關標籤/搜索