├── .gitignore ├── src ├── reactivity │ ├── index.ts │ ├── tests │ │ ├── shallowReadonly.spec.ts │ │ ├── readonly.spec.ts │ │ ├── reactive.spec.ts │ │ ├── computed.spec.ts │ │ ├── ref.spec.ts │ │ └── effect.spec.ts │ ├── computed.ts │ ├── reactive.ts │ ├── baseHandler.ts │ ├── ref.ts │ └── effect.ts ├── index.ts ├── runtime-core │ ├── componentProps.ts │ ├── h.ts │ ├── index.ts │ ├── componentEmit.ts │ ├── helpers │ │ └── renderSlots.ts │ ├── createApp.ts │ ├── componentSlots.ts │ ├── componentPublicInstance.ts │ ├── apiInject.ts │ ├── vnode.ts │ ├── component.ts │ └── renderer.ts ├── shared │ ├── ShapeFlags.ts │ └── index.ts └── runtime-dom │ └── index.ts ├── babel.config.js ├── example ├── update │ ├── main.js │ ├── index.html │ └── App.js ├── componentEmit │ ├── main.js │ ├── index.html │ ├── App.js │ └── Foo.js ├── componentSlot │ ├── main.js │ ├── index.html │ ├── Foo.js │ └── App.js ├── helloworld │ ├── main.js │ ├── Foo.js │ ├── index.html │ └── App.js ├── currentInstance │ ├── main.js │ ├── Foo.js │ ├── App.js │ └── index.html ├── customRenderer │ ├── App.js │ ├── index.html │ └── main.js └── apiInject │ ├── index.html │ └── App.js ├── rollup.config.js ├── package.json ├── .vscode └── launch.json ├── tsconfig.json └── lib ├── guide-mini-vue.esm.js └── guide-mini-vue.cjs.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export { ref, proxyRefs } from "./ref"; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // mini-vue 出口 2 | export * from "./runtime-dom" 3 | export * from "./reactivity" -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance, rawProps) { 2 | instance.props = rawProps || {} 3 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], '@babel/preset-typescript', 4 | ], 5 | }; -------------------------------------------------------------------------------- /src/runtime-core/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "./vnode"; 2 | 3 | export function h(type, props?, children?) { 4 | return createVNode(type, props, children); 5 | } -------------------------------------------------------------------------------- /example/update/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); -------------------------------------------------------------------------------- /example/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); -------------------------------------------------------------------------------- /example/componentSlot/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); -------------------------------------------------------------------------------- /example/helloworld/main.js: -------------------------------------------------------------------------------- 1 | 2 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 3 | import { App } from "./App.js"; 4 | 5 | const rootContainer = document.querySelector("#app"); 6 | createApp(App).mount(rootContainer); -------------------------------------------------------------------------------- /example/currentInstance/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); 6 | -------------------------------------------------------------------------------- /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 | SLOT_CHILDREN = 1 << 4, 7 | } -------------------------------------------------------------------------------- /example/customRenderer/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js"; 2 | 3 | export const App = { 4 | setup() { 5 | return { 6 | x: 100, 7 | y: 100, 8 | }; 9 | }, 10 | render() { 11 | return h("rect", { x: this.x, y: this.y }); 12 | }, 13 | }; -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | export { h } from "./h"; 2 | export { renderSlots } from "./helpers/renderSlots"; 3 | export { createTextVNode } from "./vnode"; 4 | export { getCurrentInstance } from "./component"; 5 | export { provide, inject } from "./apiInject"; 6 | export { createRenderer } from "./renderer"; -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from "../shared/index"; 2 | 3 | export function emit(instance, event, ...args) { 4 | const { props } = instance; 5 | const handlerName = toHandlerKey(camelize(event)); 6 | const handler = props[handlerName]; 7 | handler && handler(...args); 8 | } -------------------------------------------------------------------------------- /src/runtime-core/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, Fragment } from "../vnode"; 2 | 3 | export function renderSlots(slots, name, props) { 4 | const slot = slots[name]; 5 | 6 | if (slot) { 7 | if (typeof slot === "function") { 8 | return createVNode(Fragment, {}, slot(props)); 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /example/currentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from "../../lib/guide-mini-vue.esm.js"; 2 | 3 | export const Foo = { 4 | name:"Foo", 5 | setup() { 6 | const instance = getCurrentInstance(); 7 | console.log("Foo:", instance); 8 | return {}; 9 | }, 10 | render() { 11 | return h("div", {}, "foo"); 12 | }, 13 | }; -------------------------------------------------------------------------------- /src/runtime-core/createApp.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "./vnode"; 2 | 3 | export function createAppAPI(render) { 4 | return function createApp(rootComponent) { 5 | return { 6 | mount(rootContainer) { 7 | const vnode = createVNode(rootComponent); 8 | 9 | render(vnode, rootContainer); 10 | }, 11 | }; 12 | }; 13 | } -------------------------------------------------------------------------------- /example/helloworld/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js"; 2 | 3 | export const Foo = { 4 | setup(props) { 5 | // props.count 6 | console.log(props); 7 | 8 | // 3. 9 | // shallow readonly 10 | props.count++ 11 | console.log(props); 12 | 13 | }, 14 | render() { 15 | return h("div", {}, "foo: " + this.count); 16 | }, 17 | }; -------------------------------------------------------------------------------- /example/currentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from "../../lib/guide-mini-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | render() { 7 | return h("div", {}, [h("p", {}, "currentInstance demo"), h(Foo)]); 8 | }, 9 | 10 | setup() { 11 | const instance = getCurrentInstance(); 12 | console.log("App:", instance); 13 | }, 14 | }; -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from "./package.json"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | export default { 4 | input: "./src/index.ts", 5 | output: [ 6 | // 1. cjs -> commonjs 7 | // 2. esm 8 | { 9 | format: "cjs", 10 | file: pkg.main, 11 | }, 12 | { 13 | format: "es", 14 | file: pkg.module, 15 | }, 16 | ], 17 | 18 | plugins: [typescript()], 19 | }; -------------------------------------------------------------------------------- /example/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/componentSlot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/componentEmit/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | render() { 7 | // emit 8 | return h("div", {}, [ 9 | h("div", {}, "App"), 10 | h(Foo, { 11 | onAdd(a, b) { 12 | console.log("onAdd", a, b); 13 | }, 14 | onAddFoo() { 15 | console.log("onAddFoo"); 16 | }, 17 | }), 18 | ]); 19 | }, 20 | 21 | setup() { 22 | return {}; 23 | }, 24 | }; -------------------------------------------------------------------------------- /example/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /example/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /example/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 17 | 18 | -------------------------------------------------------------------------------- /example/currentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /example/componentEmit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js"; 2 | 3 | export const Foo = { 4 | setup(props, { emit }) { 5 | const emitAdd = () => { 6 | console.log("emit add"); 7 | emit("add",1,2); 8 | emit("add-foo"); 9 | }; 10 | 11 | return { 12 | emitAdd, 13 | }; 14 | }, 15 | render() { 16 | const btn = h( 17 | "button", 18 | { 19 | onClick: this.emitAdd, 20 | }, 21 | "emitAdd" 22 | ); 23 | 24 | const foo = h("p", {}, "foo"); 25 | return h("div", {}, [foo, btn]); 26 | }, 27 | }; -------------------------------------------------------------------------------- /src/reactivity/tests/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, shallowReadonly } from "../reactive"; 2 | 3 | describe("shallowReadonly", () => { 4 | test("should not make non-reactive properties reactive", () => { 5 | const props = shallowReadonly({ n: { foo: 1 } }); 6 | expect(isReadonly(props)).toBe(true); 7 | expect(isReadonly(props.n)).toBe(false); 8 | }); 9 | 10 | it("should call console.warn when set", () => { 11 | console.warn = jest.fn(); 12 | const user = shallowReadonly({ 13 | age: 10, 14 | }); 15 | 16 | user.age = 11; 17 | expect(console.warn).toHaveBeenCalled(); 18 | }); 19 | }); -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from "../shared/ShapeFlags"; 2 | 3 | export function initSlots(instance, children) { 4 | // slots 5 | const { vnode } = instance; 6 | if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) { 7 | normalizeObjectSlots(children, instance.slots); 8 | } 9 | } 10 | 11 | function normalizeObjectSlots(children: any, slots: any) { 12 | for (const key in children) { 13 | const value = children[key]; 14 | slots[key] = (props) => normalizeSlotValue(value(props)); 15 | } 16 | } 17 | 18 | function normalizeSlotValue(value) { 19 | return Array.isArray(value) ? value : [value]; 20 | } -------------------------------------------------------------------------------- /example/customRenderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from "../shared/index"; 2 | 3 | const publicPropertiesMap = { 4 | $el: (i) => i.vnode.el, 5 | $slots: (i) => i.slots, 6 | }; 7 | 8 | export const PublicInstanceProxyHandlers = { 9 | get({ _: instance }, key) { 10 | // setupState 11 | const { setupState, props } = instance; 12 | if (hasOwn(setupState, key)) { 13 | return setupState[key]; 14 | } else if (hasOwn(props, key)) { 15 | return props[key]; 16 | } 17 | const publicGetter = publicPropertiesMap[key]; 18 | if (publicGetter) { 19 | return publicGetter(instance); 20 | } 21 | }, 22 | }; -------------------------------------------------------------------------------- /example/componentSlot/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from "../../lib/guide-mini-vue.esm.js"; 2 | 3 | export const Foo = { 4 | setup() { 5 | return {}; 6 | }, 7 | render() { 8 | const foo = h("p", {}, "foo"); 9 | 10 | // Foo .vnode. children 11 | console.log(this.$slots); 12 | // children -> vnode 13 | // 14 | // renderSlots 15 | // 具名插槽 16 | // 1. 获取到要渲染的元素 1 17 | // 2. 要获取到渲染的位置 18 | // 作用域插槽 19 | const age = 18; 20 | return h("div", {}, [ 21 | renderSlots(this.$slots, "header", { 22 | age, 23 | }), 24 | foo, 25 | renderSlots(this.$slots, "footer"), 26 | ]); 27 | }, 28 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guide-mini-vue-2", 3 | "version": "1.0.0", 4 | "main": "lib/guide-mini-vue.cjs.js", 5 | "module": "lib/guide-mini-vue.esm.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "jest", 9 | "build": "rollup -c rollup.config.js" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.16.0", 13 | "@babel/preset-env": "^7.16.4", 14 | "@babel/preset-typescript": "^7.16.0", 15 | "@rollup/plugin-typescript": "^8.3.0", 16 | "@types/jest": "^27.0.3", 17 | "babel-jest": "^27.4.2", 18 | "jest": "^27.4.3", 19 | "rollup": "^2.60.2", 20 | "tslib": "^2.3.1", 21 | "typescript": "^4.4.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/componentSlot/App.js: -------------------------------------------------------------------------------- 1 | import { h, createTextVNode } from "../../lib/guide-mini-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | render() { 7 | const app = h("div", {}, "App"); 8 | // object key 9 | const foo = h( 10 | Foo, 11 | {}, 12 | { 13 | header: ({ age }) => [ 14 | h("p", {}, "header" + age), 15 | createTextVNode("你好呀"), 16 | ], 17 | footer: () => h("p", {}, "footer"), 18 | } 19 | ); 20 | // 数组 vnode 21 | // const foo = h(Foo, {}, h("p", {}, "123")); 22 | return h("div", {}, [app, foo]); 23 | }, 24 | 25 | setup() { 26 | return {}; 27 | }, 28 | }; -------------------------------------------------------------------------------- /src/reactivity/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from "./effect"; 2 | 3 | class ComputedRefImpl { 4 | private _getter: any; 5 | private _dirty: boolean = true; 6 | private _value: any; 7 | private _effect: ReactiveEffect; 8 | constructor(getter) { 9 | this._getter = getter 10 | this._effect = new ReactiveEffect(this._getter, () => { 11 | if (!this._dirty) { 12 | this._dirty = true 13 | } 14 | }) 15 | } 16 | get value() { 17 | if (this._dirty) { 18 | this._dirty = false 19 | this._value = this._effect.run() 20 | } 21 | return this._value 22 | } 23 | } 24 | 25 | 26 | export function computed(getter) { 27 | return new ComputedRefImpl(getter); 28 | } -------------------------------------------------------------------------------- /example/customRenderer/main.js: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "../../lib/guide-mini-vue.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const game = new PIXI.Application({ 5 | width: 500, 6 | height: 500, 7 | }); 8 | 9 | document.body.append(game.view); 10 | 11 | const renderer = createRenderer({ 12 | createElement(type) { 13 | if (type === "rect") { 14 | const rect = new PIXI.Graphics(); 15 | rect.beginFill(0xff0000); 16 | rect.drawRect(0, 0, 100, 100); 17 | rect.endFill(); 18 | 19 | return rect; 20 | } 21 | }, 22 | patchProp(el, key, val) { 23 | el[key] = val; 24 | }, 25 | insert(el, parent) { 26 | parent.addChild(el); 27 | }, 28 | }); 29 | 30 | renderer.createApp(App).mount(game.stage); -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export const extend = Object.assign; 2 | 3 | export const isObject = (value) => { 4 | return value !== null && typeof value === "object"; 5 | }; 6 | 7 | export const hasChanged = (val, newValue) => { 8 | return !Object.is(val, newValue); 9 | }; 10 | 11 | export const hasOwn = (val, key) => 12 | Object.prototype.hasOwnProperty.call(val, key); 13 | 14 | 15 | export const camelize = (str: string) => { 16 | return str.replace(/-(\w)/g, (_, c: string) => { 17 | return c ? c.toUpperCase() : ""; 18 | }); 19 | }; 20 | 21 | const capitalize = (str: string) => { 22 | return str.charAt(0).toUpperCase() + str.slice(1); 23 | }; 24 | 25 | export const toHandlerKey = (str: string) => { 26 | return str ? "on" + capitalize(str) : ""; 27 | }; -------------------------------------------------------------------------------- /src/reactivity/tests/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { readonly, isReadonly, isProxy} from "../reactive"; 2 | 3 | describe("readonly", () => { 4 | it("should make nested values readonly", () => { 5 | const original = { foo: 1, bar: { baz: 2 } }; 6 | const wrapped = readonly(original); 7 | expect(wrapped).not.toBe(original); 8 | expect(isReadonly(wrapped)).toBe(true); 9 | expect(isReadonly(original)).toBe(false); 10 | expect(isReadonly(wrapped.bar)).toBe(true); 11 | expect(isReadonly(original.bar)).toBe(false); 12 | expect(isProxy(wrapped)).toBe(true); 13 | expect(wrapped.foo).toBe(1); 14 | }); 15 | 16 | it("should call console.warn when set", () => { 17 | console.warn = jest.fn(); 18 | const user = readonly({ 19 | age: 10, 20 | }); 21 | 22 | user.age = 11; 23 | expect(console.warn).toHaveBeenCalled(); 24 | }); 25 | }); -------------------------------------------------------------------------------- /src/reactivity/tests/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive, isReactive, isProxy } from "../reactive"; 2 | describe("reactive", () => { 3 | it("happy path", () => { 4 | const original = { foo: 1 }; 5 | const observed = reactive(original); 6 | expect(observed).not.toBe(original); 7 | expect(observed.foo).toBe(1); 8 | expect(isReactive(observed)).toBe(true); 9 | expect(isReactive(original)).toBe(false); 10 | expect(isProxy(observed)).toBe(true); 11 | }); 12 | 13 | test("nested reactives", () => { 14 | const original = { 15 | nested: { 16 | foo: 1, 17 | }, 18 | array: [{ bar: 2 }], 19 | }; 20 | const observed = reactive(original); 21 | expect(isReactive(observed.nested)).toBe(true); 22 | expect(isReactive(observed.array)).toBe(true); 23 | expect(isReactive(observed.array[0])).toBe(true); 24 | }); 25 | }); -------------------------------------------------------------------------------- /src/runtime-dom/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "../runtime-core"; 2 | 3 | function createElement(type) { 4 | return document.createElement(type); 5 | } 6 | 7 | function patchProp(el, key, prevVal, nextVal) { 8 | const isOn = (key: string) => /^on[A-Z]/.test(key); 9 | if (isOn(key)) { 10 | const event = key.slice(2).toLowerCase(); 11 | el.addEventListener(event, nextVal); 12 | } else { 13 | if (nextVal === undefined || nextVal === null) { 14 | el.removeAttribute(key); 15 | } else { 16 | el.setAttribute(key, nextVal); 17 | } 18 | } 19 | } 20 | 21 | function insert(el, parent) { 22 | parent.append(el); 23 | } 24 | 25 | const renderer: any = createRenderer({ 26 | createElement, 27 | patchProp, 28 | insert, 29 | }); 30 | 31 | export function createApp(...args) { 32 | return renderer.createApp(...args); 33 | } 34 | 35 | export * from "../runtime-core"; -------------------------------------------------------------------------------- /example/helloworld/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | window.self = null 5 | export const App = { 6 | name:"App", 7 | render() { 8 | window.self = this 9 | // ui 10 | return h( 11 | "div", 12 | { 13 | id: "root", 14 | class: ["red", "hard"], 15 | onClick() { 16 | console.log("click"); 17 | }, 18 | onMousedown(){ 19 | console.log("mousedown") 20 | } 21 | }, 22 | [ 23 | h("div", {}, "hi," + this.msg), 24 | h(Foo, { 25 | count: 1, 26 | }), 27 | ] 28 | // "hi, " + this.msg 29 | // string 30 | // "hi, mini-vue" 31 | // Array 32 | // [h("p", { class:"red"}, "hi"), h("p", {class:"blue"}, "mini-vue")] 33 | ); 34 | }, 35 | 36 | setup() { 37 | return { 38 | msg: "mini-vue-mini2", 39 | }; 40 | }, 41 | }; -------------------------------------------------------------------------------- /src/runtime-core/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from "./component"; 2 | 3 | export function provide(key, value) { 4 | const currentInstance: any = getCurrentInstance(); 5 | 6 | if (currentInstance) { 7 | let { provides } = currentInstance; 8 | const parentProvides = currentInstance.parent.provides; 9 | 10 | if (provides === parentProvides) { 11 | provides = currentInstance.provides = Object.create(parentProvides); 12 | } 13 | 14 | provides[key] = value; 15 | } 16 | } 17 | 18 | export function inject(key, defaultValue) { 19 | const currentInstance: any = getCurrentInstance(); 20 | 21 | if (currentInstance) { 22 | const parentProvides = currentInstance.parent.provides; 23 | 24 | if (key in parentProvides) { 25 | return parentProvides[key]; 26 | }else if(defaultValue){ 27 | if(typeof defaultValue === "function"){ 28 | return defaultValue() 29 | } 30 | return defaultValue 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "version": "0.2.0", 10 | "configurations": [ 11 | { 12 | "name": "Debug Jest Tests", 13 | "type": "node", 14 | "request": "launch", 15 | "runtimeArgs": [ 16 | "--inspect-brk", 17 | "${workspaceRoot}/node_modules/.bin/jest", 18 | "--runInBand" 19 | ], 20 | "console": "integratedTerminal", 21 | "internalConsoleOptions": "neverOpen", 22 | "port": "9229" 23 | } 24 | ] 25 | }, 26 | { 27 | "type": "pwa-chrome", 28 | "request": "launch", 29 | "name": "Launch Chrome against localhost", 30 | "url": "http://localhost:8080", 31 | "webRoot": "${workspaceFolder}" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from "../shared/ShapeFlags"; 2 | 3 | export const Fragment = Symbol("Fragment"); 4 | export const Text = Symbol("Text"); 5 | 6 | export function createVNode(type, props?, children?) { 7 | const vnode = { 8 | type, 9 | props, 10 | children, 11 | shapeFlag: getShapeFlag(type), 12 | el: null, 13 | }; 14 | 15 | if (typeof children === "string") { 16 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN; 17 | } else if (Array.isArray(children)) { 18 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN; 19 | } 20 | 21 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 22 | if (typeof children === "object") { 23 | vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN; 24 | } 25 | } 26 | 27 | return vnode; 28 | } 29 | 30 | export function createTextVNode(text: string) { 31 | return createVNode(Text, {}, text); 32 | } 33 | 34 | function getShapeFlag(type) { 35 | return typeof type === "string" 36 | ? ShapeFlags.ELEMENT 37 | : ShapeFlags.STATEFUL_COMPONENT; 38 | } -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from "../shared/index" 2 | import { mutableHandlers, readonlyHandlers, shallowReadonlyHandlers } from "./baseHandler" 3 | 4 | export const enum ReactiveFlags { 5 | IS_REACTIVE = "__v_isReactive", 6 | IS_READONLY = "__v_isReadonly", 7 | } 8 | 9 | export function reactive (raw) { 10 | return createReactiveObject(raw, mutableHandlers) 11 | } 12 | 13 | 14 | export function readonly(raw) { 15 | return createReactiveObject(raw, readonlyHandlers) 16 | } 17 | 18 | export function shallowReadonly (raw) { 19 | return createReactiveObject(raw, shallowReadonlyHandlers) 20 | } 21 | 22 | export function isReactive(value) { 23 | return !!value[ReactiveFlags.IS_REACTIVE] 24 | } 25 | export function isReadonly(value) { 26 | return !!value[ReactiveFlags.IS_READONLY] 27 | } 28 | 29 | export function isProxy (value) { 30 | return isReactive(value) || isReadonly(value) 31 | } 32 | 33 | function createReactiveObject(target, baseHandles) { 34 | if (!isObject(target)) { 35 | console.warn(`target ${target} 必须是一个对象`); 36 | return target 37 | } 38 | 39 | return new Proxy(target, baseHandles); 40 | } -------------------------------------------------------------------------------- /src/reactivity/tests/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "../computed"; 2 | import { reactive } from "../reactive"; 3 | 4 | describe("computed", () => { 5 | it("happy path", () => { 6 | const user = reactive({ 7 | age: 1, 8 | }); 9 | 10 | const age = computed(() => { 11 | return user.age; 12 | }); 13 | 14 | expect(age.value).toBe(1); 15 | }); 16 | 17 | it("should compute lazily", () => { 18 | const value = reactive({ 19 | foo: 1, 20 | }); 21 | const getter = jest.fn(() => { 22 | return value.foo; 23 | }); 24 | const cValue = computed(getter); 25 | 26 | // lazy 27 | expect(getter).not.toHaveBeenCalled(); 28 | 29 | expect(cValue.value).toBe(1); 30 | expect(getter).toHaveBeenCalledTimes(1); 31 | 32 | // should not compute again 33 | cValue.value; // get 34 | expect(getter).toHaveBeenCalledTimes(1); 35 | 36 | // should not compute until needed 37 | value.foo = 2; 38 | expect(getter).toHaveBeenCalledTimes(1); 39 | 40 | // now it should compute 41 | expect(cValue.value).toBe(2); 42 | expect(getter).toHaveBeenCalledTimes(2); 43 | 44 | // should not compute again 45 | cValue.value; 46 | expect(getter).toHaveBeenCalledTimes(2); 47 | }); 48 | }); -------------------------------------------------------------------------------- /example/apiInject/App.js: -------------------------------------------------------------------------------- 1 | // 组件 provide 和 inject 功能 2 | import { h, provide, inject } from "../../lib/guide-mini-vue.esm.js"; 3 | 4 | const Provider = { 5 | name: "Provider", 6 | setup() { 7 | provide("foo", "fooVal"); 8 | provide("bar", "barVal"); 9 | }, 10 | render() { 11 | return h("div", {}, [h("p", {}, "Provider"), h(ProviderTwo)]); 12 | }, 13 | }; 14 | 15 | const ProviderTwo = { 16 | name: "ProviderTwo", 17 | setup() { 18 | provide("foo", "fooTwo"); 19 | const foo = inject("foo"); 20 | 21 | return { 22 | foo, 23 | }; 24 | }, 25 | render() { 26 | return h("div", {}, [ 27 | h("p", {}, `ProviderTwo foo:${this.foo}`), 28 | h(Consumer), 29 | ]); 30 | }, 31 | }; 32 | 33 | const Consumer = { 34 | name: "Consumer", 35 | setup() { 36 | const foo = inject("foo"); 37 | const bar = inject("bar"); 38 | // const baz = inject("baz", "bazDefault"); 39 | const baz = inject("baz", () => "bazDefault"); 40 | 41 | return { 42 | foo, 43 | bar, 44 | baz, 45 | }; 46 | }, 47 | 48 | render() { 49 | return h("div", {}, `Consumer: - ${this.foo} - ${this.bar}-${this.baz}`); 50 | }, 51 | }; 52 | 53 | export default { 54 | name: "App", 55 | setup() {}, 56 | render() { 57 | return h("div", {}, [h("p", {}, "apiInject"), h(Provider)]); 58 | }, 59 | }; -------------------------------------------------------------------------------- /src/reactivity/baseHandler.ts: -------------------------------------------------------------------------------- 1 | import { extend, isObject } from "../shared"; 2 | import { track, trigger } from "./effect"; 3 | import { reactive, ReactiveFlags, readonly } from "./reactive"; 4 | 5 | const get = createGetter(); 6 | const set = createSetter(); 7 | const readonlyGet = createGetter(true); 8 | const shallowReadonlyGet = createGetter(true, true); 9 | 10 | function createGetter (isReadonly = false, shallow = false) { 11 | return function get(target, key) { 12 | 13 | if (key === ReactiveFlags.IS_REACTIVE) { 14 | return !isReadonly; 15 | } else if (key === ReactiveFlags.IS_READONLY) { 16 | return isReadonly 17 | } 18 | const res = Reflect.get(target, key) 19 | if (shallow) { 20 | return res 21 | } 22 | if (isObject(res)) { 23 | return isReadonly ? readonly(res) : reactive(res); 24 | } 25 | if (!isReadonly) { 26 | // 依赖收集 27 | track(target, key) 28 | } 29 | return res 30 | } 31 | } 32 | 33 | function createSetter() { 34 | return function set(target, key, value) { 35 | const res = Reflect.set(target, key, value); 36 | trigger(target, key); 37 | return res; 38 | }; 39 | } 40 | 41 | 42 | 43 | export const mutableHandlers = { 44 | get, 45 | set 46 | } 47 | 48 | export const readonlyHandlers = { 49 | get: readonlyGet, 50 | set(target, key) { 51 | console.warn( 52 | `key :"${String(key)}" set 失败,因为 target 是 readonly 类型`, 53 | target 54 | ); 55 | 56 | return true; 57 | }, 58 | }; 59 | 60 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 61 | get: shallowReadonlyGet, 62 | }); -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged, isObject } from "../shared" 2 | import { isTracking, trackEffects, triggerEffects } from "./effect" 3 | import { reactive } from "./reactive" 4 | 5 | class RefTmpl { 6 | private _value: any 7 | public __v_isRef = true; 8 | public dep 9 | private _rawValue: any; 10 | constructor (value) { 11 | this._rawValue = value; 12 | this._value = convert(value) 13 | this.dep = new Set() 14 | } 15 | get value () { 16 | trackRefValue(this); 17 | return this._value 18 | } 19 | set value (newValue) { 20 | if (hasChanged(newValue, this._rawValue)) { 21 | this._rawValue = newValue; 22 | this._value = convert(newValue) 23 | triggerEffects(this.dep) 24 | } 25 | 26 | } 27 | } 28 | 29 | function trackRefValue(ref) { 30 | if (isTracking()) { 31 | trackEffects(ref.dep) 32 | } 33 | } 34 | 35 | 36 | function convert(value) { 37 | return isObject(value) ? reactive(value) : value; 38 | } 39 | 40 | 41 | export function ref(value) { 42 | return new RefTmpl(value) 43 | } 44 | 45 | export function isRef(ref) { 46 | return !!ref.__v_isRef 47 | } 48 | 49 | export function unRef (ref) { 50 | return isRef(ref) ? ref.value : ref; 51 | } 52 | 53 | export function proxyRefs(objectWithRefs) { 54 | return new Proxy(objectWithRefs, { 55 | get(target, key) { 56 | return unRef(Reflect.get(target, key)) 57 | }, 58 | set(target, key, value) { 59 | if (isRef(target[key]) && !isRef(value)) { 60 | return target[key].value = value 61 | } else { 62 | return Reflect.set(target, key, value) 63 | } 64 | } 65 | }) 66 | } -------------------------------------------------------------------------------- /example/update/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/guide-mini-vue.esm.js"; 2 | 3 | export const App = { 4 | name: "App", 5 | 6 | setup() { 7 | const count = ref(0); 8 | 9 | const onClick = () => { 10 | count.value++; 11 | }; 12 | 13 | const props = ref({ 14 | foo: "foo", 15 | bar: "bar", 16 | }); 17 | const onChangePropsDemo1 = () => { 18 | props.value.foo = "new-foo"; 19 | }; 20 | 21 | const onChangePropsDemo2 = () => { 22 | props.value.foo = undefined; 23 | }; 24 | 25 | const onChangePropsDemo3 = () => { 26 | props.value = { 27 | foo: "foo", 28 | }; 29 | }; 30 | 31 | return { 32 | count, 33 | onClick, 34 | onChangePropsDemo1, 35 | onChangePropsDemo2, 36 | onChangePropsDemo3, 37 | props, 38 | }; 39 | }, 40 | render() { 41 | return h( 42 | "div", 43 | { 44 | id: "root", 45 | ...this.props, 46 | }, 47 | [ 48 | h("div", {}, "count:" + this.count), 49 | h( 50 | "button", 51 | { 52 | onClick: this.onClick, 53 | }, 54 | "click" 55 | ), 56 | h( 57 | "button", 58 | { 59 | onClick: this.onChangePropsDemo1, 60 | }, 61 | "changeProps - 值改变了 - 修改" 62 | ), 63 | 64 | h( 65 | "button", 66 | { 67 | onClick: this.onChangePropsDemo2, 68 | }, 69 | "changeProps - 值变成了 undefined - 删除" 70 | ), 71 | 72 | h( 73 | "button", 74 | { 75 | onClick: this.onChangePropsDemo3, 76 | }, 77 | "changeProps - key 在新的里面没有了 - 删除" 78 | ), 79 | ] 80 | ); 81 | }, 82 | }; -------------------------------------------------------------------------------- /src/reactivity/tests/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../effect"; 2 | import { reactive } from "../reactive"; 3 | import { isRef, proxyRefs, ref, unRef } from "../ref"; 4 | describe("ref", () => { 5 | it("happy path", () => { 6 | const a = ref(1); 7 | expect(a.value).toBe(1); 8 | }); 9 | 10 | it("should be reactive", () => { 11 | const a = ref(1); 12 | let dummy; 13 | let calls = 0; 14 | effect(() => { 15 | calls++; 16 | dummy = a.value; 17 | }); 18 | expect(calls).toBe(1); 19 | expect(dummy).toBe(1); 20 | a.value = 2; 21 | expect(calls).toBe(2); 22 | expect(dummy).toBe(2); 23 | // same value should not trigger 24 | a.value = 2; 25 | expect(calls).toBe(2); 26 | expect(dummy).toBe(2); 27 | }); 28 | 29 | it("should make nested properties reactive", () => { 30 | const a = ref({ 31 | count: 1, 32 | }); 33 | let dummy; 34 | effect(() => { 35 | dummy = a.value.count; 36 | }); 37 | expect(dummy).toBe(1); 38 | a.value.count = 2; 39 | expect(dummy).toBe(2); 40 | }); 41 | 42 | it("isRef", () => { 43 | const a = ref(1); 44 | const user = reactive({ 45 | age: 1, 46 | }); 47 | expect(isRef(a)).toBe(true); 48 | expect(isRef(1)).toBe(false); 49 | expect(isRef(user)).toBe(false); 50 | }); 51 | 52 | it("unRef", () => { 53 | const a = ref(1); 54 | expect(unRef(a)).toBe(1); 55 | expect(unRef(1)).toBe(1); 56 | }); 57 | 58 | it("proxyRefs", () => { 59 | const user = { 60 | age: ref(10), 61 | name: "xiaohong", 62 | }; 63 | 64 | const proxyUser = proxyRefs(user); 65 | expect(user.age.value).toBe(10); 66 | expect(proxyUser.age).toBe(10); 67 | expect(proxyUser.name).toBe("xiaohong"); 68 | 69 | proxyUser.age = 20; 70 | 71 | expect(proxyUser.age).toBe(20); 72 | expect(user.age.value).toBe(20); 73 | 74 | proxyUser.age = ref(10); 75 | expect(proxyUser.age).toBe(10); 76 | expect(user.age.value).toBe(10); 77 | }); 78 | }); -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import { proxyRefs } from ".."; 2 | import { shallowReadonly } from "../reactivity/reactive"; 3 | import { emit } from "./componentEmit"; 4 | import { initProps } from "./componentProps"; 5 | import { PublicInstanceProxyHandlers } from "./componentPublicInstance"; 6 | import { initSlots } from "./componentSlots"; 7 | 8 | export function createComponentInstance(vnode, parent) { 9 | const component = { 10 | vnode, 11 | type: vnode.type, 12 | setupState: {}, 13 | props: {}, 14 | slots: {}, 15 | provides: parent ? parent.provides : {}, 16 | parent, 17 | isMounted: false, 18 | subTree: {}, 19 | emit: () => {} 20 | }; 21 | component.emit = emit.bind(null, component) as any; 22 | return component; 23 | } 24 | 25 | export function setupComponent(instance) { 26 | // TODO 27 | initProps(instance, instance.vnode.props); 28 | initSlots(instance, instance.vnode.children) 29 | setupStatefulComponent(instance); 30 | } 31 | 32 | function setupStatefulComponent(instance: any) { 33 | const Component = instance.type; 34 | 35 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 36 | const { setup } = Component; 37 | 38 | if (setup) { 39 | setCurrentInstance(instance); 40 | const setupResult = setup(shallowReadonly(instance.props), { 41 | emit: instance.emit, 42 | }); 43 | setCurrentInstance(null); 44 | handleSetupResult(instance, setupResult); 45 | } 46 | } 47 | 48 | function handleSetupResult(instance, setupResult: any) { 49 | // function Object 50 | // TODO function 51 | if (typeof setupResult === "object") { 52 | instance.setupState = proxyRefs(setupResult); 53 | } 54 | 55 | finishComponentSetup(instance); 56 | } 57 | 58 | function finishComponentSetup(instance: any) { 59 | const Component = instance.type; 60 | 61 | instance.render = Component.render; 62 | } 63 | 64 | 65 | let currentInstance = null; 66 | 67 | export function getCurrentInstance() { 68 | return currentInstance; 69 | } 70 | 71 | export function setCurrentInstance(instance) { 72 | currentInstance = instance; 73 | } -------------------------------------------------------------------------------- /src/reactivity/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from "../shared"; 2 | let activeEffect; 3 | let shouldTrack; 4 | export class ReactiveEffect { 5 | private _fn: any; 6 | deps =[] 7 | active = true; 8 | onStop?: () => void; 9 | constructor (fn, public scheduler?) { 10 | this._fn = fn 11 | } 12 | run () { 13 | if (!this.active) { 14 | this._fn() 15 | } 16 | shouldTrack = true 17 | activeEffect = this 18 | const result = this._fn() 19 | shouldTrack = false 20 | return result 21 | } 22 | 23 | stop() { 24 | if (this.active) { 25 | cleanupEffect( this) 26 | if (this.onStop) { 27 | this.onStop() 28 | } 29 | } 30 | this.active = false 31 | } 32 | 33 | } 34 | 35 | function cleanupEffect(effect) { 36 | effect.deps.forEach((dep: any) => { 37 | dep.delete(effect); 38 | }); 39 | effect.deps.length = 0 40 | } 41 | 42 | 43 | const targetMap = new Map() 44 | export function track(target, key) { 45 | if (!isTracking()) return; 46 | // target -> key -> dep 47 | let depsMap = targetMap.get(target) 48 | if (!depsMap) { 49 | depsMap = new Map() 50 | targetMap.set(target, depsMap) 51 | } 52 | 53 | let dep = depsMap.get(key) 54 | if (!dep) { 55 | dep = new Set() 56 | depsMap.set(key, dep) 57 | } 58 | trackEffects(dep) 59 | } 60 | 61 | export function trackEffects (dep) { 62 | // 看看 dep 之前有没有添加过,添加过的话 那么就不添加了 63 | if (dep.has(activeEffect)) return; 64 | dep.add(activeEffect) 65 | activeEffect.deps.push(dep) 66 | } 67 | 68 | export function isTracking() { 69 | return shouldTrack && activeEffect !== undefined; 70 | } 71 | 72 | export function trigger (target, key) { 73 | let depsMap = targetMap.get(target) 74 | let dep = depsMap.get(key) 75 | triggerEffects(dep) 76 | } 77 | 78 | export function triggerEffects(dep) { 79 | for (const effect of dep) { 80 | if (effect.scheduler) { 81 | effect.scheduler() 82 | } else { 83 | effect.run() 84 | } 85 | } 86 | } 87 | 88 | type effectOptions = { 89 | scheduler?: Function; 90 | }; 91 | 92 | 93 | 94 | 95 | 96 | export function effect (fn, options: effectOptions = {}) { 97 | // fn 98 | const _effect = new ReactiveEffect(fn, options.scheduler); 99 | extend(_effect, options) 100 | _effect.run() 101 | const runner: any = _effect.run.bind(_effect) 102 | runner.effect = _effect 103 | return runner 104 | } 105 | 106 | export function stop (runner) { 107 | runner.effect.stop() 108 | } -------------------------------------------------------------------------------- /src/reactivity/tests/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect, stop } from "../effect"; 2 | import { reactive } from "../reactive"; 3 | 4 | describe("effect", () => { 5 | it("happy path", () => { 6 | const user = reactive({ 7 | age: 10, 8 | }); 9 | 10 | let nextAge; 11 | effect(() => { 12 | nextAge = user.age + 1; 13 | }); 14 | 15 | expect(nextAge).toBe(11); 16 | 17 | // update 18 | user.age++; 19 | expect(nextAge).toBe(12); 20 | }); 21 | 22 | it("should return runner when call effect", () => { 23 | // 当调用 runner 的时候可以重新执行 effect.run 24 | // runner 的返回值就是用户给的 fn 的返回值 25 | let foo = 0; 26 | const runner = effect(() => { 27 | foo++; 28 | return foo; 29 | }); 30 | 31 | expect(foo).toBe(1); 32 | runner(); 33 | expect(foo).toBe(2); 34 | expect(runner()).toBe(3); 35 | }); 36 | 37 | it("scheduler", () => { 38 | // 1.通过 effect 的第二个参数给定的一个 scheduler 的 fn 39 | // 2. effect 第一次执行的时候还会执行 fn 40 | // 3. 当响应式对象 set update 不会执行 fn 而是执行 scheduler 41 | // 4. 如果说当执行 runner 的时候,会再次的执行 fn 42 | let dummy; 43 | let run: any; 44 | let runner:any 45 | const scheduler = jest.fn(() => { 46 | run = runner; 47 | }); 48 | const obj = reactive({ foo: 1 }); 49 | runner = effect( 50 | () => { 51 | dummy = obj.foo; 52 | }, 53 | { scheduler } 54 | ); 55 | expect(scheduler).not.toHaveBeenCalled(); 56 | expect(dummy).toBe(1); 57 | // should be called on first trigger 58 | obj.foo++; 59 | expect(scheduler).toHaveBeenCalledTimes(1); 60 | // // should not run yet 61 | expect(dummy).toBe(1); 62 | // // manually run 63 | run(); 64 | // // should have run 65 | expect(dummy).toBe(2); 66 | }); 67 | 68 | 69 | it("stop", () => { 70 | let dummy; 71 | const obj = reactive({ prop: 1 }); 72 | const runner = effect(() => { 73 | dummy = obj.prop; 74 | }); 75 | obj.prop = 2; 76 | expect(dummy).toBe(2); 77 | stop(runner); 78 | // obj.prop = 3; 79 | obj.prop++ 80 | expect(dummy).toBe(2); 81 | 82 | // stopped effect should still be manually callable 83 | runner(); 84 | expect(dummy).toBe(3); 85 | }); 86 | 87 | it("onStop", () => { 88 | const obj = reactive({ 89 | foo: 1, 90 | }); 91 | const onStop = jest.fn(); 92 | let dummy; 93 | const runner = effect( 94 | () => { 95 | dummy = obj.foo; 96 | }, 97 | { 98 | onStop, 99 | } 100 | ); 101 | 102 | stop(runner); 103 | expect(onStop).toBeCalledTimes(1); 104 | }); 105 | }); -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../reactivity/effect"; 2 | import { ShapeFlags } from "../shared/ShapeFlags"; 3 | import { createComponentInstance, setupComponent } from "./component"; 4 | import { createAppAPI } from "./createApp"; 5 | import { Fragment, Text } from "./vnode"; 6 | 7 | export function createRenderer(options) { 8 | const { 9 | createElement: hostCreateElement, 10 | patchProp: hostPatchProp, 11 | insert: hostInsert, 12 | } = options; 13 | 14 | function render(vnode, container) { 15 | patch(null, vnode, container, null); 16 | } 17 | 18 | function patch(n1, n2, container, parentComponent) { 19 | const { type, shapeFlag } = n2; 20 | 21 | switch (type) { 22 | case Fragment: 23 | processFragment(n1, n2, container, parentComponent); 24 | break; 25 | case Text: 26 | processText(n1, n2, container); 27 | break; 28 | 29 | default: 30 | if (shapeFlag & ShapeFlags.ELEMENT) { 31 | processElement(n1, n2, container, parentComponent); 32 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 33 | processComponent(n1, n2, container, parentComponent); 34 | } 35 | break; 36 | } 37 | } 38 | 39 | function processText(n1, n2: any, container: any) { 40 | const { children } = n2; 41 | const textNode = (n2.el = document.createTextNode(children)); 42 | container.append(textNode); 43 | } 44 | 45 | function processFragment(n1, n2: any, container: any, parentComponent) { 46 | mountChildren(n2, container, parentComponent); 47 | } 48 | 49 | function processElement(n1, n2: any, container: any, parentComponent) { 50 | if (!n1) { 51 | mountElement(n2, container, parentComponent); 52 | } else { 53 | patchElement(n1, n2, container); 54 | } 55 | } 56 | 57 | function patchElement(n1, n2, container) { 58 | console.log("patchElement"); 59 | console.log("n1", n1); 60 | console.log("n2", n2); 61 | 62 | const oldProps = n1.props || {}; 63 | const newProps = n2.props || {}; 64 | 65 | const el = (n2.el = n1.el); 66 | 67 | patchProps(el, oldProps, newProps); 68 | } 69 | 70 | function patchProps(el, oldProps, newProps) { 71 | if (oldProps !== newProps) { 72 | for (const key in newProps) { 73 | const prevProp = oldProps[key]; 74 | const nextProp = newProps[key]; 75 | 76 | if (prevProp !== nextProp) { 77 | hostPatchProp(el, key, prevProp, nextProp); 78 | } 79 | } 80 | 81 | if (oldProps !== {}) { 82 | for (const key in oldProps) { 83 | if (!(key in newProps)) { 84 | hostPatchProp(el, key, oldProps[key], null); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | function mountElement(vnode: any, container: any, parentComponent) { 92 | const el = (vnode.el = hostCreateElement(vnode.type)); 93 | 94 | const { children, shapeFlag } = vnode; 95 | 96 | // children 97 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 98 | el.textContent = children; 99 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 100 | mountChildren(vnode, el, parentComponent); 101 | } 102 | 103 | // props 104 | const { props } = vnode; 105 | for (const key in props) { 106 | const val = props[key]; 107 | hostPatchProp(el, key, null, val); 108 | } 109 | hostInsert(el, container); 110 | } 111 | 112 | function mountChildren(vnode, container, parentComponent) { 113 | vnode.children.forEach((v) => { 114 | patch(null, v, container, parentComponent); 115 | }); 116 | } 117 | 118 | function processComponent(n1, n2: any, container: any, parentComponent) { 119 | mountComponent(n2, container, parentComponent); 120 | } 121 | 122 | function mountComponent(initialVNode: any, container, parentComponent) { 123 | const instance = createComponentInstance(initialVNode, parentComponent); 124 | 125 | setupComponent(instance); 126 | setupRenderEffect(instance, initialVNode, container); 127 | } 128 | 129 | function setupRenderEffect(instance: any, initialVNode, container) { 130 | effect(() => { 131 | if (!instance.isMounted) { 132 | console.log("init"); 133 | const { proxy } = instance; 134 | const subTree = (instance.subTree = instance.render.call(proxy)); 135 | 136 | patch(null, subTree, container, instance); 137 | 138 | initialVNode.el = subTree.el; 139 | 140 | instance.isMounted = true; 141 | } else { 142 | console.log("update"); 143 | const { proxy } = instance; 144 | const subTree = instance.render.call(proxy); 145 | const prevSubTree = instance.subTree; 146 | instance.subTree = subTree; 147 | 148 | patch(prevSubTree, subTree, container, instance); 149 | } 150 | }); 151 | } 152 | 153 | return { 154 | createApp: createAppAPI(render), 155 | }; 156 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": ["DOM", "ES6"], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | "types": ["jest"], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/guide-mini-vue.esm.js: -------------------------------------------------------------------------------- 1 | const Fragment = Symbol("Fragment"); 2 | const Text = Symbol("Text"); 3 | function createVNode(type, props, children) { 4 | const vnode = { 5 | type, 6 | props, 7 | children, 8 | shapeFlag: getShapeFlag(type), 9 | el: null, 10 | }; 11 | if (typeof children === "string") { 12 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 13 | } 14 | else if (Array.isArray(children)) { 15 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 16 | } 17 | if (vnode.shapeFlag & 2 /* STATEFUL_COMPONENT */) { 18 | if (typeof children === "object") { 19 | vnode.shapeFlag |= 16 /* SLOT_CHILDREN */; 20 | } 21 | } 22 | return vnode; 23 | } 24 | function createTextVNode(text) { 25 | return createVNode(Text, {}, text); 26 | } 27 | function getShapeFlag(type) { 28 | return typeof type === "string" 29 | ? 1 /* ELEMENT */ 30 | : 2 /* STATEFUL_COMPONENT */; 31 | } 32 | 33 | function h(type, props, children) { 34 | return createVNode(type, props, children); 35 | } 36 | 37 | function renderSlots(slots, name, props) { 38 | const slot = slots[name]; 39 | if (slot) { 40 | if (typeof slot === "function") { 41 | return createVNode(Fragment, {}, slot(props)); 42 | } 43 | } 44 | } 45 | 46 | const extend = Object.assign; 47 | const isObject = (value) => { 48 | return value !== null && typeof value === "object"; 49 | }; 50 | const hasChanged = (val, newValue) => { 51 | return !Object.is(val, newValue); 52 | }; 53 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 54 | const camelize = (str) => { 55 | return str.replace(/-(\w)/g, (_, c) => { 56 | return c ? c.toUpperCase() : ""; 57 | }); 58 | }; 59 | const capitalize = (str) => { 60 | return str.charAt(0).toUpperCase() + str.slice(1); 61 | }; 62 | const toHandlerKey = (str) => { 63 | return str ? "on" + capitalize(str) : ""; 64 | }; 65 | 66 | let activeEffect; 67 | let shouldTrack; 68 | class ReactiveEffect { 69 | constructor(fn, scheduler) { 70 | this.scheduler = scheduler; 71 | this.deps = []; 72 | this.active = true; 73 | this._fn = fn; 74 | } 75 | run() { 76 | if (!this.active) { 77 | this._fn(); 78 | } 79 | shouldTrack = true; 80 | activeEffect = this; 81 | const result = this._fn(); 82 | shouldTrack = false; 83 | return result; 84 | } 85 | stop() { 86 | if (this.active) { 87 | cleanupEffect(this); 88 | if (this.onStop) { 89 | this.onStop(); 90 | } 91 | } 92 | this.active = false; 93 | } 94 | } 95 | function cleanupEffect(effect) { 96 | effect.deps.forEach((dep) => { 97 | dep.delete(effect); 98 | }); 99 | effect.deps.length = 0; 100 | } 101 | const targetMap = new Map(); 102 | function track(target, key) { 103 | if (!isTracking()) 104 | return; 105 | // target -> key -> dep 106 | let depsMap = targetMap.get(target); 107 | if (!depsMap) { 108 | depsMap = new Map(); 109 | targetMap.set(target, depsMap); 110 | } 111 | let dep = depsMap.get(key); 112 | if (!dep) { 113 | dep = new Set(); 114 | depsMap.set(key, dep); 115 | } 116 | trackEffects(dep); 117 | } 118 | function trackEffects(dep) { 119 | // 看看 dep 之前有没有添加过,添加过的话 那么就不添加了 120 | if (dep.has(activeEffect)) 121 | return; 122 | dep.add(activeEffect); 123 | activeEffect.deps.push(dep); 124 | } 125 | function isTracking() { 126 | return shouldTrack && activeEffect !== undefined; 127 | } 128 | function trigger(target, key) { 129 | let depsMap = targetMap.get(target); 130 | let dep = depsMap.get(key); 131 | triggerEffects(dep); 132 | } 133 | function triggerEffects(dep) { 134 | for (const effect of dep) { 135 | if (effect.scheduler) { 136 | effect.scheduler(); 137 | } 138 | else { 139 | effect.run(); 140 | } 141 | } 142 | } 143 | function effect(fn, options = {}) { 144 | // fn 145 | const _effect = new ReactiveEffect(fn, options.scheduler); 146 | extend(_effect, options); 147 | _effect.run(); 148 | const runner = _effect.run.bind(_effect); 149 | runner.effect = _effect; 150 | return runner; 151 | } 152 | 153 | const get = createGetter(); 154 | const set = createSetter(); 155 | const readonlyGet = createGetter(true); 156 | const shallowReadonlyGet = createGetter(true, true); 157 | function createGetter(isReadonly = false, shallow = false) { 158 | return function get(target, key) { 159 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 160 | return !isReadonly; 161 | } 162 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 163 | return isReadonly; 164 | } 165 | const res = Reflect.get(target, key); 166 | if (shallow) { 167 | return res; 168 | } 169 | if (isObject(res)) { 170 | return isReadonly ? readonly(res) : reactive(res); 171 | } 172 | if (!isReadonly) { 173 | // 依赖收集 174 | track(target, key); 175 | } 176 | return res; 177 | }; 178 | } 179 | function createSetter() { 180 | return function set(target, key, value) { 181 | const res = Reflect.set(target, key, value); 182 | trigger(target, key); 183 | return res; 184 | }; 185 | } 186 | const mutableHandlers = { 187 | get, 188 | set 189 | }; 190 | const readonlyHandlers = { 191 | get: readonlyGet, 192 | set(target, key) { 193 | console.warn(`key :"${String(key)}" set 失败,因为 target 是 readonly 类型`, target); 194 | return true; 195 | }, 196 | }; 197 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 198 | get: shallowReadonlyGet, 199 | }); 200 | 201 | function reactive(raw) { 202 | return createReactiveObject(raw, mutableHandlers); 203 | } 204 | function readonly(raw) { 205 | return createReactiveObject(raw, readonlyHandlers); 206 | } 207 | function shallowReadonly(raw) { 208 | return createReactiveObject(raw, shallowReadonlyHandlers); 209 | } 210 | function createReactiveObject(target, baseHandles) { 211 | if (!isObject(target)) { 212 | console.warn(`target ${target} 必须是一个对象`); 213 | return target; 214 | } 215 | return new Proxy(target, baseHandles); 216 | } 217 | 218 | function emit(instance, event, ...args) { 219 | const { props } = instance; 220 | const handlerName = toHandlerKey(camelize(event)); 221 | const handler = props[handlerName]; 222 | handler && handler(...args); 223 | } 224 | 225 | function initProps(instance, rawProps) { 226 | instance.props = rawProps || {}; 227 | } 228 | 229 | const publicPropertiesMap = { 230 | $el: (i) => i.vnode.el, 231 | $slots: (i) => i.slots, 232 | }; 233 | const PublicInstanceProxyHandlers = { 234 | get({ _: instance }, key) { 235 | // setupState 236 | const { setupState, props } = instance; 237 | if (hasOwn(setupState, key)) { 238 | return setupState[key]; 239 | } 240 | else if (hasOwn(props, key)) { 241 | return props[key]; 242 | } 243 | const publicGetter = publicPropertiesMap[key]; 244 | if (publicGetter) { 245 | return publicGetter(instance); 246 | } 247 | }, 248 | }; 249 | 250 | function initSlots(instance, children) { 251 | // slots 252 | const { vnode } = instance; 253 | if (vnode.shapeFlag & 16 /* SLOT_CHILDREN */) { 254 | normalizeObjectSlots(children, instance.slots); 255 | } 256 | } 257 | function normalizeObjectSlots(children, slots) { 258 | for (const key in children) { 259 | const value = children[key]; 260 | slots[key] = (props) => normalizeSlotValue(value(props)); 261 | } 262 | } 263 | function normalizeSlotValue(value) { 264 | return Array.isArray(value) ? value : [value]; 265 | } 266 | 267 | function createComponentInstance(vnode, parent) { 268 | const component = { 269 | vnode, 270 | type: vnode.type, 271 | setupState: {}, 272 | props: {}, 273 | slots: {}, 274 | provides: parent ? parent.provides : {}, 275 | parent, 276 | isMounted: false, 277 | subTree: {}, 278 | emit: () => { } 279 | }; 280 | component.emit = emit.bind(null, component); 281 | return component; 282 | } 283 | function setupComponent(instance) { 284 | // TODO 285 | initProps(instance, instance.vnode.props); 286 | initSlots(instance, instance.vnode.children); 287 | setupStatefulComponent(instance); 288 | } 289 | function setupStatefulComponent(instance) { 290 | const Component = instance.type; 291 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 292 | const { setup } = Component; 293 | if (setup) { 294 | setCurrentInstance(instance); 295 | const setupResult = setup(shallowReadonly(instance.props), { 296 | emit: instance.emit, 297 | }); 298 | setCurrentInstance(null); 299 | handleSetupResult(instance, setupResult); 300 | } 301 | } 302 | function handleSetupResult(instance, setupResult) { 303 | // function Object 304 | // TODO function 305 | if (typeof setupResult === "object") { 306 | instance.setupState = proxyRefs(setupResult); 307 | } 308 | finishComponentSetup(instance); 309 | } 310 | function finishComponentSetup(instance) { 311 | const Component = instance.type; 312 | instance.render = Component.render; 313 | } 314 | let currentInstance = null; 315 | function getCurrentInstance() { 316 | return currentInstance; 317 | } 318 | function setCurrentInstance(instance) { 319 | currentInstance = instance; 320 | } 321 | 322 | function provide(key, value) { 323 | const currentInstance = getCurrentInstance(); 324 | if (currentInstance) { 325 | let { provides } = currentInstance; 326 | const parentProvides = currentInstance.parent.provides; 327 | if (provides === parentProvides) { 328 | provides = currentInstance.provides = Object.create(parentProvides); 329 | } 330 | provides[key] = value; 331 | } 332 | } 333 | function inject(key, defaultValue) { 334 | const currentInstance = getCurrentInstance(); 335 | if (currentInstance) { 336 | const parentProvides = currentInstance.parent.provides; 337 | if (key in parentProvides) { 338 | return parentProvides[key]; 339 | } 340 | else if (defaultValue) { 341 | if (typeof defaultValue === "function") { 342 | return defaultValue(); 343 | } 344 | return defaultValue; 345 | } 346 | } 347 | } 348 | 349 | function createAppAPI(render) { 350 | return function createApp(rootComponent) { 351 | return { 352 | mount(rootContainer) { 353 | const vnode = createVNode(rootComponent); 354 | render(vnode, rootContainer); 355 | }, 356 | }; 357 | }; 358 | } 359 | 360 | function createRenderer(options) { 361 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, } = options; 362 | function render(vnode, container) { 363 | patch(null, vnode, container, null); 364 | } 365 | function patch(n1, n2, container, parentComponent) { 366 | const { type, shapeFlag } = n2; 367 | switch (type) { 368 | case Fragment: 369 | processFragment(n1, n2, container, parentComponent); 370 | break; 371 | case Text: 372 | processText(n1, n2, container); 373 | break; 374 | default: 375 | if (shapeFlag & 1 /* ELEMENT */) { 376 | processElement(n1, n2, container, parentComponent); 377 | } 378 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 379 | processComponent(n1, n2, container, parentComponent); 380 | } 381 | break; 382 | } 383 | } 384 | function processText(n1, n2, container) { 385 | const { children } = n2; 386 | const textNode = (n2.el = document.createTextNode(children)); 387 | container.append(textNode); 388 | } 389 | function processFragment(n1, n2, container, parentComponent) { 390 | mountChildren(n2, container, parentComponent); 391 | } 392 | function processElement(n1, n2, container, parentComponent) { 393 | if (!n1) { 394 | mountElement(n2, container, parentComponent); 395 | } 396 | else { 397 | patchElement(n1, n2); 398 | } 399 | } 400 | function patchElement(n1, n2, container) { 401 | console.log("patchElement"); 402 | console.log("n1", n1); 403 | console.log("n2", n2); 404 | const oldProps = n1.props || {}; 405 | const newProps = n2.props || {}; 406 | const el = (n2.el = n1.el); 407 | patchProps(el, oldProps, newProps); 408 | } 409 | function patchProps(el, oldProps, newProps) { 410 | if (oldProps !== newProps) { 411 | for (const key in newProps) { 412 | const prevProp = oldProps[key]; 413 | const nextProp = newProps[key]; 414 | if (prevProp !== nextProp) { 415 | hostPatchProp(el, key, prevProp, nextProp); 416 | } 417 | } 418 | if (oldProps !== {}) { 419 | for (const key in oldProps) { 420 | if (!(key in newProps)) { 421 | hostPatchProp(el, key, oldProps[key], null); 422 | } 423 | } 424 | } 425 | } 426 | } 427 | function mountElement(vnode, container, parentComponent) { 428 | const el = (vnode.el = hostCreateElement(vnode.type)); 429 | const { children, shapeFlag } = vnode; 430 | // children 431 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 432 | el.textContent = children; 433 | } 434 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 435 | mountChildren(vnode, el, parentComponent); 436 | } 437 | // props 438 | const { props } = vnode; 439 | for (const key in props) { 440 | const val = props[key]; 441 | hostPatchProp(el, key, null, val); 442 | } 443 | hostInsert(el, container); 444 | } 445 | function mountChildren(vnode, container, parentComponent) { 446 | vnode.children.forEach((v) => { 447 | patch(null, v, container, parentComponent); 448 | }); 449 | } 450 | function processComponent(n1, n2, container, parentComponent) { 451 | mountComponent(n2, container, parentComponent); 452 | } 453 | function mountComponent(initialVNode, container, parentComponent) { 454 | const instance = createComponentInstance(initialVNode, parentComponent); 455 | setupComponent(instance); 456 | setupRenderEffect(instance, initialVNode, container); 457 | } 458 | function setupRenderEffect(instance, initialVNode, container) { 459 | effect(() => { 460 | if (!instance.isMounted) { 461 | console.log("init"); 462 | const { proxy } = instance; 463 | const subTree = (instance.subTree = instance.render.call(proxy)); 464 | patch(null, subTree, container, instance); 465 | initialVNode.el = subTree.el; 466 | instance.isMounted = true; 467 | } 468 | else { 469 | console.log("update"); 470 | const { proxy } = instance; 471 | const subTree = instance.render.call(proxy); 472 | const prevSubTree = instance.subTree; 473 | instance.subTree = subTree; 474 | patch(prevSubTree, subTree, container, instance); 475 | } 476 | }); 477 | } 478 | return { 479 | createApp: createAppAPI(render), 480 | }; 481 | } 482 | 483 | function createElement(type) { 484 | return document.createElement(type); 485 | } 486 | function patchProp(el, key, prevVal, nextVal) { 487 | const isOn = (key) => /^on[A-Z]/.test(key); 488 | if (isOn(key)) { 489 | const event = key.slice(2).toLowerCase(); 490 | el.addEventListener(event, nextVal); 491 | } 492 | else { 493 | if (nextVal === undefined || nextVal === null) { 494 | el.removeAttribute(key); 495 | } 496 | else { 497 | el.setAttribute(key, nextVal); 498 | } 499 | } 500 | } 501 | function insert(el, parent) { 502 | parent.append(el); 503 | } 504 | const renderer = createRenderer({ 505 | createElement, 506 | patchProp, 507 | insert, 508 | }); 509 | function createApp(...args) { 510 | return renderer.createApp(...args); 511 | } 512 | 513 | class RefTmpl { 514 | constructor(value) { 515 | this.__v_isRef = true; 516 | this._rawValue = value; 517 | this._value = convert(value); 518 | this.dep = new Set(); 519 | } 520 | get value() { 521 | trackRefValue(this); 522 | return this._value; 523 | } 524 | set value(newValue) { 525 | if (hasChanged(newValue, this._rawValue)) { 526 | this._rawValue = newValue; 527 | this._value = convert(newValue); 528 | triggerEffects(this.dep); 529 | } 530 | } 531 | } 532 | function trackRefValue(ref) { 533 | if (isTracking()) { 534 | trackEffects(ref.dep); 535 | } 536 | } 537 | function convert(value) { 538 | return isObject(value) ? reactive(value) : value; 539 | } 540 | function ref(value) { 541 | return new RefTmpl(value); 542 | } 543 | function isRef(ref) { 544 | return !!ref.__v_isRef; 545 | } 546 | function unRef(ref) { 547 | return isRef(ref) ? ref.value : ref; 548 | } 549 | function proxyRefs(objectWithRefs) { 550 | return new Proxy(objectWithRefs, { 551 | get(target, key) { 552 | return unRef(Reflect.get(target, key)); 553 | }, 554 | set(target, key, value) { 555 | if (isRef(target[key]) && !isRef(value)) { 556 | return target[key].value = value; 557 | } 558 | else { 559 | return Reflect.set(target, key, value); 560 | } 561 | } 562 | }); 563 | } 564 | 565 | export { createApp, createRenderer, createTextVNode, getCurrentInstance, h, inject, provide, proxyRefs, ref, renderSlots }; 566 | -------------------------------------------------------------------------------- /lib/guide-mini-vue.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | const Fragment = Symbol("Fragment"); 6 | const Text = Symbol("Text"); 7 | function createVNode(type, props, children) { 8 | const vnode = { 9 | type, 10 | props, 11 | children, 12 | shapeFlag: getShapeFlag(type), 13 | el: null, 14 | }; 15 | if (typeof children === "string") { 16 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 17 | } 18 | else if (Array.isArray(children)) { 19 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 20 | } 21 | if (vnode.shapeFlag & 2 /* STATEFUL_COMPONENT */) { 22 | if (typeof children === "object") { 23 | vnode.shapeFlag |= 16 /* SLOT_CHILDREN */; 24 | } 25 | } 26 | return vnode; 27 | } 28 | function createTextVNode(text) { 29 | return createVNode(Text, {}, text); 30 | } 31 | function getShapeFlag(type) { 32 | return typeof type === "string" 33 | ? 1 /* ELEMENT */ 34 | : 2 /* STATEFUL_COMPONENT */; 35 | } 36 | 37 | function h(type, props, children) { 38 | return createVNode(type, props, children); 39 | } 40 | 41 | function renderSlots(slots, name, props) { 42 | const slot = slots[name]; 43 | if (slot) { 44 | if (typeof slot === "function") { 45 | return createVNode(Fragment, {}, slot(props)); 46 | } 47 | } 48 | } 49 | 50 | const extend = Object.assign; 51 | const isObject = (value) => { 52 | return value !== null && typeof value === "object"; 53 | }; 54 | const hasChanged = (val, newValue) => { 55 | return !Object.is(val, newValue); 56 | }; 57 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 58 | const camelize = (str) => { 59 | return str.replace(/-(\w)/g, (_, c) => { 60 | return c ? c.toUpperCase() : ""; 61 | }); 62 | }; 63 | const capitalize = (str) => { 64 | return str.charAt(0).toUpperCase() + str.slice(1); 65 | }; 66 | const toHandlerKey = (str) => { 67 | return str ? "on" + capitalize(str) : ""; 68 | }; 69 | 70 | let activeEffect; 71 | let shouldTrack; 72 | class ReactiveEffect { 73 | constructor(fn, scheduler) { 74 | this.scheduler = scheduler; 75 | this.deps = []; 76 | this.active = true; 77 | this._fn = fn; 78 | } 79 | run() { 80 | if (!this.active) { 81 | this._fn(); 82 | } 83 | shouldTrack = true; 84 | activeEffect = this; 85 | const result = this._fn(); 86 | shouldTrack = false; 87 | return result; 88 | } 89 | stop() { 90 | if (this.active) { 91 | cleanupEffect(this); 92 | if (this.onStop) { 93 | this.onStop(); 94 | } 95 | } 96 | this.active = false; 97 | } 98 | } 99 | function cleanupEffect(effect) { 100 | effect.deps.forEach((dep) => { 101 | dep.delete(effect); 102 | }); 103 | effect.deps.length = 0; 104 | } 105 | const targetMap = new Map(); 106 | function track(target, key) { 107 | if (!isTracking()) 108 | return; 109 | // target -> key -> dep 110 | let depsMap = targetMap.get(target); 111 | if (!depsMap) { 112 | depsMap = new Map(); 113 | targetMap.set(target, depsMap); 114 | } 115 | let dep = depsMap.get(key); 116 | if (!dep) { 117 | dep = new Set(); 118 | depsMap.set(key, dep); 119 | } 120 | trackEffects(dep); 121 | } 122 | function trackEffects(dep) { 123 | // 看看 dep 之前有没有添加过,添加过的话 那么就不添加了 124 | if (dep.has(activeEffect)) 125 | return; 126 | dep.add(activeEffect); 127 | activeEffect.deps.push(dep); 128 | } 129 | function isTracking() { 130 | return shouldTrack && activeEffect !== undefined; 131 | } 132 | function trigger(target, key) { 133 | let depsMap = targetMap.get(target); 134 | let dep = depsMap.get(key); 135 | triggerEffects(dep); 136 | } 137 | function triggerEffects(dep) { 138 | for (const effect of dep) { 139 | if (effect.scheduler) { 140 | effect.scheduler(); 141 | } 142 | else { 143 | effect.run(); 144 | } 145 | } 146 | } 147 | function effect(fn, options = {}) { 148 | // fn 149 | const _effect = new ReactiveEffect(fn, options.scheduler); 150 | extend(_effect, options); 151 | _effect.run(); 152 | const runner = _effect.run.bind(_effect); 153 | runner.effect = _effect; 154 | return runner; 155 | } 156 | 157 | const get = createGetter(); 158 | const set = createSetter(); 159 | const readonlyGet = createGetter(true); 160 | const shallowReadonlyGet = createGetter(true, true); 161 | function createGetter(isReadonly = false, shallow = false) { 162 | return function get(target, key) { 163 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 164 | return !isReadonly; 165 | } 166 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 167 | return isReadonly; 168 | } 169 | const res = Reflect.get(target, key); 170 | if (shallow) { 171 | return res; 172 | } 173 | if (isObject(res)) { 174 | return isReadonly ? readonly(res) : reactive(res); 175 | } 176 | if (!isReadonly) { 177 | // 依赖收集 178 | track(target, key); 179 | } 180 | return res; 181 | }; 182 | } 183 | function createSetter() { 184 | return function set(target, key, value) { 185 | const res = Reflect.set(target, key, value); 186 | trigger(target, key); 187 | return res; 188 | }; 189 | } 190 | const mutableHandlers = { 191 | get, 192 | set 193 | }; 194 | const readonlyHandlers = { 195 | get: readonlyGet, 196 | set(target, key) { 197 | console.warn(`key :"${String(key)}" set 失败,因为 target 是 readonly 类型`, target); 198 | return true; 199 | }, 200 | }; 201 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 202 | get: shallowReadonlyGet, 203 | }); 204 | 205 | function reactive(raw) { 206 | return createReactiveObject(raw, mutableHandlers); 207 | } 208 | function readonly(raw) { 209 | return createReactiveObject(raw, readonlyHandlers); 210 | } 211 | function shallowReadonly(raw) { 212 | return createReactiveObject(raw, shallowReadonlyHandlers); 213 | } 214 | function createReactiveObject(target, baseHandles) { 215 | if (!isObject(target)) { 216 | console.warn(`target ${target} 必须是一个对象`); 217 | return target; 218 | } 219 | return new Proxy(target, baseHandles); 220 | } 221 | 222 | function emit(instance, event, ...args) { 223 | const { props } = instance; 224 | const handlerName = toHandlerKey(camelize(event)); 225 | const handler = props[handlerName]; 226 | handler && handler(...args); 227 | } 228 | 229 | function initProps(instance, rawProps) { 230 | instance.props = rawProps || {}; 231 | } 232 | 233 | const publicPropertiesMap = { 234 | $el: (i) => i.vnode.el, 235 | $slots: (i) => i.slots, 236 | }; 237 | const PublicInstanceProxyHandlers = { 238 | get({ _: instance }, key) { 239 | // setupState 240 | const { setupState, props } = instance; 241 | if (hasOwn(setupState, key)) { 242 | return setupState[key]; 243 | } 244 | else if (hasOwn(props, key)) { 245 | return props[key]; 246 | } 247 | const publicGetter = publicPropertiesMap[key]; 248 | if (publicGetter) { 249 | return publicGetter(instance); 250 | } 251 | }, 252 | }; 253 | 254 | function initSlots(instance, children) { 255 | // slots 256 | const { vnode } = instance; 257 | if (vnode.shapeFlag & 16 /* SLOT_CHILDREN */) { 258 | normalizeObjectSlots(children, instance.slots); 259 | } 260 | } 261 | function normalizeObjectSlots(children, slots) { 262 | for (const key in children) { 263 | const value = children[key]; 264 | slots[key] = (props) => normalizeSlotValue(value(props)); 265 | } 266 | } 267 | function normalizeSlotValue(value) { 268 | return Array.isArray(value) ? value : [value]; 269 | } 270 | 271 | function createComponentInstance(vnode, parent) { 272 | const component = { 273 | vnode, 274 | type: vnode.type, 275 | setupState: {}, 276 | props: {}, 277 | slots: {}, 278 | provides: parent ? parent.provides : {}, 279 | parent, 280 | isMounted: false, 281 | subTree: {}, 282 | emit: () => { } 283 | }; 284 | component.emit = emit.bind(null, component); 285 | return component; 286 | } 287 | function setupComponent(instance) { 288 | // TODO 289 | initProps(instance, instance.vnode.props); 290 | initSlots(instance, instance.vnode.children); 291 | setupStatefulComponent(instance); 292 | } 293 | function setupStatefulComponent(instance) { 294 | const Component = instance.type; 295 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 296 | const { setup } = Component; 297 | if (setup) { 298 | setCurrentInstance(instance); 299 | const setupResult = setup(shallowReadonly(instance.props), { 300 | emit: instance.emit, 301 | }); 302 | setCurrentInstance(null); 303 | handleSetupResult(instance, setupResult); 304 | } 305 | } 306 | function handleSetupResult(instance, setupResult) { 307 | // function Object 308 | // TODO function 309 | if (typeof setupResult === "object") { 310 | instance.setupState = proxyRefs(setupResult); 311 | } 312 | finishComponentSetup(instance); 313 | } 314 | function finishComponentSetup(instance) { 315 | const Component = instance.type; 316 | instance.render = Component.render; 317 | } 318 | let currentInstance = null; 319 | function getCurrentInstance() { 320 | return currentInstance; 321 | } 322 | function setCurrentInstance(instance) { 323 | currentInstance = instance; 324 | } 325 | 326 | function provide(key, value) { 327 | const currentInstance = getCurrentInstance(); 328 | if (currentInstance) { 329 | let { provides } = currentInstance; 330 | const parentProvides = currentInstance.parent.provides; 331 | if (provides === parentProvides) { 332 | provides = currentInstance.provides = Object.create(parentProvides); 333 | } 334 | provides[key] = value; 335 | } 336 | } 337 | function inject(key, defaultValue) { 338 | const currentInstance = getCurrentInstance(); 339 | if (currentInstance) { 340 | const parentProvides = currentInstance.parent.provides; 341 | if (key in parentProvides) { 342 | return parentProvides[key]; 343 | } 344 | else if (defaultValue) { 345 | if (typeof defaultValue === "function") { 346 | return defaultValue(); 347 | } 348 | return defaultValue; 349 | } 350 | } 351 | } 352 | 353 | function createAppAPI(render) { 354 | return function createApp(rootComponent) { 355 | return { 356 | mount(rootContainer) { 357 | const vnode = createVNode(rootComponent); 358 | render(vnode, rootContainer); 359 | }, 360 | }; 361 | }; 362 | } 363 | 364 | function createRenderer(options) { 365 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, } = options; 366 | function render(vnode, container) { 367 | patch(null, vnode, container, null); 368 | } 369 | function patch(n1, n2, container, parentComponent) { 370 | const { type, shapeFlag } = n2; 371 | switch (type) { 372 | case Fragment: 373 | processFragment(n1, n2, container, parentComponent); 374 | break; 375 | case Text: 376 | processText(n1, n2, container); 377 | break; 378 | default: 379 | if (shapeFlag & 1 /* ELEMENT */) { 380 | processElement(n1, n2, container, parentComponent); 381 | } 382 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 383 | processComponent(n1, n2, container, parentComponent); 384 | } 385 | break; 386 | } 387 | } 388 | function processText(n1, n2, container) { 389 | const { children } = n2; 390 | const textNode = (n2.el = document.createTextNode(children)); 391 | container.append(textNode); 392 | } 393 | function processFragment(n1, n2, container, parentComponent) { 394 | mountChildren(n2, container, parentComponent); 395 | } 396 | function processElement(n1, n2, container, parentComponent) { 397 | if (!n1) { 398 | mountElement(n2, container, parentComponent); 399 | } 400 | else { 401 | patchElement(n1, n2); 402 | } 403 | } 404 | function patchElement(n1, n2, container) { 405 | console.log("patchElement"); 406 | console.log("n1", n1); 407 | console.log("n2", n2); 408 | const oldProps = n1.props || {}; 409 | const newProps = n2.props || {}; 410 | const el = (n2.el = n1.el); 411 | patchProps(el, oldProps, newProps); 412 | } 413 | function patchProps(el, oldProps, newProps) { 414 | if (oldProps !== newProps) { 415 | for (const key in newProps) { 416 | const prevProp = oldProps[key]; 417 | const nextProp = newProps[key]; 418 | if (prevProp !== nextProp) { 419 | hostPatchProp(el, key, prevProp, nextProp); 420 | } 421 | } 422 | if (oldProps !== {}) { 423 | for (const key in oldProps) { 424 | if (!(key in newProps)) { 425 | hostPatchProp(el, key, oldProps[key], null); 426 | } 427 | } 428 | } 429 | } 430 | } 431 | function mountElement(vnode, container, parentComponent) { 432 | const el = (vnode.el = hostCreateElement(vnode.type)); 433 | const { children, shapeFlag } = vnode; 434 | // children 435 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 436 | el.textContent = children; 437 | } 438 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 439 | mountChildren(vnode, el, parentComponent); 440 | } 441 | // props 442 | const { props } = vnode; 443 | for (const key in props) { 444 | const val = props[key]; 445 | hostPatchProp(el, key, null, val); 446 | } 447 | hostInsert(el, container); 448 | } 449 | function mountChildren(vnode, container, parentComponent) { 450 | vnode.children.forEach((v) => { 451 | patch(null, v, container, parentComponent); 452 | }); 453 | } 454 | function processComponent(n1, n2, container, parentComponent) { 455 | mountComponent(n2, container, parentComponent); 456 | } 457 | function mountComponent(initialVNode, container, parentComponent) { 458 | const instance = createComponentInstance(initialVNode, parentComponent); 459 | setupComponent(instance); 460 | setupRenderEffect(instance, initialVNode, container); 461 | } 462 | function setupRenderEffect(instance, initialVNode, container) { 463 | effect(() => { 464 | if (!instance.isMounted) { 465 | console.log("init"); 466 | const { proxy } = instance; 467 | const subTree = (instance.subTree = instance.render.call(proxy)); 468 | patch(null, subTree, container, instance); 469 | initialVNode.el = subTree.el; 470 | instance.isMounted = true; 471 | } 472 | else { 473 | console.log("update"); 474 | const { proxy } = instance; 475 | const subTree = instance.render.call(proxy); 476 | const prevSubTree = instance.subTree; 477 | instance.subTree = subTree; 478 | patch(prevSubTree, subTree, container, instance); 479 | } 480 | }); 481 | } 482 | return { 483 | createApp: createAppAPI(render), 484 | }; 485 | } 486 | 487 | function createElement(type) { 488 | return document.createElement(type); 489 | } 490 | function patchProp(el, key, prevVal, nextVal) { 491 | const isOn = (key) => /^on[A-Z]/.test(key); 492 | if (isOn(key)) { 493 | const event = key.slice(2).toLowerCase(); 494 | el.addEventListener(event, nextVal); 495 | } 496 | else { 497 | if (nextVal === undefined || nextVal === null) { 498 | el.removeAttribute(key); 499 | } 500 | else { 501 | el.setAttribute(key, nextVal); 502 | } 503 | } 504 | } 505 | function insert(el, parent) { 506 | parent.append(el); 507 | } 508 | const renderer = createRenderer({ 509 | createElement, 510 | patchProp, 511 | insert, 512 | }); 513 | function createApp(...args) { 514 | return renderer.createApp(...args); 515 | } 516 | 517 | class RefTmpl { 518 | constructor(value) { 519 | this.__v_isRef = true; 520 | this._rawValue = value; 521 | this._value = convert(value); 522 | this.dep = new Set(); 523 | } 524 | get value() { 525 | trackRefValue(this); 526 | return this._value; 527 | } 528 | set value(newValue) { 529 | if (hasChanged(newValue, this._rawValue)) { 530 | this._rawValue = newValue; 531 | this._value = convert(newValue); 532 | triggerEffects(this.dep); 533 | } 534 | } 535 | } 536 | function trackRefValue(ref) { 537 | if (isTracking()) { 538 | trackEffects(ref.dep); 539 | } 540 | } 541 | function convert(value) { 542 | return isObject(value) ? reactive(value) : value; 543 | } 544 | function ref(value) { 545 | return new RefTmpl(value); 546 | } 547 | function isRef(ref) { 548 | return !!ref.__v_isRef; 549 | } 550 | function unRef(ref) { 551 | return isRef(ref) ? ref.value : ref; 552 | } 553 | function proxyRefs(objectWithRefs) { 554 | return new Proxy(objectWithRefs, { 555 | get(target, key) { 556 | return unRef(Reflect.get(target, key)); 557 | }, 558 | set(target, key, value) { 559 | if (isRef(target[key]) && !isRef(value)) { 560 | return target[key].value = value; 561 | } 562 | else { 563 | return Reflect.set(target, key, value); 564 | } 565 | } 566 | }); 567 | } 568 | 569 | exports.createApp = createApp; 570 | exports.createRenderer = createRenderer; 571 | exports.createTextVNode = createTextVNode; 572 | exports.getCurrentInstance = getCurrentInstance; 573 | exports.h = h; 574 | exports.inject = inject; 575 | exports.provide = provide; 576 | exports.proxyRefs = proxyRefs; 577 | exports.ref = ref; 578 | exports.renderSlots = renderSlots; 579 | --------------------------------------------------------------------------------