├── 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 |
40 | {{ obj.c }}
41 |
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 |
--------------------------------------------------------------------------------