vu3.0 數據響應式(有彩蛋)

彩蛋來了 寫在前面,最近打算學習vue3.0 相關知識,本着學習一個東西,最好方法就是模仿寫一個,因此本身動手寫了一個簡化版vue3.0,本身稱做mini-vue3.0 感受對vue3.0 或者 vue2.x核心原理的理解有很大幫助,因此分享出來。mini-vue3.0主要包括:模板編譯、響應式、組件渲染過程等, 倉庫地址mini-vue3.0,歡迎starvue

響應式原理

Proxy

衆所周知,vue2.x響應式是基於Object.defineProperty的數據劫持來實現的,而在vue3.0 中則採用新的ES6 API Proxy來作數據劫持。react

具體的Proxy用法本文就不作詳述了,具體能夠參考Proxy,這裏簡單介紹一下Proxy 優缺點。git

優勢:es6

  • 能夠劫持對象新加屬性
    const obj = {
        a: 1
     }
     const proObj = new Proxy(obj, ...)
     proObj[b] = 2 // Object.defineProperty 是不能劫持的,而Proxy 能夠劫持
    複製代碼
  • 能夠劫持數組的push、shift等相關操做
    const ary = [1]
     const proObj = new Proxy(ary, ...)
     proObj[1] = 2 // Object.defineProperty 是不能劫持的,而Proxy 能夠劫持
    複製代碼

缺點:github

  • 不能深度劫持對象屬性
  • 可能會觸發屢次的數據劫持調用
const ary = [1, 2, 3]
 const proObj = new Proxy(ary, ...)
 proObj.slice(1, 0, 4) // 插入一個數字4 ,會觸發屢次proObj 數據劫持更新
 ``` ## vue3.0 響應式原理解析 在分析vue 響應式原理時,須要時刻牢記觀察者模式(發佈/訂閱模式)。很簡單理解,就是一個對象存儲回調,而後在適當時機觸發回調。 本質的思想仍是比較簡單。接下來,咱們簡單實現一個數據響應式功能。 ### 數據劫持 這裏主要是作數據攔截,當渲染模板時候,訪問響應式數據時,會作依賴收集。簡單實現以下,代碼都有詳細註釋 ```js
// 一些輔助工具函數
const isObject = (val) => val !== null && typeof val === 'object'
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (obj, key) => hasOwnProperty.call(obj, key)

const toRaw = new WeakMap() // raw -> proxy 對象映射
const toProxy = new WeakMap() // proxy -> raw 對象映射
const targetMap = new Map() // 回調收集Map

// 設置響應式
const reactive = (obj) => {
// 若是已是代理過的對象,直接返回代理對象
if (toProxy.has(obj)) {
 return toProxy.get(obj)
}
// 若是已是代理對象,直接返回代理對象
if (toRaw.has(obj)) {
 return obj
}
// 注意Proxy 只能代理到一層
const proxy = new Proxy(obj, {
 get(target, key, receiver) {
   track(target, key) // 這裏是依賴收集,具體邏輯見下文
   const value = Reflect.get(target, key, receiver)
   return isObject(value) ? reactive(value) : value
 },
 set(target, key, value, receiver) {
   const oldValue = Reflect.get(target, key, receiver)
   value = toRaw.get(value) || value
   const observed = Reflect.set(target, key, value, receiver)
   // 解決數組屢次觸發問題
   if (!hasOwn(target, key)) {
     trigger(target, key)
   } else if (value !== oldValue) {
     trigger(target, key)
   }
   if (!targetMap.has(target)) {
     // 設置對象回調函數Map
     targetMap.set(target, new Map())
   }
   return observed
 }
})
toRaw.set(proxy, obj)
toProxy.set(obj, proxy)
return proxy
}
複製代碼

依賴收集

// 依賴收集
const track = (target, key) => {
  // 獲取對象回調函數Map
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 獲取對象,對應屬性的回調函數Set
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 這裏的 activeEffect 其實就是渲染函數,你能夠認爲就是 render 函數
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
  }
}

複製代碼

觸發執行

// 這裏觸發執行觸發執行
const trigger = (target, key) => {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  const effects = depsMap.get(key)
  effects.forEach(effect => effect())
}
複製代碼

