├── .gitignore ├── README.md ├── src ├── index.ts ├── reactivity │ ├── src │ │ ├── dep.ts │ │ ├── computed.ts │ │ ├── reactive.ts │ │ ├── baseHandlers.ts │ │ ├── ref.ts │ │ └── effect.ts │ └── __tests__ │ │ ├── shallowReadonly.test.ts │ │ ├── reactive.test.ts │ │ ├── computed.test.ts │ │ ├── readonly.test.ts │ │ ├── ref.test.ts │ │ └── effect.test.ts ├── global.d.ts ├── runtime-core │ ├── componentProps.ts │ ├── h.ts │ ├── index.ts │ ├── helpers │ │ └── renderSlots.ts │ ├── componentEmit.ts │ ├── createApp.ts │ ├── componentPublicInstance.ts │ ├── componentSlots.ts │ ├── vnode.ts │ ├── component.ts │ └── render.ts └── shared │ ├── ShapeFlags.ts │ └── index.ts ├── jest.config.js ├── babel.config.js ├── tsconfig.type.json ├── .vscode └── launch.json ├── example ├── helloworld │ ├── main.js │ ├── Foo.js │ ├── index.html │ └── App.js ├── componentEmit │ ├── main.js │ ├── index.html │ ├── App.js │ └── Foo.js ├── currentInstance │ ├── main.js │ ├── Foo.js │ ├── App.js │ └── index.html └── vue.html ├── rollup.config.js ├── tsconfig.json ├── package.json ├── LICENSE └── lib ├── my-vue.esm.js └── my-vue.cjs.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # my-vue3 2 | 学习vue3.0源码 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // 项目出口文件 2 | export * from './runtime-core'; 3 | -------------------------------------------------------------------------------- /src/reactivity/src/dep.ts: -------------------------------------------------------------------------------- 1 | export function createDep() { 2 | return new Set(); 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Debug { 2 | mainPath: (string) => any; 3 | } 4 | 5 | declare var debug: Debug; 6 | -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance, props) { 2 | instance.props = props || {}; 3 | } 4 | -------------------------------------------------------------------------------- /src/runtime-core/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from './vnode'; 2 | export function h(type, props?, children?) { 3 | return createVNode(type, props, children); 4 | } 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /tsconfig.type.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "./types", 6 | "emitDeclarationOnly": true 7 | } 8 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [] 7 | } -------------------------------------------------------------------------------- /example/helloworld/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.js'; 2 | import { createApp } from './../../lib/my-vue.esm.js'; 3 | // const rootContainer = document.getElementById('app'); 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /example/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.js'; 2 | import { createApp } from './../../lib/my-vue.esm.js'; 3 | // const rootContainer = document.getElementById('app'); 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /example/currentInstance/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.js'; 2 | import { createApp } from './../../lib/my-vue.esm.js'; 3 | // const rootContainer = document.getElementById('app'); 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /src/shared/ShapeFlags.ts: -------------------------------------------------------------------------------- 1 | export const enum ShapeFlags { 2 | ELEMENT = 1, // 0001 3 | STATEFUL_COMPONENT = 1 << 1, // 0010 4 | TEXT_CHILDREN = 1 << 2, // 0100 5 | ARRAY_CHILDREN = 1 << 3, // 1000 6 | SLOTS_CHILDREN = 1 << 4, // 10000 7 | } 8 | -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | export { createApp } from './createApp'; 2 | export { h } from './h'; 3 | export { renderSlots } from './helpers/renderSlots'; 4 | export { createTextVNode } from './vnode'; 5 | export { getCurrentInstance } from './component'; 6 | -------------------------------------------------------------------------------- /src/runtime-core/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, Fragment } from './../vnode'; 2 | export function renderSlots(slots, name, props) { 3 | const slot = slots[name]; 4 | if (slot) { 5 | return createVNode(Fragment, {}, slot(props)); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/helloworld/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/my-vue.esm.js'; 2 | 3 | export const Foo = { 4 | setup(props) { 5 | console.log('foo props = ', props); 6 | }, 7 | render() { 8 | return h('div', {}, 'count = ' + this.count); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { toHandleKey } from '../shared'; 2 | 3 | export function emit(instance, event: string, ...args) { 4 | console.log('instance ==== ', instance); 5 | const { props } = instance; 6 | 7 | const handleName = toHandleKey(event); 8 | const handle = props[handleName]; 9 | handle && handle(...args); 10 | } 11 | -------------------------------------------------------------------------------- /example/currentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots, getCurrentInstance } from '../../lib/my-vue.esm.js'; 2 | export const Foo = { 3 | naem: 'Foo', 4 | setup(props, { emit }) { 5 | console.log('foo instance = ', getCurrentInstance()); 6 | return {}; 7 | }, 8 | render() { 9 | const foo = h('p', {}, 'Foo'); 10 | return h('div', {}, [foo]); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import pkg from './package.json'; 3 | export default { 4 | input: './src/index.ts', 5 | output: [ 6 | // 1. cjs 7 | // 2. esm 8 | { 9 | format: 'cjs', 10 | file: pkg.main, 11 | }, 12 | { 13 | format: 'es', 14 | file: pkg.module, 15 | }, 16 | ], 17 | plugins: [typescript()], 18 | }; 19 | -------------------------------------------------------------------------------- /example/currentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from '../../lib/my-vue.esm.js'; 2 | import { Foo } from './Foo.js'; 3 | export default { 4 | name: 'App', 5 | setup() { 6 | console.log('app instance = ', getCurrentInstance()); 7 | }, 8 | render() { 9 | const app = h('div', {}, 'app'); 10 | const foo = h(Foo, {}, 'foo'); 11 | return h('div', {}, [app, foo]); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /example/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/currentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/runtime-core/createApp.ts: -------------------------------------------------------------------------------- 1 | import { render } from './render'; 2 | import { createVNode } from './vnode'; 3 | 4 | export function createApp(rootComponent) { 5 | return { 6 | mount(rootContainer) { 7 | rootContainer = document.querySelector(rootContainer); 8 | // 根据根组件创建虚拟节点 9 | // 所有的逻辑操作都是根据虚拟节点来进行 10 | const vnode = createVNode(rootComponent); 11 | 12 | render(vnode, rootContainer); 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "moduleResolution": "node", 5 | "esModuleInterop": true, 6 | "target": "es2016", 7 | "module": "esnext", 8 | "noImplicitAny": false, 9 | "removeComments": true, 10 | "preserveConstEnums": true, 11 | "sourceMap": true, 12 | "downlevelIteration": true, 13 | "lib": ["es6", "DOM"] 14 | }, 15 | "include": ["src/index.ts", "src/global.d.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /example/componentEmit/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/my-vue.esm.js'; 2 | import { Foo } from './Foo.js'; 3 | 4 | export default { 5 | name: 'App', 6 | setup() {}, 7 | render() { 8 | return h('div', {}, [ 9 | h('div', {}, 'App'), 10 | h(Foo, { 11 | foo: 'bar', 12 | onAdd: (a, b) => { 13 | console.log('app add click', a, b); 14 | }, 15 | onAddFoo: (a) => { 16 | console.log('app add foo click', a); 17 | }, 18 | }), 19 | ]); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /example/componentEmit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/my-vue.esm.js'; 2 | export const Foo = { 3 | naem: 'Foo', 4 | setup(props, { emit }) { 5 | const emitAdd = () => { 6 | console.log('foo emit click'); 7 | emit('add', 1, 2); 8 | emit('add-foo', 'haha'); 9 | }; 10 | return { 11 | emitAdd, 12 | }; 13 | }, 14 | render() { 15 | const btn = h( 16 | 'button', 17 | { 18 | onClick: this.emitAdd, 19 | }, 20 | 'emitAdd' 21 | ); 22 | 23 | const foo = h('p', {}, 'Foo' + this.foo); 24 | 25 | return h('div', {}, [foo, btn]); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from './../shared/index'; 2 | const publicPropertiesMap = { 3 | $el: (i) => i.vnode.el, 4 | $slots: (i) => i.slots, 5 | }; 6 | export const PublicInstanceProxyHandlers = { 7 | get({ _: instance }, key) { 8 | const { setupState, props } = instance; 9 | 10 | if (hasOwn(setupState, key)) { 11 | return setupState[key]; 12 | } else if (hasOwn(props, key)) { 13 | return props[key]; 14 | } 15 | const publicGetter = publicPropertiesMap[key]; 16 | if (publicGetter) { 17 | return publicGetter(instance); 18 | } 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /example/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from '../shared/ShapeFlags'; 2 | 3 | export function initSlots(instance, children) { 4 | console.log('initSlots instance = ', instance); 5 | const { vnode } = instance; 6 | if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) { 7 | normalizeObjectSlots(children, instance.slots); 8 | } 9 | } 10 | function normalizeObjectSlots(children: any, slots: any) { 11 | for (let key in children) { 12 | let slot = children[key]; 13 | slots[key] = (props) => normalizeSlotsValue(slot(props)); 14 | } 15 | } 16 | function normalizeSlotsValue(slot) { 17 | return Array.isArray(slot) ? slot : [slot]; 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export function isObject(value) { 2 | return value !== null && typeof value === 'object'; 3 | } 4 | export function hasOwn(obj: Object, key) { 5 | return obj.hasOwnProperty(key); 6 | } 7 | 8 | export const capitalize = (eventName: string) => { 9 | return eventName 10 | ? eventName.charAt(0).toUpperCase() + eventName.slice(1) 11 | : ''; 12 | }; 13 | 14 | const camelize = (eventName: string) => { 15 | return eventName 16 | ? eventName.replace(/-(\w)/g, (_, c) => { 17 | return c ? c.toUpperCase() : ''; 18 | }) 19 | : ''; 20 | }; 21 | export const toHandleKey = (eventName: string) => { 22 | return eventName ? 'on' + camelize(capitalize(eventName)) : ''; 23 | }; 24 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/shallowReadonly.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isReactive, 3 | isReadonly, 4 | readonly, 5 | shallowReadonly, 6 | } from '../src/reactive'; 7 | 8 | describe('shallowReadonly', () => { 9 | test('should not make non-reactive properties reactive', () => { 10 | const props = shallowReadonly({ n: { foo: 1 } }); 11 | expect(isReactive(props.n)).toBe(false); 12 | }); 13 | test('should differentiate from normal readonly calls', async () => { 14 | const original = { foo: { bar: 1 } }; 15 | const shallowProxy = shallowReadonly(original); 16 | const reactiveProxy = readonly(original); 17 | expect(shallowProxy).not.toBe(reactiveProxy); 18 | expect(isReadonly(shallowProxy.foo)).toBe(false); 19 | expect(isReadonly(reactiveProxy.foo)).toBe(true); 20 | reactiveProxy.foo.bar = 2; 21 | expect(isReadonly(shallowProxy.foo)).toBe(false); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /example/helloworld/App.js: -------------------------------------------------------------------------------- 1 | import { h } from './../../lib/my-vue.esm.js'; 2 | import { Foo } from './Foo.js'; 3 | window.self = null; 4 | export default { 5 | render() { 6 | window.self = this; 7 | // return h( 8 | // 'div', 9 | // { 10 | // id: 'root', 11 | // class: ['red', 'blue'], 12 | // onClick: () => { 13 | // console.log('click'); 14 | // }, 15 | // onMouseenter: () => { 16 | // console.log('onMouseenter'); 17 | // }, 18 | // }, 19 | // 'msg: ' + this.msg 20 | // ); 21 | return h('div', { id: 'root', class: ['red', 'blue'] }, [ 22 | h('div', { class: 'yellow' }, 'this is yellow' + this.msg), 23 | // h('p', { class: 'green' }, 'this is p'), 24 | h(Foo, { count: 1 }), 25 | ]); 26 | }, 27 | setup() { 28 | return { 29 | msg: 'hello vue', 30 | }; 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/reactivity/src/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from './effect'; 2 | 3 | class ComputedRefImpl { 4 | private _getter: Function; 5 | private _value: any; 6 | private _dirty: boolean = true; 7 | private _effect: ReactiveEffect; 8 | constructor(getter) { 9 | this._getter = getter; 10 | // 将依赖收集起来 11 | this._effect = new ReactiveEffect(this._getter, () => { 12 | // scheduller 的作用是当下次触发依赖时执行此函数,而不执行传进来的getter函数 13 | if (!this._dirty) { 14 | // 这里更改_dirty的目的是当响应式对象的值发生改变时,更改_dirty为true,达到重新执行getter函数的目的 15 | this._dirty = true; 16 | } 17 | }); 18 | } 19 | get value() { 20 | // _dirty控制多次访问值的时候只执行一次getter函数 21 | if (this._dirty) { 22 | // this._value = this._getter(); 23 | // 用依赖来控制执行getter的执行 24 | this._value = this._effect.run(); 25 | this._dirty = false; 26 | } 27 | return this._value; 28 | } 29 | } 30 | export function computed(getter) { 31 | return new ComputedRefImpl(getter); 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-vue3", 3 | "version": "1.0.0", 4 | "description": "学习vue3.0源码", 5 | "main": "lib/my-vue.cjs.js", 6 | "module": "lib/my-vue.esm.js", 7 | "scripts": { 8 | "test": "jest", 9 | "build": "rollup -c rollup.config.js --watch" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/CH0918/my-vue3.git" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/CH0918/my-vue3/issues" 20 | }, 21 | "homepage": "https://github.com/CH0918/my-vue3#readme", 22 | "devDependencies": { 23 | "@babel/core": "^7.15.0", 24 | "@babel/preset-env": "^7.15.0", 25 | "@babel/preset-typescript": "^7.15.0", 26 | "@rollup/plugin-typescript": "^8.3.1", 27 | "@types/jest": "^27.0.0", 28 | "babel-jest": "^27.0.6", 29 | "jest": "^27.1.1", 30 | "rollup": "^2.69.1", 31 | "ts-jest": "^27.0.5", 32 | "tslib": "^2.3.1", 33 | "typescript": "^4.4.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 HuangDongJiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from '../shared/ShapeFlags'; 2 | export const Fragment = Symbol('Fragment'); 3 | export const Text = Symbol('Text'); 4 | export function createVNode(type, props?, children?) { 5 | const vnode = { 6 | type, 7 | props, 8 | children, 9 | el: null, 10 | shapeFlag: getShapeFlag(type), 11 | }; 12 | if (typeof vnode.children === 'string') { 13 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN; 14 | } else if (Array.isArray(vnode.children)) { 15 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN; 16 | } 17 | normalizeChildren(vnode, children); 18 | return vnode; 19 | } 20 | export function createTextVNode(text) { 21 | return createVNode(Text, {}, text); 22 | } 23 | function getShapeFlag(type: any) { 24 | // type -> string : 元素 25 | // type -> object : 说明 是一个组件对象 26 | return typeof type === 'string' 27 | ? ShapeFlags.ELEMENT 28 | : ShapeFlags.STATEFUL_COMPONENT; 29 | } 30 | function normalizeChildren(vnode, children) { 31 | if (typeof children === 'object') { 32 | if (vnode.shapeFlag & ShapeFlags.ELEMENT) { 33 | } else { 34 | // 渲染组件类型,那么children只能是插槽的形式插进来 35 | vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/vue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 |
12 | computedValue: {{ reactiveObj.age }} 13 | 14 |
15 | 16 | 41 | 42 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/reactive.test.ts: -------------------------------------------------------------------------------- 1 | import { reactive, isReactive } from '../src/reactive'; 2 | import { effect } from '../src/effect'; 3 | describe('reactive', () => { 4 | test('Object', () => { 5 | const original = { foo: 1 }; 6 | const observed = reactive(original); 7 | expect(observed).not.toBe(original); 8 | expect(isReactive(observed)).toBe(true); 9 | expect(isReactive(original)).toBe(false); 10 | // get 11 | expect(observed.foo).toBe(1); 12 | // // has 13 | expect('foo' in observed).toBe(true); 14 | // // ownKeys 15 | expect(Object.keys(observed)).toEqual(['foo']); 16 | }); 17 | 18 | test('nested reactives', () => { 19 | const original = { 20 | nested: { 21 | foo: 1, 22 | }, 23 | array: [{ bar: 2 }], 24 | }; 25 | const observed = reactive(original); 26 | expect(isReactive(observed.nested)).toBe(true); 27 | expect(isReactive(observed.array)).toBe(true); 28 | expect(isReactive(observed.array[0])).toBe(true); 29 | }); 30 | 31 | // test('toRaw', () => { 32 | // const original = { foo: 1 }; 33 | // const observed = reactive(original); 34 | // expect(toRaw(observed)).toBe(original); 35 | // expect(toRaw(original)).toBe(original); 36 | // }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/computed.test.ts: -------------------------------------------------------------------------------- 1 | import { computed } from './../src/computed'; 2 | import { reactive } from '../src/reactive'; 3 | 4 | describe('computed', () => { 5 | it('happy path', () => { 6 | const value = reactive({ 7 | foo: 1, 8 | }); 9 | 10 | const getter = computed(() => { 11 | return value.foo; 12 | }); 13 | 14 | value.foo = 2; 15 | expect(getter.value).toBe(2); 16 | }); 17 | 18 | it('should compute lazily', () => { 19 | const value = reactive({ 20 | foo: 1, 21 | }); 22 | const getter = jest.fn(() => { 23 | return value.foo; 24 | }); 25 | const cValue = computed(getter); 26 | 27 | // lazy 28 | expect(getter).not.toHaveBeenCalled(); 29 | 30 | expect(cValue.value).toBe(1); 31 | expect(getter).toHaveBeenCalledTimes(1); 32 | 33 | // should not compute again 34 | cValue.value; 35 | expect(getter).toHaveBeenCalledTimes(1); 36 | 37 | // should not compute until needed 38 | value.foo = 2; 39 | expect(getter).toHaveBeenCalledTimes(1); 40 | 41 | // // now it should compute 42 | expect(cValue.value).toBe(2); 43 | expect(getter).toHaveBeenCalledTimes(2); 44 | 45 | // should not compute again 46 | cValue.value; 47 | expect(getter).toHaveBeenCalledTimes(2); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/reactivity/src/reactive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mutableHandlers, 3 | readonlyHandlers, 4 | shallowReadonlyHandlers, 5 | } from './baseHandlers'; 6 | // export const targetMap = new Map(); 7 | export const proxyMap = new WeakMap(); 8 | export const enum ReactiveFlags { 9 | IS_REACTIVE = '__v_isReactive', 10 | IS_READONLY = '_v_isReadOnly', 11 | } 12 | export function reactive(target) { 13 | return createReactiveObject(target, proxyMap, mutableHandlers); 14 | } 15 | export function readonly(target) { 16 | return createReactiveObject(target, proxyMap, readonlyHandlers); 17 | } 18 | export function isReadonly(value) { 19 | return !!value[ReactiveFlags.IS_READONLY]; 20 | } 21 | export function shallowReadonly(target) { 22 | return createReactiveObject(target, proxyMap, shallowReadonlyHandlers); 23 | } 24 | export function isProxy(value) { 25 | return isReadonly(value) || isReactive(value); 26 | } 27 | 28 | export function createReactiveObject(target, proxyMap, baseHandlers) { 29 | let reactiveProxy; 30 | // 缓存proxy对象 31 | if (reactiveProxy) { 32 | reactiveProxy = proxyMap[target]; 33 | return reactiveProxy; 34 | } 35 | reactiveProxy = new Proxy(target, baseHandlers); 36 | proxyMap.set(target, reactiveProxy); 37 | return reactiveProxy; 38 | } 39 | 40 | export function isReactive(value) { 41 | // 普通对象没有该属性,返回undefind !!转为fales 42 | return !!value[ReactiveFlags.IS_REACTIVE]; 43 | } 44 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/readonly.test.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReactive, isReadonly, readonly } from '../src/reactive'; 2 | 3 | describe('readonly', () => { 4 | it('readonly', () => { 5 | const original = { foo: 1, bar: { baz: 2 } }; 6 | const wrapped = readonly(original); 7 | expect(wrapped).not.toBe(original); 8 | let num = 0; 9 | num = wrapped.foo + 1; 10 | expect(num).toBe(2); 11 | wrapped.foo++; 12 | expect(wrapped.foo).toBe(1); 13 | }); 14 | test('isReadonly', () => { 15 | const original = { foo: 1, bar: { baz: 2 } }; 16 | const wrapped = readonly(original); 17 | expect(isReadonly(wrapped)).toBe(true); 18 | expect(isReadonly(wrapped.bar)).toBe(true); 19 | expect(isReadonly(original)).toBe(false); 20 | }); 21 | it('should make nested values readonly', () => { 22 | const original = { foo: 1, bar: { baz: 2 } }; 23 | const wrapped = readonly(original); 24 | expect(wrapped).not.toBe(original); 25 | expect(isProxy(wrapped)).toBe(true); 26 | expect(isReactive(wrapped)).toBe(false); 27 | expect(isReadonly(wrapped)).toBe(true); 28 | expect(isReactive(original)).toBe(false); 29 | expect(isReadonly(original)).toBe(false); 30 | expect(isReactive(wrapped.bar)).toBe(false); 31 | expect(isReadonly(wrapped.bar)).toBe(true); 32 | expect(isReactive(original.bar)).toBe(false); 33 | expect(isReadonly(original.bar)).toBe(false); 34 | // get 35 | expect(wrapped.foo).toBe(1); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/reactivity/src/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { track, trigger } from './effect'; 2 | import { 3 | isReactive, 4 | isReadonly, 5 | readonly, 6 | reactive, 7 | ReactiveFlags, 8 | isProxy, 9 | } from './reactive'; 10 | import { isObject } from '../../shared'; 11 | const get = createGetter(); 12 | const set = createSetter(); 13 | const readonlyGet = createGetter(true); 14 | const shallowReadonlyGet = createGetter(true, true); 15 | export const mutableHandlers = { 16 | get, 17 | set, 18 | }; 19 | export const readonlyHandlers = { 20 | get: readonlyGet, 21 | set: function (target, key, value) { 22 | // readonly 的响应式对象不可以修改值 23 | console.warn( 24 | `Set operation on key "${String(key)}" failed: target is readonly.`, 25 | target 26 | ); 27 | return true; 28 | }, 29 | }; 30 | export const shallowReadonlyHandlers = Object.assign({}, readonlyHandlers, { 31 | get: shallowReadonlyGet, 32 | }); 33 | export function createGetter(isReadonly = false, isShallowReadonly = false) { 34 | return function get(target, key) { 35 | // 兼容isReactive方法 36 | if (key === ReactiveFlags.IS_REACTIVE) { 37 | return !isReadonly; 38 | } 39 | if (key === ReactiveFlags.IS_READONLY) { 40 | return isReadonly; 41 | } 42 | const res = Reflect.get(target, key); 43 | if (isShallowReadonly) { 44 | return res; 45 | } 46 | if (isObject(res)) { 47 | return isReadonly ? readonly(res) : reactive(res); 48 | } 49 | // 收集依赖 50 | track(target, key); 51 | return res; 52 | }; 53 | } 54 | export function createSetter() { 55 | return function set(target, key, value) { 56 | let res = Reflect.set(target, key, value); 57 | trigger(target, key); 58 | return res; 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/reactivity/src/ref.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '../../shared'; 2 | import { createDep } from './dep'; 3 | import { isTracking, trackEffect, triggerEffect } from './effect'; 4 | import { reactive } from './reactive'; 5 | class RefImpl { 6 | private _value; 7 | // dep = new Set(); 8 | dep = createDep(); 9 | __v_isRef_ = true; 10 | constructor(value) { 11 | this._value = convert(value); 12 | } 13 | get value() { 14 | trackRefValue(this); 15 | return this._value; 16 | } 17 | set value(newValue) { 18 | if (!hasChange(this.value, newValue)) return; 19 | this._value = convert(newValue); 20 | triggerEffect(this.dep); 21 | } 22 | } 23 | function trackRefValue(ref) { 24 | if (isTracking()) { 25 | trackEffect(ref.dep); 26 | } 27 | } 28 | function convert(value) { 29 | return isObject(value) ? reactive(value) : value; 30 | } 31 | export function hasChange(value, newValue) { 32 | return !Object.is(value, newValue); 33 | } 34 | export function ref(value) { 35 | return new RefImpl(value); 36 | } 37 | export function isRef(value) { 38 | return !!value.__v_isRef_; 39 | } 40 | export function unRef(value) { 41 | return isRef(value) ? value.value : value; 42 | } 43 | const shallowUnwrapHandlers = { 44 | get(target, key) { 45 | return unRef(Reflect.get(target, key)); 46 | }, 47 | set(target, key, value) { 48 | // 1.old -> ref; new -> !ref 需要.value再赋值 49 | // 2.old -> ref; new -> ref 50 | // 3.old -> !ref; new -> ref 51 | // 4.old -> !ref; new -> !ref 52 | const oldValue = target[key]; 53 | if (isRef(oldValue) && !isRef(value)) { 54 | return (target[key].value = value); 55 | } else { 56 | return Reflect.set(target, key, value); 57 | } 58 | }, 59 | }; 60 | export function proxyRefs(objectWithRefs) { 61 | return new Proxy(objectWithRefs, shallowUnwrapHandlers); 62 | } 63 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/ref.test.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../src/effect'; 2 | import { reactive } from '../src/reactive'; 3 | import { ref, proxyRefs, isRef, unRef } from '../src/ref'; 4 | describe('ref', () => { 5 | it('should be reactive', () => { 6 | const a = ref(1); 7 | expect(a.value).toBe(1); 8 | let dummy; 9 | let calls = 0; 10 | effect(() => { 11 | calls++; 12 | dummy = a.value; 13 | }); 14 | expect(calls).toBe(1); 15 | expect(dummy).toBe(1); 16 | a.value = 2; 17 | expect(calls).toBe(2); 18 | expect(dummy).toBe(2); 19 | // same value should not trigger 20 | a.value = 2; 21 | expect(calls).toBe(2); 22 | expect(dummy).toBe(2); 23 | }); 24 | 25 | it('should make nested properties reactive', () => { 26 | const a = ref({ 27 | count: 1, 28 | }); 29 | let dummy; 30 | effect(() => { 31 | dummy = a.value.count; 32 | }); 33 | expect(dummy).toBe(1); 34 | a.value.count = 2; 35 | expect(dummy).toBe(2); 36 | }); 37 | 38 | it('proxyRefs', () => { 39 | const user = { 40 | age: ref(10), 41 | name: 'xiaohong', 42 | }; 43 | const proxyUser = proxyRefs(user); 44 | expect(user.age.value).toBe(10); 45 | expect(proxyUser.age).toBe(10); 46 | expect(proxyUser.name).toBe('xiaohong'); 47 | 48 | (proxyUser as any).age = 20; 49 | expect(proxyUser.age).toBe(20); 50 | expect(user.age.value).toBe(20); 51 | 52 | proxyUser.age = ref(10); 53 | expect(proxyUser.age).toBe(10); 54 | expect(user.age.value).toBe(10); 55 | }); 56 | 57 | it('isRef', () => { 58 | const a = ref(1); 59 | const user = reactive({ 60 | age: 1, 61 | }); 62 | expect(isRef(a)).toBe(true); 63 | expect(isRef(1)).toBe(false); 64 | expect(isRef(user)).toBe(false); 65 | }); 66 | 67 | it('unRef', () => { 68 | const a = ref(1); 69 | expect(unRef(a)).toBe(1); 70 | expect(unRef(1)).toBe(1); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import { PublicInstanceProxyHandlers } from './componentPublicInstance'; 2 | import { initProps } from './componentProps'; 3 | import { shallowReadonly } from './../reactivity/src/reactive'; 4 | import { emit } from './componentEmit'; 5 | import { initSlots } from './componentSlots'; 6 | export function createComponentInstance(vnode) { 7 | const component = { 8 | vnode, 9 | type: vnode.type, 10 | proxy: {}, 11 | ctx: {}, 12 | // setupState,render 13 | setupState: {}, 14 | props: {}, 15 | slots: {}, 16 | emit: () => {}, 17 | }; 18 | component.emit = emit as any; 19 | component.ctx = { 20 | _: component, 21 | }; 22 | return component; 23 | } 24 | 25 | export function setupComponent(instance) { 26 | // A 组件 传props 到 B组件 27 | initProps(instance, instance.vnode.props); 28 | // 处理插槽 29 | initSlots(instance, instance.vnode.children); 30 | // 初始化有状态的组件,相对于无状态的函数组件来说的 31 | setupStatefulComponent(instance); 32 | } 33 | let currentInstance = null; 34 | function setupStatefulComponent(instance: any) { 35 | const component = instance.type; 36 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 37 | const { setup } = component; 38 | 39 | if (setup) { 40 | setCurrentInstance(instance); 41 | // setupResult -> function: render函数,Object 42 | const setupResult = setup(shallowReadonly(instance.props), { 43 | // 此处不需要用户传入instance, 只需要传入事件名称即可 44 | emit: instance.emit.bind(null, instance), 45 | }); 46 | setCurrentInstance(null); 47 | handleSetupResult(instance, setupResult); 48 | } 49 | } 50 | function handleSetupResult(instance, setupResult: any) { 51 | // setupResult 是Object的情况 todo function 52 | if (typeof setupResult === 'object') { 53 | instance.setupState = setupResult; 54 | } 55 | finishComponentSetup(instance); 56 | } 57 | 58 | function finishComponentSetup(instance: any) { 59 | // instance.type -> App 组件 60 | const component = instance.type; 61 | // 把render函数挂到组件实例上 62 | if (component.render) { 63 | instance.render = component.render; 64 | } 65 | } 66 | 67 | export function getCurrentInstance() { 68 | return currentInstance; 69 | } 70 | export function setCurrentInstance(instance) { 71 | currentInstance = instance; 72 | } 73 | -------------------------------------------------------------------------------- /src/reactivity/src/effect.ts: -------------------------------------------------------------------------------- 1 | // import { targetMap } from './reactive'; 2 | 3 | import { createDep } from './dep'; 4 | 5 | let activeEffect; 6 | let shouldTrack = false; 7 | export const targetMap = new WeakMap(); 8 | export class ReactiveEffect { 9 | private _fn; 10 | // public scheduler: Function | undefined; 11 | active = true; 12 | deps = []; 13 | public onStop?: () => void; 14 | 15 | constructor(fn, public scheduler?) { 16 | this._fn = fn; 17 | // this.scheduler = scheduler; 18 | } 19 | run() { 20 | // 触发effect中的fn,将收集依赖开关打开 21 | shouldTrack = true; 22 | // 执行effect的fn同时,把当前的effect实例抛出去 23 | activeEffect = this; 24 | const result = this._fn(); 25 | 26 | // 执行完fn 意味着依赖已收集完毕,重置状态 27 | shouldTrack = false; 28 | activeEffect = undefined; 29 | return result; 30 | } 31 | stop() { 32 | if (this.active) { 33 | cleanupEffect(this); 34 | this.active = false; 35 | } 36 | if (this.onStop) { 37 | this.onStop(); 38 | } 39 | } 40 | } 41 | // 清除依赖 42 | function cleanupEffect(effect) { 43 | // 清空所有的依赖 44 | effect.deps.forEach((dep: any) => { 45 | dep.delete(effect); 46 | }); 47 | } 48 | 49 | export function isTracking() { 50 | return shouldTrack && activeEffect !== undefined; 51 | } 52 | export function effect(fn, options: any = {}) { 53 | const __effect = new ReactiveEffect(fn); 54 | Object.assign(__effect, options); 55 | __effect.run(); 56 | let runner: any = __effect.run.bind(__effect); 57 | runner.effect = __effect; 58 | return runner; 59 | } 60 | export function stop(runner) { 61 | runner.effect.stop(); 62 | } 63 | // 收集依赖 64 | export function track(target, key) { 65 | if (!isTracking()) return; 66 | // targetMap -> key: target, value: deps 67 | // depsMap -> key: key, value: dep 68 | // dep: effect set 69 | // [target: [key: [effect, effect]]] 70 | let depsMap = targetMap.get(target); 71 | if (!depsMap) { 72 | depsMap = new Map(); 73 | targetMap.set(target, depsMap); 74 | } 75 | let dep = depsMap.get(key); 76 | if (!dep) { 77 | // dep = new Set(); 78 | dep = createDep(); 79 | depsMap.set(key, dep); 80 | } 81 | trackEffect(dep); 82 | } 83 | export function trackEffect(dep) { 84 | dep.add(activeEffect); 85 | // 反向收集,每个effet持有所有的依赖 86 | activeEffect.deps.push(dep); 87 | } 88 | // 触发依赖 89 | export function trigger(target, key) { 90 | let depsMap = targetMap.get(target); 91 | if (!depsMap) return; 92 | let dep = depsMap.get(key); 93 | triggerEffect(dep); 94 | } 95 | export function triggerEffect(dep) { 96 | for (const effect of dep) { 97 | if (effect.scheduler) { 98 | effect.scheduler(); 99 | } else { 100 | effect.run(); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/runtime-core/render.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from '../shared/ShapeFlags'; 2 | import { createComponentInstance, setupComponent } from './component'; 3 | import { Fragment, Text } from './vnode'; 4 | 5 | export function render(vnode, container) { 6 | console.log('render...', vnode); 7 | patch(vnode, container); 8 | } 9 | function patch(vnode, container) { 10 | // 处理组件类型 11 | const { shapeFlag, type } = vnode; 12 | switch (type) { 13 | // 处理插槽多出一个没用的元素这种情况 14 | case Fragment: 15 | processFragment(vnode, container); 16 | break; 17 | case Text: 18 | processText(vnode, container); 19 | break; 20 | default: 21 | // 通过位运算符来控制 22 | // 组件类型 : 0010 23 | // 元素类型 : 0001 24 | if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 25 | console.log('处理组件 vnode', vnode); 26 | processComponent(vnode, container); 27 | } else if (shapeFlag & ShapeFlags.ELEMENT) { 28 | processElement(vnode, container); 29 | } 30 | break; 31 | } 32 | } 33 | 34 | function processFragment(vnode, container) { 35 | mountChildren(vnode, container); 36 | } 37 | function processText(vnode: any, container: any) { 38 | const { children } = vnode; 39 | const textContent = (vnode.el = document.createTextNode(children)); 40 | container.append(textContent); 41 | } 42 | // 处理组件 43 | function processComponent(initialVnode: any, container: any) { 44 | // 挂载节点 45 | mountComponent(initialVnode, container); 46 | } 47 | function mountComponent(initialVnode: any, container) { 48 | // 创建组件实例对象 vnode->是对dom进行抽象,组件可能有很多其他的属性,例如props,slot等 所以需要创建一个实例对象来承载 49 | const instance = createComponentInstance(initialVnode); 50 | 51 | setupComponent(instance); 52 | 53 | setupRenderEffect(instance, initialVnode, container); 54 | } 55 | function setupRenderEffect(instance: any, initialVnode, container) { 56 | // const proxyInstance = new Proxy(instance.ctx, componentPublicInstance); 57 | const { proxy } = instance; 58 | // subTree -> 虚拟节点树 vnode 59 | const subTree = instance.render.call(proxy); 60 | // vnode -> element -> mountElement 61 | patch(subTree, container); 62 | // 所有element都mount 完毕后 把el挂载到组件的虚拟节点上 63 | initialVnode.el = subTree.el; 64 | } 65 | 66 | // 处理元素节点 67 | function processElement(vnode: any, container: any) { 68 | // 挂载元素节点 69 | mountElement(vnode, container); 70 | } 71 | function mountElement(vnode: any, container: any) { 72 | // vnode -> {type: 'div', props: 'hi xxxxx', children: undefined} 73 | const el = (vnode.el = document.createElement(vnode.type)); 74 | const { props, children, shapeFlag } = vnode; 75 | // children 可能是string 也可能是array 76 | 77 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 78 | el.textContent = children; 79 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 80 | mountChildren(vnode, el); 81 | } 82 | // 处理props 83 | const isOn = (key: string) => /^on[A-Z]/.test(key); 84 | for (const key in props) { 85 | const val = props[key]; 86 | 87 | if (isOn(key)) { 88 | const event = key.slice(2).toLowerCase(); 89 | el.addEventListener(event, val); 90 | } else { 91 | el.setAttribute(key, val); 92 | } 93 | } 94 | container.append(el); 95 | } 96 | function mountChildren(vnode, container) { 97 | vnode.children.forEach((vnode) => { 98 | patch(vnode, container); 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/effect.test.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from '../src/reactive'; 2 | import { effect, stop } from '../src/effect'; 3 | 4 | describe('effect', () => { 5 | it('should run the passed function once (wrapped by a effect)', () => { 6 | const fnSpy = jest.fn(() => {}); 7 | effect(fnSpy); 8 | expect(fnSpy).toHaveBeenCalledTimes(1); 9 | }); 10 | 11 | it('should observe basic properties', () => { 12 | let dummy; 13 | const counter = reactive({ num: 0 }); 14 | effect(() => (dummy = counter.num)); 15 | 16 | expect(dummy).toBe(0); 17 | counter.num = 7; 18 | expect(dummy).toBe(7); 19 | }); 20 | 21 | it('should observe multiple properties', () => { 22 | let dummy; 23 | const counter = reactive({ num1: 0, num2: 0 }); 24 | effect(() => (dummy = counter.num1 + counter.num1 + counter.num2)); 25 | 26 | expect(dummy).toBe(0); 27 | counter.num2 = 7; 28 | counter.num1 = 7; 29 | expect(dummy).toBe(21); 30 | }); 31 | it('should handle multiple effects', () => { 32 | let dummy1, dummy2; 33 | const counter = reactive({ num: 0 }); 34 | effect(() => (dummy1 = counter.num)); 35 | effect(() => (dummy2 = counter.num)); 36 | 37 | expect(dummy1).toBe(0); 38 | expect(dummy2).toBe(0); 39 | counter.num = counter.num + 1; 40 | expect(dummy1).toBe(1); 41 | expect(dummy2).toBe(1); 42 | }); 43 | 44 | it('should observe nested properties', () => { 45 | let dummy; 46 | const counter = reactive({ nested: { num: 0 } }); 47 | effect(() => (dummy = counter.nested.num)); 48 | 49 | expect(dummy).toBe(0); 50 | counter.nested.num = 8; 51 | expect(dummy).toBe(8); 52 | }); 53 | 54 | it('should observe function call chains', () => { 55 | let dummy; 56 | const counter = reactive({ num: 0 }); 57 | effect(() => (dummy = getNum())); 58 | 59 | function getNum() { 60 | return counter.num; 61 | } 62 | 63 | expect(dummy).toBe(0); 64 | counter.num = 2; 65 | expect(dummy).toBe(2); 66 | }); 67 | it('runner', () => { 68 | // 1.effect(fn) -> 返回一个function runner -> 执行runner返回fn的返回结果 69 | let age = 0; 70 | const runner = effect(() => { 71 | age++; 72 | return 'hello'; 73 | }); 74 | expect(age).toBe(1); 75 | let res = runner(); 76 | expect(age).toBe(2); 77 | expect(res).toBe('hello'); 78 | }); 79 | it('scheduler', () => { 80 | let dummy; 81 | let run: any; 82 | const scheduler = jest.fn(() => { 83 | run = runner; 84 | }); 85 | const obj = reactive({ foo: 1 }); 86 | 87 | // 1.马上触发fn 88 | // 2.更新时不触发fn,触发scheduler 89 | // 3.将fn返回出去 90 | const runner = effect( 91 | () => { 92 | dummy = obj.foo; 93 | }, 94 | { scheduler } 95 | ); 96 | expect(scheduler).not.toHaveBeenCalled(); 97 | expect(dummy).toBe(1); 98 | // should be called on first trigger 99 | obj.foo++; 100 | expect(scheduler).toHaveBeenCalledTimes(1); 101 | // // should not run yet 102 | expect(dummy).toBe(1); 103 | // // manually run 104 | run(); 105 | // // should have run 106 | expect(dummy).toBe(2); 107 | }); 108 | 109 | it('stop', () => { 110 | // 1.正常触发fn 2.调用stop update数据不再执行fn 3.执行runner,再次触发响应式 111 | let dummy; 112 | const obj = reactive({ prop: 1 }); 113 | const runner = effect(() => { 114 | dummy = obj.prop; 115 | }); 116 | obj.prop = 2; 117 | expect(dummy).toBe(2); 118 | stop(runner); 119 | // obj.prop = 3; 120 | obj.prop++; 121 | expect(dummy).toBe(2); 122 | 123 | // stopped effect should still be manually callable 124 | runner(); 125 | expect(dummy).toBe(3); 126 | }); 127 | 128 | it('events: onStop', () => { 129 | const onStop = jest.fn(); 130 | const runner = effect(() => {}, { 131 | onStop, 132 | }); 133 | 134 | stop(runner); 135 | expect(onStop).toHaveBeenCalled(); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /lib/my-vue.esm.js: -------------------------------------------------------------------------------- 1 | function isObject(value) { 2 | return value !== null && typeof value === 'object'; 3 | } 4 | function hasOwn(obj, key) { 5 | return obj.hasOwnProperty(key); 6 | } 7 | const capitalize = (eventName) => { 8 | return eventName 9 | ? eventName.charAt(0).toUpperCase() + eventName.slice(1) 10 | : ''; 11 | }; 12 | const camelize = (eventName) => { 13 | return eventName 14 | ? eventName.replace(/-(\w)/g, (_, c) => { 15 | return c ? c.toUpperCase() : ''; 16 | }) 17 | : ''; 18 | }; 19 | const toHandleKey = (eventName) => { 20 | return eventName ? 'on' + camelize(capitalize(eventName)) : ''; 21 | }; 22 | 23 | const publicPropertiesMap = { 24 | $el: (i) => i.vnode.el, 25 | $slots: (i) => i.slots, 26 | }; 27 | const PublicInstanceProxyHandlers = { 28 | get({ _: instance }, key) { 29 | const { setupState, props } = instance; 30 | if (hasOwn(setupState, key)) { 31 | return setupState[key]; 32 | } 33 | else if (hasOwn(props, key)) { 34 | return props[key]; 35 | } 36 | const publicGetter = publicPropertiesMap[key]; 37 | if (publicGetter) { 38 | return publicGetter(instance); 39 | } 40 | }, 41 | }; 42 | 43 | function initProps(instance, props) { 44 | instance.props = props || {}; 45 | } 46 | 47 | const targetMap = new WeakMap(); 48 | function trigger(target, key) { 49 | let depsMap = targetMap.get(target); 50 | if (!depsMap) 51 | return; 52 | let dep = depsMap.get(key); 53 | triggerEffect(dep); 54 | } 55 | function triggerEffect(dep) { 56 | for (const effect of dep) { 57 | if (effect.scheduler) { 58 | effect.scheduler(); 59 | } 60 | else { 61 | effect.run(); 62 | } 63 | } 64 | } 65 | 66 | const get = createGetter(); 67 | const set = createSetter(); 68 | const readonlyGet = createGetter(true); 69 | const shallowReadonlyGet = createGetter(true, true); 70 | const mutableHandlers = { 71 | get, 72 | set, 73 | }; 74 | const readonlyHandlers = { 75 | get: readonlyGet, 76 | set: function (target, key, value) { 77 | console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target); 78 | return true; 79 | }, 80 | }; 81 | const shallowReadonlyHandlers = Object.assign({}, readonlyHandlers, { 82 | get: shallowReadonlyGet, 83 | }); 84 | function createGetter(isReadonly = false, isShallowReadonly = false) { 85 | return function get(target, key) { 86 | if (key === "__v_isReactive") { 87 | return !isReadonly; 88 | } 89 | if (key === "_v_isReadOnly") { 90 | return isReadonly; 91 | } 92 | const res = Reflect.get(target, key); 93 | if (isShallowReadonly) { 94 | return res; 95 | } 96 | if (isObject(res)) { 97 | return isReadonly ? readonly(res) : reactive(res); 98 | } 99 | return res; 100 | }; 101 | } 102 | function createSetter() { 103 | return function set(target, key, value) { 104 | let res = Reflect.set(target, key, value); 105 | trigger(target, key); 106 | return res; 107 | }; 108 | } 109 | 110 | const proxyMap = new WeakMap(); 111 | var ReactiveFlags; 112 | (function (ReactiveFlags) { 113 | ReactiveFlags["IS_REACTIVE"] = "__v_isReactive"; 114 | ReactiveFlags["IS_READONLY"] = "_v_isReadOnly"; 115 | })(ReactiveFlags || (ReactiveFlags = {})); 116 | function reactive(target) { 117 | return createReactiveObject(target, proxyMap, mutableHandlers); 118 | } 119 | function readonly(target) { 120 | return createReactiveObject(target, proxyMap, readonlyHandlers); 121 | } 122 | function shallowReadonly(target) { 123 | return createReactiveObject(target, proxyMap, shallowReadonlyHandlers); 124 | } 125 | function createReactiveObject(target, proxyMap, baseHandlers) { 126 | let reactiveProxy; 127 | if (reactiveProxy) { 128 | reactiveProxy = proxyMap[target]; 129 | return reactiveProxy; 130 | } 131 | reactiveProxy = new Proxy(target, baseHandlers); 132 | proxyMap.set(target, reactiveProxy); 133 | return reactiveProxy; 134 | } 135 | 136 | function emit(instance, event, ...args) { 137 | console.log('instance ==== ', instance); 138 | const { props } = instance; 139 | const handleName = toHandleKey(event); 140 | const handle = props[handleName]; 141 | handle && handle(...args); 142 | } 143 | 144 | function initSlots(instance, children) { 145 | console.log('initSlots instance = ', instance); 146 | const { vnode } = instance; 147 | if (vnode.shapeFlag & 16) { 148 | normalizeObjectSlots(children, instance.slots); 149 | } 150 | } 151 | function normalizeObjectSlots(children, slots) { 152 | for (let key in children) { 153 | let slot = children[key]; 154 | slots[key] = (props) => normalizeSlotsValue(slot(props)); 155 | } 156 | } 157 | function normalizeSlotsValue(slot) { 158 | return Array.isArray(slot) ? slot : [slot]; 159 | } 160 | 161 | function createComponentInstance(vnode) { 162 | const component = { 163 | vnode, 164 | type: vnode.type, 165 | proxy: {}, 166 | ctx: {}, 167 | setupState: {}, 168 | props: {}, 169 | slots: {}, 170 | emit: () => { }, 171 | }; 172 | component.emit = emit; 173 | component.ctx = { 174 | _: component, 175 | }; 176 | return component; 177 | } 178 | function setupComponent(instance) { 179 | initProps(instance, instance.vnode.props); 180 | initSlots(instance, instance.vnode.children); 181 | setupStatefulComponent(instance); 182 | } 183 | let currentInstance = null; 184 | function setupStatefulComponent(instance) { 185 | const component = instance.type; 186 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 187 | const { setup } = component; 188 | if (setup) { 189 | setCurrentInstance(instance); 190 | const setupResult = setup(shallowReadonly(instance.props), { 191 | emit: instance.emit.bind(null, instance), 192 | }); 193 | setCurrentInstance(null); 194 | handleSetupResult(instance, setupResult); 195 | } 196 | } 197 | function handleSetupResult(instance, setupResult) { 198 | if (typeof setupResult === 'object') { 199 | instance.setupState = setupResult; 200 | } 201 | finishComponentSetup(instance); 202 | } 203 | function finishComponentSetup(instance) { 204 | const component = instance.type; 205 | if (component.render) { 206 | instance.render = component.render; 207 | } 208 | } 209 | function getCurrentInstance() { 210 | return currentInstance; 211 | } 212 | function setCurrentInstance(instance) { 213 | currentInstance = instance; 214 | } 215 | 216 | const Fragment = Symbol('Fragment'); 217 | const Text = Symbol('Text'); 218 | function createVNode(type, props, children) { 219 | const vnode = { 220 | type, 221 | props, 222 | children, 223 | el: null, 224 | shapeFlag: getShapeFlag(type), 225 | }; 226 | if (typeof vnode.children === 'string') { 227 | vnode.shapeFlag |= 4; 228 | } 229 | else if (Array.isArray(vnode.children)) { 230 | vnode.shapeFlag |= 8; 231 | } 232 | normalizeChildren(vnode, children); 233 | return vnode; 234 | } 235 | function createTextVNode(text) { 236 | return createVNode(Text, {}, text); 237 | } 238 | function getShapeFlag(type) { 239 | return typeof type === 'string' 240 | ? 1 241 | : 2; 242 | } 243 | function normalizeChildren(vnode, children) { 244 | if (typeof children === 'object') { 245 | if (vnode.shapeFlag & 1) ; 246 | else { 247 | vnode.shapeFlag |= 16; 248 | } 249 | } 250 | } 251 | 252 | function render(vnode, container) { 253 | console.log('render...', vnode); 254 | patch(vnode, container); 255 | } 256 | function patch(vnode, container) { 257 | const { shapeFlag, type } = vnode; 258 | switch (type) { 259 | case Fragment: 260 | processFragment(vnode, container); 261 | break; 262 | case Text: 263 | processText(vnode, container); 264 | break; 265 | default: 266 | if (shapeFlag & 2) { 267 | console.log('处理组件 vnode', vnode); 268 | processComponent(vnode, container); 269 | } 270 | else if (shapeFlag & 1) { 271 | processElement(vnode, container); 272 | } 273 | break; 274 | } 275 | } 276 | function processFragment(vnode, container) { 277 | mountChildren(vnode, container); 278 | } 279 | function processText(vnode, container) { 280 | const { children } = vnode; 281 | const textContent = (vnode.el = document.createTextNode(children)); 282 | container.append(textContent); 283 | } 284 | function processComponent(initialVnode, container) { 285 | mountComponent(initialVnode, container); 286 | } 287 | function mountComponent(initialVnode, container) { 288 | const instance = createComponentInstance(initialVnode); 289 | setupComponent(instance); 290 | setupRenderEffect(instance, initialVnode, container); 291 | } 292 | function setupRenderEffect(instance, initialVnode, container) { 293 | const { proxy } = instance; 294 | const subTree = instance.render.call(proxy); 295 | patch(subTree, container); 296 | initialVnode.el = subTree.el; 297 | } 298 | function processElement(vnode, container) { 299 | mountElement(vnode, container); 300 | } 301 | function mountElement(vnode, container) { 302 | const el = (vnode.el = document.createElement(vnode.type)); 303 | const { props, children, shapeFlag } = vnode; 304 | if (shapeFlag & 4) { 305 | el.textContent = children; 306 | } 307 | else if (shapeFlag & 8) { 308 | mountChildren(vnode, el); 309 | } 310 | const isOn = (key) => /^on[A-Z]/.test(key); 311 | for (const key in props) { 312 | const val = props[key]; 313 | if (isOn(key)) { 314 | const event = key.slice(2).toLowerCase(); 315 | el.addEventListener(event, val); 316 | } 317 | else { 318 | el.setAttribute(key, val); 319 | } 320 | } 321 | container.append(el); 322 | } 323 | function mountChildren(vnode, container) { 324 | vnode.children.forEach((vnode) => { 325 | patch(vnode, container); 326 | }); 327 | } 328 | 329 | function createApp(rootComponent) { 330 | return { 331 | mount(rootContainer) { 332 | rootContainer = document.querySelector(rootContainer); 333 | const vnode = createVNode(rootComponent); 334 | render(vnode, rootContainer); 335 | }, 336 | }; 337 | } 338 | 339 | function h(type, props, children) { 340 | return createVNode(type, props, children); 341 | } 342 | 343 | function renderSlots(slots, name, props) { 344 | const slot = slots[name]; 345 | if (slot) { 346 | return createVNode(Fragment, {}, slot(props)); 347 | } 348 | } 349 | 350 | export { createApp, createTextVNode, getCurrentInstance, h, renderSlots }; 351 | -------------------------------------------------------------------------------- /lib/my-vue.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | function isObject(value) { 6 | return value !== null && typeof value === 'object'; 7 | } 8 | function hasOwn(obj, key) { 9 | return obj.hasOwnProperty(key); 10 | } 11 | const capitalize = (eventName) => { 12 | return eventName 13 | ? eventName.charAt(0).toUpperCase() + eventName.slice(1) 14 | : ''; 15 | }; 16 | const camelize = (eventName) => { 17 | return eventName 18 | ? eventName.replace(/-(\w)/g, (_, c) => { 19 | return c ? c.toUpperCase() : ''; 20 | }) 21 | : ''; 22 | }; 23 | const toHandleKey = (eventName) => { 24 | return eventName ? 'on' + camelize(capitalize(eventName)) : ''; 25 | }; 26 | 27 | const publicPropertiesMap = { 28 | $el: (i) => i.vnode.el, 29 | $slots: (i) => i.slots, 30 | }; 31 | const PublicInstanceProxyHandlers = { 32 | get({ _: instance }, key) { 33 | const { setupState, props } = instance; 34 | if (hasOwn(setupState, key)) { 35 | return setupState[key]; 36 | } 37 | else if (hasOwn(props, key)) { 38 | return props[key]; 39 | } 40 | const publicGetter = publicPropertiesMap[key]; 41 | if (publicGetter) { 42 | return publicGetter(instance); 43 | } 44 | }, 45 | }; 46 | 47 | function initProps(instance, props) { 48 | instance.props = props || {}; 49 | } 50 | 51 | const targetMap = new WeakMap(); 52 | function trigger(target, key) { 53 | let depsMap = targetMap.get(target); 54 | if (!depsMap) 55 | return; 56 | let dep = depsMap.get(key); 57 | triggerEffect(dep); 58 | } 59 | function triggerEffect(dep) { 60 | for (const effect of dep) { 61 | if (effect.scheduler) { 62 | effect.scheduler(); 63 | } 64 | else { 65 | effect.run(); 66 | } 67 | } 68 | } 69 | 70 | const get = createGetter(); 71 | const set = createSetter(); 72 | const readonlyGet = createGetter(true); 73 | const shallowReadonlyGet = createGetter(true, true); 74 | const mutableHandlers = { 75 | get, 76 | set, 77 | }; 78 | const readonlyHandlers = { 79 | get: readonlyGet, 80 | set: function (target, key, value) { 81 | console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target); 82 | return true; 83 | }, 84 | }; 85 | const shallowReadonlyHandlers = Object.assign({}, readonlyHandlers, { 86 | get: shallowReadonlyGet, 87 | }); 88 | function createGetter(isReadonly = false, isShallowReadonly = false) { 89 | return function get(target, key) { 90 | if (key === "__v_isReactive") { 91 | return !isReadonly; 92 | } 93 | if (key === "_v_isReadOnly") { 94 | return isReadonly; 95 | } 96 | const res = Reflect.get(target, key); 97 | if (isShallowReadonly) { 98 | return res; 99 | } 100 | if (isObject(res)) { 101 | return isReadonly ? readonly(res) : reactive(res); 102 | } 103 | return res; 104 | }; 105 | } 106 | function createSetter() { 107 | return function set(target, key, value) { 108 | let res = Reflect.set(target, key, value); 109 | trigger(target, key); 110 | return res; 111 | }; 112 | } 113 | 114 | const proxyMap = new WeakMap(); 115 | var ReactiveFlags; 116 | (function (ReactiveFlags) { 117 | ReactiveFlags["IS_REACTIVE"] = "__v_isReactive"; 118 | ReactiveFlags["IS_READONLY"] = "_v_isReadOnly"; 119 | })(ReactiveFlags || (ReactiveFlags = {})); 120 | function reactive(target) { 121 | return createReactiveObject(target, proxyMap, mutableHandlers); 122 | } 123 | function readonly(target) { 124 | return createReactiveObject(target, proxyMap, readonlyHandlers); 125 | } 126 | function shallowReadonly(target) { 127 | return createReactiveObject(target, proxyMap, shallowReadonlyHandlers); 128 | } 129 | function createReactiveObject(target, proxyMap, baseHandlers) { 130 | let reactiveProxy; 131 | if (reactiveProxy) { 132 | reactiveProxy = proxyMap[target]; 133 | return reactiveProxy; 134 | } 135 | reactiveProxy = new Proxy(target, baseHandlers); 136 | proxyMap.set(target, reactiveProxy); 137 | return reactiveProxy; 138 | } 139 | 140 | function emit(instance, event, ...args) { 141 | console.log('instance ==== ', instance); 142 | const { props } = instance; 143 | const handleName = toHandleKey(event); 144 | const handle = props[handleName]; 145 | handle && handle(...args); 146 | } 147 | 148 | function initSlots(instance, children) { 149 | console.log('initSlots instance = ', instance); 150 | const { vnode } = instance; 151 | if (vnode.shapeFlag & 16) { 152 | normalizeObjectSlots(children, instance.slots); 153 | } 154 | } 155 | function normalizeObjectSlots(children, slots) { 156 | for (let key in children) { 157 | let slot = children[key]; 158 | slots[key] = (props) => normalizeSlotsValue(slot(props)); 159 | } 160 | } 161 | function normalizeSlotsValue(slot) { 162 | return Array.isArray(slot) ? slot : [slot]; 163 | } 164 | 165 | function createComponentInstance(vnode) { 166 | const component = { 167 | vnode, 168 | type: vnode.type, 169 | proxy: {}, 170 | ctx: {}, 171 | setupState: {}, 172 | props: {}, 173 | slots: {}, 174 | emit: () => { }, 175 | }; 176 | component.emit = emit; 177 | component.ctx = { 178 | _: component, 179 | }; 180 | return component; 181 | } 182 | function setupComponent(instance) { 183 | initProps(instance, instance.vnode.props); 184 | initSlots(instance, instance.vnode.children); 185 | setupStatefulComponent(instance); 186 | } 187 | let currentInstance = null; 188 | function setupStatefulComponent(instance) { 189 | const component = instance.type; 190 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 191 | const { setup } = component; 192 | if (setup) { 193 | setCurrentInstance(instance); 194 | const setupResult = setup(shallowReadonly(instance.props), { 195 | emit: instance.emit.bind(null, instance), 196 | }); 197 | setCurrentInstance(null); 198 | handleSetupResult(instance, setupResult); 199 | } 200 | } 201 | function handleSetupResult(instance, setupResult) { 202 | if (typeof setupResult === 'object') { 203 | instance.setupState = setupResult; 204 | } 205 | finishComponentSetup(instance); 206 | } 207 | function finishComponentSetup(instance) { 208 | const component = instance.type; 209 | if (component.render) { 210 | instance.render = component.render; 211 | } 212 | } 213 | function getCurrentInstance() { 214 | return currentInstance; 215 | } 216 | function setCurrentInstance(instance) { 217 | currentInstance = instance; 218 | } 219 | 220 | const Fragment = Symbol('Fragment'); 221 | const Text = Symbol('Text'); 222 | function createVNode(type, props, children) { 223 | const vnode = { 224 | type, 225 | props, 226 | children, 227 | el: null, 228 | shapeFlag: getShapeFlag(type), 229 | }; 230 | if (typeof vnode.children === 'string') { 231 | vnode.shapeFlag |= 4; 232 | } 233 | else if (Array.isArray(vnode.children)) { 234 | vnode.shapeFlag |= 8; 235 | } 236 | normalizeChildren(vnode, children); 237 | return vnode; 238 | } 239 | function createTextVNode(text) { 240 | return createVNode(Text, {}, text); 241 | } 242 | function getShapeFlag(type) { 243 | return typeof type === 'string' 244 | ? 1 245 | : 2; 246 | } 247 | function normalizeChildren(vnode, children) { 248 | if (typeof children === 'object') { 249 | if (vnode.shapeFlag & 1) ; 250 | else { 251 | vnode.shapeFlag |= 16; 252 | } 253 | } 254 | } 255 | 256 | function render(vnode, container) { 257 | console.log('render...', vnode); 258 | patch(vnode, container); 259 | } 260 | function patch(vnode, container) { 261 | const { shapeFlag, type } = vnode; 262 | switch (type) { 263 | case Fragment: 264 | processFragment(vnode, container); 265 | break; 266 | case Text: 267 | processText(vnode, container); 268 | break; 269 | default: 270 | if (shapeFlag & 2) { 271 | console.log('处理组件 vnode', vnode); 272 | processComponent(vnode, container); 273 | } 274 | else if (shapeFlag & 1) { 275 | processElement(vnode, container); 276 | } 277 | break; 278 | } 279 | } 280 | function processFragment(vnode, container) { 281 | mountChildren(vnode, container); 282 | } 283 | function processText(vnode, container) { 284 | const { children } = vnode; 285 | const textContent = (vnode.el = document.createTextNode(children)); 286 | container.append(textContent); 287 | } 288 | function processComponent(initialVnode, container) { 289 | mountComponent(initialVnode, container); 290 | } 291 | function mountComponent(initialVnode, container) { 292 | const instance = createComponentInstance(initialVnode); 293 | setupComponent(instance); 294 | setupRenderEffect(instance, initialVnode, container); 295 | } 296 | function setupRenderEffect(instance, initialVnode, container) { 297 | const { proxy } = instance; 298 | const subTree = instance.render.call(proxy); 299 | patch(subTree, container); 300 | initialVnode.el = subTree.el; 301 | } 302 | function processElement(vnode, container) { 303 | mountElement(vnode, container); 304 | } 305 | function mountElement(vnode, container) { 306 | const el = (vnode.el = document.createElement(vnode.type)); 307 | const { props, children, shapeFlag } = vnode; 308 | if (shapeFlag & 4) { 309 | el.textContent = children; 310 | } 311 | else if (shapeFlag & 8) { 312 | mountChildren(vnode, el); 313 | } 314 | const isOn = (key) => /^on[A-Z]/.test(key); 315 | for (const key in props) { 316 | const val = props[key]; 317 | if (isOn(key)) { 318 | const event = key.slice(2).toLowerCase(); 319 | el.addEventListener(event, val); 320 | } 321 | else { 322 | el.setAttribute(key, val); 323 | } 324 | } 325 | container.append(el); 326 | } 327 | function mountChildren(vnode, container) { 328 | vnode.children.forEach((vnode) => { 329 | patch(vnode, container); 330 | }); 331 | } 332 | 333 | function createApp(rootComponent) { 334 | return { 335 | mount(rootContainer) { 336 | rootContainer = document.querySelector(rootContainer); 337 | const vnode = createVNode(rootComponent); 338 | render(vnode, rootContainer); 339 | }, 340 | }; 341 | } 342 | 343 | function h(type, props, children) { 344 | return createVNode(type, props, children); 345 | } 346 | 347 | function renderSlots(slots, name, props) { 348 | const slot = slots[name]; 349 | if (slot) { 350 | return createVNode(Fragment, {}, slot(props)); 351 | } 352 | } 353 | 354 | exports.createApp = createApp; 355 | exports.createTextVNode = createTextVNode; 356 | exports.getCurrentInstance = getCurrentInstance; 357 | exports.h = h; 358 | exports.renderSlots = renderSlots; 359 | --------------------------------------------------------------------------------