hdcljt / learn_vue3_reactivity

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

简单实现 Vue3 源码

当前 vue-next 版本 3.0.0-rc.10。 参考源码,简单实现一下 reactive, effect, computed(目的加深自己的理解)。

了解 Proxy/Reflect

  • Proxy 【代理】代理目标对象,自定义行为
  • Reflect 【反射】对自身行为的描述
const createProxy = obj =>
    new Proxy(obj, {
        get(target, key, receiver) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.get(...arguments) // 读取target对象key属性的值(target[key])
        },
        set(target, key, value, receiver) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.set(...arguments) // 给target对象的key属性赋值value(target[key] = value)
        },
        has(target, key) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.has(...arguments) // 属性key是否在target对象或其原型链中(key in target)
        },
        ownKeys(target) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.ownKeys(...arguments) // 获取自身属性的数组(Object.keys/Reflect.ownKeys/Object.getOwnPropertyNames/Object.getOwnPropertySymbols)
        },
        deleteProperty(target, key) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.deleteProperty(...arguments) // 删除target对象的key属性(delete target[key])
        },
        defineProperty(target, key, descriptor) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.defineProperty(...arguments) // 给target对象定义key属性,描述符选项为descriptor
        },
        getOwnPropertyDescriptor(target, key) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.getOwnPropertyDescriptor(...arguments) // 获取key的属性描述符
        },
        getPrototypeOf(target) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.getPrototypeOf(...arguments) // 读取target原型
        },
        setPrototypeOf(target, proto) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.setPrototypeOf(...arguments) // 设置target的原型为proto
        },
        isExtensible(target) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.isExtensible(...arguments) // target对象是否可扩展(被freeze/seal/preventExtensions方法标记过)
        },
        preventExtensions(target) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.preventExtensions(...arguments) // 阻止target对象扩展属性
        },
        apply(target, thisArg, argList) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.apply(...arguments) // target作为函数被调用
        },
        construct(target, argList, newTarget) {
            console.log(`[${arguments.callee.name}]`, ...arguments)
            return Reflect.construct(...arguments) // target作为构造函数创建实例(new)
        }
    })
  • defineProperty(target, key, descriptor)
    • descriptor
      • configurable 删(修改描述符),默认 false
      • enumerable 查,默认 falsefor...in/Object.keys/Object.assign/{...obj}
      • writable 改,默认 false
      • value 增,默认 undefined
      • get() / set(value) 增/改,默认 undefined(跟 value/writable 互斥)

响应式的基本原理

new Proxy(target, {
    get(target, key, receiver) {
        track(target, key) // 【跟踪】收集依赖该属性的回调函数
        return Reflect.get(...arguments)
    },
    set(target, key, value, receiver) {
        const res = Reflect.set(...arguments)
        trigger(target, key) // 【触发】执行收集的函数(数据更新后)
        return res
    }
})

实现简单的响应式系统

基于 vue@3.0.0-rc.10 版本的简化,代码仓库在此

最基础的响应式

  • reactivity.js (核心是 reactiveeffect
/** @type {ProxyHandler} */
const baseHandlers = {
    get(target, key, receiver) {
        track(target, key) // 读取属性的时候,收集依赖
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        Reflect.set(target, key, value, receiver)
        trigger(target, key) // 使用修改后的属性值,通知更新
        return true
    }
}

function reactive(target) {
    return new Proxy(target, baseHandlers)
}

/** @type {Function} */
let activeEffect

/** @param {Function} fn */
function effect(fn) {
    try {
        activeEffect = fn
        fn() // 立即执行,观察该回调函数使用到的所有依赖,并被各个依赖项所绑定
    } finally {
        activeEffect = null
    }
}

/** @type {WeakMap<object, Map<string, Set<Function>>>} */
const targetMap = new WeakMap() // 缓存:当属性值修改时,需要触发更新的回调集合

/**
 *  targetMap: WeakMap, depsMap: Map, deps: Set
 *  targetMap.set(target, depsMap)
 *  depsMap.set(key, deps)
 *  deps.add(activeEffect)
 *  {
 *     [target] => {
 *         ...,
 *         [key] => [
 *             ...,
 *             activeEffect
 *         ]
 *     },
 *  }
 */
function track(target, key) {
    if (!activeEffect) return // 没有回调需要依赖此属性
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map())) // 缓存对象
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, (deps = new Set())) // 缓存属性
    }
    if (!deps.has(activeEffect)) {
        deps.add(activeEffect) // 绑定依赖此属性的回调
    }
}

function trigger(target, key) {
    const depsMap = targetMap.get(target)
    if (!depsMap) return // 没有回调需要依赖此对象
    const deps = depsMap.get(key)
    if (!deps) return // 没有回调需要依赖此属性
    deps.forEach(dep => dep()) // 重新执行依赖此属性的所有回调
}

