├── global.d.ts ├── src ├── index.ts ├── internals.ts ├── handlers │ ├── index.ts │ ├── base.ts │ └── collections.ts ├── observe.ts ├── reactive.ts ├── reaction.ts └── store.ts ├── example ├── index.ts └── index.html ├── tsconfig.json ├── webpack.dev.config.js ├── types └── index.ts ├── webpack.config.js ├── .gitignore ├── package.json └── README.md /global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | data: any 3 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { reactive, raw } from '@/reactive' 2 | export { observe, unobserve } from '@/observe' -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { reactive, observe, unobserve } from "@/index" 2 | 3 | const data = reactive(new Map([['a', 1]])) 4 | observe(() => { 5 | for (let [key, val] of data) { 6 | console.log(key, val) 7 | } 8 | }) 9 | 10 | data.set('b', 5) 11 | window.data = data -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 请在控制台调试 11 | 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": false, 4 | "outDir": "./dist", 5 | "target": "esnext", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "importHelpers": true, 9 | "declaration": true, 10 | "declarationDir": "./dist/types/", 11 | "esModuleInterop": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "@/*": ["src/*"], 15 | }, 16 | "allowSyntheticDefaultImports": true, 17 | "noImplicitThis": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const config = require('./webpack.config') 2 | const merge = require('webpack-merge') 3 | const path = require("path") 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const { resolve } = path 6 | 7 | module.exports = merge(config, { 8 | mode: 'development', 9 | entry: [resolve('example')], 10 | devServer: { 11 | port: 8088, 12 | }, 13 | plugins: [ 14 | new HtmlWebpackPlugin({ // 打包输出HTML 15 | title: 'Reactive Example', 16 | filename: 'index.html', 17 | template: 'example/index.html' 18 | }), 19 | ] 20 | }) -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | // 对象key值的所有形式 2 | export type Key = string | number | symbol 3 | 4 | // 需要定义响应式的原值 5 | export type Raw = object 6 | 7 | // 定义成响应式后的proxy 8 | export type ReactiveProxy = object 9 | 10 | // 收集响应依赖的的函数 11 | export type ReactionFunction = Function & { 12 | cleaners?: ReactionForKey[] 13 | unobserved?: boolean 14 | } 15 | 16 | // reactionForRaw的key为对象key值 value为这个key值收集到的Reaction集合 17 | export type ReactionForRaw = Map 18 | 19 | // key值收集到的Reaction集合 20 | export type ReactionForKey = Set 21 | 22 | // 操作符 用来做依赖收集和触发依赖更新 23 | export interface Operation { 24 | type: "get" | "iterate" | "add" | "set" | "delete" | "clear" | "has" 25 | target: object 26 | key?: Key 27 | receiver?: any 28 | value?: any 29 | oldValue?: any 30 | } 31 | 32 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const { resolve } = path 3 | 4 | module.exports = { 5 | mode: "production", 6 | output: { 7 | library: 'library', 8 | libraryTarget: 'umd' 9 | }, 10 | entry: [resolve("./src")], 11 | resolve: { 12 | extensions: [".ts", ".tsx", ".js", ".jsx"], 13 | alias: { 14 | "@": resolve("./src") 15 | } 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | use: [ 22 | { 23 | loader: 'ts-loader', 24 | options: { 25 | // 指定特定的ts编译配置,为了区分脚本的ts配置 26 | configFile: path.resolve(__dirname, './tsconfig.json'), 27 | }, 28 | }, 29 | ], 30 | exclude: /node_modules/, 31 | }, 32 | ], 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /src/internals.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveProxy, Raw } from 'types' 2 | import { handlers } from './handlers' 3 | 4 | export const proxyToRaw = new WeakMap() 5 | export const rawToProxy = new WeakMap() 6 | 7 | export function isObject(val: any): val is object { 8 | return typeof val === "object" && val !== "null" 9 | } 10 | 11 | // 全局对象 12 | const globalObj = 13 | typeof window === "object" ? window : Function("return this")() 14 | 15 | /** 对于内置的一些对象不去处理 */ 16 | export function shouldInstrument({ constructor }: Raw) { 17 | const isBuiltIn = 18 | typeof constructor === "function" && 19 | constructor.name in globalObj && 20 | globalObj[constructor.name] === constructor 21 | return !isBuiltIn || handlers.has(constructor) 22 | } 23 | 24 | export const hasOwnProperty = Object.prototype.hasOwnProperty 25 | -------------------------------------------------------------------------------- /src/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { collectionHandlers } from "./collections" 2 | import { baseHandlers } from "./base" 3 | import { Raw } from "types" 4 | 5 | // @ts-ignore 6 | // 根据对象的类型 获取Proxy的handlers 7 | export const handlers = new Map([ 8 | [Map, collectionHandlers], 9 | [Set, collectionHandlers], 10 | [WeakMap, collectionHandlers], 11 | [WeakSet, collectionHandlers], 12 | [Object, baseHandlers], 13 | [Array, baseHandlers], 14 | [Int8Array, baseHandlers], 15 | [Uint8Array, baseHandlers], 16 | [Uint8ClampedArray, baseHandlers], 17 | [Int16Array, baseHandlers], 18 | [Uint16Array, baseHandlers], 19 | [Int32Array, baseHandlers], 20 | [Uint32Array, baseHandlers], 21 | [Float32Array, baseHandlers], 22 | [Float64Array, baseHandlers], 23 | ]) 24 | 25 | /** 获取Proxy的handlers */ 26 | export function getHandlers(obj: Raw) { 27 | return handlers.get(obj.constructor) 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Build 34 | dist 35 | dist.js 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Local optimization utils 44 | opt 45 | 46 | # reify cache for allowing import export in node 47 | .reify-cache 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxy-reactive", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "dev": "webpack-dev-server --config webpack.dev.config.js", 11 | "build": "rimraf dist && npm run build-lib & npm run build-types", 12 | "build-lib": "webpack", 13 | "build-types": "tsc -d --declarationDir ./dist/types", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "html-webpack-plugin": "^3.2.0", 21 | "rimraf": "^3.0.0", 22 | "ts-loader": "^6.2.1", 23 | "tslib": "^1.10.0", 24 | "typescript": "^3.7.4", 25 | "webpack": "^4.41.5", 26 | "webpack-cli": "^3.3.10", 27 | "webpack-dev-server": "^3.10.1", 28 | "webpack-merge": "^4.2.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/observe.ts: -------------------------------------------------------------------------------- 1 | import { runReactionWrap } from '@/reaction' 2 | import { ReactionFunction } from 'types' 3 | import { releaseReaction } from '@/store' 4 | 5 | const IS_REACTION = Symbol('is reaction') 6 | 7 | /** 8 | * 观察函数 9 | * 在传入的函数里去访问响应式的proxy 会收集传入的函数作为依赖 10 | * 下次访问的key发生变化的时候 就会重新运行这个函数 11 | */ 12 | export function observe(fn: Function): ReactionFunction { 13 | if (fn[IS_REACTION]) { 14 | return fn as ReactionFunction 15 | } 16 | 17 | // reaction是包装了原始函数只后的观察函数 18 | // 在runReactionWrap的上下文中执行原始函数 可以收集到依赖。 19 | const reaction: ReactionFunction = (...args: any[]) => { 20 | return runReactionWrap(reaction, fn, this, args) 21 | } 22 | 23 | // 先执行一遍reaction 24 | reaction() 25 | 26 | // 返回出去 让外部也可以手动调用 27 | return reaction 28 | } 29 | 30 | /** 停止观察函数 */ 31 | export function unobserve(reaction: ReactionFunction) { 32 | if (!reaction.unobserved) { 33 | reaction.unobserved = true 34 | releaseReaction(reaction) 35 | } 36 | } -------------------------------------------------------------------------------- /src/reactive.ts: -------------------------------------------------------------------------------- 1 | import { proxyToRaw, rawToProxy, shouldInstrument } from "@/internals" 2 | import { getHandlers } from "@/handlers" 3 | import { Raw, ReactiveProxy } from "types" 4 | import { storeObservable } from "@/store" 5 | 6 | export function reactive(raw: T): T { 7 | // 已经被定义成响应式proxy了 或者传入的是内置对象 就直接原封不动的返回 8 | if (proxyToRaw.has(raw) || !shouldInstrument(raw)) { 9 | return raw 10 | } 11 | 12 | // 如果这个原始对象已经被定义过响应式 就返回存储的响应式proxy 13 | const existProxy = rawToProxy.get(raw) 14 | if (existProxy) { 15 | return existProxy as T 16 | } 17 | 18 | // 新建响应式proxy 19 | return createReactive(raw) 20 | } 21 | 22 | function createReactive(raw: T): T { 23 | const reactive = new Proxy(raw, getHandlers(raw)) 24 | 25 | // 双向存储原始值和响应式proxy的映射 26 | rawToProxy.set(raw, reactive) 27 | proxyToRaw.set(reactive, raw) 28 | 29 | // 建立一个映射 30 | // 原始值 -> 存储这个原始值的各个key收集到的依赖函数的Map 31 | storeObservable(raw) 32 | 33 | // 返回响应式proxy 34 | return reactive as T 35 | } 36 | 37 | export function raw(proxy: T) { 38 | return proxyToRaw.get(proxy) as T 39 | } 40 | -------------------------------------------------------------------------------- /src/reaction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | registerReactionForOperation, 3 | releaseReaction, 4 | getReactionsForOperation, 5 | } from "@/store" 6 | import { Operation, ReactionFunction } from "types" 7 | 8 | /** 依赖收集栈 */ 9 | const reactionStack: ReactionFunction[] = [] 10 | 11 | /** 依赖收集 在get操作的时候要调用 */ 12 | export function registerRunningReaction(operation: Operation) { 13 | const runningReaction = getRunningReaction() 14 | if (runningReaction) { 15 | registerReactionForOperation(runningReaction, operation) 16 | } 17 | } 18 | 19 | /** 值更新时触发观察函数 */ 20 | export function queueReactionsForOperation(operation: Operation) { 21 | getReactionsForOperation(operation).forEach(reaction => reaction()) 22 | } 23 | 24 | /** 把函数包裹为观察函数 */ 25 | export function runReactionWrap( 26 | reaction: ReactionFunction, 27 | fn: Function, 28 | context: any, 29 | args: any[], 30 | ) { 31 | // 已经取消观察了 就直接执行原函数 32 | if (reaction.unobserved) { 33 | return Reflect.apply(fn, context, args) 34 | } 35 | 36 | // 如果观察函数是已经在运行 直接返回 37 | if (isRunning(reaction)) { 38 | return 39 | } 40 | 41 | // 把上次收集到的依赖清空 重新收集依赖 42 | // 这点对于函数内有分支的情况很重要 43 | // 保证每次收集的都是确实能访问到的依赖 44 | releaseReaction(reaction) 45 | try { 46 | // 把当前的观察函数推入栈内 开始观察响应式proxy 47 | reactionStack.push(reaction) 48 | // 运行用户传入的函数 这个函数里访问proxy就会收集reaction函数作为依赖了 49 | return Reflect.apply(fn, context, args) 50 | } finally { 51 | // 运行完了永远要出栈 52 | reactionStack.pop() 53 | } 54 | } 55 | 56 | /** 传入的观察函数是否正在运行 */ 57 | export function isRunning(reaction: ReactionFunction) { 58 | return reactionStack.includes(reaction) 59 | } 60 | 61 | /** 当前是否有正在运行的观察函数 */ 62 | export function hasRunningReaction() { 63 | return reactionStack.length > 0 64 | } 65 | 66 | /** 从栈的末尾取到正在运行的observe包裹的函数 */ 67 | function getRunningReaction() { 68 | const [runningReaction] = reactionStack.slice(-1) 69 | return runningReaction 70 | } 71 | -------------------------------------------------------------------------------- /src/handlers/base.ts: -------------------------------------------------------------------------------- 1 | import { proxyToRaw, rawToProxy, isObject, hasOwnProperty } from "@/internals" 2 | import { registerRunningReaction, queueReactionsForOperation } from "@/reaction" 3 | import { Raw, Key, ReactiveProxy } from "types" 4 | import { reactive } from "@/reactive" 5 | 6 | const wellKnownSymbols = new Set( 7 | Object.getOwnPropertyNames(Symbol) 8 | .map(key => Symbol[key]) 9 | .filter(value => typeof value === "symbol"), 10 | ) 11 | 12 | /** 劫持get访问 收集依赖 */ 13 | function get(target: Raw, key: Key, receiver: ReactiveProxy) { 14 | const result = Reflect.get(target, key, receiver) 15 | // 内置的Symbol不观察 16 | if (typeof key === "symbol" && wellKnownSymbols.has(key)) { 17 | return result 18 | } 19 | // 收集依赖 20 | registerRunningReaction({ target, key, receiver, type: "get" }) 21 | 22 | // 如果访问的是对象 则返回这个对象的响应式proxy 23 | // 如果没有就重新调用reactive新建一个proxy 24 | const reativeResult = rawToProxy.get(result) 25 | if (isObject(result)) { 26 | if (reativeResult) { 27 | return reativeResult 28 | } 29 | return reactive(result) 30 | } 31 | 32 | return result 33 | } 34 | 35 | /** 劫持一些遍历访问 比如Object.keys */ 36 | function ownKeys(target: Raw) { 37 | registerRunningReaction({ target, type: "iterate" }) 38 | return Reflect.ownKeys(target) 39 | } 40 | 41 | /** 劫持set访问 触发收集到的观察函数 */ 42 | function set(target: Raw, key: Key, value: any, receiver: ReactiveProxy) { 43 | // 确保原始值里不要被响应式对象污染 44 | if (isObject(value)) { 45 | value = proxyToRaw.get(value) || value 46 | } 47 | // 先检查一下这个key是不是新增的 48 | const hadKey = hasOwnProperty.call(target, key) 49 | // 拿到旧值 50 | const oldValue = target[key] 51 | // 设置新值 52 | const result = Reflect.set(target, key, value, receiver) 53 | 54 | if (!hadKey) { 55 | // 新增key值时以type: add触发观察函数 56 | queueReactionsForOperation({ target, key, value, receiver, type: "add" }) 57 | } else if (value !== oldValue) { 58 | // 已存在的key的值发生变化时以type: set触发观察函数 59 | queueReactionsForOperation({ 60 | target, 61 | key, 62 | value, 63 | oldValue, 64 | receiver, 65 | type: "set", 66 | }) 67 | } 68 | 69 | return result 70 | } 71 | 72 | /** 劫持删除操作 触发收集到的观察函数 */ 73 | function deleteProperty (target: Raw, key: Key) { 74 | // 先检查一下是否存在这个key 75 | const hadKey = hasOwnProperty.call(target, key) 76 | // 拿到旧值 77 | const oldValue = target[key] 78 | // 删除这个属性 79 | const result = Reflect.deleteProperty(target, key) 80 | // 只有这个key存在的时候才触发更新 81 | if (hadKey) { 82 | // type为delete的话 会触发遍历相关的观察函数更新 83 | queueReactionsForOperation({ target, key, oldValue, type: 'delete' }) 84 | } 85 | return result 86 | } 87 | 88 | export const baseHandlers = { 89 | get, 90 | set, 91 | ownKeys, 92 | deleteProperty, 93 | } 94 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Raw, 3 | ReactionFunction, 4 | Operation, 5 | ReactionForRaw, 6 | ReactionForKey, 7 | Key, 8 | } from "types" 9 | 10 | const connectionStore = new WeakMap() 11 | const ITERATION_KEY = Symbol("iteration key") 12 | 13 | export function storeObservable(value: object) { 14 | // 存储对象和它内部的key -> reaction的映射 15 | connectionStore.set(value, new Map() as ReactionForRaw) 16 | } 17 | 18 | /**  19 | * 把对响应式对象key的访问与观察函数建立关联 20 | * 后续就可以在修改这个key的时候 找到响应的观察函数触发 21 | */ 22 | export function registerReactionForOperation( 23 | reaction: ReactionFunction, 24 | { target, key, type }: Operation, 25 | ) { 26 | if (type === "iterate") { 27 | key = ITERATION_KEY 28 | } 29 | 30 | // 拿到原始对象 -> 观察者的map 31 | const reactionsForRaw = connectionStore.get(target) 32 | // 拿到key -> 观察者的set 33 | let reactionsForKey = reactionsForRaw.get(key) 34 | 35 | if (!reactionsForKey) { 36 | // 如果这个key之前没有收集过观察函数 就新建一个 37 | reactionsForKey = new Set() 38 | // set到整个value的存储里去 39 | reactionsForRaw.set(key, reactionsForKey) 40 | } 41 | 42 | if (!reactionsForKey.has(reaction)) { 43 | // 把这个key对应的观察函数收集起来 44 | reactionsForKey.add(reaction) 45 | // 把key收集的观察函数集合 加到cleaners队列中 便于后续取消观察 46 | reaction.cleaners.push(reactionsForKey) 47 | } 48 | } 49 | 50 | /** 51 | * 根据key,type和原始对象 拿到需要触发的所有观察函数 52 | */ 53 | export function getReactionsForOperation({ target, key, type }: Operation) { 54 | // 拿到原始对象 -> 观察者的map 55 | const reactionsForTarget = connectionStore.get(target) 56 | const reactionsForKey: ReactionForKey = new Set() 57 | 58 | // 把所有需要触发的观察函数都收集到新的set里 59 | addReactionsForKey(reactionsForKey, reactionsForTarget, key) 60 | 61 | // add和delete的操作 需要触发某些由循环触发的观察函数收集 62 | // observer(() => rectiveProxy.forEach(() => proxy.foo)) 63 | if (type === "add" || type === "delete" || type === "clear") { 64 | // ITERATION_KEY: 65 | // 如果proxy拦截到的ownKeys的操作 就会用ITERATION_KEY作为观察函数收集的key 66 | // 比如在观察函数里通过Object.keys()访问了proxy对象 就会以这个key进行观察函数收集 67 | // 那么比如在delete操作的时候 是要触发这个观察函数的 因为很明显Object.keys()的值更新了 68 | 69 | // length: 70 | // 遍历一个数组的相关操作都会触发对length这个属性的访问 71 | // 所以如果是数组 只要把访问length时收集到的观察函数重新触发一下就可以了 72 | // 如observe(() => proxyArray.forEach(() => {})) 73 | const iterationKey = Array.isArray(target) ? "length" : ITERATION_KEY 74 | addReactionsForKey(reactionsForKey, reactionsForTarget, iterationKey) 75 | } 76 | 77 | return reactionsForKey 78 | } 79 | 80 | function addReactionsForKey( 81 | reactionsForKey: ReactionForKey, 82 | reactionsForTarget: ReactionForRaw, 83 | key: Key, 84 | ) { 85 | const reactions = reactionsForTarget.get(key) 86 | reactions && 87 | reactions.forEach(reaction => { 88 | reactionsForKey.add(reaction) 89 | }) 90 | } 91 | 92 | /** 93 | * 把上次收集到的观察函数清空 重新收集观察函数 94 | * 这点对于函数内有分支的情况很重要 95 | * 保证每次收集的都是确实能访问到的观察函数 96 | */ 97 | export function releaseReaction(reaction: ReactionFunction) { 98 | if (reaction.cleaners) { 99 | // 把key -> reaction的set里相应的观察函数清楚掉 100 | reaction.cleaners.forEach((reactionsForKey: ReactionForKey) => { 101 | reactionsForKey.delete(reaction) 102 | }) 103 | } 104 | // 重置队列 105 | reaction.cleaners = [] 106 | } 107 | -------------------------------------------------------------------------------- /src/handlers/collections.ts: -------------------------------------------------------------------------------- 1 | import { 2 | registerRunningReaction, 3 | hasRunningReaction, 4 | queueReactionsForOperation, 5 | } from "@/reaction" 6 | import { proxyToRaw, rawToProxy, isObject, hasOwnProperty } from "@/internals" 7 | import { Key, Raw, ReactiveProxy } from "types" 8 | import { reactive } from "@/reactive" 9 | 10 | /** 对于返回值 如果是复杂类型 再进一步的定义为响应式 */ 11 | function findReactive(obj: Raw) { 12 | const reactiveObj = rawToProxy.get(obj) 13 | // 只有正在运行观察函数的时候才去定义响应式 14 | if (hasRunningReaction() && isObject(obj)) { 15 | if (reactiveObj) { 16 | return reactiveObj 17 | } 18 | return reactive(obj) 19 | } 20 | return reactiveObj || obj 21 | } 22 | 23 | /** 把iterator劫持成响应式的iterator */ 24 | function patchIterator (iterator, isEntries) { 25 | const originalNext = iterator.next 26 | iterator.next = () => { 27 | let { done, value } = originalNext.call(iterator) 28 | if (!done) { 29 | if (isEntries) { 30 | value[1] = findReactive(value[1]) 31 | } else { 32 | value = findReactive(value) 33 | } 34 | } 35 | return { done, value } 36 | } 37 | return iterator 38 | } 39 | 40 | export const instrumentations = { 41 | has (key: Key) { 42 | const target = proxyToRaw.get(this) 43 | const proto: any = Reflect.getPrototypeOf(this) 44 | registerRunningReaction({ target, key, type: "has" }) 45 | return proto.has.apply(target, arguments) 46 | }, 47 | get(key: Key) { 48 | // 获取原始数据 49 | const target = proxyToRaw.get(this) 50 | // 获取原始数据的__proto__ 拿到原型链上的方法 51 | const proto: any = Reflect.getPrototypeOf(this) 52 | // 注册get类型的依赖 53 | registerRunningReaction({ target, key, type: "get" }) 54 | // 调用原型链上的get方法求值 然后对于复杂类型继续定义成响应式 55 | return findReactive(proto.get.apply(target, arguments)) 56 | }, 57 | forEach (cb: Function, ...args: any[]) { 58 | const target = proxyToRaw.get(this) 59 | const proto: any = Reflect.getPrototypeOf(this) 60 | registerRunningReaction({ target, type: 'iterate' }) 61 | /** 62 | * wrappedCb包裹了用户自己传给forEach的cb函数,然后传给了集合对象原型链上的forEach,这又是一个函数劫持。用户传入的是map.forEach(cb),而我们最终调用的是map.forEach(wrappedCb)。 63 | * 在这个wrappedCb中,我们把cb中本应该获得的原始值value通过`findObservable`定义成响应式数据交给用户,这样用户在forEach中进行的响应式操作一样可以收集到依赖了。 64 | */ 65 | const wrappedCb = (value: any, ...rest: any[]) => cb(findReactive(value), ...rest) 66 | return proto.forEach.call(target, wrappedCb, ...args) 67 | }, 68 | set(key: Key, value: any) { 69 | const target = proxyToRaw.get(this) 70 | const proto: any = Reflect.getPrototypeOf(this) 71 | // 是否是新增的key 72 | const hadKey = proto.has.call(target, key) 73 | // 拿到旧值 74 | const oldValue = proto.get.call(target, key) 75 | // 求出结果 76 | const result = proto.set.apply(target, arguments) 77 | if (!hadKey) { 78 | // 新增key值时以type: add触发观察函数 79 | queueReactionsForOperation({ target, key, type: "add" }) 80 | } else if (value !== oldValue) { 81 | // 已存在的key的值发生变化时以type: set触发观察函数 82 | queueReactionsForOperation({ target, key, type: "set" }) 83 | } 84 | return result 85 | }, 86 | add (key: Key) { 87 | const target = proxyToRaw.get(this) 88 | const proto: any = Reflect.getPrototypeOf(this) 89 | const hadKey = proto.has.call(target, key) 90 | const result = proto.add.apply(target, arguments) 91 | if (!hadKey) { 92 | queueReactionsForOperation({ target, key, type: 'add' }) 93 | } 94 | return result 95 | }, 96 | delete (key: Key) { 97 | const target = proxyToRaw.get(this) 98 | const proto: any = Reflect.getPrototypeOf(this) 99 | const hadKey = proto.has.call(target, key) 100 | const result = proto.delete.apply(target, arguments) 101 | if (hadKey) { 102 | queueReactionsForOperation({ target, key, type: 'delete' }) 103 | } 104 | return result 105 | }, 106 | clear () { 107 | const target: any = proxyToRaw.get(this) 108 | const proto: any = Reflect.getPrototypeOf(this) 109 | const hadItems = target.size !== 0 110 | const result = proto.clear.apply(target, arguments) 111 | if (hadItems) { 112 | queueReactionsForOperation({ target, type: 'clear' }) 113 | } 114 | return result 115 | }, 116 | keys () { 117 | const target = proxyToRaw.get(this) 118 | const proto: any = Reflect.getPrototypeOf(this) 119 | registerRunningReaction({ target, type: 'iterate' }) 120 | return proto.keys.apply(target, arguments) 121 | }, 122 | values () { 123 | const target = proxyToRaw.get(this) 124 | const proto: any = Reflect.getPrototypeOf(this) 125 | registerRunningReaction({ target, type: 'iterate' }) 126 | const iterator = proto.values.apply(target, arguments) 127 | return patchIterator(iterator, false) 128 | }, 129 | entries () { 130 | const target = proxyToRaw.get(this) 131 | const proto: any = Reflect.getPrototypeOf(this) 132 | registerRunningReaction({ target, type: 'iterate' }) 133 | const iterator = proto.entries.apply(target, arguments) 134 | return patchIterator(iterator, true) 135 | }, 136 | [Symbol.iterator] () { 137 | const target = proxyToRaw.get(this) 138 | const proto: any = Reflect.getPrototypeOf(this) 139 | registerRunningReaction({ target, type: 'iterate' }) 140 | const iterator = proto[Symbol.iterator].apply(target, arguments) 141 | return patchIterator(iterator, target instanceof Map) 142 | }, 143 | get size () { 144 | const target = proxyToRaw.get(this) 145 | const proto = Reflect.getPrototypeOf(this) 146 | registerRunningReaction({ target, type: 'iterate' }) 147 | return Reflect.get(proto, 'size', target) 148 | } 149 | } 150 | 151 | // 真正交给Proxy第二个参数的handlers只有一个get 152 | // 把用户对于map的get、set这些api的访问全部移交给上面的劫持函数 153 | export const collectionHandlers = { 154 | get(target: Raw, key: Key, receiver: ReactiveProxy) { 155 | // 返回上面被劫持的api 156 | target = hasOwnProperty.call(instrumentations, key) 157 | ? instrumentations 158 | : target 159 | return Reflect.get(target, key, receiver) 160 | }, 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 笔者最近在浏览React状态管理库的时候,发现了一些响应式的状态管理库如 3 | `hodux`,`react-easy-state`,内部有一个基于proxy实现响应式的基础仓库`observer-util`,它的代码实现和Vue3中的响应式原理非常相似,这篇文章就从这个仓库入手,一步一步带你剖析响应式的实现。 4 | 5 | 第二篇传送门: 6 | [TypeScript从零实现基于Proxy的响应式库 基于函数劫持实现Map和Set的响应式](https://github.com/sl1673495/blogs/issues/31) 7 | 8 | 本文的代码是我参考`observer-util`用ts的重写的,并且会加上非常详细的注释。 9 | 10 | 阅读本文可能需要的一些前置知识: 11 | 12 | [Proxy](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 13 | [WeakMap](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) 14 | [Reflect](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect) 15 | 16 | 首先看一下`observer-util`给出的代码示例: 17 | ```js 18 | import { observable, observe } from '@nx-js/observer-util'; 19 | 20 | const counter = observable({ num: 0 }); 21 | 22 | // 会在控制台打印出0 23 | const countLogger = observe(() => console.log(counter.num)); 24 | 25 | // 会在控制台打印出1 26 | counter.num++; 27 | ``` 28 | 这就是一个最精简的响应式模型了,乍一看好像和Vue2里的响应式系统也没啥区别,那么还是先看一下Vue2和Vue3响应式系统之间的差异吧。 29 | 30 | ## 和Vue2的差异 31 | 32 | 关于Vue2的响应式原理,感兴趣的也可以去看我之前的一篇文章: 33 | [实现一个最精简的响应式系统来学习Vue的data、computed、watch源码](https://juejin.im/post/5db6433b51882564912fc30f) 34 | 35 | 其实这个问题本质上就是基于Proxy和基于Object.defineProperty之间的差异,来看Vue2中的一个案例: 36 | 37 | ### Object.defineProperty 38 | ```xml 39 | 42 | 52 | ``` 53 | 54 | 这个例子中,我们对obj上原本不存在的`c`属性进行了一个赋值,但是在Vue2中,这是不会触发响应式的。 55 | 56 | 这是因为Object.defineProperty必须对于确定的`key`值进行响应式的定义, 57 | 58 | 这就导致了如果data在初始化的时候没有`c`属性,那么后续对于`c`属性的赋值都不会触发Object.defineProperty中的劫持。 59 | 60 | 在Vue2中,这里只能用一个额外的api `Vue.set`来解决。 61 | 62 | ### Proxy 63 | 再看一下`Proxy`的api, 64 | ```js 65 | const raw = {} 66 | const data = new Proxy(raw, { 67 | get(target, key) { }, 68 | set(target, key, value) { } 69 | }) 70 | ``` 71 | 72 | 可以看出来,Proxy在定义的时候并不用关心key值, 73 | 74 | 只要你定义了get方法,那么后续对于data上任何属性的访问(哪怕是不存在的), 75 | 76 | 都会触发`get`的劫持,`set`也是同理。 77 | 78 | 这样Vue3中,对于需要定义响应式的值,初始化时候的要求就没那么高了,只要保证它是个可以被Proxy接受的对象或者数组类型即可。 79 | 80 | 当然,Proxy对于数据拦截带来的便利还不止于此,往下看就知道。 81 | 82 | ## 实现 83 | 接下来就一步步实现这个基于Proxy的响应式系统: 84 | 85 | ### 类型描述 86 | 本仓库基于TypeScript重构,所以会有一个类型定义的文件,可以当做接口先大致看一下 87 | 88 | https://github.com/sl1673495/typescript-proxy-reactive/blob/master/types/index.ts 89 | 90 | ### 思路 91 | 首先响应式的思路无外乎这样一个模型: 92 | 93 | 1. 定义某个数据为`响应式数据`,它会拥有收集`访问它的函数`的能力。 94 | 2. 定义观察函数,在这个函数内部去访问`响应式数据`。 95 | 96 | 以开头的例子来说 97 | ```js 98 | // 响应式数据 99 | const counter = observable({ num: 0 }); 100 | 101 | // 观察函数 102 | observe(() => console.log(counter.num)); 103 | ``` 104 | 这已经一目了然了, 105 | - 用`observable`包裹的数据叫做响应式数据, 106 | - 在`observe`内部执行的函数叫`观察函数`。 107 | 108 | 观察函数首先开启某个开关, 109 | 110 | #### 访问时 111 | 112 | observe函数会帮你去执行`console.log(counter.num)`, 113 | 114 | 这时候`proxy`的`get`拦截到了对于`counter.num`的访问, 115 | 116 | 这时候又可以知道访问者是`() => console.log(counter.num)`这个函数, 117 | 118 | 那么就把这个函数作为`num`这个key值的`观察函数`收集在一个地方。 119 | 120 | #### 修改时 121 | 下次对于`counter.num`修改的时候,去找`num`这个key下所有的`观察函数`,轮流执行一遍。 122 | 123 | 这样就实现了响应式模型。 124 | 125 | ## reactive的实现(定义响应式数据) 126 | 127 | 上文中关于`observable`的api,我换了个名字: `reactive`,感觉更好理解一些。 128 | 129 | 130 | ```js 131 | // 需要定义响应式的原值 132 | export type Raw = object 133 | // 定义成响应式后的proxy 134 | export type ReactiveProxy = object 135 | 136 | export const proxyToRaw = new WeakMap() 137 | export const rawToProxy = new WeakMap() 138 | 139 | function createReactive(raw: T): T { 140 | const reactive = new Proxy(raw, baseHandlers) 141 | 142 | // 双向存储原始值和响应式proxy的映射 143 | rawToProxy.set(raw, reactive) 144 | proxyToRaw.set(reactive, raw) 145 | 146 | // 建立一个映射 147 | // 原始值 -> 存储这个原始值的各个key收集到的依赖函数的Map 148 | storeObservable(raw) 149 | 150 | // 返回响应式proxy 151 | return reactive as T 152 | } 153 | ``` 154 | 155 | 首先是定义proxy 156 | ```js 157 | const reactive = new Proxy(raw, baseHandlers) 158 | ``` 159 | 这个baseHandlers里就是对于数据的`get`、`set`之类的劫持, 160 | 161 | 这里有两个WeakMap: `proxyToRaw`和`rawToProxy`, 162 | 163 | 可以看到在定义响应式数据为一个Proxy的时候,会进行一个双向的存储, 164 | 165 | 这样后续无论是拿到原始对象还是拿到响应式proxy,都可以很容易的拿到它们的`另一半`。 166 | 167 | 之后`storeObservable`,是用原始对象建立一个map: 168 | ```js 169 | const connectionStore = new WeakMap() 170 | 171 | function storeObservable(value: object) { 172 | // 存储对象和它内部的key -> reaction的映射 173 | connectionStore.set(value, new Map() as ReactionForRaw) 174 | } 175 | ``` 176 | 177 | 通过connectionStore的泛型也可以知道, 178 | 179 | 这是一个`Raw` -> `ReactionForRaw`的map。 180 | 181 | 也就是`原始数据` -> `这个数据收集到的观察函数依赖` 182 | 183 | 更清晰的描述可以看Type定义: 184 | ```js 185 | // 收集响应依赖的的函数 186 | export type ReactionFunction = Function & { 187 | cleaners?: ReactionForKey[] 188 | unobserved?: boolean 189 | } 190 | 191 | // reactionForRaw的key为对象key值 value为这个key值收集到的Reaction集合 192 | export type ReactionForRaw = Map 193 | 194 | // key值收集到的Reaction集合 195 | export type ReactionForKey = Set 196 | 197 | // 收集响应依赖的的函数 198 | export type ReactionFunction = Function & { 199 | cleaners?: ReactionForKey[] 200 | unobserved?: boolean 201 | } 202 | ``` 203 | 204 | 那接下来的重点就是proxy的第二个参数`baseHandler`里的`get`和`set`了 205 | 206 | ## proxy的get 207 | ```js 208 | /** 劫持get访问 收集依赖 */ 209 | function get(target: Raw, key: Key, receiver: ReactiveProxy) { 210 | const result = Reflect.get(target, key, receiver) 211 | 212 | // 收集依赖 213 | registerRunningReaction({ target, key, receiver, type: "get" }) 214 | 215 | return result 216 | } 217 | 218 | ``` 219 | 关于receiver这个参数,这里可以先简单理解为`响应式proxy`本身,不影响流程。 220 | 221 | 这里就是简单的做了一个求值,然后进入了`registerRunningReaction`函数, 222 | 223 | ### 注册依赖 224 | ```js 225 | // 收集响应依赖的的函数 226 | type ReactionFunction = Function & { 227 | cleaners?: ReactionForKey[] 228 | unobserved?: boolean 229 | } 230 | 231 | // 操作符 用来做依赖收集和触发依赖更新 232 | interface Operation { 233 | type: "get" | "iterate" | "add" | "set" | "delete" | "clear" 234 | target: object 235 | key?: Key 236 | receiver?: any 237 | value?: any 238 | oldValue?: any 239 | } 240 | 241 | /** 依赖收集栈 */ 242 | const reactionStack: ReactionFunction[] = [] 243 | 244 | /** 依赖收集 在get操作的时候要调用 */ 245 | export function registerRunningReaction(operation: Operation) { 246 | const runningReaction = getRunningReaction() 247 | if (runningReaction) { 248 | // 拿到原始对象 -> 观察者的map 249 | const reactionsForRaw = connectionStore.get(target) 250 | // 拿到key -> 观察者的set 251 | let reactionsForKey = reactionsForRaw.get(key) 252 | 253 | if (!reactionsForKey) { 254 | // 如果这个key之前没有收集过观察函数 就新建一个 255 | reactionsForKey = new Set() 256 | // set到整个value的存储里去 257 | reactionsForRaw.set(key, reactionsForKey) 258 | } 259 | 260 | if (!reactionsForKey.has(reaction)) { 261 | // 把这个key对应的观察函数收集起来 262 | reactionsForKey.add(reaction) 263 | // 把key收集的观察函数集合 加到cleaners队列中 便于后续取消观察 264 | reaction.cleaners.push(reactionsForKey) 265 | } 266 | } 267 | } 268 | 269 | /** 从栈的末尾取到正在运行的observe包裹的函数 */ 270 | function getRunningReaction() { 271 | const [runningReaction] = reactionStack.slice(-1) 272 | return runningReaction 273 | } 274 | ``` 275 | 276 | 这里做的一系列操作,就是把用`原始数据`从`connectionStore`里拿到依赖收集的map,然后在`reaction`观察函数把对于某个`key`访问的时候,把`reaction`观察函数本身增加到这个`key`的观察函数集合里。 277 | 278 | 那么这个`runningReaction`正在运行的观察函数是哪来的呢,剧透一下,当然是`observe`这个api内部开启观察模式后去做的。 279 | 280 | ```js 281 | // 此时 () => console.log(counter.num) 会被包装成reaction函数 282 | observe(() => console.log(counter.num)); 283 | ``` 284 | 285 | ### set 286 | ```js 287 | /** 劫持set访问 触发收集到的观察函数 */ 288 | function set(target: Raw, key: Key, value: any, receiver: ReactiveProxy) { 289 | // 拿到旧值 290 | const oldValue = target[key] 291 | // 设置新值 292 | const result = Reflect.set(target, key, value, receiver) 293 | 294 | queueReactionsForOperation({ 295 | target, 296 | key, 297 | value, 298 | oldValue, 299 | receiver, 300 | type: 'set' 301 | }) 302 | 303 | return result 304 | } 305 | 306 | /** 值更新时触发观察函数 */ 307 | export function queueReactionsForOperation(operation: Operation) { 308 | getReactionsForOperation(operation).forEach(reaction => reaction()) 309 | } 310 | 311 | /** 312 | * 根据key,type和原始对象 拿到需要触发的所有观察函数 313 | */ 314 | export function getReactionsForOperation({ target, key, type }: Operation) { 315 | // 拿到原始对象 -> 观察者的map 316 | const reactionsForTarget = connectionStore.get(target) 317 | const reactionsForKey: ReactionForKey = new Set() 318 | 319 | // 把所有需要触发的观察函数都收集到新的set里 320 | addReactionsForKey(reactionsForKey, reactionsForTarget, key) 321 | 322 | return reactionsForKey 323 | } 324 | ``` 325 | `set`赋值操作的时候,本质上就是去检查这个`key`收集到了哪些`reaction`观察函数,然后依次触发。 326 | 327 | ## observe 观察函数 328 | 329 | `observe`这个api接受一个用户传入的函数,在这个函数内访问响应式数据才会去收集观察函数作为自己的依赖。 330 | 331 | ```js 332 | /** 333 | * 观察函数 334 | * 在传入的函数里去访问响应式的proxy 会收集传入的函数作为依赖 335 | * 下次访问的key发生变化的时候 就会重新运行这个函数 336 | */ 337 | export function observe(fn: Function): ReactionFunction { 338 | // reaction是包装了原始函数只后的观察函数 339 | // 在runReactionWrap的上下文中执行原始函数 可以收集到依赖。 340 | const reaction: ReactionFunction = (...args: any[]) => { 341 | return runReactionWrap(reaction, fn, this, args) 342 | } 343 | 344 | // 先执行一遍reaction 345 | reaction() 346 | 347 | // 返回出去 让外部也可以手动调用 348 | return reaction 349 | } 350 | ``` 351 | 352 | 核心的逻辑在`runReactionWrap`里, 353 | ```js 354 | 355 | /** 把函数包裹为观察函数 */ 356 | export function runReactionWrap( 357 | reaction: ReactionFunction, 358 | fn: Function, 359 | context: any, 360 | args: any[], 361 | ) { 362 | try { 363 | // 把当前的观察函数推入栈内 开始观察响应式proxy 364 | reactionStack.push(reaction) 365 | // 运行用户传入的函数 这个函数里访问proxy就会收集reaction函数作为依赖了 366 | return Reflect.apply(fn, context, args) 367 | } finally { 368 | // 运行完了永远要出栈 369 | reactionStack.pop() 370 | } 371 | } 372 | ``` 373 | 374 | 简化后的核心逻辑很简单, 375 | 376 | 把`reaction`推入`reactionStack`后开始执行用户传入的函数, 377 | 378 | 在函数内访问`响应式proxy`的属性,又会触发`get`的拦截, 379 | 380 | 这时候`get`去`reactionStack`找当前正在运行的`reaction`,就可以成功的收集到依赖了。 381 | 382 | 下一次用户进行赋值的时候 383 | ```js 384 | const counter = reactive({ num: 0 }); 385 | 386 | // 会在控制台打印出0 387 | const counterReaction = observe(() => console.log(counter.num)); 388 | 389 | // 会在控制台打印出1 390 | counter.num = 1; 391 | ``` 392 | 以这个示例来说,observe内部对于counter的key值`num的`访问,会收集`counterReaction`作为`num`的依赖。 393 | 394 | `counter.num = 1`的操作,会触发对于counter的`set`劫持,此时就会从`key`值的依赖收集里面找到`counterReaction`,再重新执行一遍。 395 | 396 | ## 边界情况 397 | 以上实现只是一个最基础的响应式模型,还没有实现的点有: 398 | 399 | - 深层数据的劫持 400 | - 数组和对象新增、删除项的响应 401 | 402 | 接下来在上面的代码的基础上来实现这两种情况: 403 | 404 | ### 深层数据的劫持 405 | 在刚刚的代码实现中,我们只对Proxy的第一层属性做了拦截,假设有这样的一个场景 406 | ```js 407 | const counter = reactive({ data: { num: 0 } }); 408 | 409 | // 会在控制台打印出0 410 | const counterReaction = observe(() => console.log(counter.data.num)); 411 | 412 | counter.data.num = 1; 413 | ``` 414 | 415 | 这种场景就不能实能触发`counterReaction`自动执行了。 416 | 417 | 因为counter.data.num其实是对`data`上的`num`属性进行赋值,而counter虽然是一个`响应式proxy`,但`counter.data`却只是一个普通的对象,回想一下刚刚的proxy`get`的拦截函数: 418 | 419 | ```js 420 | /** 劫持get访问 收集依赖 */ 421 | function get(target: Raw, key: Key, receiver: ReactiveProxy) { 422 | const result = Reflect.get(target, key, receiver) 423 | 424 | // 收集依赖 425 | registerRunningReaction({ target, key, receiver, type: "get" }) 426 | 427 | return result 428 | } 429 | ``` 430 | `counter.data`只是通过Reflect.get拿到了原始的 { data: {number } }对象,然后对这个对象的赋值不会被proxy拦截到。 431 | 432 | 那么思路其实也有了,就是在深层访问的时候,如果访问的数据是个对象,就把这个对象也用`reactive`包装成proxy再返回,这样在进行`counter.data.num = 1;`赋值的时候,其实也是针对一个`响应式proxy`赋值了。 433 | 434 | ```diff 435 | /** 劫持get访问 收集依赖 */ 436 | function get(target: Raw, key: Key, receiver: ReactiveProxy) { 437 | const result = Reflect.get(target, key, receiver) 438 | // 收集依赖 439 | registerRunningReaction({ target, key, receiver, type: "get" }) 440 | 441 | + // 如果访问的是对象 则返回这个对象的响应式proxy 442 | + if (isObject(result)) { 443 | + return reactive(result) 444 | + } 445 | 446 | return result 447 | } 448 | 449 | ``` 450 | 451 | ### 数组和对象新增、删除项的响应 452 | 以这样一个场景为例 453 | 454 | ```js 455 | const data: any = reactive({ a: 1, b: 2}) 456 | 457 | observe(() => console.log( Object.keys(data))) 458 | 459 | data.c = 5 460 | ``` 461 | 462 | 其实在用Object.keys访问data的时候,后续不管是data上的key发生了新增或者删除,都应该触发这个观察函数,那么这是怎么实现的呢? 463 | 464 | 首先我们需要知道,Object.keys(data)访问proxy的时候,会触发proxy的`ownKeys`拦截。 465 | 466 | 那么我们在`baseHandler`中先新增对于`ownKeys`的访问拦截: 467 | ```diff 468 | /** 劫持get访问 收集依赖 */ 469 | function get() {} 470 | 471 | /** 劫持set访问 触发收集到的观察函数 */ 472 | function set() { 473 | } 474 | 475 | /** 劫持一些遍历访问 比如Object.keys */ 476 | + function ownKeys (target: Raw) { 477 | + registerRunningReaction({ target, type: 'iterate' }) 478 | + return Reflect.ownKeys(target) 479 | + } 480 | ``` 481 | 482 | 还是和get方法一样,调用`registerRunningReaction`方法注册依赖,但是type我们需要定义成`iterate`,这个type怎么用呢。我们继续改造`registerRunningReaction`函数: 483 | 484 | ```diff 485 | + const ITERATION_KEY = Symbol("iteration key") 486 | 487 | export function registerRunningReaction(operation: Operation) { 488 | const runningReaction = getRunningReaction() 489 | if (runningReaction) { 490 | + if (type === "iterate") { 491 | + key = ITERATION_KEY 492 | + } 493 | // 拿到原始对象 -> 观察者的map 494 | const reactionsForRaw = connectionStore.get(target) 495 | // 拿到key -> 观察者的set 496 | let reactionsForKey = reactionsForRaw.get(key) 497 | 498 | if (!reactionsForKey) { 499 | // 如果这个key之前没有收集过观察函数 就新建一个 500 | reactionsForKey = new Set() 501 | // set到整个value的存储里去 502 | reactionsForRaw.set(key, reactionsForKey) 503 | } 504 | 505 | if (!reactionsForKey.has(reaction)) { 506 | // 把这个key对应的观察函数收集起来 507 | reactionsForKey.add(reaction) 508 | // 把key收集的观察函数集合 加到cleaners队列中 便于后续取消观察 509 | reaction.cleaners.push(reactionsForKey) 510 | } 511 | } 512 | } 513 | ``` 514 | 515 | 也就是`type: iterate`触发的依赖收集,我们会放在key为`ITERATION_KEY`的一个特殊的set里,那么再来看看触发更新时的`set`改造: 516 | 517 | ```diff 518 | /** 劫持set访问 触发收集到的观察函数 */ 519 | function set(target: Raw, key: Key, value: any, receiver: ReactiveProxy) { 520 | // 拿到旧值 521 | const oldValue = target[key] 522 | // 设置新值 523 | const result = Reflect.set(target, key, value, receiver) 524 | + // 先检查一下这个key是不是新增的 525 | + const hadKey = hasOwnProperty.call(target, key) 526 | 527 | + if (!hadKey) { 528 | + // 新增key值时触发观察函数 529 | + queueReactionsForOperation({ target, key, value, receiver, type: 'add' }) 530 | } else if (value !== oldValue) { 531 | // 已存在的key的值发生变化时触发观察函数 532 | queueReactionsForOperation({ 533 | target, 534 | key, 535 | value, 536 | oldValue, 537 | receiver, 538 | type: 'set' 539 | }) 540 | } 541 | 542 | return result 543 | } 544 | 545 | ``` 546 | 547 | 这里对新增的key也进行了的判断,传入`queueReactionsForOperation`的type变成了`add` 548 | 549 | ```diff 550 | 551 | /** 值更新时触发观察函数 */ 552 | export function queueReactionsForOperation(operation: Operation) { 553 | getReactionsForOperation(operation).forEach(reaction => reaction()) 554 | } 555 | 556 | /** 557 | * 根据key,type和原始对象 拿到需要触发的所有观察函数 558 | */ 559 | export function getReactionsForOperation({ target, key, type }: Operation) { 560 | // 拿到原始对象 -> 观察者的map 561 | const reactionsForTarget = connectionStore.get(target) 562 | const reactionsForKey: ReactionForKey = new Set() 563 | 564 | // 把所有需要触发的观察函数都收集到新的set里 565 | addReactionsForKey(reactionsForKey, reactionsForTarget, key) 566 | 567 | // add和delete的操作 需要触发某些由循环触发的观察函数收集 568 | // observer(() => rectiveProxy.forEach(() => proxy.foo)) 569 | + if (type === "add" || type === "delete") { 570 | + const iterationKey = Array.isArray(target) ? "length" : ITERATION_KEY 571 | + addReactionsForKey(reactionsForKey, reactionsForTarget, iterationKey) 572 | } 573 | return reactionsForKey 574 | } 575 | ``` 576 | 577 | 这里需要注意的是,对于数组新增和删除项来说,如果我们在观察函数中做了遍历操作,也需要触发它的更新, 578 | 579 | 这里又有一个知识点,对于数组遍历的操作,都会触发它对`length`的读取,然后把观察函数收集到`length`这个key的依赖中,比如 580 | ```js 581 | observe(() => proxyArray.forEach(() => {})) 582 | // 会访问proxyArray的length。 583 | ``` 584 | 585 | 所以在触发更新的时候, 586 | 1. 如果目标是个数组,那就从`length`的依赖里收集, 587 | 2. 如果目标是对象,就从`ITERATION_KEY`的依赖里收集。(也就是对于对象做Object.keys读取时,由`ownKeys`拦截收集的依赖)。 588 | 589 | ## 源码地址 590 | https://github.com/sl1673495/typescript-proxy-reactive 591 | 592 | ## 总结 593 | 由于篇幅原因,有一些优化的操作我没有在文中写出来,在仓库里做了几乎是逐行注释,而且也可以用`npm run dev`对example文件夹中的例子进行调试。感兴趣的同学可以自己看一下。 594 | 595 | 如果读完了还觉得有兴致,也可以直接去看`observe-util`这个库的源码,里面对于更多的边界情况做了处理,代码也写的非常优雅,值得学习。 596 | 597 | 从本文里讲解的一些边界情况也可以看出,基于Proxy的响应式方案比Object.defineProperty要强大很多,希望大家尽情的享受Vue3带来的快落吧。 598 | 599 | --------------------------------------------------------------------------------