├── .gitignore ├── README.md ├── babel.config.js ├── example ├── componentEmit │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── componentSlot │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── currentInstance │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js └── helloworld │ ├── App.js │ ├── index.html │ ├── main.js │ └── vue.runtime.esm-browser.js ├── jest.config.ts ├── lib ├── mini-vue3.cjs.js ├── mini-vue3.esm.js └── types │ └── mini-vue3.d.ts ├── 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 │ │ ├── shallowReactive.spec.ts │ │ └── shallowReadonly.spec.ts │ ├── index.ts │ └── src │ │ ├── baseHandlers.ts │ │ ├── computed.ts │ │ ├── dep.ts │ │ ├── effect.ts │ │ ├── enumeration.ts │ │ ├── reactive.ts │ │ └── ref.ts ├── runtime-core │ ├── index.ts │ └── src │ │ ├── component.ts │ │ ├── componentEmit.ts │ │ ├── componentProps.ts │ │ ├── componentPublicInstance.ts │ │ ├── componentSlots.ts │ │ ├── creatApp.ts │ │ ├── h.ts │ │ ├── helpers │ │ └── renderSlots.ts │ │ ├── renderer.ts │ │ └── vnode.ts └── shared │ ├── __tests__ │ ├── base.spec.ts │ └── isType.spec.ts │ ├── index.ts │ └── src │ ├── ShapFlags.ts │ ├── base.ts │ └── isType.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-vue 2 | 3 | In order to learn vue source code 4 | 5 | ## reactivity 6 | 7 | 1. reactive 8 | 9 | - [x] reactive 10 | - [x] readonly 11 | - [x] isReactive 12 | - [x] isReadonly 13 | - [x] isProxy 14 | - [x] shallowReactive 15 | - [x] shallowReadonly 16 | - [ ] markRaw 17 | - [x] toRaw 18 | 19 | 2. effect 20 | 21 | - [x] effect 22 | - [x] stop 23 | - [x] trigger 24 | - [x] track 25 | - [ ] enableTracking 26 | - [ ] pauseTracking 27 | - [ ] resetTracking 28 | 29 | 3. refs 30 | 31 | - [x] ref 32 | - [x] shallowRef 33 | - [x] isRef 34 | - [x] toRef 35 | - [x] toRefs 36 | - [x] unref 37 | - [x] proxyRefs 38 | - [ ] customRef 39 | - [ ] triggerRef 40 | 41 | 4. computed 42 | 43 | - [x] computed 44 | 45 | 5. effectScope 46 | 47 | - [ ] effectScope 48 | - [ ] getCurrentScope 49 | - [ ] onScopeDispose 50 | 51 | ## runtime-core 52 | 53 | ## runtime-dom 54 | 55 | ## compiler-core 56 | 57 | ## compiler-dom 58 | 59 | ## compiler-sfc 60 | 61 | ## compiler-ssr 62 | 63 | ## server-renderer 64 | 65 | ## shared 66 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /example/componentEmit/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue3.esm.js' 2 | import { Foo } from './Foo.js' 3 | 4 | export const App = { 5 | name: 'App', 6 | render() { 7 | // emit 8 | return h('div', {}, [ 9 | h('div', {}, 'App'), 10 | h(Foo, { 11 | onAdd(a, b) { 12 | console.log('onAdd', a, b) 13 | }, 14 | onAddFoo() { 15 | console.log('onAddFoo') 16 | } 17 | }) 18 | ]) 19 | }, 20 | 21 | setup() { 22 | return {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/componentEmit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue3.esm.js' 2 | 3 | export const Foo = { 4 | setup(props, { emit }) { 5 | const emitAdd = () => { 6 | console.log('emit add') 7 | emit('add', 1, 2) 8 | emit('add-foo') 9 | } 10 | 11 | return { 12 | emitAdd 13 | } 14 | }, 15 | render() { 16 | const btn = h( 17 | 'button', 18 | { 19 | onClick: this.emitAdd 20 | }, 21 | 'emitAdd' 22 | ) 23 | 24 | const foo = h('p', {}, 'foo') 25 | return h('div', {}, [foo, btn]) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /example/componentSlot/App.js: -------------------------------------------------------------------------------- 1 | import { h, createTextVNode } from '../../lib/mini-vue3.esm.js' 2 | import { Foo } from './Foo.js' 3 | 4 | export const App = { 5 | name: 'App', 6 | render() { 7 | const app = h('div', {}, 'App') 8 | // object key 9 | const foo = h( 10 | Foo, 11 | {}, 12 | { 13 | header: ({ age }) => [ 14 | h('p', {}, 'header' + age), 15 | createTextVNode('你好呀') 16 | ], 17 | footer: () => h('p', {}, 'footer'), 18 | default: () => h('p', {}, 'footer2') 19 | } 20 | ) 21 | // 数组 vnode 22 | // const foo = h(Foo, {}, h("p", {}, "123")); 23 | return h('div', {}, [app, foo]) 24 | }, 25 | 26 | setup() { 27 | return {} 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/componentSlot/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from '../../lib/mini-vue3.esm.js' 2 | 3 | export const Foo = { 4 | setup() { 5 | return {} 6 | }, 7 | render() { 8 | const foo = h('p', {}, 'foo') 9 | 10 | // Foo .vnode. children 11 | console.log(this.$slots) 12 | // children -> vnode 13 | // 14 | // renderSlots 15 | // 具名插槽 16 | // 1. 获取到要渲染的元素 1 17 | // 2. 要获取到渲染的位置 18 | // 作用域插槽 19 | const age = 18 20 | return h('div', {}, [ 21 | renderSlots(this.$slots, 'header', { 22 | age 23 | }), 24 | foo, 25 | renderSlots(this.$slots, 'footer'), 26 | renderSlots(this.$slots, 'default') 27 | ]) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/componentSlot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/componentSlot/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /example/currentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from '../../lib/mini-vue3.esm.js' 2 | import { Foo } from './Foo.js' 3 | 4 | export const App = { 5 | name: 'App', 6 | render() { 7 | return h('div', {}, [h('p', {}, 'currentInstance demo'), h(Foo)]) 8 | }, 9 | 10 | setup() { 11 | const instance = getCurrentInstance() 12 | console.log('App:', instance) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/currentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from '../../lib/mini-vue3.esm.js' 2 | 3 | export const Foo = { 4 | name: 'Foo', 5 | setup() { 6 | const instance = getCurrentInstance() 7 | console.log('Foo:', instance) 8 | return {} 9 | }, 10 | render() { 11 | return h('div', {}, 'foo') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/currentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/currentInstance/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /example/helloworld/App.js: -------------------------------------------------------------------------------- 1 | // import { h } from './vue.runtime.esm-browser.js' 2 | import { h } from '../../lib/mini-vue3.esm.js' 3 | window.self = null 4 | const Foo = { 5 | setup(props) { 6 | console.log(props) 7 | 8 | props.count++ 9 | console.log(props) 10 | }, 11 | render() { 12 | return h('span', {}, `count: ${this.count}`) 13 | } 14 | } 15 | export const App = { 16 | setup() { 17 | return { 18 | msg: 'mini-vue' 19 | } 20 | }, 21 | render() { 22 | window.self = this 23 | return h( 24 | 'div', 25 | { 26 | class: ['bold'], 27 | id: 'root', 28 | onClick: () => { 29 | console.log('click') 30 | } 31 | }, 32 | [ 33 | h('span', { class: 'red' }, 'hi '), 34 | h('span', { class: 'blue' }, `"${this.msg}"`), 35 | h(Foo, { count: 999 }) 36 | ] 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/helloworld/main.js: -------------------------------------------------------------------------------- 1 | // import { createApp } from './vue.runtime.esm-browser.js' 2 | import { createApp } from '../../lib/mini-vue3.esm.js' 3 | import { App } from './App.js' 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "C:\\Users\\whylost\\AppData\\Local\\Temp\\jest", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | collectCoverageFrom: ['src/*/src/**/*.ts'], 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: 'coverage', 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "\\\\node_modules\\\\" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | // coverageProvider: 'v8', 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | coverageReporters: ['html', 'lcov', 'text'], 38 | 39 | // An object that configures minimum threshold enforcement for coverage results 40 | // coverageThreshold: undefined, 41 | 42 | // A path to a custom dependency extractor 43 | // dependencyExtractor: undefined, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files using an array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: undefined, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: undefined, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | // globals: {}, 59 | 60 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 61 | // maxWorkers: "50%", 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], 70 | 71 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 72 | // moduleNameMapper: {}, 73 | 74 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 75 | // modulePathIgnorePatterns: [], 76 | 77 | // Activates notifications for test results 78 | // notify: false, 79 | 80 | // An enum that specifies notification mode. Requires { notify: true } 81 | // notifyMode: "failure-change", 82 | 83 | // A preset that is used as a base for Jest's configuration 84 | // preset: undefined, 85 | 86 | // Run tests from one or more projects 87 | // projects: undefined, 88 | 89 | // Use this configuration option to add custom reporters to Jest 90 | // reporters: undefined, 91 | 92 | // Automatically reset mock state between every test 93 | // resetMocks: false, 94 | 95 | // Reset the module registry before running each individual test 96 | // resetModules: false, 97 | 98 | // A path to a custom resolver 99 | // resolver: undefined, 100 | 101 | // Automatically restore mock state between every test 102 | // restoreMocks: false, 103 | 104 | // The root directory that Jest should scan for tests and modules within 105 | rootDir: __dirname, 106 | 107 | // A list of paths to directories that Jest should use to search for files in 108 | // roots: [ 109 | // "" 110 | // ], 111 | 112 | // Allows you to use a custom runner instead of Jest's default test runner 113 | // runner: "jest-runner", 114 | 115 | // The paths to modules that run some code to configure or set up the testing environment before each test 116 | // setupFiles: [], 117 | 118 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 119 | // setupFilesAfterEnv: [], 120 | 121 | // The number of seconds after which a test is considered as slow and reported as such in the results. 122 | // slowTestThreshold: 5, 123 | 124 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 125 | // snapshotSerializers: [], 126 | 127 | // The test environment that will be used for testing 128 | testEnvironment: 'jsdom' 129 | 130 | // Options that will be passed to the testEnvironment 131 | // testEnvironmentOptions: {}, 132 | 133 | // Adds a location field to test results 134 | // testLocationInResults: false, 135 | 136 | // The glob patterns Jest uses to detect test files 137 | // testMatch: [ 138 | // "**/__tests__/**/*.[jt]s?(x)", 139 | // "**/?(*.)+(spec|test).[tj]s?(x)" 140 | // ], 141 | 142 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 143 | // testPathIgnorePatterns: [ 144 | // "\\\\node_modules\\\\" 145 | // ], 146 | 147 | // The regexp pattern or array of patterns that Jest uses to detect test files 148 | // testRegex: [], 149 | 150 | // This option allows the use of a custom results processor 151 | // testResultsProcessor: undefined, 152 | 153 | // This option allows use of a custom test runner 154 | // testRunner: "jest-circus/runner", 155 | 156 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 157 | // testURL: "http://localhost", 158 | 159 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 160 | // timers: "real", 161 | 162 | // A map from regular expressions to paths to transformers 163 | // transform: undefined, 164 | 165 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 166 | // transformIgnorePatterns: [ 167 | // "\\\\node_modules\\\\", 168 | // "\\.pnp\\.[^\\\\]+$" 169 | // ], 170 | 171 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 172 | // unmockedModulePathPatterns: undefined, 173 | 174 | // Indicates whether each individual test should be reported during the run 175 | // verbose: undefined, 176 | 177 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 178 | // watchPathIgnorePatterns: [], 179 | 180 | // Whether to use watchman for file crawling 181 | // watchman: true, 182 | } 183 | -------------------------------------------------------------------------------- /lib/mini-vue3.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | /** 对象属性合并 */ 6 | const extend = Object.assign; 7 | /** 对象自身属性中是否具有指定的属性 */ 8 | const hasOwnProperty = Object.prototype.hasOwnProperty; 9 | const hasOwn = (val, key) => hasOwnProperty.call(val, key); 10 | /** 空对象 */ 11 | const EMPTY_OBJ = {}; 12 | /** 是on开头的事件 */ 13 | const isOn = (key) => /^on[A-Z]/.test(key); 14 | /* -n 字符串驼峰化 */ 15 | const camelize = (str) => str.replace(/-(\w)/g, (_, c) => { 16 | return c ? c.toUpperCase() : ''; 17 | }); 18 | /* 首字母大写 */ 19 | const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); 20 | /* 变成事件名称 */ 21 | const toHandlerKey = (str) => (str ? 'on' + capitalize(str) : ''); 22 | 23 | /** 判断是不是对象 */ 24 | const isObject = (val) => !!val && typeof val === 'object'; 25 | /** 判断是不是String */ 26 | const isString = (val) => typeof val === 'string'; 27 | /** 判断是不是函数 */ 28 | const isFunction = (val) => typeof val === 'function'; 29 | 30 | // REVIEW 这一段依赖收集的逻辑关系 需要多复习 31 | const targetMap = new WeakMap(); 32 | /** 33 | * 触发依赖执行(从reactive的target->key->dep 然后收集依赖) 34 | */ 35 | function trigger(target, key) { 36 | let depsMap = targetMap.get(target); 37 | // 没有收集过依赖(tracked),直接跳过trigger 38 | if (!depsMap) 39 | return; 40 | let dep = depsMap.get(key); 41 | dep && triggerEffects(dep); 42 | } 43 | /** 44 | * 触发依赖执行(直接触发dep中的依赖) 45 | */ 46 | function triggerEffects(dep) { 47 | dep.forEach((effect) => { 48 | if (effect.scheduler) { 49 | effect.scheduler(); 50 | } 51 | else { 52 | effect.run(); 53 | } 54 | }); 55 | } 56 | 57 | // 储存reactive和其原始对象对应关系的 全局WeakMap 58 | const reactiveMap = new WeakMap(); 59 | const shallowReactiveMap = new WeakMap(); 60 | const readonlyMap = new WeakMap(); 61 | const shallowReadonlyMap = new WeakMap(); 62 | function reactive(raw) { 63 | return createReactiveObject(raw, reactiveHandlers, reactiveMap); 64 | } 65 | /** 66 | * 返回原始对象的只读代理 67 | * @param raw 68 | */ 69 | function readonly(raw) { 70 | return createReactiveObject(raw, readonlyHandlers, readonlyMap); 71 | } 72 | function shallowReadonly(raw) { 73 | return createReactiveObject(raw, shallowReadonlyHandlers, shallowReadonlyMap); 74 | } 75 | /** 76 | * 创建反应对象 77 | * @param target 原始对象 78 | * @param baseHandlers Proxy处理函数的对象 79 | */ 80 | function createReactiveObject(target, baseHandlers, proxyMap) { 81 | // target 已经具有相应的 Proxy 82 | const existingProxy = proxyMap.get(target); 83 | if (existingProxy) { 84 | return existingProxy; 85 | } 86 | const proxy = new Proxy(target, baseHandlers); 87 | proxyMap.set(target, proxy); 88 | return proxy; 89 | } 90 | 91 | const reactvieGet = createGetter(); 92 | const reactvieSet = createSetter(); 93 | /** reactive 拦截方法 */ 94 | const reactiveHandlers = { 95 | get: reactvieGet, 96 | set: reactvieSet 97 | }; 98 | const readonlyGet = createGetter(true); 99 | const readonlySet = readonlySetter; 100 | /** readonly 拦截方法 */ 101 | const readonlyHandlers = { 102 | get: readonlyGet, 103 | set: readonlySet 104 | }; 105 | const shallowReadonlyGet = createGetter(true, true); 106 | const shallowReadonlySet = readonlySetter; 107 | /** shallowReadonly 拦截方法 */ 108 | const shallowReadonlyHandlers = { 109 | get: shallowReadonlyGet, 110 | set: shallowReadonlySet 111 | }; 112 | /** 113 | * 创建proxy对象的get方法 114 | * @param isReadonly 115 | */ 116 | function createGetter(isReadonly = false, shallow = false) { 117 | return (target, key, receiver) => { 118 | // isReactive和isReadonly 检测 不是readonly的就是reactive 119 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 120 | return !isReadonly; 121 | } 122 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 123 | return isReadonly; 124 | } 125 | else if (key === "__v_raw" /* RAW */ && 126 | receiver === 127 | (isReadonly 128 | ? shallow 129 | ? shallowReadonlyMap 130 | : readonlyMap 131 | : shallow 132 | ? shallowReactiveMap 133 | : reactiveMap).get(target)) { 134 | return target; 135 | } 136 | const res = Reflect.get(target, key, receiver); 137 | // 不执行嵌套对象的深度readonly转换 138 | if (shallow) { 139 | return res; 140 | } 141 | // 实现 reactive和readonly 的嵌套对象转换 142 | if (isObject(res)) { 143 | return isReadonly ? readonly(res) : reactive(res); 144 | } 145 | return res; 146 | }; 147 | } 148 | /** 149 | * 创建非readoly Proxy对象的 set 方法 150 | */ 151 | function createSetter() { 152 | // reactive创建的代理对象 触发 set 操作时 触发依赖执行 153 | return (target, key, value, receiver) => { 154 | const res = Reflect.set(target, key, value, receiver); 155 | // 触发依赖执行 156 | trigger(target, key); 157 | return res; 158 | }; 159 | } 160 | /** 161 | * readoly Proxy对象的 set 方法 162 | */ 163 | function readonlySetter(target, key) { 164 | // readonly创建的代理对象, 触发 set 操作时控制台进行警告提示 165 | console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target); 166 | return true; 167 | } 168 | 169 | function emit(instance, eventName, ...args) { 170 | const { props } = instance; 171 | const handlerName = toHandlerKey(camelize(eventName)); 172 | const handler = props[handlerName]; 173 | handler && handler(...args); 174 | } 175 | 176 | function initProps(instance, rawProps) { 177 | instance.props = rawProps || {}; 178 | } 179 | 180 | const publicPropertiesMap = extend(Object.create(null), { 181 | $el: (i) => i.vnode.el, 182 | $slots: (i) => i.slots 183 | }); 184 | /** publicInstanceProxyHandlers */ 185 | const publicInstanceProxyHandlers = { 186 | get({ _: instance }, key) { 187 | const { setupState, props } = instance; 188 | if (hasOwn(setupState, key)) { 189 | return setupState[key]; 190 | } 191 | else if (hasOwn(props, key)) { 192 | return props[key]; 193 | } 194 | // 如果 key 存在于公共Api中 则返回对应的公共Api 195 | else if (hasOwn(publicPropertiesMap, key)) { 196 | return publicPropertiesMap[key](instance); 197 | } 198 | } 199 | }; 200 | 201 | function initSlots(instance, children) { 202 | const { vnode } = instance; 203 | if (vnode.shapeFlag & 32 /* SLOT_CHILDREN */) { 204 | normalizeObjectSlots(children, instance.slots); 205 | } 206 | } 207 | function normalizeObjectSlots(children, slots) { 208 | for (const key in children) { 209 | const value = children[key]; 210 | slots[key] = (props) => normalizeSlotValue(value(props)); 211 | } 212 | } 213 | function normalizeSlotValue(value) { 214 | return Array.isArray(value) ? value : [value]; 215 | } 216 | 217 | /** 创建组件实例 */ 218 | function createComponentInstance(vnode) { 219 | const type = vnode.type; 220 | const instance = { 221 | vnode, 222 | props: EMPTY_OBJ, 223 | emit: () => { }, 224 | slots: EMPTY_OBJ, 225 | type, 226 | render: null, 227 | subTree: null, 228 | setupState: EMPTY_OBJ, 229 | proxy: null, 230 | ctx: EMPTY_OBJ 231 | }; 232 | instance.emit = emit.bind(null, instance); 233 | instance.ctx = { _: instance }; 234 | return instance; 235 | } 236 | /** 初始化组件 */ 237 | function setupComponent(instance) { 238 | const { props, children } = instance.vnode; 239 | initProps(instance, props); 240 | initSlots(instance, children); 241 | setupStatefulComponent(instance); 242 | } 243 | /** 初始化 有状态的(非函数式)组件 */ 244 | function setupStatefulComponent(instance) { 245 | // 此处 type = component VNode 246 | const { type: { setup }, props, ctx } = instance; 247 | // context 248 | instance.proxy = new Proxy(ctx, publicInstanceProxyHandlers); 249 | if (setup) { 250 | // 执行 setup 前 将currentInstance指向当前组件的instance 251 | currentInstance = instance; 252 | const setupResult = setup(shallowReadonly(props), instance); 253 | // 当前组件的 setup 执行完成后 清空将currentInstance 254 | currentInstance = null; 255 | handleSetupResult(instance, setupResult); 256 | } 257 | } 258 | /** 处理组件内 setup函数 的返回值 */ 259 | function handleSetupResult(instance, setupResult) { 260 | // TODO setup 返回的是函数时 261 | // 如果 setup 返回的是对象时 将setupResult赋值给组件实例对象上的setupState 262 | if (isObject(setupResult)) { 263 | instance.setupState = setupResult; 264 | } 265 | finishComponentSetup(instance); 266 | } 267 | /** 组件初始化 完成 */ 268 | function finishComponentSetup(instance) { 269 | const { type: component } = instance; 270 | instance.render = component.render; 271 | } 272 | // 组件实例全局变量 273 | let currentInstance = null; 274 | // 在setup里获取当前组件的实例 275 | const getCurrentInstance = () => currentInstance; 276 | 277 | const Fragment = Symbol('Fragment'); 278 | const Text = Symbol('Text'); 279 | /** 创建虚拟节点 */ 280 | const createVNode = (type, props = null, children = null, shapeFlag = initShapFlag(type)) => { 281 | const vnode = { 282 | type, 283 | props, 284 | children, 285 | el: null, 286 | shapeFlag 287 | }; 288 | /* 子节点是文本时 标为 TEXT_CHILDREN 否则视为 ARRAY_CHILDREN */ 289 | vnode.shapeFlag |= isString(children) 290 | ? 8 /* TEXT_CHILDREN */ 291 | : 16 /* ARRAY_CHILDREN */; 292 | if (vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */) { 293 | if (typeof children === 'object') { 294 | vnode.shapeFlag |= 32 /* SLOT_CHILDREN */; 295 | } 296 | } 297 | return vnode; 298 | }; 299 | /** 初始化VNode的默认shapeFlags */ 300 | const initShapFlag = (type) => isString(type) 301 | ? 1 /* ELEMENT */ 302 | : isFunction(type) 303 | ? 2 /* FUNCTIONAL_COMPONENT */ 304 | : 4 /* STATEFUL_COMPONENT */; 305 | const createTextVNode = (text) => { 306 | return createVNode(Text, null, text); 307 | }; 308 | 309 | /** render */ 310 | function render(vnode, container) { 311 | // 调用 patch 方法 312 | patch(vnode, container); 313 | } 314 | /** 处理各种vnode */ 315 | function patch(vnode, container) { 316 | const { type, shapeFlag } = vnode; 317 | switch (type) { 318 | case Fragment: 319 | // 处理Fragment类型的vnode 320 | processFragment(vnode, container); 321 | break; 322 | case Text: 323 | // 处理Fragment类型的vnode 324 | processText(vnode, container); 325 | break; 326 | default: 327 | if (shapeFlag & 1 /* ELEMENT */) { 328 | // 处理Element类型的vnode 329 | processElement(vnode, container); 330 | } 331 | else if (shapeFlag & 4 /* STATEFUL_COMPONENT */) { 332 | // 处理有状态的组件vnode 333 | processComponent(vnode, container); 334 | } 335 | break; 336 | } 337 | } 338 | /** 处理Fragment */ 339 | const processText = (vnode, container) => { 340 | const { children } = vnode; 341 | const textNode = (vnode.el = document.createTextNode(children)); 342 | container.append(textNode); 343 | }; 344 | /** 处理Fragment */ 345 | const processFragment = (vnode, container) => mountChildren(vnode.children, container); 346 | /** 处理Element */ 347 | const processElement = (vnode, container) => mountElement(vnode, container); 348 | /** 处理组件 */ 349 | const processComponent = (vnode, container) => mountComponent(vnode, container); 350 | /** 挂载Element */ 351 | const mountElement = (vnode, container) => { 352 | // 根据tagName创建HTML节点 353 | const el = (vnode.el = document.createElement(vnode.type)); 354 | const { children, props } = vnode; 355 | // 处理 props 通过循环给DOM节点设置属性 356 | for (const key in props) { 357 | // 挂载事件 358 | if (isOn(key) && isFunction(props[key])) { 359 | el.addEventListener(key.slice(2).toLowerCase(), props[key]); 360 | } 361 | else { 362 | el.setAttribute(key, props[key]); 363 | } 364 | } 365 | // 处理子节点 子节点可能是 TEXT_CHILDREN 或 ARRAY_CHILDREN 366 | if (vnode.shapeFlag & 8 /* TEXT_CHILDREN */) { 367 | // 获取el节点及其后代的文本内容并把子节点替换为文本 368 | el.textContent = children; 369 | } 370 | else if (vnode.shapeFlag & 16 /* ARRAY_CHILDREN */) { 371 | mountChildren(children, el); 372 | } 373 | // 将el节点插入到容器节点的子末位 374 | container.append(el); 375 | }; 376 | /** 挂载子节点 */ 377 | const mountChildren = (vnodeArray, container) => vnodeArray.forEach((vnode) => patch(vnode, container)); 378 | /** 挂载组件 */ 379 | const mountComponent = (initialVNode, container) => { 380 | const instance = createComponentInstance(initialVNode); 381 | setupComponent(instance); 382 | setupRenderEffect(instance, container); 383 | }; 384 | /** setupRenderEffect */ 385 | function setupRenderEffect(instance, container) { 386 | const subTree = instance.render.call(instance.proxy); 387 | patch(subTree, container); 388 | instance.vnode.el = subTree.el; 389 | } 390 | 391 | /** 创建一个提供应用上下文的应用实例 */ 392 | function createApp(rootComponent) { 393 | return { 394 | mount(rootContainer) { 395 | // 先把组件转化成虚拟节点 component -> VNode 396 | // 所有逻辑操作都会基于 VNode 做处理 397 | const vnode = createVNode(rootComponent); 398 | render(vnode, rootContainer); 399 | } 400 | }; 401 | } 402 | 403 | /** 返回一个`虚拟节点` */ 404 | function h(type, props, children) { 405 | return createVNode(type, props, children); 406 | } 407 | 408 | function renderSlots(slots, slotName, props) { 409 | const slot = slots === null || slots === void 0 ? void 0 : slots[slotName]; 410 | if (slot) { 411 | if (typeof slot === 'function') { 412 | return createVNode(Fragment, {}, slot(props)); 413 | } 414 | } 415 | } 416 | 417 | exports.createApp = createApp; 418 | exports.createTextVNode = createTextVNode; 419 | exports.getCurrentInstance = getCurrentInstance; 420 | exports.h = h; 421 | exports.renderSlots = renderSlots; 422 | -------------------------------------------------------------------------------- /lib/mini-vue3.esm.js: -------------------------------------------------------------------------------- 1 | /** 对象属性合并 */ 2 | const extend = Object.assign; 3 | /** 对象自身属性中是否具有指定的属性 */ 4 | const hasOwnProperty = Object.prototype.hasOwnProperty; 5 | const hasOwn = (val, key) => hasOwnProperty.call(val, key); 6 | /** 空对象 */ 7 | const EMPTY_OBJ = {}; 8 | /** 是on开头的事件 */ 9 | const isOn = (key) => /^on[A-Z]/.test(key); 10 | /* -n 字符串驼峰化 */ 11 | const camelize = (str) => str.replace(/-(\w)/g, (_, c) => { 12 | return c ? c.toUpperCase() : ''; 13 | }); 14 | /* 首字母大写 */ 15 | const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); 16 | /* 变成事件名称 */ 17 | const toHandlerKey = (str) => (str ? 'on' + capitalize(str) : ''); 18 | 19 | /** 判断是不是对象 */ 20 | const isObject = (val) => !!val && typeof val === 'object'; 21 | /** 判断是不是String */ 22 | const isString = (val) => typeof val === 'string'; 23 | /** 判断是不是函数 */ 24 | const isFunction = (val) => typeof val === 'function'; 25 | 26 | // REVIEW 这一段依赖收集的逻辑关系 需要多复习 27 | const targetMap = new WeakMap(); 28 | /** 29 | * 触发依赖执行(从reactive的target->key->dep 然后收集依赖) 30 | */ 31 | function trigger(target, key) { 32 | let depsMap = targetMap.get(target); 33 | // 没有收集过依赖(tracked),直接跳过trigger 34 | if (!depsMap) 35 | return; 36 | let dep = depsMap.get(key); 37 | dep && triggerEffects(dep); 38 | } 39 | /** 40 | * 触发依赖执行(直接触发dep中的依赖) 41 | */ 42 | function triggerEffects(dep) { 43 | dep.forEach((effect) => { 44 | if (effect.scheduler) { 45 | effect.scheduler(); 46 | } 47 | else { 48 | effect.run(); 49 | } 50 | }); 51 | } 52 | 53 | // 储存reactive和其原始对象对应关系的 全局WeakMap 54 | const reactiveMap = new WeakMap(); 55 | const shallowReactiveMap = new WeakMap(); 56 | const readonlyMap = new WeakMap(); 57 | const shallowReadonlyMap = new WeakMap(); 58 | function reactive(raw) { 59 | return createReactiveObject(raw, reactiveHandlers, reactiveMap); 60 | } 61 | /** 62 | * 返回原始对象的只读代理 63 | * @param raw 64 | */ 65 | function readonly(raw) { 66 | return createReactiveObject(raw, readonlyHandlers, readonlyMap); 67 | } 68 | function shallowReadonly(raw) { 69 | return createReactiveObject(raw, shallowReadonlyHandlers, shallowReadonlyMap); 70 | } 71 | /** 72 | * 创建反应对象 73 | * @param target 原始对象 74 | * @param baseHandlers Proxy处理函数的对象 75 | */ 76 | function createReactiveObject(target, baseHandlers, proxyMap) { 77 | // target 已经具有相应的 Proxy 78 | const existingProxy = proxyMap.get(target); 79 | if (existingProxy) { 80 | return existingProxy; 81 | } 82 | const proxy = new Proxy(target, baseHandlers); 83 | proxyMap.set(target, proxy); 84 | return proxy; 85 | } 86 | 87 | const reactvieGet = createGetter(); 88 | const reactvieSet = createSetter(); 89 | /** reactive 拦截方法 */ 90 | const reactiveHandlers = { 91 | get: reactvieGet, 92 | set: reactvieSet 93 | }; 94 | const readonlyGet = createGetter(true); 95 | const readonlySet = readonlySetter; 96 | /** readonly 拦截方法 */ 97 | const readonlyHandlers = { 98 | get: readonlyGet, 99 | set: readonlySet 100 | }; 101 | const shallowReadonlyGet = createGetter(true, true); 102 | const shallowReadonlySet = readonlySetter; 103 | /** shallowReadonly 拦截方法 */ 104 | const shallowReadonlyHandlers = { 105 | get: shallowReadonlyGet, 106 | set: shallowReadonlySet 107 | }; 108 | /** 109 | * 创建proxy对象的get方法 110 | * @param isReadonly 111 | */ 112 | function createGetter(isReadonly = false, shallow = false) { 113 | return (target, key, receiver) => { 114 | // isReactive和isReadonly 检测 不是readonly的就是reactive 115 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 116 | return !isReadonly; 117 | } 118 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 119 | return isReadonly; 120 | } 121 | else if (key === "__v_raw" /* RAW */ && 122 | receiver === 123 | (isReadonly 124 | ? shallow 125 | ? shallowReadonlyMap 126 | : readonlyMap 127 | : shallow 128 | ? shallowReactiveMap 129 | : reactiveMap).get(target)) { 130 | return target; 131 | } 132 | const res = Reflect.get(target, key, receiver); 133 | // 不执行嵌套对象的深度readonly转换 134 | if (shallow) { 135 | return res; 136 | } 137 | // 实现 reactive和readonly 的嵌套对象转换 138 | if (isObject(res)) { 139 | return isReadonly ? readonly(res) : reactive(res); 140 | } 141 | return res; 142 | }; 143 | } 144 | /** 145 | * 创建非readoly Proxy对象的 set 方法 146 | */ 147 | function createSetter() { 148 | // reactive创建的代理对象 触发 set 操作时 触发依赖执行 149 | return (target, key, value, receiver) => { 150 | const res = Reflect.set(target, key, value, receiver); 151 | // 触发依赖执行 152 | trigger(target, key); 153 | return res; 154 | }; 155 | } 156 | /** 157 | * readoly Proxy对象的 set 方法 158 | */ 159 | function readonlySetter(target, key) { 160 | // readonly创建的代理对象, 触发 set 操作时控制台进行警告提示 161 | console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target); 162 | return true; 163 | } 164 | 165 | function emit(instance, eventName, ...args) { 166 | const { props } = instance; 167 | const handlerName = toHandlerKey(camelize(eventName)); 168 | const handler = props[handlerName]; 169 | handler && handler(...args); 170 | } 171 | 172 | function initProps(instance, rawProps) { 173 | instance.props = rawProps || {}; 174 | } 175 | 176 | const publicPropertiesMap = extend(Object.create(null), { 177 | $el: (i) => i.vnode.el, 178 | $slots: (i) => i.slots 179 | }); 180 | /** publicInstanceProxyHandlers */ 181 | const publicInstanceProxyHandlers = { 182 | get({ _: instance }, key) { 183 | const { setupState, props } = instance; 184 | if (hasOwn(setupState, key)) { 185 | return setupState[key]; 186 | } 187 | else if (hasOwn(props, key)) { 188 | return props[key]; 189 | } 190 | // 如果 key 存在于公共Api中 则返回对应的公共Api 191 | else if (hasOwn(publicPropertiesMap, key)) { 192 | return publicPropertiesMap[key](instance); 193 | } 194 | } 195 | }; 196 | 197 | function initSlots(instance, children) { 198 | const { vnode } = instance; 199 | if (vnode.shapeFlag & 32 /* SLOT_CHILDREN */) { 200 | normalizeObjectSlots(children, instance.slots); 201 | } 202 | } 203 | function normalizeObjectSlots(children, slots) { 204 | for (const key in children) { 205 | const value = children[key]; 206 | slots[key] = (props) => normalizeSlotValue(value(props)); 207 | } 208 | } 209 | function normalizeSlotValue(value) { 210 | return Array.isArray(value) ? value : [value]; 211 | } 212 | 213 | /** 创建组件实例 */ 214 | function createComponentInstance(vnode) { 215 | const type = vnode.type; 216 | const instance = { 217 | vnode, 218 | props: EMPTY_OBJ, 219 | emit: () => { }, 220 | slots: EMPTY_OBJ, 221 | type, 222 | render: null, 223 | subTree: null, 224 | setupState: EMPTY_OBJ, 225 | proxy: null, 226 | ctx: EMPTY_OBJ 227 | }; 228 | instance.emit = emit.bind(null, instance); 229 | instance.ctx = { _: instance }; 230 | return instance; 231 | } 232 | /** 初始化组件 */ 233 | function setupComponent(instance) { 234 | const { props, children } = instance.vnode; 235 | initProps(instance, props); 236 | initSlots(instance, children); 237 | setupStatefulComponent(instance); 238 | } 239 | /** 初始化 有状态的(非函数式)组件 */ 240 | function setupStatefulComponent(instance) { 241 | // 此处 type = component VNode 242 | const { type: { setup }, props, ctx } = instance; 243 | // context 244 | instance.proxy = new Proxy(ctx, publicInstanceProxyHandlers); 245 | if (setup) { 246 | // 执行 setup 前 将currentInstance指向当前组件的instance 247 | currentInstance = instance; 248 | const setupResult = setup(shallowReadonly(props), instance); 249 | // 当前组件的 setup 执行完成后 清空将currentInstance 250 | currentInstance = null; 251 | handleSetupResult(instance, setupResult); 252 | } 253 | } 254 | /** 处理组件内 setup函数 的返回值 */ 255 | function handleSetupResult(instance, setupResult) { 256 | // TODO setup 返回的是函数时 257 | // 如果 setup 返回的是对象时 将setupResult赋值给组件实例对象上的setupState 258 | if (isObject(setupResult)) { 259 | instance.setupState = setupResult; 260 | } 261 | finishComponentSetup(instance); 262 | } 263 | /** 组件初始化 完成 */ 264 | function finishComponentSetup(instance) { 265 | const { type: component } = instance; 266 | instance.render = component.render; 267 | } 268 | // 组件实例全局变量 269 | let currentInstance = null; 270 | // 在setup里获取当前组件的实例 271 | const getCurrentInstance = () => currentInstance; 272 | 273 | const Fragment = Symbol('Fragment'); 274 | const Text = Symbol('Text'); 275 | /** 创建虚拟节点 */ 276 | const createVNode = (type, props = null, children = null, shapeFlag = initShapFlag(type)) => { 277 | const vnode = { 278 | type, 279 | props, 280 | children, 281 | el: null, 282 | shapeFlag 283 | }; 284 | /* 子节点是文本时 标为 TEXT_CHILDREN 否则视为 ARRAY_CHILDREN */ 285 | vnode.shapeFlag |= isString(children) 286 | ? 8 /* TEXT_CHILDREN */ 287 | : 16 /* ARRAY_CHILDREN */; 288 | if (vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */) { 289 | if (typeof children === 'object') { 290 | vnode.shapeFlag |= 32 /* SLOT_CHILDREN */; 291 | } 292 | } 293 | return vnode; 294 | }; 295 | /** 初始化VNode的默认shapeFlags */ 296 | const initShapFlag = (type) => isString(type) 297 | ? 1 /* ELEMENT */ 298 | : isFunction(type) 299 | ? 2 /* FUNCTIONAL_COMPONENT */ 300 | : 4 /* STATEFUL_COMPONENT */; 301 | const createTextVNode = (text) => { 302 | return createVNode(Text, null, text); 303 | }; 304 | 305 | /** render */ 306 | function render(vnode, container) { 307 | // 调用 patch 方法 308 | patch(vnode, container); 309 | } 310 | /** 处理各种vnode */ 311 | function patch(vnode, container) { 312 | const { type, shapeFlag } = vnode; 313 | switch (type) { 314 | case Fragment: 315 | // 处理Fragment类型的vnode 316 | processFragment(vnode, container); 317 | break; 318 | case Text: 319 | // 处理Fragment类型的vnode 320 | processText(vnode, container); 321 | break; 322 | default: 323 | if (shapeFlag & 1 /* ELEMENT */) { 324 | // 处理Element类型的vnode 325 | processElement(vnode, container); 326 | } 327 | else if (shapeFlag & 4 /* STATEFUL_COMPONENT */) { 328 | // 处理有状态的组件vnode 329 | processComponent(vnode, container); 330 | } 331 | break; 332 | } 333 | } 334 | /** 处理Fragment */ 335 | const processText = (vnode, container) => { 336 | const { children } = vnode; 337 | const textNode = (vnode.el = document.createTextNode(children)); 338 | container.append(textNode); 339 | }; 340 | /** 处理Fragment */ 341 | const processFragment = (vnode, container) => mountChildren(vnode.children, container); 342 | /** 处理Element */ 343 | const processElement = (vnode, container) => mountElement(vnode, container); 344 | /** 处理组件 */ 345 | const processComponent = (vnode, container) => mountComponent(vnode, container); 346 | /** 挂载Element */ 347 | const mountElement = (vnode, container) => { 348 | // 根据tagName创建HTML节点 349 | const el = (vnode.el = document.createElement(vnode.type)); 350 | const { children, props } = vnode; 351 | // 处理 props 通过循环给DOM节点设置属性 352 | for (const key in props) { 353 | // 挂载事件 354 | if (isOn(key) && isFunction(props[key])) { 355 | el.addEventListener(key.slice(2).toLowerCase(), props[key]); 356 | } 357 | else { 358 | el.setAttribute(key, props[key]); 359 | } 360 | } 361 | // 处理子节点 子节点可能是 TEXT_CHILDREN 或 ARRAY_CHILDREN 362 | if (vnode.shapeFlag & 8 /* TEXT_CHILDREN */) { 363 | // 获取el节点及其后代的文本内容并把子节点替换为文本 364 | el.textContent = children; 365 | } 366 | else if (vnode.shapeFlag & 16 /* ARRAY_CHILDREN */) { 367 | mountChildren(children, el); 368 | } 369 | // 将el节点插入到容器节点的子末位 370 | container.append(el); 371 | }; 372 | /** 挂载子节点 */ 373 | const mountChildren = (vnodeArray, container) => vnodeArray.forEach((vnode) => patch(vnode, container)); 374 | /** 挂载组件 */ 375 | const mountComponent = (initialVNode, container) => { 376 | const instance = createComponentInstance(initialVNode); 377 | setupComponent(instance); 378 | setupRenderEffect(instance, container); 379 | }; 380 | /** setupRenderEffect */ 381 | function setupRenderEffect(instance, container) { 382 | const subTree = instance.render.call(instance.proxy); 383 | patch(subTree, container); 384 | instance.vnode.el = subTree.el; 385 | } 386 | 387 | /** 创建一个提供应用上下文的应用实例 */ 388 | function createApp(rootComponent) { 389 | return { 390 | mount(rootContainer) { 391 | // 先把组件转化成虚拟节点 component -> VNode 392 | // 所有逻辑操作都会基于 VNode 做处理 393 | const vnode = createVNode(rootComponent); 394 | render(vnode, rootContainer); 395 | } 396 | }; 397 | } 398 | 399 | /** 返回一个`虚拟节点` */ 400 | function h(type, props, children) { 401 | return createVNode(type, props, children); 402 | } 403 | 404 | function renderSlots(slots, slotName, props) { 405 | const slot = slots === null || slots === void 0 ? void 0 : slots[slotName]; 406 | if (slot) { 407 | if (typeof slot === 'function') { 408 | return createVNode(Fragment, {}, slot(props)); 409 | } 410 | } 411 | } 412 | 413 | export { createApp, createTextVNode, getCurrentInstance, h, renderSlots }; 414 | -------------------------------------------------------------------------------- /lib/types/mini-vue3.d.ts: -------------------------------------------------------------------------------- 1 | /** 创建一个提供应用上下文的应用实例 */ 2 | declare function createApp(rootComponent: any): { 3 | mount(rootContainer: any): void; 4 | }; 5 | 6 | declare const enum ShapeFlags { 7 | ELEMENT = 1, 8 | FUNCTIONAL_COMPONENT = 2, 9 | STATEFUL_COMPONENT = 4, 10 | TEXT_CHILDREN = 8, 11 | ARRAY_CHILDREN = 16, 12 | SLOT_CHILDREN = 32, 13 | COMPONENT = 6 14 | } 15 | 16 | declare type Component = { 17 | setup: (props: object) => object; 18 | render: () => (type: any, props?: any, children?: any) => VNode; 19 | }; 20 | declare type Data = Record; 21 | declare type Slot = (...args: any[]) => VNode[]; 22 | declare type InternalSlots = { 23 | [name: string]: Slot | undefined; 24 | }; 25 | declare type Slots = Readonly; 26 | interface ComponentInternalInstance { 27 | vnode: VNode; 28 | props: Data; 29 | emit: Function; 30 | slots: InternalSlots; 31 | type: any; 32 | /** 33 | * 此组件vdom树的 根vnode(虚拟节点) 34 | */ 35 | subTree: VNode; 36 | /** 37 | * proxy 38 | */ 39 | proxy: any; 40 | /** 41 | * ctx 42 | */ 43 | ctx: Data; 44 | } 45 | declare const getCurrentInstance: () => ComponentInternalInstance; 46 | 47 | declare type VNodeTypes = string | VNode | Component | typeof Fragment | typeof Text; 48 | interface VNode { 49 | type: VNodeTypes; 50 | props: Record | null; 51 | children: VNodeNormalizedChildren; 52 | el: Node | null; 53 | shapeFlag: ShapeFlags; 54 | } 55 | declare type VNodeChildAtom = VNode | string | number | boolean | null | undefined | void; 56 | declare type VNodeArrayChildren = Array; 57 | declare type VNodeNormalizedChildren = string | VNodeArrayChildren | null; 58 | declare const Fragment: unique symbol; 59 | declare const Text: unique symbol; 60 | declare const createTextVNode: (text: string) => VNode; 61 | 62 | /** 返回一个`虚拟节点` */ 63 | declare function h(type: any, props?: any, children?: any): VNode; 64 | 65 | declare function renderSlots(slots: Slots, slotName: string, props: any): VNode | undefined; 66 | 67 | export { createApp, createTextVNode, getCurrentInstance, h, renderSlots }; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-vue", 3 | "version": "1.0.0", 4 | "types": "lib/types/mini-vue3.d.ts", 5 | "main": "lib/mini-vue3.cjs.js", 6 | "module": "lib/mini-vue3.esm.js", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "rollup -c rollup.config.js", 10 | "build-watch": "rollup -c rollup.config.js --watch", 11 | "test": "jest --runInBand", 12 | "test-coverage": "jest --runInBand --coverage", 13 | "test-watchAll": "jest --runInBand --watchAll" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.15.5", 17 | "@babel/preset-env": "^7.15.4", 18 | "@babel/preset-typescript": "^7.15.0", 19 | "@rollup/plugin-typescript": "^8.2.5", 20 | "@types/jest": "^27.0.1", 21 | "babel-jest": "^27.1.0", 22 | "jest": "^27.1.0", 23 | "rollup": "^2.58.0", 24 | "rollup-plugin-dts": "^4.0.0", 25 | "ts-node": "^10.2.1", 26 | "tslib": "^2.3.1", 27 | "typescript": "^4.4.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json' 2 | import typescript from '@rollup/plugin-typescript' 3 | import dts from 'rollup-plugin-dts' 4 | export default [ 5 | { 6 | input: './src/index.ts', 7 | output: [ 8 | // cjs(commonjs) 9 | { 10 | format: 'cjs', 11 | file: pkg.main 12 | }, 13 | // esm 14 | { 15 | format: 'es', 16 | file: pkg.module 17 | } 18 | ], 19 | 20 | plugins: [typescript()] 21 | }, 22 | { 23 | input: './src/index.ts', 24 | output: [{ file: pkg.types, format: 'es' }], 25 | plugins: [dts()] 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** mini-vue3 的出口文件 */ 2 | export * from './runtime-core/index' 3 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '../src/computed' 2 | import { effect } from '../src/effect' 3 | import { isReadonly, reactive } from '../src/reactive' 4 | import { ref } from '../src/ref' 5 | 6 | describe('reactivity/computed', () => { 7 | it('should return updated value', () => { 8 | const reactiveObj = reactive<{ foo?: number }>({}) 9 | const computedValue = computed(() => reactiveObj.foo) 10 | console.warn = jest.fn() 11 | 12 | // 默认情况下 computed 是只读的 13 | expect(isReadonly(computedValue)).toBe(true) 14 | // @ts-ignore-nextline 对只读的computed赋值不会成功,并且会发出警告 15 | computedValue.value = 1 16 | expect(console.warn).toBeCalledTimes(1) 17 | 18 | expect(computedValue.value).toBe(undefined) 19 | reactiveObj.foo = 1 20 | expect(computedValue.value).toBe(1) 21 | }) 22 | 23 | it('should compute lazily', () => { 24 | const reactiveObj = reactive<{ foo?: number }>({}) 25 | const getter = jest.fn(() => reactiveObj.foo) 26 | const computedValue = computed(getter) 27 | 28 | // 懒执行 29 | expect(getter).not.toHaveBeenCalled() 30 | 31 | expect(computedValue.value).toBe(undefined) 32 | expect(getter).toHaveBeenCalledTimes(1) 33 | 34 | // computed 的依赖不改变时, 再次获取值时使用缓存的computed值 35 | expect(computedValue.value).toBe(undefined) 36 | expect(getter).toHaveBeenCalledTimes(1) 37 | 38 | // computed的依赖改变后,在被再次读取值之前不重新计算 39 | reactiveObj.foo = 0 40 | reactiveObj.foo = 1 41 | expect(getter).toHaveBeenCalledTimes(1) 42 | 43 | // computed的依赖改变后,再次读取值时重新计算 44 | expect(computedValue.value).toBe(1) 45 | expect(getter).toHaveBeenCalledTimes(2) 46 | 47 | // computed 的依赖不改变时, 再次获取值时使用缓存的computed值,不会重新计算 48 | computedValue.value 49 | expect(getter).toHaveBeenCalledTimes(2) 50 | }) 51 | 52 | it('should trigger effect', () => { 53 | const reactiveObj = reactive<{ foo?: number }>({}) 54 | const computedValue = computed(() => reactiveObj.foo) 55 | let dummy 56 | effect(() => { 57 | dummy = computedValue.value 58 | }) 59 | expect(dummy).toBe(undefined) 60 | reactiveObj.foo = 1 61 | expect(dummy).toBe(1) 62 | }) 63 | 64 | it('should support setter', () => { 65 | const n = ref(1) 66 | const plusOne = computed({ 67 | get: () => n.value + 1, 68 | set: (val) => { 69 | n.value = val - 1 70 | } 71 | }) 72 | 73 | expect(isReadonly(plusOne)).toBe(false) 74 | expect(plusOne.value).toBe(2) 75 | n.value++ 76 | expect(plusOne.value).toBe(3) 77 | 78 | plusOne.value = 0 79 | expect(n.value).toBe(-1) 80 | }) 81 | it('should not support setter when the configuration object only has getter', () => { 82 | const n = ref(1) 83 | // @ts-ignore-nextline 84 | const plusOne = computed({ get: () => n.value + 1 }) 85 | console.warn = jest.fn() 86 | 87 | expect(isReadonly(plusOne)).toBe(true) 88 | expect(plusOne.value).toBe(2) 89 | n.value++ 90 | expect(plusOne.value).toBe(3) 91 | 92 | // @ts-ignore-nextline 93 | plusOne.value = 0 94 | expect(console.warn).toHaveBeenCalledTimes(1) 95 | expect(n.value).toBe(2) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from '../src/reactive' 2 | import { effect, stop } from '../src/effect' 3 | describe('effect', () => { 4 | it('happy path', () => { 5 | const user = reactive({ 6 | age: 10 7 | }) 8 | 9 | let nextAge 10 | effect(() => { 11 | nextAge = user.age + 1 12 | }) 13 | expect(nextAge).toBe(11) 14 | 15 | // update 16 | user.age++ 17 | expect(nextAge).toBe(12) 18 | }) 19 | 20 | // 调用 effect(fn) -> 返回一个 runner函数 21 | // -> 手动调用 runner()时 -> 会调用effect(fn)内的 fn() 22 | // -> 返回fn()的返回值 23 | it('should return runner when call effect', () => { 24 | let foo = 0 25 | const runner = effect(() => { 26 | foo++ 27 | return foo 28 | }) 29 | // effect(fn)函数会默认执行一次fn() 30 | expect(foo).toBe(1) 31 | // 调用effect(fn)返回的runner()函数会再次执行一次fn() 32 | runner() 33 | expect(foo).toBe(2) 34 | // 调用effect(fn)返回的runner()函数会得到再次执行一次fn()后fn()的返回值 35 | expect(runner()).toBe(3) 36 | }) 37 | 38 | it('scheduler', () => { 39 | let dummy 40 | let run: any 41 | const scheduler = jest.fn(() => { 42 | run = runner 43 | }) 44 | const obj = reactive({ foo: 1 }) 45 | const runner = effect( 46 | () => { 47 | dummy = obj.foo 48 | }, 49 | { scheduler } 50 | ) 51 | // 调用effect时默认会调用一次fn 不调用scheduler函数 52 | expect(scheduler).not.toHaveBeenCalled() 53 | expect(dummy).toBe(1) 54 | 55 | // 此后修改effect的依赖的值时 56 | // 默认情况下有scheduler时会触发scheduler函数并且不调用fn() 57 | // 无scheduler时,会触发调用fn() 58 | obj.foo++ 59 | expect(scheduler).toHaveBeenCalledTimes(1) 60 | expect(dummy).toBe(1) 61 | // 手动运行runner函数时 只调用fn() 不会调用 scheduler函数 62 | run() // 通过scheduler()函数把effect()返回的runner函数赋值给 run -> run===runner 63 | expect(dummy).toBe(2) 64 | expect(scheduler).toHaveBeenCalledTimes(1) 65 | }) 66 | 67 | it('stop', () => { 68 | let dummy 69 | const obj = reactive({ prop: 1 }) 70 | const runner = effect(() => { 71 | dummy = obj.prop 72 | }) 73 | obj.prop = 2 74 | expect(dummy).toBe(2) 75 | 76 | stop(runner) 77 | obj.prop++ 78 | expect(dummy).toBe(2) 79 | // 不会重复stop 和 清空依赖 80 | stop(runner) 81 | 82 | // stop掉的effect 应该仍然可以手动调用 runner 83 | runner() 84 | expect(dummy).toBe(3) 85 | }) 86 | 87 | it('onStop', () => { 88 | const onStop = jest.fn() 89 | const runner = effect(() => {}, { 90 | onStop 91 | }) 92 | 93 | // 对effect()返回的runner执行stop()操作时 94 | // 会执行一次,生成runner函数时传入的 onStop 函数 95 | stop(runner) 96 | expect(onStop).toBeCalledTimes(1) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | reactive, 3 | isReactive, 4 | isProxy, 5 | toRaw, 6 | readonly, 7 | shallowReadonly, 8 | shallowReactive, 9 | toReactive, 10 | isReadonly, 11 | toReadonly 12 | } from '../src/reactive' 13 | describe('reactive', () => { 14 | it('happy path', () => { 15 | const original = { foo: 1 } 16 | const observed = reactive(original) 17 | expect(observed).not.toBe(original) // 包装后的对象与源对象不同 18 | expect(observed.foo).toBe(1) // 可以获取到被包装对象内的值 与源对象对应的值一致 19 | expect(isProxy(original)).toBe(false) // 验证 original 不是是一个proxy对象 20 | expect(isReactive(original)).toBe(false) // 验证 original 不是是一个 reactive 21 | expect(isProxy(observed)).toBe(true) // 验证 observed 是一个proxy对象 22 | expect(isReactive(observed)).toBe(true) // 验证 observed 是一个 reactive 23 | }) 24 | 25 | it('nested reactives', () => { 26 | const original = { 27 | text: 'some text', 28 | nested: { 29 | foo: 1 30 | }, 31 | array: [{ bar: 2 }] 32 | } 33 | const observed = reactive(original) 34 | // 此时没有track过依赖 没有触发trigger 35 | observed.text = 'text' 36 | // 只有嵌套的对象被转换为 reactive 37 | expect(isReactive(observed.text)).toBe(false) 38 | expect(isReactive(observed.nested)).toBe(true) 39 | expect(isReactive(observed.array)).toBe(true) 40 | expect(isReactive(observed.array[0])).toBe(true) 41 | }) 42 | 43 | it('toRaw', () => { 44 | const obj = { foo: { bar: 'bar' }, sum: 1 } 45 | const reactiveObj = reactive(obj) 46 | const shallowReactiveObj = shallowReactive(obj) 47 | const readonlyObj = readonly(obj) 48 | const shallowReadonlyObj = shallowReadonly(obj) 49 | expect(toRaw(obj)).toBe(obj) 50 | expect(toRaw(reactiveObj)).toBe(obj) 51 | expect(toRaw(shallowReactiveObj)).toBe(obj) 52 | expect(toRaw(readonlyObj)).toBe(obj) 53 | expect(toRaw(shallowReadonlyObj)).toBe(obj) 54 | }) 55 | 56 | it('toReactive', () => { 57 | const obj = { num: 5 } 58 | const str = 'text' 59 | // 非对象数据返回其本身 60 | expect(toReactive(str)).toBe(str) 61 | expect(isReactive(toReactive(str))).toBe(false) 62 | // 返回对象的响应式副本 63 | expect(toReactive(obj)).not.toBe(obj) 64 | expect(isReactive(toReactive(obj))).toBe(true) 65 | expect(isReadonly(toReactive(obj))).toBe(false) 66 | }) 67 | 68 | it('toReadonly', () => { 69 | const obj = { num: 5 } 70 | const str = 'text' 71 | // 非对象数据返回其本身 72 | expect(toReadonly(str)).toBe(str) 73 | expect(isReadonly(toReadonly(str))).toBe(false) 74 | // 返回对象的响应式副本 75 | expect(toReadonly(obj)).not.toBe(obj) 76 | expect(isReadonly(toReadonly(obj))).toBe(true) 77 | expect(isReactive(toReadonly(obj))).toBe(false) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReadonly, readonly } from '../src/reactive' 2 | 3 | describe('readonly', () => { 4 | it('happy path', () => { 5 | const original = { foo: 1, bar: { baz: 2 } } 6 | const wrapped = readonly(original) 7 | 8 | // readonly 代理后生成一个与源对象内容相同新的proxy对象 9 | expect(wrapped).not.toBe(original) 10 | expect(isProxy(wrapped)).toBe(true) 11 | expect(isReadonly(wrapped)).toBe(true) 12 | expect(isProxy(original)).toBe(false) 13 | expect(isReadonly(original)).toBe(false) 14 | expect(wrapped.foo).toBe(1) 15 | expect(wrapped.bar.baz).toBe(2) 16 | }) 17 | 18 | it('should warn when called set', () => { 19 | // readonly() 创建的proxy对象不可以被set 20 | console.warn = jest.fn() 21 | const user = readonly({ 22 | age: 10 23 | }) 24 | user.age = 11 25 | expect(console.warn).toBeCalledTimes(1) 26 | user.age++ 27 | expect(console.warn).toBeCalledTimes(2) 28 | }) 29 | 30 | it('nested readonlys', () => { 31 | const original = { 32 | text: 'some text', 33 | nested: { 34 | foo: 1 35 | }, 36 | array: [{ bar: 2 }] 37 | } 38 | const wrapped = readonly(original) 39 | // 只有嵌套的对象被转换为 readonly 40 | expect(isReadonly(wrapped.text)).toBe(false) 41 | expect(isReadonly(wrapped.nested)).toBe(true) 42 | expect(isReadonly(wrapped.array)).toBe(true) 43 | expect(isReadonly(wrapped.array[0])).toBe(true) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../src/effect' 2 | import { isReactive, isReadonly, reactive } from '../src/reactive' 3 | import { isRef, proxyRefs, ref, shallowRef, toRef, toRefs, unref } from '../src/ref' 4 | describe('reactivity/ref', () => { 5 | it('ref happy path', () => { 6 | const r = ref(1) 7 | expect(isReadonly(r)).toBe(false) 8 | expect(r.value).toBe(1) 9 | r.value = 2 10 | expect(r.value).toBe(2) 11 | }) 12 | // ref() 没有初始值时也能正常工作 13 | it('should work without initial value', () => { 14 | const a = ref() 15 | let dummy 16 | effect(() => { 17 | dummy = a.value 18 | }) 19 | expect(dummy).toBe(undefined) 20 | a.value = 2 21 | expect(dummy).toBe(2) 22 | }) 23 | // ref()创建的变量应该是响应式的 24 | it('should be reactive', () => { 25 | const a = ref(1) 26 | let dummy 27 | let calls = 0 28 | effect(() => { 29 | calls++ 30 | dummy = a.value 31 | }) 32 | expect(calls).toBe(1) 33 | expect(dummy).toBe(1) 34 | a.value = 2 35 | expect(calls).toBe(2) 36 | expect(dummy).toBe(2) 37 | // 相同的值不触发 trigger() 38 | a.value = 2 39 | expect(calls).toBe(2) 40 | expect(dummy).toBe(2) 41 | }) 42 | // 嵌套属性应该是响应式的 43 | it('should make nested properties reactive', () => { 44 | const a = ref({ 45 | count: 1 46 | }) 47 | // 嵌套属性应该是被reactive代理的响应式对象 48 | expect(isReactive(a.value)).toBe(true) 49 | let dummy 50 | effect(() => { 51 | dummy = a.value.count 52 | }) 53 | expect(dummy).toBe(1) 54 | a.value.count = 2 55 | expect(dummy).toBe(2) 56 | }) 57 | 58 | it('isRef', () => { 59 | expect(isRef(ref(1))).toBe(true) 60 | // expect(isRef(computed(() => 1))).toBe(true) 61 | 62 | expect(isRef(0)).toBe(false) 63 | expect(isRef(1)).toBe(false) 64 | // 看起来像 ref 的对象不一定是 ref 65 | expect(isRef({ value: 0 })).toBe(false) 66 | }) 67 | 68 | it('unref', () => { 69 | expect(unref(1)).toBe(1) 70 | expect(unref(ref(1))).toBe(1) 71 | }) 72 | 73 | it('proxyRefs', () => { 74 | const user = { 75 | age: ref(10), 76 | name: 'xiaohong' 77 | } 78 | 79 | const proxyUser = proxyRefs(user) 80 | expect(user.age.value).toBe(10) 81 | expect(proxyUser.age).toBe(10) 82 | expect(proxyUser.name).toBe('xiaohong') 83 | 84 | proxyUser.age = 20 85 | expect(proxyUser.age).toBe(20) 86 | expect(user.age.value).toBe(20) 87 | 88 | // @ts-ignore 89 | proxyUser.age = ref(10) 90 | expect(proxyUser.age).toBe(10) 91 | expect(user.age.value).toBe(10) 92 | }) 93 | 94 | it('shallowRef', () => { 95 | const sref = shallowRef({ a: 1 }) 96 | expect(isReactive(sref.value)).toBe(false) 97 | 98 | let dummy 99 | effect(() => { 100 | dummy = sref.value.a 101 | }) 102 | expect(dummy).toBe(1) 103 | 104 | sref.value = { a: 2 } 105 | expect(isReactive(sref.value)).toBe(false) 106 | expect(dummy).toBe(2) 107 | }) 108 | it('should return the ref when the shallowRef wraps a ref', () => { 109 | const r = ref({ num: 1 }) 110 | const sr = shallowRef(r) 111 | expect(sr).toBe(r) 112 | expect(isReactive(sr.value)).toBe(true) 113 | }) 114 | 115 | it('shallowRef force trigger', () => { 116 | const sref = shallowRef({ a: 1 }) 117 | let dummy 118 | effect(() => { 119 | dummy = sref.value.a 120 | }) 121 | expect(dummy).toBe(1) 122 | 123 | sref.value.a = 2 124 | expect(dummy).toBe(1) // should not trigger yet 125 | 126 | // force trigger 127 | // triggerRef(sref) 128 | // expect(dummy).toBe(2) 129 | }) 130 | 131 | it('toRef', () => { 132 | const a = reactive({ 133 | x: 1 134 | }) 135 | const x = toRef(a, 'x') 136 | expect(isRef(x)).toBe(true) 137 | expect(x.value).toBe(1) 138 | 139 | // source -> proxy 140 | a.x = 2 141 | expect(x.value).toBe(2) 142 | 143 | // proxy -> source 144 | x.value = 3 145 | expect(a.x).toBe(3) 146 | 147 | // reactivity 148 | let dummyX 149 | effect(() => { 150 | dummyX = x.value 151 | }) 152 | expect(dummyX).toBe(x.value) 153 | 154 | // mutating source should trigger effect using the proxy refs 155 | a.x = 4 156 | expect(dummyX).toBe(4) 157 | 158 | // should keep ref 159 | const r = { x: ref(1) } 160 | const rr = toRef(r, 'x') 161 | expect(rr).toBe(r.x) 162 | rr.value = 2 163 | expect(r.x.value).toBe(2) 164 | r.x.value = 3 165 | expect(rr.value).toBe(3) 166 | }) 167 | 168 | it('toRefs', () => { 169 | const a = reactive({ 170 | x: 1, 171 | y: 2 172 | }) 173 | 174 | const { x, y } = toRefs(a) 175 | 176 | expect(isRef(x)).toBe(true) 177 | expect(isRef(y)).toBe(true) 178 | expect(x.value).toBe(1) 179 | expect(y.value).toBe(2) 180 | 181 | // source -> proxy 182 | a.x = 2 183 | a.y = 3 184 | expect(x.value).toBe(2) 185 | expect(y.value).toBe(3) 186 | 187 | // proxy -> source 188 | x.value = 3 189 | y.value = 4 190 | expect(a.x).toBe(3) 191 | expect(a.y).toBe(4) 192 | 193 | // reactivity 194 | let dummyX, dummyY 195 | effect(() => { 196 | dummyX = x.value 197 | dummyY = y.value 198 | }) 199 | expect(dummyX).toBe(x.value) 200 | expect(dummyY).toBe(y.value) 201 | 202 | // mutating source should trigger effect using the proxy refs 203 | a.x = 4 204 | a.y = 5 205 | expect(dummyX).toBe(4) 206 | expect(dummyY).toBe(5) 207 | }) 208 | 209 | it('toRefs should warn on plain object', () => { 210 | console.warn = jest.fn() 211 | toRefs({}) 212 | expect(console.warn).toHaveBeenCalledTimes(1) 213 | }) 214 | 215 | it('toRefs should warn on plain array', () => { 216 | console.warn = jest.fn() 217 | toRefs([]) 218 | expect(console.warn).toHaveBeenCalledTimes(1) 219 | }) 220 | 221 | it('toRefs reactive array', () => { 222 | const arr = reactive(['a', 'b', 'c']) 223 | const refs = toRefs(arr) 224 | 225 | expect(Array.isArray(refs)).toBe(true) 226 | 227 | refs[0].value = '1' 228 | expect(arr[0]).toBe('1') 229 | 230 | arr[1] = '2' 231 | expect(refs[1].value).toBe('2') 232 | }) 233 | }) 234 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/shallowReactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, shallowReactive } from '../src/reactive' 2 | 3 | describe('shallowReactive', () => { 4 | it('should only set the non-reactive attribute of the root node as reactive', () => { 5 | const props = shallowReactive({ n: { foo: 1 } }) 6 | expect(isReactive(props)).toBe(true) 7 | expect(isReactive(props.n)).toBe(false) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, shallowReadonly } from '../src/reactive' 2 | 3 | describe('shallowReadonly', () => { 4 | it('should only set the non-reactive attribute of the root node as readonly', () => { 5 | const props = shallowReadonly({ n: { foo: 1 } }) 6 | expect(isReadonly(props)).toBe(true) 7 | expect(isReadonly(props.n)).toBe(false) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/ref' 2 | export * from './src/reactive' 3 | export * from './src/effect' 4 | export * from './src/computed' 5 | -------------------------------------------------------------------------------- /src/reactivity/src/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '../../shared/index' 2 | import { track, trigger } from './effect' 3 | import { 4 | reactive, 5 | reactiveMap, 6 | readonly, 7 | readonlyMap, 8 | shallowReactiveMap, 9 | shallowReadonlyMap, 10 | Target 11 | } from './reactive' 12 | import { ReactivityFlags } from './enumeration' 13 | import { isRef, unref } from './ref' 14 | 15 | type ReactiveGetter = ( 16 | target: T & Target, 17 | key: PropertyKey, 18 | receiver?: any 19 | ) => T 20 | type ReactiveSetter = ( 21 | target: T & Target, 22 | key: PropertyKey, 23 | value: any, 24 | receiver?: any 25 | ) => boolean 26 | 27 | const reactvieGet = createGetter() 28 | const reactvieSet = createSetter() 29 | /** reactive 拦截方法 */ 30 | export const reactiveHandlers: ProxyHandler = { 31 | get: reactvieGet, 32 | set: reactvieSet 33 | } 34 | 35 | const shallowReactiveGet = createGetter(false, true) 36 | const shallowReactiveSet = reactvieSet 37 | /** shallowReactive 拦截方法 */ 38 | export const shallowReactiveHandlers: ProxyHandler = { 39 | get: shallowReactiveGet, 40 | set: shallowReactiveSet 41 | } 42 | 43 | const readonlyGet = createGetter(true) 44 | const readonlySet = readonlySetter 45 | /** readonly 拦截方法 */ 46 | export const readonlyHandlers: ProxyHandler = { 47 | get: readonlyGet, 48 | set: readonlySet 49 | } 50 | 51 | const shallowReadonlyGet = createGetter(true, true) 52 | const shallowReadonlySet = readonlySetter 53 | /** shallowReadonly 拦截方法 */ 54 | export const shallowReadonlyHandlers: ProxyHandler = { 55 | get: shallowReadonlyGet, 56 | set: shallowReadonlySet 57 | } 58 | 59 | export const shallowUnwrapRefHandlers: ProxyHandler = { 60 | get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)), 61 | set: (target, key, newValue, receiver) => { 62 | const oldValue = Reflect.get(target, key, receiver) 63 | if (isRef(oldValue) && !isRef(newValue)) { 64 | oldValue.value = newValue 65 | return true 66 | } else { 67 | return Reflect.set(target, key, newValue, receiver) 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * 创建proxy对象的get方法 74 | * @param isReadonly 75 | */ 76 | function createGetter( 77 | isReadonly: boolean = false, 78 | shallow: boolean = false 79 | ): ReactiveGetter { 80 | return (target, key, receiver) => { 81 | // isReactive和isReadonly 检测 不是readonly的就是reactive 82 | if (key === ReactivityFlags.IS_REACTIVE) { 83 | return !isReadonly 84 | } else if (key === ReactivityFlags.IS_READONLY) { 85 | return isReadonly 86 | } else if ( 87 | key === ReactivityFlags.RAW && 88 | receiver === 89 | (isReadonly 90 | ? shallow 91 | ? shallowReadonlyMap 92 | : readonlyMap 93 | : shallow 94 | ? shallowReactiveMap 95 | : reactiveMap 96 | ).get(target) 97 | ) { 98 | return target 99 | } 100 | 101 | const res = Reflect.get(target, key, receiver) 102 | 103 | // 不执行嵌套对象的深度readonly转换 104 | if (shallow) { 105 | return res 106 | } 107 | 108 | // 实现 reactive和readonly 的嵌套对象转换 109 | if (isObject(res)) { 110 | return isReadonly ? readonly(res) : reactive(res) 111 | } 112 | 113 | // 当不是readonly创建的代理对象时(reactive) 进行依赖收集 114 | if (!isReadonly) { 115 | track(target, key) 116 | } 117 | 118 | return res 119 | } 120 | } 121 | /** 122 | * 创建非readoly Proxy对象的 set 方法 123 | */ 124 | function createSetter(): ReactiveSetter { 125 | // reactive创建的代理对象 触发 set 操作时 触发依赖执行 126 | return (target, key, value, receiver) => { 127 | const res = Reflect.set(target, key, value, receiver) 128 | // 触发依赖执行 129 | trigger(target, key) 130 | return res 131 | } 132 | } 133 | /** 134 | * readoly Proxy对象的 set 方法 135 | */ 136 | function readonlySetter(target: T, key: PropertyKey): boolean { 137 | // readonly创建的代理对象, 触发 set 操作时控制台进行警告提示 138 | console.warn( 139 | `Set operation on key "${String(key)}" failed: target is readonly.`, 140 | target 141 | ) 142 | return true 143 | } 144 | -------------------------------------------------------------------------------- /src/reactivity/src/computed.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '../../shared/index' 2 | import { Dep } from './dep' 3 | import { ReactiveEffect, triggerEffects } from './effect' 4 | import { ReactivityFlags } from './enumeration' 5 | import { trackRefValue } from './ref' 6 | 7 | type ComputedGetter = (...args: any[]) => T 8 | type ComputedSetter = (v: T) => void 9 | export type WritableComputedOptions = { 10 | get: ComputedGetter 11 | set: ComputedSetter 12 | } 13 | 14 | type ComputedRef = { 15 | readonly value: T 16 | } 17 | type WritableComputedRef = { 18 | value: T 19 | } 20 | 21 | class ComputedRefImpl { 22 | private _value!: T /* 缓存 */ 23 | private _cached: boolean = false /* 是否已经缓存过 根据依赖计算出的最新值 */ 24 | public dep?: Dep = void 0 /* dep 只会在读取ref的值时才会进行初始化 */ 25 | public readonly [ReactivityFlags.IS_REF]: boolean = true /* ref类型标志 */ 26 | public readonly [ReactivityFlags.IS_READONLY]: boolean /* 是否只读标志 */ 27 | public readonly effect: ReactiveEffect /* ComputedRefImpl实例的副作用函数 */ 28 | 29 | constructor( 30 | public getter: ComputedGetter, 31 | private readonly _setter: ComputedSetter, 32 | isReadonly: boolean 33 | ) { 34 | this[ReactivityFlags.IS_READONLY] = isReadonly 35 | this.effect = new ReactiveEffect(getter, () => { 36 | if (this._cached) { 37 | this._cached = false 38 | this.dep && triggerEffects(this.dep) 39 | } 40 | }) 41 | } 42 | get value() { 43 | trackRefValue(this) 44 | if (!this._cached) { 45 | this._cached = true 46 | this._value = this.effect.run()! 47 | } 48 | return this._value 49 | } 50 | 51 | set value(newValue: T) { 52 | this._setter(newValue) 53 | } 54 | } 55 | 56 | export function computed(getter: ComputedGetter): ComputedRef 57 | export function computed( 58 | options: WritableComputedOptions 59 | ): WritableComputedRef 60 | export function computed( 61 | getterOrOptions: ComputedGetter | WritableComputedOptions 62 | ) { 63 | let getter: ComputedGetter 64 | let setter: ComputedSetter 65 | let isReadonly: boolean = true 66 | if (isFunction(getterOrOptions)) { 67 | getter = getterOrOptions 68 | setter = () => { 69 | console.warn('Write operation failed: computed value is readonly') 70 | } 71 | } else { 72 | isReadonly = !getterOrOptions.set 73 | getter = getterOrOptions.get 74 | setter = isReadonly 75 | ? () => { 76 | console.warn('Write operation failed: computed value is readonly') 77 | } 78 | : getterOrOptions.set 79 | } 80 | return new ComputedRefImpl(getter, setter, isReadonly) 81 | } 82 | -------------------------------------------------------------------------------- /src/reactivity/src/dep.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from './effect' 2 | 3 | export type Dep = Set 4 | 5 | /** 创建并初始化一个空的 `dep: Set` */ 6 | export function createDep(effects?: ReactiveEffect[]): Dep { 7 | return new Set(effects) 8 | } 9 | -------------------------------------------------------------------------------- /src/reactivity/src/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from '../../shared/index' 2 | import { Dep } from './dep' 3 | /** 4 | * TS 类型 5 | */ 6 | type KeyToDepMap = Map 7 | export type ReactiveEffectOptions = { 8 | scheduler?: EffectScheduler 9 | onStop?: () => void 10 | } 11 | type EffectScheduler = (...args: any[]) => any 12 | export type ReactiveEffectRunner = { 13 | (): T 14 | effect: ReactiveEffect 15 | } 16 | 17 | /** 18 | * 全局变量 19 | * activeEffect 指向当前 effect 20 | * shouldTrack 依赖收集标志 21 | */ 22 | let activeEffect: ReactiveEffect | undefined 23 | let shouldTrack: boolean = false 24 | 25 | export class ReactiveEffect { 26 | public deps: Dep[] = [] 27 | // 是否是活跃的 (没有被stop) 28 | public active: boolean = true 29 | // effect() options里的onStop函数 30 | public onStop?: () => void 31 | constructor( 32 | public fn: () => T, 33 | public scheduler: EffectScheduler | null = null 34 | ) {} 35 | 36 | run() { 37 | // effect没有stop过 是活跃的 打开依赖收集标志 38 | // 并将全局变量activeEffect 指向当前 effect 39 | if (this.active) { 40 | shouldTrack = true 41 | activeEffect = this 42 | } 43 | // 因为fn()中有可能包含reactive代理对象 44 | // 对reactive代理对象的get操作会触发依赖收集 45 | // 依赖收集函数track()会根据依赖收集标志shouldTrack判断是否执行 46 | const result = this.fn() 47 | // 执行过fn()后将依赖收集标志shouldTrack重置为false 48 | shouldTrack = false 49 | return result 50 | } 51 | 52 | stop() { 53 | // 如果是活跃的 则清空对应依赖 如果已经stop过 跳过依赖清空过程 54 | if (this.active) { 55 | cleanupEffect(this) 56 | if (this.onStop) { 57 | this.onStop() 58 | } 59 | this.active = false 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * 从包含当前effect的依赖(dep)中 清除当前effect 并把effect内的deps清空 66 | */ 67 | function cleanupEffect(effect: ReactiveEffect) { 68 | // 从包含当前effect的依赖(dep)中 清除当前effect 69 | effect.deps.forEach((dep) => { 70 | dep.delete(effect) 71 | }) 72 | // 清空effect内的deps 73 | effect.deps.length = 0 74 | } 75 | 76 | /** 77 | * 副作用函数 78 | * @param fn 要执行的副作用 注意: 默认自执行一次 fn 79 | */ 80 | export function effect( 81 | fn: () => T, 82 | options?: ReactiveEffectOptions 83 | ): ReactiveEffectRunner { 84 | const _effect = new ReactiveEffect(fn) 85 | 86 | options && extend(_effect, options) 87 | 88 | _effect.run() 89 | // 返回当前 effect对应的的run()方法 90 | const runner = _effect.run.bind(_effect) as ReactiveEffectRunner 91 | runner.effect = _effect 92 | return runner 93 | } 94 | /** 95 | * 停止响应式监听 96 | * @param runner 97 | */ 98 | export function stop(runner: ReactiveEffectRunner) { 99 | runner.effect.stop() 100 | } 101 | 102 | // REVIEW 这一段依赖收集的逻辑关系 需要多复习 103 | const targetMap = new WeakMap() 104 | /** 105 | * 依赖收集(从reactive的target->key->dep 然后收集依赖) 106 | */ 107 | export function track(target: object, key: unknown) { 108 | // 判断是否进行依赖收集 不需要的直接 return 109 | if (!isTracking()) return 110 | 111 | // 依赖对应关系: target -> key -> dep 112 | let depsMap = targetMap.get(target) 113 | if (!depsMap) { 114 | targetMap.set(target, (depsMap = new Map() as KeyToDepMap)) 115 | } 116 | let dep = depsMap.get(key) 117 | if (!dep) { 118 | depsMap.set(key, (dep = new Set() as Dep)) 119 | } 120 | trackEffects(dep) 121 | } 122 | /** 123 | * 依赖收集(直接收集dep依赖) 124 | */ 125 | export function trackEffects(dep: Dep) { 126 | // 已经收集过的依赖 就不需要重复收集了 127 | if (dep.has(activeEffect!)) return 128 | 129 | // 未收集的依赖 进行收集 并且当前effect进行反向收集 dep 130 | dep.add(activeEffect!) 131 | activeEffect!.deps.push(dep) 132 | } 133 | 134 | /** 135 | * 判断是否应该进行进行依赖收集 136 | */ 137 | export function isTracking() { 138 | // 如果activeEffect是 undefined 则跳过依赖收集 139 | // 如果shouldTrack为false 则跳过依赖收集 140 | return activeEffect !== void 0 && shouldTrack 141 | } 142 | 143 | /** 144 | * 触发依赖执行(从reactive的target->key->dep 然后收集依赖) 145 | */ 146 | export function trigger(target: object, key: unknown) { 147 | let depsMap = targetMap.get(target) 148 | // 没有收集过依赖(tracked),直接跳过trigger 149 | if (!depsMap) return 150 | 151 | let dep = depsMap.get(key) 152 | dep && triggerEffects(dep) 153 | } 154 | /** 155 | * 触发依赖执行(直接触发dep中的依赖) 156 | */ 157 | export function triggerEffects(dep: Dep) { 158 | dep.forEach((effect) => { 159 | if (effect.scheduler) { 160 | effect.scheduler() 161 | } else { 162 | effect.run() 163 | } 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /src/reactivity/src/enumeration.ts: -------------------------------------------------------------------------------- 1 | export const enum ReactivityFlags { 2 | IS_REACTIVE = '__v_isReactive', 3 | IS_REF = '__v_isRef', 4 | IS_READONLY = '__v_isReadonly', 5 | RAW = '__v_raw' 6 | } 7 | -------------------------------------------------------------------------------- /src/reactivity/src/reactive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '../../shared/index' 2 | import { 3 | reactiveHandlers, 4 | readonlyHandlers, 5 | shallowReadonlyHandlers, 6 | shallowReactiveHandlers 7 | } from './baseHandlers' 8 | import { ReactivityFlags } from './enumeration' 9 | 10 | export type Target = { 11 | [ReactivityFlags.IS_REACTIVE]?: boolean 12 | [ReactivityFlags.IS_READONLY]?: boolean 13 | [ReactivityFlags.RAW]?: any 14 | } 15 | 16 | export type ProxyToRawWeakMap = WeakMap 17 | // 储存reactive和其原始对象对应关系的 全局WeakMap 18 | export const reactiveMap: ProxyToRawWeakMap = new WeakMap() 19 | export const shallowReactiveMap: ProxyToRawWeakMap = new WeakMap() 20 | export const readonlyMap: ProxyToRawWeakMap = new WeakMap() 21 | export const shallowReadonlyMap: ProxyToRawWeakMap = new WeakMap() 22 | /** 23 | * 创建并返回一个响应式对象 24 | * @param raw 25 | */ /* TS重载 */ 26 | export function reactive(target: T): T 27 | export function reactive(raw: object) { 28 | return createReactiveObject(raw, reactiveHandlers, reactiveMap) 29 | } 30 | export function shallowReactive(raw: T): T { 31 | return createReactiveObject(raw, shallowReactiveHandlers, shallowReactiveMap) 32 | } 33 | 34 | /** 35 | * 返回原始对象的只读代理 36 | * @param raw 37 | */ 38 | export function readonly(raw: T): T { 39 | return createReactiveObject(raw, readonlyHandlers, readonlyMap) 40 | } 41 | export function shallowReadonly(raw: T): T { 42 | return createReactiveObject(raw, shallowReadonlyHandlers, shallowReadonlyMap) 43 | } 44 | 45 | /** 46 | * 创建反应对象 47 | * @param target 原始对象 48 | * @param baseHandlers Proxy处理函数的对象 49 | */ 50 | function createReactiveObject( 51 | target: T, 52 | baseHandlers: ProxyHandler, 53 | proxyMap: ProxyToRawWeakMap 54 | ) { 55 | // target 已经具有相应的 Proxy 56 | const existingProxy = proxyMap.get(target) 57 | if (existingProxy) { 58 | return existingProxy 59 | } 60 | const proxy = new Proxy(target, baseHandlers) 61 | proxyMap.set(target, proxy) 62 | return proxy 63 | } 64 | 65 | export function isReactive(value: unknown): boolean { 66 | return !!(value as Target)[ReactivityFlags.IS_REACTIVE] 67 | } 68 | export function isReadonly(value: unknown): boolean { 69 | return !!(value as Target)[ReactivityFlags.IS_READONLY] 70 | } 71 | export function isProxy(value: unknown): boolean { 72 | return isReactive(value) || isReadonly(value) 73 | } 74 | 75 | /** 将对象类型的值用reactive代理 */ 76 | export const toReactive = (value: T): T => 77 | isObject(value) ? reactive(value) : value 78 | /** 将对象类型的值用readonly代理 */ 79 | export const toReadonly = (value: T): T => 80 | isObject(value) ? readonly(value) : value 81 | 82 | export function toRaw(value: T): T { 83 | const raw = value && (value as Target)[ReactivityFlags.RAW] 84 | return raw ? toRaw(raw) : value 85 | } 86 | -------------------------------------------------------------------------------- /src/reactivity/src/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged, isArray } from '../../shared/index' 2 | import { shallowUnwrapRefHandlers } from './baseHandlers' 3 | import { createDep, Dep } from './dep' 4 | import { isTracking, trackEffects, triggerEffects } from './effect' 5 | import { isProxy, toReactive } from './reactive' 6 | import { ReactivityFlags } from './enumeration' 7 | 8 | type Ref = { 9 | value: T 10 | } 11 | type RefBase = { 12 | dep?: Dep 13 | value: T 14 | } 15 | // proxyRefs中 Ref 类型浅解包 16 | export type ShallowUnwrapRef = { 17 | // 如果 `V` 是`unknown` ,则表示它不是 extend `Ref` 并且是 undefined 18 | [K in keyof T]: T[K] extends Ref 19 | ? V 20 | : T[K] extends Ref | undefined 21 | ? unknown extends V 22 | ? undefined 23 | : V | undefined 24 | : T[K] 25 | } 26 | 27 | // REVIEW 28 | export type UnwrapRef = T extends Ref 29 | ? UnwrapRefSimple 30 | : UnwrapRefSimple 31 | // REVIEW 32 | export type UnwrapRefSimple = T extends 33 | | Function 34 | // | CollectionTypes 35 | // | BaseTypes 36 | | Ref 37 | ? // | RefUnwrapBailTypes[keyof RefUnwrapBailTypes] 38 | T 39 | : T extends Array 40 | ? { [K in keyof T]: UnwrapRefSimple } 41 | : T extends object 42 | ? { 43 | [P in keyof T]: P extends symbol ? T[P] : UnwrapRef 44 | } 45 | : T 46 | 47 | // Ref实现类 48 | class RefImpl { 49 | private _rawValue: T 50 | private _value: T 51 | public dep?: Dep = void 0 /* dep 只会在读取ref的值时才会进行初始化 */ 52 | public readonly [ReactivityFlags.IS_REF]: boolean = true /* ref类型标志 */ 53 | public readonly [ReactivityFlags.IS_READONLY]: boolean = false /* 是否只读标志 */ 54 | 55 | constructor(value: T, private readonly _shallow: boolean) { 56 | // 如果传入的是一个对象,则用reactive将此对象代理成响应式 57 | this._rawValue = value 58 | this._value = _shallow ? value : toReactive(value) 59 | } 60 | get value() { 61 | trackRefValue(this) 62 | return this._value 63 | } 64 | set value(newValue) { 65 | // 新值与旧值不相等时,才会对ref执行重新赋值操作 66 | if (hasChanged(this._rawValue, newValue)) { 67 | // 先修改value,再触发依赖 68 | this._rawValue = newValue 69 | // 如果传入的是一个对象,则用reactive将此对象代理成响应式 70 | this._value = this._shallow ? newValue : toReactive(newValue) 71 | this.dep && triggerEffects(this.dep) 72 | } 73 | } 74 | } 75 | 76 | /** 收集ref的依赖 */ 77 | export function trackRefValue(ref: RefBase) { 78 | // 判断是否应该进行进行依赖收集 79 | // 如果这个ref被收集过依赖并且对应的effect没有被stop过的话,进行依赖收集 80 | if (!isTracking()) return 81 | // 如果ref对应的dep没有初始化,就对其进行初始化 82 | ref.dep || (ref.dep = createDep()) 83 | trackEffects(ref.dep) 84 | } 85 | 86 | /** REVIEW 为源响应式对象上的某个 property 新创建一个 ref */ 87 | class ObjectRefImpl { 88 | public readonly [ReactivityFlags.IS_REF]: boolean = true 89 | 90 | constructor(private readonly _rawObject: T, private readonly _key: K) {} 91 | 92 | get value() { 93 | return this._rawObject[this._key] 94 | } 95 | set value(newVal) { 96 | this._rawObject[this._key] = newVal 97 | } 98 | } 99 | 100 | /** 101 | * 创建一个 Ref 变量 102 | */ /* TS重载 */ 103 | export function ref(raw: T): Ref 104 | export function ref(raw: T): Ref 105 | export function ref(): Ref 106 | export function ref(raw?: unknown) { 107 | return createRef(raw) 108 | } 109 | 110 | export function shallowRef(value: T): T extends Ref ? T : Ref 111 | export function shallowRef(value: T): Ref 112 | export function shallowRef(): Ref 113 | export function shallowRef(raw?: unknown) { 114 | return createRef(raw, true) 115 | } 116 | 117 | const createRef = (rawValue: unknown, shallow: boolean = false) => 118 | isRef(rawValue) ? rawValue : new RefImpl(rawValue, shallow) 119 | 120 | /** 检查变量是否为一个 ref 对象 */ 121 | export const isRef = (value: Ref | unknown): value is Ref => 122 | !!(value && (value as any)[ReactivityFlags.IS_REF] === true) 123 | 124 | /** 如果参数是一个 ref,则返回内部值,否则返回参数本身 */ 125 | export const unref = (ref: Ref | T): T => (isRef(ref) ? ref.value : ref) 126 | 127 | /** REVIEW toRef 为源响应式对象上的某个 property 新创建一个 ref(是ref的直接返回) */ 128 | export type ToRef = [T] extends [Ref] ? T : Ref> 129 | export const toRef = ( 130 | rawObject: T, 131 | key: K 132 | ): ToRef => { 133 | const val = rawObject[key] 134 | return isRef(val) ? val : (new ObjectRefImpl(rawObject, key) as any) 135 | } 136 | 137 | /** REVIEW toRefs */ 138 | export type ToRefs = { 139 | [K in keyof T]: T[K] extends Ref ? T[K] : Ref> 140 | } 141 | export function toRefs(object: T): ToRefs { 142 | if (!isProxy(object)) { 143 | console.warn(`toRefs() expects a reactive object but received a plain one.`) 144 | } 145 | const ret: any = isArray(object) ? new Array(object.length) : {} 146 | for (const key in object) { 147 | ret[key] = toRef(object, key) 148 | } 149 | return ret 150 | } 151 | 152 | /** proxyRefs 对 object对象中的 ref 进行浅解包 */ 153 | export function proxyRefs(objectWithRefs: T): ShallowUnwrapRef { 154 | return new Proxy(objectWithRefs, shallowUnwrapRefHandlers) 155 | } 156 | -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | export { createApp } from './src/creatApp' 2 | export { h } from './src/h' 3 | export { getCurrentInstance } from './src/component' 4 | export { createTextVNode } from './src/vnode' 5 | export { renderSlots } from './src/helpers/renderSlots' 6 | -------------------------------------------------------------------------------- /src/runtime-core/src/component.ts: -------------------------------------------------------------------------------- 1 | import { shallowReadonly } from '../../reactivity/index' 2 | import { EMPTY_OBJ, isObject } from '../../shared/index' 3 | import { emit } from './componentEmit' 4 | import { initProps } from './componentProps' 5 | import { publicInstanceProxyHandlers } from './componentPublicInstance' 6 | import { initSlots } from './componentSlots' 7 | import { VNode, VNodeChildren } from './vnode' 8 | 9 | export type Component = { 10 | setup: (props: object) => object 11 | render: () => (type: any, props?: any, children?: any) => VNode 12 | } 13 | export type Data = Record 14 | 15 | export type Slot = (...args: any[]) => VNode[] 16 | export type InternalSlots = { 17 | [name: string]: Slot | undefined 18 | } 19 | export type Slots = Readonly 20 | 21 | /** 22 | * @internal 23 | */ 24 | export type InternalRenderFunction = { 25 | (): VNodeChildren 26 | } 27 | // 组件实例对象接口 28 | export interface ComponentInternalInstance { 29 | vnode: VNode 30 | props: Data 31 | emit: Function 32 | slots: InternalSlots 33 | type: any 34 | /** 35 | * 返回vdom树的渲染函数 36 | * @internal 37 | */ 38 | render: InternalRenderFunction | null 39 | /** 40 | * 此组件vdom树的 根vnode(虚拟节点) 41 | */ 42 | subTree: VNode 43 | /** 44 | * setup related 45 | * @internal 46 | */ 47 | setupState: Data 48 | /** 49 | * proxy 50 | */ 51 | proxy: any 52 | /** 53 | * ctx 54 | */ 55 | ctx: Data 56 | } 57 | 58 | /** 创建组件实例 */ 59 | export function createComponentInstance(vnode: VNode): ComponentInternalInstance { 60 | const type = vnode.type 61 | const instance = { 62 | vnode, 63 | props: EMPTY_OBJ, 64 | emit: () => {}, 65 | slots: EMPTY_OBJ, 66 | type, 67 | render: null, 68 | subTree: null!, 69 | setupState: EMPTY_OBJ, 70 | proxy: null, 71 | ctx: EMPTY_OBJ 72 | } 73 | instance.emit = emit.bind(null, instance) as any 74 | instance.ctx = { _: instance } 75 | 76 | return instance 77 | } 78 | 79 | /** 初始化组件 */ 80 | export function setupComponent(instance: ComponentInternalInstance) { 81 | const { props, children } = instance.vnode 82 | initProps(instance, props) 83 | initSlots(instance, children) 84 | setupStatefulComponent(instance) 85 | } 86 | 87 | /** 初始化 有状态的(非函数式)组件 */ 88 | function setupStatefulComponent(instance: ComponentInternalInstance) { 89 | // 此处 type = component VNode 90 | const { 91 | type: { setup }, 92 | props, 93 | ctx 94 | } = instance 95 | 96 | // context 97 | instance.proxy = new Proxy(ctx, publicInstanceProxyHandlers) 98 | 99 | if (setup) { 100 | // 执行 setup 前 将currentInstance指向当前组件的instance 101 | setCurrentInstance(instance) 102 | const setupResult = setup(shallowReadonly(props), instance) 103 | // 当前组件的 setup 执行完成后 清空将currentInstance 104 | setCurrentInstance(null) 105 | handleSetupResult(instance, setupResult) 106 | } 107 | } 108 | 109 | /** 处理组件内 setup函数 的返回值 */ 110 | function handleSetupResult( 111 | instance: ComponentInternalInstance, 112 | setupResult: unknown 113 | ) { 114 | // TODO setup 返回的是函数时 115 | 116 | // 如果 setup 返回的是对象时 将setupResult赋值给组件实例对象上的setupState 117 | if (isObject(setupResult)) { 118 | instance.setupState = setupResult 119 | } 120 | 121 | finishComponentSetup(instance) 122 | } 123 | 124 | /** 组件初始化 完成 */ 125 | function finishComponentSetup(instance: ComponentInternalInstance) { 126 | const { type: component } = instance 127 | 128 | instance.render = component.render 129 | } 130 | 131 | // 组件实例全局变量 132 | let currentInstance: ComponentInternalInstance | null = null 133 | // 在setup里获取当前组件的实例 134 | export const getCurrentInstance = () => currentInstance! 135 | // 修改组件实例全局变量或清空 136 | const setCurrentInstance = (instance: ComponentInternalInstance | null) => 137 | (currentInstance = instance) 138 | -------------------------------------------------------------------------------- /src/runtime-core/src/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from '../../shared/index' 2 | import { ComponentInternalInstance } from './component' 3 | 4 | export function emit( 5 | instance: ComponentInternalInstance, 6 | eventName: string, 7 | ...args: any 8 | ) { 9 | const { props } = instance 10 | const handlerName = toHandlerKey(camelize(eventName)) 11 | const handler = props[handlerName] as Function 12 | handler && handler(...args) 13 | } 14 | -------------------------------------------------------------------------------- /src/runtime-core/src/componentProps.ts: -------------------------------------------------------------------------------- 1 | import { ComponentInternalInstance, Data } from './component' 2 | 3 | export function initProps( 4 | instance: ComponentInternalInstance, 5 | rawProps: Data | null 6 | ) { 7 | instance.props = rawProps || {} 8 | } 9 | -------------------------------------------------------------------------------- /src/runtime-core/src/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { extend, hasOwn } from '../../shared/index' 2 | import { ComponentInternalInstance } from './component' 3 | export interface ComponentRenderContext { 4 | _: ComponentInternalInstance 5 | } 6 | type PublicPropertiesMap = Record any> 7 | 8 | export const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), { 9 | $el: (i) => i.vnode.el, 10 | $slots: (i) => i.slots 11 | } as PublicPropertiesMap) 12 | 13 | /** publicInstanceProxyHandlers */ 14 | export const publicInstanceProxyHandlers: ProxyHandler = { 15 | get({ _: instance }: ComponentRenderContext, key: string) { 16 | const { setupState, props } = instance 17 | if (hasOwn(setupState, key)) { 18 | return setupState[key] 19 | } else if (hasOwn(props, key)) { 20 | return props[key] 21 | } 22 | // 如果 key 存在于公共Api中 则返回对应的公共Api 23 | else if (hasOwn(publicPropertiesMap, key)) { 24 | return publicPropertiesMap[key](instance) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/runtime-core/src/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { isArray, ShapeFlags } from '../../shared/index' 2 | import { ComponentInternalInstance } from './component' 3 | import { VNodeNormalizedChildren } from './vnode' 4 | 5 | export function initSlots( 6 | instance: ComponentInternalInstance, 7 | children: VNodeNormalizedChildren 8 | ) { 9 | const { vnode } = instance 10 | if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) { 11 | normalizeObjectSlots(children, instance.slots) 12 | } 13 | } 14 | function normalizeObjectSlots(children: any, slots: any) { 15 | for (const key in children) { 16 | const value = children[key] 17 | slots[key] = (props: any) => normalizeSlotValue(value(props)) 18 | } 19 | } 20 | 21 | function normalizeSlotValue(value: any) { 22 | return Array.isArray(value) ? value : [value] 23 | } 24 | -------------------------------------------------------------------------------- /src/runtime-core/src/creatApp.ts: -------------------------------------------------------------------------------- 1 | import { render } from './renderer' 2 | import { createVNode } from './vnode' 3 | 4 | /** 创建一个提供应用上下文的应用实例 */ 5 | export function createApp(rootComponent: any) { 6 | return { 7 | mount(rootContainer: any) { 8 | // 先把组件转化成虚拟节点 component -> VNode 9 | // 所有逻辑操作都会基于 VNode 做处理 10 | const vnode = createVNode(rootComponent) 11 | 12 | render(vnode, rootContainer) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/runtime-core/src/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from './vnode' 2 | 3 | /** 返回一个`虚拟节点` */ 4 | export function h(type: any, props?: any, children?: any) { 5 | return createVNode(type, props, children) 6 | } 7 | -------------------------------------------------------------------------------- /src/runtime-core/src/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { Slots } from '../component' 2 | import { createVNode, Fragment } from '../vnode' 3 | 4 | export function renderSlots(slots: Slots, slotName: string, props: any) { 5 | const slot = slots?.[slotName] 6 | if (slot) { 7 | if (typeof slot === 'function') { 8 | return createVNode(Fragment, {}, slot(props)) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/runtime-core/src/renderer.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isOn, ShapeFlags } from '../../shared/index' 2 | import { 3 | ComponentInternalInstance, 4 | createComponentInstance, 5 | setupComponent 6 | } from './component' 7 | import { Fragment, Text, VNode, VNodeArrayChildren } from './vnode' 8 | 9 | /** render */ 10 | export function render(vnode: VNode, container: any) { 11 | // 调用 patch 方法 12 | patch(vnode, container) 13 | } 14 | 15 | /** 处理各种vnode */ 16 | function patch(vnode: VNode, container: any) { 17 | const { type, shapeFlag } = vnode 18 | switch (type) { 19 | case Fragment: 20 | // 处理Fragment类型的vnode 21 | processFragment(vnode, container) 22 | break 23 | case Text: 24 | // 处理Fragment类型的vnode 25 | processText(vnode, container) 26 | break 27 | default: 28 | if (shapeFlag & ShapeFlags.ELEMENT) { 29 | // 处理Element类型的vnode 30 | processElement(vnode, container) 31 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 32 | // 处理有状态的组件vnode 33 | processComponent(vnode, container) 34 | } 35 | break 36 | } 37 | } 38 | 39 | /** 处理Fragment */ 40 | const processText = (vnode: VNode, container: any) => { 41 | const { children } = vnode 42 | const textNode = (vnode.el = document.createTextNode(children as string)) 43 | container.append(textNode) 44 | } 45 | 46 | /** 处理Fragment */ 47 | const processFragment = (vnode: VNode, container: any) => 48 | mountChildren(vnode.children as VNodeArrayChildren, container) 49 | 50 | /** 处理Element */ 51 | const processElement = (vnode: VNode, container: any) => 52 | mountElement(vnode, container) 53 | 54 | /** 处理组件 */ 55 | const processComponent = (vnode: VNode, container: any) => 56 | mountComponent(vnode, container) 57 | 58 | /** 挂载Element */ 59 | const mountElement = (vnode: VNode, container: Element) => { 60 | // 根据tagName创建HTML节点 61 | const el: Element = (vnode.el = document.createElement(vnode.type as string)) 62 | 63 | const { children, props } = vnode 64 | // 处理 props 通过循环给DOM节点设置属性 65 | for (const key in props) { 66 | // 挂载事件 67 | if (isOn(key) && isFunction(props[key])) { 68 | el.addEventListener(key.slice(2).toLowerCase(), props[key]) 69 | } else { 70 | el.setAttribute(key, props[key]) 71 | } 72 | } 73 | 74 | // 处理子节点 子节点可能是 TEXT_CHILDREN 或 ARRAY_CHILDREN 75 | if (vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN) { 76 | // 获取el节点及其后代的文本内容并把子节点替换为文本 77 | el.textContent = children as string 78 | } else if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 79 | mountChildren(children as VNodeArrayChildren, el) 80 | } 81 | 82 | // 将el节点插入到容器节点的子末位 83 | container.append(el) 84 | } 85 | 86 | /** 挂载子节点 */ 87 | const mountChildren = (vnodeArray: VNodeArrayChildren, container: any) => 88 | vnodeArray.forEach((vnode) => patch(vnode as VNode, container)) 89 | 90 | /** 挂载组件 */ 91 | const mountComponent = (initialVNode: VNode, container: any) => { 92 | const instance = createComponentInstance(initialVNode) 93 | setupComponent(instance) 94 | setupRenderEffect(instance, container) 95 | } 96 | 97 | /** setupRenderEffect */ 98 | function setupRenderEffect(instance: ComponentInternalInstance, container: any) { 99 | const subTree = instance.render!.call(instance.proxy) as VNode 100 | patch(subTree, container) 101 | instance.vnode.el = subTree.el 102 | } 103 | -------------------------------------------------------------------------------- /src/runtime-core/src/vnode.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isString, ShapeFlags } from '../../shared/index' 2 | import { Component, Data } from './component' 3 | 4 | export type VNodeTypes = string | VNode | Component | typeof Fragment | typeof Text 5 | 6 | export interface VNode { 7 | type: VNodeTypes 8 | props: Record | null 9 | children: VNodeNormalizedChildren 10 | el: Node | null 11 | shapeFlag: ShapeFlags 12 | } 13 | 14 | type VNodeChildAtom = VNode | string | number | boolean | null | undefined | void 15 | export type VNodeArrayChildren = Array 16 | export type VNodeChildren = VNodeChildAtom | VNodeArrayChildren 17 | export type VNodeNormalizedChildren = 18 | | string 19 | | VNodeArrayChildren 20 | // | RawSlots 21 | | null 22 | 23 | export const Fragment = Symbol('Fragment') 24 | export const Text = Symbol('Text') 25 | 26 | /** 创建虚拟节点 */ 27 | export const createVNode = ( 28 | type: VNodeTypes, 29 | props: Data | null = null, 30 | children: VNodeNormalizedChildren = null, 31 | shapeFlag: ShapeFlags = initShapFlag(type) 32 | ): VNode => { 33 | const vnode = { 34 | type, 35 | props, 36 | children, 37 | el: null, 38 | shapeFlag 39 | } as VNode 40 | 41 | /* 子节点是文本时 标为 TEXT_CHILDREN 否则视为 ARRAY_CHILDREN */ 42 | vnode.shapeFlag |= isString(children) 43 | ? ShapeFlags.TEXT_CHILDREN 44 | : ShapeFlags.ARRAY_CHILDREN 45 | 46 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 47 | if (typeof children === 'object') { 48 | vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN 49 | } 50 | } 51 | 52 | return vnode 53 | } 54 | /** 初始化VNode的默认shapeFlags */ 55 | const initShapFlag = (type: VNodeTypes) => 56 | isString(type) 57 | ? ShapeFlags.ELEMENT 58 | : isFunction(type) 59 | ? ShapeFlags.FUNCTIONAL_COMPONENT 60 | : ShapeFlags.STATEFUL_COMPONENT 61 | 62 | export const createTextVNode = (text: string) => { 63 | return createVNode(Text, null, text) 64 | } 65 | -------------------------------------------------------------------------------- /src/shared/__tests__/base.spec.ts: -------------------------------------------------------------------------------- 1 | import { extend, hasChanged, objectToString } from '../src/base' 2 | 3 | describe('shared/base', () => { 4 | it('extend', () => { 5 | expect(extend).toEqual(Object.assign) 6 | }) 7 | it('objectToString', () => { 8 | expect(objectToString).toEqual(Object.prototype.toString) 9 | }) 10 | it('hasChanged', () => { 11 | const obj1 = { foo: 1 } 12 | const obj2 = { foo: 1 } 13 | const obj3 = extend(obj1, { bar: 1 }) 14 | expect(hasChanged(obj1, obj2)).toBe(true) 15 | expect(hasChanged(obj1, obj3)).toBe(false) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/shared/__tests__/isType.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isArray, 3 | isDate, 4 | isFunction, 5 | isMap, 6 | isNumber, 7 | isObject, 8 | isPlainObject, 9 | isSet, 10 | isString, 11 | isSymbol 12 | } from '../src/isType' 13 | 14 | describe('shared/isType', () => { 15 | const str = 'string' 16 | const num = 1 17 | const bool = false 18 | const symb = Symbol() 19 | const date = new Date() 20 | const obj = {} 21 | const arr: unknown = [] 22 | const set = new Set() 23 | const map = new Map() 24 | const func1 = () => {} 25 | function func2() {} 26 | 27 | it('isObject', () => { 28 | expect(isObject(str)).toBe(false) 29 | expect(isObject(num)).toBe(false) 30 | expect(isObject(bool)).toBe(false) 31 | expect(isObject(symb)).toBe(false) 32 | expect(isObject(null)).toBe(false) 33 | expect(isObject(void 0)).toBe(false) 34 | expect(isObject(date)).toBe(true) 35 | expect(isObject(obj)).toBe(true) 36 | expect(isObject(arr)).toBe(true) 37 | expect(isObject(set)).toBe(true) 38 | expect(isObject(map)).toBe(true) 39 | expect(isObject(func1)).toBe(false) 40 | expect(isObject(func2)).toBe(false) 41 | }) 42 | it('isPlainObject', () => { 43 | expect(isPlainObject(str)).toBe(false) 44 | expect(isPlainObject(bool)).toBe(false) 45 | expect(isPlainObject(num)).toBe(false) 46 | expect(isPlainObject(symb)).toBe(false) 47 | expect(isPlainObject(null)).toBe(false) 48 | expect(isPlainObject(void 0)).toBe(false) 49 | expect(isPlainObject(date)).toBe(false) 50 | expect(isPlainObject(obj)).toBe(true) 51 | expect(isPlainObject(arr)).toBe(false) 52 | expect(isPlainObject(set)).toBe(false) 53 | expect(isPlainObject(map)).toBe(false) 54 | expect(isPlainObject(func1)).toBe(false) 55 | expect(isPlainObject(func2)).toBe(false) 56 | }) 57 | it('isArray', () => { 58 | expect(isArray(str)).toBe(false) 59 | expect(isArray(num)).toBe(false) 60 | expect(isArray(bool)).toBe(false) 61 | expect(isArray(symb)).toBe(false) 62 | expect(isArray(null)).toBe(false) 63 | expect(isArray(void 0)).toBe(false) 64 | expect(isArray(date)).toBe(false) 65 | expect(isArray(obj)).toBe(false) 66 | expect(isArray(arr)).toBe(true) 67 | expect(isArray(set)).toBe(false) 68 | expect(isArray(map)).toBe(false) 69 | expect(isArray(func1)).toBe(false) 70 | expect(isArray(func2)).toBe(false) 71 | }) 72 | it('isMap', () => { 73 | expect(isMap(str)).toBe(false) 74 | expect(isMap(num)).toBe(false) 75 | expect(isMap(bool)).toBe(false) 76 | expect(isMap(symb)).toBe(false) 77 | expect(isMap(null)).toBe(false) 78 | expect(isMap(void 0)).toBe(false) 79 | expect(isMap(date)).toBe(false) 80 | expect(isMap(obj)).toBe(false) 81 | expect(isMap(arr)).toBe(false) 82 | expect(isMap(set)).toBe(false) 83 | expect(isMap(map)).toBe(true) 84 | expect(isMap(func1)).toBe(false) 85 | expect(isMap(func2)).toBe(false) 86 | }) 87 | it('isMap', () => { 88 | expect(isSet(str)).toBe(false) 89 | expect(isSet(num)).toBe(false) 90 | expect(isSet(bool)).toBe(false) 91 | expect(isSet(symb)).toBe(false) 92 | expect(isSet(null)).toBe(false) 93 | expect(isSet(void 0)).toBe(false) 94 | expect(isSet(date)).toBe(false) 95 | expect(isSet(obj)).toBe(false) 96 | expect(isSet(arr)).toBe(false) 97 | expect(isSet(set)).toBe(true) 98 | expect(isSet(map)).toBe(false) 99 | expect(isSet(func1)).toBe(false) 100 | expect(isSet(func2)).toBe(false) 101 | }) 102 | it('isString', () => { 103 | expect(isString(str)).toBe(true) 104 | expect(isString(num)).toBe(false) 105 | expect(isString(bool)).toBe(false) 106 | expect(isString(symb)).toBe(false) 107 | expect(isString(null)).toBe(false) 108 | expect(isString(void 0)).toBe(false) 109 | expect(isString(date)).toBe(false) 110 | expect(isString(obj)).toBe(false) 111 | expect(isString(arr)).toBe(false) 112 | expect(isString(set)).toBe(false) 113 | expect(isString(map)).toBe(false) 114 | expect(isString(func1)).toBe(false) 115 | expect(isString(func2)).toBe(false) 116 | }) 117 | it('isNumber', () => { 118 | expect(isNumber(str)).toBe(false) 119 | expect(isNumber(num)).toBe(true) 120 | expect(isNumber(bool)).toBe(false) 121 | expect(isNumber(symb)).toBe(false) 122 | expect(isNumber(null)).toBe(false) 123 | expect(isNumber(void 0)).toBe(false) 124 | expect(isNumber(date)).toBe(false) 125 | expect(isNumber(obj)).toBe(false) 126 | expect(isNumber(arr)).toBe(false) 127 | expect(isNumber(set)).toBe(false) 128 | expect(isNumber(map)).toBe(false) 129 | expect(isNumber(func1)).toBe(false) 130 | expect(isNumber(func2)).toBe(false) 131 | }) 132 | it('isSymbol', () => { 133 | expect(isSymbol(str)).toBe(false) 134 | expect(isSymbol(num)).toBe(false) 135 | expect(isSymbol(bool)).toBe(false) 136 | expect(isSymbol(symb)).toBe(true) 137 | expect(isSymbol(null)).toBe(false) 138 | expect(isSymbol(void 0)).toBe(false) 139 | expect(isSymbol(date)).toBe(false) 140 | expect(isSymbol(obj)).toBe(false) 141 | expect(isSymbol(arr)).toBe(false) 142 | expect(isSymbol(set)).toBe(false) 143 | expect(isSymbol(map)).toBe(false) 144 | expect(isSymbol(func1)).toBe(false) 145 | expect(isSymbol(func2)).toBe(false) 146 | }) 147 | it('isSymbol', () => { 148 | expect(isSymbol(str)).toBe(false) 149 | expect(isSymbol(num)).toBe(false) 150 | expect(isSymbol(bool)).toBe(false) 151 | expect(isSymbol(symb)).toBe(true) 152 | expect(isSymbol(null)).toBe(false) 153 | expect(isSymbol(void 0)).toBe(false) 154 | expect(isSymbol(date)).toBe(false) 155 | expect(isSymbol(obj)).toBe(false) 156 | expect(isSymbol(arr)).toBe(false) 157 | expect(isSymbol(set)).toBe(false) 158 | expect(isSymbol(map)).toBe(false) 159 | expect(isSymbol(func1)).toBe(false) 160 | expect(isSymbol(func2)).toBe(false) 161 | }) 162 | it('isDate', () => { 163 | expect(isDate(str)).toBe(false) 164 | expect(isDate(num)).toBe(false) 165 | expect(isDate(bool)).toBe(false) 166 | expect(isDate(symb)).toBe(false) 167 | expect(isDate(null)).toBe(false) 168 | expect(isDate(void 0)).toBe(false) 169 | expect(isDate(date)).toBe(true) 170 | expect(isDate(obj)).toBe(false) 171 | expect(isDate(arr)).toBe(false) 172 | expect(isDate(set)).toBe(false) 173 | expect(isDate(map)).toBe(false) 174 | expect(isDate(func1)).toBe(false) 175 | expect(isDate(func2)).toBe(false) 176 | }) 177 | it('isFunction', () => { 178 | expect(isFunction(str)).toBe(false) 179 | expect(isFunction(num)).toBe(false) 180 | expect(isFunction(bool)).toBe(false) 181 | expect(isFunction(symb)).toBe(false) 182 | expect(isFunction(null)).toBe(false) 183 | expect(isFunction(void 0)).toBe(false) 184 | expect(isFunction(date)).toBe(false) 185 | expect(isFunction(obj)).toBe(false) 186 | expect(isFunction(arr)).toBe(false) 187 | expect(isFunction(set)).toBe(false) 188 | expect(isFunction(map)).toBe(false) 189 | expect(isFunction(func1)).toBe(true) 190 | expect(isFunction(func2)).toBe(true) 191 | }) 192 | }) 193 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/base' 2 | export * from './src/isType' 3 | export * from './src/ShapFlags' 4 | -------------------------------------------------------------------------------- /src/shared/src/ShapFlags.ts: -------------------------------------------------------------------------------- 1 | import { extend } from './base' 2 | 3 | export const enum ShapeFlags { 4 | ELEMENT = 1, 5 | FUNCTIONAL_COMPONENT = 1 << 1, 6 | STATEFUL_COMPONENT = 1 << 2, 7 | TEXT_CHILDREN = 1 << 3, 8 | ARRAY_CHILDREN = 1 << 4, 9 | SLOT_CHILDREN = 1 << 5, 10 | COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/src/base.ts: -------------------------------------------------------------------------------- 1 | /** 对象属性合并 */ 2 | export const extend = Object.assign 3 | /** 对象自身属性中是否具有指定的属性 */ 4 | const hasOwnProperty = Object.prototype.hasOwnProperty 5 | export const hasOwn = (val: object, key: string | symbol): key is keyof typeof val => 6 | hasOwnProperty.call(val, key) 7 | /** 获取包装类型的字符串 Object.prototype.toString的别名 */ 8 | export const objectToString = Object.prototype.toString 9 | /** 判断有没有改变 */ 10 | export const hasChanged = (val: any, newVal: any): boolean => !Object.is(val, newVal) 11 | /** 空对象 */ 12 | export const EMPTY_OBJ: { readonly [key: string]: any } = {} 13 | /** 是on开头的事件 */ 14 | export const isOn = (key: string) => /^on[A-Z]/.test(key) 15 | 16 | /* -n 字符串驼峰化 */ 17 | export const camelize = (str: string) => 18 | str.replace(/-(\w)/g, (_, c: string) => { 19 | return c ? c.toUpperCase() : '' 20 | }) 21 | 22 | /* 首字母大写 */ 23 | const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1) 24 | 25 | /* 变成事件名称 */ 26 | export const toHandlerKey = (str: string) => (str ? 'on' + capitalize(str) : '') 27 | -------------------------------------------------------------------------------- /src/shared/src/isType.ts: -------------------------------------------------------------------------------- 1 | import { objectToString } from './base' 2 | 3 | /** 从字符串(如"[object RawType]")中提取"RawType" */ 4 | export const toRawType = (value: unknown): string => 5 | objectToString.call(value).slice(8, -1) // 通过 .call 指定要判断类型的value 6 | 7 | /** 判断是不是对象 */ 8 | export const isObject = (val: unknown): val is Record => 9 | !!val && typeof val === 'object' 10 | /** 判断是不是普通对象 */ 11 | export const isPlainObject = (val: unknown): val is object => 12 | toRawType(val) === 'Object' 13 | /** 判断是不是Array */ 14 | export const isArray = Array.isArray 15 | /** 判断是不是Map */ 16 | export const isMap = (val: unknown): val is Map => toRawType(val) === 'Map' 17 | /** 判断是不是Set */ 18 | export const isSet = (val: unknown): val is Set => toRawType(val) === 'Set' 19 | /** 判断是不是String */ 20 | export const isString = (val: unknown): val is string => typeof val === 'string' 21 | /** 判断是不是Number */ 22 | export const isNumber = (val: unknown): val is number => typeof val === 'number' 23 | /** 判断是不是Symbol */ 24 | export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol' 25 | /** 判断是不是Date */ 26 | export const isDate = (val: unknown): val is Date => val instanceof Date 27 | /** 判断是不是函数 */ 28 | export const isFunction = (val: unknown): val is Function => 29 | typeof val === 'function' 30 | -------------------------------------------------------------------------------- /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": "ES6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "DOM", 17 | "ESNext" 18 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 19 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 20 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 25 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | 29 | /* Modules */ 30 | "module": "ESNext" /* Specify what module code is generated. */, 31 | // "rootDir": "./", /* Specify the root folder within your source files. */ 32 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 33 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 34 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 37 | "types": [ 38 | "node", 39 | "jest" 40 | ] /* Specify type package names to be included without being referenced in a source file. */, 41 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files */ 43 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 44 | 45 | /* JavaScript Support */ 46 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 47 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 48 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 49 | 50 | /* Emit */ 51 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 52 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 53 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 54 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 55 | // "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. */ 56 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 57 | // "removeComments": true, /* Disable emitting comments. */ 58 | // "noEmit": true, /* Disable emitting files from a compilation. */ 59 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 60 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 61 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 62 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 63 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 64 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 65 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 66 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 67 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 68 | "stripInternal": true /* Disable emitting declarations that have `@internal` in their JSDoc comments. */, 69 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 70 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 71 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 72 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 80 | 81 | /* Type Checking */ 82 | "strict": true /* Enable all strict type-checking options. */, 83 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 84 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 85 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 86 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 87 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 88 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 89 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 91 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 96 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 101 | 102 | /* Completeness */ 103 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 104 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 105 | } 106 | } 107 | --------------------------------------------------------------------------------