├── .gitignore
├── README.md
├── babel.config.js
├── example
└── helloworld
│ ├── App.js
│ ├── index.html
│ └── main.js
├── lib
├── mini-vue3.cjs.js
└── mini-vue3.esm.js
├── package.json
├── rollup.config.js
├── src
├── index.ts
├── reactivity
│ ├── __tests__
│ │ ├── computed.spec.ts
│ │ ├── effect.spec.ts
│ │ ├── reactive.spec.ts
│ │ ├── readonly.spec.ts
│ │ ├── ref.spec.ts
│ │ └── shallowReadonly.spec.ts
│ ├── baseHandles.ts
│ ├── computed.ts
│ ├── effect.ts
│ ├── reactive.ts
│ └── ref.ts
├── runtime-core
│ ├── component.ts
│ ├── createApp.ts
│ ├── h.ts
│ ├── index.ts
│ ├── renderer.ts
│ └── vnode.ts
└── shared
│ └── index.ts
└── tsconfig.json
/.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 | # mini-vue3
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', { targets: { node: 'current' } }],
4 | '@babel/preset-typescript'
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/example/helloworld/App.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../lib/mini-vue3.esm.js'
2 | export const App = {
3 | render() {
4 | console.log(this, 'this');
5 | return h(
6 | 'div',
7 | {
8 | class: [1, 2, 3, 4, 5]
9 | },
10 | [h('p', { class: 'red' }, '1234'), h('p', { class: 'blue' }, '4567')]
11 | )
12 | },
13 | setup() {
14 | return {
15 | msg: 'hello world'
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/example/helloworld/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/helloworld/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from '../../lib/mini-vue3.esm.js'
2 | import { App } from './App.js'
3 | const app = document.querySelector('#app')
4 | createApp(App).mount(app)
5 |
--------------------------------------------------------------------------------
/lib/mini-vue3.cjs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, '__esModule', { value: true });
4 |
5 | const isObject = (value) => {
6 | return value !== null && typeof value === 'object';
7 | };
8 |
9 | function createComponentInstance(vnode) {
10 | const component = {
11 | vnode,
12 | type: vnode.type
13 | };
14 | return component;
15 | }
16 | function setupComponent(instance) {
17 | // TODO initProps
18 | // TODO initSlots
19 | setupStatefulComponent(instance);
20 | }
21 | function setupStatefulComponent(instance) {
22 | const Component = instance.type;
23 | const { setup } = Component;
24 | if (setup) {
25 | const setupResult = setup();
26 | handleSetupResult(instance, setupResult);
27 | }
28 | }
29 | function handleSetupResult(instance, setupResult) {
30 | // TODO 实现 setupResult == function
31 | if (typeof setupResult === 'object') {
32 | instance.setupState = setupResult;
33 | }
34 | finishComponentState(instance);
35 | }
36 | function finishComponentState(instance) {
37 | const Component = instance.type;
38 | if (Component.render) {
39 | instance.render = Component.render;
40 | }
41 | }
42 |
43 | function render(vnode, container) {
44 | patch(vnode, container);
45 | }
46 | function patch(vnode, container) {
47 | if (isObject(vnode.type)) {
48 | // 处理 component
49 | processComponent(vnode, container);
50 | }
51 | else if (typeof vnode.type === 'string') {
52 | // 处理 element
53 | processElement(vnode, container);
54 | }
55 | }
56 | function processComponent(vnode, container) {
57 | mountComponent(vnode, container);
58 | }
59 | function mountComponent(vnode, container) {
60 | const instance = createComponentInstance(vnode);
61 | setupComponent(instance);
62 | setUpRenderEffect(instance, container);
63 | }
64 | function setUpRenderEffect(instance, container) {
65 | const subTree = instance.render();
66 | patch(subTree, container);
67 | }
68 | function processElement(vnode, container) {
69 | const el = document.createElement(vnode.type);
70 | // attrbuite
71 | const { props, children } = vnode;
72 | for (const key in props) {
73 | // vnode.props
74 | el.setAttribute(key, props[key]);
75 | }
76 | if (typeof children === 'string') {
77 | el.textContent = children;
78 | }
79 | else if (Array.isArray(children)) {
80 | renderChildren(children, el);
81 | }
82 | container.append(el);
83 | }
84 | function renderChildren(children, container) {
85 | children.forEach((vnode) => {
86 | patch(vnode, container);
87 | });
88 | }
89 |
90 | function createVNode(type, props, children) {
91 | const vnode = {
92 | type,
93 | props,
94 | children
95 | };
96 | return vnode;
97 | }
98 |
99 | function createApp(rootComponent) {
100 | return {
101 | mount(rootContainer) {
102 | // 先转换成 vnode
103 | const vnode = createVNode(rootComponent);
104 | render(vnode, rootContainer);
105 | }
106 | };
107 | }
108 |
109 | function h(type, props, children) {
110 | return createVNode(type, props, children);
111 | }
112 |
113 | exports.createApp = createApp;
114 | exports.h = h;
115 |
--------------------------------------------------------------------------------
/lib/mini-vue3.esm.js:
--------------------------------------------------------------------------------
1 | const isObject = (value) => {
2 | return value !== null && typeof value === 'object';
3 | };
4 |
5 | function createComponentInstance(vnode) {
6 | const component = {
7 | vnode,
8 | type: vnode.type
9 | };
10 | return component;
11 | }
12 | function setupComponent(instance) {
13 | // TODO initProps
14 | // TODO initSlots
15 | setupStatefulComponent(instance);
16 | }
17 | function setupStatefulComponent(instance) {
18 | const Component = instance.type;
19 | const { setup } = Component;
20 | if (setup) {
21 | const setupResult = setup();
22 | handleSetupResult(instance, setupResult);
23 | }
24 | }
25 | function handleSetupResult(instance, setupResult) {
26 | // TODO 实现 setupResult == function
27 | if (typeof setupResult === 'object') {
28 | instance.setupState = setupResult;
29 | }
30 | finishComponentState(instance);
31 | }
32 | function finishComponentState(instance) {
33 | const Component = instance.type;
34 | if (Component.render) {
35 | instance.render = Component.render;
36 | }
37 | }
38 |
39 | function render(vnode, container) {
40 | patch(vnode, container);
41 | }
42 | function patch(vnode, container) {
43 | if (isObject(vnode.type)) {
44 | // 处理 component
45 | processComponent(vnode, container);
46 | }
47 | else if (typeof vnode.type === 'string') {
48 | // 处理 element
49 | processElement(vnode, container);
50 | }
51 | }
52 | function processComponent(vnode, container) {
53 | mountComponent(vnode, container);
54 | }
55 | function mountComponent(vnode, container) {
56 | const instance = createComponentInstance(vnode);
57 | setupComponent(instance);
58 | setUpRenderEffect(instance, container);
59 | }
60 | function setUpRenderEffect(instance, container) {
61 | const subTree = instance.render();
62 | patch(subTree, container);
63 | }
64 | function processElement(vnode, container) {
65 | const el = document.createElement(vnode.type);
66 | // attrbuite
67 | const { props, children } = vnode;
68 | for (const key in props) {
69 | // vnode.props
70 | el.setAttribute(key, props[key]);
71 | }
72 | if (typeof children === 'string') {
73 | el.textContent = children;
74 | }
75 | else if (Array.isArray(children)) {
76 | renderChildren(children, el);
77 | }
78 | container.append(el);
79 | }
80 | function renderChildren(children, container) {
81 | children.forEach((vnode) => {
82 | patch(vnode, container);
83 | });
84 | }
85 |
86 | function createVNode(type, props, children) {
87 | const vnode = {
88 | type,
89 | props,
90 | children
91 | };
92 | return vnode;
93 | }
94 |
95 | function createApp(rootComponent) {
96 | return {
97 | mount(rootContainer) {
98 | // 先转换成 vnode
99 | const vnode = createVNode(rootComponent);
100 | render(vnode, rootContainer);
101 | }
102 | };
103 | }
104 |
105 | function h(type, props, children) {
106 | return createVNode(type, props, children);
107 | }
108 |
109 | export { createApp, h };
110 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-vue3",
3 | "version": "1.0.0",
4 | "main": "lib/mini-vue3.cjs.js",
5 | "module": "lib/mini-vue3.esm.js",
6 | "repository": "https://github.com/orzjose/mini-vue3.git",
7 | "author": "orzjose ",
8 | "scripts": {
9 | "test": "jest",
10 | "build": "rollup -c rollup.config.js"
11 | },
12 | "license": "MIT",
13 | "devDependencies": {
14 | "@babel/core": "^7.17.5",
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 | }
26 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript'
2 | import pkg from './package.json'
3 |
4 | export default {
5 | input: './src/index.ts',
6 | output: [
7 | { file: pkg.main, format: 'cjs' },
8 | { file: pkg.module, format: 'es' }
9 | ],
10 | plugins: [typescript()]
11 | }
12 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './runtime-core'
2 |
--------------------------------------------------------------------------------
/src/reactivity/__tests__/computed.spec.ts:
--------------------------------------------------------------------------------
1 | import { computed } from '../computed'
2 | import { reactive } from '../reactive'
3 |
4 | describe('computed', () => {
5 | it('happy path', () => {
6 | const user = reactive({
7 | age: 1
8 | })
9 | const age = computed(() => {
10 | return user.age
11 | })
12 | expect(age.value).toBe(1)
13 | })
14 |
15 | it('computed cache', () => {
16 | const user = reactive({ age: 1 })
17 | const fn = jest.fn(() => {
18 | return user.age
19 | })
20 | const age = computed(fn)
21 | // 如果不调用 age.value fn 不会被触发
22 | expect(fn).not.toBeCalled()
23 | // 调用 age.value 时 触发执行 fn
24 | expect(age.value).toBe(1);
25 | expect(fn).toHaveBeenCalledTimes(1)
26 |
27 | // age.value 值未进行修改的时候, 不触发 fn 回调
28 | age.value;
29 | expect(fn).toHaveBeenCalledTimes(1)
30 | expect(age.value).toBe(1);
31 |
32 | user.age = 2;
33 | expect(fn).toHaveBeenCalledTimes(1)
34 | expect(age.value).toBe(2)
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/src/reactivity/__tests__/effect.spec.ts:
--------------------------------------------------------------------------------
1 | import { effect, stop } from '../effect'
2 | import { reactive } from '../reactive'
3 |
4 | describe('effect', () => {
5 | it('happy path', () => {
6 | let age
7 | const observed = reactive({ age: 1 })
8 |
9 | effect(() => (age = observed.age))
10 |
11 | expect(age).toBe(1)
12 | observed.age = 2
13 | expect(age).toBe(2)
14 | })
15 |
16 | it('effect scheduler', () => {
17 | let age
18 | const observed = reactive({ age: 1 })
19 |
20 | const scheduler = jest.fn(() => {
21 | age = observed.age + 10
22 | })
23 |
24 | effect(
25 | () => {
26 | age = observed.age
27 | },
28 | { scheduler }
29 | )
30 |
31 | expect(scheduler).not.toHaveBeenCalled()
32 | expect(age).toBe(1)
33 |
34 | observed.age++
35 |
36 | expect(scheduler).toHaveBeenCalledTimes(1)
37 | expect(age).toBe(12)
38 |
39 | observed.age++
40 | expect(scheduler).toHaveBeenCalledTimes(2)
41 | expect(age).toBe(13)
42 | })
43 |
44 | it('effect return runner', () => {
45 | // effect 返回 fn 函数
46 | let age = 0
47 |
48 | const fn = jest.fn(() => {
49 | age += 1
50 | return 'result'
51 | })
52 | const runner = effect(fn)
53 |
54 | expect(age).toBe(1)
55 |
56 | const result = runner()
57 | expect(age).toBe(2)
58 | expect(result).toBe('result')
59 | })
60 |
61 | it('effect stop', () => {
62 | let dummy
63 | const obj = reactive({ prop: 1 })
64 | const runner = effect(() => {
65 | dummy = obj.prop
66 | })
67 |
68 | obj.prop = 2
69 | expect(dummy).toBe(2)
70 | // 执行 stop(runner) 将 effect 内部响应式对象移除
71 | stop(runner)
72 |
73 | obj.prop++;
74 | expect(dummy).toBe(2)
75 |
76 | runner()
77 | expect(dummy).toBe(3)
78 | })
79 |
80 | it('effect onStop', () => {
81 | // 当用户传入 onStop 时,调用 stop(runner) 会触发执行 onStop
82 | let dummy
83 | const obj = reactive({ prop: 1 })
84 | const onStop = jest.fn(() => {})
85 | const runner = effect(
86 | () => {
87 | dummy = obj.prop
88 | },
89 | { onStop }
90 | )
91 |
92 | expect(onStop).not.toHaveBeenCalled()
93 | stop(runner)
94 | expect(onStop).toBeCalledTimes(1)
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/src/reactivity/__tests__/reactive.spec.ts:
--------------------------------------------------------------------------------
1 | import { isProxy, isReactive, reactive } from '../reactive'
2 |
3 | describe('reactive', () => {
4 | it('happy path', () => {
5 | const origin = { age: 1 }
6 | const observed = reactive(origin)
7 | expect(origin).not.toBe(observed)
8 | expect(origin.age).toBe(observed.age)
9 | })
10 |
11 | it('reactive isReactive', () => {
12 | // isReactive
13 | const observed = reactive({ age: 1 })
14 | expect(isReactive(observed)).toBe(true)
15 | })
16 |
17 | it('reactive embed', () => {
18 | const observed = reactive({
19 | foo: 1,
20 | bar: { age: 10 }
21 | })
22 | expect(isReactive(observed.bar)).toBe(true)
23 | })
24 |
25 | it('reactive isProxy', () => {
26 | const observed = reactive({
27 | foo: 1,
28 | bar: { age: 10 }
29 | })
30 | expect(isProxy(observed)).toBe(true)
31 | expect(isProxy(observed.bar)).toBe(true)
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/src/reactivity/__tests__/readonly.spec.ts:
--------------------------------------------------------------------------------
1 | import { isProxy, isReadonly, readonly } from '../reactive'
2 |
3 | describe('readonly', () => {
4 | it('happy path', () => {
5 | // readonly 不能被set
6 | const origin = { foo: 1 }
7 | const wrapped = readonly(origin)
8 |
9 | wrapped.foo = 2
10 | expect(wrapped.foo).toBe(1)
11 | })
12 |
13 | it('warn then call set', () => {
14 | // readonly 对象调用set 时触发 console.warn 警告
15 | console.warn = jest.fn()
16 | const wrapped = readonly({ foo: 1 })
17 | wrapped.foo = 2
18 | expect(console.warn).toBeCalled()
19 | })
20 |
21 | it('isReadonly', () => {
22 | const origin = readonly({ age: 1 })
23 | expect(isReadonly(origin)).toBe(true)
24 | })
25 |
26 | it('embed readonly', () => {
27 | const origin = readonly({
28 | foo: 1,
29 | bar: { age: 10 }
30 | })
31 | expect(isReadonly(origin.bar)).toBe(true)
32 | })
33 |
34 | it('isProxy', () => {
35 | const origin = readonly({ age: 1 })
36 | expect(isProxy(origin)).toBe(true)
37 | })
38 | })
39 |
--------------------------------------------------------------------------------
/src/reactivity/__tests__/ref.spec.ts:
--------------------------------------------------------------------------------
1 | import { effect } from '../effect'
2 | import { reactive } from '../reactive'
3 | import { ref, isRef, unRef, proxyRefs } from '../ref'
4 |
5 | describe('ref', () => {
6 | it('happy path', () => {
7 | const a = ref(1)
8 | expect(a.value).toBe(1)
9 | })
10 | it('should be reactive', () => {
11 | const a = ref(1)
12 | let dummy
13 | const fn = jest.fn(() => (dummy = a.value))
14 | effect(fn)
15 |
16 | expect(fn).toHaveBeenCalledTimes(1)
17 | expect(dummy).toBe(1)
18 | a.value = 2
19 | expect(fn).toHaveBeenCalledTimes(2);
20 | expect(dummy).toBe(2)
21 | // same value should not trigger
22 | a.value = 2
23 | expect(fn).toHaveBeenCalledTimes(2);
24 | })
25 |
26 | it('should make nested properties reactive', () => {
27 | const a = ref({
28 | count: 1
29 | })
30 | let dummy
31 | effect(() => {
32 | dummy = a.value.count
33 | })
34 | expect(dummy).toBe(1)
35 | a.value.count = 2
36 | expect(dummy).toBe(2)
37 | })
38 |
39 | it('isRef', () => {
40 | const a = ref(1)
41 | const user = reactive({
42 | age: 1
43 | })
44 | expect(isRef(a)).toBe(true)
45 | expect(isRef(1)).toBe(false)
46 | expect(isRef(user)).toBe(false)
47 | })
48 |
49 | it('unRef', () => {
50 | const a = ref(1)
51 | expect(unRef(a)).toBe(1)
52 | expect(unRef(1)).toBe(1)
53 | })
54 |
55 | it('proxyRefs', () => {
56 | const user = {
57 | age: ref(10),
58 | name: 'xiaohong'
59 | }
60 |
61 | const proxy = proxyRefs(user)
62 |
63 | expect(proxy.age).toBe(10) // 自动解引用
64 | expect(proxy.name).toBe('xiaohong')
65 |
66 | proxy.age = 20
67 | expect(proxy.age).toBe(20) // 自动修改ref值
68 | expect(user.age.value).toBe(20)
69 | proxy.age = ref(30)
70 | expect(proxy.age).toBe(30) // 自动修改ref值
71 | expect(user.age.value).toBe(30)
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/src/reactivity/__tests__/shallowReadonly.spec.ts:
--------------------------------------------------------------------------------
1 | import { isProxy, isReadonly, shallowReadonly } from '../reactive'
2 |
3 | describe('shallowReadonly', () => {
4 | it('happy path', () => {
5 | const props = shallowReadonly({ n: { foo: 1 } })
6 | expect(isReadonly(props)).toBe(true)
7 | expect(isReadonly(props.n)).toBe(false)
8 | })
9 |
10 | it('warn then call set', () => {
11 | // readonly 对象调用set 时触发 console.warn 警告
12 | console.warn = jest.fn()
13 | const wrapped = shallowReadonly({ foo: 1 })
14 | wrapped.foo = 2
15 | expect(console.warn).toHaveBeenCalled()
16 | })
17 | it('isProxy', () => {
18 | const origin = shallowReadonly({ n: { foo: 1 } })
19 | expect(isProxy(origin)).toBe(true)
20 | expect(isProxy(origin.n)).toBe(false)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/src/reactivity/baseHandles.ts:
--------------------------------------------------------------------------------
1 | import { extend, isObject } from '../shared'
2 | import { track, trigger } from './effect'
3 | import { reactive, ReactiveFlags, readonly } from './reactive'
4 |
5 | const get = createGetter()
6 | const set = createSetter()
7 |
8 | const readonlyGet = createGetter(true)
9 | const shallowReadonlyGet = createGetter(true, true)
10 |
11 | function createGetter(isReadonly = false, shallow = false) {
12 | return function get(target, key) {
13 | if (key === ReactiveFlags.IS_REACTIVE) {
14 | return !isReadonly
15 | } else if (key === ReactiveFlags.IS_READONLY) {
16 | return isReadonly
17 | }
18 | const res = Reflect.get(target, key)
19 |
20 | if (shallow) {
21 | return res
22 | }
23 |
24 | if (isObject(res)) {
25 | return isReadonly ? readonly(res) : reactive(res)
26 | }
27 | !isReadonly && track(target, key)
28 | return res
29 | }
30 | }
31 |
32 | function createSetter() {
33 | return function set(target, key, value) {
34 | const res = Reflect.set(target, key, value)
35 | trigger(target, key)
36 | return res
37 | }
38 | }
39 |
40 | export const mutableHandlers = {
41 | get,
42 | set
43 | }
44 |
45 | export const readonlyHandlers = {
46 | get: readonlyGet,
47 | set(target, key, value) {
48 | console.warn(`key:${key} cannot to be set!`)
49 | return true
50 | }
51 | }
52 |
53 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, {
54 | get: shallowReadonlyGet
55 | })
56 |
--------------------------------------------------------------------------------
/src/reactivity/computed.ts:
--------------------------------------------------------------------------------
1 | import { ReactiveEffect } from './effect'
2 |
3 | class ComputedRefImpl {
4 | private _effect: ReactiveEffect
5 | private _dirty: any = true
6 | private _value: any
7 | constructor(getter) {
8 | this._effect = new ReactiveEffect(getter, () => {
9 | if (!this._dirty) {
10 | this._dirty = true
11 | }
12 | })
13 | }
14 | get value() {
15 | if (this._dirty) {
16 | this._dirty = false
17 | this._value = this._effect.run()
18 | }
19 | return this._value
20 | }
21 | }
22 |
23 | export function computed(getter) {
24 | return new ComputedRefImpl(getter)
25 | }
26 |
--------------------------------------------------------------------------------
/src/reactivity/effect.ts:
--------------------------------------------------------------------------------
1 | import { extend } from '../shared'
2 |
3 | let activeEffect
4 | let shouldTrack
5 | const targetMap = new WeakMap()
6 | export class ReactiveEffect {
7 | private _fn: any
8 | public onStop?: () => void
9 | active = true
10 | deps: [] = []
11 | constructor(fn, public scheduler?) {
12 | this._fn = fn
13 | this.scheduler = scheduler
14 | }
15 | run() {
16 | if (!this.active) {
17 | return this._fn()
18 | }
19 | shouldTrack = true
20 | activeEffect = this
21 | const result = this._fn()
22 | shouldTrack = false
23 | return result
24 | // return this._fn()
25 | }
26 | stop() {
27 | if (this.active) {
28 | cleanupEffect(this)
29 | this.onStop && this.onStop()
30 | this.active = false
31 | }
32 | }
33 | }
34 |
35 | function cleanupEffect(effect) {
36 | effect.deps.forEach((dep) => {
37 | dep.delete(effect)
38 | })
39 | effect.deps.length = 0
40 | }
41 |
42 | export function effect(fn, options:any = {}) {
43 | const _effect = new ReactiveEffect(fn, options.scheduler)
44 | extend(_effect, options)
45 | _effect.run()
46 |
47 | const runner: any = _effect.run.bind(_effect)
48 | runner.effect = _effect
49 | return runner
50 | }
51 |
52 | export function track(target, key) {
53 | if (!isTracking()) return
54 | let depMap = targetMap.get(target)
55 | if (!depMap) {
56 | depMap = new Map()
57 | targetMap.set(target, depMap)
58 | }
59 | let deps = depMap.get(key)
60 | if (!deps) {
61 | deps = new Set()
62 | depMap.set(key, deps)
63 | }
64 | trackEffects(deps)
65 | }
66 |
67 | export function trackEffects(deps) {
68 | if (deps.has(activeEffect)) return
69 | deps.add(activeEffect)
70 | activeEffect.deps.push(deps)
71 | }
72 |
73 | export function isTracking() {
74 | return shouldTrack && activeEffect !== undefined
75 | }
76 |
77 | export function trigger(target, key) {
78 | const depMap = targetMap.get(target)
79 | const deps = depMap.get(key)
80 |
81 | triggerEffects(deps)
82 | }
83 |
84 | export function triggerEffects(deps) {
85 | for (const effect of deps) {
86 | effect.scheduler ? effect.scheduler() : effect.run()
87 | }
88 | }
89 |
90 | export function stop(runner) {
91 | runner.effect.stop()
92 | }
93 |
--------------------------------------------------------------------------------
/src/reactivity/reactive.ts:
--------------------------------------------------------------------------------
1 | import {
2 | mutableHandlers,
3 | readonlyHandlers,
4 | shallowReadonlyHandlers
5 | } from './baseHandles'
6 |
7 | export enum ReactiveFlags {
8 | IS_REACTIVE = '__v_isReactive',
9 | IS_READONLY = '__v_isReadonly'
10 | }
11 |
12 | export function reactive(raw) {
13 | return createActiveObject(raw, mutableHandlers)
14 | }
15 |
16 | export function readonly(raw) {
17 | return createActiveObject(raw, readonlyHandlers)
18 | }
19 | export function shallowReadonly(raw) {
20 | return createActiveObject(raw, shallowReadonlyHandlers)
21 | }
22 |
23 | function createActiveObject(raw, baseHandles) {
24 | return new Proxy(raw, baseHandles)
25 | }
26 |
27 | export function isReactive(obj) {
28 | return !!obj[ReactiveFlags.IS_REACTIVE]
29 | }
30 | export function isReadonly(obj) {
31 | return !!obj[ReactiveFlags.IS_READONLY]
32 | }
33 | export function isProxy(obj) {
34 | return isReactive(obj) || isReadonly(obj)
35 | }
36 |
--------------------------------------------------------------------------------
/src/reactivity/ref.ts:
--------------------------------------------------------------------------------
1 | import { hasChanged, isObject } from '../shared'
2 | import { isTracking, trackEffects, triggerEffects } from './effect'
3 | import { reactive } from './reactive'
4 |
5 | class RefImpl {
6 | private _value: any
7 | private deps
8 | private rawValue
9 | public __v_isRef = true
10 | constructor(value) {
11 | this.rawValue = value
12 | this._value = convert(value)
13 | this.deps = new Set()
14 | }
15 | get value() {
16 | isTracking() && trackEffects(this.deps)
17 | return this._value
18 | }
19 |
20 | set value(newValue) {
21 | if (hasChanged(newValue, this.rawValue)) {
22 | this._value = newValue
23 | this.rawValue = convert(newValue)
24 | triggerEffects(this.deps)
25 | }
26 | }
27 | }
28 |
29 | function convert(value) {
30 | return isObject(value) ? reactive(value) : value
31 | }
32 | export function ref(value) {
33 | return new RefImpl(value)
34 | }
35 |
36 | export function isRef(ref) {
37 | return !!ref.__v_isRef
38 | }
39 |
40 | export function unRef(ref) {
41 | return isRef(ref) ? ref.value : ref
42 | }
43 |
44 | export function proxyRefs(objectWithRefs) {
45 | return new Proxy(objectWithRefs, {
46 | get(target, key) {
47 | // 如果是 ref 对象, 返回 ref.value
48 | // 不是 ref 对象,返回本身
49 | return unRef(Reflect.get(target, key))
50 | },
51 | set(target, key, value) {
52 | // 如果修改的对象是 ref 类型 && 传入的值不是 ref 类型, 将源对象 .value 替换
53 | if (isRef(target[key]) && !isRef(value)) {
54 | return Reflect.set(target[key], 'value', value)
55 | } else {
56 | // 如果修改的对象是 ref 类型 && 传入的对象也是ref 类型, 直接将对象替换掉
57 | return Reflect.set(target, key, value)
58 | }
59 | }
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/src/runtime-core/component.ts:
--------------------------------------------------------------------------------
1 | export function createComponentInstance(vnode) {
2 | const component = {
3 | vnode,
4 | type: vnode.type
5 | }
6 | return component
7 | }
8 |
9 | export function setupComponent(instance) {
10 | // TODO initProps
11 | // TODO initSlots
12 |
13 | setupStatefulComponent(instance)
14 | }
15 |
16 | function setupStatefulComponent(instance) {
17 | const Component = instance.type
18 |
19 | const { setup } = Component
20 | if (setup) {
21 | const setupResult = setup()
22 | handleSetupResult(instance, setupResult)
23 | }
24 | }
25 |
26 | function handleSetupResult(instance, setupResult) {
27 | // TODO 实现 setupResult == function
28 | if (typeof setupResult === 'object') {
29 | instance.setupState = setupResult
30 | }
31 | finishComponentState(instance)
32 | }
33 |
34 | function finishComponentState(instance) {
35 | const Component = instance.type
36 | if (Component.render) {
37 | instance.render = Component.render
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/runtime-core/createApp.ts:
--------------------------------------------------------------------------------
1 | import { render } from './renderer'
2 | import { createVNode } from './vnode'
3 |
4 | export function createApp(rootComponent) {
5 | return {
6 | mount(rootContainer) {
7 | // 先转换成 vnode
8 | const vnode = createVNode(rootComponent)
9 |
10 | render(vnode, rootContainer)
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/runtime-core/h.ts:
--------------------------------------------------------------------------------
1 | import { createVNode } from './vnode'
2 | export function h(type, props?, children?) {
3 | return createVNode(type, props, children)
4 | }
5 |
--------------------------------------------------------------------------------
/src/runtime-core/index.ts:
--------------------------------------------------------------------------------
1 | export { createApp } from './createApp'
2 | export { h } from './h'
3 |
--------------------------------------------------------------------------------
/src/runtime-core/renderer.ts:
--------------------------------------------------------------------------------
1 | import { isObject } from '../shared'
2 | import { createComponentInstance, setupComponent } from './component'
3 |
4 | export function render(vnode, container) {
5 | patch(vnode, container)
6 | }
7 |
8 | function patch(vnode, container) {
9 | if (isObject(vnode.type)) {
10 | // 处理 component
11 | processComponent(vnode, container)
12 | } else if (typeof vnode.type === 'string') {
13 | // 处理 element
14 | processElement(vnode, container)
15 | }
16 | }
17 |
18 | function processComponent(vnode, container) {
19 | mountComponent(vnode, container)
20 | }
21 |
22 | function mountComponent(vnode, container) {
23 | const instance = createComponentInstance(vnode)
24 |
25 | setupComponent(instance)
26 | setUpRenderEffect(instance, container)
27 | }
28 |
29 | function setUpRenderEffect(instance, container) {
30 | const subTree = instance.render()
31 |
32 | patch(subTree, container)
33 | }
34 | function processElement(vnode, container) {
35 | const el: HTMLElement = document.createElement(vnode.type)
36 | // attrbuite
37 | const { props, children } = vnode
38 | for (const key in props) {
39 | // vnode.props
40 | el.setAttribute(key, props[key])
41 | }
42 |
43 | if (typeof children === 'string') {
44 | el.textContent = children
45 | } else if (Array.isArray(children)) {
46 | renderChildren(children, el)
47 | }
48 | container.append(el)
49 | }
50 | function renderChildren(children, container) {
51 | children.forEach((vnode) => {
52 | patch(vnode, container)
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/src/runtime-core/vnode.ts:
--------------------------------------------------------------------------------
1 | export function createVNode(type, props?, children?) {
2 | const vnode = {
3 | type,
4 | props,
5 | children
6 | }
7 | return vnode
8 | }
9 |
--------------------------------------------------------------------------------
/src/shared/index.ts:
--------------------------------------------------------------------------------
1 | export const extend = Object.assign
2 | export const isObject = (value) => {
3 | return value !== null && typeof value === 'object'
4 | }
5 |
6 | export const hasChanged = (value, newValue) => {
7 | return !Object.is(value, newValue)
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Enable incremental compilation */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | "lib": ["DOM", "ES6"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 |
26 | /* Modules */
27 | "module": "esnext", /* Specify what module code is generated. */
28 | // "rootDir": "./", /* Specify the root folder within your source files. */
29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
34 | "types": ["jest"], /* Specify type package names to be included without being referenced in a source file. */
35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
36 | // "resolveJsonModule": true, /* Enable importing .json files */
37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
38 |
39 | /* JavaScript Support */
40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
43 |
44 | /* Emit */
45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
50 | // "outDir": "./", /* Specify an output folder for all emitted files. */
51 | // "removeComments": true, /* Disable emitting comments. */
52 | // "noEmit": true, /* Disable emitting files from a compilation. */
53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
61 | // "newLine": "crlf", /* Set the newline character for emitting files. */
62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
68 |
69 | /* Interop Constraints */
70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
75 |
76 | /* Type Checking */
77 | "strict": true, /* Enable all strict type-checking options. */
78 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
96 |
97 | /* Completeness */
98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
100 | }
101 | }
102 |
--------------------------------------------------------------------------------