export { reactive, effect }
  • demo
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Demo</title>
        <style>
            body {
                display: flex;
                flex-direction: column;
                align-items: center;
                font-size: x-large;
            }
            label {
                display: flex;
                align-items: center;
            }
        </style>
    </head>
    <body>
        <label>Range 1:<input type="range" min="0" max="100" id="r1" /></label>
        <label>Range 2:<input type="range" min="0" max="100" id="r2" /></label>
        <div id="text"></div>
        <script type="module">
            import { reactive, effect } from './reactivity.js'
            const obj = reactive({ r: 20 })
            effect(() => {
                r1.value = obj.r
                text.innerHTML = obj.r
            })
            r1.oninput = () => {
                obj.r = r1.value
            }
            r2.oninput = () => {
                obj.r = r2.value
            }
        </script>
    </body>
</html>

嵌套(对象属性依旧是对象)

+ const isObject = val => val !== null && typeof val === 'object'
const baseHandlers = {
    get(target, key, receiver) {
+       const res = Reflect.get(target, key, receiver) // 和set保持格式一致
        track(target, key) // 读取属性的时候,收集依赖
-       return Reflect.get(target, key, receiver)
+       return isObject(res) ? reactive(res) : res // 值是对象则递归代理
    },
    set(target, key, value, receiver) {
-       Reflect.set(target, key, value, receiver)
+       const res = Reflect.set(target, key, value, receiver)
        trigger(target, key) // 使用修改后的属性值,通知更新
-       return true
+       return res // 是否修改成功
    }
}

计算属性 computed

+ /** @type {Function[]} */
+ const effectStack = []
/** @type {Function} */
let activeEffect
+ let uid = 0

- function effect(fn) {
-     try {
-         activeEffect = fn
-         fn() // 立即执行,观察该回调函数使用到的所有依赖,并被各个依赖项所绑定
-     } finally {
-         activeEffect = null
-     }
- }
+ function effect(fn, options = {}) {
+     // 对回调 fn 做一层包装
+     const effect = function reactiveEffect() {
+         if (effectStack.includes(effect)) return // 回调中可能使用了多个计算属性
+         cleanup(effect)
+         try {
+             effectStack.push(effect) // 压栈
+             activeEffect = effect
+             return fn() // 计算属性有返回值,观察该回调函数使用到的所有依赖,并被各个依赖项所绑定
+         } finally {
+             effectStack.pop() // 出栈
+             activeEffect = effectStack[effectStack.length - 1]
+         }
+     }
+     effect.id = uid++ // 每个effect的唯一标识
+     /** @type {Array<Set<Function>>} */
+     effect.deps = [] // 反向绑定,记录被当前回调依赖项所绑定的所有回调(便于清理相关属性对当前回调的绑定)
+     effect.options = options
+     if (!options.lazy) {
+         effect() // 计算属性只有被访问时才执行
+     }
+     return effect // 便于计算属性延迟执行
+ }

+ /** @param {{ deps: Array<Set<Function>> }} effect */
+ function cleanup(effect) {
+     const { deps } = effect
+     deps.forEach(dep => {
+         dep.delete(effect) // 清理与该回调相关的所有属性对该回调的绑定
+     })
+     deps.length = 0
+ }

+ /** @param {Function} getter */
+ function computed(getter) {
+     let dirty = true // 是否需要重新计算
+     let val // 缓存的计算结果
+     const computed = {
+         get value() {
+             if (dirty) {
+                 dirty = false
+                 val = runner() // 执行getter():1.获取计算结果;2.依赖项绑定getter
+                 track(computed, 'value') // value属性绑定effect回调
+             }
+             return val
+         }
+     }
+     const runner = effect(getter, {
+         lazy: true, // 计算属性延迟执行getter
+         scheduler: () => {
+             if (dirty) return // 会在计算属性依赖项更新后,执行该调度器
+             dirty = true // 触发重新计算
+             trigger(computed, 'value') // 触发value属性绑定的回调被执行,执行后会触发读取value属性值
+         }
+     })
+     return computed // 返回计算属性对象
+ }

function track(target, key) {
    ...
    if (!deps.has(activeEffect)) {
        deps.add(activeEffect) // 给当前属性绑定依赖此属性的回调
+       activeEffect.deps.push(deps) // 给当前回调反向绑定依赖此属性的所有回调
    }
}

