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