├── .gitignore
├── README.md
├── babel.config.js
├── example
├── apiInject
│ ├── App.js
│ └── index.html
├── componentEmit.js
│ ├── App.js
│ ├── Child.js
│ └── index.html
├── componentUpdate
│ ├── App.js
│ ├── Child.js
│ ├── index.html
│ └── main.js
├── customRenderer
│ ├── App.js
│ ├── game.js
│ ├── index.html
│ ├── main.js
│ └── renderer.js
├── getCurrentInstance
│ ├── App.js
│ └── index.html
├── helloworld
│ ├── App.js
│ ├── Foo.js
│ ├── index.html
│ └── main.js
├── nextTicker
│ ├── App.js
│ ├── index.html
│ └── main.js
├── patchChildren
│ ├── App.js
│ ├── ArrayToArray.js
│ ├── ArrayToText.js
│ ├── TextToArray.js
│ ├── TextToText.js
│ ├── index.html
│ └── main.js
└── slotsComponent
│ ├── App.js
│ ├── Child.js
│ └── index.html
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── index.ts
├── reactivity
│ ├── index.ts
│ ├── src
│ │ ├── baseHandlers.ts
│ │ ├── computed.ts
│ │ ├── dep.ts
│ │ ├── effective.ts
│ │ ├── reactive.ts
│ │ ├── ref.ts
│ │ ├── scheduler.ts
│ │ └── watch.ts
│ └── tests
│ │ ├── computed.spec.ts
│ │ ├── effect.spec.ts
│ │ ├── reactive.spec.ts
│ │ ├── readonly.spec.ts
│ │ ├── ref.spec.ts
│ │ ├── shallowReactive.spec.ts
│ │ ├── shallowReadonly.spec.ts
│ │ └── watch.spec.ts
├── runtime-core
│ ├── apiInject.ts
│ ├── component.ts
│ ├── componentEmit.ts
│ ├── componentProps.ts
│ ├── componentPublicInstance.ts
│ ├── componentSlots.ts
│ ├── componentUpdateUtils.ts
│ ├── createApp.ts
│ ├── h.ts
│ ├── helpers
│ │ └── renderSlots.ts
│ ├── index.ts
│ ├── render.ts
│ ├── scheduler.ts
│ └── vnode.ts
├── runtime-dom
│ └── index.ts
└── shared
│ ├── ShapeFlags.ts
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # my-mini-vue3
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', {targets: {node: 'current'}}],
4 | '@babel/preset-typescript',
5 | ],
6 | };
--------------------------------------------------------------------------------
/example/apiInject/App.js:
--------------------------------------------------------------------------------
1 | // 组件 provide 和 inject 功能
2 | import {
3 | h,
4 | provide,
5 | inject,
6 | } from "../../lib/mini-vue.esm.js";
7 |
8 | const ProviderOne = {
9 | setup() {
10 | provide("foo", "foo");
11 | provide("bar", "bar");
12 | },
13 | render() {
14 | return h(ProviderTwo);
15 | }
16 | };
17 |
18 | const ProviderTwo = {
19 | setup() {
20 | // override parent value
21 | provide("foo", "fooOverride");
22 | provide("baz", "baz");
23 | const foo = inject("foo");
24 | // 这里获取的 foo 的值应该是 "foo"
25 | // 这个组件的子组件获取的 foo ,才应该是 fooOverride
26 | if (foo !== "foo") {
27 | throw new Error("Foo should equal to foo");
28 | }
29 | return {
30 | foo
31 | }
32 | },
33 | render() {
34 | return h("div", {}, [h("div", {}, `${this.foo}`), h(Consumer)]);
35 | }
36 | };
37 |
38 | const Consumer = {
39 | setup() {
40 | const foo = inject("foo");
41 | const bar = inject("bar");
42 | const baz = inject("baz");
43 | return {
44 | foo,
45 | bar,
46 | baz
47 | }
48 | },
49 | render() {
50 | return h("div", {}, `${this.foo}-${this.bar}-${this.baz}`);
51 | }
52 | };
53 |
54 | export default {
55 | name: "App",
56 | setup() {
57 |
58 | },
59 | render() {
60 | return h("div", {}, [h("p", {}, "apiInject"), h(ProviderOne)]);
61 | }
62 | };
63 |
--------------------------------------------------------------------------------
/example/apiInject/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/example/componentEmit.js/App.js:
--------------------------------------------------------------------------------
1 | // 组件 emit 逻辑 demo
2 | // click emit 发出 change, 可以触发 App 组件内定义好的侦听函数
3 | // 允许接收多个参数
4 | import { h } from "../../lib/mini-vue.esm.js";
5 | import Child from "./Child.js";
6 |
7 | export default {
8 | name: "App",
9 | setup() {},
10 |
11 | render() {
12 | return h("div", {}, [
13 | h("div", {}, "你好"),
14 | h(Child, {
15 | msg: "your name is child",
16 | onChange(a, b) {
17 | console.log("---------------change------------------");
18 | console.log(a, b);
19 | },
20 | onChangePageName(a, b) {
21 | console.log("---------------change-page-name------------------");
22 | console.log(a, b);
23 | },
24 | }),
25 | ]);
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/example/componentEmit.js/Child.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/mini-vue.esm.js";
2 | export default {
3 | name: "Child",
4 | setup(props, { emit }) {
5 | emit("change", "aaaaa", "bbbbbb");
6 | // 支持多个 -
7 | emit("change-page-name", "start", "game");
8 | },
9 | render() {
10 | return h("div", {}, "child");
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/example/componentEmit.js/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/example/componentUpdate/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref, onMounted } from "../../lib/mini-vue.esm.js";
2 | import Child from "./Child.js";
3 |
4 | export const App = {
5 | name: "App",
6 | setup() {
7 | const msg = ref("123");
8 | const count = ref(1);
9 |
10 | window.msg = msg;
11 |
12 | onMounted(() => {
13 | console.log("App —————— onMounted 1");
14 | })
15 |
16 | onMounted(() => {
17 | console.log("App —————— onMounted 2");
18 | })
19 |
20 | const changeChildProps = () => {
21 | msg.value = "456";
22 | };
23 |
24 | const changeCount = () => {
25 | count.value++;
26 | };
27 |
28 | return { msg, changeChildProps, changeCount, count };
29 | },
30 |
31 | beforeCreate() {
32 | console.log("App —————— beforeCreate");
33 | },
34 | created() {
35 | console.log("App —————— created");
36 | },
37 | beforeMount() {
38 | console.log("App —————— beforeMount");
39 | },
40 | mounted() {
41 | console.log("App —————— mounted");
42 | },
43 | beforeUpdate() {
44 | console.log("App —————— beforeUpdate");
45 | },
46 | updated() {
47 | console.log("App —————— updated");
48 | },
49 |
50 | render() {
51 | return h("div", {}, [
52 | h("div", {}, "你好"),
53 | h(
54 | "button",
55 | {
56 | onClick: this.changeChildProps,
57 | },
58 | "change child props"
59 | ),
60 | h(Child, {
61 | msg: this.msg,
62 | }),
63 | h(
64 | "button",
65 | {
66 | onClick: this.changeCount,
67 | },
68 | "change self count"
69 | ),
70 | h("p", {}, "count: " + this.count),
71 | ]);
72 | },
73 | };
74 |
--------------------------------------------------------------------------------
/example/componentUpdate/Child.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/mini-vue.esm.js";
2 | export default {
3 | name: "Child",
4 | setup(props, { emit }) {},
5 | beforeCreate() {
6 | console.log("Child —————— beforeCreate");
7 | },
8 | created() {
9 | console.log("Child —————— created");
10 | },
11 | beforeMount() {
12 | console.log("Child —————— beforeMount");
13 | },
14 | mounted() {
15 | console.log("Child —————— mounted");
16 | },
17 | beforeUpdate() {
18 | console.log("Child —————— beforeUpdate");
19 | },
20 | updated() {
21 | console.log("Child —————— updated");
22 | },
23 | render(proxy) {
24 | return h("div", {}, [h("div", {}, "child - props - msg: " + this.$props.msg)]);
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/example/componentUpdate/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/example/componentUpdate/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js";
2 | import { App } from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#app");
5 | createApp(App).mount(rootContainer);
6 |
--------------------------------------------------------------------------------
/example/customRenderer/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/mini-vue.esm.js";
2 |
3 | export default {
4 | name: "App",
5 | setup() {
6 | return {
7 | x: 100,
8 | y: 100,
9 | };
10 | },
11 |
12 | render() {
13 | return h("rect", { x: this.x, y: this.y });
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/example/customRenderer/game.js:
--------------------------------------------------------------------------------
1 | export const game = new PIXI.Application({
2 | width: 500,
3 | height: 500,
4 | });
5 |
6 | document.body.append(game.view);
7 |
8 | export function createRootContainer() {
9 | return game.stage;
10 | }
11 |
--------------------------------------------------------------------------------
/example/customRenderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/customRenderer/main.js:
--------------------------------------------------------------------------------
1 | import App from "./App.js";
2 | import { createApp } from "./renderer.js";
3 | import { createRootContainer } from "./game.js";
4 |
5 | createApp(App).mount(createRootContainer());
6 |
--------------------------------------------------------------------------------
/example/customRenderer/renderer.js:
--------------------------------------------------------------------------------
1 | import { createRenderer } from "../../lib/mini-vue.esm.js";
2 |
3 | // 给基于 pixi.js 的渲染函数
4 | const renderer = createRenderer({
5 | createElement(type) {
6 | const rect = new PIXI.Graphics();
7 | rect.beginFill(0xff0000);
8 | rect.drawRect(0, 0, 100, 100);
9 | rect.endFill();
10 |
11 | return rect;
12 | },
13 |
14 | patchProp(el, key, prevValue, nextValue) {
15 | el[key] = nextValue;
16 | },
17 |
18 | insert(el, parent) {
19 | parent.addChild(el);
20 | },
21 | });
22 |
23 | export function createApp(rootComponent) {
24 | return renderer.createApp(rootComponent);
25 | }
26 |
--------------------------------------------------------------------------------
/example/getCurrentInstance/App.js:
--------------------------------------------------------------------------------
1 | // 可以在 setup 中使用 getCurrentInstance 获取组件实例对象
2 | import { h, getCurrentInstance } from "../../lib/mini-vue.esm.js";
3 |
4 | export default {
5 | name: "App",
6 | setup() {
7 | console.log(getCurrentInstance());
8 | },
9 | render() {
10 | return h("div", {}, [h("p", {}, "getCurrentInstance")]);
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/example/getCurrentInstance/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/example/helloworld/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/mini-vue.esm.js"
2 | import { Foo } from "./Foo.js"
3 |
4 | window.self = null;
5 | // 这里直接 render 和写template一样,template 需要经过编译转换成 render 函数
6 | export const App = {
7 | render() {
8 | window.self = this;
9 | return h(
10 | "div",
11 | {
12 | id: "root",
13 | onClick() {
14 | console.log("click");
15 | },
16 | onMousedown() {
17 | console.log("mousedown");
18 | }
19 | },
20 | [
21 | h("div", {
22 | class: [{blue: true}, "red", "12 34"],
23 | }, "hi, " + this.msg),
24 | h(Foo, {
25 | count: 1,
26 | })
27 | ]
28 | );
29 | },
30 | setup() {
31 | // composition api
32 | return {
33 | msg: "mini-vue"
34 | }
35 | },
36 | }
--------------------------------------------------------------------------------
/example/helloworld/Foo.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/mini-vue.esm.js"
2 |
3 | export const Foo = {
4 | setup(props) {
5 | // props.count
6 | // shallow readonly
7 | props.count++;
8 | console.log(props);
9 | },
10 | render() {
11 | return h(
12 | "div",
13 | {},
14 | "foo: " + this.count
15 | );
16 | },
17 | }
--------------------------------------------------------------------------------
/example/helloworld/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/helloworld/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from "./App.js"
3 |
4 | const rootContainer = document.querySelector("#app");
5 | console.log(rootContainer)
6 | createApp(App).mount(rootContainer);
--------------------------------------------------------------------------------
/example/nextTicker/App.js:
--------------------------------------------------------------------------------
1 | import {
2 | h,
3 | ref,
4 | getCurrentInstance,
5 | nextTick,
6 | } from "../../lib/mini-vue.esm.js";
7 |
8 | export default {
9 | name: "App",
10 | setup() {
11 | const count = ref(1);
12 | const instance = getCurrentInstance();
13 |
14 | function onClick() {
15 | for (let i = 0; i < 100; i++) {
16 | console.log("update");
17 | count.value = i;
18 | }
19 |
20 | debugger;
21 | console.log(instance);
22 | nextTick(() => {
23 | console.log(instance);
24 | });
25 |
26 | // await nextTick()
27 | // console.log(instance)
28 | }
29 |
30 | return {
31 | onClick,
32 | count,
33 | };
34 | },
35 | render() {
36 | const button = h("button", { onClick: this.onClick }, "update");
37 | const p = h("p", {}, "count:" + this.count);
38 |
39 | return h("div", {}, [button, p]);
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/example/nextTicker/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/nextTicker/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js";
2 | import App from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#root");
5 | createApp(App).mount(rootContainer);
6 |
--------------------------------------------------------------------------------
/example/patchChildren/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/mini-vue.esm.js";
2 |
3 | import ArrayToText from "./ArrayToText.js";
4 | import TextToText from "./TextToText.js";
5 | import TextToArray from "./TextToArray.js";
6 | import ArrayToArray from "./ArrayToArray.js";
7 |
8 | export default {
9 | name: "App",
10 | setup() {},
11 |
12 | render() {
13 | return h("div", { tId: 1 }, [
14 | h("p", {}, "主页"),
15 | // 老的是 array 新的是 text
16 | // h(ArrayToText),
17 | // 老的是 text 新的是 text
18 | // h(TextToText),
19 | // 老的是 text 新的是 array
20 | // h(TextToArray)
21 | // 老的是 array 新的是 array
22 | h(ArrayToArray),
23 | ]);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/example/patchChildren/ArrayToArray.js:
--------------------------------------------------------------------------------
1 | // 老的是 array
2 | // 新的是 array
3 |
4 | import { ref, h } from "../../lib/mini-vue.esm.js";
5 |
6 | // 1. 左侧的对比
7 | // (a b) c
8 | // (a b) d e
9 | // const prevChildren = [
10 | // h("p", { key: "A" }, "A"),
11 | // h("p", { key: "B" }, "B"),
12 | // h("p", { key: "C" }, "C"),
13 | // ];
14 | // const nextChildren = [
15 | // h("p", { key: "A" }, "A"),
16 | // h("p", { key: "B" }, "B"),
17 | // h("p", { key: "D" }, "D"),
18 | // h("p", { key: "E" }, "E"),
19 | // ];
20 |
21 | // 2. 右侧的对比
22 | // a (b c)
23 | // d e (b c)
24 | // const prevChildren = [
25 | // h("p", { key: "A" }, "A"),
26 | // h("p", { key: "B" }, "B"),
27 | // h("p", { key: "C" }, "C"),
28 | // ];
29 | // const nextChildren = [
30 | // h("p", { key: "D" }, "D"),
31 | // h("p", { key: "E" }, "E"),
32 | // h("p", { key: "B" }, "B"),
33 | // h("p", { key: "C" }, "C"),
34 | // ];
35 |
36 | // 3. 新的比老的长
37 | // 创建新的
38 | // 左侧
39 | // (a b)
40 | // (a b) c
41 | // i = 2, e1 = 1, e2 = 2
42 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
43 | // const nextChildren = [
44 | // h("p", { key: "A" }, "A"),
45 | // h("p", { key: "B" }, "B"),
46 | // h("p", { key: "C" }, "C"),
47 | // h("p", { key: "D" }, "D"),
48 | // ];
49 |
50 | // 右侧
51 | // (a b)
52 | // c (a b)
53 | // i = 0, e1 = -1, e2 = 0
54 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
55 | // const nextChildren = [
56 | // h("p", { key: "C" }, "C"),
57 | // h("p", { key: "A" }, "A"),
58 | // h("p", { key: "B" }, "B"),
59 | // ];
60 |
61 | // 4. 老的比新的长
62 | // 删除老的
63 | // 左侧
64 | // (a b) c
65 | // (a b)
66 | // i = 2, e1 = 2, e2 = 1
67 | // const prevChildren = [
68 | // h("p", { key: "A" }, "A"),
69 | // h("p", { key: "B" }, "B"),
70 | // h("p", { key: "C" }, "C"),
71 | // ];
72 | // const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
73 |
74 | // 右侧
75 | // a (b c)
76 | // (b c)
77 | // i = 0, e1 = 0, e2 = -1
78 |
79 | // const prevChildren = [
80 | // h("p", { key: "A" }, "A"),
81 | // h("p", { key: "B" }, "B"),
82 | // h("p", { key: "C" }, "C"),
83 | // ];
84 | // const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")];
85 |
86 | // 5. 对比中间的部分
87 | // 删除老的 (在老的里面存在,新的里面不存在)
88 | // 5.1
89 | // a,b,(c,d),f,g
90 | // a,b,(e,c),f,g
91 | // D 节点在新的里面是没有的 - 需要删除掉
92 | // C 节点 props 也发生了变化
93 |
94 | // const prevChildren = [
95 | // h("p", { key: "A" }, "A"),
96 | // h("p", { key: "B" }, "B"),
97 | // h("p", { key: "C", id: "c-prev" }, "C"),
98 | // h("p", { key: "D" }, "D"),
99 | // h("p", { key: "F" }, "F"),
100 | // h("p", { key: "G" }, "G"),
101 | // ];
102 |
103 | // const nextChildren = [
104 | // h("p", { key: "A" }, "A"),
105 | // h("p", { key: "B" }, "B"),
106 | // h("p", { key: "E" }, "E"),
107 | // h("p", { key: "C", id:"c-next" }, "C"),
108 | // h("p", { key: "F" }, "F"),
109 | // h("p", { key: "G" }, "G"),
110 | // ];
111 |
112 | // 5.1.1
113 | // a,b,(c,e,d),f,g
114 | // a,b,(e,c),f,g
115 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑)
116 | // const prevChildren = [
117 | // h("p", { key: "A" }, "A"),
118 | // h("p", { key: "B" }, "B"),
119 | // h("p", { key: "C", id: "c-prev" }, "C"),
120 | // h("p", { key: "E" }, "E"),
121 | // h("p", { key: "D" }, "D"),
122 | // h("p", { key: "F" }, "F"),
123 | // h("p", { key: "G" }, "G"),
124 | // ];
125 |
126 | // const nextChildren = [
127 | // h("p", { key: "A" }, "A"),
128 | // h("p", { key: "B" }, "B"),
129 | // h("p", { key: "E" }, "E"),
130 | // h("p", { key: "C", id:"c-next" }, "C"),
131 | // h("p", { key: "F" }, "F"),
132 | // h("p", { key: "G" }, "G"),
133 | // ];
134 |
135 | // 2 移动 (节点存在于新的和老的里面,但是位置变了)
136 |
137 | // 2.1
138 | // a,b,(c,d,e),f,g
139 | // a,b,(e,c,d),f,g
140 | // 最长子序列: [1,2]
141 |
142 | // const prevChildren = [
143 | // h("p", { key: "A" }, "A"),
144 | // h("p", { key: "B" }, "B"),
145 | // h("p", { key: "C" }, "C"),
146 | // h("p", { key: "D" }, "D"),
147 | // h("p", { key: "E" }, "E"),
148 | // h("p", { key: "F" }, "F"),
149 | // h("p", { key: "G" }, "G"),
150 | // ];
151 |
152 | // const nextChildren = [
153 | // h("p", { key: "A" }, "A"),
154 | // h("p", { key: "B" }, "B"),
155 | // h("p", { key: "E" }, "E"),
156 | // h("p", { key: "C" }, "C"),
157 | // h("p", { key: "D" }, "D"),
158 | // h("p", { key: "F" }, "F"),
159 | // h("p", { key: "G" }, "G"),
160 | // ];
161 |
162 | // 3. 创建新的节点
163 | // a,b,(c,e),f,g
164 | // a,b,(e,c,d),f,g
165 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建
166 | // const prevChildren = [
167 | // h("p", { key: "A" }, "A"),
168 | // h("p", { key: "B" }, "B"),
169 | // h("p", { key: "C" }, "C"),
170 | // h("p", { key: "E" }, "E"),
171 | // h("p", { key: "F" }, "F"),
172 | // h("p", { key: "G" }, "G"),
173 | // ];
174 |
175 | // const nextChildren = [
176 | // h("p", { key: "A" }, "A"),
177 | // h("p", { key: "B" }, "B"),
178 | // h("p", { key: "E" }, "E"),
179 | // h("p", { key: "C" }, "C"),
180 | // h("p", { key: "D" }, "D"),
181 | // h("p", { key: "F" }, "F"),
182 | // h("p", { key: "G" }, "G"),
183 | // ];
184 |
185 | // 综合例子
186 | // a,b,(c,d,e,z),f,g
187 | // a,b,(d,c,y,e),f,g
188 |
189 | // const prevChildren = [
190 | // h("p", { key: "A" }, "A"),
191 | // h("p", { key: "B" }, "B"),
192 | // h("p", { key: "C" }, "C"),
193 | // h("p", { key: "D" }, "D"),
194 | // h("p", { key: "E" }, "E"),
195 | // h("p", { key: "Z" }, "Z"),
196 | // h("p", { key: "F" }, "F"),
197 | // h("p", { key: "G" }, "G"),
198 | // ];
199 |
200 | // const nextChildren = [
201 | // h("p", { key: "A" }, "A"),
202 | // h("p", { key: "B" }, "B"),
203 | // h("p", { key: "D" }, "D"),
204 | // h("p", { key: "C" }, "C"),
205 | // h("p", { key: "Y" }, "Y"),
206 | // h("p", { key: "E" }, "E"),
207 | // h("p", { key: "F" }, "F"),
208 | // h("p", { key: "G" }, "G"),
209 | // ];
210 |
211 | // fix c 节点应该是 move 而不是删除之后重新创建的
212 | const prevChildren = [
213 | h("p", { key: "A" }, "A"),
214 | h("p", {}, "C"),
215 | h("p", { key: "B" }, "B"),
216 | h("p", { key: "D" }, "D"),
217 | ];
218 |
219 | const nextChildren = [
220 | h("p", { key: "A" }, "A"),
221 | h("p", { key: "B" }, "B"),
222 | h("p", {}, "C"),
223 | h("p", { key: "D" }, "D"),
224 | ];
225 |
226 | export default {
227 | name: "ArrayToArray",
228 | setup() {
229 | const isChange = ref(false);
230 | window.isChange = isChange;
231 |
232 | return {
233 | isChange,
234 | };
235 | },
236 | render() {
237 | const self = this;
238 |
239 | return self.isChange === true
240 | ? h("div", {}, nextChildren)
241 | : h("div", {}, prevChildren);
242 | },
243 | };
244 |
--------------------------------------------------------------------------------
/example/patchChildren/ArrayToText.js:
--------------------------------------------------------------------------------
1 | // 老的是 array
2 | // 新的是 text
3 |
4 | import { ref, h } from "../../lib/mini-vue.esm.js";
5 | const nextChildren = "newChildren";
6 | const prevChildren = [h("div", {}, "A"), h("div", {}, "B")];
7 |
8 | export default {
9 | name: "ArrayToText",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/example/patchChildren/TextToArray.js:
--------------------------------------------------------------------------------
1 | // 新的是 array
2 | // 老的是 text
3 | import { ref, h } from "../../lib/mini-vue.esm.js";
4 |
5 | const prevChildren = "oldChild";
6 | const nextChildren = [h("div", {}, "A"), h("div", {}, "B")];
7 |
8 | export default {
9 | name: "TextToArray",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/example/patchChildren/TextToText.js:
--------------------------------------------------------------------------------
1 | // 新的是 text
2 | // 老的是 text
3 | import { ref, h } from "../../lib/mini-vue.esm.js";
4 |
5 | const prevChildren = "oldChild";
6 | const nextChildren = "newChild";
7 |
8 | export default {
9 | name: "TextToText",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/example/patchChildren/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/patchChildren/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js";
2 | import App from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#root");
5 | createApp(App).mount(rootContainer);
6 |
--------------------------------------------------------------------------------
/example/slotsComponent/App.js:
--------------------------------------------------------------------------------
1 | import { h, createTextVNode, createCommentVNode } from "../../lib/mini-vue.esm.js";
2 | import Child from "./Child.js";
3 |
4 | export default {
5 | name: "App",
6 | setup() {},
7 |
8 | render() {
9 | return h("div", {}, [
10 | h("div", {}, "你好"),
11 | h(
12 | Child,
13 | {
14 | msg: "your name is child",
15 | },
16 | {
17 | default: ({ age }) => [
18 | h("p", {}, "我是通过 slot 渲染出来的第一个元素 "),
19 | h("p", {}, "我是通过 slot 渲染出来的第二个元素"),
20 | h("p", {}, `我可以接收到 age: ${age}`),
21 | createTextVNode("hello"),
22 | createCommentVNode("我是注释"),
23 | ],
24 | }
25 | ),
26 | ]);
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/example/slotsComponent/Child.js:
--------------------------------------------------------------------------------
1 | import { h, renderSlots } from "../../lib/mini-vue.esm.js";
2 | export default {
3 | name: "Child",
4 | setup(props, context) {},
5 | render() {
6 | console.log(this.$slots)
7 | return h("div", {}, [
8 | h("div", {}, "child"),
9 | // renderSlot 会返回一个 vnode
10 | // 其本质和 h 是一样的
11 | // 第三个参数给出数据
12 | // 具名插槽
13 | // 作用域插槽
14 | renderSlots(this.$slots, "default", {
15 | age: 16,
16 | }),
17 | ]);
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/example/slotsComponent/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-mini-vue3",
3 | "version": "1.0.0",
4 | "main": "lib/mini-vue.cjs.js",
5 | "module": "lib/mini-vue.esm.js",
6 | "repository": "https://github.com/dannykbsoul/my-mini-vue3.git",
7 | "author": "moises.zhou ",
8 | "license": "MIT",
9 | "scripts": {
10 | "test": "jest",
11 | "build": "rollup -c rollup.config.js"
12 | },
13 | "devDependencies": {
14 | "@babel/core": "^7.17.8",
15 | "@babel/preset-env": "^7.16.11",
16 | "@babel/preset-typescript": "^7.16.7",
17 | "@rollup/plugin-typescript": "^8.3.1",
18 | "@types/jest": "^27.4.1",
19 | "babel-jest": "^27.5.1",
20 | "jest": "^27.5.1",
21 | "rollup": "^2.70.1",
22 | "tslib": "^2.3.1",
23 | "typescript": "^4.6.2"
24 | },
25 | "dependencies": {
26 | "pixi.js": "^6.2.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import pkg from "./package.json"
2 | import typescript from "@rollup/plugin-typescript"
3 |
4 | export default {
5 | input: "./src/index.ts",
6 | output: [
7 | {
8 | format: "cjs",
9 | file: pkg.main
10 | },
11 | {
12 | format: "es",
13 | file: pkg.module
14 | },
15 | ],
16 | plugins: [
17 | typescript()
18 | ]
19 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./runtime-dom"
2 | export * from "./reactivity"
--------------------------------------------------------------------------------
/src/reactivity/index.ts:
--------------------------------------------------------------------------------
1 | export { ref, proxyRefs } from "./src/ref";
2 | export { reactive } from "./src/reactive";
--------------------------------------------------------------------------------
/src/reactivity/src/baseHandlers.ts:
--------------------------------------------------------------------------------
1 | import { extend, hasChanged, hasOwn, isArray, isIntegerKey, isObject, isSymbol } from "../../shared";
2 | import { enableTracking, pauseTracking, track, trigger } from "./effective";
3 | import { reactive, ReactiveFlags, readonly } from "./reactive";
4 | import { isRef } from "./ref";
5 |
6 | // 对于一些内置的 symbol 类型,不希望收集依赖
7 | const builtInSymbols = new Set(
8 | Object.getOwnPropertyNames(Symbol)
9 | .map((key) => (Symbol as any)[key])
10 | );
11 |
12 | const get = createGetter();
13 | const set = createSetter();
14 | const shallowGet = createGetter(false, true);
15 | const readonlyGet = createGetter(true);
16 | const shallowReadonlyGet = createGetter(true, true);
17 |
18 | const arrayInstrumentations = createArrayInstrumentations();
19 |
20 | function createArrayInstrumentations() {
21 | const instrumentations = {};
22 | ["includes", "indexOf", "lastIndexOf"].forEach((key) => {
23 | const originMethod = Array.prototype[key];
24 | instrumentations[key] = function (...args: any) {
25 | let res = originMethod.apply(this, args);
26 |
27 | if (!res) {
28 | res = originMethod.apply((this as any).raw, args);
29 | }
30 | return res;
31 | };
32 | });
33 | ["push", "pop", "shift", "unshift", "splice"].forEach((key) => {
34 | const originMethod = Array.prototype[key];
35 | instrumentations[key] = function (...args: any) {
36 | // 暂停 track
37 | pauseTracking();
38 | let res = originMethod.apply(this, args);
39 | // 恢复 track
40 | enableTracking();
41 | return res;
42 | };
43 | });
44 | return instrumentations;
45 | }
46 |
47 | export const ITERATE_KEY = Symbol();
48 | export const enum TriggerType {
49 | SET = "SET",
50 | ADD = "ADD",
51 | DEL = "DEL",
52 | }
53 |
54 | function createGetter(isReadonly = false, isShallow = false) {
55 | return function get(target, key, receiver) {
56 | if (key === ReactiveFlags.IS_REACTIVE) {
57 | return !isReadonly;
58 | } else if (key === ReactiveFlags.IS_READONLY) {
59 | return isReadonly;
60 | } else if (key === ReactiveFlags.IS_SHALLOW) {
61 | return isShallow;
62 | }
63 | // 代理对象可以通过 raw 属性访问到原始数据
64 | if (key === "raw") {
65 | return target;
66 | }
67 |
68 | if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
69 | return Reflect.get(arrayInstrumentations, key, receiver);
70 | }
71 | // key 是内置 symbol 类型,不希望收集依赖
72 | if (!isReadonly && !(isSymbol(key) && builtInSymbols.has(key))) {
73 | // 只读的情况 不需要依赖收集
74 | track(target, key);
75 | }
76 | const res = Reflect.get(target, key);
77 | if (isShallow) return res;
78 | // 深响应
79 | if (isObject(res)) {
80 | return isReadonly ? readonly(res) : reactive(res);
81 | }
82 | return res;
83 | };
84 | }
85 |
86 | function createSetter() {
87 | return function set(target, key, value, reveiver) {
88 | const type = isArray(target) && isIntegerKey(key)
89 | ? Number(key) < target.length
90 | ? TriggerType.SET
91 | : TriggerType.ADD
92 | : hasOwn(target, key)
93 | ? TriggerType.SET
94 | : TriggerType.ADD;
95 | const oldValue = Reflect.get(target, key);
96 | if (isRef(oldValue) && !isRef(value)) {
97 | oldValue.value = value;
98 | }
99 | const res = Reflect.set(target, key, value);
100 | // reveiver 就是 target 的代理对象,此时才会去考虑触发
101 | if (reveiver.raw === target) {
102 | hasChanged(oldValue, value) && trigger(target, key, type, value);
103 | }
104 | return res;
105 | };
106 | }
107 |
108 | function deleteProperty(target, key) {
109 | // 检查被操作的属性是否是对象自己的属性
110 | const hadKey = hasOwn(target, key);
111 | const result = Reflect.deleteProperty(target, key);
112 | // 只有当被删除的属性是对象自己的属性 并且 删除成功的时候,才会去触发更新
113 | if (hadKey && result) {
114 | trigger(target, key, TriggerType.DEL);
115 | }
116 | return result;
117 | }
118 |
119 | // key in obj 类似的操作
120 | function has(target, key) {
121 | track(target, key);
122 | return Reflect.has(target, key);
123 | }
124 |
125 | // for ... in 类似的操作
126 | // 对象属性新增、删除才需要触发
127 | // for ... in 操作对于数组也是可以遍历,哪些操作会影响到 for ... in的遍历
128 | // 1. 添加新元素 arr[100] = 1;
129 | // 2. 修改数组长度 arr.length = 0;
130 | // 无论是新增还是修改数组长度,本质都是 length 的变化,所以将 length 与副作用函数绑定在一起
131 | // 将来 length 变化的时候去触发相应的副作用函数
132 | function ownKeys(target) {
133 | track(target, isArray(target) ? "length" : ITERATE_KEY);
134 | return Reflect.ownKeys(target);
135 | }
136 |
137 |
138 | export const mutableHandlers = {
139 | get,
140 | set,
141 | deleteProperty,
142 | has,
143 | ownKeys,
144 | };
145 |
146 | export const shallowReactiveHandlers = extend({}, mutableHandlers, {
147 | get: shallowGet,
148 | });
149 |
150 | export const readonlyHandlers = {
151 | get: readonlyGet,
152 | set: function set(target, key, value) {
153 | console.warn(`Set operation on key "${key}" failed: target is readonly.`);
154 | return true;
155 | },
156 | };
157 |
158 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, {
159 | get: shallowReadonlyGet,
160 | });
--------------------------------------------------------------------------------
/src/reactivity/src/computed.ts:
--------------------------------------------------------------------------------
1 | import { createDep } from "./dep";
2 | import { ReactiveEffect, triggerEffects } from "./effective";
3 | import { trackRefValue } from "./ref";
4 |
5 | class ComputedRefImpl {
6 | public dep: any;
7 | private _getter: any;
8 | private _dirty = true;
9 | private _value: any;
10 | private _effect: any;
11 | constructor(getter) {
12 | this._getter = getter;
13 | this.dep = new Set();
14 | // 巧妙的运用了 effect 的 scheduler,当 getter 依赖的响应式对象发生 set 的时候
15 | // 此时会调用 scheduler,将 _dirty 置为 true,当下次调用 .value 的时候,会去执行 this._effect.run();
16 | // 此时 _dirty 为 false,当接下来没有响应式对象发生 set,调用 .value,直接返回 this._value,实现缓存
17 | this._effect = new ReactiveEffect(getter, () => {
18 | if (!this._dirty) {
19 | this._dirty = true;
20 | triggerEffects(createDep(this.dep));
21 | }
22 | });
23 | }
24 | get value() {
25 | trackRefValue(this);
26 | if (this._dirty) {
27 | this._dirty = false;
28 | this._value = this._effect.run();
29 | }
30 | return this._value;
31 | }
32 | }
33 |
34 | export function computed(getter) {
35 | return new ComputedRefImpl(getter);
36 | }
--------------------------------------------------------------------------------
/src/reactivity/src/dep.ts:
--------------------------------------------------------------------------------
1 | export const createDep = (effects?) => {
2 | const dep = new Set(effects);
3 | return dep;
4 | };
5 |
--------------------------------------------------------------------------------
/src/reactivity/src/effective.ts:
--------------------------------------------------------------------------------
1 | import { extend, isArray } from "../../shared";
2 | import { ITERATE_KEY, TriggerType } from "./baseHandlers";
3 | import { createDep } from "./dep";
4 |
5 | let activeEffect;
6 | let shouldTrack;
7 | // 当嵌套 effect 存在时,activeEffect就不够用了,因为当嵌套的 activeEffect 激活的时候就会覆盖上一个 activeEffect
8 | // 此时内层的 activeEffect执行完,需要找到外层的 activeEffect,所以需要一个 stack 来存储
9 | const effectStack: unknown[] = [];
10 | export class ReactiveEffect {
11 | private _fn: any;
12 | deps = [];
13 | active = true;
14 | onStop?: () => void;
15 | public scheduler: Function | undefined;
16 | constructor(fn, scheduler?: Function) {
17 | this._fn = fn;
18 | this.scheduler = scheduler;
19 | }
20 | run() {
21 | // 每次都需要清除一次依赖再去收集依赖,用于分支切换
22 | cleanupEffect(this);
23 | if (!this.active) {
24 | return this._fn();
25 | }
26 | shouldTrack = true;
27 | activeEffect && effectStack.push(activeEffect);
28 | activeEffect = this;
29 | const result = this._fn();
30 | // 执行完当前的 effect,需要还原之前的 effect
31 | activeEffect = effectStack.pop();
32 | // activeEffect 为空代表没有要执行的 effect,此时 shouldTrack 关闭
33 | !activeEffect && (shouldTrack = false);
34 | return result;
35 | }
36 | stop() {
37 | // 确保反复执行 stop 只清空一次
38 | if (this.active) {
39 | cleanupEffect(this);
40 | this.onStop && this.onStop();
41 | this.active = false;
42 | }
43 | }
44 | }
45 |
46 | function cleanupEffect(effect) {
47 | const { deps } = effect;
48 | if (deps.length) {
49 | for (let i = 0; i < deps.length; i++) {
50 | deps[i].delete(effect);
51 | }
52 | deps.length = 0;
53 | }
54 | effect.deps.length = 0;
55 | }
56 |
57 | // why use WeakMap?
58 | // 因为 targetMap 都是对象作为键名,但是对键名所引用的对象是弱引用,不会影响垃圾回收机制
59 | // 只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。
60 | // 也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
61 | // 应用场景
62 | // WeakMap 经常用于存储那些只有当 key 所引用的对象存在时才有价值的信息,例如这里的 targetMap 对应的 target,
63 | // 如果 target 对象没有任何的引用了,说明用户侧不再需要它了,此时如果用 Map 的话,即使用户侧的代码对 target 没有任何引用,
64 | // 这个 target 也不会被回收,最后可能导致内存溢出
65 | const targetMap = new WeakMap();
66 | export function track(target, key) {
67 | // 没有 activeEffect,也就没有必要进行依赖收集
68 | if (!isTracking()) return;
69 | // target -> key -> deps(effect)
70 | // 这样的映射关系可以精确的实现响应式
71 | // 取出 depsMap,数据类型是 Map: key -> deps(effect)
72 | let depsMap = targetMap.get(target);
73 | if (!depsMap) {
74 | targetMap.set(target, (depsMap = new Map()));
75 | }
76 | // 根据 key 取出 deps,数据类型是 Set
77 | let dep = depsMap.get(key);
78 | if (!dep) {
79 | depsMap.set(key, (dep = new Set()));
80 | }
81 | trackEffects(dep);
82 | }
83 |
84 | export function trackEffects(dep) {
85 | if (dep.has(activeEffect)) return;
86 | dep.add(activeEffect);
87 | // activeEffect中去记录dep
88 | activeEffect.deps.push(dep);
89 | }
90 |
91 | export function isTracking() {
92 | return shouldTrack && activeEffect;
93 | }
94 |
95 | export function pauseTracking() {
96 | shouldTrack = false;
97 | }
98 |
99 | export function enableTracking() {
100 | shouldTrack = true;
101 | }
102 |
103 | export function trigger(target, key, type, value?) {
104 | const depsMap = targetMap.get(target);
105 | if (!depsMap) return;
106 | // const deps = depsMap.get(key) || [];
107 | let deps: any = [];
108 | // 如果新增了 key,需要触发之前 for in 收集到的 effect
109 | // delete 会影响 for ... in的遍历结果,所以也需要触发它的依赖
110 | const iterateDeps =
111 | (type === TriggerType.ADD || TriggerType.DEL ? depsMap.get(ITERATE_KEY) : []) || [];
112 | // 数组 set length,会触发 length 的effect,以及修改 length 导致部分数组元素的变更
113 | // 比如 arr = [1, 2, 3],此时设置arr.length = 1; 会导致arr[1]、arr[2]这两个元素发生变更,即要触发相应的副作用函数
114 | // 同样 如果数组情况下是 ADD 的情况,则同样需要触发 length 的副作用函数
115 | if (isArray(target)) {
116 | // length 触发会影响到数组元素的变更
117 | if (key === "length") {
118 | depsMap.forEach((dep, key) => {
119 | if (key === "length" || Number(key) >= value) {
120 | deps.push(...dep);
121 | }
122 | })
123 | }
124 | // 新增了元素也需要触发 length 绑定的副作用函数
125 | if (type === TriggerType.ADD) {
126 | deps.push(...(depsMap.get("length")) || []);
127 | }
128 | if (type === TriggerType.SET) {
129 | deps.push(...(depsMap.get(key) || []));
130 | }
131 | } else {
132 | deps.push(...(depsMap.get(key)||[]));
133 | }
134 | // deps执行前进行保存,防止陷入死循环
135 | triggerEffects(createDep([...deps, ...iterateDeps]));
136 | }
137 |
138 | export function triggerEffects(deps) {
139 | for (const effect of deps) {
140 | // 当前执行的 activeEffect 与trigger触发的 effect 相同,则不触发执行
141 | if (effect === activeEffect) return;
142 | if (effect.scheduler) {
143 | effect.scheduler();
144 | } else {
145 | effect.run();
146 | }
147 | }
148 | }
149 |
150 | export function stop(runner) {
151 | runner.effect.stop();
152 | }
153 |
154 | export function effect(fn, options: any = {}) {
155 | const _effect = new ReactiveEffect(fn, options.scheduler);
156 | extend(_effect, options);
157 | !options.lazy && _effect.run();
158 | const runner: any = _effect.run.bind(_effect);
159 | runner.effect = _effect;
160 | return runner;
161 | }
--------------------------------------------------------------------------------
/src/reactivity/src/reactive.ts:
--------------------------------------------------------------------------------
1 | import { toRawType } from "../../shared";
2 | import { mutableHandlers, readonlyHandlers, shallowReactiveHandlers, shallowReadonlyHandlers } from "./baseHandlers";
3 |
4 | export const enum ReactiveFlags {
5 | IS_REACTIVE = "__v_isReactive",
6 | IS_READONLY = "__v_isReadonly",
7 | IS_SHALLOW = '__v_isShallow',
8 | }
9 |
10 | export const reactiveMap = new WeakMap();
11 | export const shallowReactiveMap = new WeakMap();
12 |
13 | const enum TargetType {
14 | INVALID = 0,
15 | COMMON = 1,
16 | COLLECTION = 2,
17 | }
18 |
19 | function targetTypeMap(rawType: string) {
20 | switch (rawType) {
21 | case "Object":
22 | case "Array":
23 | return TargetType.COMMON;
24 | case "Map":
25 | case "Set":
26 | case "WeakMap":
27 | case "WeakSet":
28 | return TargetType.COLLECTION;
29 | default:
30 | return TargetType.INVALID;
31 | }
32 | }
33 |
34 | function getTargetType(value) {
35 | return targetTypeMap(toRawType(value));
36 | }
37 |
38 | export function reactive(raw) {
39 | // 优先通过原始对象寻找之前创建的代理对象
40 | const existProxy = reactiveMap.get(raw);
41 | if (existProxy) return existProxy;
42 | // 防止影响到内层的 reactive,如果已经是转换过的,直接返回
43 | if (isProxy(raw)) return raw;
44 | const targetType = getTargetType(raw);
45 | if (targetType === TargetType.INVALID) {
46 | return raw;
47 | }
48 | const proxy = createActiveObject(
49 | raw,
50 | targetType === TargetType.COLLECTION
51 | ? null
52 | : mutableHandlers
53 | );
54 | reactiveMap.set(raw, proxy);
55 | return proxy;
56 | }
57 |
58 | export function shallowReactive(raw) {
59 | const existProxy = shallowReactiveMap.get(raw);
60 | if (existProxy) return existProxy;
61 | const proxy = createActiveObject(raw, shallowReactiveHandlers);
62 | shallowReactiveMap.set(raw, proxy);
63 | return proxy;
64 | }
65 |
66 | export function readonly(raw) {
67 | return createActiveObject(raw, readonlyHandlers);
68 | }
69 |
70 | export function shallowReadonly(raw) {
71 | return createActiveObject(raw, shallowReadonlyHandlers);
72 | }
73 |
74 | function createActiveObject(raw, baseHandlers) {
75 | return new Proxy(raw, baseHandlers);
76 | }
77 |
78 | // if value not wrapped will return undefined,so we need !! operator
79 | export function isReactive(value) {
80 | return !!value[ReactiveFlags.IS_REACTIVE];
81 | }
82 |
83 | export function isReadonly(value) {
84 | return !!value[ReactiveFlags.IS_READONLY];
85 | }
86 |
87 | export function isShallow(value) {
88 | return !!value[ReactiveFlags.IS_SHALLOW];
89 | }
90 |
91 | export function isProxy(value) {
92 | return isReactive(value) || isReadonly(value);
93 | }
--------------------------------------------------------------------------------
/src/reactivity/src/ref.ts:
--------------------------------------------------------------------------------
1 | import { hasChanged, isArray, isObject } from "../../shared";
2 | import { createDep } from "./dep";
3 | import { isTracking, trackEffects, triggerEffects } from "./effective";
4 | import { isProxy, reactive } from "./reactive";
5 |
6 | class RefImpl {
7 | // ref只有一个值,只会对应一个dep
8 | public dep: any;
9 | public __v_isRef = true;
10 | private _value: any;
11 | private _rawValue: any;
12 | constructor(value, isShallow = false) {
13 | this._rawValue = value;
14 | // 如果 value 是 object,则需要用 reactive 包裹 value
15 | this._value = isShallow ? value : convert(value);
16 | this.dep = new Set();
17 | }
18 | get value() {
19 | trackRefValue(this);
20 | return this._value;
21 | }
22 | set value(newValue) {
23 | // 新旧值对比的时候需要考虑到value是对象的时候,其实我们用reactive去包装了
24 | // 所以对比需要用包装前的值和现有的值做比较
25 | if (hasChanged(this._rawValue, newValue)) {
26 | // 先修改值 再执行trigger
27 | this._rawValue = newValue;
28 | this._value = convert(newValue);
29 | triggerRefValue(this);
30 | }
31 | }
32 | }
33 |
34 | class ObjectRefImpl {
35 | public readonly __v_isRef = true;
36 |
37 | constructor(
38 | private readonly _object,
39 | private readonly _key,
40 | private readonly _defaultValue
41 | ) {}
42 |
43 | get value() {
44 | const val = this._object[this._key];
45 | return val === undefined ? this._defaultValue : val;
46 | }
47 |
48 | set value(newVal) {
49 | this._object[this._key] = newVal;
50 | }
51 | }
52 |
53 | export function trackRefValue(ref) {
54 | isTracking() && trackEffects(ref.dep || (ref.dep = createDep()));
55 | }
56 |
57 | export function triggerRefValue(ref) {
58 | triggerEffects(createDep(ref.dep));
59 | }
60 |
61 | function convert(value) {
62 | return isObject(value) ? reactive(value) : value;
63 | }
64 |
65 | export function ref(value?: unknown) {
66 | return new RefImpl(value);
67 | }
68 |
69 | export function shallowRef(value?: unknown) {
70 | return new RefImpl(value, true);
71 | }
72 |
73 | export function toRef(target, key, defaultValue?) {
74 | const val = target[key];
75 | return isRef(val)
76 | ? val
77 | : new ObjectRefImpl(target, key, defaultValue);
78 | }
79 |
80 | export function toRefs(target) {
81 | if (!isProxy(target)) {
82 | console.warn(
83 | `toRefs() expects a reactive object but received a plain one.`
84 | );
85 | }
86 | const ret: any = isArray(target) ? new Array(target.length) : {};
87 | for (const key in target) {
88 | ret[key] = toRef(target, key);
89 | }
90 | return ret;
91 | }
92 |
93 | class CustomRef {
94 | // ref只有一个值,只会对应一个dep
95 | public dep: any;
96 | public __v_isRef = true;
97 | private _get: any;
98 | private _set: any;
99 | constructor(func) {
100 | const { get, set } = func(
101 | () => trackRefValue(this),
102 | () => triggerRefValue(this)
103 | );
104 | this._get = get;
105 | this._set = set;
106 | }
107 | get value() {
108 | return this._get();
109 | }
110 | set value(newVal) {
111 | this._set(newVal);
112 | }
113 | }
114 |
115 | export function customRef(func) {
116 | return new CustomRef(func);
117 | }
118 |
119 | export function isRef(ref) {
120 | return !!(ref && ref.__v_isRef);
121 | }
122 |
123 | export function unref(ref) {
124 | return isRef(ref) ? ref.value : ref;
125 | }
126 |
127 | export function proxyRefs(objectWithRefs) {
128 | return new Proxy(objectWithRefs, {
129 | get(target, key) {
130 | return unref(Reflect.get(target, key));
131 | },
132 | set(target, key, newVal) {
133 | // 传过来的新值如果不是 ref 则需要.value替换
134 | // 如果是 ref 则需要整体替换
135 | if (isRef(Reflect.get(target, key)) && !isRef(newVal)) {
136 | return Reflect.get(target, key).value = newVal;
137 | }
138 | return Reflect.set(target, key, newVal);
139 | },
140 | });
141 | }
--------------------------------------------------------------------------------
/src/reactivity/src/scheduler.ts:
--------------------------------------------------------------------------------
1 | import { effect } from "./effective";
2 | import { reactive } from "./reactive";
3 |
4 | export function scheduler() {
5 | const jobQueue = new Set();
6 | const p = Promise.resolve();
7 |
8 | let isFlushing = false;
9 | function flushJob() {
10 | if (isFlushing) return;
11 | isFlushing = true;
12 | p.then(() => {
13 | jobQueue.forEach((job: any) => job());
14 | }).finally(() => {
15 | isFlushing = false;
16 | })
17 | }
18 |
19 | const obj = reactive({
20 | foo: 1
21 | })
22 | effect(() => {
23 | console.log(obj.foo);
24 | }, {
25 | scheduler(fn) {
26 | jobQueue.add(fn);
27 | flushJob();
28 | }
29 | })
30 | obj.foo++;
31 | obj.foo++;
32 | // 最终只会输出 1 3
33 | // scheduler 帮我们很好的实现不包含过渡状态的操作
34 | // 这个操作类似于 vue 中连续多次修改响应式数据但只会触发一次更新
35 | }
--------------------------------------------------------------------------------
/src/reactivity/src/watch.ts:
--------------------------------------------------------------------------------
1 | import { isFunction, isObject } from "../../shared";
2 | import { effect } from "./effective";
3 |
4 | // source 为 watch 的响应式对象 or 函数
5 | // 函数可以指定该 watch 依赖哪些响应式数据
6 | export function watch(source, cb, options?: any) {
7 | let getter;
8 | if (isFunction(source)) {
9 | getter = source;
10 | } else {
11 | getter = () => traverse(source);
12 | }
13 |
14 | let oldValue, newValue;
15 |
16 | // 用来存储用户注册的过期回调
17 | let cleanup;
18 |
19 | function onInvalidate(fn) {
20 | // 将过期 cb 存储到 cleanup 中
21 | cleanup = fn;
22 | }
23 |
24 | const job = () => {
25 | // 在 scheduler 中重新执行 effect,得到 newValue
26 | newValue = effectFn();
27 | // 在调用当前函数之前,先调用上一个 cb 传过来过期函数
28 | if (cleanup) cleanup();
29 | cb(newValue, oldValue, onInvalidate);
30 | // 更新 oldValue
31 | oldValue = newValue;
32 | };
33 | // 懒执行 这样第一次 effectFn() 拿到 oldValue
34 | const effectFn = effect(getter, {
35 | lazy: true,
36 | scheduler: () => {
37 | // adjust the callback's flush timing
38 | // 默认值 为 pre,if you attempt to access the DOM inside a watcher callback, the DOM will be in the state before Vue has applied any updates.
39 | // flush 为 post,access the DOM in a watcher callback after Vue has updated it
40 | if (options && options.flush === "post") {
41 | const p = Promise.resolve();
42 | p.then(job);
43 | } else {
44 | // 类似于 sync 的实现机制,pre 的方式暂时没办法模拟
45 | job();
46 | }
47 | },
48 | });
49 | // immdiate 立即触发回调函数
50 | if (options && options.immdiate) {
51 | job();
52 | } else {
53 | oldValue = effectFn();
54 | }
55 | }
56 |
57 | // 对 source 递归的进行调用,即依赖收集,如果 source 有相关变化则会执行 cb
58 | // seen 用来记录 traverse 读取过了,避免循环引用导致的死循环
59 | // 暂时只考虑 object 的情况
60 | function traverse(value, seen = new Set()) {
61 | if (!isObject(value) || seen.has(value)) return;
62 | seen.add(value);
63 | for (const k in value) {
64 | traverse(value[k], seen);
65 | }
66 | return value;
67 | }
--------------------------------------------------------------------------------
/src/reactivity/tests/computed.spec.ts:
--------------------------------------------------------------------------------
1 | import { computed } from "../src/computed";
2 | import { effect } from "../src/effective";
3 | import { reactive } from "../src/reactive";
4 |
5 | describe("reactivity/computed", () => {
6 | // 和 ref 类似,但具有缓存功能
7 | it("happy path", () => {
8 | const user = reactive({
9 | age: 1
10 | })
11 |
12 | const age = computed(() => {
13 | return user.age
14 | })
15 |
16 | expect(age.value).toBe(1);
17 | })
18 | it("should return updated value", () => {
19 | const value = reactive({
20 | foo: 1,
21 | });
22 | const cValue = computed(() => value.foo);
23 | expect(cValue.value).toBe(1);
24 | value.foo = 2;
25 | expect(cValue.value).toBe(2);
26 | });
27 |
28 | it("should compute lazily", () => {
29 | const value = reactive({
30 | foo: 1
31 | });
32 | const getter = jest.fn(() => value.foo);
33 | // 当调用 cValue 的 value 属性,才会去执行getter,并且如果依赖的值没有改变,只会执行一次
34 | const cValue = computed(getter);
35 |
36 | // lazy
37 | expect(getter).not.toHaveBeenCalled();
38 |
39 | expect(cValue.value).toBe(1);
40 | expect(getter).toHaveBeenCalledTimes(1);
41 |
42 | // should not compute again
43 | cValue.value;
44 | expect(getter).toHaveBeenCalledTimes(1);
45 |
46 | // should not compute until needed
47 | value.foo = 2;
48 | expect(getter).toHaveBeenCalledTimes(1);
49 |
50 | // now it should compute
51 | expect(cValue.value).toBe(2);
52 | expect(getter).toHaveBeenCalledTimes(2);
53 |
54 | // should not compute again
55 | cValue.value;
56 | expect(getter).toHaveBeenCalledTimes(2);
57 | });
58 |
59 | // 需要 computed 在 effect中生效
60 | // 当
61 | it("should trigger effect", () => {
62 | const value = reactive({});
63 | const cValue = computed(() => value.foo);
64 | let dummy;
65 | effect(() => {
66 | dummy = cValue.value;
67 | });
68 | expect(dummy).toBe(undefined);
69 | value.foo = 1;
70 | expect(dummy).toBe(1);
71 | });
72 |
73 | // it("should work when chained", () => {
74 | // const value = reactive({ foo: 0 });
75 | // const c1 = computed(() => value.foo);
76 | // const c2 = computed(() => c1.value + 1);
77 | // expect(c2.value).toBe(1);
78 | // expect(c1.value).toBe(0);
79 | // value.foo++;
80 | // expect(c2.value).toBe(2);
81 | // expect(c1.value).toBe(1);
82 | // });
83 |
84 | // it("should trigger effect when chained", () => {
85 | // const value = reactive({ foo: 0 });
86 | // const getter1 = jest.fn(() => value.foo);
87 | // const getter2 = jest.fn(() => {
88 | // return c1.value + 1;
89 | // });
90 | // const c1 = computed(getter1);
91 | // const c2 = computed(getter2);
92 |
93 | // let dummy;
94 | // effect(() => {
95 | // dummy = c2.value;
96 | // });
97 | // expect(dummy).toBe(1);
98 | // expect(getter1).toHaveBeenCalledTimes(1);
99 | // expect(getter2).toHaveBeenCalledTimes(1);
100 | // value.foo++;
101 | // expect(dummy).toBe(2);
102 | // // should not result in duplicate calls
103 | // expect(getter1).toHaveBeenCalledTimes(2);
104 | // expect(getter2).toHaveBeenCalledTimes(2);
105 | // });
106 |
107 | // it("should trigger effect when chained (mixed invocations)", () => {
108 | // const value = reactive({ foo: 0 });
109 | // const getter1 = jest.fn(() => value.foo);
110 | // const getter2 = jest.fn(() => {
111 | // return c1.value + 1;
112 | // });
113 | // const c1 = computed(getter1);
114 | // const c2 = computed(getter2);
115 |
116 | // let dummy;
117 | // effect(() => {
118 | // dummy = c1.value + c2.value;
119 | // });
120 | // expect(dummy).toBe(1);
121 |
122 | // expect(getter1).toHaveBeenCalledTimes(1);
123 | // expect(getter2).toHaveBeenCalledTimes(1);
124 | // value.foo++;
125 | // expect(dummy).toBe(3);
126 | // // should not result in duplicate calls
127 | // expect(getter1).toHaveBeenCalledTimes(2);
128 | // expect(getter2).toHaveBeenCalledTimes(2);
129 | // });
130 |
131 | // it("should no longer update when stopped", () => {
132 | // const value = reactive<{ foo?: number }>({});
133 | // const cValue = computed(() => value.foo);
134 | // let dummy;
135 | // effect(() => {
136 | // dummy = cValue.value;
137 | // });
138 | // expect(dummy).toBe(undefined);
139 | // value.foo = 1;
140 | // expect(dummy).toBe(1);
141 | // cValue.effect.stop();
142 | // value.foo = 2;
143 | // expect(dummy).toBe(1);
144 | // });
145 |
146 | // it("should support setter", () => {
147 | // const n = ref(1);
148 | // const plusOne = computed({
149 | // get: () => n.value + 1,
150 | // set: (val) => {
151 | // n.value = val - 1;
152 | // },
153 | // });
154 |
155 | // expect(plusOne.value).toBe(2);
156 | // n.value++;
157 | // expect(plusOne.value).toBe(3);
158 |
159 | // plusOne.value = 0;
160 | // expect(n.value).toBe(-1);
161 | // });
162 |
163 | // it("should trigger effect w/ setter", () => {
164 | // const n = ref(1);
165 | // const plusOne = computed({
166 | // get: () => n.value + 1,
167 | // set: (val) => {
168 | // n.value = val - 1;
169 | // },
170 | // });
171 |
172 | // let dummy;
173 | // effect(() => {
174 | // dummy = n.value;
175 | // });
176 | // expect(dummy).toBe(1);
177 |
178 | // plusOne.value = 0;
179 | // expect(dummy).toBe(-1);
180 | // });
181 |
182 | // it("should warn if trying to set a readonly computed", () => {
183 | // const n = ref(1);
184 | // const plusOne = computed(() => n.value + 1);
185 | // (plusOne as WritableComputedRef).value++; // Type cast to prevent TS from preventing the error
186 |
187 | // expect(
188 | // "Write operation failed: computed value is readonly"
189 | // ).toHaveBeenWarnedLast();
190 | // });
191 |
192 | // it("should be readonly", () => {
193 | // let a = { a: 1 };
194 | // const x = computed(() => a);
195 | // expect(isReadonly(x)).toBe(true);
196 | // expect(isReadonly(x.value)).toBe(false);
197 | // expect(isReadonly(x.value.a)).toBe(false);
198 | // const z = computed({
199 | // get() {
200 | // return a;
201 | // },
202 | // set(v) {
203 | // a = v;
204 | // },
205 | // });
206 | // expect(isReadonly(z)).toBe(false);
207 | // expect(isReadonly(z.value.a)).toBe(false);
208 | // });
209 |
210 | // it("should expose value when stopped", () => {
211 | // const x = computed(() => 1);
212 | // x.effect.stop();
213 | // expect(x.value).toBe(1);
214 | // });
215 |
216 | // it("debug: onTrack", () => {
217 | // let events: DebuggerEvent[] = [];
218 | // const onTrack = jest.fn((e: DebuggerEvent) => {
219 | // events.push(e);
220 | // });
221 | // const obj = reactive({ foo: 1, bar: 2 });
222 | // const c = computed(() => (obj.foo, "bar" in obj, Object.keys(obj)), {
223 | // onTrack,
224 | // });
225 | // expect(c.value).toEqual(["foo", "bar"]);
226 | // expect(onTrack).toHaveBeenCalledTimes(3);
227 | // expect(events).toEqual([
228 | // {
229 | // effect: c.effect,
230 | // target: toRaw(obj),
231 | // type: TrackOpTypes.GET,
232 | // key: "foo",
233 | // },
234 | // {
235 | // effect: c.effect,
236 | // target: toRaw(obj),
237 | // type: TrackOpTypes.HAS,
238 | // key: "bar",
239 | // },
240 | // {
241 | // effect: c.effect,
242 | // target: toRaw(obj),
243 | // type: TrackOpTypes.ITERATE,
244 | // key: ITERATE_KEY,
245 | // },
246 | // ]);
247 | // });
248 |
249 | // it("debug: onTrigger", () => {
250 | // let events: DebuggerEvent[] = [];
251 | // const onTrigger = jest.fn((e: DebuggerEvent) => {
252 | // events.push(e);
253 | // });
254 | // const obj = reactive({ foo: 1 });
255 | // const c = computed(() => obj.foo, { onTrigger });
256 |
257 | // // computed won't trigger compute until accessed
258 | // c.value;
259 |
260 | // obj.foo++;
261 | // expect(c.value).toBe(2);
262 | // expect(onTrigger).toHaveBeenCalledTimes(1);
263 | // expect(events[0]).toEqual({
264 | // effect: c.effect,
265 | // target: toRaw(obj),
266 | // type: TriggerOpTypes.SET,
267 | // key: "foo",
268 | // oldValue: 1,
269 | // newValue: 2,
270 | // });
271 |
272 | // // @ts-ignore
273 | // delete obj.foo;
274 | // expect(c.value).toBeUndefined();
275 | // expect(onTrigger).toHaveBeenCalledTimes(2);
276 | // expect(events[1]).toEqual({
277 | // effect: c.effect,
278 | // target: toRaw(obj),
279 | // type: TriggerOpTypes.DELETE,
280 | // key: "foo",
281 | // oldValue: 2,
282 | // });
283 | // });
284 | });
285 |
--------------------------------------------------------------------------------
/src/reactivity/tests/effect.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from "../src/reactive";
2 | import { effect, stop } from "../src/effective";
3 |
4 | describe("reactivity/effect", () => {
5 | it("should run the passed function once (wrapped by a effect)", () => {
6 | const fnSpy = jest.fn(() => {});
7 | effect(fnSpy);
8 | expect(fnSpy).toHaveBeenCalledTimes(1);
9 | });
10 | it("should observe basic properties", () => {
11 | let dummy;
12 | const counter = reactive({ num: 0 });
13 | effect(() => (dummy = counter.num));
14 | expect(dummy).toBe(0);
15 | counter.num = 7;
16 | expect(dummy).toBe(7);
17 | });
18 |
19 | it("should return runner when call effect", () => {
20 | let dummy = 10;
21 | const runner = effect(() => {
22 | dummy++;
23 | return "foo";
24 | });
25 | expect(dummy).toBe(11);
26 | const r = runner();
27 | expect(dummy).toBe(12);
28 | expect(r).toBe("foo");
29 | });
30 |
31 | it("should observe multiple properties", () => {
32 | let dummy;
33 | const counter = reactive({ num1: 0, num2: 0 });
34 | effect(() => (dummy = counter.num1 + counter.num1 + counter.num2));
35 |
36 | expect(dummy).toBe(0);
37 | counter.num1 = counter.num2 = 7;
38 | expect(dummy).toBe(21);
39 | });
40 |
41 | it("should handle multiple effects", () => {
42 | let dummy1, dummy2;
43 | const counter = reactive({ num: 0 });
44 | effect(() => (dummy1 = counter.num));
45 | effect(() => (dummy2 = counter.num));
46 |
47 | expect(dummy1).toBe(0);
48 | expect(dummy2).toBe(0);
49 | counter.num++;
50 | expect(dummy1).toBe(1);
51 | expect(dummy2).toBe(1);
52 | });
53 |
54 | it("should observe nested properties", () => {
55 | let dummy;
56 | const counter = reactive({ nested: { num: 0 } });
57 | effect(() => (dummy = counter.nested.num));
58 |
59 | expect(dummy).toBe(0);
60 | counter.nested.num = 8;
61 | expect(dummy).toBe(8);
62 | });
63 |
64 | it("should observe delete operations", () => {
65 | let dummy;
66 | const obj = reactive({ prop: "value" });
67 | effect(() => (dummy = obj.prop));
68 |
69 | expect(dummy).toBe("value");
70 | // @ts-ignore
71 | delete obj.prop;
72 | expect(dummy).toBe(undefined);
73 | });
74 |
75 | it("should observe has operations", () => {
76 | let dummy;
77 | const obj = reactive({ prop: "value" });
78 | effect(() => (dummy = "prop" in obj));
79 |
80 | expect(dummy).toBe(true);
81 | // @ts-ignore
82 | delete obj.prop;
83 | expect(dummy).toBe(false);
84 | obj.prop = 12;
85 | expect(dummy).toBe(true);
86 | });
87 |
88 | it("should observe properties on the prototype chain", () => {
89 | let dummy;
90 | const counter = reactive({ num: 0 });
91 | const parentCounter = reactive({ num: 2 });
92 | Object.setPrototypeOf(counter, parentCounter);
93 | effect(() => (dummy = counter.num));
94 |
95 | expect(dummy).toBe(0);
96 | // @ts-ignore
97 | delete counter.num;
98 | expect(dummy).toBe(2);
99 | parentCounter.num = 4;
100 | expect(dummy).toBe(4);
101 | counter.num = 3;
102 | expect(dummy).toBe(3);
103 | });
104 |
105 | it("should observe has operations on the prototype chain", () => {
106 | let dummy;
107 | const counter = reactive({ num: 0 });
108 | const parentCounter = reactive({ num: 2 });
109 | Object.setPrototypeOf(counter, parentCounter);
110 | effect(() => (dummy = "num" in counter));
111 |
112 | expect(dummy).toBe(true);
113 | // @ts-ignore
114 | delete counter.num;
115 | expect(dummy).toBe(true);
116 | // @ts-ignore
117 | delete parentCounter.num;
118 | expect(dummy).toBe(false);
119 | counter.num = 3;
120 | expect(dummy).toBe(true);
121 | });
122 |
123 | // obj.prop = 4; 会导致副作用函数执行两次
124 | // effect(() => (dummy = obj.prop));
125 | // 1. 读取 obj 的 prop 属性,此时会执行 obj 的 get 方法,此时obj.prop与副作用函数有了响应式联系
126 | // 2. 当 obj 中 发现找不到 prop 属性,会去它的原型上找,此时触发 parent 的 get 方法,此时 parent.prop与副作用函数有了响应式联系
127 | // 3. 当我们 obj.prop = 4; 设置的时候,首先会去触发 obj 的 set 方法,然后触发依赖的副作用函数执行
128 | // 4. 由于 set 的时候发现找不到 prop 属性,则去它的原型上 set,即触发 parent 的 set 方法,然后触发依赖的副作用函数执行
129 | // 5. 所以一次设置会导致副作用函数重复的执行两次
130 |
131 | // 优化
132 | // 通过 reveiver 来判断当前 set 触发 是自己的行为还是他人的行为
133 | // 这里 obj get 的时候,target 是 {}, receiver 是 obj
134 | // 通过 obj get 触发从而间接触发 parent get 的时候,target 是 proto,reveiver 是 obj
135 | it("should observe inherited property accessors", () => {
136 | let dummy, parentDummy, hiddenValue: any;
137 | const obj = reactive({});
138 | const parent = reactive({
139 | set prop(value) {
140 | hiddenValue = value;
141 | },
142 | get prop() {
143 | return hiddenValue;
144 | },
145 | });
146 | Object.setPrototypeOf(obj, parent);
147 | effect(() => (dummy = obj.prop));
148 | effect(() => (parentDummy = parent.prop));
149 |
150 | expect(dummy).toBe(undefined);
151 | expect(parentDummy).toBe(undefined);
152 | obj.prop = 4;
153 | expect(dummy).toBe(4);
154 | // this doesn't work, should it?
155 | // expect(parentDummy).toBe(4)
156 | parent.prop = 2;
157 | expect(dummy).toBe(2);
158 | expect(parentDummy).toBe(2);
159 | });
160 |
161 | it("should observe function call chains", () => {
162 | let dummy;
163 | const counter = reactive({ num: 0 });
164 | effect(() => (dummy = getNum()));
165 |
166 | function getNum() {
167 | return counter.num;
168 | }
169 |
170 | expect(dummy).toBe(0);
171 | counter.num = 2;
172 | expect(dummy).toBe(2);
173 | });
174 |
175 | // 对数组的读取操作
176 | // 1. arr[0]
177 | // 2. arr.length
178 | // 3. arr 当作对象,使用 for ... in 循环遍历
179 | // 4. for ... of遍历数组
180 | // 5. 数组的原型方法 concat/join/every/some/find/findIndex/includes以及其他不改变原数组的原型方法
181 |
182 | // 对数组的设置操作
183 | // 1. arr[1] = 3
184 | // 2. arr.length = 0
185 | // 3. 栈方法push/pop/shift/unshift
186 | // 4. 修改原数组的原型方法 splice/fill/sort
187 | it("should observe iteration", () => {
188 | let dummy;
189 | const list = reactive(["Hello"]);
190 | effect(() => (dummy = list.join(" ")));
191 |
192 | expect(dummy).toBe("Hello");
193 | list.push("World!");
194 | expect(dummy).toBe("Hello World!");
195 | list.shift();
196 | expect(dummy).toBe("World!");
197 | });
198 |
199 | it("should observe implicit array length changes", () => {
200 | let dummy;
201 | const list = reactive(["Hello"]);
202 | effect(() => (dummy = list.join(" ")));
203 |
204 | expect(dummy).toBe("Hello");
205 | list[1] = "World!";
206 | expect(dummy).toBe("Hello World!");
207 | list[3] = "Hello!";
208 | expect(dummy).toBe("Hello World! Hello!");
209 | });
210 |
211 | // 由于 reactive 具有深响应的特性
212 | // get arr[0] 的时候,发现 arr[0] 是 object,则会继续调用 reactive 进行包装
213 | // arr.includes 内部也会调用 get arr[0],这样又会调用 reactive 进行包装
214 | // 两次 reactive 返回的对象不一致,所以我们考虑维护一个 reactiveMap,用来维护原始对象 raw 和 proxy 对象的map
215 | it("avoid create reactive multiply", () => {
216 | const obj = {};
217 | const arr = reactive([obj]);
218 | expect(arr.includes(arr[0])).toBe(true);
219 | // 我们希望这也是 true,所以我们需要拦截 includes 方法,当在响应式对象中没找到符合的
220 | // 则去 raw 上找是否符合
221 | expect(arr.includes(obj)).toBe(true);
222 | });
223 |
224 | it("should observe sparse array mutations", () => {
225 | let dummy;
226 | const list = reactive([]);
227 | list[1] = "World!";
228 | effect(() => (dummy = list.join(" ")));
229 |
230 | expect(dummy).toBe(" World!");
231 | list[0] = "Hello";
232 | expect(dummy).toBe("Hello World!");
233 | list.pop();
234 | expect(dummy).toBe("Hello");
235 | });
236 |
237 | it("should observe enumeration", () => {
238 | let dummy = 0;
239 | const numbers = reactive({ num1: 3 });
240 | effect(() => {
241 | dummy = 0;
242 | for (let key in numbers) {
243 | dummy += numbers[key];
244 | }
245 | });
246 |
247 | expect(dummy).toBe(3);
248 | numbers.num2 = 4;
249 | expect(dummy).toBe(7);
250 | delete numbers.num1;
251 | expect(dummy).toBe(4);
252 | });
253 |
254 | it("should observe symbol keyed properties", () => {
255 | const key = Symbol("symbol keyed prop");
256 | let dummy, hasDummy;
257 | const obj = reactive({ [key]: "value" });
258 | effect(() => (dummy = obj[key]));
259 | effect(() => (hasDummy = key in obj));
260 |
261 | expect(dummy).toBe("value");
262 | expect(hasDummy).toBe(true);
263 | obj[key] = "newValue";
264 | expect(dummy).toBe("newValue");
265 | // @ts-ignore
266 | delete obj[key];
267 | expect(dummy).toBe(undefined);
268 | expect(hasDummy).toBe(false);
269 | });
270 |
271 | it("should not observe well-known symbol keyed properties", () => {
272 | const key = Symbol.isConcatSpreadable;
273 | let dummy;
274 | const array: any = reactive([]);
275 | effect(() => (dummy = array[key]));
276 |
277 | expect(array[key]).toBe(undefined);
278 | expect(dummy).toBe(undefined);
279 | array[key] = true;
280 | expect(array[key]).toBe(true);
281 | expect(dummy).toBe(undefined);
282 | });
283 |
284 | it("should observe function valued properties", () => {
285 | const oldFunc = () => {};
286 | const newFunc = () => {};
287 |
288 | let dummy;
289 | const obj = reactive({ func: oldFunc });
290 | effect(() => (dummy = obj.func));
291 |
292 | expect(dummy).toBe(oldFunc);
293 | obj.func = newFunc;
294 | expect(dummy).toBe(newFunc);
295 | });
296 |
297 | // it("should observe chained getters relying on this", () => {
298 | // const obj = reactive({
299 | // a: 1,
300 | // get b() {
301 | // return this.a;
302 | // },
303 | // });
304 |
305 | // let dummy;
306 | // effect(() => (dummy = obj.b));
307 | // expect(dummy).toBe(1);
308 | // obj.a++;
309 | // expect(dummy).toBe(2);
310 | // });
311 |
312 | // it("should observe methods relying on this", () => {
313 | // const obj = reactive({
314 | // a: 1,
315 | // b() {
316 | // return this.a;
317 | // },
318 | // });
319 |
320 | // let dummy;
321 | // effect(() => (dummy = obj.b()));
322 | // expect(dummy).toBe(1);
323 | // obj.a++;
324 | // expect(dummy).toBe(2);
325 | // });
326 |
327 | // it("should not observe set operations without a value change", () => {
328 | // let hasDummy, getDummy;
329 | // const obj = reactive({ prop: "value" });
330 |
331 | // const getSpy = jest.fn(() => (getDummy = obj.prop));
332 | // const hasSpy = jest.fn(() => (hasDummy = "prop" in obj));
333 | // effect(getSpy);
334 | // effect(hasSpy);
335 |
336 | // expect(getDummy).toBe("value");
337 | // expect(hasDummy).toBe(true);
338 | // obj.prop = "value";
339 | // expect(getSpy).toHaveBeenCalledTimes(1);
340 | // expect(hasSpy).toHaveBeenCalledTimes(1);
341 | // expect(getDummy).toBe("value");
342 | // expect(hasDummy).toBe(true);
343 | // });
344 |
345 | // it("should not observe raw mutations", () => {
346 | // let dummy;
347 | // const obj = reactive<{ prop?: string }>({});
348 | // effect(() => (dummy = toRaw(obj).prop));
349 |
350 | // expect(dummy).toBe(undefined);
351 | // obj.prop = "value";
352 | // expect(dummy).toBe(undefined);
353 | // });
354 |
355 | // it("should not be triggered by raw mutations", () => {
356 | // let dummy;
357 | // const obj = reactive<{ prop?: string }>({});
358 | // effect(() => (dummy = obj.prop));
359 |
360 | // expect(dummy).toBe(undefined);
361 | // toRaw(obj).prop = "value";
362 | // expect(dummy).toBe(undefined);
363 | // });
364 |
365 | // it("should not be triggered by inherited raw setters", () => {
366 | // let dummy, parentDummy, hiddenValue: any;
367 | // const obj = reactive<{ prop?: number }>({});
368 | // const parent = reactive({
369 | // set prop(value) {
370 | // hiddenValue = value;
371 | // },
372 | // get prop() {
373 | // return hiddenValue;
374 | // },
375 | // });
376 | // Object.setPrototypeOf(obj, parent);
377 | // effect(() => (dummy = obj.prop));
378 | // effect(() => (parentDummy = parent.prop));
379 |
380 | // expect(dummy).toBe(undefined);
381 | // expect(parentDummy).toBe(undefined);
382 | // toRaw(obj).prop = 4;
383 | // expect(dummy).toBe(undefined);
384 | // expect(parentDummy).toBe(undefined);
385 | // });
386 |
387 | // it("should avoid implicit infinite recursive loops with itself", () => {
388 | // const counter = reactive({ num: 0 });
389 |
390 | // const counterSpy = jest.fn(() => counter.num++);
391 | // effect(counterSpy);
392 | // expect(counter.num).toBe(1);
393 | // expect(counterSpy).toHaveBeenCalledTimes(1);
394 | // counter.num = 4;
395 | // expect(counter.num).toBe(5);
396 | // expect(counterSpy).toHaveBeenCalledTimes(2);
397 | // });
398 |
399 | // effect 里面嵌套了诸如 push 等栈操作
400 | // push 既有读取 length 的操作,也有设置 length 的操作
401 | // 所以当有两个副作用函数在一起
402 | // 第一个 副作用函数执行完毕后,会与 length 属性建立响应式联系
403 | // 当第二个副作用函数执行 push,导致 length 变化,从而第一个副作用函数也执行
404 | // 第一个执行,length 变化,又会导致第二个副作用函数执行
405 | // 。。。
406 |
407 | // 考虑 push 的时候停止 track,因为 push 的本意是修改数组,而不是读取操作,push 的时候加上 shouldTrack 状态
408 | it("should avoid infinite recursive loops when use Array.prototype.push/unshift/pop/shift", () => {
409 | (["push", "unshift"] as const).forEach((key) => {
410 | const arr = reactive([]);
411 | const counterSpy1 = jest.fn(() => (arr[key] as any)(1));
412 | const counterSpy2 = jest.fn(() => (arr[key] as any)(2));
413 | effect(counterSpy1);
414 | effect(counterSpy2);
415 | expect(arr.length).toBe(2);
416 | expect(counterSpy1).toHaveBeenCalledTimes(1);
417 | expect(counterSpy2).toHaveBeenCalledTimes(1);
418 | });
419 | (["pop", "shift"] as const).forEach((key) => {
420 | const arr = reactive([1, 2, 3, 4]);
421 | const counterSpy1 = jest.fn(() => (arr[key] as any)());
422 | const counterSpy2 = jest.fn(() => (arr[key] as any)());
423 | effect(counterSpy1);
424 | effect(counterSpy2);
425 | expect(arr.length).toBe(2);
426 | expect(counterSpy1).toHaveBeenCalledTimes(1);
427 | expect(counterSpy2).toHaveBeenCalledTimes(1);
428 | });
429 | });
430 |
431 | // it("should allow explicitly recursive raw function loops", () => {
432 | // const counter = reactive({ num: 0 });
433 | // const numSpy = jest.fn(() => {
434 | // counter.num++;
435 | // if (counter.num < 10) {
436 | // numSpy();
437 | // }
438 | // });
439 | // effect(numSpy);
440 | // expect(counter.num).toEqual(10);
441 | // expect(numSpy).toHaveBeenCalledTimes(10);
442 | // });
443 |
444 | // it("should avoid infinite loops with other effects", () => {
445 | // const nums = reactive({ num1: 0, num2: 1 });
446 |
447 | // const spy1 = jest.fn(() => (nums.num1 = nums.num2));
448 | // const spy2 = jest.fn(() => (nums.num2 = nums.num1));
449 | // effect(spy1);
450 | // effect(spy2);
451 | // expect(nums.num1).toBe(1);
452 | // expect(nums.num2).toBe(1);
453 | // expect(spy1).toHaveBeenCalledTimes(1);
454 | // expect(spy2).toHaveBeenCalledTimes(1);
455 | // nums.num2 = 4;
456 | // expect(nums.num1).toBe(4);
457 | // expect(nums.num2).toBe(4);
458 | // expect(spy1).toHaveBeenCalledTimes(2);
459 | // expect(spy2).toHaveBeenCalledTimes(2);
460 | // nums.num1 = 10;
461 | // expect(nums.num1).toBe(10);
462 | // expect(nums.num2).toBe(10);
463 | // expect(spy1).toHaveBeenCalledTimes(3);
464 | // expect(spy2).toHaveBeenCalledTimes(3);
465 | // });
466 |
467 | // it("should return a new reactive version of the function", () => {
468 | // function greet() {
469 | // return "Hello World";
470 | // }
471 | // const effect1 = effect(greet);
472 | // const effect2 = effect(greet);
473 | // expect(typeof effect1).toBe("function");
474 | // expect(typeof effect2).toBe("function");
475 | // expect(effect1).not.toBe(greet);
476 | // expect(effect1).not.toBe(effect2);
477 | // });
478 |
479 | // it("should discover new branches while running automatically", () => {
480 | // let dummy;
481 | // const obj = reactive({ prop: "value", run: false });
482 |
483 | // const conditionalSpy = jest.fn(() => {
484 | // dummy = obj.run ? obj.prop : "other";
485 | // });
486 | // effect(conditionalSpy);
487 |
488 | // expect(dummy).toBe("other");
489 | // expect(conditionalSpy).toHaveBeenCalledTimes(1);
490 | // obj.prop = "Hi";
491 | // expect(dummy).toBe("other");
492 | // expect(conditionalSpy).toHaveBeenCalledTimes(1);
493 | // obj.run = true;
494 | // expect(dummy).toBe("Hi");
495 | // expect(conditionalSpy).toHaveBeenCalledTimes(2);
496 | // obj.prop = "World";
497 | // expect(dummy).toBe("World");
498 | // expect(conditionalSpy).toHaveBeenCalledTimes(3);
499 | // });
500 |
501 | // it("should discover new branches when running manually", () => {
502 | // let dummy;
503 | // let run = false;
504 | // const obj = reactive({ prop: "value" });
505 | // const runner = effect(() => {
506 | // dummy = run ? obj.prop : "other";
507 | // });
508 |
509 | // expect(dummy).toBe("other");
510 | // runner();
511 | // expect(dummy).toBe("other");
512 | // run = true;
513 | // runner();
514 | // expect(dummy).toBe("value");
515 | // obj.prop = "World";
516 | // expect(dummy).toBe("World");
517 | // });
518 |
519 | // 分支切换与cleanup
520 | it("should not be triggered by mutating a property, which is used in an inactive branch", () => {
521 | let dummy;
522 | const obj = reactive({ prop: "value", run: true });
523 |
524 | const conditionalSpy = jest.fn(() => {
525 | dummy = obj.run ? obj.prop : "other";
526 | });
527 | effect(conditionalSpy);
528 |
529 | expect(dummy).toBe("value");
530 | expect(conditionalSpy).toHaveBeenCalledTimes(1);
531 | // 此时 run 为 false,应该不再依赖 obj.prop,也就是后续 obj.prop 变动,effect也不会执行
532 | obj.run = false;
533 | expect(dummy).toBe("other");
534 | expect(conditionalSpy).toHaveBeenCalledTimes(2);
535 | obj.prop = "value2";
536 | expect(dummy).toBe("other");
537 | expect(conditionalSpy).toHaveBeenCalledTimes(2);
538 | });
539 |
540 | // obj.foo++ 拆分成 obj.foo = obj.foo + 1
541 | // 1. 首先读取 obj.foo,会触发 track 操作,将当前的 effect 收集起来
542 | // 2. 接着将 +1 后的 foo 赋值 给 obj.foo,此时会触发 trigger 操作,即把之前收集的 effect 拿出来执行
543 | // 3. 但其实上一个 effect 还在执行中,然后又把自己拿出来执行,就会导致无限递归的调用自己,造成栈溢出
544 | // 我们发现对 obj.foo 的读取和设置都在同一个 effect 中执行
545 | // 所以无论是 track 时候要收集的 effect 还是 trigger 时候要触发的 effect,都是 activeEffect
546 | // 基于此我们可以在 trigger 的时候增加 守卫条件
547 | it("should avoid infinite recursion", () => {
548 | let obj = reactive({
549 | foo: 1,
550 | });
551 | effect(() => {
552 | obj.foo++;
553 | });
554 | expect(obj.foo).toBe(2);
555 | });
556 |
557 | // 允许嵌套 effect
558 | // 应用场景:
559 | // vue 的渲染函数其实就是在一个 effect 中执行的,Bar是Foo的子组件
560 | // effect(() => {
561 | // Foo.render();
562 | // effect(() => {
563 | // Bar.render();
564 | // })
565 | // })
566 | it("should allow nested effects", () => {
567 | const nums = reactive({ num1: 0, num2: 1, num3: 2 });
568 | const dummy: any = {};
569 |
570 | const childSpy = jest.fn(() => (dummy.num1 = nums.num1));
571 | const childeffect = effect(childSpy);
572 | const parentSpy = jest.fn(() => {
573 | dummy.num2 = nums.num2;
574 | childeffect();
575 | dummy.num3 = nums.num3;
576 | });
577 | effect(parentSpy);
578 |
579 | expect(dummy).toEqual({ num1: 0, num2: 1, num3: 2 });
580 | expect(parentSpy).toHaveBeenCalledTimes(1);
581 | expect(childSpy).toHaveBeenCalledTimes(2);
582 | // this should only call the childeffect
583 | nums.num1 = 4;
584 | expect(dummy).toEqual({ num1: 4, num2: 1, num3: 2 });
585 | expect(parentSpy).toHaveBeenCalledTimes(1);
586 | expect(childSpy).toHaveBeenCalledTimes(3);
587 | // this calls the parenteffect, which calls the childeffect once
588 | nums.num2 = 10;
589 | expect(dummy).toEqual({ num1: 4, num2: 10, num3: 2 });
590 | expect(parentSpy).toHaveBeenCalledTimes(2);
591 | expect(childSpy).toHaveBeenCalledTimes(4);
592 | // this calls the parenteffect, which calls the childeffect once
593 | nums.num3 = 7;
594 | expect(dummy).toEqual({ num1: 4, num2: 10, num3: 7 });
595 | expect(parentSpy).toHaveBeenCalledTimes(3);
596 | expect(childSpy).toHaveBeenCalledTimes(5);
597 | });
598 |
599 | // 懒执行 effect
600 | it("lazy", () => {
601 | const obj = reactive({ foo: 1 });
602 | let dummy;
603 | const runner = effect(() => (dummy = obj.foo), { lazy: true });
604 | expect(dummy).toBe(undefined);
605 |
606 | expect(runner()).toBe(1);
607 | expect(dummy).toBe(1);
608 | obj.foo = 2;
609 | expect(dummy).toBe(2);
610 | });
611 |
612 | // 可调度
613 | // 当 trigger 动作触发 effect 重新执行时,有能力决定 effect 执行的时机、次数以及方式
614 | it("scheduler", () => {
615 | // 通过 effect 的第二个参数给定 scheduler
616 | // 当 effect 第一次执行的时候还会执行 fn
617 | // 当 set 的时候,不会执行 fn,而是执行 scheduler
618 | // 如果执行 runner 的时候,会再次执行 fn
619 | let dummy;
620 | let run: any;
621 | const scheduler = jest.fn(() => {
622 | run = runner;
623 | });
624 | const obj = reactive({ foo: 1 });
625 | const runner = effect(
626 | () => {
627 | dummy = obj.foo;
628 | },
629 | { scheduler }
630 | );
631 | expect(scheduler).not.toHaveBeenCalled();
632 | expect(dummy).toBe(1);
633 | // should be called on first trigger
634 | obj.foo++;
635 | expect(scheduler).toHaveBeenCalledTimes(1);
636 | // should not run yet
637 | expect(dummy).toBe(1);
638 | // manually run
639 | run();
640 | // should have run
641 | expect(dummy).toBe(2);
642 | });
643 |
644 | it("stop", () => {
645 | // 通过 stop(runner) 以后,通过 set 触发 fn 执行就不存在了
646 | // 但是仍然可以手动执行 runner
647 | // 所以我们考虑将当调用stop的时候将 effect 从 deps 中删除,这样我们 set 操作以后,就不会执行 effect
648 | // 之前是用 depsMap 记录了 ReactiveEffect,那么如何通过 ReactiveEffect 找到 deps
649 | // 通过双向记录的方式
650 | let dummy;
651 | const obj = reactive({ prop: 1 });
652 | const runner = effect(() => {
653 | dummy = obj.prop;
654 | });
655 | obj.prop = 2;
656 | expect(dummy).toBe(2);
657 | stop(runner);
658 | // obj.prop = 3;
659 | // obj.prop = obj.prop + 1;
660 | // 相比于 obj.prop = 3,++涉及到了get操作,这时候又会去收集依赖,之前
661 | // stop 的时候清除的依赖白做了,考虑给全局加个 shouldTrack 参数用来判断
662 | obj.prop++;
663 | expect(dummy).toBe(2);
664 |
665 | // stopped effect should still be manually callable
666 | runner();
667 | expect(dummy).toBe(3);
668 | });
669 |
670 | it("events: onStop", () => {
671 | const onStop = jest.fn();
672 | const runner = effect(() => {}, {
673 | onStop,
674 | });
675 |
676 | stop(runner);
677 | expect(onStop).toHaveBeenCalled();
678 | });
679 | });
680 |
--------------------------------------------------------------------------------
/src/reactivity/tests/reactive.spec.ts:
--------------------------------------------------------------------------------
1 | import { isReactive, reactive } from "../src/reactive"
2 |
3 | describe('reactivity/reactive', () => {
4 | test("Object", () => {
5 | const original = { foo: 1 };
6 | const observed = reactive(original);
7 | expect(observed).not.toBe(original);
8 | expect(isReactive(observed)).toBe(true);
9 | expect(isReactive(original)).toBe(false);
10 | // get
11 | expect(observed.foo).toBe(1);
12 | // has
13 | expect("foo" in observed).toBe(true);
14 | // ownKeys
15 | expect(Object.keys(observed)).toEqual(["foo"]);
16 | });
17 |
18 | test("proto", () => {
19 | const obj = {};
20 | const reactiveObj = reactive(obj);
21 | expect(isReactive(reactiveObj)).toBe(true);
22 | // read prop of reactiveObject will cause reactiveObj[prop] to be reactive
23 | // @ts-ignore
24 | const prototype = reactiveObj["__proto__"];
25 | const otherObj = { data: ["a"] };
26 | expect(isReactive(otherObj)).toBe(false);
27 | const reactiveOther = reactive(otherObj);
28 | expect(isReactive(reactiveOther)).toBe(true);
29 | expect(reactiveOther.data[0]).toBe("a");
30 | });
31 |
32 | test("nested reactives", () => {
33 | const original = {
34 | nested: {
35 | foo: 1,
36 | },
37 | array: [{ bar: 2 }],
38 | };
39 | const observed = reactive(original);
40 | expect(isReactive(observed.nested)).toBe(true);
41 | expect(isReactive(observed.array)).toBe(true);
42 | expect(isReactive(observed.array[0])).toBe(true);
43 | });
44 |
45 | // test("observing subtypes of IterableCollections(Map, Set)", () => {
46 | // // subtypes of Map
47 | // class CustomMap extends Map {}
48 | // const cmap = reactive(new CustomMap());
49 |
50 | // expect(cmap instanceof Map).toBe(true);
51 | // expect(isReactive(cmap)).toBe(true);
52 |
53 | // cmap.set("key", {});
54 | // expect(isReactive(cmap.get("key"))).toBe(true);
55 |
56 | // // subtypes of Set
57 | // class CustomSet extends Set {}
58 | // const cset = reactive(new CustomSet());
59 |
60 | // expect(cset instanceof Set).toBe(true);
61 | // expect(isReactive(cset)).toBe(true);
62 |
63 | // let dummy;
64 | // effect(() => (dummy = cset.has("value")));
65 | // expect(dummy).toBe(false);
66 | // cset.add("value");
67 | // expect(dummy).toBe(true);
68 | // cset.delete("value");
69 | // expect(dummy).toBe(false);
70 | // });
71 |
72 | // test("observing subtypes of WeakCollections(WeakMap, WeakSet)", () => {
73 | // // subtypes of WeakMap
74 | // class CustomMap extends WeakMap {}
75 | // const cmap = reactive(new CustomMap());
76 |
77 | // expect(cmap instanceof WeakMap).toBe(true);
78 | // expect(isReactive(cmap)).toBe(true);
79 |
80 | // const key = {};
81 | // cmap.set(key, {});
82 | // expect(isReactive(cmap.get(key))).toBe(true);
83 |
84 | // // subtypes of WeakSet
85 | // class CustomSet extends WeakSet {}
86 | // const cset = reactive(new CustomSet());
87 |
88 | // expect(cset instanceof WeakSet).toBe(true);
89 | // expect(isReactive(cset)).toBe(true);
90 |
91 | // let dummy;
92 | // effect(() => (dummy = cset.has(key)));
93 | // expect(dummy).toBe(false);
94 | // cset.add(key);
95 | // expect(dummy).toBe(true);
96 | // cset.delete(key);
97 | // expect(dummy).toBe(false);
98 | // });
99 |
100 | // test("observed value should proxy mutations to original (Object)", () => {
101 | // const original: any = { foo: 1 };
102 | // const observed = reactive(original);
103 | // // set
104 | // observed.bar = 1;
105 | // expect(observed.bar).toBe(1);
106 | // expect(original.bar).toBe(1);
107 | // // delete
108 | // delete observed.foo;
109 | // expect("foo" in observed).toBe(false);
110 | // expect("foo" in original).toBe(false);
111 | // });
112 |
113 | // test("original value change should reflect in observed value (Object)", () => {
114 | // const original: any = { foo: 1 };
115 | // const observed = reactive(original);
116 | // // set
117 | // original.bar = 1;
118 | // expect(original.bar).toBe(1);
119 | // expect(observed.bar).toBe(1);
120 | // // delete
121 | // delete original.foo;
122 | // expect("foo" in original).toBe(false);
123 | // expect("foo" in observed).toBe(false);
124 | // });
125 |
126 | // test("setting a property with an unobserved value should wrap with reactive", () => {
127 | // const observed = reactive<{ foo?: object }>({});
128 | // const raw = {};
129 | // observed.foo = raw;
130 | // expect(observed.foo).not.toBe(raw);
131 | // expect(isReactive(observed.foo)).toBe(true);
132 | // });
133 |
134 | // test("observing already observed value should return same Proxy", () => {
135 | // const original = { foo: 1 };
136 | // const observed = reactive(original);
137 | // const observed2 = reactive(observed);
138 | // expect(observed2).toBe(observed);
139 | // });
140 |
141 | // test("observing the same value multiple times should return same Proxy", () => {
142 | // const original = { foo: 1 };
143 | // const observed = reactive(original);
144 | // const observed2 = reactive(original);
145 | // expect(observed2).toBe(observed);
146 | // });
147 |
148 | // test("should not pollute original object with Proxies", () => {
149 | // const original: any = { foo: 1 };
150 | // const original2 = { bar: 2 };
151 | // const observed = reactive(original);
152 | // const observed2 = reactive(original2);
153 | // observed.bar = observed2;
154 | // expect(observed.bar).toBe(observed2);
155 | // expect(original.bar).toBe(original2);
156 | // });
157 |
158 | // test("toRaw", () => {
159 | // const original = { foo: 1 };
160 | // const observed = reactive(original);
161 | // expect(toRaw(observed)).toBe(original);
162 | // expect(toRaw(original)).toBe(original);
163 | // });
164 |
165 | // test("toRaw on object using reactive as prototype", () => {
166 | // const original = reactive({});
167 | // const obj = Object.create(original);
168 | // const raw = toRaw(obj);
169 | // expect(raw).toBe(obj);
170 | // expect(raw).not.toBe(toRaw(original));
171 | // });
172 |
173 | // test("should not unwrap Ref", () => {
174 | // const observedNumberRef = reactive(ref(1));
175 | // const observedObjectRef = reactive(ref({ foo: 1 }));
176 |
177 | // expect(isRef(observedNumberRef)).toBe(true);
178 | // expect(isRef(observedObjectRef)).toBe(true);
179 | // });
180 |
181 | // test("should unwrap computed refs", () => {
182 | // // readonly
183 | // const a = computed(() => 1);
184 | // // writable
185 | // const b = computed({
186 | // get: () => 1,
187 | // set: () => {},
188 | // });
189 | // const obj = reactive({ a, b });
190 | // // check type
191 | // obj.a + 1;
192 | // obj.b + 1;
193 | // expect(typeof obj.a).toBe(`number`);
194 | // expect(typeof obj.b).toBe(`number`);
195 | // });
196 |
197 | // test("should allow setting property from a ref to another ref", () => {
198 | // const foo = ref(0);
199 | // const bar = ref(1);
200 | // const observed = reactive({ a: foo });
201 | // const dummy = computed(() => observed.a);
202 | // expect(dummy.value).toBe(0);
203 |
204 | // // @ts-ignore
205 | // observed.a = bar;
206 | // expect(dummy.value).toBe(1);
207 |
208 | // bar.value++;
209 | // expect(dummy.value).toBe(2);
210 | // });
211 |
212 | // test("non-observable values", () => {
213 | // const assertValue = (value: any) => {
214 | // reactive(value);
215 | // expect(
216 | // `value cannot be made reactive: ${String(value)}`
217 | // ).toHaveBeenWarnedLast();
218 | // };
219 |
220 | // // number
221 | // assertValue(1);
222 | // // string
223 | // assertValue("foo");
224 | // // boolean
225 | // assertValue(false);
226 | // // null
227 | // assertValue(null);
228 | // // undefined
229 | // assertValue(undefined);
230 | // // symbol
231 | // const s = Symbol();
232 | // assertValue(s);
233 |
234 | // // built-ins should work and return same value
235 | // const p = Promise.resolve();
236 | // expect(reactive(p)).toBe(p);
237 | // const r = new RegExp("");
238 | // expect(reactive(r)).toBe(r);
239 | // const d = new Date();
240 | // expect(reactive(d)).toBe(d);
241 | // });
242 |
243 | // test("markRaw", () => {
244 | // const obj = reactive({
245 | // foo: { a: 1 },
246 | // bar: markRaw({ b: 2 }),
247 | // });
248 | // expect(isReactive(obj.foo)).toBe(true);
249 | // expect(isReactive(obj.bar)).toBe(false);
250 | // });
251 |
252 | // test("should not observe non-extensible objects", () => {
253 | // const obj = reactive({
254 | // foo: Object.preventExtensions({ a: 1 }),
255 | // // sealed or frozen objects are considered non-extensible as well
256 | // bar: Object.freeze({ a: 1 }),
257 | // baz: Object.seal({ a: 1 }),
258 | // });
259 | // expect(isReactive(obj.foo)).toBe(false);
260 | // expect(isReactive(obj.bar)).toBe(false);
261 | // expect(isReactive(obj.baz)).toBe(false);
262 | // });
263 |
264 | // test("should not observe objects with __v_skip", () => {
265 | // const original = {
266 | // foo: 1,
267 | // __v_skip: true,
268 | // };
269 | // const observed = reactive(original);
270 | // expect(isReactive(observed)).toBe(false);
271 | // });
272 | })
--------------------------------------------------------------------------------
/src/reactivity/tests/readonly.spec.ts:
--------------------------------------------------------------------------------
1 | import { isProxy, isReactive, isReadonly, readonly } from "../src/reactive";
2 |
3 | describe('reactivity/readonly', () => {
4 | it("should make nested values readonly", () => {
5 | const original = { foo: 1, bar: { baz: 2 } };
6 | const wrapped = readonly(original);
7 | expect(wrapped).not.toBe(original);
8 | expect(isProxy(wrapped)).toBe(true);
9 | expect(isReactive(wrapped)).toBe(false);
10 | expect(isReadonly(wrapped)).toBe(true);
11 | expect(isReactive(original)).toBe(false);
12 | expect(isReadonly(original)).toBe(false);
13 | expect(isReactive(wrapped.bar)).toBe(false);
14 | expect(isReadonly(wrapped.bar)).toBe(true);
15 | expect(isReactive(original.bar)).toBe(false);
16 | expect(isReadonly(original.bar)).toBe(false);
17 | // get
18 | expect(wrapped.foo).toBe(1);
19 | // has
20 | expect("foo" in wrapped).toBe(true);
21 | // ownKeys
22 | expect(Object.keys(wrapped)).toEqual(["foo", "bar"]);
23 | });
24 |
25 | it("warn then call set", () => {
26 | console.warn = jest.fn();
27 | const original = { foo: 1, bar: { baz: 2 } };
28 | const wrapped = readonly(original);
29 | wrapped.foo = 2;
30 | expect(console.warn).toBeCalled();
31 | })
32 | })
--------------------------------------------------------------------------------
/src/reactivity/tests/ref.spec.ts:
--------------------------------------------------------------------------------
1 | import { effect } from "../src/effective";
2 | import { reactive } from "../src/reactive";
3 | import { customRef, isRef, proxyRefs, ref, shallowRef, toRef, toRefs, unref } from "../src/ref";
4 |
5 | describe("reactivity/ref", () => {
6 | it("should hold a value", () => {
7 | const a = ref(1);
8 | expect(a.value).toBe(1);
9 | a.value = 2;
10 | expect(a.value).toBe(2);
11 | });
12 |
13 | it("should be reactive", () => {
14 | const a = ref(1);
15 | let dummy;
16 | let calls = 0;
17 | effect(() => {
18 | calls++;
19 | dummy = a.value;
20 | });
21 | expect(calls).toBe(1);
22 | expect(dummy).toBe(1);
23 | a.value = 2;
24 | expect(calls).toBe(2);
25 | expect(dummy).toBe(2);
26 | // same value should not trigger
27 | a.value = 2;
28 | expect(calls).toBe(2);
29 | });
30 |
31 | it("should make nested properties reactive", () => {
32 | const a = ref({
33 | count: 1,
34 | });
35 | let dummy;
36 | effect(() => {
37 | dummy = a.value.count;
38 | });
39 | expect(dummy).toBe(1);
40 | a.value.count = 2;
41 | expect(dummy).toBe(2);
42 | });
43 |
44 | // setup() 返回的对象值有ref类型的,但是我们实际用的时候没有再 .value
45 | it("proxyRefs", () => {
46 | const user = {
47 | age: ref(10),
48 | name: "zhou",
49 | };
50 | const proxyUser = proxyRefs(user);
51 | expect(user.age.value).toBe(10);
52 | expect(proxyUser.age).toBe(10);
53 | expect(proxyUser.name).toBe("zhou");
54 |
55 | proxyUser.age = 20;
56 | expect(user.age.value).toBe(20);
57 | expect(proxyUser.age).toBe(20);
58 |
59 | proxyUser.age = ref(10);
60 | expect(user.age.value).toBe(10);
61 | expect(proxyUser.age).toBe(10);
62 | });
63 |
64 | it("should work without initial value", () => {
65 | const a = ref();
66 | let dummy;
67 | effect(() => {
68 | dummy = a.value;
69 | });
70 | expect(dummy).toBe(undefined);
71 | a.value = 2;
72 | expect(dummy).toBe(2);
73 | });
74 |
75 | // it("should work like a normal property when nested in a reactive object", () => {
76 | // const a = ref(1);
77 | // const obj = reactive({
78 | // a,
79 | // b: {
80 | // c: a,
81 | // },
82 | // });
83 |
84 | // let dummy1: number;
85 | // let dummy2: number;
86 |
87 | // effect(() => {
88 | // dummy1 = obj.a;
89 | // dummy2 = obj.b.c;
90 | // });
91 |
92 | // const assertDummiesEqualTo = (val: number) =>
93 | // [dummy1, dummy2].forEach((dummy) => expect(dummy).toBe(val));
94 |
95 | // assertDummiesEqualTo(1);
96 | // a.value++;
97 | // assertDummiesEqualTo(2);
98 | // obj.a++;
99 | // assertDummiesEqualTo(3);
100 | // obj.b.c++;
101 | // assertDummiesEqualTo(4);
102 | // });
103 |
104 | // it("should unwrap nested ref in types", () => {
105 | // const a = ref(0);
106 | // const b = ref(a);
107 |
108 | // expect(typeof (b.value + 1)).toBe("number");
109 | // });
110 |
111 | // it("should unwrap nested values in types", () => {
112 | // const a = {
113 | // b: ref(0),
114 | // };
115 |
116 | // const c = ref(a);
117 |
118 | // expect(typeof (c.value.b + 1)).toBe("number");
119 | // });
120 |
121 | // it("should NOT unwrap ref types nested inside arrays", () => {
122 | // const arr = ref([1, ref(3)]).value;
123 | // expect(isRef(arr[0])).toBe(false);
124 | // expect(isRef(arr[1])).toBe(true);
125 | // expect((arr[1] as Ref).value).toBe(3);
126 | // });
127 |
128 | // it("should unwrap ref types as props of arrays", () => {
129 | // const arr = [ref(0)];
130 | // const symbolKey = Symbol("");
131 | // arr["" as any] = ref(1);
132 | // arr[symbolKey as any] = ref(2);
133 | // const arrRef = ref(arr).value;
134 | // expect(isRef(arrRef[0])).toBe(true);
135 | // expect(isRef(arrRef["" as any])).toBe(false);
136 | // expect(isRef(arrRef[symbolKey as any])).toBe(false);
137 | // expect(arrRef["" as any]).toBe(1);
138 | // expect(arrRef[symbolKey as any]).toBe(2);
139 | // });
140 |
141 | // it("should keep tuple types", () => {
142 | // const tuple: [number, string, { a: number }, () => number, Ref] = [
143 | // 0,
144 | // "1",
145 | // { a: 1 },
146 | // () => 0,
147 | // ref(0),
148 | // ];
149 | // const tupleRef = ref(tuple);
150 |
151 | // tupleRef.value[0]++;
152 | // expect(tupleRef.value[0]).toBe(1);
153 | // tupleRef.value[1] += "1";
154 | // expect(tupleRef.value[1]).toBe("11");
155 | // tupleRef.value[2].a++;
156 | // expect(tupleRef.value[2].a).toBe(2);
157 | // expect(tupleRef.value[3]()).toBe(0);
158 | // tupleRef.value[4].value++;
159 | // expect(tupleRef.value[4].value).toBe(1);
160 | // });
161 |
162 | // it("should keep symbols", () => {
163 | // const customSymbol = Symbol();
164 | // const obj = {
165 | // [Symbol.asyncIterator]: ref(1),
166 | // [Symbol.hasInstance]: { a: ref("a") },
167 | // [Symbol.isConcatSpreadable]: { b: ref(true) },
168 | // [Symbol.iterator]: [ref(1)],
169 | // [Symbol.match]: new Set[>(),
170 | // [Symbol.matchAll]: new Map>(),
171 | // [Symbol.replace]: { arr: [ref("a")] },
172 | // [Symbol.search]: { set: new Set][>() },
173 | // [Symbol.species]: { map: new Map>() },
174 | // [Symbol.split]: new WeakSet][>(),
175 | // [Symbol.toPrimitive]: new WeakMap][, string>(),
176 | // [Symbol.toStringTag]: { weakSet: new WeakSet][>() },
177 | // [Symbol.unscopables]: { weakMap: new WeakMap][, string>() },
178 | // [customSymbol]: { arr: [ref(1)] },
179 | // };
180 |
181 | // const objRef = ref(obj);
182 |
183 | // const keys: (keyof typeof obj)[] = [
184 | // Symbol.asyncIterator,
185 | // Symbol.hasInstance,
186 | // Symbol.isConcatSpreadable,
187 | // Symbol.iterator,
188 | // Symbol.match,
189 | // Symbol.matchAll,
190 | // Symbol.replace,
191 | // Symbol.search,
192 | // Symbol.species,
193 | // Symbol.split,
194 | // Symbol.toPrimitive,
195 | // Symbol.toStringTag,
196 | // Symbol.unscopables,
197 | // customSymbol,
198 | // ];
199 |
200 | // keys.forEach((key) => {
201 | // expect(objRef.value[key]).toStrictEqual(obj[key]);
202 | // });
203 | // });
204 |
205 | test("unref", () => {
206 | expect(unref(1)).toBe(1);
207 | expect(unref(ref(1))).toBe(1);
208 | });
209 |
210 | // test("shallowRef", () => {
211 | // const sref = shallowRef({ a: 1 });
212 | // expect(isReactive(sref.value)).toBe(false);
213 |
214 | // let dummy;
215 | // effect(() => {
216 | // dummy = sref.value.a;
217 | // });
218 | // expect(dummy).toBe(1);
219 |
220 | // sref.value = { a: 2 };
221 | // expect(isReactive(sref.value)).toBe(false);
222 | // expect(dummy).toBe(2);
223 | // });
224 |
225 | // test("shallowRef force trigger", () => {
226 | // const sref = shallowRef({ a: 1 });
227 | // let dummy;
228 | // effect(() => {
229 | // dummy = sref.value.a;
230 | // });
231 | // expect(dummy).toBe(1);
232 |
233 | // sref.value.a = 2;
234 | // expect(dummy).toBe(1); // should not trigger yet
235 |
236 | // // force trigger
237 | // triggerRef(sref);
238 | // expect(dummy).toBe(2);
239 | // });
240 |
241 | // test("shallowRef isShallow", () => {
242 | // expect(isShallow(shallowRef({ a: 1 }))).toBe(true);
243 | // });
244 |
245 | test("isRef", () => {
246 | expect(isRef(ref(1))).toBe(true);
247 | // expect(isRef(computed(() => 1))).toBe(true);
248 |
249 | expect(isRef(0)).toBe(false);
250 | expect(isRef(1)).toBe(false);
251 | // an object that looks like a ref isn't necessarily a ref
252 | expect(isRef({ value: 0 })).toBe(false);
253 | });
254 |
255 | // toRef 主要用来解决响应丢失问题
256 | test("toRef", () => {
257 | const a = reactive({
258 | x: 1,
259 | });
260 | const x = toRef(a, "x");
261 | expect(isRef(x)).toBe(true);
262 | expect(x.value).toBe(1);
263 |
264 | // source -> proxy
265 | a.x = 2;
266 | expect(x.value).toBe(2);
267 |
268 | // proxy -> source
269 | x.value = 3;
270 | expect(a.x).toBe(3);
271 |
272 | // reactivity
273 | let dummyX;
274 | effect(() => {
275 | dummyX = x.value;
276 | });
277 | expect(dummyX).toBe(x.value);
278 |
279 | // mutating source should trigger effect using the proxy refs
280 | a.x = 4;
281 | expect(dummyX).toBe(4);
282 |
283 | // should keep ref
284 | const r = { x: ref(1) };
285 | expect(toRef(r, "x")).toBe(r.x);
286 | });
287 |
288 | test("toRef default value", () => {
289 | const a: { x: number | undefined } = { x: undefined };
290 | const x = toRef(a, "x", 1);
291 | expect(x.value).toBe(1);
292 |
293 | a.x = 2;
294 | expect(x.value).toBe(2);
295 |
296 | a.x = undefined;
297 | expect(x.value).toBe(1);
298 | });
299 |
300 | test("toRefs", () => {
301 | const a = reactive({
302 | x: 1,
303 | y: 2,
304 | });
305 |
306 | const { x, y } = toRefs(a);
307 |
308 | expect(isRef(x)).toBe(true);
309 | expect(isRef(y)).toBe(true);
310 | expect(x.value).toBe(1);
311 | expect(y.value).toBe(2);
312 |
313 | // source -> proxy
314 | a.x = 2;
315 | a.y = 3;
316 | expect(x.value).toBe(2);
317 | expect(y.value).toBe(3);
318 |
319 | // proxy -> source
320 | x.value = 3;
321 | y.value = 4;
322 | expect(a.x).toBe(3);
323 | expect(a.y).toBe(4);
324 |
325 | // reactivity
326 | let dummyX, dummyY;
327 | effect(() => {
328 | dummyX = x.value;
329 | dummyY = y.value;
330 | });
331 | expect(dummyX).toBe(x.value);
332 | expect(dummyY).toBe(y.value);
333 |
334 | // mutating source should trigger effect using the proxy refs
335 | a.x = 4;
336 | a.y = 5;
337 | expect(dummyX).toBe(4);
338 | expect(dummyY).toBe(5);
339 | });
340 |
341 | // test("toRefs should warn on plain object", () => {
342 | // toRefs({});
343 | // expect(`toRefs() expects a reactive object`).toHaveBeenWarned();
344 | // });
345 |
346 | // test("toRefs should warn on plain array", () => {
347 | // toRefs([]);
348 | // expect(`toRefs() expects a reactive object`).toHaveBeenWarned();
349 | // });
350 |
351 | test("toRefs reactive array", () => {
352 | const arr = reactive(["a", "b", "c"]);
353 | const refs = toRefs(arr);
354 |
355 | expect(Array.isArray(refs)).toBe(true);
356 |
357 | refs[0].value = "1";
358 | expect(arr[0]).toBe("1");
359 |
360 | arr[1] = "2";
361 | expect(refs[1].value).toBe("2");
362 | });
363 |
364 | test("customRef", () => {
365 | let value = 1;
366 | let _trigger: () => void;
367 |
368 | const custom = customRef((track, trigger) => ({
369 | get() {
370 | track();
371 | return value;
372 | },
373 | set(newValue: number) {
374 | value = newValue;
375 | _trigger = trigger;
376 | },
377 | }));
378 |
379 | expect(isRef(custom)).toBe(true);
380 |
381 | let dummy;
382 | effect(() => {
383 | dummy = custom.value;
384 | });
385 | expect(dummy).toBe(1);
386 |
387 | custom.value = 2;
388 | // should not trigger yet
389 | expect(dummy).toBe(1);
390 |
391 | _trigger!();
392 | expect(dummy).toBe(2);
393 | });
394 |
395 | test("should not trigger when setting value to same proxy", () => {
396 | const obj = reactive({ count: 0 });
397 |
398 | const a = ref(obj);
399 | const spy1 = jest.fn(() => a.value);
400 |
401 | effect(spy1);
402 |
403 | a.value = obj;
404 | expect(spy1).toBeCalledTimes(1);
405 |
406 | const b = shallowRef(obj);
407 | const spy2 = jest.fn(() => b.value);
408 |
409 | effect(spy2);
410 |
411 | b.value = obj;
412 | expect(spy2).toBeCalledTimes(1);
413 | });
414 | });
415 |
--------------------------------------------------------------------------------
/src/reactivity/tests/shallowReactive.spec.ts:
--------------------------------------------------------------------------------
1 | import { effect } from "../src/effective";
2 | import { isReactive, isShallow, reactive, shallowReactive, shallowReadonly } from "../src/reactive";
3 |
4 | describe("shallowReactive", () => {
5 | test("should not make non-reactive properties reactive", () => {
6 | const props = shallowReactive({ n: { foo: 1 } });
7 | expect(isReactive(props.n)).toBe(false);
8 | });
9 |
10 | test("should keep reactive properties reactive", () => {
11 | const props: any = shallowReactive({ n: reactive({ foo: 1 }) });
12 | props.n = reactive({ foo: 2 });
13 | expect(isReactive(props.n)).toBe(true);
14 | });
15 |
16 | // #2843
17 | test("should allow shallow and normal reactive for same target", () => {
18 | const original = { foo: {} };
19 | const shallowProxy = shallowReactive(original);
20 | const reactiveProxy = reactive(original);
21 | expect(shallowProxy).not.toBe(reactiveProxy);
22 | expect(isReactive(shallowProxy.foo)).toBe(false);
23 | expect(isReactive(reactiveProxy.foo)).toBe(true);
24 | });
25 |
26 | test("isShallow", () => {
27 | expect(isShallow(shallowReactive({}))).toBe(true);
28 | expect(isShallow(shallowReadonly({}))).toBe(true);
29 | });
30 |
31 | // #5271
32 | test("should respect shallow reactive nested inside reactive on reset", () => {
33 | const r = reactive({ foo: shallowReactive({ bar: {} }) });
34 | expect(isShallow(r.foo)).toBe(true);
35 | expect(isReactive(r.foo.bar)).toBe(false);
36 |
37 | r.foo = shallowReactive({ bar: {} });
38 | expect(isShallow(r.foo)).toBe(true);
39 | expect(isReactive(r.foo.bar)).toBe(false);
40 | });
41 |
42 | test("should respect shallow/deep versions of same target on access", () => {
43 | const original = {};
44 | const shallow = shallowReactive(original);
45 | const deep = reactive(original);
46 | const r = reactive({ shallow, deep });
47 | expect(r.shallow).toBe(shallow);
48 | expect(r.deep).toBe(deep);
49 | });
50 |
51 | // describe("collections", () => {
52 | // test("should be reactive", () => {
53 | // const shallowSet = shallowReactive(new Set());
54 | // const a = {};
55 | // let size;
56 |
57 | // effect(() => {
58 | // size = shallowSet.size;
59 | // });
60 |
61 | // expect(size).toBe(0);
62 |
63 | // shallowSet.add(a);
64 | // expect(size).toBe(1);
65 |
66 | // shallowSet.delete(a);
67 | // expect(size).toBe(0);
68 | // });
69 |
70 | // test("should not observe when iterating", () => {
71 | // const shallowSet = shallowReactive(new Set());
72 | // const a = {};
73 | // shallowSet.add(a);
74 |
75 | // const spreadA = [...shallowSet][0];
76 | // expect(isReactive(spreadA)).toBe(false);
77 | // });
78 |
79 | // test("should not get reactive entry", () => {
80 | // const shallowMap = shallowReactive(new Map());
81 | // const a = {};
82 | // const key = "a";
83 |
84 | // shallowMap.set(key, a);
85 |
86 | // expect(isReactive(shallowMap.get(key))).toBe(false);
87 | // });
88 |
89 | // test("should not get reactive on foreach", () => {
90 | // const shallowSet = shallowReactive(new Set());
91 | // const a = {};
92 | // shallowSet.add(a);
93 |
94 | // shallowSet.forEach((x) => expect(isReactive(x)).toBe(false));
95 | // });
96 |
97 | // // #1210
98 | // test("onTrack on called on objectSpread", () => {
99 | // const onTrackFn = jest.fn();
100 | // const shallowSet = shallowReactive(new Set());
101 | // let a;
102 | // effect(
103 | // () => {
104 | // a = Array.from(shallowSet);
105 | // },
106 | // {
107 | // onTrack: onTrackFn,
108 | // }
109 | // );
110 |
111 | // expect(a).toMatchObject([]);
112 | // expect(onTrackFn).toHaveBeenCalled();
113 | // });
114 | // });
115 |
116 | // describe("array", () => {
117 | // test("should be reactive", () => {
118 | // const shallowArray = shallowReactive([]);
119 | // const a = {};
120 | // let size;
121 |
122 | // effect(() => {
123 | // size = shallowArray.length;
124 | // });
125 |
126 | // expect(size).toBe(0);
127 |
128 | // shallowArray.push(a);
129 | // expect(size).toBe(1);
130 |
131 | // shallowArray.pop();
132 | // expect(size).toBe(0);
133 | // });
134 | // test("should not observe when iterating", () => {
135 | // const shallowArray = shallowReactive]