簡單舉個例子說明一下:數組

// 例如將以下數據設置響應式
const data = {
  a: 1
}
// 設置數據響應式
const proxyData = reactive(data);

// 此時

toProxy = {
  data: proxyData
}

toRaw = {
  proxyData: data
}

// 當咱們訪問數據屬性時候

activeEffect = () => {
  console.log(proxyData.a)
}
activeEffect()

// 會收集回調函數 activeEffect
targetMap = {
  data: {
    a: [activeEffect]
  }
}

// 咱們改變響應數據
proxyData.a = 2

// 則會觸發執行,開始從新收集依賴
targetMap[data][a].forEach(cb => cb())

複製代碼

以上只是簡單說明vue3.0 響應式核心原理,vue 3.0數據源代碼實現複雜的多,有興趣同窗能夠自行了解。有了上面基礎,想必會更加容易了緩存

ref、computed實現原理

介紹一個數據響應式原理,這裏再簡單介紹一下ref、computed的原理函數

ref實現原理

爲何會須要ref函數?

由於reactive 和 ref知足兩種代碼風格工具

  1. reactive 風格
const reac = reactive({
  a: 1,
  b: 2
})

複製代碼
  1. ref 風格
const a = ref(1)
 const b = ref(2)
複製代碼

一個典型實際應用例子,好比咱們常常在頁面設置各類loading,控制加載。源碼分析

// 風格1
const loading = {
  a: false,
  b: false
}
// 風格2
let loadingA = false
let loadingB = false

複製代碼
ref 源碼分析
function ref(raw) {
    // 判斷是否已經通過ref 處理
    if (isRef(raw)) {
      return raw
    }
    // 若是值爲對象,設置數據響應式
    raw = reactive(raw)
    const r = {
      _isRef: true,
      get value() {
        // 依賴收集
        track(r, TrackOpTypes.GET, 'value')
        return raw
      },
      set value(newVal) {
        raw = reactive(newVal)
        // 觸發響應式回調
        trigger(
          r,
          TriggerOpTypes.SET,
          'value',
        )
      }
    }
    return r
  }
複製代碼

其實ref實現原理比較簡單,就是在原始數據外面再包一層代理,實現響應式

computed實現原理

如下爲 computed 簡單實現代碼

function computed(getterOrOptions) let getter let setter // 參數若是爲函數的話,默認爲getter 函數 if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = () => {
        console.warn('Write operation failed: computed value is readonly')
      }
     
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  let dirty = true
  let value
  // effect 等價於 vue2.x 中 watcher
  const runner = effect(getter, {
    lazy: true, // 不會當即執行,因此computed 能夠起到緩存的做用
    computed: true,
    scheduler: () => {
      dirty = true
    }
  })
  return {
    _isRef: true,
    get value() {
      // 爲dirty時候纔會從新求值
      if (dirty) {
        value = runner()
        dirty = false
      }
      // 具體做用見下文分析
      trackChildRun(runner)
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  }
}
複製代碼
  1. 舉例分析:
const b = reactive({ a: 1})
const c = computed(() => b.a)

複製代碼

當改變b的值時候,如 b.a = 2。此時只會觸發computed的scheduler,設置dirty =true

只有當訪問 c.value 值時,纔會觸發computed的get代理,執行runner函數,從新計算求值

  1. trackChildRun做用:實現鏈式計算屬性,父effect會記錄computed的runner記錄的dep回調函數,從而實現鏈式計算屬性

這裏舉個例子說明,會更清晰一些

const obj = {a: 1}
const objProxy = reactive(obj)
const comp = computed(() =>  { console.log(objProxy.a)} )

// 當咱們訪問 comp
comp.value

// obj1 響應式依賴收集時,得到計算屬性的 runner函數,做爲回調
targetMap[obj].a = [runner]

// 渲染模板
//<div>{{comp.value}}</div>

// 調用render 函數後
targetMap[obj].a = [runner, render]

objProxy.a = 2

同時觸發計算屬性表達式從新求值、模板更新,達到鏈式調用

複製代碼
相關文章
相關標籤/搜索