function trigger(target, key) {
    ...
    const deps = depsMap.get(key)
    if (!deps) return // 没有回调需要依赖此属性
-   deps.forEach(dep => dep()) // 重新执行依赖此属性的所有回调
+   /** @type {Set<Function>} */
+   const effects = new Set()
+   deps.forEach(dep => effects.add(dep))
+   /**
+    *  effects用于拷贝deps,因为deps循环过程中,delete=>add会陷入死循环
+    *  const arr = [1]
+    *  arr.forEach(v => {
+    *      console.log(v)
+    *      arr.length = 0
+    *      arr.push(++v)
+    *  })
+    *  const dep = new Set([1])
+    *  dep.forEach(v => {
+    *      console.log(v)
+    *      dep.delete(v)
+    *      dep.add(++v)
+    *  })
+    */
+   effects.forEach(effect => {
+       if (effect.options.scheduler) {
+           effect.options.scheduler() // 被计算属性依赖,触发调度器,由调度器触发执行依赖value属性的+effect回调
+       } else {
+           effect() // 普通属性更新,执行依赖此属性的effect回调
+       }
+   })
}

- export { reactive, effect }
+ export { reactive, effect, computed }

补充对象逻辑

  • reactivity.js
- const isObject = val => val !== null && typeof val === 'object'
+ import {
+     isEffect,
+     isFunction,
+     isObject,
+     getTargetType,
+     TargetType,
+     ReactiveFlags
+ } from './utils.js'

+ const proxyMap = new WeakMap() // 缓存已经被代理的对象

const baseHandlers = {
    get(target, key, receiver) {
+       if (key === ReactiveFlags.IS_REACTIVE) return true // 检查是否被代理
+       if (key === ReactiveFlags.RAW && receiver === proxyMap.get(target)) {
+           return target // 访问被代理的目标对象
+       }
        const res = Reflect.get(target, key, receiver) // 和set保持格式一致
        track(target, key) // 读取属性的时候,收集依赖
        return isObject(res) ? reactive(res) : res // 值是对象则递归代理
    },
    ...
}

function reactive(target) {
-    return new Proxy(target, baseHandlers)
+    if (!isObject(target)) return target // 非对象

+    if (target[ReactiveFlags.RAW]) return target // 已经是个代理了

+    const existingProxy = proxyMap.get(target)
+    if (existingProxy) return existingProxy // 已经被代理过了

+    if (getTargetType(target) === TargetType.INVALID) return target // 不代理(1.makeRaw;2.freeze/+seal/preventExtensions)

+    const proxy = new Proxy(target, baseHandlers)
+    proxyMap.set(target, proxy) // 缓存
+    return proxy
}

function effect(fn, options = {}) {
+   if (isEffect(fn)) {
+       fn = fn.raw // 传入回调是 effect,则取出原始值,之后重新包装
+   }
    // 对回调 fn 做一层包装
    const effect = function reactiveEffect() {
    ...
    effect.options = options
+   effect._isEffect = true // 标识回调经过了 effect 包装
+   effect.raw = fn // 创建时的原始回调
    if (!options.lazy) {
        effect() // 计算属性只有被访问时才执行
    }
    return effect // 便于计算属性延迟执行
}

- /** @param {Function} getter */
+ /** @param {Function|{ get: () => any, set: (v: any) => void }} getterOrOptions */
- function computed(getter) {
+ function computed(getterOrOptions) {
+   let getter, setter = () => {}
+   if (isFunction(getterOrOptions)) {
+       getter = getterOrOptions
+   } else {
+       getter = getterOrOptions.get
+       setter = getterOrOptions.set
+   }
    ...
    const computed = {
        get value() {
            ...
        },
+       set value(newValue) {
+           setter(newValue)
+       }
    }
    ...
    return computed // 返回计算属性对象
}
  • utils.js
export const isEffect = fn => fn && fn._isEffect === true

export const isFunction = val => typeof val === 'function'

export const isObject = val => val !== null && typeof val === 'object'

export const getTargetType = val =>
    val[ReactiveFlags.SKIP] || !Reflect.isExtensible(val)
        ? TargetType.INVALID
        : TargetType.COMMON

export const TargetType = {
    INVALID: 0, // default
    COMMON: 1, // Object / Array
    COLLECTION: 2 // Map / Set / WeakMap / WeakSet
}

export const ReactiveFlags = {
    RAW: '__v_raw',
    SKIP: '__v_skip',
    IS_REACTIVE: '__v_isReactive',
    IS_READONLY: '__v_isReadonly'
}

添加数组逻辑

  • reactivity.js
import {
    ...
    ReactiveFlags,
+   TriggerOpTypes,
+   isArray,
+   hasOwn,
+   toRaw,
+   hasChanged
} from './utils.js'

+ const arrayInstrumentations = {}
+ ;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
+     arrayInstrumentations[key] = function (...args) {
+         const arr = toRaw(this)
+         let len = this.length
+         while (len--) {
+             track(arr, len + '') // 跟踪数组的每一项
+         }
+         let res = arr[key](...args) // 先使用原始参数调用方法(参数可能是响应式的)
+         if (res === -1 || res === false) {
+             res = arr[key](...args.map(toRaw)) // 失败后,尝试使用参数的原始值再次调用
+         }
+         return res
+     }
+ })

