├── .gitignore ├── README.md ├── babel.config.js ├── example ├── componentEmit │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── componentSlot │ ├── APP.js │ ├── Foo.js │ ├── index.html │ └── main.js └── helloworld │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── jest.config.js ├── lib ├── my-vue-next.cjs.js └── my-vue-next.esm.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── index.ts ├── reactivity │ ├── baseHandlers.ts │ ├── computed.ts │ ├── effect.ts │ ├── index.ts │ ├── reactive.ts │ ├── ref.ts │ └── tests │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ └── shallowReadonly.spec.ts ├── runtime-core │ ├── component.ts │ ├── componentEmit.ts │ ├── componentProps.ts │ ├── componentPublicInstance.ts │ ├── componentSlots.ts │ ├── createApp.ts │ ├── h.ts │ ├── helpers │ │ └── renderSlots.ts │ ├── index.ts │ ├── renderer.ts │ └── vnode.ts └── shared │ ├── ShapeFlags.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 实现简易 vue3 2 | 3 | -- reactivity 4 | 5 | 依赖安装 6 | yarn 7 | 8 | 单元测试 9 | yarn test 10 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /example/componentEmit/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/my-vue-next.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | render() { 7 | // emit 8 | return h("div", {}, [ 9 | h("div", {}, "App"), 10 | h(Foo, { 11 | onAdd(a, b) { 12 | console.log("onAdd", a, b); 13 | }, 14 | onAddFoo() { 15 | console.log("onAddFoo"); 16 | }, 17 | }), 18 | ]); 19 | }, 20 | 21 | setup() { 22 | return {}; 23 | }, 24 | }; -------------------------------------------------------------------------------- /example/componentEmit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/my-vue-next.esm.js"; 2 | 3 | export const Foo = { 4 | setup(props, { emit }) { 5 | const emitAdd = () => { 6 | console.log("emit add"); 7 | emit("add",1,2); 8 | emit("add-foo"); 9 | }; 10 | 11 | return { 12 | emitAdd, 13 | }; 14 | }, 15 | render() { 16 | const btn = h( 17 | "button", 18 | { 19 | onClick: this.emitAdd, 20 | }, 21 | "emitAdd" 22 | ); 23 | 24 | const foo = h("p", {}, "foo"); 25 | return h("div", {}, [foo, btn]); 26 | }, 27 | }; -------------------------------------------------------------------------------- /example/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/my-vue-next.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); -------------------------------------------------------------------------------- /example/componentSlot/APP.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/my-vue-next.esm.js"; 2 | import { Foo } from "./Foo.js"; 3 | 4 | export const App = { 5 | name: "App", 6 | render() { 7 | const app = h("div", {}, "App"); 8 | // const foo = h(Foo,{},h("p",{},"123")) 9 | const foo = h(Foo,{},[h("p",{},"123"),h("p",{},"456")]) 10 | // object key 11 | // const foo = h( 12 | // Foo, 13 | // {}, 14 | // { 15 | // header: ({ age }) => h("p", {}, "header" + age), 16 | // footer: () => h("p", {}, "footer"), 17 | // } 18 | // ); 19 | // 数组 vnode 20 | // const foo = h(Foo, {}, h("p", {}, "123")); 21 | return h("div", {}, [app,foo]); 22 | }, 23 | 24 | setup() { 25 | return {}; 26 | }, 27 | }; -------------------------------------------------------------------------------- /example/componentSlot/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from "../../lib/my-vue-next.esm.js"; 2 | 3 | 4 | export const Foo = { 5 | setup() { 6 | return {}; 7 | }, 8 | render() { 9 | const foo = h("p", {}, "foo"); 10 | 11 | // Foo .vnode. children 12 | console.log(this.$slots); 13 | return h("div",{},[foo,renderSlots(this.$slots)]) 14 | // children -> vnode 15 | // 16 | // renderSlots 17 | // 具名插槽 18 | // 1. 获取到要渲染的元素 1 19 | // 2. 要获取到渲染的位置 20 | // 作用域插槽 21 | // const age = 18; 22 | // return h("div", {}, [ 23 | // renderSlots(this.$slots, "header", { 24 | // age, 25 | // }), 26 | // foo, 27 | // renderSlots(this.$slots, "footer"), 28 | // ]); 29 | }, 30 | }; -------------------------------------------------------------------------------- /example/componentSlot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/componentSlot/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "../../lib/my-vue-next.esm.js"; 2 | import { App } from "./App.js"; 3 | 4 | const rootContainer = document.querySelector("#app"); 5 | createApp(App).mount(rootContainer); -------------------------------------------------------------------------------- /example/helloworld/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/my-vue-next.esm.js" 2 | import { Foo } from "./Foo.js"; 3 | 4 | window.self = null 5 | export const App = { 6 | name:'App', 7 | render() { 8 | window.self = this 9 | // ui 10 | return h( 11 | "div", 12 | { 13 | id: "root", 14 | class: ["red", "hard"], 15 | onClick() { 16 | console.log('click') 17 | }, 18 | onMousedown() { 19 | console.log('mousedown') 20 | } 21 | }, 22 | // string 23 | // "hi, "+ this.msg 24 | [ 25 | h("div", {}, "hi," + this.msg), 26 | h(Foo, { 27 | count: 1, 28 | }), 29 | ] 30 | // Array 31 | // [h("p", { class:"red"}, "hi"), h("p", {class:"blue"}, "123")] 32 | ); 33 | }, 34 | 35 | setup() { 36 | return { 37 | msg: "data123", 38 | }; 39 | }, 40 | }; -------------------------------------------------------------------------------- /example/helloworld/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/my-vue-next.esm.js"; 2 | 3 | export const Foo = { 4 | setup(props) { 5 | // props.count 6 | console.log(props); 7 | 8 | // 3. 9 | // shallow readonly 10 | props.count++ 11 | console.log(props); 12 | 13 | }, 14 | render() { 15 | return h("div", {}, "foo: " + this.count); 16 | }, 17 | }; -------------------------------------------------------------------------------- /example/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /example/helloworld/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from '../../lib/my-vue-next.esm.js' 2 | import {App} from './App.js' 3 | 4 | const rootContainer = document.querySelector("#app") 5 | createApp(App).mount(rootContainer); -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZHENGGEGE/my-vue-next/8c9d43ab1ffef6128e07a5c4c5661634f4fdb1fe/jest.config.js -------------------------------------------------------------------------------- /lib/my-vue-next.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | var extend = Object.assign; 6 | var isObject = function (value) { 7 | return value !== null && typeof value === "object"; 8 | }; 9 | var hasOwn = function (val, key) { 10 | return Object.prototype.hasOwnProperty.call(val, key); 11 | }; 12 | var camelize = function (str) { 13 | return str.replace(/-(\w)/g, function (_, c) { 14 | return c ? c.toUpperCase() : ""; 15 | }); 16 | }; 17 | var capitalize = function (str) { 18 | return str.charAt(0).toUpperCase() + str.slice(1); 19 | }; 20 | var toHandlerKey = function (str) { 21 | return str ? "on" + capitalize(str) : ""; 22 | }; 23 | 24 | var targetMap = new Map(); 25 | function trigger(target, key) { 26 | var depsMap = targetMap.get(target); 27 | var dep = depsMap.get(key); 28 | triggerEffect(dep); 29 | } 30 | function triggerEffect(dep) { 31 | for (var _i = 0, dep_1 = dep; _i < dep_1.length; _i++) { 32 | var effect_1 = dep_1[_i]; 33 | // 添加调度器 34 | if (effect_1.scheduler) { 35 | effect_1.scheduler(); 36 | } 37 | else { 38 | effect_1.run(); 39 | } 40 | } 41 | } 42 | 43 | var get = createGetter(); 44 | var set = createSetter(); 45 | var readonlyGet = createGetter(true); 46 | var shallowReadonlyGet = createGetter(true, true); 47 | function createGetter(isReadonly, shallow) { 48 | if (isReadonly === void 0) { isReadonly = false; } 49 | if (shallow === void 0) { shallow = false; } 50 | return function get(target, key) { 51 | console.log("get target", target); 52 | console.log("get key", key); 53 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 54 | return !isReadonly; 55 | } 56 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 57 | return isReadonly; 58 | } 59 | var res = Reflect.get(target, key); 60 | // console.log("res", res); 61 | if (shallow) { 62 | return res; 63 | } 64 | if (isObject(res)) { 65 | return isReadonly ? readonly(res) : reactive(res); 66 | } 67 | return res; 68 | }; 69 | } 70 | function createSetter() { 71 | return function set(target, key, value) { 72 | var res = Reflect.set(target, key, value); 73 | // 触发依赖 74 | trigger(target, key); 75 | return res; 76 | }; 77 | } 78 | var mutableHandlers = { 79 | get: get, 80 | set: set, 81 | }; 82 | var readonlyHandlers = { 83 | get: readonlyGet, 84 | set: function (target, key, value) { 85 | console.warn("key:".concat(key, " set\u5931\u8D25\u56E0\u4E3Atarget\u4E3Areadlony"), target); 86 | return true; 87 | }, 88 | }; 89 | var shallowReadonlyHandlers = extend({}, readonlyHandlers, { 90 | get: shallowReadonlyGet, 91 | }); 92 | 93 | function reactive(raw) { 94 | return createActiveObject(raw, mutableHandlers); 95 | } 96 | function readonly(raw) { 97 | return createActiveObject(raw, readonlyHandlers); 98 | } 99 | function shallowReadonly(raw) { 100 | return createActiveObject(raw, shallowReadonlyHandlers); 101 | } 102 | function createActiveObject(raw, baseHandlers) { 103 | return new Proxy(raw, baseHandlers); 104 | } 105 | 106 | function emit(instance, event) { 107 | var args = []; 108 | for (var _i = 2; _i < arguments.length; _i++) { 109 | args[_i - 2] = arguments[_i]; 110 | } 111 | console.log("emit", event); 112 | // instance.props => event 113 | var props = instance.props; 114 | //tpp 115 | var handlerName = toHandlerKey(camelize(event)); 116 | var handler = props[handlerName]; 117 | handler && handler.apply(void 0, args); 118 | } 119 | 120 | function initProps(instance, rawProps) { 121 | instance.props = rawProps || {}; 122 | } 123 | 124 | var publicPropertiesMap = { 125 | $el: function (i) { return i.vnode.el; }, 126 | $slots: function (i) { return i.slots; }, 127 | }; 128 | var PublicInstanceProxyHandlers = { 129 | get: function (_a, key) { 130 | var instance = _a._; 131 | // setupState 132 | var setupState = instance.setupState, props = instance.props; 133 | if (hasOwn(setupState, key)) { 134 | return setupState[key]; 135 | } 136 | else if (hasOwn(props, key)) { 137 | return props[key]; 138 | } 139 | // key->$el 140 | // if (key === "$el") { 141 | // return instance.vnode.el; 142 | // } 143 | var publicGetter = publicPropertiesMap[key]; 144 | if (publicGetter) { 145 | return publicGetter(instance); 146 | } 147 | }, 148 | }; 149 | 150 | function initSlots(instance, children) { 151 | instance.slots = Array.isArray(children) ? children : [children]; 152 | } 153 | 154 | function createComponentIntance(vnode) { 155 | var component = { 156 | vnode: vnode, 157 | type: vnode.type, 158 | setupState: {}, 159 | props: {}, 160 | slots: {}, 161 | emit: function () { }, 162 | }; 163 | component.emit = emit.bind(null, component); 164 | return component; 165 | } 166 | function setupComponent(instance) { 167 | // TODO 168 | initProps(instance, instance.vnode.props); 169 | initSlots(instance, instance.vnode.children); 170 | setupStatefulComponent(instance); 171 | } 172 | function setupStatefulComponent(instance) { 173 | var Component = instance.type; 174 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 175 | var setup = Component.setup; 176 | if (setup) { 177 | var setupResult = setup(shallowReadonly(instance.props), { 178 | emit: instance.emit, 179 | }); 180 | handleSetupResult(instance, setupResult); 181 | } 182 | } 183 | function handleSetupResult(instance, setupResult) { 184 | if (typeof setupResult === "object") { 185 | instance.setupState = setupResult; 186 | } 187 | finishComponentSetup(instance); 188 | } 189 | function finishComponentSetup(instance) { 190 | var Component = instance.type; 191 | instance.render = Component.render; 192 | } 193 | 194 | function render(vnode, container) { 195 | patch(vnode, container); 196 | } 197 | function patch(vnode, container) { 198 | // TODO 判断vnode 是不是一个element 是element的话 处理element 思考 如何区分是element还是vnode 199 | // console.log(vnode.type); 200 | var shapeFlag = vnode.shapeFlag; 201 | if (shapeFlag & 1 /* ELEMENT */) { 202 | processElement(vnode, container); 203 | // STATEFUL_COMPONENT 204 | } 205 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 206 | processComponent(vnode, container); 207 | } 208 | } 209 | function processElement(vnode, container) { 210 | mountElement(vnode, container); 211 | } 212 | function processComponent(vnode, container) { 213 | mountComponent(vnode, container); 214 | } 215 | function mountElement(vnode, container) { 216 | // const el = document.createElement("div"); 217 | // el.textContent = "hi su"; 218 | // el.setAttribute("id", "root"); 219 | // document.body.append(el); 220 | var el = (vnode.el = document.createElement(vnode.type)); 221 | var children = vnode.children, shapeFlag = vnode.shapeFlag; 222 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 223 | // text_children 224 | el.textContent = children; 225 | } 226 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 227 | mountChildren(vnode, el); 228 | } 229 | var props = vnode.props; 230 | for (var key in props) { 231 | console.log(key); 232 | var val = props[key]; 233 | var isOn = function (key) { return /^on[A-Z]/.test(key); }; 234 | if (isOn(key)) { 235 | var event_1 = key.slice(2).toLowerCase(); 236 | el.addEventListener(event_1, val); 237 | } 238 | else { 239 | el.setAttribute(key, val); 240 | } 241 | } 242 | container.append(el); 243 | } 244 | function mountChildren(vnode, container) { 245 | vnode.children.forEach(function (v) { 246 | patch(v, container); 247 | }); 248 | } 249 | function mountComponent(initialVnode, container) { 250 | var instance = createComponentIntance(initialVnode); 251 | setupComponent(instance); 252 | setupRenderEffect(instance, initialVnode, container); 253 | } 254 | function setupRenderEffect(instance, initialVnode, container) { 255 | var proxy = instance.proxy; 256 | // TODO 257 | var subTree = instance.render.call(proxy); 258 | console.log("proxy", proxy); 259 | console.log("subTree", subTree); 260 | patch(subTree, container); 261 | // element -> mount 262 | initialVnode.el = subTree.el; 263 | } 264 | 265 | function createVNode(type, props, children) { 266 | var vnode = { 267 | type: type, 268 | props: props, 269 | children: children, 270 | shapeFlag: getShapeFlag(type), 271 | el: null, 272 | }; 273 | if (typeof children === "string") { 274 | // vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN; 275 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 276 | } 277 | else if (Array.isArray(children)) { 278 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 279 | } 280 | return vnode; 281 | } 282 | function getShapeFlag(type) { 283 | return typeof type === "string" 284 | ? 1 /* ELEMENT */ 285 | : 2 /* STATEFUL_COMPONENT */; 286 | } 287 | 288 | function createApp(rootComponent) { 289 | return { 290 | mount: function (rootContainer) { 291 | var vnode = createVNode(rootComponent); 292 | render(vnode, rootContainer); 293 | }, 294 | }; 295 | } 296 | 297 | function h(type, props, children) { 298 | return createVNode(type, props, children); 299 | } 300 | 301 | function renderSlots(slots) { 302 | return createVNode("div", {}, slots); 303 | } 304 | 305 | exports.createApp = createApp; 306 | exports.h = h; 307 | exports.renderSlots = renderSlots; 308 | -------------------------------------------------------------------------------- /lib/my-vue-next.esm.js: -------------------------------------------------------------------------------- 1 | var extend = Object.assign; 2 | var isObject = function (value) { 3 | return value !== null && typeof value === "object"; 4 | }; 5 | var hasOwn = function (val, key) { 6 | return Object.prototype.hasOwnProperty.call(val, key); 7 | }; 8 | var camelize = function (str) { 9 | return str.replace(/-(\w)/g, function (_, c) { 10 | return c ? c.toUpperCase() : ""; 11 | }); 12 | }; 13 | var capitalize = function (str) { 14 | return str.charAt(0).toUpperCase() + str.slice(1); 15 | }; 16 | var toHandlerKey = function (str) { 17 | return str ? "on" + capitalize(str) : ""; 18 | }; 19 | 20 | var targetMap = new Map(); 21 | function trigger(target, key) { 22 | var depsMap = targetMap.get(target); 23 | var dep = depsMap.get(key); 24 | triggerEffect(dep); 25 | } 26 | function triggerEffect(dep) { 27 | for (var _i = 0, dep_1 = dep; _i < dep_1.length; _i++) { 28 | var effect_1 = dep_1[_i]; 29 | // 添加调度器 30 | if (effect_1.scheduler) { 31 | effect_1.scheduler(); 32 | } 33 | else { 34 | effect_1.run(); 35 | } 36 | } 37 | } 38 | 39 | var get = createGetter(); 40 | var set = createSetter(); 41 | var readonlyGet = createGetter(true); 42 | var shallowReadonlyGet = createGetter(true, true); 43 | function createGetter(isReadonly, shallow) { 44 | if (isReadonly === void 0) { isReadonly = false; } 45 | if (shallow === void 0) { shallow = false; } 46 | return function get(target, key) { 47 | console.log("get target", target); 48 | console.log("get key", key); 49 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 50 | return !isReadonly; 51 | } 52 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 53 | return isReadonly; 54 | } 55 | var res = Reflect.get(target, key); 56 | // console.log("res", res); 57 | if (shallow) { 58 | return res; 59 | } 60 | if (isObject(res)) { 61 | return isReadonly ? readonly(res) : reactive(res); 62 | } 63 | return res; 64 | }; 65 | } 66 | function createSetter() { 67 | return function set(target, key, value) { 68 | var res = Reflect.set(target, key, value); 69 | // 触发依赖 70 | trigger(target, key); 71 | return res; 72 | }; 73 | } 74 | var mutableHandlers = { 75 | get: get, 76 | set: set, 77 | }; 78 | var readonlyHandlers = { 79 | get: readonlyGet, 80 | set: function (target, key, value) { 81 | console.warn("key:".concat(key, " set\u5931\u8D25\u56E0\u4E3Atarget\u4E3Areadlony"), target); 82 | return true; 83 | }, 84 | }; 85 | var shallowReadonlyHandlers = extend({}, readonlyHandlers, { 86 | get: shallowReadonlyGet, 87 | }); 88 | 89 | function reactive(raw) { 90 | return createActiveObject(raw, mutableHandlers); 91 | } 92 | function readonly(raw) { 93 | return createActiveObject(raw, readonlyHandlers); 94 | } 95 | function shallowReadonly(raw) { 96 | return createActiveObject(raw, shallowReadonlyHandlers); 97 | } 98 | function createActiveObject(raw, baseHandlers) { 99 | return new Proxy(raw, baseHandlers); 100 | } 101 | 102 | function emit(instance, event) { 103 | var args = []; 104 | for (var _i = 2; _i < arguments.length; _i++) { 105 | args[_i - 2] = arguments[_i]; 106 | } 107 | console.log("emit", event); 108 | // instance.props => event 109 | var props = instance.props; 110 | //tpp 111 | var handlerName = toHandlerKey(camelize(event)); 112 | var handler = props[handlerName]; 113 | handler && handler.apply(void 0, args); 114 | } 115 | 116 | function initProps(instance, rawProps) { 117 | instance.props = rawProps || {}; 118 | } 119 | 120 | var publicPropertiesMap = { 121 | $el: function (i) { return i.vnode.el; }, 122 | $slots: function (i) { return i.slots; }, 123 | }; 124 | var PublicInstanceProxyHandlers = { 125 | get: function (_a, key) { 126 | var instance = _a._; 127 | // setupState 128 | var setupState = instance.setupState, props = instance.props; 129 | if (hasOwn(setupState, key)) { 130 | return setupState[key]; 131 | } 132 | else if (hasOwn(props, key)) { 133 | return props[key]; 134 | } 135 | // key->$el 136 | // if (key === "$el") { 137 | // return instance.vnode.el; 138 | // } 139 | var publicGetter = publicPropertiesMap[key]; 140 | if (publicGetter) { 141 | return publicGetter(instance); 142 | } 143 | }, 144 | }; 145 | 146 | function initSlots(instance, children) { 147 | instance.slots = Array.isArray(children) ? children : [children]; 148 | } 149 | 150 | function createComponentIntance(vnode) { 151 | var component = { 152 | vnode: vnode, 153 | type: vnode.type, 154 | setupState: {}, 155 | props: {}, 156 | slots: {}, 157 | emit: function () { }, 158 | }; 159 | component.emit = emit.bind(null, component); 160 | return component; 161 | } 162 | function setupComponent(instance) { 163 | // TODO 164 | initProps(instance, instance.vnode.props); 165 | initSlots(instance, instance.vnode.children); 166 | setupStatefulComponent(instance); 167 | } 168 | function setupStatefulComponent(instance) { 169 | var Component = instance.type; 170 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 171 | var setup = Component.setup; 172 | if (setup) { 173 | var setupResult = setup(shallowReadonly(instance.props), { 174 | emit: instance.emit, 175 | }); 176 | handleSetupResult(instance, setupResult); 177 | } 178 | } 179 | function handleSetupResult(instance, setupResult) { 180 | if (typeof setupResult === "object") { 181 | instance.setupState = setupResult; 182 | } 183 | finishComponentSetup(instance); 184 | } 185 | function finishComponentSetup(instance) { 186 | var Component = instance.type; 187 | instance.render = Component.render; 188 | } 189 | 190 | function render(vnode, container) { 191 | patch(vnode, container); 192 | } 193 | function patch(vnode, container) { 194 | // TODO 判断vnode 是不是一个element 是element的话 处理element 思考 如何区分是element还是vnode 195 | // console.log(vnode.type); 196 | var shapeFlag = vnode.shapeFlag; 197 | if (shapeFlag & 1 /* ELEMENT */) { 198 | processElement(vnode, container); 199 | // STATEFUL_COMPONENT 200 | } 201 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 202 | processComponent(vnode, container); 203 | } 204 | } 205 | function processElement(vnode, container) { 206 | mountElement(vnode, container); 207 | } 208 | function processComponent(vnode, container) { 209 | mountComponent(vnode, container); 210 | } 211 | function mountElement(vnode, container) { 212 | // const el = document.createElement("div"); 213 | // el.textContent = "hi su"; 214 | // el.setAttribute("id", "root"); 215 | // document.body.append(el); 216 | var el = (vnode.el = document.createElement(vnode.type)); 217 | var children = vnode.children, shapeFlag = vnode.shapeFlag; 218 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 219 | // text_children 220 | el.textContent = children; 221 | } 222 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 223 | mountChildren(vnode, el); 224 | } 225 | var props = vnode.props; 226 | for (var key in props) { 227 | console.log(key); 228 | var val = props[key]; 229 | var isOn = function (key) { return /^on[A-Z]/.test(key); }; 230 | if (isOn(key)) { 231 | var event_1 = key.slice(2).toLowerCase(); 232 | el.addEventListener(event_1, val); 233 | } 234 | else { 235 | el.setAttribute(key, val); 236 | } 237 | } 238 | container.append(el); 239 | } 240 | function mountChildren(vnode, container) { 241 | vnode.children.forEach(function (v) { 242 | patch(v, container); 243 | }); 244 | } 245 | function mountComponent(initialVnode, container) { 246 | var instance = createComponentIntance(initialVnode); 247 | setupComponent(instance); 248 | setupRenderEffect(instance, initialVnode, container); 249 | } 250 | function setupRenderEffect(instance, initialVnode, container) { 251 | var proxy = instance.proxy; 252 | // TODO 253 | var subTree = instance.render.call(proxy); 254 | console.log("proxy", proxy); 255 | console.log("subTree", subTree); 256 | patch(subTree, container); 257 | // element -> mount 258 | initialVnode.el = subTree.el; 259 | } 260 | 261 | function createVNode(type, props, children) { 262 | var vnode = { 263 | type: type, 264 | props: props, 265 | children: children, 266 | shapeFlag: getShapeFlag(type), 267 | el: null, 268 | }; 269 | if (typeof children === "string") { 270 | // vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN; 271 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 272 | } 273 | else if (Array.isArray(children)) { 274 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 275 | } 276 | return vnode; 277 | } 278 | function getShapeFlag(type) { 279 | return typeof type === "string" 280 | ? 1 /* ELEMENT */ 281 | : 2 /* STATEFUL_COMPONENT */; 282 | } 283 | 284 | function createApp(rootComponent) { 285 | return { 286 | mount: function (rootContainer) { 287 | var vnode = createVNode(rootComponent); 288 | render(vnode, rootContainer); 289 | }, 290 | }; 291 | } 292 | 293 | function h(type, props, children) { 294 | return createVNode(type, props, children); 295 | } 296 | 297 | function renderSlots(slots) { 298 | return createVNode("div", {}, slots); 299 | } 300 | 301 | export { createApp, h, renderSlots }; 302 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-vue-next", 3 | "version": "1.0.0", 4 | "main": "lib/my-vue-next.cjs.js", 5 | "module":"lib/my-vue-next.esm.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "yarn": "^1.22.17" 9 | }, 10 | "scripts": { 11 | "test": "jest", 12 | "build":"rollup -c rollup.config.js" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.16.5", 16 | "@babel/preset-env": "^7.15.4", 17 | "@babel/preset-typescript": "^7.15.0", 18 | "@rollup/plugin-typescript": "^8.3.0", 19 | "@types/jest": "^27.0.3", 20 | "babel-jest": "^27.1.0", 21 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 22 | "jest": "^27.4.0", 23 | "rollup": "^2.67.2", 24 | "tslib": "^2.3.1", 25 | "typescript": "^4.5.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import pkg from './package.json' 3 | 4 | export default { 5 | input: "./src/index.ts", 6 | output: [ 7 | // 1.cjs -> commonjs 8 | // 2.esm 9 | { 10 | format: "cjs", 11 | file: pkg.main, 12 | }, 13 | { 14 | format: "es", 15 | file: pkg.module, 16 | }, 17 | ], 18 | plugins: [typescript()], 19 | }; 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./runtime-core/index"; 2 | -------------------------------------------------------------------------------- /src/reactivity/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { extend, isObject } from "../shared/index"; 2 | import { track, trigger } from "./effect"; 3 | import { reactive, ReactiveFlags, readonly } from "./reactive"; 4 | 5 | const get = createGetter(); 6 | const set = createSetter(); 7 | const readonlyGet = createGetter(true); 8 | const shallowReadonlyGet = createGetter(true, true); 9 | 10 | function createGetter(isReadonly = false, shallow = false) { 11 | return function get(target, key) { 12 | console.log("get target", target); 13 | console.log("get key", key); 14 | if (key === ReactiveFlags.IS_REACTIVE) { 15 | return !isReadonly; 16 | } else if (key === ReactiveFlags.IS_READONLY) { 17 | return isReadonly; 18 | } 19 | 20 | const res = Reflect.get(target, key); 21 | // console.log("res", res); 22 | if (shallow) { 23 | return res; 24 | } 25 | 26 | if (isObject(res)) { 27 | return isReadonly ? readonly(res) : reactive(res); 28 | } 29 | 30 | if (!isReadonly) { 31 | // 依赖收集 32 | track(target, key); 33 | } 34 | return res; 35 | }; 36 | } 37 | 38 | function createSetter() { 39 | return function set(target, key, value) { 40 | const res = Reflect.set(target, key, value); 41 | 42 | // 触发依赖 43 | trigger(target, key); 44 | return res; 45 | }; 46 | } 47 | 48 | export const mutableHandlers = { 49 | get, 50 | set, 51 | }; 52 | 53 | export const readonlyHandlers = { 54 | get: readonlyGet, 55 | 56 | set(target, key, value) { 57 | console.warn(`key:${key} set失败因为target为readlony`, target); 58 | return true; 59 | }, 60 | }; 61 | 62 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 63 | get: shallowReadonlyGet, 64 | }); 65 | -------------------------------------------------------------------------------- /src/reactivity/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from "./effect"; 2 | 3 | class ComputedRefImpl { 4 | private _getter: any; 5 | private _dirty: boolean = true; 6 | private _value: any; 7 | private _effect: ReactiveEffect; 8 | constructor(getter) { 9 | this._getter = getter; 10 | this._effect = new ReactiveEffect(getter, () => { 11 | if (!this._dirty) { 12 | this._dirty = true; 13 | } 14 | }); 15 | } 16 | 17 | get value() { 18 | // get 19 | // 当依赖的响应式对象值发生改变的时候 20 | // effect 21 | if (this._dirty) { 22 | this._dirty = false; 23 | this._value = this._effect.run(); 24 | } 25 | return this._value; 26 | } 27 | } 28 | 29 | export function computed(getter) { 30 | return new ComputedRefImpl(getter); 31 | } 32 | -------------------------------------------------------------------------------- /src/reactivity/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from "../shared/index"; 2 | 3 | let activeEffect; 4 | let shouldTrack = false; 5 | export class ReactiveEffect { 6 | private _fn: any; 7 | deps = []; 8 | active = true; 9 | onStop?: () => void; 10 | public scheduler: Function | undefined; 11 | 12 | constructor(fn, scheduler?: Function) { 13 | this._fn = fn; 14 | this.scheduler = scheduler; 15 | } 16 | 17 | run() { 18 | if (!this.active) { 19 | return this._fn(); 20 | } 21 | shouldTrack = true; 22 | activeEffect = this; 23 | const r = this._fn(); 24 | // 重置 25 | shouldTrack = false; 26 | return r; 27 | } 28 | 29 | stop() { 30 | if (this.active) { 31 | cleanupEffect(this); 32 | if (this.onStop) { 33 | this.onStop(); 34 | } 35 | this.active = false; 36 | } 37 | } 38 | } 39 | 40 | function cleanupEffect(effect) { 41 | effect.deps.forEach((dep: any) => { 42 | dep.delete(effect); 43 | }); 44 | effect.deps.length = 0; 45 | } 46 | 47 | const targetMap = new Map(); 48 | export function track(target, key) { 49 | if (!isTracking()) return; 50 | let depsMap = targetMap.get(target); 51 | if (!depsMap) { 52 | depsMap = new Map(); 53 | targetMap.set(target, depsMap); 54 | } 55 | let dep = depsMap.get(key); 56 | if (!dep) { 57 | /** 58 | * set 依赖不重复 选set 59 | * target -> key -> dep 60 | */ 61 | dep = new Set(); 62 | depsMap.set(key, dep); 63 | } 64 | 65 | trackEffects(dep); 66 | } 67 | 68 | export function trackEffects(dep) { 69 | // 看看 dep 之前有没有添加过,添加过的话 那么就不添加了 70 | if (dep.has(activeEffect)) return; 71 | dep.add(activeEffect); 72 | activeEffect.deps.push(dep); 73 | } 74 | 75 | export function isTracking() { 76 | return shouldTrack && activeEffect !== undefined; 77 | } 78 | 79 | export function trigger(target, key) { 80 | let depsMap = targetMap.get(target); 81 | let dep = depsMap.get(key); 82 | triggerEffect(dep); 83 | } 84 | 85 | export function triggerEffect(dep) { 86 | for (const effect of dep) { 87 | // 添加调度器 88 | if (effect.scheduler) { 89 | effect.scheduler(); 90 | } else { 91 | effect.run(); 92 | } 93 | } 94 | } 95 | 96 | export function effect(fn, options: any = {}) { 97 | //fn 98 | const _effect = new ReactiveEffect(fn, options.scheduler); 99 | // extend 100 | extend(_effect, options); 101 | 102 | _effect.run(); 103 | 104 | const runner: any = _effect.run.bind(_effect); 105 | 106 | runner.effect = _effect; 107 | 108 | return runner; 109 | } 110 | 111 | export function stop(runner) { 112 | runner.effect.stop(); 113 | } 114 | -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export function add(a, b) { 2 | return a + b; 3 | } 4 | -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mutableHandlers, 3 | readonlyHandlers, 4 | shallowReadonlyHandlers, 5 | } from "./baseHandlers"; 6 | 7 | export const enum ReactiveFlags { 8 | IS_REACTIVE = "__v_isReactive", 9 | IS_READONLY = "__v_isReadonly", 10 | } 11 | 12 | export function reactive(raw) { 13 | return createActiveObject(raw, mutableHandlers); 14 | } 15 | 16 | export function readonly(raw) { 17 | return createActiveObject(raw, readonlyHandlers); 18 | } 19 | 20 | export function shallowReadonly(raw) { 21 | return createActiveObject(raw, shallowReadonlyHandlers); 22 | } 23 | 24 | export function isReactive(value) { 25 | return !!value[ReactiveFlags.IS_REACTIVE]; 26 | } 27 | 28 | export function isReadonly(value) { 29 | return !!value[ReactiveFlags.IS_READONLY]; 30 | } 31 | 32 | export function isProxy(value) { 33 | return isReactive(value) || isReadonly(value); 34 | } 35 | 36 | function createActiveObject(raw: any, baseHandlers) { 37 | return new Proxy(raw, baseHandlers); 38 | } 39 | -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged, isObject } from "../shared/index"; 2 | import { isTracking, trackEffects, triggerEffect } from "./effect"; 3 | import { reactive } from "./reactive"; 4 | 5 | class RefImpl { 6 | public dep; 7 | private _value: any; 8 | public __v_isRef = true; 9 | 10 | constructor(value) { 11 | this._value = convert(value); 12 | this.dep = new Set(); 13 | } 14 | 15 | get value() { 16 | trackRefValue(this); 17 | return this._value; 18 | } 19 | 20 | set value(newValue) { 21 | if (hasChanged(newValue, this._value)) { 22 | this._value = convert(newValue); 23 | triggerEffect(this.dep); 24 | } 25 | } 26 | } 27 | 28 | function convert(value) { 29 | return isObject(value) ? reactive(value) : value; 30 | } 31 | 32 | function trackRefValue(ref) { 33 | if (isTracking()) { 34 | trackEffects(ref.dep); 35 | } 36 | } 37 | 38 | export function ref(value) { 39 | return new RefImpl(value); 40 | } 41 | 42 | export function isRef(ref) { 43 | return !!ref.__v_isRef; 44 | } 45 | 46 | export function unRef(ref) { 47 | return isRef(ref) ? ref.value : ref; 48 | } 49 | 50 | export function proxyRefs(objectWithRefs) { 51 | return new Proxy(objectWithRefs, { 52 | get(target, key) { 53 | return unRef(Reflect.get(target, key)); 54 | }, 55 | set(target, key, value) { 56 | if (isRef(target[key]) && !isRef(value)) { 57 | return (target[key].value = value); 58 | } else { 59 | return Reflect.set(target, key, value); 60 | } 61 | }, 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/reactivity/tests/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "../computed"; 2 | import { reactive } from "../reactive"; 3 | 4 | describe("computed", () => { 5 | it("happy path", () => { 6 | // ref 7 | // .value 8 | const user = reactive({ 9 | age: 1, 10 | }); 11 | 12 | const age = computed(() => { 13 | return user.age; 14 | }); 15 | 16 | expect(age.value).toBe(1); 17 | }); 18 | 19 | it("should compute lazily", () => { 20 | const value = reactive({ 21 | foo: 1, 22 | }); 23 | const getter = jest.fn(() => { 24 | return value.foo; 25 | }); 26 | const cValue = computed(getter); 27 | 28 | // lazy 29 | expect(getter).not.toHaveBeenCalled(); 30 | 31 | expect(cValue.value).toBe(1); 32 | expect(getter).toHaveBeenCalledTimes(1); 33 | 34 | // should not compute again 35 | cValue.value; // get 36 | expect(getter).toHaveBeenCalledTimes(1); 37 | 38 | // should not compute until needed 39 | value.foo = 2; // trigger 40 | expect(getter).toHaveBeenCalledTimes(1); 41 | 42 | // // now it should compute 43 | expect(cValue.value).toBe(2); 44 | // expect(getter).toHaveBeenCalledTimes(2); 45 | 46 | // should not compute again 47 | cValue.value; 48 | expect(getter).toHaveBeenCalledTimes(2); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/reactivity/tests/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "../reactive"; 2 | import { effect, stop } from "../effect"; 3 | 4 | describe("effect", () => { 5 | it("happy path", () => { 6 | const user = reactive({ 7 | age: 10, 8 | }); 9 | let nextAge; 10 | effect(() => { 11 | nextAge = user.age + 1; 12 | }); 13 | expect(nextAge).toBe(11); 14 | // update 15 | user.age++; 16 | expect(nextAge).toBe(12); 17 | }); 18 | 19 | it("runner", () => { 20 | // 1.effect(fn) -> function(runner) -> fn -> return 21 | let foo = 10; 22 | const runner = effect(() => { 23 | foo++; 24 | return "foo"; 25 | }); 26 | expect(foo).toBe(11); 27 | const r = runner(); 28 | expect(r).toBe("foo"); 29 | }); 30 | 31 | it("scheduler", () => { 32 | // 1.通过effect的第二个参数给定的一个scheduler的fn 33 | // 2.effect第一次执行的时候,还会执行fn 34 | // 3.当响应式对象set update 不会执行 fn 而是执行 scheduler 35 | // 4.如果说当执行runner的时候,会再次的执行fn 36 | let dummy; 37 | let run: any; 38 | const scheduler = jest.fn(() => { 39 | run = runner; 40 | }); 41 | const obj = reactive({ foo: 1 }); 42 | const runner = effect( 43 | () => { 44 | dummy = obj.foo; 45 | }, 46 | { scheduler } 47 | ); 48 | expect(scheduler).not.toHaveBeenCalled(); 49 | expect(dummy).toBe(1); 50 | // should be called on first trigger 51 | obj.foo++; 52 | expect(scheduler).toHaveBeenCalledTimes(1); 53 | // // should not run yet 54 | expect(dummy).toBe(1); 55 | // // manually run 56 | run(); 57 | // // should have run 58 | expect(dummy).toBe(2); 59 | }); 60 | 61 | it("stop", () => { 62 | let dummy; 63 | const obj = reactive({ prop: 1 }); 64 | const runner = effect(() => { 65 | dummy = obj.prop; 66 | }); 67 | obj.prop = 2; 68 | expect(dummy).toBe(2); 69 | stop(runner); 70 | // obj.prop = 3; 71 | obj.prop++; 72 | expect(dummy).toBe(2); 73 | 74 | // stopped effect should still be manually callable 75 | runner(); 76 | expect(dummy).toBe(3); 77 | }); 78 | 79 | it("onStop", () => { 80 | const obj = reactive({ 81 | foo: 1, 82 | }); 83 | const onStop = jest.fn(); 84 | let dummy; 85 | const runner = effect( 86 | () => { 87 | dummy = obj.foo; 88 | }, 89 | { 90 | onStop, 91 | } 92 | ); 93 | 94 | stop(runner); 95 | expect(onStop).toBeCalledTimes(1); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /src/reactivity/tests/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, reactive, isProxy } from "../reactive"; 2 | 3 | describe("reactive", () => { 4 | it("happy path", () => { 5 | const original = { foo: 1 }; 6 | const observed = reactive(original); 7 | console.log("original", original); 8 | console.log("observed", observed); 9 | expect(observed).not.toBe(original); 10 | expect(observed.foo).toBe(1); 11 | expect(isReactive(observed)).toBe(true); 12 | expect(isReactive(original)).toBe(false); 13 | expect(isProxy(observed)).toBe(true); 14 | }); 15 | 16 | test("nested reactives", () => { 17 | const original = { 18 | nested: { 19 | foo: 1, 20 | }, 21 | array: [{ bar: 2 }], 22 | }; 23 | const observed = reactive(original); 24 | expect(isReactive(observed.nested)).toBe(true); 25 | expect(isReactive(observed.array)).toBe(true); 26 | expect(isReactive(observed.array[0])).toBe(true); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/reactivity/tests/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReadonly, readonly } from "../reactive"; 2 | 3 | describe("readonly", () => { 4 | it("should make nested values readonly", () => { 5 | const original = { foo: 1, bar: { baz: 2 } }; 6 | const wrapped = readonly(original); 7 | expect(wrapped).not.toBe(original); 8 | expect(isReadonly(wrapped)).toBe(true); 9 | expect(isReadonly(original)).toBe(false); 10 | expect(isReadonly(wrapped.bar)).toBe(true); 11 | expect(isReadonly(original.bar)).toBe(false); 12 | expect(wrapped.foo).toBe(1); 13 | expect(isProxy(wrapped)).toBe(true); 14 | }); 15 | 16 | it("should call console.warn when set", () => { 17 | // mock 18 | console.warn = jest.fn(); 19 | const user = readonly({ 20 | age: 10, 21 | }); 22 | 23 | user.age = 11; 24 | expect(console.warn).toHaveBeenCalled(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/reactivity/tests/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from "../effect"; 2 | import { reactive } from "../reactive"; 3 | import { isRef, proxyRefs, ref, unRef } from "../ref"; 4 | describe("ref", () => { 5 | it("happy path", () => { 6 | const a = ref(1); 7 | expect(a.value).toBe(1); 8 | }); 9 | it("should be reactive", () => { 10 | const a = ref(1); 11 | let dummy; 12 | let calls = 0; 13 | effect(() => { 14 | calls++; 15 | dummy = a.value; 16 | }); 17 | expect(calls).toBe(1); 18 | expect(dummy).toBe(1); 19 | a.value = 2; 20 | expect(calls).toBe(2); 21 | expect(dummy).toBe(2); 22 | // same value should not trigger 23 | a.value = 2; 24 | expect(calls).toBe(2); 25 | expect(dummy).toBe(2); 26 | a.value++; 27 | expect(dummy).toBe(3); 28 | }); 29 | it("should make nested properties reactive", () => { 30 | const a = ref({ 31 | count: 1, 32 | }); 33 | let dummy; 34 | effect(() => { 35 | dummy = a.value.count; 36 | }); 37 | expect(dummy).toBe(1); 38 | a.value.count = 2; 39 | expect(dummy).toBe(2); 40 | }); 41 | 42 | it("isRef", () => { 43 | const a = ref(1); 44 | const user = reactive({ 45 | age: 1, 46 | }); 47 | expect(isRef(a)).toBe(true); 48 | expect(isRef(1)).toBe(false); 49 | expect(isRef(user)).toBe(false); 50 | }); 51 | 52 | it("unRef", () => { 53 | const a = ref(1); 54 | expect(unRef(a)).toBe(1); 55 | expect(unRef(1)).toBe(1); 56 | }); 57 | 58 | it("proxyRefs", () => { 59 | const user = { 60 | age: ref(10), 61 | name: "xiaohong", 62 | }; 63 | 64 | const proxyUser = proxyRefs(user); 65 | expect(user.age.value).toBe(10); 66 | expect(proxyUser.age).toBe(10); 67 | expect(proxyUser.name).toBe("xiaohong"); 68 | 69 | proxyUser.age = 20; 70 | 71 | expect(proxyUser.age).toBe(20); 72 | expect(user.age.value).toBe(20); 73 | 74 | proxyUser.age = ref(10); 75 | expect(proxyUser.age).toBe(10); 76 | expect(user.age.value).toBe(10); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/reactivity/tests/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, shallowReadonly } from "../reactive"; 2 | 3 | describe("shallowReadonly", () => { 4 | test("should not make non-reactive properties reactive", () => { 5 | const props = shallowReadonly({ n: { foo: 1 } }); 6 | expect(isReadonly(props)).toBe(true); 7 | expect(isReadonly(props.n)).toBe(false); 8 | }); 9 | 10 | it("should call console.warn when set", () => { 11 | console.warn = jest.fn(); 12 | const user = shallowReadonly({ 13 | age: 10, 14 | }); 15 | 16 | user.age = 11; 17 | expect(console.warn).toHaveBeenCalled(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import { shallowReadonly } from "../reactivity/reactive"; 2 | import { emit } from "./componentEmit"; 3 | import { initProps } from "./componentProps"; 4 | import { PublicInstanceProxyHandlers } from "./componentPublicInstance"; 5 | import { initSlots } from "./componentSlots"; 6 | 7 | export function createComponentIntance(vnode) { 8 | const component = { 9 | vnode, 10 | type: vnode.type, 11 | setupState: {}, 12 | props: {}, 13 | slots: {}, 14 | emit: () => {}, 15 | }; 16 | 17 | component.emit = emit.bind(null, component) as any; 18 | 19 | return component; 20 | } 21 | 22 | export function setupComponent(instance) { 23 | // TODO 24 | initProps(instance, instance.vnode.props); 25 | initSlots(instance, instance.vnode.children); 26 | setupStatefulComponent(instance); 27 | } 28 | 29 | function setupStatefulComponent(instance: any) { 30 | const Component = instance.type; 31 | 32 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 33 | 34 | const { setup } = Component; 35 | if (setup) { 36 | const setupResult = setup(shallowReadonly(instance.props), { 37 | emit: instance.emit, 38 | }); 39 | handleSetupResult(instance, setupResult); 40 | } 41 | } 42 | 43 | function handleSetupResult(instance, setupResult) { 44 | if (typeof setupResult === "object") { 45 | instance.setupState = setupResult; 46 | } 47 | finishComponentSetup(instance); 48 | } 49 | 50 | function finishComponentSetup(instance: any) { 51 | const Component = instance.type; 52 | instance.render = Component.render; 53 | } 54 | -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from "../shared/index"; 2 | 3 | export function emit(instance, event, ...args) { 4 | console.log("emit", event); 5 | 6 | // instance.props => event 7 | const { props } = instance; 8 | //tpp 9 | const handlerName = toHandlerKey(camelize(event)); 10 | const handler = props[handlerName]; 11 | handler && handler(...args); 12 | } 13 | -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance, rawProps) { 2 | instance.props = rawProps || {}; 3 | } 4 | -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from "../shared/index"; 2 | 3 | const publicPropertiesMap = { 4 | $el: (i) => i.vnode.el, 5 | $slots: (i) => i.slots, 6 | }; 7 | 8 | export const PublicInstanceProxyHandlers = { 9 | get({ _: instance }, key) { 10 | // setupState 11 | const { setupState, props } = instance; 12 | 13 | if (hasOwn(setupState, key)) { 14 | return setupState[key]; 15 | } else if (hasOwn(props, key)) { 16 | return props[key]; 17 | } 18 | // key->$el 19 | // if (key === "$el") { 20 | // return instance.vnode.el; 21 | // } 22 | 23 | const publicGetter = publicPropertiesMap[key]; 24 | if (publicGetter) { 25 | return publicGetter(instance); 26 | } 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | export function initSlots(instance, children) { 2 | instance.slots = Array.isArray(children) ? children : [children]; 3 | } 4 | -------------------------------------------------------------------------------- /src/runtime-core/createApp.ts: -------------------------------------------------------------------------------- 1 | import { render } from "./renderer"; 2 | import { createVNode } from "./vnode"; 3 | 4 | export function createApp(rootComponent) { 5 | return { 6 | mount(rootContainer) { 7 | const vnode = createVNode(rootComponent); 8 | 9 | render(vnode, rootContainer); 10 | }, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/runtime-core/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "../vnode"; 2 | 3 | export function renderSlots(slots) { 4 | return createVNode("div", {}, slots); 5 | } 6 | -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | export { createApp } from "./createApp"; 2 | export { h } from "./h"; 3 | export { renderSlots } from "./helpers/renderSlots"; 4 | -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from "../shared/index"; 2 | import { ShapeFlags } from "../shared/ShapeFlags"; 3 | import { createComponentIntance, setupComponent } from "./component"; 4 | 5 | export function render(vnode, container) { 6 | patch(vnode, container); 7 | } 8 | 9 | function patch(vnode, container) { 10 | // TODO 判断vnode 是不是一个element 是element的话 处理element 思考 如何区分是element还是vnode 11 | // console.log(vnode.type); 12 | const { shapeFlag } = vnode; 13 | 14 | if (shapeFlag & ShapeFlags.ELEMENT) { 15 | processElement(vnode, container); 16 | // STATEFUL_COMPONENT 17 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 18 | processComponent(vnode, container); 19 | } 20 | } 21 | 22 | function processElement(vnode: any, container: any) { 23 | mountElement(vnode, container); 24 | } 25 | 26 | function processComponent(vnode, container) { 27 | mountComponent(vnode, container); 28 | } 29 | 30 | function mountElement(vnode, container) { 31 | // const el = document.createElement("div"); 32 | // el.textContent = "hi su"; 33 | // el.setAttribute("id", "root"); 34 | // document.body.append(el); 35 | const el = (vnode.el = document.createElement(vnode.type)); 36 | 37 | const { children, shapeFlag } = vnode; 38 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 39 | // text_children 40 | el.textContent = children; 41 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 42 | mountChildren(vnode, el); 43 | } 44 | 45 | const { props } = vnode; 46 | 47 | for (const key in props) { 48 | console.log(key); 49 | const val = props[key]; 50 | const isOn = (key: string) => /^on[A-Z]/.test(key); 51 | if (isOn(key)) { 52 | const event = key.slice(2).toLowerCase(); 53 | el.addEventListener(event, val); 54 | } else { 55 | el.setAttribute(key, val); 56 | } 57 | } 58 | 59 | container.append(el); 60 | } 61 | 62 | function mountChildren(vnode, container) { 63 | vnode.children.forEach((v) => { 64 | patch(v, container); 65 | }); 66 | } 67 | 68 | function mountComponent(initialVnode, container) { 69 | const instance = createComponentIntance(initialVnode); 70 | 71 | setupComponent(instance); 72 | setupRenderEffect(instance, initialVnode, container); 73 | } 74 | 75 | function setupRenderEffect(instance, initialVnode, container) { 76 | const { proxy } = instance; 77 | // TODO 78 | const subTree = instance.render.call(proxy); 79 | console.log("proxy", proxy); 80 | console.log("subTree", subTree); 81 | patch(subTree, container); 82 | // element -> mount 83 | initialVnode.el = subTree.el; 84 | } 85 | -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from "../shared/ShapeFlags"; 2 | 3 | export function createVNode(type, props?, children?) { 4 | const vnode = { 5 | type, 6 | props, 7 | children, 8 | shapeFlag: getShapeFlag(type), 9 | el: null, 10 | }; 11 | 12 | if (typeof children === "string") { 13 | // vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN; 14 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN; 15 | } else if (Array.isArray(children)) { 16 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN; 17 | } 18 | return vnode; 19 | } 20 | 21 | function getShapeFlag(type) { 22 | return typeof type === "string" 23 | ? ShapeFlags.ELEMENT 24 | : ShapeFlags.STATEFUL_COMPONENT; 25 | } 26 | -------------------------------------------------------------------------------- /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 | } 7 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export const extend = Object.assign; 2 | 3 | export const isObject = (value) => { 4 | return value !== null && typeof value === "object"; 5 | }; 6 | 7 | export const hasChanged = (val, newVal) => { 8 | return !Object.is(val, newVal); 9 | }; 10 | 11 | export const hasOwn = (val, key) => { 12 | return Object.prototype.hasOwnProperty.call(val, key); 13 | }; 14 | 15 | export const camelize = (str: string) => { 16 | return str.replace(/-(\w)/g, (_, c: string) => { 17 | return c ? c.toUpperCase() : ""; 18 | }); 19 | }; 20 | 21 | export const capitalize = (str: string) => { 22 | return str.charAt(0).toUpperCase() + str.slice(1); 23 | }; 24 | 25 | export const toHandlerKey = (str: string) => { 26 | return str ? "on" + capitalize(str) : ""; 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "DOM", 17 | "es6" 18 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 19 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 20 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 25 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | 29 | /* Modules */ 30 | "module": "esnext" /* Specify what module code is generated. */, 31 | // "rootDir": "./", /* Specify the root folder within your source files. */ 32 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 33 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 34 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 37 | "types": [ 38 | "jest" 39 | ] /* Specify type package names to be included without being referenced in a source file. */, 40 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 41 | // "resolveJsonModule": true, /* Enable importing .json files */ 42 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 43 | 44 | /* JavaScript Support */ 45 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 46 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 47 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 48 | 49 | /* Emit */ 50 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 51 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 54 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 55 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 56 | // "removeComments": true, /* Disable emitting comments. */ 57 | // "noEmit": true, /* Disable emitting files from a compilation. */ 58 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 59 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 64 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 65 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 66 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 67 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 68 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 69 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 70 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 71 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 72 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 80 | 81 | /* Type Checking */ 82 | "strict": true /* Enable all strict type-checking options. */, 83 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 84 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 85 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 86 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 87 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 88 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 89 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 91 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 96 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 101 | 102 | /* Completeness */ 103 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 104 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 105 | } 106 | } 107 | --------------------------------------------------------------------------------