├── .gitignore ├── .vscode ├── settings.json └── launch.json ├── src ├── index.ts ├── compiler-core │ ├── src │ │ ├── ast.ts │ │ ├── runtimeHelpers.ts │ │ ├── transforms │ │ │ └── transformExpression.ts │ │ ├── transform.ts │ │ ├── codegen.ts │ │ └── parse.ts │ └── tests │ │ ├── __snapshots__ │ │ └── codegen.spec.ts.snap │ │ ├── transform.spec.ts │ │ ├── codegen.spec.ts │ │ └── parse.spec.ts ├── runtime-core │ ├── componentProps.ts │ ├── h.ts │ ├── componentUpdateUtils.ts │ ├── componentEmit.ts │ ├── index.ts │ ├── helpers │ │ └── renderSlots.ts │ ├── scheduler.ts │ ├── componentPublicInstance.ts │ ├── componentSlots.ts │ ├── createApp.ts │ ├── apiInject.ts │ ├── vnode.ts │ ├── components.ts │ └── renderer.ts ├── reactive │ ├── effectScope.ts │ ├── index.ts │ ├── computed.ts │ ├── test │ │ ├── effectScope.spec.ts │ │ ├── computed.spec.ts │ │ ├── watch.spec.ts │ │ ├── readonly.spec.ts │ │ ├── reactive.spec.ts │ │ ├── effect.spec.ts │ │ └── ref.spec.ts │ ├── watch.ts │ ├── reactive.ts │ ├── baseHandlers.ts │ ├── ref.ts │ └── effect.ts ├── shared │ ├── ShapeFlags.ts │ └── index.ts └── runtime-dom │ └── index.ts ├── babel.config.js ├── example ├── update_component │ ├── Child.js │ └── App.js ├── app.component │ └── App.js ├── slots │ ├── Foo.js │ └── App.js ├── getCurrentInstance │ ├── Foo.js │ └── App.js ├── index.html ├── emit │ ├── App.js │ └── Foo.js ├── nextTick │ └── App.js ├── main.js ├── provide_inject │ └── App.js ├── update_props │ └── App.js └── update_children │ ├── App.js │ └── Diff.js ├── rollup.config.js ├── filetoc.config.js ├── package.json ├── README.md ├── tsconfig.json └── lib ├── mini-vue.esm.js └── mini-vue.cjs.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5502 3 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./runtime-dom/index"; 2 | export * from "./reactive/index"; 3 | -------------------------------------------------------------------------------- /src/compiler-core/src/ast.ts: -------------------------------------------------------------------------------- 1 | export const enum NODE_TYPES { 2 | INTERPOLATION, 3 | SIMPLE_EXPRESSION, 4 | ELEMENT, 5 | TEXT, 6 | ROOT 7 | } -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance, rawProps) { 2 | // 将props挂载到实例上 3 | instance.props = rawProps; 4 | 5 | // attrs 6 | } 7 | -------------------------------------------------------------------------------- /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 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /src/compiler-core/src/runtimeHelpers.ts: -------------------------------------------------------------------------------- 1 | export const TO_DISPLAY_STRING = Symbol("toDisplayString"); 2 | 3 | export const helperMapName = { 4 | [TO_DISPLAY_STRING]: "toDisplayString", 5 | }; 6 | -------------------------------------------------------------------------------- /src/reactive/effectScope.ts: -------------------------------------------------------------------------------- 1 | export class EffectScope { 2 | effects: any[] = [] 3 | run(fn) { 4 | const res = fn() 5 | return res 6 | } 7 | } 8 | 9 | export function effectScope() {} 10 | -------------------------------------------------------------------------------- /src/reactive/index.ts: -------------------------------------------------------------------------------- 1 | export { reactive, readonly, shallowReadonly } from "./reactive"; 2 | export { computed } from "./computed"; 3 | export { ref, proxyRefs } from "./ref"; 4 | export { effect } from "./effect"; 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 | SLOT_CHILDREN = 1 << 4, 7 | } 8 | -------------------------------------------------------------------------------- /example/update_component/Child.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../../lib/mini-vue.esm.js"; 2 | 3 | export const Child = { 4 | name: "Child", 5 | setup(props) {}, 6 | render() { 7 | return h("div", {}, this.$props.msg); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [] 7 | } -------------------------------------------------------------------------------- /example/app.component/App.js: -------------------------------------------------------------------------------- 1 | import { h, resolveComponent } from "../../../lib/mini-vue.esm.js"; 2 | 3 | export default { 4 | setup() { 5 | return {}; 6 | }, 7 | render() { 8 | return h("div", {}, [h(resolveComponent("my-component")), h("div", {}, "hahaha")]); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/runtime-core/componentUpdateUtils.ts: -------------------------------------------------------------------------------- 1 | export function shouldUpdateComponent(n1, n2) { 2 | const { props: oldProps } = n1; 3 | const { props: newProps } = n2; 4 | for (const key in newProps) { 5 | if (oldProps[key] !== newProps[key]) { 6 | return true; 7 | } 8 | } 9 | return false; 10 | } 11 | -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, handleEventName } from "../shared/index"; 2 | 3 | export function emit(instance, event, ...args) { 4 | const { props } = instance; 5 | 6 | const handlerName = camelize(handleEventName(event)); 7 | const handler = props[handlerName]; 8 | 9 | handler && handler(...args); 10 | } 11 | -------------------------------------------------------------------------------- /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 "./components"; 5 | export { provide, inject } from "./apiInject"; 6 | export { nextTick } from "./scheduler"; 7 | export { createRenderer } from "./renderer"; 8 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | 3 | export default { 4 | input: "./src/index.ts", 5 | output: [ 6 | { 7 | format: "cjs", 8 | file: "lib/mini-vue.cjs.js", 9 | }, 10 | { 11 | format: "es", 12 | file: "lib/mini-vue.esm.js", 13 | }, 14 | ], 15 | plugins: [typescript()], 16 | }; 17 | -------------------------------------------------------------------------------- /example/slots/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from "../../lib/mini-vue.esm.js"; 2 | 3 | export default { 4 | setup(props) {}, 5 | render() { 6 | const foo = h("p", {}, "Foo"); 7 | const age = 18; 8 | return h("div", {}, [ 9 | renderSlots(this.$slots, age), 10 | foo, 11 | renderSlots(this.$slots, age, "footer"), 12 | ]); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /example/getCurrentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | renderSlots, 4 | getCurrentInstance, 5 | } from "../../../lib/mini-vue.esm.js"; 6 | 7 | export default { 8 | name: "Foo", 9 | setup() { 10 | const instance = getCurrentInstance(); 11 | console.log(instance); 12 | }, 13 | render() { 14 | return h("div", {}, [renderSlots(this.$slots)]); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/compiler-core/src/transforms/transformExpression.ts: -------------------------------------------------------------------------------- 1 | import { NODE_TYPES } from "../ast"; 2 | 3 | export function transformExpression(node) { 4 | if (node.type === NODE_TYPES.INTERPOLATION) { 5 | node.content = processExpression(node.content) 6 | } 7 | } 8 | 9 | function processExpression(node) { 10 | node.content = '_ctx.' + node.content 11 | return node 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/compiler-core/tests/__snapshots__/codegen.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`codegen interpolation 1`] = ` 4 | "const { toDisplayString: _toDisplayString } = Vue 5 | return function render(_ctx, _cache) {return _toDisplayString(_ctx.message)}" 6 | `; 7 | 8 | exports[`codegen string 1`] = ` 9 | " 10 | return function render(_ctx, _cache) {return \\"hi\\"}" 11 | `; 12 | -------------------------------------------------------------------------------- /filetoc.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | remoteUrl: "https://github.com/chenfan0/mini-vue3", // 仓库地址 3 | mainBranch: "main", // 主分支 4 | dirPath: "./src/", // 要生成toc的目录路径 5 | mdPath: "./README.md", // 生成的toc添加到的md文件路径 6 | excludes: ["example"], 7 | }; 8 | 9 | // 212 root.helpers.splice(root.helpers.indexOf(name), 1) 10 | // 200 11 | // if (count === 0) { 12 | // context.root.helpers.push(name) 13 | // } 14 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/getCurrentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from "../../../lib/mini-vue.esm.js"; 2 | import Foo from "./Foo.js"; 3 | 4 | export default { 5 | name: "App", 6 | setup() { 7 | const instance = getCurrentInstance(); 8 | console.log(instance); 9 | }, 10 | render() { 11 | const slots = () => h("div", {}, "slots"); 12 | return h("div", {}, [h(Foo, {}, { slots })]); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /example/emit/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../../lib/mini-vue.esm.js"; 2 | import Foo from "./Foo.js"; 3 | 4 | export const App = { 5 | setup(props, { emit }) {}, 6 | render() { 7 | return h("div", {}, [ 8 | h(Foo, { 9 | onAdd: (...args) => { 10 | console.log("onAdd", ...args); 11 | }, 12 | onFooAdd: (...args) => { 13 | console.log("onFooAdd", ...args); 14 | }, 15 | }), 16 | ]); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /example/slots/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | createTextVNode, 4 | createRenderer, 5 | } from "../../lib/mini-vue.esm.js"; 6 | import Foo from "./Foo.js"; 7 | 8 | export default { 9 | setup() {}, 10 | render() { 11 | const header = (props) => h("p", {}, "header" + props); 12 | const footer = (props) => h("p", {}, "footer" + props); 13 | return h("div", {}, [ 14 | createTextVNode("你好"), 15 | h(Foo, {}, { header, footer }), 16 | ]); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "jest", 4 | "build": "rollup -c rollup.config.js" 5 | }, 6 | "devDependencies": { 7 | "@babel/core": "^7.17.2", 8 | "@babel/preset-env": "^7.16.11", 9 | "@babel/preset-typescript": "^7.16.7", 10 | "@rollup/plugin-typescript": "^8.3.0", 11 | "@types/jest": "^27.4.0", 12 | "babel-jest": "^27.5.1", 13 | "jest": "^27.5.1", 14 | "rollup": "^2.67.2", 15 | "tslib": "^2.3.1", 16 | "typescript": "^4.5.5" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/runtime-core/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, Fragment } from "../vnode"; 2 | 3 | export function renderSlots(slots, props, name) { 4 | // 处理具名插槽 5 | if (slots[name]) { 6 | if (typeof slots[name] === "function") { 7 | return createVNode(Fragment, {}, slots[name](props)); 8 | } 9 | } 10 | 11 | // 处理默认插槽 12 | const totalSlots: any = []; 13 | 14 | for (const slot in slots) { 15 | totalSlots.push(...slots[slot](props)); 16 | } 17 | 18 | return createVNode(Fragment, {}, totalSlots); 19 | } 20 | -------------------------------------------------------------------------------- /example/emit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../../lib/mini-vue.esm.js"; 2 | 3 | export default { 4 | name: "Foo", 5 | setup(props, { emit }) { 6 | function click() { 7 | console.log("click"); 8 | emit("add", 1, 2); 9 | emit("foo-add", "foo"); 10 | } 11 | return { 12 | click, 13 | }; 14 | }, 15 | render() { 16 | const btn = h( 17 | "button", 18 | { 19 | onClick: this.click, 20 | }, 21 | "addEmit" 22 | ); 23 | return h("div", {}, [btn]); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/compiler-core/tests/transform.spec.ts: -------------------------------------------------------------------------------- 1 | import { NODE_TYPES } from "../src/ast"; 2 | import { baseParse } from "../src/parse"; 3 | import { transform } from "../src/transform"; 4 | 5 | describe("transform", () => { 6 | it("happy path", () => { 7 | const ast = baseParse("
hi,{{message}}
"); 8 | const plugin = (node) => { 9 | if (node.type === NODE_TYPES.TEXT) { 10 | node.content += 'miniVue' 11 | } 12 | } 13 | transform(ast, { 14 | nodeTransforms: [plugin] 15 | }); 16 | expect(ast.children[0].children[0].content).toBe("hi,miniVue"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/runtime-core/scheduler.ts: -------------------------------------------------------------------------------- 1 | const queue: any[] = []; 2 | let isFlushPending = false; 3 | const p = Promise.resolve(); 4 | 5 | export function nextTick(fn) { 6 | return fn ? p.then(fn) : p; 7 | } 8 | 9 | export function queueJobs(fn) { 10 | // queue.in; 11 | if (!queue.includes(fn)) { 12 | queue.push(fn); 13 | } 14 | queueFlush(); 15 | } 16 | 17 | function queueFlush() { 18 | if (isFlushPending) return; 19 | isFlushPending = true; 20 | nextTick(flushJobs); 21 | } 22 | 23 | function flushJobs() { 24 | isFlushPending = false; 25 | let job; 26 | while ((job = queue.shift())) { 27 | job && job(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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 | $props: (i) => i.props, 7 | }; 8 | 9 | export const publicInstanceProxyHandlers = { 10 | get({ _: instance }, key) { 11 | if (hasOwn(instance.setupState, key)) { 12 | return instance.setupState[key]; 13 | } else if (hasOwn(instance.props, key)) { 14 | return instance.props[key]; 15 | } 16 | 17 | const publicGetter = publicPropertiesMap[key]; 18 | if (publicGetter) { 19 | return publicGetter(instance); 20 | } 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from "../shared/index"; 2 | import { ShapeFlags } from "../shared/ShapeFlags"; 3 | 4 | export function initSlots(instance, children) { 5 | // instance.slots = Array.isArray(children) ? children : [children]; 6 | const { vnode } = instance; 7 | if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) { 8 | instance.slots = children; 9 | 10 | for (const slot in children) { 11 | const value = children[slot]; 12 | 13 | children[slot] = (props) => normalizeSlotValue(value(props)); 14 | } 15 | } 16 | } 17 | 18 | function normalizeSlotValue(value) { 19 | return isArray(value) ? value : [value]; 20 | } 21 | -------------------------------------------------------------------------------- /example/nextTick/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref, 4 | getCurrentInstance, 5 | nextTick, 6 | } from "../../../lib/mini-vue.esm.js"; 7 | 8 | export const App = { 9 | name: "APP", 10 | setup() { 11 | const count = ref(0); 12 | const instance = getCurrentInstance(); 13 | function update() { 14 | for (let i = 0; i < 100; i++) { 15 | count.value++; 16 | } 17 | debugger; 18 | nextTick(() => { 19 | console.log(instance); 20 | }); 21 | } 22 | 23 | return { 24 | count, 25 | update, 26 | }; 27 | }, 28 | render() { 29 | return h("div", {}, [ 30 | h("div", {}, `count: ${this.count}`), 31 | h("button", { onClick: this.update }, "update"), 32 | ]); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/compiler-core/tests/codegen.spec.ts: -------------------------------------------------------------------------------- 1 | import { baseParse } from "../src/parse" 2 | import {generate} from '../src/codegen' 3 | import { transform } from "../src/transform" 4 | import { transformExpression } from "../src/transforms/transformExpression" 5 | 6 | describe('codegen', () => { 7 | it('string', () => { 8 | const ast = baseParse('hi') 9 | 10 | transform(ast) 11 | const { code } = generate(ast) 12 | 13 | expect(code).toMatchSnapshot() 14 | }) 15 | 16 | it('interpolation', () => { 17 | const ast = baseParse('{{message}}') 18 | 19 | transform(ast, { 20 | nodeTransforms: [transformExpression] 21 | }) 22 | const { code } = generate(ast) 23 | 24 | expect(code).toMatchSnapshot() 25 | }) 26 | }) 27 | 28 | -------------------------------------------------------------------------------- /src/runtime-core/createApp.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "./vnode"; 2 | 3 | function createAppContext() { 4 | return { 5 | app: null as any, 6 | components: {}, 7 | }; 8 | } 9 | 10 | export function createAppAPI(render) { 11 | return function createApp(App) { 12 | const context = createAppContext(); 13 | 14 | const app = (context.app = { 15 | mount(rootComponent) { 16 | const vnode: any = createVNode(App); 17 | vnode.appContext = context; 18 | render(vnode, rootComponent, null); 19 | }, 20 | component(name: string, component?) { 21 | if (!component) { 22 | return context.components[name]; 23 | } 24 | context.components[name] = component; 25 | }, 26 | }); 27 | 28 | return app; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /example/update_component/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../../lib/mini-vue.esm.js"; 2 | 3 | import { Child } from "./Child.js"; 4 | 5 | export const App = { 6 | name: "App", 7 | setup() { 8 | const msg = ref("123"); 9 | const count = ref(0); 10 | 11 | function changeCount() { 12 | count.value++; 13 | } 14 | window.msg = msg; 15 | 16 | function changeMsg() { 17 | msg.value += "1"; 18 | } 19 | 20 | return { 21 | msg, 22 | count, 23 | changeCount, 24 | changeMsg, 25 | }; 26 | }, 27 | render() { 28 | return h("div", {}, [ 29 | h("button", { onClick: this.changeCount }, `count: ${this.count}`), 30 | h("button", { onClick: this.changeMsg }, "change msg"), 31 | h(Child, { msg: this.msg }), 32 | ]); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from "../lib/mini-vue.esm.js"; 2 | 3 | // import { App } from "./App.js"; 4 | // import { App } from "./emit/App.js"; 5 | import App from "./slots/App.js"; 6 | // import App from "./getCurrentInstance/App.js"; 7 | // import App from "./provide_inject/App.js"; 8 | // import { App } from "./update_props/App.js"; 9 | // import { App } from "./update_children/App.js"; 10 | // import { App } from "./update_children/Diff.js"; 11 | // import { App } from "./update_component/App.js"; 12 | // import { App } from "./nextTick/App.js"; 13 | // import App from "./app.component/App.js"; 14 | 15 | const app = createApp(App); 16 | 17 | app.component("myComponent", { 18 | render() { 19 | return h("h1", {}, "my-component"); 20 | }, 21 | }); 22 | 23 | app.mount(document.querySelector("#app")); 24 | -------------------------------------------------------------------------------- /src/reactive/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from "./effect"; 2 | 3 | class ComputedRefImpl { 4 | private _value; 5 | private _getter; 6 | private _effect; 7 | private _dirty = true; 8 | 9 | constructor(getter) { 10 | this._getter = getter; 11 | // 12 | this._effect = new ReactiveEffect(this._getter, () => { 13 | // scheduler函数会在依赖改变时,执行 14 | // 当computed依赖的值发生改变时,将this._dirty设置为true 15 | // 这样当下次获取.value时,this._dirty就为true,就会重新触发getter函数,获取最新的值 16 | this._dirty = true; 17 | }); 18 | } 19 | 20 | get value() { 21 | if (this._dirty) { 22 | // this._dirty 如果为true,则说明需要重新执行getter函数 23 | this._value = this._effect.run(); 24 | // 执行完getter函数将this._dirty设置为false,这样当依赖没有改变时,再次获取.value时,不会重新出发getter函数 25 | this._dirty = false; 26 | } 27 | return this._value; 28 | } 29 | } 30 | 31 | export function computed(getter) { 32 | return new ComputedRefImpl(getter); 33 | } 34 | -------------------------------------------------------------------------------- /src/runtime-core/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from "./components"; 2 | 3 | export function provide(key, value) { 4 | const instance: any = getCurrentInstance(); 5 | const parentProvides = instance.parent ? instance.parent.provides : {}; 6 | 7 | if (instance) { 8 | let { provides } = instance; 9 | if (parentProvides === provides) { 10 | // parentProvides === provides则表明是第一次进行provide 11 | provides = instance.provides = Object.create(parentProvides); 12 | } 13 | provides[key] = value; 14 | } 15 | } 16 | 17 | export function inject(key, value?: any) { 18 | const instance: any = getCurrentInstance(); 19 | 20 | if (instance) { 21 | const { provides } = instance; 22 | const injectValue = provides[key]; 23 | 24 | if (!injectValue) { 25 | if (typeof value === "function") { 26 | return value(); 27 | } else { 28 | return value; 29 | } 30 | } 31 | return injectValue; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/provide_inject/App.js: -------------------------------------------------------------------------------- 1 | import { h, provide, inject } from "../../../lib/mini-vue.esm.js"; 2 | 3 | export default { 4 | name: "App", 5 | setup() { 6 | provide("name", "App"); 7 | const name = inject("name"); 8 | return { 9 | name, 10 | }; 11 | }, 12 | render() { 13 | return h("div", {}, [h("div", {}, `App provide ${this.name}`), h(cpn1)]); 14 | }, 15 | }; 16 | 17 | const cpn1 = { 18 | name: "cpn1", 19 | setup() { 20 | const name = inject("name"); 21 | provide("name2", "cpn2"); 22 | 23 | return { 24 | name, 25 | }; 26 | }, 27 | render() { 28 | return h("div", {}, [h("div", {}, `cpn1 ${this.name}`), h(cpn2)]); 29 | }, 30 | }; 31 | 32 | const cpn2 = { 33 | name: "cpn1", 34 | setup() { 35 | const name = inject("name"); 36 | const abc = inject("abc", "abc"); 37 | return { 38 | name, 39 | abc, 40 | }; 41 | }, 42 | render() { 43 | return h("div", {}, `cpn2 ${this.name} - ${this.abc}`); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/reactive/test/effectScope.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "../reactive"; 2 | import { effect } from "../effect"; 3 | import { effectScope, EffectScope } from "../effectScope"; 4 | 5 | describe("reactivity/effect/scope", () => { 6 | it("should run", () => { 7 | const fnSpy = jest.fn(() => {}); 8 | new EffectScope().run(fnSpy); 9 | expect(fnSpy).toHaveBeenCalledTimes(1); 10 | }); 11 | 12 | it("should accept zero argument", () => { 13 | const scope = new EffectScope(); 14 | expect(scope.effects.length).toBe(0); 15 | }); 16 | 17 | it("should return run value", () => { 18 | expect(new EffectScope().run(() => 1)).toBe(1); 19 | }); 20 | 21 | // it("should collect the effects", () => { 22 | // const scope = new EffectScope(); 23 | // scope.run(() => { 24 | // let dummy; 25 | // const counter = reactive({ num: 0 }); 26 | // effect(() => (dummy = counter.num)); 27 | 28 | // expect(dummy).toBe(0); 29 | // counter.num = 7; 30 | // expect(dummy).toBe(7); 31 | // }); 32 | 33 | // expect(scope.effects.length).toBe(1); 34 | // }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/reactive/watch.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from "./effect"; 2 | 3 | export function watch(source, cb, options: any = {}) { 4 | let getter, oldValue, newValue; 5 | const job = () => { 6 | newValue = _effect.run(); 7 | cb(newValue, oldValue); 8 | // 将newValue赋值为oldValue,这样下次执行才不会出错。 9 | oldValue = newValue; 10 | }; 11 | 12 | if (typeof source === "function") { 13 | getter = source; 14 | } else { 15 | getter = () => traverse(source); 16 | } 17 | 18 | const _effect = new ReactiveEffect(getter, () => { 19 | if (options.flush === "post") { 20 | // 当flush为post,将job放到微任务中执行 21 | const p = Promise.resolve(); 22 | p.then(job); 23 | } else { 24 | job(); 25 | } 26 | }); 27 | if (options.immediate === true) { 28 | job(); 29 | } else { 30 | oldValue = _effect.run(); 31 | } 32 | } 33 | 34 | // 通用的读取操作 35 | function traverse(value, seen = new Set()) { 36 | if (typeof value !== "object" || typeof value === null || seen.has(value)) 37 | return; 38 | 39 | seen.add(value); 40 | 41 | for (const key in value) { 42 | traverse(value[key], seen); 43 | } 44 | return value; 45 | } 46 | -------------------------------------------------------------------------------- /src/reactive/test/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "../reactive"; 2 | import { computed } from "../computed"; 3 | 4 | describe("computed", () => { 5 | it("happy path", () => { 6 | const obj = reactive({ 7 | age: 10, 8 | }); 9 | const cValue = computed(() => { 10 | return obj.age; 11 | }); 12 | expect(cValue.value).toBe(10); 13 | }); 14 | 15 | it("should compute layily", () => { 16 | const value = reactive({ 17 | foo: 1, 18 | }); 19 | const getter = jest.fn(() => value.foo); 20 | const cValue = computed(getter); 21 | // lazy 22 | // 当没有调用cValue.value时,是不会调用getter函数的 23 | expect(getter).not.toHaveBeenCalled(); 24 | 25 | expect(cValue.value).toBe(1); 26 | expect(getter).toHaveBeenCalledTimes(1); 27 | // 多次获取.value不会重新调用getter 28 | cValue.value; 29 | expect(getter).toHaveBeenCalledTimes(1); 30 | 31 | // 修改依赖值 32 | value.foo = 2; 33 | // 没有.value所以不会执行getter 34 | expect(getter).toHaveBeenCalledTimes(1); 35 | 36 | expect(cValue.value).toBe(2); 37 | expect(getter).toHaveBeenCalledTimes(2); 38 | 39 | cValue.value; 40 | expect(getter).toHaveBeenCalledTimes(2); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /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 hasChange = (value, newValue) => { 8 | return !Object.is(value, newValue); 9 | }; 10 | 11 | export function hasOwn(obj, key) { 12 | return Object.prototype.hasOwnProperty.call(obj, key); 13 | } 14 | 15 | export function isArray(value) { 16 | return Array.isArray(value); 17 | } 18 | 19 | // foo-add => fooAdd 20 | export const camelize = (str: string) => { 21 | return str.replace(/-(\w)/g, (_, p: string) => { 22 | return p ? p.toUpperCase() : ""; 23 | }); 24 | }; 25 | 26 | export const capitalize = (str: string) => { 27 | return str.charAt(0).toUpperCase() + str.slice(1); 28 | }; 29 | 30 | // fooAdd => onFooAdd 31 | export const handleEventName = (name: string) => { 32 | return name ? "on" + capitalize(name) : ""; 33 | }; 34 | 35 | export function warn(...args) { 36 | console.warn(...args); 37 | } 38 | 39 | export const EMPTY_OBJ = {}; 40 | 41 | export function def(obj, key, value) { 42 | Object.defineProperty(obj, key, { 43 | configurable: true, 44 | enumerable: false, 45 | value, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/reactive/test/watch.spec.ts: -------------------------------------------------------------------------------- 1 | import { watch } from "../watch"; 2 | import { reactive } from "../reactive"; 3 | 4 | describe("watch", () => { 5 | it("happy path", () => { 6 | const obj = reactive({ 7 | age: 10, 8 | }); 9 | let old = 0; 10 | let newV = 0; 11 | 12 | // watch(obj, () => { 13 | // old++; 14 | // }); 15 | // obj.age = 11; 16 | // expect(old).toBe(1); 17 | watch( 18 | () => obj.age, 19 | (newValue, oldValue) => { 20 | old = oldValue; 21 | newV = newValue; 22 | }, 23 | { 24 | immediate: true, 25 | } 26 | ); 27 | expect(old).toBe(undefined); 28 | expect(newV).toBe(10); 29 | obj.age = 12; 30 | expect(old).toBe(10); 31 | expect(newV).toBe(12); 32 | // watch( 33 | // () => obj.age, 34 | // (oldValue, newValue) => { 35 | // console.log("1111111111"); 36 | 37 | // old = oldValue; 38 | // newV = newValue; 39 | // }, 40 | // { immediate: true } 41 | // ); 42 | // expect(old).toBe(undefined); 43 | // expect(newV).toBe(10); 44 | // obj.age = 11; 45 | 46 | // expect(old).toBe(10); 47 | // expect(newV).toBe(11); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from "../shared/index"; 2 | import { ShapeFlags } from "../shared/ShapeFlags"; 3 | 4 | export const Fragment = Symbol("Fragment"); 5 | export const Text = Symbol("Text"); 6 | 7 | export function createVNode(type, props: any = {}, children: any = []) { 8 | const vnode = { 9 | type, 10 | props, 11 | children, 12 | appContext: null, 13 | component: null, // 组件实例 14 | key: props.key || undefined, 15 | el: null, 16 | shapeFlag: getShapeFlag(type), 17 | }; 18 | 19 | if (Array.isArray(children)) { 20 | // 子节点为数组 21 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN; 22 | } else if (typeof children === "string") { 23 | // 子节点为text类型 24 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN; 25 | } 26 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 27 | if (isObject(children)) { 28 | vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN; 29 | } 30 | } 31 | 32 | return vnode; 33 | } 34 | 35 | function getShapeFlag(type) { 36 | return typeof type === "string" 37 | ? ShapeFlags.ELEMENT 38 | : ShapeFlags.STATEFUL_COMPONENT; 39 | } 40 | 41 | export function createTextVNode(text: string) { 42 | return createVNode(Text, {}, text); 43 | } 44 | -------------------------------------------------------------------------------- /src/reactive/test/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReadonly, readonly, shallowReadonly } from "../reactive"; 2 | 3 | describe("redonly", () => { 4 | it("happy path", () => { 5 | const original = { foo: 1, bar: { baz: 2 } }; 6 | const wrapped = readonly(original); 7 | expect(wrapped).not.toBe(original); 8 | expect(wrapped.foo).toBe(1); 9 | expect(isReadonly(original)).toBe(false); 10 | expect(isProxy(wrapped)).toBe(true); 11 | expect(isProxy(original)).toBe(false); 12 | }); 13 | 14 | it("warn when call set", () => { 15 | console.warn = jest.fn(); 16 | 17 | const user = readonly({ 18 | age: 10, 19 | }); 20 | 21 | user.age = 11; 22 | 23 | expect(console.warn).toBeCalled(); 24 | expect(isReadonly(user)).toBe(true); 25 | }); 26 | 27 | it("nested readonly", () => { 28 | const obj = readonly({ 29 | foo: { 30 | age: 10, 31 | }, 32 | }); 33 | expect(isReadonly(obj.foo)).toBe(true); 34 | }); 35 | 36 | it("shallowReadonly", () => { 37 | const obj = shallowReadonly({ 38 | foo: { 39 | age: 10, 40 | }, 41 | }); 42 | expect(isReadonly(obj)).toBe(true); 43 | expect(isReadonly(obj.foo)).toBe(false); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/runtime-dom/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "../runtime-core/renderer"; 2 | 3 | function createElement(type) { 4 | return document.createElement(type); 5 | } 6 | 7 | function patchProps(el, key, val) { 8 | // handle props 9 | // 如果key为 on开头并且on后面的第一个字符为大写,则认定为事件监听 10 | const isOn = (key: string) => /^on[A-Z]/.test(key); 11 | // 处理事件 12 | if (isOn(key)) { 13 | const event = key.slice(2).toLowerCase(); 14 | el.addEventListener(event, val); 15 | } else if (val !== undefined && val !== null) { 16 | el.setAttribute(key, val); 17 | } else { 18 | // 当属性值为undefined或者为null时,直接删除该属性即可 19 | el.removeAttribute(key); 20 | } 21 | } 22 | 23 | function insert(child, container, anchor) { 24 | container.insertBefore(child, anchor); 25 | } 26 | 27 | function remove(el) { 28 | const parent = el.parentNode; 29 | if (parent) { 30 | parent.removeChild(el); 31 | } 32 | } 33 | 34 | function setElementText(el, text) { 35 | el.textContent = text; 36 | } 37 | 38 | export const renderer: any = createRenderer({ 39 | createElement, 40 | patchProps, 41 | insert, 42 | remove, 43 | setElementText, 44 | }); 45 | 46 | export function createApp(...args) { 47 | return renderer.createApp(...args); 48 | } 49 | 50 | export * from "../runtime-core/index"; 51 | -------------------------------------------------------------------------------- /example/update_props/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../../lib/mini-vue.esm.js"; 2 | 3 | export const App = { 4 | name: "App", 5 | setup() { 6 | const count = ref(0); 7 | const attr = ref({ 8 | foo: "foo", 9 | bar: "bar", 10 | }); 11 | 12 | const onClick = () => { 13 | count.value++; 14 | }; 15 | const add = () => { 16 | attr.value = { 17 | foo: "foo", 18 | bar: "bar", 19 | baz: "baz", 20 | }; 21 | }; 22 | 23 | const set = () => { 24 | attr.value.foo = undefined; 25 | }; 26 | 27 | const patch = () => { 28 | attr.value.bar = "new bar"; 29 | }; 30 | 31 | const remove = () => { 32 | attr.value = { 33 | foo: "foo", 34 | }; 35 | }; 36 | 37 | return { 38 | count, 39 | onClick, 40 | attr, 41 | add, 42 | remove, 43 | patch, 44 | set, 45 | }; 46 | }, 47 | render() { 48 | // patch props demo 49 | return h("div", { ...this.attr }, [ 50 | h("div", {}, "count " + this.count), 51 | h("button", { onClick: this.onClick }, "click"), 52 | h("button", { onClick: this.set }, "设置undefined"), 53 | h("button", { onClick: this.patch }, "修改属性"), 54 | h("button", { onClick: this.add }, "添加属性"), 55 | h("button", { onClick: this.remove }, "删除属性"), 56 | ]); 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /example/update_children/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../../lib/mini-vue.esm.js"; 2 | 3 | // 老的是数组,新的是文本 4 | const text = h("div", {}, "text"); 5 | const text1 = h("div", {}, "text1"); 6 | const array = h("div", {}, [h("div", {}, "arr1"), h("div", {}, "arr2")]); 7 | // 旧子节点是数组,新子节点是文本 8 | const ArrayToText = { 9 | setup() { 10 | const isChange = ref(false); 11 | window.isChange = isChange; 12 | 13 | return { 14 | isChange, 15 | }; 16 | }, 17 | render() { 18 | const self = this; 19 | return self.isChange === true ? text : array; 20 | }, 21 | }; 22 | 23 | const TextToArray = { 24 | name: "TextToArray", 25 | setup() { 26 | const isChange = ref(false); 27 | window.isChange = isChange; 28 | 29 | return { 30 | isChange, 31 | }; 32 | }, 33 | render() { 34 | const self = this; 35 | return self.isChange === false ? text : array; 36 | }, 37 | }; 38 | 39 | const TextToText = { 40 | setup() { 41 | const isChange = ref(false); 42 | window.isChange = isChange; 43 | 44 | return { 45 | isChange, 46 | }; 47 | }, 48 | render() { 49 | const self = this; 50 | return self.isChange === false ? text : text1; 51 | }, 52 | }; 53 | 54 | export const App = { 55 | name: "App", 56 | setup() {}, 57 | render() { 58 | return h("div", {}, [ 59 | // h(ArrayToText), 60 | // h(TextToArray), 61 | h(TextToText), 62 | ]); 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/compiler-core/src/transform.ts: -------------------------------------------------------------------------------- 1 | import { NODE_TYPES } from "./ast"; 2 | import { TO_DISPLAY_STRING } from "./runtimeHelpers"; 3 | 4 | export function transform(root, options = {}) { 5 | const context = createTransformContext(root, options); 6 | 7 | traverseNode(context, root); 8 | createRootCodegen(root) 9 | 10 | root.helpers = [...context.helpers.keys()] 11 | } 12 | 13 | function createRootCodegen(root) { 14 | root.codegenNode = root.children[0] 15 | } 16 | 17 | function createTransformContext(root, options) { 18 | const context = { 19 | root, 20 | nodeTransforms: options.nodeTransforms || [], 21 | helpers: new Map(), 22 | helper(key) { 23 | context.helpers.set(key, 1) 24 | } 25 | }; 26 | 27 | return context; 28 | } 29 | 30 | function traverseNode(context, node) { 31 | const nodeTransforms: any[] = context.nodeTransforms; 32 | 33 | for (let i = 0; i < nodeTransforms.length; i++) { 34 | const nodeTransform = nodeTransforms[i] 35 | nodeTransform(node); 36 | } 37 | 38 | switch (node.type) { 39 | case NODE_TYPES.INTERPOLATION: 40 | context.helper(TO_DISPLAY_STRING) 41 | break 42 | case NODE_TYPES.ROOT: 43 | case NODE_TYPES.ELEMENT: 44 | traverseChildren(context, node.children) 45 | break 46 | default: 47 | break 48 | } 49 | } 50 | 51 | function traverseChildren(context, children) { 52 | if (children === undefined) return; 53 | 54 | for (let i = 0; i < children.length; i++) { 55 | traverseNode(context, children[i]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/reactive/reactive.ts: -------------------------------------------------------------------------------- 1 | import { def, isObject } from "../shared/index"; 2 | import { 3 | mutableHandlers, 4 | shallowReactiveHandlers, 5 | readonlyHandlers, 6 | shallowReadonlyHandlers, 7 | } from "./baseHandlers"; 8 | 9 | export const enum ReactiveFlags { 10 | IS_REACTIVE = "__v_isReactive", 11 | IS_READONLY = "__v_isReadonly", 12 | RAW = "__v_raw", 13 | SKIP = "__v_skip", 14 | } 15 | 16 | function getTargetType(raw) { 17 | return raw[ReactiveFlags.SKIP]; 18 | } 19 | 20 | function createReactiveObj(raw: any, baseHandlers) { 21 | if (!isObject(raw)) { 22 | console.error(`${raw} 必须是一个对象`); 23 | } 24 | if (getTargetType(raw)) return raw; 25 | return new Proxy(raw, baseHandlers); 26 | } 27 | 28 | export function reactive(raw: any) { 29 | return createReactiveObj(raw, mutableHandlers); 30 | } 31 | 32 | export function shallowReactive(raw: any) { 33 | return createReactiveObj(raw, shallowReactiveHandlers); 34 | } 35 | 36 | export function readonly(raw) { 37 | return createReactiveObj(raw, readonlyHandlers); 38 | } 39 | 40 | export function shallowReadonly(raw) { 41 | return createReactiveObj(raw, shallowReadonlyHandlers); 42 | } 43 | 44 | export function isReactive(value) { 45 | return !!value[ReactiveFlags.IS_REACTIVE]; 46 | } 47 | 48 | export function isReadonly(value) { 49 | return !!value[ReactiveFlags.IS_READONLY]; 50 | } 51 | 52 | export function isProxy(value) { 53 | return isReactive(value) || isReadonly(value); 54 | } 55 | 56 | export function toRaw(observed) { 57 | const raw = observed && observed[ReactiveFlags.RAW]; 58 | return raw ? raw : observed; 59 | } 60 | 61 | export function markRaw(value) { 62 | def(value, ReactiveFlags.SKIP, true); 63 | return value; 64 | } 65 | -------------------------------------------------------------------------------- /src/reactive/test/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../effect"; 2 | import { reactive, isReactive, isProxy, shallowReactive, toRaw, markRaw } from "../reactive"; 3 | 4 | describe("reactive", () => { 5 | it("happy path", () => { 6 | const obj = { 7 | age: 18, 8 | }; 9 | const proxyObj: any = reactive(obj); 10 | expect(obj).not.toBe(proxyObj); 11 | proxyObj.age++; 12 | expect(proxyObj.age).toBe(19); 13 | expect(isReactive(proxyObj)).toBe(true); 14 | expect(isReactive(obj)).toBe(false); 15 | expect(isProxy(obj)).toBe(false); 16 | expect(isProxy(proxyObj)).toBe(true); 17 | }); 18 | it("nest reactive", () => { 19 | const obj = reactive({ 20 | foo: { 21 | age: 10, 22 | }, 23 | bar: 10, 24 | }); 25 | let dummy; 26 | effect(() => { 27 | dummy = obj.foo; 28 | }); 29 | expect(isReactive(obj.foo)).toBe(true); 30 | obj.foo = 1; 31 | expect(dummy).toBe(1); 32 | expect(obj.bar).toBe(10); 33 | }); 34 | 35 | it("shallowRef", () => { 36 | const obj = { 37 | foo: { 38 | name: 123, 39 | }, 40 | }; 41 | const shallow: any = shallowReactive(obj); 42 | expect(isReactive(shallow)).toBe(true); 43 | expect(isReactive(shallow.foo)).toBe(false); 44 | }); 45 | 46 | it('toRaw', () => { 47 | const original = { foo: 1 } 48 | const observed = reactive(original) 49 | expect(toRaw(observed)).toBe(original) 50 | expect(toRaw(original)).toBe(original) 51 | }) 52 | 53 | it('markRaw', () => { 54 | const foo = markRaw({}) 55 | expect(isReactive(reactive(foo))).toBe(false) 56 | 57 | const bar = reactive({ foo }) 58 | expect(isReactive(bar.foo)).toBe(false) 59 | }) 60 | }); 61 | -------------------------------------------------------------------------------- /src/reactive/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { track, trigger } from "./effect"; 2 | 3 | import { reactive, ReactiveFlags, readonly } from "./reactive"; 4 | 5 | import { isObject, extend } from "../shared/index"; 6 | 7 | function createGetter(isReadonly = false, isShallow = false) { 8 | return function get(target, key, receiver) { 9 | const res = Reflect.get(target, key, receiver); 10 | 11 | if (key === ReactiveFlags.IS_REACTIVE) { 12 | return !isReadonly; 13 | } else if (key === ReactiveFlags.IS_READONLY) { 14 | return isReadonly; 15 | } else if (key === ReactiveFlags.RAW) { 16 | return target; 17 | } 18 | 19 | if (!isReadonly) { 20 | track(target, key); 21 | } 22 | 23 | if (isObject(res) && !isShallow) { 24 | return isReadonly ? readonly(res) : reactive(res); 25 | } 26 | 27 | return res; 28 | }; 29 | } 30 | 31 | function createSetter() { 32 | return function set(target, key, newValue, receiver) { 33 | const res = Reflect.set(target, key, newValue, receiver); 34 | // 触发依赖 35 | trigger(target, key); 36 | 37 | return res; 38 | }; 39 | } 40 | 41 | const get = createGetter(); 42 | const set = createSetter(); 43 | const readonlyGet = createGetter(true); 44 | const shallowReactiveGet = createGetter(false, true); 45 | const shallowReadonlyGet = createGetter(true, true); 46 | 47 | export const mutableHandlers = { 48 | get, 49 | set, 50 | }; 51 | 52 | export const shallowReactiveHandlers = { 53 | get: shallowReactiveGet, 54 | set, 55 | }; 56 | 57 | export const readonlyHandlers = { 58 | get: readonlyGet, 59 | set(target) { 60 | console.warn(`${target} is a readonly can not be set`); 61 | return true; 62 | }, 63 | }; 64 | 65 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 66 | get: shallowReadonlyGet, 67 | }); 68 | -------------------------------------------------------------------------------- /example/update_children/Diff.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../../lib/mini-vue.esm.js"; 2 | 3 | // 左侧对比 4 | // const prev = h("div", {}, [ 5 | // h("div", { key: "a" }, "a"), 6 | // h("div", { key: "b" }, "b"), 7 | // h("div", { key: "c" }, "c"), 8 | // ]); 9 | 10 | // const next = h("div", {}, [ 11 | // h("div", { key: "a" }, "a"), 12 | // h("div", { key: "b" }, "b"), 13 | // h("div", { key: "c" }, "c"), 14 | // h("div", { key: "d" }, "d"), 15 | // h("div", { key: "e" }, "e"), 16 | // ]); 17 | 18 | // // 右侧对比 19 | // const prev = h("div", {}, [ 20 | // h("div", { key: "c" }, "c"), 21 | // h("div", { key: "d" }, "d"), 22 | // h("div", { key: "e" }, "e"), 23 | // ]); 24 | 25 | // const next = h("div", {}, [ 26 | // h("div", { key: "a" }, "a"), 27 | // h("div", { key: "b" }, "b"), 28 | // h("div", { key: "c", id: "c" }, "c"), 29 | // h("div", { key: "d" }, "d"), 30 | // h("div", { key: "e" }, "e"), 31 | // ]); 32 | 33 | const prev = h("div", {}, [ 34 | h("div", { key: "a" }, "a"), 35 | h("div", { key: "b" }, "b"), 36 | h("div", { key: "c" }, "c"), 37 | h("div", { key: "d" }, "d"), 38 | h("div", { key: "e" }, "e"), 39 | h("div", { key: "z" }, "z"), 40 | h("div", { key: "f" }, "f"), 41 | h("div", { key: "g" }, "g"), 42 | ]); 43 | 44 | const next = h("div", {}, [ 45 | h("div", { key: "a" }, "a"), 46 | h("div", { key: "b" }, "b"), 47 | h("div", { key: "d" }, "d"), 48 | h("div", { key: "c" }, "c"), 49 | h("div", { key: "y" }, "y"), 50 | h("div", { key: "e" }, "e"), 51 | h("div", { key: "f" }, "f"), 52 | h("div", { key: "g" }, "g"), 53 | ]); 54 | 55 | export const App = { 56 | setup() { 57 | const isChange = ref(true); 58 | window.isChange = isChange; 59 | return { 60 | isChange, 61 | }; 62 | }, 63 | render() { 64 | return this.isChange === true ? prev : next; 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /src/compiler-core/src/codegen.ts: -------------------------------------------------------------------------------- 1 | import { NODE_TYPES } from "./ast"; 2 | import { helperMapName, TO_DISPLAY_STRING } from "./runtimeHelpers"; 3 | 4 | export function generate(ast) { 5 | const context = createCodegenContext(); 6 | const { push } = context; 7 | 8 | if (ast.helpers.length > 0) { 9 | genFunctionPreamble(ast, context); 10 | } 11 | 12 | push("\n"); 13 | push("return "); 14 | 15 | const args = ["_ctx", "_cache"]; 16 | const functionName = "render"; 17 | 18 | push(`function ${functionName}(${args.join(", ")}) {`); 19 | push('return ') 20 | geneNode(ast.codegenNode, context); 21 | push(`}`); 22 | 23 | return { 24 | code: context.code, 25 | }; 26 | 27 | function genFunctionPreamble(ast, context) { 28 | const { push } = context; 29 | const VueBinging = "Vue"; 30 | const aliasHelper = (s) => `${helperMapName[s]}: _${helperMapName[s]}`; 31 | push( 32 | `const { ${ast.helpers.map(aliasHelper).join(", ")} } = ${VueBinging}` 33 | ); 34 | } 35 | } 36 | 37 | function createCodegenContext() { 38 | const context = { 39 | code: "", 40 | push(source: string) { 41 | context.code += source; 42 | }, 43 | helper(key) { 44 | return `_${helperMapName[key]}` 45 | } 46 | }; 47 | 48 | return context; 49 | } 50 | 51 | function geneNode(node, context) { 52 | switch (node.type) { 53 | case NODE_TYPES.TEXT: 54 | genText(node, context); 55 | break; 56 | case NODE_TYPES.INTERPOLATION: 57 | genInterpolation(node, context); 58 | break; 59 | case NODE_TYPES.SIMPLE_EXPRESSION: 60 | genExpression(node, context) 61 | break 62 | default: 63 | break 64 | } 65 | } 66 | function genText(node, context) { 67 | const { push } = context; 68 | push(`"${node.content}"`); 69 | } 70 | 71 | function genInterpolation(node, context) { 72 | const { push, helper } = context; 73 | 74 | push(`${helper(TO_DISPLAY_STRING)}(`); 75 | geneNode(node.content, context) 76 | push(')') 77 | } 78 | 79 | function genExpression(node, context) { 80 | const { push } = context 81 | push(`${node.content}`) 82 | } 83 | -------------------------------------------------------------------------------- /src/runtime-core/components.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from "../shared/index"; 2 | import { publicInstanceProxyHandlers } from "./componentPublicInstance"; 3 | import { initProps } from "./componentProps"; 4 | import { shallowReadonly } from "../reactive/reactive"; 5 | import { emit } from "./componentEmit"; 6 | import { initSlots } from "./componentSlots"; 7 | import { proxyRefs } from "../index"; 8 | 9 | export let currentInstance = null; 10 | 11 | export function createComponentInstance(vnode, parent) { 12 | const component = { 13 | vnode, 14 | type: vnode.type, 15 | setupState: {}, 16 | props: {}, 17 | slots: {}, 18 | provides: parent ? parent.provides : {}, 19 | isMounted: false, 20 | subTree: {}, 21 | next: null, 22 | update: null, 23 | emit: () => {}, 24 | }; 25 | 26 | component.emit = emit.bind(null, component) as any; 27 | 28 | return component; 29 | } 30 | 31 | export function setupComponent(instance) { 32 | const rawProps = instance.vnode.props; 33 | 34 | initProps(instance, rawProps); 35 | initSlots(instance, instance.vnode.children); 36 | setupStatefulComponent(instance); 37 | } 38 | 39 | function setupStatefulComponent(instance) { 40 | const component = instance.type; 41 | const props = instance.vnode.props; 42 | const emit = instance.emit; 43 | 44 | // ctx 代理 45 | instance.proxy = new Proxy({ _: instance }, publicInstanceProxyHandlers); 46 | 47 | const { setup } = component; 48 | 49 | if (setup) { 50 | setCurrentInstance(instance); 51 | const setupResult = setup(shallowReadonly(props), { emit }); 52 | setCurrentInstance(null); 53 | handleSetupResult(instance, setupResult); 54 | } 55 | } 56 | 57 | function handleSetupResult(instance, setupResult) { 58 | // TODO function 59 | 60 | // 如果返回的是一个对象,会将返回的值注入到instance中 61 | if (isObject(setupResult)) { 62 | instance.setupState = proxyRefs(setupResult); 63 | } 64 | 65 | finishComponentSetup(instance); 66 | } 67 | 68 | function finishComponentSetup(instance) { 69 | const component = instance.type; 70 | 71 | if (component.render) { 72 | instance.render = component.render; 73 | } 74 | } 75 | 76 | export function getCurrentInstance() { 77 | return currentInstance; 78 | } 79 | 80 | function setCurrentInstance(instance) { 81 | currentInstance = instance; 82 | } 83 | -------------------------------------------------------------------------------- /src/reactive/ref.ts: -------------------------------------------------------------------------------- 1 | import { trackEffects, triggerEffects, isTracking } from "./effect"; 2 | import { reactive, isProxy } from "./reactive"; 3 | 4 | import { hasChange, isObject } from "../shared/index"; 5 | 6 | class RefImplement { 7 | private _value; 8 | private deps; 9 | private _rawValue; 10 | public readonly __v_isRef = true; 11 | 12 | constructor(value) { 13 | this._rawValue = value; 14 | this._value = convert(value); 15 | this.deps = new Set(); 16 | } 17 | 18 | get value() { 19 | if (isTracking()) { 20 | trackEffects(this.deps); 21 | } 22 | return this._value; 23 | } 24 | 25 | set value(newValue) { 26 | if (!hasChange(this._rawValue, newValue)) return; 27 | this._rawValue = newValue; 28 | this._value = convert(newValue); 29 | 30 | triggerEffects(this.deps); 31 | } 32 | } 33 | 34 | function convert(value) { 35 | return isObject(value) ? reactive(value) : value; 36 | } 37 | 38 | export function ref(value) { 39 | return new RefImplement(value); 40 | } 41 | 42 | export function isRef(ref) { 43 | if (!ref) return false; 44 | return !!ref.__v_isRef; 45 | } 46 | 47 | export function unRef(ref) { 48 | return isRef(ref) ? ref.value : ref; 49 | } 50 | 51 | export function proxyRefs(objWithRefs) { 52 | return new Proxy(objWithRefs, { 53 | get(target, key, receiver) { 54 | return unRef(Reflect.get(target, key, receiver)); 55 | }, 56 | set(target, key, newValue, receiver) { 57 | // 当原来值是ref类型,而修改的值不是ref类型 58 | if (isRef(target[key]) && !isRef(newValue)) { 59 | return (target[key].value = newValue); 60 | } 61 | return Reflect.set(target, key, newValue, receiver); 62 | }, 63 | }); 64 | } 65 | 66 | class ObjRefImple { 67 | public readonly __v_isRef = true; 68 | 69 | constructor(private readonly _object, private readonly _key) { } 70 | 71 | get value() { 72 | return this._object[this._key] 73 | } 74 | 75 | set value(newVal) { 76 | this._object[this._key] = newVal 77 | } 78 | } 79 | 80 | export function toRef(object, key) { 81 | const val = object[key]; 82 | return isRef(val) ? val : new ObjRefImple(object, key); 83 | } 84 | 85 | export function toRefs(object) { 86 | if (!isProxy(object)) { 87 | console.warn(`toRefs() expects a reactive object but received a plain one.`) 88 | } 89 | const ret = Array.isArray(object) ? new Array(object.length) : {} 90 | for (const key in object) { 91 | ret[key] = isRef(object[key]) ? object[key] : new ObjRefImple(object, key) 92 | } 93 | return ret 94 | } 95 | -------------------------------------------------------------------------------- /src/compiler-core/tests/parse.spec.ts: -------------------------------------------------------------------------------- 1 | import { NODE_TYPES } from "../src/ast"; 2 | import { baseParse } from "../src/parse"; 3 | 4 | describe("Parse", () => { 5 | describe("interpolation", () => { 6 | test("simple interpolation", () => { 7 | const ast: any = baseParse("{{message}}"); 8 | 9 | expect(ast.children[0]).toStrictEqual({ 10 | type: NODE_TYPES.INTERPOLATION, 11 | content: { 12 | type: NODE_TYPES.SIMPLE_EXPRESSION, 13 | content: "message", 14 | }, 15 | }); 16 | }); 17 | }); 18 | 19 | describe("element", () => { 20 | test("simple element div", () => { 21 | const ast: any = baseParse("
"); 22 | 23 | expect(ast.children[0]).toStrictEqual({ 24 | type: NODE_TYPES.ELEMENT, 25 | tag: "div", 26 | children: [], 27 | }); 28 | }); 29 | }); 30 | 31 | describe("text", () => { 32 | test("simple text", () => { 33 | const ast: any = baseParse("some text"); 34 | 35 | expect(ast.children[0]).toStrictEqual({ 36 | type: NODE_TYPES.TEXT, 37 | content: "some text", 38 | }); 39 | }); 40 | }); 41 | 42 | test("hello word", () => { 43 | const ast = baseParse("

hi,{{message}}

"); 44 | 45 | expect(ast.children[0]).toStrictEqual({ 46 | type: NODE_TYPES.ELEMENT, 47 | tag: "p", 48 | children: [ 49 | { 50 | type: NODE_TYPES.TEXT, 51 | content: "hi,", 52 | }, 53 | { 54 | type: NODE_TYPES.INTERPOLATION, 55 | content: { 56 | type: NODE_TYPES.SIMPLE_EXPRESSION, 57 | content: "message", 58 | }, 59 | }, 60 | ], 61 | }); 62 | }); 63 | 64 | test("lack end tag", () => { 65 | expect(() => baseParse("

")).toThrow('div标签没有结束标签'); 66 | }); 67 | 68 | test("nested element", () => { 69 | const ast = baseParse("

hi,

{{message}}
"); 70 | 71 | expect(ast.children[0]).toStrictEqual({ 72 | type: NODE_TYPES.ELEMENT, 73 | tag: "div", 74 | children: [ 75 | { 76 | type: NODE_TYPES.ELEMENT, 77 | tag: "p", 78 | children: [ 79 | { 80 | type: NODE_TYPES.TEXT, 81 | content: "hi,", 82 | }, 83 | ], 84 | }, 85 | { 86 | type: NODE_TYPES.INTERPOLATION, 87 | content: { 88 | type: NODE_TYPES.SIMPLE_EXPRESSION, 89 | content: "message", 90 | }, 91 | }, 92 | ], 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/reactive/test/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "../reactive"; 2 | import { effect, stop } from "../effect"; 3 | 4 | describe("effect", () => { 5 | it("happy path", () => { 6 | const obj = reactive({ 7 | age: 18, 8 | }); 9 | let nextAge; 10 | effect(() => { 11 | nextAge = obj.age + 1; 12 | }); 13 | 14 | expect(nextAge).toBe(19); 15 | 16 | obj.age++; 17 | 18 | expect(nextAge).toBe(20); 19 | }); 20 | 21 | it("runner", () => { 22 | // effect函数会返回一个runner函数,该函数实际上就是传入的fn函数 23 | let foo = 10; 24 | const runner: any = effect(() => { 25 | foo++; 26 | return "foo"; 27 | }); 28 | expect(foo).toBe(11); 29 | 30 | const r = runner(); 31 | 32 | expect(r).toBe("foo"); 33 | expect(foo).toBe(12); 34 | }); 35 | 36 | it("sheduler", () => { 37 | // 当有传递scheduler参数时,第一次执行还是执行effct的第一个参数fn 38 | // 当数据发生变化时,执行的是scheduler函数 39 | let dummy; 40 | let run: any; 41 | const scheduler = jest.fn(() => { 42 | run = runner; 43 | }); 44 | const obj = reactive({ foo: 1 }); 45 | const runner = effect( 46 | () => { 47 | dummy = obj.foo; 48 | }, 49 | { 50 | scheduler, 51 | } 52 | ); 53 | // 一开始scheduler不会被调用 54 | expect(scheduler).not.toHaveBeenCalled(); 55 | // effect函数的第一个参数fn会被调用 56 | expect(dummy).toBe(1); 57 | 58 | // 当obj发送变化时 59 | obj.foo++; 60 | // 调用scheduler 61 | expect(scheduler).toHaveBeenCalledTimes(1); 62 | // 不会执行effect的第一个参数 63 | expect(dummy).toBe(1); 64 | 65 | run(); 66 | 67 | expect(dummy).toBe(2); 68 | }); 69 | 70 | it("stop", () => { 71 | let dummy; 72 | const obj = reactive({ prop: 1 }); 73 | const runner = effect(() => { 74 | dummy = obj.prop; 75 | }); 76 | obj.prop = 2; 77 | expect(dummy).toBe(2); 78 | stop(runner); 79 | // obj.prop = 3; 80 | obj.prop++; 81 | expect(dummy).toBe(2); 82 | runner(); 83 | expect(dummy).toBe(3); 84 | }); 85 | 86 | it("onStop", () => { 87 | const obj = reactive({ 88 | foo: 1, 89 | }); 90 | const onStop = jest.fn(); 91 | let dummy; 92 | const runner = effect( 93 | () => { 94 | dummy = obj.foo; 95 | }, 96 | { 97 | onStop, 98 | } 99 | ); 100 | // 调用stop后,onStop会被执行 101 | stop(runner); 102 | expect(onStop).toBeCalledTimes(1); 103 | }); 104 | 105 | 106 | it("nested effect", () => { 107 | let dummy; 108 | let dummy1; 109 | let obj = reactive({ 110 | count: 1, 111 | }); 112 | 113 | // effect1 effect2 114 | // 执行effect1过程又会 115 | effect(() => { 116 | console.log(111); 117 | effect(() => { 118 | console.log(222); 119 | 120 | dummy = obj.count; 121 | }); 122 | dummy1 = obj.count; 123 | }); 124 | 125 | expect(dummy).toBe(1); 126 | expect(dummy1).toBe(1); 127 | obj.count = 2; 128 | expect(dummy).toBe(2); 129 | expect(dummy1).toBe(2); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/reactive/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from "../shared/index"; 2 | 3 | // 这里使用一个变量保存活跃的effect, 4 | // 但是如果存在effect嵌套的情况,就会有问题,需要把这个变成一个栈。 5 | let activeEffect: ReactiveEffect | undefined; 6 | const effectStack: ReactiveEffect[] = []; 7 | let shouldTrack; 8 | const targetMap = new WeakMap(); 9 | 10 | const trackStack: boolean[] = []; 11 | 12 | export function pauseTracking() { 13 | trackStack.push(shouldTrack); 14 | shouldTrack = false; 15 | } 16 | 17 | export function enableTracking() { 18 | trackStack.push(shouldTrack); 19 | shouldTrack = true; 20 | } 21 | 22 | export function resetTracking() { 23 | const last = trackStack.pop(); 24 | shouldTrack = last === undefined ? true : last; 25 | } 26 | 27 | export class ReactiveEffect { 28 | // effect传递的第一个参数 29 | private _fn: (...args) => void; 30 | // effect传递的第二个对象的scheduler属性。如果有传该参数,则trigger是会触发该函数 31 | // 调用stop函数会执行该函数 32 | onStop?: () => void; 33 | // 该变量用来记录是否调过stop函数 34 | active = true; 35 | // 收集该effect的dep 36 | deps: any[] = []; 37 | 38 | constructor(fn, public scheduler?) { 39 | this._fn = fn; 40 | this.scheduler = scheduler; 41 | } 42 | 43 | run(...args) { 44 | if (!this.active) { 45 | return this._fn(...args); 46 | } 47 | if (!effectStack.includes(this)) { 48 | effectStack.push((activeEffect = this)); 49 | 50 | // 51 | enableTracking() 52 | 53 | const res = this._fn(...args); 54 | resetTracking() 55 | effectStack.pop(); 56 | const n = effectStack.length; 57 | activeEffect = n > 0 ? effectStack[n - 1] : undefined; 58 | return res; 59 | } 60 | } 61 | 62 | stop() { 63 | if (this.active) { 64 | cleanupEffect(this); 65 | this.onStop && this.onStop(); 66 | this.active = false; 67 | } 68 | } 69 | } 70 | 71 | function cleanupEffect(effect: ReactiveEffect) { 72 | effect.deps.forEach((dep) => { 73 | dep.delete(effect); 74 | }); 75 | // 清空effect.deps 76 | effect.deps.length = 0; 77 | } 78 | 79 | export function track(target, key) { 80 | if (!isTracking()) return; 81 | 82 | let depsMap = targetMap.get(target); 83 | if (!depsMap) { 84 | depsMap = new Map(); 85 | targetMap.set(target, depsMap); 86 | } 87 | let dep = depsMap.get(key); 88 | if (!dep) { 89 | dep = new Set(); 90 | depsMap.set(key, dep); 91 | } 92 | trackEffects(dep); 93 | } 94 | 95 | export function trackEffects(dep) { 96 | dep.add(activeEffect); 97 | 98 | activeEffect!.deps.push(dep); 99 | } 100 | 101 | export function isTracking() { 102 | return shouldTrack && activeEffect !== undefined; 103 | } 104 | 105 | export function trigger(target, key) { 106 | const depsMap = targetMap.get(target); 107 | if (!depsMap) return; 108 | const dep = depsMap.get(key); 109 | 110 | triggerEffects(dep); 111 | } 112 | 113 | export function triggerEffects(dep) { 114 | if (!dep) return; 115 | 116 | for (const effect of dep) { 117 | if (effect.scheduler) { 118 | effect.scheduler(); 119 | } else { 120 | effect.run(); 121 | } 122 | } 123 | } 124 | 125 | export function effect(fn, options: any = {}) { 126 | const _effect = new ReactiveEffect(fn, options.scheduler); 127 | 128 | extend(_effect, options); 129 | _effect.run(); 130 | 131 | const runner: any = _effect.run.bind(_effect); 132 | runner.effect = _effect; 133 | 134 | return runner; 135 | } 136 | 137 | export function stop(runner) { 138 | runner.effect.stop(); 139 | } 140 | -------------------------------------------------------------------------------- /src/compiler-core/src/parse.ts: -------------------------------------------------------------------------------- 1 | import { NODE_TYPES } from "./ast"; 2 | 3 | const enum TAG_TYPE { 4 | START, 5 | END, 6 | } 7 | 8 | export function baseParse(content: string) { 9 | const context = createParseContext(content); 10 | 11 | return createRoot(createParseChildren(context, [])); 12 | } 13 | 14 | function createParseContext(content: string) { 15 | return { 16 | source: content, 17 | }; 18 | } 19 | 20 | function createRoot(children) { 21 | return { 22 | helpers: [], 23 | children, 24 | type: NODE_TYPES.ROOT 25 | }; 26 | } 27 | 28 | function createParseChildren(context, ancestors) { 29 | const nodes: any[] = []; 30 | while (!isEnd(context, ancestors)) { 31 | let node; 32 | const s = context.source; 33 | if (s.startsWith("{{")) { 34 | node = parseInterpolation(context); 35 | } else if (s[0] === "<") { 36 | if (/[a-z]/i.test(s[1])) { 37 | node = parseElement(context, ancestors); 38 | } 39 | } 40 | 41 | if (!node) { 42 | node = parseText(context); 43 | } 44 | 45 | nodes.push(node); 46 | } 47 | 48 | return nodes; 49 | } 50 | 51 | function isEnd(context, ancestors) { 52 | const s: string = context.source; 53 | if (s.startsWith("= 0; i--) { 55 | const tag = ancestors[i]; 56 | if (startsWithEndOpen(s, tag)) { 57 | return true; 58 | } 59 | } 60 | } 61 | return !s; 62 | } 63 | 64 | function parseInterpolation(context) { 65 | const openDelimiter = "{{"; 66 | const closeDelimiter = "}}"; 67 | const closeIndex = context.source.indexOf( 68 | closeDelimiter, 69 | openDelimiter.length 70 | ); 71 | // {{message}} -> message}} 72 | advanceBy(context, openDelimiter.length); 73 | const rawContentLength = closeIndex - openDelimiter.length; 74 | const rawContent = parseTextData(context, rawContentLength); 75 | advanceBy(context, closeDelimiter.length); 76 | const content = rawContent.trim(); 77 | 78 | return { 79 | type: NODE_TYPES.INTERPOLATION, 80 | content: { 81 | type: NODE_TYPES.SIMPLE_EXPRESSION, 82 | content, 83 | }, 84 | }; 85 | } 86 | 87 | function parseElement(context, ancestors: string[]) { 88 | const element: any = parseTag(context, TAG_TYPE.START); 89 | ancestors.push(element.tag); 90 | element.children = createParseChildren(context, ancestors); 91 | ancestors.pop(); 92 | 93 | if (startsWithEndOpen(context.source, element.tag)) { 94 | parseTag(context, TAG_TYPE.END); 95 | } else { 96 | throw new Error(`${element.tag}标签没有结束标签`); 97 | } 98 | 99 | return element; 100 | } 101 | 102 | function startsWithEndOpen(source: string, tag: string) { 103 | return ( 104 | source.startsWith(" -1 && endIndex > index) { 127 | endIndex = index; 128 | } 129 | } 130 | 131 | const content = parseTextData(context, endIndex); 132 | 133 | return { 134 | type: NODE_TYPES.TEXT, 135 | content, 136 | }; 137 | } 138 | 139 | function parseTextData(context, length: number) { 140 | const content = context.source.slice(0, length); 141 | 142 | advanceBy(context, length); 143 | 144 | return content; 145 | } 146 | 147 | function advanceBy(context, length: number) { 148 | context.source = context.source.slice(length); 149 | } 150 | -------------------------------------------------------------------------------- /src/reactive/test/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../effect"; 2 | import { reactive } from "../reactive"; 3 | import { isRef, ref, unRef, proxyRefs, toRef, toRefs } from "../ref"; 4 | 5 | describe("ref", () => { 6 | it("happy path", () => { 7 | const a = ref(1); 8 | expect(a.value).toBe(1); 9 | }); 10 | 11 | it("should be reactive", () => { 12 | const a = ref(1); 13 | let dummy; 14 | let calls = 0; 15 | effect(() => { 16 | calls++; 17 | dummy = a.value; 18 | }); 19 | expect(calls).toBe(1); 20 | expect(dummy).toBe(1); 21 | a.value = 2; 22 | expect(calls).toBe(2); 23 | expect(dummy).toBe(2); 24 | // 相同value不会触发trigger 25 | a.value = 2; 26 | expect(calls).toBe(2); 27 | expect(dummy).toBe(2); 28 | }); 29 | 30 | it("should make nested properties reactive", () => { 31 | const a = ref({ 32 | count: 1, 33 | }); 34 | let dummy; 35 | effect(() => { 36 | dummy = a.value.count; 37 | }); 38 | expect(dummy).toBe(1); 39 | a.value.count = 2; 40 | expect(dummy).toBe(2); 41 | }); 42 | 43 | it("isRef", () => { 44 | const a = ref(1); 45 | expect(isRef(a)).toBe(true); 46 | expect(isRef(1)).toBe(false); 47 | }); 48 | 49 | it("unRef", () => { 50 | const a = ref(1); 51 | expect(unRef(a)).toBe(1); 52 | expect(unRef(1)).toBe(1); 53 | }); 54 | 55 | it("proxyRefs", () => { 56 | const user = { 57 | age: ref(10), 58 | name: "aaa", 59 | }; 60 | 61 | const proxyUser = proxyRefs(user); 62 | expect(user.age.value).toBe(10); 63 | expect(proxyUser.age).toBe(10); 64 | expect(proxyUser.name).toBe("aaa"); 65 | 66 | proxyUser.age = 20; 67 | // proxyUser.age改变,user.age.value和proxy.age都需要改变 68 | expect(proxyUser.age).toBe(20); 69 | expect(user.age.value).toBe(20); 70 | 71 | proxyUser.age = ref(30); 72 | expect(proxyUser.age).toBe(30); 73 | expect(user.age.value).toBe(30); 74 | }); 75 | 76 | test("toRef", () => { 77 | const a = reactive({ 78 | x: 1, 79 | }); 80 | const x: any = toRef(a, "x"); 81 | expect(isRef(x)).toBe(true); 82 | expect(x.value).toBe(1); 83 | 84 | // source -> proxy 85 | a.x = 2; 86 | expect(x.value).toBe(2); 87 | 88 | // proxy -> source 89 | x.value = 3; 90 | expect(a.x).toBe(3); 91 | 92 | // reactivity 93 | let dummyX; 94 | effect(() => { 95 | dummyX = x.value; 96 | }); 97 | expect(dummyX).toBe(x.value); 98 | 99 | // mutating source should trigger effect using the proxy refs 100 | a.x = 4; 101 | expect(dummyX).toBe(4); 102 | 103 | // should keep ref 104 | const r = { x: ref(1) }; 105 | expect(toRef(r, "x")).toBe(r.x); 106 | }); 107 | 108 | test("toRefs", () => { 109 | const a = reactive({ 110 | x: 1, 111 | y: 2, 112 | }); 113 | 114 | const { x, y }: any = toRefs(a); 115 | 116 | expect(isRef(x)).toBe(true); 117 | expect(isRef(y)).toBe(true); 118 | expect(x.value).toBe(1); 119 | expect(y.value).toBe(2); 120 | 121 | // source -> proxy 122 | a.x = 2; 123 | a.y = 3; 124 | expect(x.value).toBe(2); 125 | expect(y.value).toBe(3); 126 | 127 | // proxy -> source 128 | x.value = 3; 129 | y.value = 4; 130 | expect(a.x).toBe(3); 131 | expect(a.y).toBe(4); 132 | 133 | // reactivity 134 | let dummyX, dummyY; 135 | effect(() => { 136 | dummyX = x.value; 137 | dummyY = y.value; 138 | }); 139 | expect(dummyX).toBe(x.value); 140 | expect(dummyY).toBe(y.value); 141 | 142 | // mutating source should trigger effect using the proxy refs 143 | a.x = 4; 144 | a.y = 5; 145 | expect(dummyX).toBe(4); 146 | expect(dummyY).toBe(5); 147 | }); 148 | 149 | test('toRefs should warn on plain object', () => { 150 | toRefs({}) 151 | }) 152 | 153 | 154 | test('toRefs reactive array', () => { 155 | const arr = reactive(['a', 'b', 'c']) 156 | const refs = toRefs(arr) 157 | 158 | expect(Array.isArray(refs)).toBe(true) 159 | 160 | refs[0].value = '1' 161 | expect(arr[0]).toBe('1') 162 | 163 | arr[1] = '2' 164 | expect(refs[1].value).toBe('2') 165 | }) 166 | }); 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-vue3 2 | - 实现 Vue3 核心逻辑的最简模型,本项目参考 https://github.com/cuixiaorui/mini-vue 实现。 3 | - 与该仓库有关的文章可以访问: https://juejin.cn/column/7065487683673391118 4 | 5 | - [reactive](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive) 6 | - [test](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/test) 7 | - [computed.spec.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/test/computed.spec.ts) 8 | - [effect.spec.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/test/effect.spec.ts) 9 | - [effectScope.spec.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/test/effectScope.spec.ts) 10 | - [reactive.spec.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/test/reactive.spec.ts) 11 | - [readonly.spec.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/test/readonly.spec.ts) 12 | - [ref.spec.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/test/ref.spec.ts) 13 | - [watch.spec.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/test/watch.spec.ts) 14 | - [baseHandlers.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/baseHandlers.ts) 15 | - [computed.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/computed.ts) 16 | - [effect.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/effect.ts) 17 | - [effectScope.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/effectScope.ts) 18 | - [index.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/index.ts) 19 | - [reactive.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/reactive.ts) 20 | - [ref.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/ref.ts) 21 | - [watch.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/reactive/watch.ts) 22 | - [runtime-core](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core) 23 | - [helpers](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/helpers) 24 | - [renderSlots.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/helpers/renderSlots.ts) 25 | - [apiInject.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/apiInject.ts) 26 | - [componentEmit.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/componentEmit.ts) 27 | - [componentProps.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/componentProps.ts) 28 | - [componentPublicInstance.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/componentPublicInstance.ts) 29 | - [components.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/components.ts) 30 | - [componentSlots.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/componentSlots.ts) 31 | - [componentUpdateUtils.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/componentUpdateUtils.ts) 32 | - [createApp.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/createApp.ts) 33 | - [h.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/h.ts) 34 | - [index.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/index.ts) 35 | - [renderer.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/renderer.ts) 36 | - [scheduler.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/scheduler.ts) 37 | - [vnode.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-core/vnode.ts) 38 | - [runtime-dom](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-dom) 39 | - [index.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/runtime-dom/index.ts) 40 | - [shared](https://github.com/chenfan0/mini-vue3/tree/main/src/shared) 41 | - [index.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/shared/index.ts) 42 | - [ShapeFlags.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/shared/ShapeFlags.ts) 43 | - [index.ts](https://github.com/chenfan0/mini-vue3/tree/main/src/index.ts) 44 | 45 | ## 目前实现功能 46 | ### reactivity 47 | - [x] reactive 48 | - [x] shallowReactive 49 | - [x] readonly 50 | - [x] shallowReadonly 51 | - [x] isReactive 52 | - [x] isReadonly 53 | - [x] isProxy 54 | - [x] ref 55 | - [x] isRef 56 | - [x] unRef 57 | - [X] toRaw 58 | - [x] proxyRefs 59 | - [x] computed 60 | - [x] effect 61 | - [x] watch 62 | ### runtime-core 63 | - [x] 初始化Component主流程 64 | - [x] 初始化Element主流程 65 | - [x] shapeFlags 66 | - [x] 组件代理对象 67 | - [x] 注册事件 68 | - [x] 组件props 69 | - [x] 组件emit 70 | - [x] 组件slots 71 | - [x] getCurrentInstance 72 | - [x] provide/inject 73 | - [x] 更新element的基本流程 74 | - [x] 更新Component的基本流程 75 | - [x] nextTick 76 | ### runtime-dom 77 | - [x] custom renderer 78 | ### compiler 79 | 暂未实现 80 | -------------------------------------------------------------------------------- /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', 'ES2021', or 'ESNEXT'. */ 8 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": ["DOM", "es6", "ES2016"], /* 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', 'react', 'react-jsx' or 'react-jsxdev'. */ 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 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | "types": ["jest"], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | import { createComponentInstance, setupComponent } from "./components"; 2 | import { ShapeFlags } from "../shared/ShapeFlags"; 3 | import { Fragment, Text } from "./vnode"; 4 | import { createAppAPI } from "./createApp"; 5 | import { effect } from "../reactive/index"; 6 | import { EMPTY_OBJ } from "../shared/index"; 7 | import { shouldUpdateComponent } from "./componentUpdateUtils"; 8 | import { queueJobs } from "./scheduler"; 9 | 10 | export function createRenderer(options) { 11 | const { 12 | createElement: hostCreateElement, 13 | patchProps: hostPatchProps, 14 | insert: hostInsert, 15 | remove: hostRemove, 16 | setElementText: hostSetElementText, 17 | } = options; 18 | 19 | function render(vnode, container, parent) { 20 | patch(null, vnode, container, parent, null); 21 | } 22 | 23 | function patch(n1, n2, container, parent, anchor) { 24 | const { shapeFlag, type } = n2; 25 | 26 | switch (type) { 27 | case Fragment: 28 | processFragment(n2, container, parent); 29 | break; 30 | case Text: 31 | processText(n2, container); 32 | break; 33 | default: 34 | if (shapeFlag & ShapeFlags.ELEMENT) { 35 | processElement(n1, n2, container, parent, anchor); 36 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 37 | processComponent(n1, n2, container, parent, anchor); 38 | } 39 | } 40 | } 41 | 42 | function processFragment(vnode, container, parent) { 43 | mountChildren(vnode, container, parent); 44 | } 45 | 46 | function processText(vnode, container) { 47 | const el = (vnode.el = document.createTextNode(vnode.children)); 48 | 49 | container.appendChild(el); 50 | } 51 | 52 | function processComponent(n1, n2, container, parent, anchor) { 53 | if (!n1) { 54 | mountComponent(n2, container, parent, anchor); 55 | } else { 56 | updateComponent(n1, n2); 57 | } 58 | } 59 | 60 | function mountComponent(initialVNode, container, parent, anchor) { 61 | const instance = (initialVNode.component = createComponentInstance( 62 | initialVNode, 63 | parent 64 | )); 65 | 66 | setupComponent(instance); 67 | setupRenderEffect(instance, initialVNode, container, anchor); 68 | } 69 | 70 | function updateComponent(n1, n2) { 71 | // 需要把n1.component赋值给n2.component 72 | // 不然下次更新n2.component会为null 73 | const instance = (n2.component = n1.component); 74 | if (shouldUpdateComponent(n1, n2)) { 75 | instance.next = n2; 76 | instance.update(); 77 | } 78 | } 79 | 80 | function setupRenderEffect(instance, vnode, container, anchor) { 81 | instance.update = effect( 82 | () => { 83 | const { proxy, isMounted } = instance; 84 | if (!isMounted) { 85 | const subTree = (instance.subTree = instance.render.call(proxy)); 86 | 87 | patch(null, subTree, container, instance, anchor); 88 | 89 | vnode.el = subTree.el; 90 | instance.isMounted = true; 91 | } else { 92 | // next是新的虚拟节点,vnode是旧的虚拟节点 93 | const { next, vnode } = instance; 94 | if (next) { 95 | next.el = vnode.el; 96 | updateComponentPreRender(instance, next); 97 | } 98 | const subTree = instance.render.call(proxy); 99 | const prevTree = instance.subTree; 100 | instance.subTree = subTree; 101 | patch(prevTree, subTree, container, instance, anchor); 102 | } 103 | }, 104 | { 105 | scheduler() { 106 | queueJobs(instance.update); 107 | }, 108 | } 109 | ); 110 | } 111 | 112 | function updateComponentPreRender(instance, next) { 113 | instance.props = next.props; 114 | instance.next = null; 115 | instance.vnode = next; 116 | } 117 | 118 | function processElement(n1, n2, container, parent, anchor) { 119 | if (!n1) { 120 | mountElement(n2, container, parent, anchor); 121 | } else { 122 | patchElement(n1, n2, parent, anchor); 123 | } 124 | } 125 | 126 | function mountElement(vnode, container, parent, anchor) { 127 | const { type, props, children, shapeFlag } = vnode; 128 | // 创建DOM节点 129 | const el = (vnode.el = hostCreateElement(type)); 130 | 131 | // 处理props 132 | for (const key in props) { 133 | hostPatchProps(el, key, props[key]); 134 | } 135 | 136 | // 处理children属性 137 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 138 | el.textContent = children; 139 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 140 | mountChildren(vnode, el, parent); 141 | } 142 | hostInsert(el, container, anchor); 143 | } 144 | 145 | function patchElement(n1, n2, parent, anchor) { 146 | // p1 oldProps, p2 newProps 147 | const p1 = n1.props || EMPTY_OBJ; 148 | const p2 = n2.props || EMPTY_OBJ; 149 | // 将el赋值给n2,因为下次patch n2就是n1 150 | const el = (n2.el = n1.el); 151 | 152 | patchProps(p1, p2, el); 153 | 154 | patchChildren(n1, n2, el, parent, anchor); 155 | } 156 | 157 | function patchChildren(n1, n2, container, parent, anchor) { 158 | const c1 = n1.children; 159 | const c2 = n2.children; 160 | const el = n1.el; 161 | const prevShapeFlag = n1.shapeFlag; 162 | const { shapeFlag } = n2; 163 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 164 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 165 | // 新节点children是文本类型, 旧节点children是数组类型 166 | unmountChildren(n1); 167 | hostSetElementText(el, c2); 168 | } else { 169 | patchKeydChildren(c1, c2, container, parent, anchor); 170 | } 171 | } else { 172 | if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 173 | // 新节点children是数组类型,旧节点children是文本类型 174 | hostSetElementText(el, ""); 175 | mountChildren(n2, el, parent); 176 | } else { 177 | // 新旧节点children类型都是文本类型 178 | if (c1 !== c2) { 179 | hostSetElementText(el, c2); 180 | } 181 | } 182 | } 183 | } 184 | 185 | function isSameVNodeType(n1, n2) { 186 | return n1.type === n2.type && n1.key === n2.key; 187 | } 188 | 189 | function patchKeydChildren( 190 | c1: any[], 191 | c2: any[], 192 | container, 193 | parent, 194 | parentAnchor 195 | ) { 196 | let i = 0; 197 | let e1 = c1.length - 1; 198 | let e2 = c2.length - 1; 199 | const l2 = c2.length; 200 | 201 | while (i <= e1 && i <= e2) { 202 | if (isSameVNodeType(c1[i], c2[i])) { 203 | patch(c1[i], c2[i], container, parent, parentAnchor); 204 | } else { 205 | break; 206 | } 207 | i++; 208 | } 209 | 210 | while (i <= e1 && i <= e2) { 211 | const n1 = c1[e1]; 212 | const n2 = c2[e2]; 213 | if (isSameVNodeType(n1, n2)) { 214 | patch(c1[e1], c2[e2], container, parent, parentAnchor); 215 | } else { 216 | break; 217 | } 218 | e1--; 219 | e2--; 220 | } 221 | 222 | if (i > e1) { 223 | if (i <= e2) { 224 | // 找到锚点位置 225 | const nextPos = e2 + 1; 226 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 227 | 228 | while (i <= e2) { 229 | // abc -> abcde 230 | // bcd -> abcd 231 | // 新增节点 232 | patch(null, c2[i], container, parent, anchor); 233 | i++; 234 | } 235 | } 236 | } else if (i > e2) { 237 | while (i <= e1) { 238 | // 删除节点 239 | hostRemove(c1[i].el); 240 | i++; 241 | } 242 | } else { 243 | let s1 = i; 244 | let s2 = i; 245 | const toBePatched = e2 - s2 + 1; 246 | let patched = 0; 247 | const keyToNewIndexMap = new Map(); 248 | const newIndexToOldIndexMap = new Array(toBePatched).fill(0); 249 | 250 | let moved = false; 251 | let maxIndexSoFar = 0; 252 | 253 | for (let i = s2; i <= e2; i++) { 254 | const key = c2[i].key; 255 | keyToNewIndexMap.set(key, i); 256 | } 257 | 258 | for (let i = s1; i <= e1; i++) { 259 | if (patched >= toBePatched) { 260 | hostRemove(c2[i]); 261 | continue; 262 | } 263 | 264 | const preVnode = c1[i]; 265 | const key = preVnode.key; 266 | let newIndex; 267 | 268 | if (key !== null || key !== undefined) { 269 | newIndex = keyToNewIndexMap.get(key); 270 | } else { 271 | for (let j = s2; j <= e2; j++) { 272 | if (isSameVNodeType(preVnode, c2[j])) { 273 | newIndex = j; 274 | break; 275 | } 276 | } 277 | } 278 | if (newIndex === undefined) { 279 | hostRemove(preVnode.el); 280 | } else { 281 | if (maxIndexSoFar < newIndex) { 282 | maxIndexSoFar = newIndex; 283 | } else { 284 | moved = true; 285 | } 286 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 287 | patch(preVnode, c2[newIndex], container, parent, null); 288 | patched++; 289 | } 290 | } 291 | 292 | // 返回最长递增子序列的下标,也就是新数组的下标。 293 | // 新数组节点的下标在该数组中,则说明不需要移动 294 | const increasingNewIndexSequence = moved 295 | ? getSequence(newIndexToOldIndexMap) 296 | : []; 297 | 298 | let j = increasingNewIndexSequence.length - 1; 299 | 300 | for (let i = toBePatched - 1; i >= 0; i--) { 301 | const nextIndex = i + s2; 302 | const nextChild = c2[nextIndex]; 303 | const anchor = nextIndex + 1 >= l2 ? null : c2[nextIndex + 1].el; 304 | 305 | if (newIndexToOldIndexMap[i] === 0) { 306 | // 需要新增节点 307 | patch(null, nextChild, container, parent, anchor); 308 | } else if (moved) { 309 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 310 | hostInsert(nextChild.el, container, anchor); 311 | } else { 312 | j--; 313 | } 314 | } 315 | } 316 | } 317 | } 318 | 319 | function unmountChildren(vnode) { 320 | vnode.children.forEach((v) => { 321 | const el = v.el; 322 | hostRemove(el); 323 | }); 324 | } 325 | 326 | function patchProps(oldProps, newProps, el) { 327 | if (oldProps !== newProps) { 328 | // 遍历新属性,增加或者修改属性值 329 | for (const key in newProps) { 330 | const prevProp = oldProps[key]; 331 | const currentProp = newProps[key]; 332 | 333 | if (prevProp !== currentProp) { 334 | hostPatchProps(el, key, currentProp); 335 | } 336 | } 337 | // 当旧属性不为空对象时 338 | if (oldProps !== EMPTY_OBJ) { 339 | // 遍历旧属性,删除旧属性有而新属性没有的属性 340 | for (const key in oldProps) { 341 | if (!(key in newProps)) { 342 | hostPatchProps(el, key, null); 343 | } 344 | } 345 | } 346 | } 347 | } 348 | 349 | function mountChildren(vnode, container, parent) { 350 | vnode.children.forEach((item) => { 351 | patch(null, item, container, parent, null); 352 | }); 353 | } 354 | 355 | return { 356 | createApp: createAppAPI(render), 357 | }; 358 | } 359 | 360 | function getSequence(arr: number[]): number[] { 361 | const p = arr.slice(); 362 | const result = [0]; 363 | let i, j, u, v, c; 364 | const len = arr.length; 365 | for (i = 0; i < len; i++) { 366 | const arrI = arr[i]; 367 | if (arrI !== 0) { 368 | j = result[result.length - 1]; 369 | if (arr[j] < arrI) { 370 | p[i] = j; 371 | result.push(i); 372 | continue; 373 | } 374 | u = 0; 375 | v = result.length - 1; 376 | while (u < v) { 377 | c = (u + v) >> 1; 378 | if (arr[result[c]] < arrI) { 379 | u = c + 1; 380 | } else { 381 | v = c; 382 | } 383 | } 384 | if (arrI < arr[result[u]]) { 385 | if (u > 0) { 386 | p[i] = result[u - 1]; 387 | } 388 | result[u] = i; 389 | } 390 | } 391 | } 392 | u = result.length; 393 | v = result[u - 1]; 394 | while (u-- > 0) { 395 | result[u] = v; 396 | v = p[v]; 397 | } 398 | return result; 399 | } 400 | -------------------------------------------------------------------------------- /lib/mini-vue.esm.js: -------------------------------------------------------------------------------- 1 | const extend = Object.assign; 2 | const isObject = (value) => { 3 | return value !== null && typeof value === "object"; 4 | }; 5 | const hasChange = (value, newValue) => { 6 | return !Object.is(value, newValue); 7 | }; 8 | function hasOwn(obj, key) { 9 | return Object.prototype.hasOwnProperty.call(obj, key); 10 | } 11 | function isArray(value) { 12 | return Array.isArray(value); 13 | } 14 | // foo-add => fooAdd 15 | const camelize = (str) => { 16 | return str.replace(/-(\w)/g, (_, p) => { 17 | return p ? p.toUpperCase() : ""; 18 | }); 19 | }; 20 | const capitalize = (str) => { 21 | return str.charAt(0).toUpperCase() + str.slice(1); 22 | }; 23 | // fooAdd => onFooAdd 24 | const handleEventName = (name) => { 25 | return name ? "on" + capitalize(name) : ""; 26 | }; 27 | const EMPTY_OBJ = {}; 28 | 29 | const publicPropertiesMap = { 30 | $el: (i) => i.vnode.el, 31 | $slots: (i) => i.slots, 32 | $props: (i) => i.props, 33 | }; 34 | const publicInstanceProxyHandlers = { 35 | get({ _: instance }, key) { 36 | if (hasOwn(instance.setupState, key)) { 37 | return instance.setupState[key]; 38 | } 39 | else if (hasOwn(instance.props, key)) { 40 | return instance.props[key]; 41 | } 42 | const publicGetter = publicPropertiesMap[key]; 43 | if (publicGetter) { 44 | return publicGetter(instance); 45 | } 46 | }, 47 | }; 48 | 49 | function initProps(instance, rawProps) { 50 | // 将props挂载到实例上 51 | instance.props = rawProps; 52 | // attrs 53 | } 54 | 55 | let activeEffect; 56 | let shouldTrack; 57 | const targetMap = new WeakMap(); 58 | class ReactiveEffect { 59 | constructor(fn, scheduler) { 60 | this.scheduler = scheduler; 61 | // 该变量用来记录是否调过stop函数 62 | this.active = true; 63 | // 收集该effect的dep 64 | this.deps = []; 65 | this._fn = fn; 66 | this.scheduler = scheduler; 67 | } 68 | run(...args) { 69 | activeEffect = this; 70 | if (!this.active) { 71 | return this._fn(...args); 72 | } 73 | shouldTrack = true; 74 | const res = this._fn(...args); 75 | shouldTrack = false; 76 | return res; 77 | } 78 | stop() { 79 | if (this.active) { 80 | cleanupEffect(this); 81 | this.onStop && this.onStop(); 82 | this.active = false; 83 | } 84 | } 85 | } 86 | function cleanupEffect(effect) { 87 | effect.deps.forEach((dep) => { 88 | dep.delete(effect); 89 | }); 90 | // 清空effect.deps 91 | effect.deps.length = 0; 92 | } 93 | function track(target, key) { 94 | if (!isTracking()) 95 | return; 96 | let depsMap = targetMap.get(target); 97 | if (!depsMap) { 98 | depsMap = new Map(); 99 | targetMap.set(target, depsMap); 100 | } 101 | let dep = depsMap.get(key); 102 | if (!dep) { 103 | dep = new Set(); 104 | depsMap.set(key, dep); 105 | } 106 | trackEffects(dep); 107 | } 108 | function trackEffects(dep) { 109 | dep.add(activeEffect); 110 | activeEffect.deps.push(dep); 111 | } 112 | function isTracking() { 113 | return shouldTrack && activeEffect !== undefined; 114 | } 115 | function trigger(target, key) { 116 | const depsMap = targetMap.get(target); 117 | if (!depsMap) 118 | return; 119 | const dep = depsMap.get(key); 120 | triggerEffects(dep); 121 | } 122 | function triggerEffects(dep) { 123 | if (!dep) 124 | return; 125 | for (const effect of dep) { 126 | if (effect.scheduler) { 127 | effect.scheduler(); 128 | } 129 | else { 130 | effect.run(); 131 | } 132 | } 133 | } 134 | function effect(fn, options = {}) { 135 | const _effect = new ReactiveEffect(fn, options.scheduler); 136 | extend(_effect, options); 137 | _effect.run(); 138 | const runner = _effect.run.bind(_effect); 139 | runner.effect = _effect; 140 | return runner; 141 | } 142 | 143 | function createGetter(isReadonly = false, isShallow = false) { 144 | return function get(target, key, receiver) { 145 | const res = Reflect.get(target, key, receiver); 146 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 147 | return !isReadonly; 148 | } 149 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 150 | return isReadonly; 151 | } 152 | if (!isReadonly) { 153 | track(target, key); 154 | } 155 | if (isObject(res) && !isShallow) { 156 | return isReadonly ? readonly(res) : reactive(res); 157 | } 158 | return res; 159 | }; 160 | } 161 | function createSetter() { 162 | return function set(target, key, newValue, receiver) { 163 | const res = Reflect.set(target, key, newValue, receiver); 164 | // 触发依赖 165 | trigger(target, key); 166 | return res; 167 | }; 168 | } 169 | const get = createGetter(); 170 | const set = createSetter(); 171 | const readonlyGet = createGetter(true); 172 | const shallowReadonlyGet = createGetter(true, true); 173 | const mutableHandlers = { 174 | get, 175 | set, 176 | }; 177 | const readonlyHandlers = { 178 | get: readonlyGet, 179 | set(target) { 180 | console.warn(`${target} is a readonly can not be set`); 181 | return true; 182 | }, 183 | }; 184 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 185 | get: shallowReadonlyGet, 186 | }); 187 | 188 | function createReactiveObj(raw, baseHandlers) { 189 | if (!isObject(raw)) { 190 | console.error(`${raw} 必须是一个对象`); 191 | } 192 | return new Proxy(raw, baseHandlers); 193 | } 194 | function reactive(raw) { 195 | return createReactiveObj(raw, mutableHandlers); 196 | } 197 | function readonly(raw) { 198 | return createReactiveObj(raw, readonlyHandlers); 199 | } 200 | function shallowReadonly(raw) { 201 | return createReactiveObj(raw, shallowReadonlyHandlers); 202 | } 203 | 204 | function emit(instance, event, ...args) { 205 | const { props } = instance; 206 | const handlerName = camelize(handleEventName(event)); 207 | const handler = props[handlerName]; 208 | handler && handler(...args); 209 | } 210 | 211 | function initSlots(instance, children) { 212 | // instance.slots = Array.isArray(children) ? children : [children]; 213 | const { vnode } = instance; 214 | if (vnode.shapeFlag & 16 /* SLOT_CHILDREN */) { 215 | instance.slots = children; 216 | for (const slot in children) { 217 | const value = children[slot]; 218 | children[slot] = (props) => normalizeSlotValue(value(props)); 219 | } 220 | } 221 | } 222 | function normalizeSlotValue(value) { 223 | return isArray(value) ? value : [value]; 224 | } 225 | 226 | let currentInstance = null; 227 | function createComponentInstance(vnode, parent) { 228 | const component = { 229 | vnode, 230 | type: vnode.type, 231 | setupState: {}, 232 | props: {}, 233 | slots: {}, 234 | provides: parent ? parent.provides : {}, 235 | isMounted: false, 236 | subTree: {}, 237 | next: null, 238 | update: null, 239 | emit: () => { }, 240 | }; 241 | component.emit = emit.bind(null, component); 242 | return component; 243 | } 244 | function setupComponent(instance) { 245 | const rawProps = instance.vnode.props; 246 | initProps(instance, rawProps); 247 | initSlots(instance, instance.vnode.children); 248 | setupStatefulComponent(instance); 249 | } 250 | function setupStatefulComponent(instance) { 251 | const component = instance.type; 252 | const props = instance.vnode.props; 253 | const emit = instance.emit; 254 | // ctx 代理 255 | instance.proxy = new Proxy({ _: instance }, publicInstanceProxyHandlers); 256 | const { setup } = component; 257 | if (setup) { 258 | setCurrentInstance(instance); 259 | const setupResult = setup(shallowReadonly(props), { emit }); 260 | setCurrentInstance(null); 261 | handleSetupResult(instance, setupResult); 262 | } 263 | } 264 | function handleSetupResult(instance, setupResult) { 265 | // TODO function 266 | // 如果返回的是一个对象,会将返回的值注入到instance中 267 | if (isObject(setupResult)) { 268 | instance.setupState = proxyRefs(setupResult); 269 | } 270 | finishComponentSetup(instance); 271 | } 272 | function finishComponentSetup(instance) { 273 | const component = instance.type; 274 | if (component.render) { 275 | instance.render = component.render; 276 | } 277 | } 278 | function getCurrentInstance() { 279 | return currentInstance; 280 | } 281 | function setCurrentInstance(instance) { 282 | currentInstance = instance; 283 | } 284 | 285 | const Fragment = Symbol("Fragment"); 286 | const Text = Symbol("Text"); 287 | function createVNode(type, props = {}, children = []) { 288 | const vnode = { 289 | type, 290 | props, 291 | children, 292 | appContext: null, 293 | component: null, 294 | key: props.key || undefined, 295 | el: null, 296 | shapeFlag: getShapeFlag(type), 297 | }; 298 | if (Array.isArray(children)) { 299 | // 子节点为数组 300 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 301 | } 302 | else if (typeof children === "string") { 303 | // 子节点为text类型 304 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 305 | } 306 | if (vnode.shapeFlag & 2 /* STATEFUL_COMPONENT */) { 307 | if (isObject(children)) { 308 | vnode.shapeFlag |= 16 /* SLOT_CHILDREN */; 309 | } 310 | } 311 | return vnode; 312 | } 313 | function getShapeFlag(type) { 314 | return typeof type === "string" 315 | ? 1 /* ELEMENT */ 316 | : 2 /* STATEFUL_COMPONENT */; 317 | } 318 | function createTextVNode(text) { 319 | return createVNode(Text, {}, text); 320 | } 321 | 322 | function createAppContext() { 323 | return { 324 | app: null, 325 | components: {}, 326 | }; 327 | } 328 | function createAppAPI(render) { 329 | return function createApp(App) { 330 | const context = createAppContext(); 331 | const app = (context.app = { 332 | mount(rootComponent) { 333 | const vnode = createVNode(App); 334 | vnode.appContext = context; 335 | render(vnode, rootComponent, null); 336 | }, 337 | component(name, component) { 338 | if (!component) { 339 | return context.components[name]; 340 | } 341 | context.components[name] = component; 342 | }, 343 | }); 344 | return app; 345 | }; 346 | } 347 | 348 | class ComputedRefImpl { 349 | constructor(getter) { 350 | this._dirty = true; 351 | this._getter = getter; 352 | // 353 | this._effect = new ReactiveEffect(this._getter, () => { 354 | // scheduler函数会在依赖改变时,执行 355 | // 当computed依赖的值发生改变时,将this._dirty设置为true 356 | // 这样当下次获取.value时,this._dirty就为true,就会重新触发getter函数,获取最新的值 357 | this._dirty = true; 358 | }); 359 | } 360 | get value() { 361 | if (this._dirty) { 362 | // this._dirty 如果为true,则说明需要重新执行getter函数 363 | this._value = this._effect.run(); 364 | // 执行完getter函数将this._dirty设置为false,这样当依赖没有改变时,再次获取.value时,不会重新出发getter函数 365 | this._dirty = false; 366 | } 367 | return this._value; 368 | } 369 | } 370 | function computed(getter) { 371 | return new ComputedRefImpl(getter); 372 | } 373 | 374 | class RefImplement { 375 | constructor(value) { 376 | this.__v_isRef = true; 377 | this._rawValue = value; 378 | this._value = convert(value); 379 | this.deps = new Set(); 380 | } 381 | get value() { 382 | if (isTracking()) { 383 | trackEffects(this.deps); 384 | } 385 | return this._value; 386 | } 387 | set value(newValue) { 388 | if (!hasChange(this._rawValue, newValue)) 389 | return; 390 | this._rawValue = newValue; 391 | this._value = convert(newValue); 392 | triggerEffects(this.deps); 393 | } 394 | } 395 | function convert(value) { 396 | return isObject(value) ? reactive(value) : value; 397 | } 398 | function ref(value) { 399 | return new RefImplement(value); 400 | } 401 | function isRef(ref) { 402 | if (!ref) 403 | return false; 404 | return !!ref.__v_isRef; 405 | } 406 | function unRef(ref) { 407 | return isRef(ref) ? ref.value : ref; 408 | } 409 | function proxyRefs(objWithRefs) { 410 | return new Proxy(objWithRefs, { 411 | get(target, key, receiver) { 412 | return unRef(Reflect.get(target, key, receiver)); 413 | }, 414 | set(target, key, newValue, receiver) { 415 | // 当原来值是ref类型,而修改的值不是ref类型 416 | if (isRef(target[key]) && !isRef(newValue)) { 417 | return (target[key].value = newValue); 418 | } 419 | return Reflect.set(target, key, newValue, receiver); 420 | }, 421 | }); 422 | } 423 | 424 | function shouldUpdateComponent(n1, n2) { 425 | const { props: oldProps } = n1; 426 | const { props: newProps } = n2; 427 | for (const key in newProps) { 428 | if (oldProps[key] !== newProps[key]) { 429 | return true; 430 | } 431 | } 432 | return false; 433 | } 434 | 435 | const queue = []; 436 | let isFlushPending = false; 437 | const p = Promise.resolve(); 438 | function nextTick(fn) { 439 | return fn ? p.then(fn) : p; 440 | } 441 | function queueJobs(fn) { 442 | // queue.in; 443 | if (!queue.includes(fn)) { 444 | queue.push(fn); 445 | } 446 | queueFlush(); 447 | } 448 | function queueFlush() { 449 | if (isFlushPending) 450 | return; 451 | isFlushPending = true; 452 | nextTick(flushJobs); 453 | } 454 | function flushJobs() { 455 | isFlushPending = false; 456 | let job; 457 | while ((job = queue.shift())) { 458 | job && job(); 459 | } 460 | } 461 | 462 | function createRenderer(options) { 463 | const { createElement: hostCreateElement, patchProps: hostPatchProps, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 464 | function render(vnode, container, parent) { 465 | patch(null, vnode, container, parent, null); 466 | } 467 | function patch(n1, n2, container, parent, anchor) { 468 | const { shapeFlag, type } = n2; 469 | switch (type) { 470 | case Fragment: 471 | processFragment(n2, container, parent); 472 | break; 473 | case Text: 474 | processText(n2, container); 475 | break; 476 | default: 477 | if (shapeFlag & 1 /* ELEMENT */) { 478 | processElement(n1, n2, container, parent, anchor); 479 | } 480 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 481 | processComponent(n1, n2, container, parent, anchor); 482 | } 483 | } 484 | } 485 | function processFragment(vnode, container, parent) { 486 | mountChildren(vnode, container, parent); 487 | } 488 | function processText(vnode, container) { 489 | const el = (vnode.el = document.createTextNode(vnode.children)); 490 | container.appendChild(el); 491 | } 492 | function processComponent(n1, n2, container, parent, anchor) { 493 | if (!n1) { 494 | mountComponent(n2, container, parent, anchor); 495 | } 496 | else { 497 | updateComponent(n1, n2); 498 | } 499 | } 500 | function mountComponent(initialVNode, container, parent, anchor) { 501 | const instance = (initialVNode.component = createComponentInstance(initialVNode, parent)); 502 | setupComponent(instance); 503 | setupRenderEffect(instance, initialVNode, container, anchor); 504 | } 505 | function updateComponent(n1, n2) { 506 | // 需要把n1.component赋值给n2.component 507 | // 不然下次更新n2.component会为null 508 | const instance = (n2.component = n1.component); 509 | if (shouldUpdateComponent(n1, n2)) { 510 | instance.next = n2; 511 | instance.update(); 512 | } 513 | } 514 | function setupRenderEffect(instance, vnode, container, anchor) { 515 | instance.update = effect(() => { 516 | const { proxy, isMounted } = instance; 517 | if (!isMounted) { 518 | const subTree = (instance.subTree = instance.render.call(proxy)); 519 | patch(null, subTree, container, instance, anchor); 520 | vnode.el = subTree.el; 521 | instance.isMounted = true; 522 | } 523 | else { 524 | // next是新的虚拟节点,vnode是旧的虚拟节点 525 | const { next, vnode } = instance; 526 | if (next) { 527 | next.el = vnode.el; 528 | updateComponentPreRender(instance, next); 529 | } 530 | const subTree = instance.render.call(proxy); 531 | const prevTree = instance.subTree; 532 | instance.subTree = subTree; 533 | patch(prevTree, subTree, container, instance, anchor); 534 | } 535 | }, { 536 | scheduler() { 537 | queueJobs(instance.update); 538 | }, 539 | }); 540 | } 541 | function updateComponentPreRender(instance, next) { 542 | instance.props = next.props; 543 | instance.next = null; 544 | instance.vnode = next; 545 | } 546 | function processElement(n1, n2, container, parent, anchor) { 547 | if (!n1) { 548 | mountElement(n2, container, parent, anchor); 549 | } 550 | else { 551 | patchElement(n1, n2, parent, anchor); 552 | } 553 | } 554 | function mountElement(vnode, container, parent, anchor) { 555 | const { type, props, children, shapeFlag } = vnode; 556 | // 创建DOM节点 557 | const el = (vnode.el = hostCreateElement(type)); 558 | // 处理props 559 | for (const key in props) { 560 | hostPatchProps(el, key, props[key]); 561 | } 562 | // 处理children属性 563 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 564 | el.textContent = children; 565 | } 566 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 567 | mountChildren(vnode, el, parent); 568 | } 569 | hostInsert(el, container, anchor); 570 | } 571 | function patchElement(n1, n2, parent, anchor) { 572 | // p1 oldProps, p2 newProps 573 | const p1 = n1.props || EMPTY_OBJ; 574 | const p2 = n2.props || EMPTY_OBJ; 575 | // 将el赋值给n2,因为下次patch n2就是n1 576 | const el = (n2.el = n1.el); 577 | patchProps(p1, p2, el); 578 | patchChildren(n1, n2, el, parent, anchor); 579 | } 580 | function patchChildren(n1, n2, container, parent, anchor) { 581 | const c1 = n1.children; 582 | const c2 = n2.children; 583 | const el = n1.el; 584 | const prevShapeFlag = n1.shapeFlag; 585 | const { shapeFlag } = n2; 586 | if (prevShapeFlag & 8 /* ARRAY_CHILDREN */) { 587 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 588 | // 新节点children是文本类型, 旧节点children是数组类型 589 | unmountChildren(n1); 590 | hostSetElementText(el, c2); 591 | } 592 | else { 593 | patchKeydChildren(c1, c2, container, parent, anchor); 594 | } 595 | } 596 | else { 597 | if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 598 | // 新节点children是数组类型,旧节点children是文本类型 599 | hostSetElementText(el, ""); 600 | mountChildren(n2, el, parent); 601 | } 602 | else { 603 | // 新旧节点children类型都是文本类型 604 | if (c1 !== c2) { 605 | hostSetElementText(el, c2); 606 | } 607 | } 608 | } 609 | } 610 | function isSameVNodeType(n1, n2) { 611 | return n1.type === n2.type && n1.key === n2.key; 612 | } 613 | function patchKeydChildren(c1, c2, container, parent, parentAnchor) { 614 | let i = 0; 615 | let e1 = c1.length - 1; 616 | let e2 = c2.length - 1; 617 | const l2 = c2.length; 618 | while (i <= e1 && i <= e2) { 619 | if (isSameVNodeType(c1[i], c2[i])) { 620 | patch(c1[i], c2[i], container, parent, parentAnchor); 621 | } 622 | else { 623 | break; 624 | } 625 | i++; 626 | } 627 | while (i <= e1 && i <= e2) { 628 | const n1 = c1[e1]; 629 | const n2 = c2[e2]; 630 | if (isSameVNodeType(n1, n2)) { 631 | patch(c1[e1], c2[e2], container, parent, parentAnchor); 632 | } 633 | else { 634 | break; 635 | } 636 | e1--; 637 | e2--; 638 | } 639 | if (i > e1) { 640 | if (i <= e2) { 641 | // 找到锚点位置 642 | const nextPos = e2 + 1; 643 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 644 | while (i <= e2) { 645 | // abc -> abcde 646 | // bcd -> abcd 647 | // 新增节点 648 | patch(null, c2[i], container, parent, anchor); 649 | i++; 650 | } 651 | } 652 | } 653 | else if (i > e2) { 654 | while (i <= e1) { 655 | // 删除节点 656 | hostRemove(c1[i].el); 657 | i++; 658 | } 659 | } 660 | else { 661 | let s1 = i; 662 | let s2 = i; 663 | const toBePatched = e2 - s2 + 1; 664 | let patched = 0; 665 | const keyToNewIndexMap = new Map(); 666 | const newIndexToOldIndexMap = new Array(toBePatched).fill(0); 667 | let moved = false; 668 | let maxIndexSoFar = 0; 669 | for (let i = s2; i <= e2; i++) { 670 | const key = c2[i].key; 671 | keyToNewIndexMap.set(key, i); 672 | } 673 | for (let i = s1; i <= e1; i++) { 674 | if (patched >= toBePatched) { 675 | hostRemove(c2[i]); 676 | continue; 677 | } 678 | const preVnode = c1[i]; 679 | const key = preVnode.key; 680 | let newIndex; 681 | if (key !== null || key !== undefined) { 682 | newIndex = keyToNewIndexMap.get(key); 683 | } 684 | else { 685 | for (let j = s2; j <= e2; j++) { 686 | if (isSameVNodeType(preVnode, c2[j])) { 687 | newIndex = j; 688 | break; 689 | } 690 | } 691 | } 692 | if (newIndex === undefined) { 693 | hostRemove(preVnode.el); 694 | } 695 | else { 696 | if (maxIndexSoFar < newIndex) { 697 | maxIndexSoFar = newIndex; 698 | } 699 | else { 700 | moved = true; 701 | } 702 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 703 | patch(preVnode, c2[newIndex], container, parent, null); 704 | patched++; 705 | } 706 | } 707 | // 返回最长递增子序列的下标,也就是新数组的下标。 708 | // 新数组节点的下标在该数组中,则说明不需要移动 709 | const increasingNewIndexSequence = moved 710 | ? getSequence(newIndexToOldIndexMap) 711 | : []; 712 | let j = increasingNewIndexSequence.length - 1; 713 | for (let i = toBePatched - 1; i >= 0; i--) { 714 | const nextIndex = i + s2; 715 | const nextChild = c2[nextIndex]; 716 | const anchor = nextIndex + 1 >= l2 ? null : c2[nextIndex + 1].el; 717 | if (newIndexToOldIndexMap[i] === 0) { 718 | // 需要新增节点 719 | patch(null, nextChild, container, parent, anchor); 720 | } 721 | else if (moved) { 722 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 723 | hostInsert(nextChild.el, container, anchor); 724 | } 725 | else { 726 | j--; 727 | } 728 | } 729 | } 730 | } 731 | } 732 | function unmountChildren(vnode) { 733 | vnode.children.forEach((v) => { 734 | const el = v.el; 735 | hostRemove(el); 736 | }); 737 | } 738 | function patchProps(oldProps, newProps, el) { 739 | if (oldProps !== newProps) { 740 | // 遍历新属性,增加或者修改属性值 741 | for (const key in newProps) { 742 | const prevProp = oldProps[key]; 743 | const currentProp = newProps[key]; 744 | if (prevProp !== currentProp) { 745 | hostPatchProps(el, key, currentProp); 746 | } 747 | } 748 | // 当旧属性不为空对象时 749 | if (oldProps !== EMPTY_OBJ) { 750 | // 遍历旧属性,删除旧属性有而新属性没有的属性 751 | for (const key in oldProps) { 752 | if (!(key in newProps)) { 753 | hostPatchProps(el, key, null); 754 | } 755 | } 756 | } 757 | } 758 | } 759 | function mountChildren(vnode, container, parent) { 760 | vnode.children.forEach((item) => { 761 | patch(null, item, container, parent, null); 762 | }); 763 | } 764 | return { 765 | createApp: createAppAPI(render), 766 | }; 767 | } 768 | function getSequence(arr) { 769 | const p = arr.slice(); 770 | const result = [0]; 771 | let i, j, u, v, c; 772 | const len = arr.length; 773 | for (i = 0; i < len; i++) { 774 | const arrI = arr[i]; 775 | if (arrI !== 0) { 776 | j = result[result.length - 1]; 777 | if (arr[j] < arrI) { 778 | p[i] = j; 779 | result.push(i); 780 | continue; 781 | } 782 | u = 0; 783 | v = result.length - 1; 784 | while (u < v) { 785 | c = (u + v) >> 1; 786 | if (arr[result[c]] < arrI) { 787 | u = c + 1; 788 | } 789 | else { 790 | v = c; 791 | } 792 | } 793 | if (arrI < arr[result[u]]) { 794 | if (u > 0) { 795 | p[i] = result[u - 1]; 796 | } 797 | result[u] = i; 798 | } 799 | } 800 | } 801 | u = result.length; 802 | v = result[u - 1]; 803 | while (u-- > 0) { 804 | result[u] = v; 805 | v = p[v]; 806 | } 807 | return result; 808 | } 809 | 810 | function h(type, props, children) { 811 | return createVNode(type, props, children); 812 | } 813 | 814 | function renderSlots(slots, props, name) { 815 | // 处理具名插槽 816 | if (slots[name]) { 817 | if (typeof slots[name] === "function") { 818 | return createVNode(Fragment, {}, slots[name](props)); 819 | } 820 | } 821 | // 处理默认插槽 822 | const totalSlots = []; 823 | for (const slot in slots) { 824 | totalSlots.push(...slots[slot](props)); 825 | } 826 | return createVNode(Fragment, {}, totalSlots); 827 | } 828 | 829 | const COMPONENTS = "components"; 830 | function resolveComponent(name) { 831 | return resolveAsset(COMPONENTS, name); 832 | } 833 | function resolveAsset(type, name) { 834 | const instance = currentInstance; 835 | if (!instance) 836 | return name; 837 | const res = instance.appContext[type][name]; 838 | return res === undefined ? instance.type : res; 839 | } 840 | 841 | function provide(key, value) { 842 | const instance = getCurrentInstance(); 843 | const parentProvides = instance.parent ? instance.parent.provides : {}; 844 | if (instance) { 845 | let { provides } = instance; 846 | if (parentProvides === provides) { 847 | // parentProvides === provides则表明是第一次进行provide 848 | provides = instance.provides = Object.create(parentProvides); 849 | } 850 | provides[key] = value; 851 | } 852 | } 853 | function inject(key, value) { 854 | const instance = getCurrentInstance(); 855 | if (instance) { 856 | const { provides } = instance; 857 | const injectValue = provides[key]; 858 | if (!injectValue) { 859 | if (typeof value === "function") { 860 | return value(); 861 | } 862 | else { 863 | return value; 864 | } 865 | } 866 | return injectValue; 867 | } 868 | } 869 | 870 | function createElement(type) { 871 | return document.createElement(type); 872 | } 873 | function patchProps(el, key, val) { 874 | // handle props 875 | // 如果key为 on开头并且on后面的第一个字符为大写,则认定为事件监听 876 | const isOn = (key) => /^on[A-Z]/.test(key); 877 | // 处理事件 878 | if (isOn(key)) { 879 | const event = key.slice(2).toLocaleLowerCase(); 880 | el.addEventListener(event, val); 881 | } 882 | else if (val !== undefined && val !== null) { 883 | el.setAttribute(key, val); 884 | } 885 | else { 886 | // 当属性值为undefined或者为null时,直接删除该属性即可 887 | el.removeAttribute(key); 888 | } 889 | } 890 | function insert(child, container, anchor) { 891 | container.insertBefore(child, anchor); 892 | } 893 | function remove(el) { 894 | const parent = el.parentNode; 895 | if (parent) { 896 | parent.removeChild(el); 897 | } 898 | } 899 | function setElementText(el, text) { 900 | el.textContent = text; 901 | } 902 | const renderer = createRenderer({ 903 | createElement, 904 | patchProps, 905 | insert, 906 | remove, 907 | setElementText, 908 | }); 909 | function createApp(...args) { 910 | return renderer.createApp(...args); 911 | } 912 | 913 | export { computed, createApp, createRenderer, createTextVNode, effect, getCurrentInstance, h, inject, nextTick, provide, proxyRefs, reactive, readonly, ref, renderSlots, renderer, resolveComponent, shallowReadonly }; 914 | -------------------------------------------------------------------------------- /lib/mini-vue.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | const extend = Object.assign; 6 | const isObject = (value) => { 7 | return value !== null && typeof value === "object"; 8 | }; 9 | const hasChange = (value, newValue) => { 10 | return !Object.is(value, newValue); 11 | }; 12 | function hasOwn(obj, key) { 13 | return Object.prototype.hasOwnProperty.call(obj, key); 14 | } 15 | function isArray(value) { 16 | return Array.isArray(value); 17 | } 18 | // foo-add => fooAdd 19 | const camelize = (str) => { 20 | return str.replace(/-(\w)/g, (_, p) => { 21 | return p ? p.toUpperCase() : ""; 22 | }); 23 | }; 24 | const capitalize = (str) => { 25 | return str.charAt(0).toUpperCase() + str.slice(1); 26 | }; 27 | // fooAdd => onFooAdd 28 | const handleEventName = (name) => { 29 | return name ? "on" + capitalize(name) : ""; 30 | }; 31 | const EMPTY_OBJ = {}; 32 | 33 | const publicPropertiesMap = { 34 | $el: (i) => i.vnode.el, 35 | $slots: (i) => i.slots, 36 | $props: (i) => i.props, 37 | }; 38 | const publicInstanceProxyHandlers = { 39 | get({ _: instance }, key) { 40 | if (hasOwn(instance.setupState, key)) { 41 | return instance.setupState[key]; 42 | } 43 | else if (hasOwn(instance.props, key)) { 44 | return instance.props[key]; 45 | } 46 | const publicGetter = publicPropertiesMap[key]; 47 | if (publicGetter) { 48 | return publicGetter(instance); 49 | } 50 | }, 51 | }; 52 | 53 | function initProps(instance, rawProps) { 54 | // 将props挂载到实例上 55 | instance.props = rawProps; 56 | // attrs 57 | } 58 | 59 | let activeEffect; 60 | let shouldTrack; 61 | const targetMap = new WeakMap(); 62 | class ReactiveEffect { 63 | constructor(fn, scheduler) { 64 | this.scheduler = scheduler; 65 | // 该变量用来记录是否调过stop函数 66 | this.active = true; 67 | // 收集该effect的dep 68 | this.deps = []; 69 | this._fn = fn; 70 | this.scheduler = scheduler; 71 | } 72 | run(...args) { 73 | activeEffect = this; 74 | if (!this.active) { 75 | return this._fn(...args); 76 | } 77 | shouldTrack = true; 78 | const res = this._fn(...args); 79 | shouldTrack = false; 80 | return res; 81 | } 82 | stop() { 83 | if (this.active) { 84 | cleanupEffect(this); 85 | this.onStop && this.onStop(); 86 | this.active = false; 87 | } 88 | } 89 | } 90 | function cleanupEffect(effect) { 91 | effect.deps.forEach((dep) => { 92 | dep.delete(effect); 93 | }); 94 | // 清空effect.deps 95 | effect.deps.length = 0; 96 | } 97 | function track(target, key) { 98 | if (!isTracking()) 99 | return; 100 | let depsMap = targetMap.get(target); 101 | if (!depsMap) { 102 | depsMap = new Map(); 103 | targetMap.set(target, depsMap); 104 | } 105 | let dep = depsMap.get(key); 106 | if (!dep) { 107 | dep = new Set(); 108 | depsMap.set(key, dep); 109 | } 110 | trackEffects(dep); 111 | } 112 | function trackEffects(dep) { 113 | dep.add(activeEffect); 114 | activeEffect.deps.push(dep); 115 | } 116 | function isTracking() { 117 | return shouldTrack && activeEffect !== undefined; 118 | } 119 | function trigger(target, key) { 120 | const depsMap = targetMap.get(target); 121 | if (!depsMap) 122 | return; 123 | const dep = depsMap.get(key); 124 | triggerEffects(dep); 125 | } 126 | function triggerEffects(dep) { 127 | if (!dep) 128 | return; 129 | for (const effect of dep) { 130 | if (effect.scheduler) { 131 | effect.scheduler(); 132 | } 133 | else { 134 | effect.run(); 135 | } 136 | } 137 | } 138 | function effect(fn, options = {}) { 139 | const _effect = new ReactiveEffect(fn, options.scheduler); 140 | extend(_effect, options); 141 | _effect.run(); 142 | const runner = _effect.run.bind(_effect); 143 | runner.effect = _effect; 144 | return runner; 145 | } 146 | 147 | function createGetter(isReadonly = false, isShallow = false) { 148 | return function get(target, key, receiver) { 149 | const res = Reflect.get(target, key, receiver); 150 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 151 | return !isReadonly; 152 | } 153 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 154 | return isReadonly; 155 | } 156 | if (!isReadonly) { 157 | track(target, key); 158 | } 159 | if (isObject(res) && !isShallow) { 160 | return isReadonly ? readonly(res) : reactive(res); 161 | } 162 | return res; 163 | }; 164 | } 165 | function createSetter() { 166 | return function set(target, key, newValue, receiver) { 167 | const res = Reflect.set(target, key, newValue, receiver); 168 | // 触发依赖 169 | trigger(target, key); 170 | return res; 171 | }; 172 | } 173 | const get = createGetter(); 174 | const set = createSetter(); 175 | const readonlyGet = createGetter(true); 176 | const shallowReadonlyGet = createGetter(true, true); 177 | const mutableHandlers = { 178 | get, 179 | set, 180 | }; 181 | const readonlyHandlers = { 182 | get: readonlyGet, 183 | set(target) { 184 | console.warn(`${target} is a readonly can not be set`); 185 | return true; 186 | }, 187 | }; 188 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 189 | get: shallowReadonlyGet, 190 | }); 191 | 192 | function createReactiveObj(raw, baseHandlers) { 193 | if (!isObject(raw)) { 194 | console.error(`${raw} 必须是一个对象`); 195 | } 196 | return new Proxy(raw, baseHandlers); 197 | } 198 | function reactive(raw) { 199 | return createReactiveObj(raw, mutableHandlers); 200 | } 201 | function readonly(raw) { 202 | return createReactiveObj(raw, readonlyHandlers); 203 | } 204 | function shallowReadonly(raw) { 205 | return createReactiveObj(raw, shallowReadonlyHandlers); 206 | } 207 | 208 | function emit(instance, event, ...args) { 209 | const { props } = instance; 210 | const handlerName = camelize(handleEventName(event)); 211 | const handler = props[handlerName]; 212 | handler && handler(...args); 213 | } 214 | 215 | function initSlots(instance, children) { 216 | // instance.slots = Array.isArray(children) ? children : [children]; 217 | const { vnode } = instance; 218 | if (vnode.shapeFlag & 16 /* SLOT_CHILDREN */) { 219 | instance.slots = children; 220 | for (const slot in children) { 221 | const value = children[slot]; 222 | children[slot] = (props) => normalizeSlotValue(value(props)); 223 | } 224 | } 225 | } 226 | function normalizeSlotValue(value) { 227 | return isArray(value) ? value : [value]; 228 | } 229 | 230 | let currentInstance = null; 231 | function createComponentInstance(vnode, parent) { 232 | const component = { 233 | vnode, 234 | type: vnode.type, 235 | setupState: {}, 236 | props: {}, 237 | slots: {}, 238 | provides: parent ? parent.provides : {}, 239 | isMounted: false, 240 | subTree: {}, 241 | next: null, 242 | update: null, 243 | emit: () => { }, 244 | }; 245 | component.emit = emit.bind(null, component); 246 | return component; 247 | } 248 | function setupComponent(instance) { 249 | const rawProps = instance.vnode.props; 250 | initProps(instance, rawProps); 251 | initSlots(instance, instance.vnode.children); 252 | setupStatefulComponent(instance); 253 | } 254 | function setupStatefulComponent(instance) { 255 | const component = instance.type; 256 | const props = instance.vnode.props; 257 | const emit = instance.emit; 258 | // ctx 代理 259 | instance.proxy = new Proxy({ _: instance }, publicInstanceProxyHandlers); 260 | const { setup } = component; 261 | if (setup) { 262 | setCurrentInstance(instance); 263 | const setupResult = setup(shallowReadonly(props), { emit }); 264 | setCurrentInstance(null); 265 | handleSetupResult(instance, setupResult); 266 | } 267 | } 268 | function handleSetupResult(instance, setupResult) { 269 | // TODO function 270 | // 如果返回的是一个对象,会将返回的值注入到instance中 271 | if (isObject(setupResult)) { 272 | instance.setupState = proxyRefs(setupResult); 273 | } 274 | finishComponentSetup(instance); 275 | } 276 | function finishComponentSetup(instance) { 277 | const component = instance.type; 278 | if (component.render) { 279 | instance.render = component.render; 280 | } 281 | } 282 | function getCurrentInstance() { 283 | return currentInstance; 284 | } 285 | function setCurrentInstance(instance) { 286 | currentInstance = instance; 287 | } 288 | 289 | const Fragment = Symbol("Fragment"); 290 | const Text = Symbol("Text"); 291 | function createVNode(type, props = {}, children = []) { 292 | const vnode = { 293 | type, 294 | props, 295 | children, 296 | appContext: null, 297 | component: null, 298 | key: props.key || undefined, 299 | el: null, 300 | shapeFlag: getShapeFlag(type), 301 | }; 302 | if (Array.isArray(children)) { 303 | // 子节点为数组 304 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 305 | } 306 | else if (typeof children === "string") { 307 | // 子节点为text类型 308 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 309 | } 310 | if (vnode.shapeFlag & 2 /* STATEFUL_COMPONENT */) { 311 | if (isObject(children)) { 312 | vnode.shapeFlag |= 16 /* SLOT_CHILDREN */; 313 | } 314 | } 315 | return vnode; 316 | } 317 | function getShapeFlag(type) { 318 | return typeof type === "string" 319 | ? 1 /* ELEMENT */ 320 | : 2 /* STATEFUL_COMPONENT */; 321 | } 322 | function createTextVNode(text) { 323 | return createVNode(Text, {}, text); 324 | } 325 | 326 | function createAppContext() { 327 | return { 328 | app: null, 329 | components: {}, 330 | }; 331 | } 332 | function createAppAPI(render) { 333 | return function createApp(App) { 334 | const context = createAppContext(); 335 | const app = (context.app = { 336 | mount(rootComponent) { 337 | const vnode = createVNode(App); 338 | vnode.appContext = context; 339 | render(vnode, rootComponent, null); 340 | }, 341 | component(name, component) { 342 | if (!component) { 343 | return context.components[name]; 344 | } 345 | context.components[name] = component; 346 | }, 347 | }); 348 | return app; 349 | }; 350 | } 351 | 352 | class ComputedRefImpl { 353 | constructor(getter) { 354 | this._dirty = true; 355 | this._getter = getter; 356 | // 357 | this._effect = new ReactiveEffect(this._getter, () => { 358 | // scheduler函数会在依赖改变时,执行 359 | // 当computed依赖的值发生改变时,将this._dirty设置为true 360 | // 这样当下次获取.value时,this._dirty就为true,就会重新触发getter函数,获取最新的值 361 | this._dirty = true; 362 | }); 363 | } 364 | get value() { 365 | if (this._dirty) { 366 | // this._dirty 如果为true,则说明需要重新执行getter函数 367 | this._value = this._effect.run(); 368 | // 执行完getter函数将this._dirty设置为false,这样当依赖没有改变时,再次获取.value时,不会重新出发getter函数 369 | this._dirty = false; 370 | } 371 | return this._value; 372 | } 373 | } 374 | function computed(getter) { 375 | return new ComputedRefImpl(getter); 376 | } 377 | 378 | class RefImplement { 379 | constructor(value) { 380 | this.__v_isRef = true; 381 | this._rawValue = value; 382 | this._value = convert(value); 383 | this.deps = new Set(); 384 | } 385 | get value() { 386 | if (isTracking()) { 387 | trackEffects(this.deps); 388 | } 389 | return this._value; 390 | } 391 | set value(newValue) { 392 | if (!hasChange(this._rawValue, newValue)) 393 | return; 394 | this._rawValue = newValue; 395 | this._value = convert(newValue); 396 | triggerEffects(this.deps); 397 | } 398 | } 399 | function convert(value) { 400 | return isObject(value) ? reactive(value) : value; 401 | } 402 | function ref(value) { 403 | return new RefImplement(value); 404 | } 405 | function isRef(ref) { 406 | if (!ref) 407 | return false; 408 | return !!ref.__v_isRef; 409 | } 410 | function unRef(ref) { 411 | return isRef(ref) ? ref.value : ref; 412 | } 413 | function proxyRefs(objWithRefs) { 414 | return new Proxy(objWithRefs, { 415 | get(target, key, receiver) { 416 | return unRef(Reflect.get(target, key, receiver)); 417 | }, 418 | set(target, key, newValue, receiver) { 419 | // 当原来值是ref类型,而修改的值不是ref类型 420 | if (isRef(target[key]) && !isRef(newValue)) { 421 | return (target[key].value = newValue); 422 | } 423 | return Reflect.set(target, key, newValue, receiver); 424 | }, 425 | }); 426 | } 427 | 428 | function shouldUpdateComponent(n1, n2) { 429 | const { props: oldProps } = n1; 430 | const { props: newProps } = n2; 431 | for (const key in newProps) { 432 | if (oldProps[key] !== newProps[key]) { 433 | return true; 434 | } 435 | } 436 | return false; 437 | } 438 | 439 | const queue = []; 440 | let isFlushPending = false; 441 | const p = Promise.resolve(); 442 | function nextTick(fn) { 443 | return fn ? p.then(fn) : p; 444 | } 445 | function queueJobs(fn) { 446 | // queue.in; 447 | if (!queue.includes(fn)) { 448 | queue.push(fn); 449 | } 450 | queueFlush(); 451 | } 452 | function queueFlush() { 453 | if (isFlushPending) 454 | return; 455 | isFlushPending = true; 456 | nextTick(flushJobs); 457 | } 458 | function flushJobs() { 459 | isFlushPending = false; 460 | let job; 461 | while ((job = queue.shift())) { 462 | job && job(); 463 | } 464 | } 465 | 466 | function createRenderer(options) { 467 | const { createElement: hostCreateElement, patchProps: hostPatchProps, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 468 | function render(vnode, container, parent) { 469 | patch(null, vnode, container, parent, null); 470 | } 471 | function patch(n1, n2, container, parent, anchor) { 472 | const { shapeFlag, type } = n2; 473 | switch (type) { 474 | case Fragment: 475 | processFragment(n2, container, parent); 476 | break; 477 | case Text: 478 | processText(n2, container); 479 | break; 480 | default: 481 | if (shapeFlag & 1 /* ELEMENT */) { 482 | processElement(n1, n2, container, parent, anchor); 483 | } 484 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 485 | processComponent(n1, n2, container, parent, anchor); 486 | } 487 | } 488 | } 489 | function processFragment(vnode, container, parent) { 490 | mountChildren(vnode, container, parent); 491 | } 492 | function processText(vnode, container) { 493 | const el = (vnode.el = document.createTextNode(vnode.children)); 494 | container.appendChild(el); 495 | } 496 | function processComponent(n1, n2, container, parent, anchor) { 497 | if (!n1) { 498 | mountComponent(n2, container, parent, anchor); 499 | } 500 | else { 501 | updateComponent(n1, n2); 502 | } 503 | } 504 | function mountComponent(initialVNode, container, parent, anchor) { 505 | const instance = (initialVNode.component = createComponentInstance(initialVNode, parent)); 506 | setupComponent(instance); 507 | setupRenderEffect(instance, initialVNode, container, anchor); 508 | } 509 | function updateComponent(n1, n2) { 510 | // 需要把n1.component赋值给n2.component 511 | // 不然下次更新n2.component会为null 512 | const instance = (n2.component = n1.component); 513 | if (shouldUpdateComponent(n1, n2)) { 514 | instance.next = n2; 515 | instance.update(); 516 | } 517 | } 518 | function setupRenderEffect(instance, vnode, container, anchor) { 519 | instance.update = effect(() => { 520 | const { proxy, isMounted } = instance; 521 | if (!isMounted) { 522 | const subTree = (instance.subTree = instance.render.call(proxy)); 523 | patch(null, subTree, container, instance, anchor); 524 | vnode.el = subTree.el; 525 | instance.isMounted = true; 526 | } 527 | else { 528 | // next是新的虚拟节点,vnode是旧的虚拟节点 529 | const { next, vnode } = instance; 530 | if (next) { 531 | next.el = vnode.el; 532 | updateComponentPreRender(instance, next); 533 | } 534 | const subTree = instance.render.call(proxy); 535 | const prevTree = instance.subTree; 536 | instance.subTree = subTree; 537 | patch(prevTree, subTree, container, instance, anchor); 538 | } 539 | }, { 540 | scheduler() { 541 | queueJobs(instance.update); 542 | }, 543 | }); 544 | } 545 | function updateComponentPreRender(instance, next) { 546 | instance.props = next.props; 547 | instance.next = null; 548 | instance.vnode = next; 549 | } 550 | function processElement(n1, n2, container, parent, anchor) { 551 | if (!n1) { 552 | mountElement(n2, container, parent, anchor); 553 | } 554 | else { 555 | patchElement(n1, n2, parent, anchor); 556 | } 557 | } 558 | function mountElement(vnode, container, parent, anchor) { 559 | const { type, props, children, shapeFlag } = vnode; 560 | // 创建DOM节点 561 | const el = (vnode.el = hostCreateElement(type)); 562 | // 处理props 563 | for (const key in props) { 564 | hostPatchProps(el, key, props[key]); 565 | } 566 | // 处理children属性 567 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 568 | el.textContent = children; 569 | } 570 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 571 | mountChildren(vnode, el, parent); 572 | } 573 | hostInsert(el, container, anchor); 574 | } 575 | function patchElement(n1, n2, parent, anchor) { 576 | // p1 oldProps, p2 newProps 577 | const p1 = n1.props || EMPTY_OBJ; 578 | const p2 = n2.props || EMPTY_OBJ; 579 | // 将el赋值给n2,因为下次patch n2就是n1 580 | const el = (n2.el = n1.el); 581 | patchProps(p1, p2, el); 582 | patchChildren(n1, n2, el, parent, anchor); 583 | } 584 | function patchChildren(n1, n2, container, parent, anchor) { 585 | const c1 = n1.children; 586 | const c2 = n2.children; 587 | const el = n1.el; 588 | const prevShapeFlag = n1.shapeFlag; 589 | const { shapeFlag } = n2; 590 | if (prevShapeFlag & 8 /* ARRAY_CHILDREN */) { 591 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 592 | // 新节点children是文本类型, 旧节点children是数组类型 593 | unmountChildren(n1); 594 | hostSetElementText(el, c2); 595 | } 596 | else { 597 | patchKeydChildren(c1, c2, container, parent, anchor); 598 | } 599 | } 600 | else { 601 | if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 602 | // 新节点children是数组类型,旧节点children是文本类型 603 | hostSetElementText(el, ""); 604 | mountChildren(n2, el, parent); 605 | } 606 | else { 607 | // 新旧节点children类型都是文本类型 608 | if (c1 !== c2) { 609 | hostSetElementText(el, c2); 610 | } 611 | } 612 | } 613 | } 614 | function isSameVNodeType(n1, n2) { 615 | return n1.type === n2.type && n1.key === n2.key; 616 | } 617 | function patchKeydChildren(c1, c2, container, parent, parentAnchor) { 618 | let i = 0; 619 | let e1 = c1.length - 1; 620 | let e2 = c2.length - 1; 621 | const l2 = c2.length; 622 | while (i <= e1 && i <= e2) { 623 | if (isSameVNodeType(c1[i], c2[i])) { 624 | patch(c1[i], c2[i], container, parent, parentAnchor); 625 | } 626 | else { 627 | break; 628 | } 629 | i++; 630 | } 631 | while (i <= e1 && i <= e2) { 632 | const n1 = c1[e1]; 633 | const n2 = c2[e2]; 634 | if (isSameVNodeType(n1, n2)) { 635 | patch(c1[e1], c2[e2], container, parent, parentAnchor); 636 | } 637 | else { 638 | break; 639 | } 640 | e1--; 641 | e2--; 642 | } 643 | if (i > e1) { 644 | if (i <= e2) { 645 | // 找到锚点位置 646 | const nextPos = e2 + 1; 647 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 648 | while (i <= e2) { 649 | // abc -> abcde 650 | // bcd -> abcd 651 | // 新增节点 652 | patch(null, c2[i], container, parent, anchor); 653 | i++; 654 | } 655 | } 656 | } 657 | else if (i > e2) { 658 | while (i <= e1) { 659 | // 删除节点 660 | hostRemove(c1[i].el); 661 | i++; 662 | } 663 | } 664 | else { 665 | let s1 = i; 666 | let s2 = i; 667 | const toBePatched = e2 - s2 + 1; 668 | let patched = 0; 669 | const keyToNewIndexMap = new Map(); 670 | const newIndexToOldIndexMap = new Array(toBePatched).fill(0); 671 | let moved = false; 672 | let maxIndexSoFar = 0; 673 | for (let i = s2; i <= e2; i++) { 674 | const key = c2[i].key; 675 | keyToNewIndexMap.set(key, i); 676 | } 677 | for (let i = s1; i <= e1; i++) { 678 | if (patched >= toBePatched) { 679 | hostRemove(c2[i]); 680 | continue; 681 | } 682 | const preVnode = c1[i]; 683 | const key = preVnode.key; 684 | let newIndex; 685 | if (key !== null || key !== undefined) { 686 | newIndex = keyToNewIndexMap.get(key); 687 | } 688 | else { 689 | for (let j = s2; j <= e2; j++) { 690 | if (isSameVNodeType(preVnode, c2[j])) { 691 | newIndex = j; 692 | break; 693 | } 694 | } 695 | } 696 | if (newIndex === undefined) { 697 | hostRemove(preVnode.el); 698 | } 699 | else { 700 | if (maxIndexSoFar < newIndex) { 701 | maxIndexSoFar = newIndex; 702 | } 703 | else { 704 | moved = true; 705 | } 706 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 707 | patch(preVnode, c2[newIndex], container, parent, null); 708 | patched++; 709 | } 710 | } 711 | // 返回最长递增子序列的下标,也就是新数组的下标。 712 | // 新数组节点的下标在该数组中,则说明不需要移动 713 | const increasingNewIndexSequence = moved 714 | ? getSequence(newIndexToOldIndexMap) 715 | : []; 716 | let j = increasingNewIndexSequence.length - 1; 717 | for (let i = toBePatched - 1; i >= 0; i--) { 718 | const nextIndex = i + s2; 719 | const nextChild = c2[nextIndex]; 720 | const anchor = nextIndex + 1 >= l2 ? null : c2[nextIndex + 1].el; 721 | if (newIndexToOldIndexMap[i] === 0) { 722 | // 需要新增节点 723 | patch(null, nextChild, container, parent, anchor); 724 | } 725 | else if (moved) { 726 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 727 | hostInsert(nextChild.el, container, anchor); 728 | } 729 | else { 730 | j--; 731 | } 732 | } 733 | } 734 | } 735 | } 736 | function unmountChildren(vnode) { 737 | vnode.children.forEach((v) => { 738 | const el = v.el; 739 | hostRemove(el); 740 | }); 741 | } 742 | function patchProps(oldProps, newProps, el) { 743 | if (oldProps !== newProps) { 744 | // 遍历新属性,增加或者修改属性值 745 | for (const key in newProps) { 746 | const prevProp = oldProps[key]; 747 | const currentProp = newProps[key]; 748 | if (prevProp !== currentProp) { 749 | hostPatchProps(el, key, currentProp); 750 | } 751 | } 752 | // 当旧属性不为空对象时 753 | if (oldProps !== EMPTY_OBJ) { 754 | // 遍历旧属性,删除旧属性有而新属性没有的属性 755 | for (const key in oldProps) { 756 | if (!(key in newProps)) { 757 | hostPatchProps(el, key, null); 758 | } 759 | } 760 | } 761 | } 762 | } 763 | function mountChildren(vnode, container, parent) { 764 | vnode.children.forEach((item) => { 765 | patch(null, item, container, parent, null); 766 | }); 767 | } 768 | return { 769 | createApp: createAppAPI(render), 770 | }; 771 | } 772 | function getSequence(arr) { 773 | const p = arr.slice(); 774 | const result = [0]; 775 | let i, j, u, v, c; 776 | const len = arr.length; 777 | for (i = 0; i < len; i++) { 778 | const arrI = arr[i]; 779 | if (arrI !== 0) { 780 | j = result[result.length - 1]; 781 | if (arr[j] < arrI) { 782 | p[i] = j; 783 | result.push(i); 784 | continue; 785 | } 786 | u = 0; 787 | v = result.length - 1; 788 | while (u < v) { 789 | c = (u + v) >> 1; 790 | if (arr[result[c]] < arrI) { 791 | u = c + 1; 792 | } 793 | else { 794 | v = c; 795 | } 796 | } 797 | if (arrI < arr[result[u]]) { 798 | if (u > 0) { 799 | p[i] = result[u - 1]; 800 | } 801 | result[u] = i; 802 | } 803 | } 804 | } 805 | u = result.length; 806 | v = result[u - 1]; 807 | while (u-- > 0) { 808 | result[u] = v; 809 | v = p[v]; 810 | } 811 | return result; 812 | } 813 | 814 | function h(type, props, children) { 815 | return createVNode(type, props, children); 816 | } 817 | 818 | function renderSlots(slots, props, name) { 819 | // 处理具名插槽 820 | if (slots[name]) { 821 | if (typeof slots[name] === "function") { 822 | return createVNode(Fragment, {}, slots[name](props)); 823 | } 824 | } 825 | // 处理默认插槽 826 | const totalSlots = []; 827 | for (const slot in slots) { 828 | totalSlots.push(...slots[slot](props)); 829 | } 830 | return createVNode(Fragment, {}, totalSlots); 831 | } 832 | 833 | const COMPONENTS = "components"; 834 | function resolveComponent(name) { 835 | return resolveAsset(COMPONENTS, name); 836 | } 837 | function resolveAsset(type, name) { 838 | const instance = currentInstance; 839 | if (!instance) 840 | return name; 841 | const res = instance.appContext[type][name]; 842 | return res === undefined ? instance.type : res; 843 | } 844 | 845 | function provide(key, value) { 846 | const instance = getCurrentInstance(); 847 | const parentProvides = instance.parent ? instance.parent.provides : {}; 848 | if (instance) { 849 | let { provides } = instance; 850 | if (parentProvides === provides) { 851 | // parentProvides === provides则表明是第一次进行provide 852 | provides = instance.provides = Object.create(parentProvides); 853 | } 854 | provides[key] = value; 855 | } 856 | } 857 | function inject(key, value) { 858 | const instance = getCurrentInstance(); 859 | if (instance) { 860 | const { provides } = instance; 861 | const injectValue = provides[key]; 862 | if (!injectValue) { 863 | if (typeof value === "function") { 864 | return value(); 865 | } 866 | else { 867 | return value; 868 | } 869 | } 870 | return injectValue; 871 | } 872 | } 873 | 874 | function createElement(type) { 875 | return document.createElement(type); 876 | } 877 | function patchProps(el, key, val) { 878 | // handle props 879 | // 如果key为 on开头并且on后面的第一个字符为大写,则认定为事件监听 880 | const isOn = (key) => /^on[A-Z]/.test(key); 881 | // 处理事件 882 | if (isOn(key)) { 883 | const event = key.slice(2).toLocaleLowerCase(); 884 | el.addEventListener(event, val); 885 | } 886 | else if (val !== undefined && val !== null) { 887 | el.setAttribute(key, val); 888 | } 889 | else { 890 | // 当属性值为undefined或者为null时,直接删除该属性即可 891 | el.removeAttribute(key); 892 | } 893 | } 894 | function insert(child, container, anchor) { 895 | container.insertBefore(child, anchor); 896 | } 897 | function remove(el) { 898 | const parent = el.parentNode; 899 | if (parent) { 900 | parent.removeChild(el); 901 | } 902 | } 903 | function setElementText(el, text) { 904 | el.textContent = text; 905 | } 906 | const renderer = createRenderer({ 907 | createElement, 908 | patchProps, 909 | insert, 910 | remove, 911 | setElementText, 912 | }); 913 | function createApp(...args) { 914 | return renderer.createApp(...args); 915 | } 916 | 917 | exports.computed = computed; 918 | exports.createApp = createApp; 919 | exports.createRenderer = createRenderer; 920 | exports.createTextVNode = createTextVNode; 921 | exports.effect = effect; 922 | exports.getCurrentInstance = getCurrentInstance; 923 | exports.h = h; 924 | exports.inject = inject; 925 | exports.nextTick = nextTick; 926 | exports.provide = provide; 927 | exports.proxyRefs = proxyRefs; 928 | exports.reactive = reactive; 929 | exports.readonly = readonly; 930 | exports.ref = ref; 931 | exports.renderSlots = renderSlots; 932 | exports.renderer = renderer; 933 | exports.resolveComponent = resolveComponent; 934 | exports.shallowReadonly = shallowReadonly; 935 | --------------------------------------------------------------------------------