├── .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 | --------------------------------------------------------------------------------