彩蛋來了 寫在前面,最近打算學習vue3.0 相關知識,本着學習一個東西,最好方法就是模仿寫一個,因此本身動手寫了一個簡化版vue3.0,本身稱做mini-vue3.0 感受對vue3.0 或者 vue2.x核心原理的理解有很大幫助,因此分享出來。mini-vue3.0主要包括:模板編譯、響應式、組件渲染過程等, 倉庫地址mini-vue3.0,歡迎starvue
衆所周知,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 能夠劫持
複製代碼
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的原理函數
由於reactive 和 ref知足兩種代碼風格工具
const reac = reactive({
a: 1,
b: 2
})
複製代碼
const a = ref(1)
const b = ref(2)
複製代碼
一個典型實際應用例子,好比咱們常常在頁面設置各類loading,控制加載。源碼分析
// 風格1
const loading = {
a: false,
b: false
}
// 風格2
let loadingA = false
let loadingB = false
複製代碼
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 簡單實現代碼
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)
}
}
}
複製代碼
const b = reactive({ a: 1})
const c = computed(() => b.a)
複製代碼
當改變b的值時候,如 b.a = 2。此時只會觸發computed的scheduler,設置dirty =true
只有當訪問 c.value 值時,纔會觸發computed的get代理,執行runner函數,從新計算求值
這裏舉個例子說明,會更清晰一些
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
同時觸發計算屬性表達式從新求值、模板更新,達到鏈式調用
複製代碼