標(biāo)簽(空格分隔): vue 前端
前言
首先自己實現(xiàn)了一遍 reactive 的兩個api始锚, 對依賴變化的監(jiān)測有了一定的了解遇汞, 現(xiàn)在再看看源碼是怎么寫。 為了更好理解涧窒, 自己按著源碼重新寫一遍。
重寫源碼
以下代碼可直接復(fù)制到一個 html 文件上運行锭亏。
或者直接在 codepen 上查看
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>vue3 reactive demo</title>
</head>
<body>
<div id="app">
</div>
<div id="app2"></div>
</body>
<!-- core -->
<script>
function isObject(obj) {
return typeof obj === 'object'
}
function isArray(obj) {
return obj instanceof Array
}
function isRef(raw) {
return raw ? !!raw._isRef : false
}
/** 監(jiān)聽原始值 */
function ref(primitive) {
// 是否已經(jīng)包裝過
if (isRef(primitive)) { return primitive }
// object 類型使用 reactive 包裝纠吴,其他不用變
const convertObj = (raw) => isObject(raw) ? reactive(raw) : raw
primitive = convertObj(primitive)
// 將原始值包裝起來
const wrapper = {
_isRef: true,
get value() {
// 收集依賴
track(wrapper, 'get', 'value')
return primitive
},
set value(v) {
primitive = convertObj(v)
trigger(wrapper, 'set', 'value')
}
}
return wrapper
}
// 原始對象 與 代理對象 互相映射,緩存
const rawToReactive = new WeakMap()
const reactiveToRaw = new WeakMap()
const isReative = (obj) => reactiveToRaw.has(obj)
/** 對象類型 響應(yīng)式包裝 */
function reactive(obj) {
if (!isObject(obj)) { return obj }
const cache = rawToReactive.get(obj)
// 原始對象已經(jīng)包裝過
if (cache) { return cache }
// 已經(jīng)是代理對象
if (isReative(obj)) { return obj }
const proxy = new Proxy(obj, {
get(obj, key, receiver) {
const res = Reflect.get(obj, key, receiver)
// 收集依賴
track(obj, 'get', key)
// 遞歸包裝子對象
return isObject(res) ? reactive(res) : res
},
set(obj, key, newVal, receiver) {
const hasKey = Reflect.has(obj, key)
const oldVal = obj[key]
const isChanged = oldVal !== newVal
const res = Reflect.set(obj, key, newVal, receiver)
// 如果當(dāng)前對象其實在原型鏈上被設(shè)值慧瘤,就不通知訂閱
if (obj === rawToReactive.get(receiver)) { return res}
if (!hasKey) {
// add
trigger(obj, 'add', key, newVal)
} else if (isChanged) {
// set
trigger(obj, 'set', key, newVal)
}
return res
},
has(obj, key) {
track(obj, 'has', key)
return Reflect.has(obj, key)
},
ownKeys(obj) {
track(obj, 'iterate')
return Reflect.ownKeys(obj)
}
})
rawToReactive.set(obj, proxy)
reactiveToRaw.set(proxy, obj)
// 這里其實不是必須戴已,track 的時候會新建
// if (!targetMap.get(obj)) { targetMap.set(obj, new Map())}
return proxy
}
/** 儲存訂閱 WeakMap<object, Map<key, Set<Function>>> */
const targetMap = new WeakMap()
/** 數(shù)組各 index(012345...) 的訂閱統(tǒng)一到該鍵上 */
const ITERATE_KEY = Symbol('iterate')
/** 收集依賴 */
function track(obj, type, key = '') {
console.log('track key', key)
// 沒有當(dāng)前需要收集的訂閱事件固该,就不需要收集
if (effectStack.length === 0) { return }
// ownKeys 時收集
if (type === 'iterate') {
key = ITERATE_KEY
}
// 存取訂閱到對應(yīng)位置
let target = targetMap.get(obj)
if (!target) {
targetMap.set(obj, target = new Map())
}
let deps = target.get(key)
if (!deps) {
console.log('key', key)
target.set(key, deps= new Set())
}
const currentEffect = effectStack[effectStack.length - 1]
// 如果已經(jīng)訂閱,就不收集
if (deps.has(currentEffect)) { return }
deps.add(currentEffect)
currentEffect.deps.push(deps)
}
// setter 時通知訂閱
function trigger(obj, type, key, newValue) {
const target = targetMap.get(obj)
if (!target) { return }
// 區(qū)分兩種訂閱糖儡,先通知 computed
const computedRunners = new Set()
const effects = new Set()
const addRunners = (key) => {
const depList = target.get(key)
if (!depList) { return }
depList.forEach(effect => {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
})
}
console.log('trigger key', key)
// 普通修改先加 key
if (key != void 0) { addRunners(key) }
// 屬性添加 刪除 操作還有通知對應(yīng)的 key
// 數(shù)組屬性 0 1 2 3統(tǒng)一為一個 IterateKey
if (type === 'add' || type === 'delete') {
const iterationKey = isArray(obj) ? 'length' : ITERATE_KEY
addRunners(iterationKey)
}
const run = (effect) => {
// 自定義執(zhí)行方式伐坏,主要是 computed 需要自定義
if (effect.options.scheduler !== void 0) {
effect.options.scheduler(effect)
} else {
effect()
}
}
// 先執(zhí)行 computed
computedRunners.forEach(run)
effects.forEach(run)
}
/** 存儲當(dāng)前需要收集依賴的訂閱,暫時性 */
const effectStack = []
const isEffect = (e) => !!e._isEffect
function effect(fn, opt = {}) {
if (isEffect(fn)) {
fn = fn.raw
}
// 包裝 訂閱函數(shù)
// 這樣每次執(zhí)行回調(diào)函數(shù)都會收集依賴
const effectWrapper = function reactiveEffect(...args) {
if (!effectStack.includes(effectWrapper)) {
// 去掉所有訂閱列表中當(dāng)前 effect
effectWrapper.deps.forEach(dep => {
dep.delete(effectWrapper)
})
effectWrapper.deps.length = 0
try {
// 當(dāng)前要訂閱的 effect, 重新收集
effectStack.push(effectWrapper)
// 開始收集
return fn(...args)
} finally {
// 收集完清理
effectStack.pop()
}
}
}
effectWrapper._isEffect = true
effectWrapper.raw = fn
effectWrapper.deps = []
effectWrapper.options = opt
if (!opt.lazy) {
effectWrapper()
}
return effectWrapper
}
// computed 是一個有 effect 的 ref
function computed(fnOrObj) {
let getter, setter
if (typeof fnOrObj === 'object') {
getter = fnOrObj.gettter
setter = fnOrObj.setter
} else {
getter = fnOrObj
}
let value
// 臟值檢查休玩,第一次要設(shè)為true著淆,
// 這樣第一次get的時候 才會跑一下 runner 收集到訂閱的事件
let dirty = true
const runner = effect(getter, {
computed: true,
lazy: true,
// 自定義訂閱的執(zhí)行方式,這里意思是依賴發(fā)送通知時拴疤,不執(zhí)行永部,但標(biāo)記為臟值。
// 延遲到 getter 時才執(zhí)行
scheduler:() => { dirty = true }
})
return {
_isRef: true,
// 暴露 effect 用于停止監(jiān)聽
effect: runner,
get value() {
if (dirty) {
dirty = false
value = runner()
}
// 將依賴 computed 的訂閱函數(shù) 記錄到對應(yīng)列表
if (effectStack.length !== 0) {
const currentEffect = effectStack[effectStack.length - 1]
runner.deps.forEach(dep => {
if (!dep.has(currentEffect)) {
dep.add(currentEffect)
currentEffect.deps.push(dep)
}
})
}
return value
},
set value(newVal) {
if (setter) setter(newVal)
}
}
}
</script>
<!-- example -->
<script>
function setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
const state = reactive({
text: 'hello',
obj: {
a: 1,
b: 2,
},
arr: [1, 2, 3]
})
return {
count,
double,
addCount: () => { count.value ++ },
state,
changeState: () => {
console.log(targetMap)
state.text = Math.random().toFixed(2)
state.obj.a ++
state.obj.b --
state.arr.push(Math.random().toFixed(2) * 100)
}
}
}
function main() {
// 渲染上下文
const ctx = setup()
// 模板渲染呐矾,事件綁定
const app = document.querySelector('#app')
const app2 = document.querySelector('#app2')
window.changeState = ctx.changeState
window.addCount = ctx.addCount
effect(() => {
app.innerHTML = `
<p><button onclick="changeState()">change</button></p>
<p>state: ${JSON.stringify(ctx.state)}</p>
<p><button onclick="addCount()">count: ${ctx.count.value}</button></p>
<p>double: ${ctx.double.value}</p>
`
})
effect(() => {
console.log("=== app2 changed ===")
app2.innerHTML = `arr: ${JSON.stringify(ctx.state.arr)}`
})
}
window.onload = main
</script>
</html>