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