const baseHandlers = {
    get(target, key, receiver) {
        if (key === ReactiveFlags.IS_REACTIVE) return true // 检查是否被代理
        if (key === ReactiveFlags.RAW && receiver === proxyMap.get(target)) {
            return target // 访问被代理的目标对象
        }

+       if (isArray(target) && hasOwn(arrayInstrumentations, key)) {
+           return Reflect.get(arrayInstrumentations, key, receiver)
+       }

        const res = Reflect.get(target, key, receiver) // 和set保持格式一致
        track(target, key) // 读取属性的时候,收集依赖
        return isObject(res) ? reactive(res) : res // 值是对象则递归代理
    },
    set(target, key, value, receiver) {
+       const hadKey = hasOwn(target, key)
+       const oldValue = target[key]

        const res = Reflect.set(target, key, value, receiver)

-       trigger(target, key) // 使用修改后的属性值,通知更新
+       if (!hadKey) {
+           // 新增主键(数组调用push依次触发:1.添加索引, 2.修改length)
+           trigger(target, key, TriggerOpTypes.ADD, value) // 修改成功,使用修改后的属性值,通知更新
+       } else if (hasChanged(value, oldValue)) {
+           // 值修改(调用push,当索引添加后,length值也是最新的,不会再触发)
+           trigger(target, key, TriggerOpTypes.SET, value) // 修改成功,使用修改后的属性值,通知更新
+       }
        return res // 是否修改成功
    }
}

function trigger(target, key, type, newValue) {
    const depsMap = targetMap.get(target)
    if (!depsMap) return // 没有回调需要依赖此对象
-   const deps = depsMap.get(key)
-   if (!deps) return // 没有回调需要依赖此属性
    /** @type {Set<Function>} */
    const effects = new Set()
-   deps.forEach(dep => effects.add(dep))
+   /** @param {Set<Function>} effectsToAdd 依赖此属性的回调集合 */
+   const add = effectsToAdd => {
+       if (!effectsToAdd) return // 没有回调需要依赖此属性
+       effectsToAdd.forEach(effect => effects.add(effect))
+   }

+   if (key == 'length' && isArray(target)) {
+       // 使用 arr.length = num 截断数组,多余的元素更新为 undefined
+       depsMap.forEach((deps, key) => {
+           if (key != 'length' && key < newValue) return
+           add(deps) // key == 'length' || index >= newLength
+       })
+   } else {
+       add(depsMap.get(key))
+   }
    ...
    effects.forEach(effect => {
        ...
    })
}
  • utils.js
+ export const TriggerOpTypes = {
+     ADD: 'add',
+     SET: 'set',
+     DELETE: 'delete',
+     CLEAR: 'clear'
+ }

+ export const isArray = Array.isArray

+ export const hasOwn = (val, key) =>
+    Object.prototype.hasOwnProperty.call(val, key)

+ export const toRaw = observed =>
+     (observed && toRaw(observed[ReactiveFlags.RAW])) || observed

+ export const hasChanged = (value, oldValue) =>
+     value !== oldValue && (value === value || oldValue === oldValue)

补充代理行为 (deleteProperty/has/ownKeys)

  • reactivity.js
import {
    ...
    hasChanged,
+   ITERATE_KEY
} from './utils.js'

const baseHandlers = {
    get(target, key, receiver) {
        ...
    },
    set(target, key, value, receiver) {
        ...
    },
+   deleteProperty(target, key) {
+       const hadKey = hasOwn(target, key)
+       const res = Reflect.deleteProperty(target, key)
+       if (res && hadKey) {
+           trigger(target, key, TriggerOpTypes.DELETE, undefined)
+       }
+       return res
+   },
+   has(target, key) {
+       track(target, key)
+       return Reflect.has(target, key)
+   },
+   ownKeys(target) {
+       track(target, ITERATE_KEY)
+       return Reflect.ownKeys(target)
+   }
}

function trigger(target, key, type, newValue) {
    ...
    if (key == 'length' && isArray(target)) {
        // 使用 arr.length = num 截断数组,多余的元素更新为 undefined
        depsMap.forEach((deps, key) => {
            if (key != 'length' && key < newValue) return
            add(deps) // key == 'length' || index >= newLength
        })
    } else {
        add(depsMap.get(key))
+       if (type === TriggerOpTypes.ADD) {
+           // 新增(没有指定键的依赖,数组查找length,对象查找ITERATE_KEY)
+           add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
+       }
    }
    ...
}
  • utils.js
+ export const ITERATE_KEY = Symbol('iterate')

参考文献

About


Languages

Language:JavaScript 89.3%Language:HTML 9.5%Language:TypeScript 1.2%