├── .gitignore
├── Preview
├── App.js
├── core
│ ├── h.js
│ ├── index.js
│ ├── reactivity
│ │ └── reactivity.js
│ └── renderer
│ │ └── index.js
├── index.css
├── index.html
└── index.js
├── README.md
├── babel.config.js
├── demo
├── .DS_Store
├── componentEmit
│ ├── App.js
│ ├── Foo.js
│ ├── index.html
│ └── main.js
├── componentProvideInject
│ ├── App.js
│ ├── index.html
│ └── main.js
├── componentSlots
│ ├── App.js
│ ├── Foo.js
│ ├── index.html
│ └── main.js
├── currentInstance
│ ├── App.js
│ ├── Foo.js
│ ├── index.html
│ └── main.js
├── customRenderer
│ ├── App.js
│ ├── index.html
│ └── main.js
├── helloworld
│ ├── App.js
│ ├── Props.js
│ ├── index.html
│ └── main.js
├── patchChildren
│ ├── App.js
│ ├── ArrayToArray.js
│ ├── ArrayToText.js
│ ├── TextToArray.js
│ ├── TextToText.js
│ ├── index.html
│ └── main.js
└── update
│ ├── App.js
│ ├── index.html
│ └── main.js
├── 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
│ │ ├── effct.spec.ts
│ │ ├── reactive.spec.ts
│ │ ├── readonly.spec.ts
│ │ ├── ref.spec.ts
│ │ └── shallowReadonly.test.ts
├── runtime-core
│ ├── apiInject.ts
│ ├── component.ts
│ ├── componentEmit.ts
│ ├── componentProps.ts
│ ├── componentPublicInstance.ts
│ ├── componentSlots.ts
│ ├── createApp.ts
│ ├── h.ts
│ ├── helpers
│ │ └── renderSlots.ts
│ ├── index.ts
│ ├── renderer.ts
│ └── vnode.ts
├── runtime-dom
│ └── index.ts
└── shared
│ ├── ShapeFlags.ts
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | word.txt
3 | .vscode
4 | lib
--------------------------------------------------------------------------------
/Preview/App.js:
--------------------------------------------------------------------------------
1 | import { reactive } from "./core/reactivity/reactivity.js";
2 | import { h } from "./core/h.js";
3 |
4 | // 实际就是组件
5 | export default {
6 | // 构建视图
7 | // 返回虚拟dom
8 | render(context) {
9 | //简单案例返回真实dom
10 | // let ele = document.createElement('div')
11 | // ele.innerText = context.state.count
12 | // return ele
13 | // 如何表达一个dom?
14 | //1、tag
15 | //2、props
16 | //3、chiildren
17 | //使用vdom提高性能 > 为diff算法做准备
18 |
19 | // string=>array
20 | // return h(
21 | // "div",
22 | // { class: "test" },
23 | // context.state.count > 0 ? [h("div", {}, "现在是1")] : "现在是0"
24 | // );
25 |
26 | // string => strings
27 | // return h(
28 | // "div",
29 | // { class: "test" },
30 | // context.state.count > 0 ? "现在是1" : "现在是0"
31 | // );
32 |
33 | // (array) => strings;
34 | // return h(
35 | // "div",
36 | // { class: "test" },
37 | // context.state.count > 0 ? "string" : [h("div", {}, "array")]
38 | // );
39 |
40 | // oldarray > newarray;
41 | // return h(
42 | // "div",
43 | // { class: "test" },
44 | // context.state.count > 0
45 | // ? [h("div", {}, "1")]
46 | // : [h("div", {}, "1"), h("div", {}, "2")]
47 | // );
48 |
49 | // newarray > oldarray;
50 | return h(
51 | "div",
52 | { class: "test" },
53 | context.state.count > 0
54 | ? [h("div", {}, "1"), h("div", {}, "2")]
55 | : [h("div", {}, "1")]
56 | );
57 | },
58 | setup() {
59 | let state = reactive({
60 | count: 0,
61 | });
62 | window.state = state;
63 | return { state };
64 | },
65 | };
66 |
--------------------------------------------------------------------------------
/Preview/core/h.js:
--------------------------------------------------------------------------------
1 | // 用于返回vdom
2 | export function h(tag, props, children) {
3 | return {
4 | tag,
5 | props,
6 | children,
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/Preview/core/index.js:
--------------------------------------------------------------------------------
1 | import { effectWatch } from "./reactivity/reactivity.js";
2 | import { mountElement, diff } from "./renderer/index.js";
3 | // component.render(component.setup())
4 | // createApp(component).mount(Element)
5 |
6 | export function createApp(component) {
7 | return {
8 | // 将dom挂载到页面上
9 | mount(id, css) {
10 | // 相当于声明
11 | let context = component.setup();
12 | let isMonted = false;
13 | let prevDom;
14 | // 挂载样式
15 | const head = document.getElementsByTagName("head")[0];
16 | const style = document.createElement("style");
17 | style.innerHTML = css;
18 | head.appendChild(style);
19 |
20 | effectWatch(() => {
21 | let el = document.getElementById(id);
22 | const vdom = component.render(context);
23 |
24 | //可以增加吧css挂载进去
25 | if (!isMonted) {
26 | //init
27 | el.innerHTML = "";
28 | //把虚拟dom挂载
29 | mountElement(vdom, el);
30 | isMonted = true;
31 | } else {
32 | //update
33 | // 比较差异修改
34 | diff(prevDom, vdom);
35 | }
36 | //为下次比较做准备
37 | prevDom = vdom;
38 | });
39 | },
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/Preview/core/reactivity/reactivity.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 核心点
3 | * 1、使用cureffect连接类与函数,使得能进行依赖收集
4 | * 2、巧用get set,get时收集依赖,set时执行依赖
5 | * 3、依赖就是effect中的方法
6 | */
7 |
8 | let cureffect;
9 |
10 | class Ref {
11 | constructor(val) {
12 | this.val = val;
13 | this.effects = new Set();
14 | }
15 | get value() {
16 | //用到的时候才收集依赖
17 | this.depend();
18 | return this.val;
19 | }
20 | set value(val) {
21 | this.val = val;
22 | this.notice();
23 | }
24 | // 收集依赖
25 | depend() {
26 | if (cureffect) {
27 | this.effects.add(cureffect);
28 | }
29 | }
30 | // 触发依赖
31 | notice() {
32 | this.effects.forEach((effect) => {
33 | effect();
34 | });
35 | }
36 | }
37 |
38 | // effectWatch的实现
39 | export function effectWatch(effect) {
40 | cureffect = effect;
41 | //a.depend()
42 | //一上来会调用一次
43 | effect();
44 | cureffect = null;
45 | }
46 |
47 | //类似ref的实现
48 | // let a = new Ref(1);
49 | // let b = 0;
50 |
51 | // effectWatch(() => {
52 | // console.log("effct执行了");
53 | // b = a.value + 20;
54 | // });
55 |
56 | // a.value = 80;
57 | // console.log(a.value,b)
58 |
59 | let targetMap = new Map();
60 | //传入对象
61 | //每个对象对应 targetmap中的一个 refs
62 | //每个对象的key 对应refs中的 ref
63 |
64 | // obj(传入的对象) refs(obj对应的ref集合) ref(obj[key] 对应的ref)
65 | // targetMao.get(obj)=>refs refs.get(key)=>ref
66 |
67 | function getRef(target, key) {
68 | let refs = targetMap.get(target);
69 | if (!refs) {
70 | //第一次进来不存在就new
71 | refs = new Map();
72 | targetMap.set(target, refs);
73 | }
74 | let ref = refs.get(key);
75 | if (!ref) {
76 | //第一次进来
77 | ref = new Ref();
78 | refs.set(key, ref);
79 | }
80 | return ref;
81 | }
82 | export function reactive(raw) {
83 | //对每个key进行响应式处理,使用proxy,可以批量对key进行get/set操作
84 | return new Proxy(raw, {
85 | //target 目标对象 key访问键值
86 | // 只是对依赖进行收集
87 | get(target, key) {
88 | const ref = getRef(target, key);
89 | // 收集依赖
90 | ref.depend();
91 | return Reflect.get(target, key);
92 | },
93 |
94 | // 我的理解是这里仅仅使用了ref收集依赖的功能进行执行,而不是对ref进行赋值操作
95 | // 先对原始对象进赋值 然后使用对应的ref进行依赖的执行
96 | set(target, key, value) {
97 | const ref = getRef(target, key);
98 | const res = Reflect.set(target, key, value);
99 | // 依赖执行
100 | ref.notice();
101 | return res;
102 | },
103 | });
104 | }
105 |
106 | // let c = reactive({
107 | // name:'666'
108 | // })
109 |
110 | // effectWatch(()=>{
111 | // console.log('执行更新')
112 | // })
113 | // c.name = '999'
114 | // console.log(c)
115 |
--------------------------------------------------------------------------------
/Preview/core/renderer/index.js:
--------------------------------------------------------------------------------
1 | // vdom => dom
2 | /**
3 | * 使用vdom创建真实dom
4 | * @param {*} vdom 虚拟节点
5 | * @param {*} elementContainer 元素容器
6 | */
7 | export function mountElement(vdom, elementContainer) {
8 | const { tag, props, children } = vdom;
9 | //tag
10 | const el = document.createElement(tag);
11 | // 把el赋值给vdom方便diff去操作 把自己创建的元素赋值给自己
12 | vdom.el = el;
13 | // props
14 | if (props) {
15 | Object.keys(props).forEach((key) => {
16 | el.setAttribute(key, props[key]);
17 | });
18 | }
19 | if (children) {
20 | //children 可能是单个可能是多个
21 | if (typeof children === "string") {
22 | const textNode = document.createTextNode(children);
23 | el.append(textNode);
24 | } else if (Array.isArray(children)) {
25 | // 递归的挂载
26 | children.forEach((v) => {
27 | mountElement(v, el);
28 | });
29 | }
30 | }
31 | //render
32 | elementContainer.append(el);
33 | }
34 |
35 | // n1老的 n2新的
36 | /**
37 | * diff算法
38 | * @param {*} n1 新vdom
39 | * @param {*} n2 老vdom
40 | */
41 | export function diff(n1, n2) {
42 | // 三种改变
43 | //tag
44 | //props
45 | //children
46 |
47 | //1、处理tag tag不一样则改变
48 | if (n1.tag !== n2.tag) {
49 | n1.el.replaceWith(document.createElement(n2.tag));
50 | } else {
51 | // 2、处理props
52 | //传递 要不保存为prevdom时找不到el
53 | n2.el = n1.el;
54 | const { props: newProps } = n2;
55 | const { props: oldProps } = n1;
56 | //新的 增加 改变
57 | if (newProps && oldProps) {
58 | Object.keys(newProps).forEach((key) => {
59 | const newVal = newProps[key];
60 | const oldVal = oldProps[key];
61 | //值不一样的话则设置
62 | if (newVal !== oldVal) {
63 | n1.el.setAttribute(key, newVal);
64 | }
65 | });
66 | }
67 | //新的 删除
68 | if (oldProps) {
69 | Object.keys(oldProps).forEach((key) => {
70 | const newVal = newProps[key];
71 | //如果新的里没有则删除
72 | if (!newVal) {
73 | n1.el.removeAttribute(key);
74 | }
75 | });
76 | }
77 |
78 | //3、处理children
79 | const { children: newChilren } = n2;
80 | const { children: oldChilren } = n1;
81 | // 四种情况
82 | // string newchilren=> string oldchilren
83 | // string newchilren=> array oldchilren
84 | // array newchilren=> array oldchilren
85 | // array newchilren=>string oldchilren
86 |
87 | // newChildren string时
88 | if (typeof newChilren === "string") {
89 | if (typeof oldChilren === "string") {
90 | // 不相等时才更新
91 | if (newChilren !== oldChilren) {
92 | n1.el.innerText = newChilren;
93 | }
94 | } else if (Array.isArray(oldChilren)) {
95 | n1.el.innerText = newChilren;
96 | }
97 | }
98 | // newChildren array时
99 | if (Array.isArray(newChilren)) {
100 | if (typeof oldChilren === "string") {
101 | // 清空元素然后再创建
102 | n1.el.innerHTML = "";
103 | newChilren.forEach((v) => {
104 | mountElement(v, n1.el);
105 | });
106 | } else if (Array.isArray(oldChilren)) {
107 | const length = Math.min(newChilren.length, oldChilren.length);
108 | for (let index = 0; index < length; index++) {
109 | const newDom = newChilren[index];
110 | const oldDom = oldChilren[index];
111 | // 将最小长度的部分 进行diff
112 | diff(newDom, oldDom);
113 | }
114 | // 新的比老的多 则创建元素
115 | if (newChilren.length > length) {
116 | for (let index = length; index < newChilren.length; index++) {
117 | const newDom = newChilren[index];
118 | mountElement(newDom, n1.el);
119 | }
120 | }
121 | // 老的比新的多 则删除元素
122 | if (oldChilren.length > length) {
123 | for (let index = length; index < oldChilren.length; index++) {
124 | const oldDom = oldChilren[index];
125 | oldDom.el.parentNode.removeChild(oldDom.el);
126 | }
127 | }
128 | }
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Preview/index.css:
--------------------------------------------------------------------------------
1 | .test {
2 | background-color: #888;
3 | color: white;
4 | }
5 |
--------------------------------------------------------------------------------
/Preview/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Preview/index.js:
--------------------------------------------------------------------------------
1 | // 入口文件
2 | import { createApp } from "./core/index.js";
3 | import App from "./App.js";
4 | const css = `
5 | .test {
6 | background-color: #888;
7 | color: white;
8 | }
9 | `;
10 | createApp(App).mount("app", css);
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cai-Vue
2 |
3 | 菜的简易 vue 实现
4 |
5 | ## reactivity
6 |
7 | 1. reactive
8 |
9 | - [x] reactive
10 | - [x] readonly
11 | - [x] isReactive
12 | - [x] isReadonly
13 | - [x] isProxy
14 | - [x] shallowReactive
15 | - [x] shallowReadonly
16 | - [ ] markRaw
17 | - [x] toRaw
18 |
19 | 2. effect
20 |
21 | - [x] effect
22 | - [x] stop
23 | - [x] trigger
24 | - [x] track
25 | - [ ] enableTracking
26 | - [ ] pauseTracking
27 | - [ ] resetTracking
28 |
29 | 3. refs
30 |
31 | - [x] ref
32 | - [x] shallowRef
33 | - [x] isRef
34 | - [x] toRef
35 | - [x] toRefs
36 | - [x] unref
37 | - [x] proxyRefs
38 | - [ ] customRef
39 | - [ ] triggerRef
40 |
41 | 4. computed
42 |
43 | - [x] computed
44 |
45 | 5. effectScope
46 |
47 | - [ ] effectScope
48 | - [ ] getCurrentScope
49 | - [ ] onScopeDispose
50 |
51 | ## runtime-core
52 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ["@babel/preset-env", { targets: { node: "current" } }],
4 | "@babel/preset-typescript",
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/demo/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GU0FORY1/Cai-Vue/66fb0f394e00da2fdaf35b9c1e4c1bfcdc46a2ce/demo/.DS_Store
--------------------------------------------------------------------------------
/demo/componentEmit/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/cai-vue.esm.js";
2 | import Foo from "./Foo.js";
3 | export default {
4 | render(){
5 | return h('div',{},[h(Foo,{
6 | onAdd(a){
7 | console.log('触发了onAdd',a)
8 | }
9 | })])
10 | },
11 | //状态
12 | setup(){
13 | return {
14 | msg:'03'
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/demo/componentEmit/Foo.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/cai-vue.esm.js";
2 | export default {
3 | render(){
4 | return h('button',{onClick:this.emitEvent},'event')
5 | },
6 | //状态
7 | setup(props,{emit}){
8 | const emitEvent=()=>{
9 | emit('add','传入参数')
10 | }
11 | return {
12 | msg:'03',
13 | emitEvent
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/demo/componentEmit/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/componentEmit/main.js:
--------------------------------------------------------------------------------
1 | import {createApp } from "../../lib/cai-vue.esm.js";
2 |
3 | import App from "./App.js";
4 | const rootContainer = document.getElementById('app')
5 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/demo/componentProvideInject/App.js:
--------------------------------------------------------------------------------
1 | import { h, provide, inject } from "../../lib/cai-vue.esm.js";
2 | export default {
3 | name: "app",
4 | render() {
5 | return h("div", {}, [h("div", {}, "APP"), h(Foo)]);
6 | },
7 | setup() {
8 | provide("name", "tom");
9 | provide("age", 12);
10 | provide("val", "appVal");
11 | },
12 | };
13 |
14 | const Foo = {
15 | name: "Foo",
16 | render() {
17 | return h("div", {}, [
18 | h("span", {}, [
19 | h("span", {}, `fooval val ${this.val} ;${this.name}-${this.age}`),
20 | h(son),
21 | ]),
22 | ]);
23 | },
24 | setup() {
25 | provide("val", "fooVal");
26 | const age = inject("age");
27 | const name = inject("name");
28 | //这里我想取的是appval 不判断则会被替换掉
29 | const val = inject("val");
30 | return {
31 | name,
32 | age,
33 | val,
34 | };
35 | },
36 | };
37 | const son = {
38 | name: "son",
39 | render() {
40 | return h("div", {}, [h("div", {}, `son val${this.val}`)]);
41 | },
42 | setup() {
43 | const val = inject("val");
44 | return {
45 | val,
46 | };
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/demo/componentProvideInject/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/componentProvideInject/main.js:
--------------------------------------------------------------------------------
1 | import {createApp } from "../../lib/cai-vue.esm.js";
2 |
3 | import App from "./App.js";
4 | const rootContainer = document.getElementById('app')
5 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/demo/componentSlots/App.js:
--------------------------------------------------------------------------------
1 | import { h,createTextNode } from "../../lib/cai-vue.esm.js";
2 | import Foo from "./Foo.js";
3 | export default {
4 | render(){
5 | //单值传入
6 | // return h('div',{},[h(Foo,{},h('span',{},'我是传入的slots'))])
7 | //数组传入
8 | // return h('div',{},[h(Foo,{},
9 | // [
10 | // h('span',{},'我是传入的slots1'),
11 | // h('span',{},'我是传入的slots2')
12 | // ])])
13 | // 具名插槽
14 | // return h('div',{},[h(Foo,{},
15 | // {
16 | // header: h('h1',{},'header'),
17 | // footer: h('h1',{},'footer')
18 | // })])
19 | // 作用域
20 | return h('div',{},[h(Foo,{},
21 | {
22 | header: ({age})=>[h('h1',{},'header'+age),createTextNode('我是textnode')],
23 | footer: ()=>h('h1',{},'footer')
24 | })])
25 | },
26 | //状态
27 | setup(){
28 | return {
29 | msg:'03'
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/demo/componentSlots/Foo.js:
--------------------------------------------------------------------------------
1 | import { h,renderSlots } from "../../lib/cai-vue.esm.js";
2 | export default {
3 | render(){
4 | const a = h('div',{},'中心')
5 | // //使用renderSlots 进行slots渲染
6 | // return h('div',{},[
7 | // a,
8 | // renderSlots(this.$slots)
9 | // ])
10 | const age = 11
11 | //指定插槽位置渲染
12 | return h('div',{},[
13 | renderSlots(this.$slots,'header',{
14 | age
15 | }),
16 | a,
17 | renderSlots(this.$slots,'footer')
18 | ])
19 | },
20 | setup(){
21 | }
22 | }
--------------------------------------------------------------------------------
/demo/componentSlots/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/componentSlots/main.js:
--------------------------------------------------------------------------------
1 | import {createApp } from "../../lib/cai-vue.esm.js";
2 |
3 | import App from "./App.js";
4 | const rootContainer = document.getElementById('app')
5 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/demo/currentInstance/App.js:
--------------------------------------------------------------------------------
1 | import { h,getCurrentInstance } from "../../lib/cai-vue.esm.js";
2 | import Foo from "./Foo.js";
3 | export default {
4 | name:'app',
5 | render(){
6 | return h('div',{},[h('div',{},'APP'),h(Foo)])
7 | },
8 | setup(){
9 | console.log("app:",getCurrentInstance())
10 | }
11 | }
--------------------------------------------------------------------------------
/demo/currentInstance/Foo.js:
--------------------------------------------------------------------------------
1 | import { h,getCurrentInstance } from "../../lib/cai-vue.esm.js";
2 | export default {
3 | name:'foo',
4 | render(){
5 | return h('div',{},'FOO')
6 | },
7 | setup(){
8 | console.log("FOO:",getCurrentInstance())
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/currentInstance/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/currentInstance/main.js:
--------------------------------------------------------------------------------
1 | import {createApp } from "../../lib/cai-vue.esm.js";
2 |
3 | import App from "./App.js";
4 | const rootContainer = document.getElementById('app')
5 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/demo/customRenderer/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/cai-vue.esm.js";
2 |
3 | export const App = {
4 | setup() {
5 | return {
6 | x: 100,
7 | y: 100,
8 | };
9 | },
10 | render() {
11 | return h("rect", { x: this.x, y: this.y });
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/demo/customRenderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/demo/customRenderer/main.js:
--------------------------------------------------------------------------------
1 | import { createRenderer } from "../../lib/cai-vue.esm.js";
2 | import { App } from "./App.js";
3 |
4 | const game = new PIXI.Application({
5 | width: 500,
6 | height: 500,
7 | });
8 |
9 | document.body.append(game.view);
10 |
11 | const renderer = createRenderer({
12 | createElement(type) {
13 | if (type === "rect") {
14 | const rect = new PIXI.Graphics();
15 | rect.beginFill(0xff0000);
16 | rect.drawRect(0, 0, 100, 100);
17 | rect.endFill();
18 |
19 | return rect;
20 | }
21 | },
22 | patchProp(el, key, val) {
23 | el[key] = val;
24 | },
25 | insert(el, parent) {
26 | parent.addChild(el);
27 | },
28 | });
29 |
30 | renderer.createApp(App).mount(game.stage);
31 |
--------------------------------------------------------------------------------
/demo/helloworld/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/cai-vue.esm.js";
2 | import Props from "./Props.js";
3 | export default {
4 | //渲染视图
5 | render(){
6 | // return h('div',{},'hello'+this.msg)
7 | window.self = this
8 | // return h('div',{},'console root')
9 | return h('div',{},[h('div',{},'hello'),h('div',
10 | {
11 | onClick(){
12 | console.log('点击了')
13 | },
14 | onMousemove(){
15 | console.log('移动')
16 | }
17 | },'click'),
18 | h(Props,{count:3,age:12})
19 | ])
20 | },
21 | //状态
22 | setup(){
23 | return {
24 | msg:'03'
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/demo/helloworld/Props.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/cai-vue.esm.js";
2 | //两个点
3 | // 1、setup接受props参数
4 | // 2、render中通过this.xxx访问到props
5 | // 3、 props不能修改
6 | export default {
7 | name:'props',
8 | render(){
9 | console.log(this.count)
10 | return h('div',{},'props:'+this.count)
11 | },
12 | //状态
13 | setup(props){
14 | props.count = 1
15 | console.log(props)
16 | }
17 | }
--------------------------------------------------------------------------------
/demo/helloworld/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/helloworld/main.js:
--------------------------------------------------------------------------------
1 | import {createApp } from "../../lib/cai-vue.esm.js";
2 |
3 | import App from "./App.js";
4 | const rootContainer = document.getElementById('app')
5 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/demo/patchChildren/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/cai-vue.esm.js";
2 |
3 | import ArrayToText from "./ArrayToText.js";
4 | import TextToText from "./TextToText.js";
5 | import TextToArray from "./TextToArray.js";
6 | import ArrayToArray from "./ArrayToArray.js";
7 |
8 | export default {
9 | name: "App",
10 | setup() {},
11 |
12 | render() {
13 | return h("div", { tId: 1 }, [
14 | h("p", {}, "主页"),
15 | // 老的是 array 新的是 text
16 | // h(ArrayToText),
17 | // 老的是 text 新的是 text
18 | // h(TextToText),
19 | // 老的是 text 新的是 array
20 | h(TextToArray),
21 | // 老的是 array 新的是 array
22 | // h(ArrayToArray)
23 | ]);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/demo/patchChildren/ArrayToArray.js:
--------------------------------------------------------------------------------
1 | // TODO
2 |
3 | import { ref, h } from "../../lib/cai-vue.esm.js";
4 | export default {
5 | name: "ArrayToArray",
6 | };
7 |
--------------------------------------------------------------------------------
/demo/patchChildren/ArrayToText.js:
--------------------------------------------------------------------------------
1 | // 老的是 array
2 | // 新的是 text
3 |
4 | import { ref, h } from "../../lib/cai-vue.esm.js";
5 | const nextChildren = "newChildren";
6 | const prevChildren = [h("div", {}, "A"), h("div", {}, "B")];
7 |
8 | export default {
9 | name: "ArrayToText",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/demo/patchChildren/TextToArray.js:
--------------------------------------------------------------------------------
1 | // 新的是 array
2 | // 老的是 text
3 | import { ref, h } from "../../lib/cai-vue.esm.js";
4 |
5 | const prevChildren = "oldChild";
6 | const nextChildren = [h("div", {}, "A"), h("div", {}, "B")];
7 |
8 | export default {
9 | name: "TextToArray",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/demo/patchChildren/TextToText.js:
--------------------------------------------------------------------------------
1 | // 新的是 text
2 | // 老的是 text
3 | import { ref, h } from "../../lib/cai-vue.esm.js";
4 |
5 | const prevChildren = "oldChild";
6 | const nextChildren = "newChild";
7 |
8 | export default {
9 | name: "TextToText",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/demo/patchChildren/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/patchChildren/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/cai-vue.esm.js";
2 | import App from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#root");
5 | createApp(App).mount(rootContainer);
6 |
--------------------------------------------------------------------------------
/demo/update/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from "../../lib/cai-vue.esm.js";
2 |
3 | export const App = {
4 | name: "App",
5 |
6 | setup() {
7 | const count = ref(0);
8 |
9 | const onClick = () => {
10 | count.value++;
11 | };
12 |
13 | const props = ref({
14 | foo: "foo",
15 | bar: "bar",
16 | });
17 | const onChangePropsDemo1 = () => {
18 | props.value.foo = "new-foo";
19 | };
20 |
21 | const onChangePropsDemo2 = () => {
22 | props.value.foo = undefined;
23 | };
24 |
25 | const onChangePropsDemo3 = () => {
26 | props.value = {
27 | foo: "foo",
28 | };
29 | };
30 |
31 | return {
32 | count,
33 | onClick,
34 | onChangePropsDemo1,
35 | onChangePropsDemo2,
36 | onChangePropsDemo3,
37 | props,
38 | };
39 | },
40 | render() {
41 | return h(
42 | "div",
43 | {
44 | id: "root",
45 | ...this.props,
46 | },
47 | [
48 | h("div", {}, "count:" + this.count),
49 | h(
50 | "button",
51 | {
52 | onClick: this.onClick,
53 | },
54 | "click"
55 | ),
56 | h(
57 | "button",
58 | {
59 | onClick: this.onChangePropsDemo1,
60 | },
61 | "changeProps - 值改变了 - 修改"
62 | ),
63 |
64 | h(
65 | "button",
66 | {
67 | onClick: this.onChangePropsDemo2,
68 | },
69 | "changeProps - 值变成了 undefined - 删除"
70 | ),
71 |
72 | h(
73 | "button",
74 | {
75 | onClick: this.onChangePropsDemo3,
76 | },
77 | "changeProps - key 在新的里面没有了 - 删除"
78 | ),
79 | ]
80 | );
81 | },
82 | };
83 |
--------------------------------------------------------------------------------
/demo/update/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/demo/update/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/cai-vue.esm.js";
2 | import { App } from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#app");
5 | createApp(App).mount(rootContainer);
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "caib-vue",
3 | "version": "1.0.0",
4 | "description": "一个菜逼的简易vue学习实现笔记",
5 | "main": "lib/cai-vue.cjs.js",
6 | "module": "lib/cai-vue.esm.js",
7 | "scripts": {
8 | "test": "jest",
9 | "build": "rollup -c rollup.config.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/GU0FORY1/CaiB-Vue.git"
14 | },
15 | "author": "GU0FORY1",
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/GU0FORY1/CaiB-Vue/issues"
19 | },
20 | "homepage": "https://github.com/GU0FORY1/CaiB-Vue#readme",
21 | "dependencies": {},
22 | "devDependencies": {
23 | "@babel/core": "^7.15.5",
24 | "@babel/preset-env": "^7.15.4",
25 | "@babel/preset-typescript": "^7.15.0",
26 | "@rollup/plugin-typescript": "^8.2.5",
27 | "@types/jest": "^27.0.1",
28 | "babel-jest": "^27.1.1",
29 | "jest": "^27.1.1",
30 | "rollup": "^2.58.0",
31 | "tslib": "^2.3.1",
32 | "typescript": "^4.4.2"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import ts from "@rollup/plugin-typescript";
2 | import pkg from "./package.json";
3 | export default {
4 | input:'./src/index.ts',
5 | output:[
6 | {
7 | format:'cjs',
8 | file:pkg.main
9 | },
10 | {
11 | format:'es',
12 | file:pkg.module
13 | }
14 | ],
15 | plugins:[ts()]
16 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // mini-vue 出口
2 | export * from "./runtime-dom";
3 | export * from "./reactivity";
4 |
--------------------------------------------------------------------------------
/src/reactivity/baseHandlers.ts:
--------------------------------------------------------------------------------
1 | import { extend, isObject } from "../shared";
2 | import { track, trigger } from "./effect";
3 | import { reactive, ReactiveFlags, readonly } from "./reactive";
4 |
5 | // get函数创造器
6 | function createGetter(isReadonly = false, shallow = false) {
7 | return function get(target, key) {
8 | if (key === ReactiveFlags.IS_REACTIVE) {
9 | return !isReadonly;
10 | } else if (key === ReactiveFlags.IS_READONLY) {
11 | return isReadonly;
12 | }
13 | const res = Reflect.get(target, key);
14 | // 判断是否为表层Readonly
15 | if (shallow) {
16 | return res;
17 | }
18 | // 解决嵌套对象 判断是否为对象 若为对象则包裹
19 | if (isObject(res)) {
20 | return isReadonly ? readonly(res) : reactive(res);
21 | }
22 | if (!isReadonly) {
23 | //依赖收集
24 | track(target, key);
25 | }
26 | return res;
27 | };
28 | }
29 | // set函数创造器
30 | function createSetter() {
31 | return function set(target, key, value) {
32 | const res = Reflect.set(target, key, value);
33 | //执行依赖
34 | trigger(target, key);
35 | return res;
36 | };
37 | }
38 | // 优化性能 没有必要没次都 return一个函数
39 | const get = createGetter();
40 | const set = createSetter();
41 | const readonlyGet = createGetter(true);
42 | const shallowReadonlyGet = createGetter(true, true);
43 |
44 | // reactive的
45 | export const mutablerHsndlers = {
46 | get,
47 | set,
48 | };
49 | // readonly的
50 | export const readonlyHandlers = {
51 | get: readonlyGet,
52 | set(target, key, value) {
53 | console.warn(`readonly不能被set`);
54 | return true;
55 | },
56 | };
57 | // shallowReadonly的
58 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, {
59 | get: shallowReadonlyGet,
60 | });
61 |
--------------------------------------------------------------------------------
/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 | //*有点忘了 巧妙运用ReactiveEffect
11 | //set 时scheduler会调用
12 | this._effect = new ReactiveEffect(getter, () => {
13 | //触发set了 为下一次计算属性做准备
14 | if (!this._dirty) {
15 | this._dirty = true;
16 | }
17 | });
18 | }
19 | get value() {
20 | // 判断是否是需要执行getter
21 | if (this._dirty) {
22 | this._dirty = false;
23 | //缓存一下执行结果
24 | this._value = this._effect.run();
25 | }
26 | return this._value;
27 | }
28 | }
29 | export function computed(getter) {
30 | return new ComputedRefImpl(getter);
31 | }
32 |
--------------------------------------------------------------------------------
/src/reactivity/effect.ts:
--------------------------------------------------------------------------------
1 | import { extend } from "../shared";
2 | let activeEffect;
3 | let shouldTrack;
4 |
5 | // 依赖收集类
6 | export class ReactiveEffect {
7 | private _fn: any;
8 | deps = [];
9 | onStop?: () => void;
10 | isActive = true;
11 | constructor(fn, public scheduler?) {
12 | this._fn = fn;
13 | }
14 | run() {
15 | //runner运行后 要的返回值
16 | // stop 下的状态 不用收集依赖
17 | if (!this.isActive) {
18 | return this._fn();
19 | }
20 | // 保存当前执行的fn
21 | activeEffect = this;
22 | // 设置需要收集标识
23 | shouldTrack = true;
24 | // 执行fn的时候 就会访问响应式数据中的get 从而触发依赖收集
25 | const result = this._fn();
26 | // 重置需要收集标识 以防收集到不需要收集的依赖
27 | shouldTrack = false;
28 | return result;
29 | }
30 | stop() {
31 | // 防止stop多次执行
32 | if (this.isActive) {
33 | if (this.onStop) {
34 | this.onStop();
35 | }
36 | cleanEffect(this);
37 | this.isActive = false;
38 | }
39 | }
40 | }
41 | function cleanEffect(effect) {
42 | effect.deps.forEach((dep: any) => {
43 | dep.delete(effect);
44 | });
45 | // 优化点
46 | effect.deps.length = 0;
47 | }
48 |
49 | // 用来存储不同对象的deps
50 | const targetMap = new Map();
51 |
52 | // 依赖收集
53 | export function track(target, key) {
54 | // 判断是否收集依赖
55 | if (!isTracking()) return;
56 |
57 | // taget => key => dep
58 | // dep存储的是每个key对应的依赖 [fn1,fn2,fn3]
59 | let depsMap = targetMap.get(target);
60 | if (!depsMap) {
61 | depsMap = new Map();
62 | targetMap.set(target, depsMap);
63 | }
64 | let dep = depsMap.get(key);
65 | if (!dep) {
66 | dep = new Set();
67 | depsMap.set(key, dep);
68 | }
69 |
70 | trackEffect(dep);
71 | }
72 | //收集依赖
73 | export function trackEffect(dep) {
74 | // 处理重复依赖
75 | if (dep.has(activeEffect)) return;
76 | dep.add(activeEffect);
77 | // 反向收集 把当前的所有依赖 放入effect当中 比如在使用停止函数的时候进行使用
78 | activeEffect.deps.push(dep);
79 | }
80 | //判断当前能否收集依赖
81 | export function isTracking() {
82 | return shouldTrack && activeEffect !== undefined;
83 | }
84 | // 触发依赖
85 | export function trigger(target, key) {
86 | let despMap = targetMap.get(target);
87 | let dep = despMap.get(key);
88 |
89 | triggerEffects(dep);
90 | }
91 | // 执行依赖
92 | export function triggerEffects(dep) {
93 | // for of 遍历数组 forin遍历对象
94 | for (const effect of dep) {
95 | if (effect.scheduler) {
96 | effect.scheduler();
97 | } else {
98 | effect.run();
99 | }
100 | }
101 | }
102 | //依赖收集
103 | //effct才会把_effect挂载到activeEffect
104 | export function effect(fn, options: any = {}) {
105 | const { scheduler, onStop } = options;
106 | const _effect = new ReactiveEffect(fn, scheduler);
107 | // _effect.onStop = onStop;
108 | //把options全部挂载到_effect上
109 | extend(_effect, options); //语义化重构
110 | // 第一次执行先运行fn
111 | _effect.run();
112 | const runner: any = _effect.run.bind(_effect);
113 | // 把_effct挂载上方便stop事执行_effect中的stop方法
114 | runner.effect = _effect;
115 | // _effect bind里面的this
116 | return runner;
117 | }
118 |
119 | export function stop(runner) {
120 | // 执行RE类中的stop方法
121 | runner.effect.stop();
122 | }
123 |
--------------------------------------------------------------------------------
/src/reactivity/index.ts:
--------------------------------------------------------------------------------
1 | export { ref } from "./ref";
2 |
--------------------------------------------------------------------------------
/src/reactivity/reactive.ts:
--------------------------------------------------------------------------------
1 | import { isObject } from "../shared";
2 | import {
3 | mutablerHsndlers,
4 | readonlyHandlers,
5 | shallowReadonlyHandlers,
6 | } from "./baseHandlers";
7 |
8 | export const enum ReactiveFlags {
9 | IS_REACTIVE = "__v_isReactive",
10 | IS_READONLY = "__v_isReadonly",
11 | }
12 |
13 | export function reactive(raw) {
14 | return createReactiveObject(raw, mutablerHsndlers);
15 | }
16 |
17 | export function readonly(raw) {
18 | return createReactiveObject(raw, readonlyHandlers);
19 | }
20 |
21 | export function shallowReadonly(raw) {
22 | return createReactiveObject(raw, shallowReadonlyHandlers);
23 | }
24 |
25 | export function isReactive(value) {
26 | // return !!value[ReactiveFlags.IS_REACTIVE]; 这种写法实际是 Boolean(xxx) 返回的布尔类型
27 | return !!value[ReactiveFlags.IS_REACTIVE];
28 | }
29 |
30 | export function isReadonly(value) {
31 | return !!value[ReactiveFlags.IS_READONLY];
32 | }
33 |
34 | export function isProxy(value) {
35 | return isReadonly(value) || isReactive(value);
36 | }
37 |
38 | export function createReactiveObject(target, baseHandlers) {
39 | if (!isObject(target)) {
40 | console.warn(`target ${target} 必须是一个对象`);
41 | return target;
42 | }
43 | return new Proxy(target, baseHandlers);
44 | }
45 |
--------------------------------------------------------------------------------
/src/reactivity/ref.ts:
--------------------------------------------------------------------------------
1 | import { hasChanged, isObject } from "../shared";
2 | import { isTracking, trackEffect, triggerEffects } from "./effect";
3 | import { reactive } from "./reactive";
4 |
5 | //为什么?ref都是单值 1 true “1” 怎么监听改变
6 | // 通过对象 {}ref这个类 value 触发get 触发set
7 | class RefImpl {
8 | private _value: any;
9 | public dep;
10 | public __v_isRef = true;
11 | private _rawValue: any;
12 | constructor(value) {
13 | this._rawValue = value;
14 | //判断是否为对象否则赋值reactive
15 | this._value = convert(value);
16 | this.dep = new Set();
17 | }
18 | get value() {
19 | //不能收集依赖则直接返回
20 | //优化
21 | trackRefValue(this);
22 | return this._value;
23 | }
24 | set value(newValue) {
25 | // 如相等直接返回
26 | // 为什么不用=== 与===差异 1、NaN等于自身 2、+0不等于-0
27 | // 如果用=== 新旧值都为NaN时则会触发更新
28 | // 优化
29 | //这里用this._value比较有问题 可能是reactive包裹后的 所以要存储一个原始的
30 | if (hasChanged(this._rawValue, newValue)) {
31 | this._rawValue = newValue;
32 | this._value = convert(newValue);
33 | triggerEffects(this.dep);
34 | }
35 | }
36 | }
37 |
38 | export function ref(value) {
39 | return new RefImpl(value);
40 | }
41 |
42 | function trackRefValue(ref) {
43 | if (isTracking()) {
44 | trackEffect(ref.dep);
45 | }
46 | }
47 |
48 | //转换 优化
49 | function convert(value) {
50 | return isObject(value) ? reactive(value) : value;
51 | }
52 |
53 | export function isRef(ref) {
54 | return !!ref.__v_isRef;
55 | }
56 |
57 | export function unRef(ref) {
58 | return isRef(ref) ? ref.value : ref;
59 | }
60 |
61 | export function proxyRefs(objectWithRefs) {
62 | return new Proxy(objectWithRefs, {
63 | get(target, key) {
64 | //是ref则返回。value 否则返回本身
65 | return unRef(Reflect.get(target, key));
66 | },
67 | set(target, key, value) {
68 | //原来为ref 切新值不为 ref 直接修改
69 | // 其他情况直接替换
70 | if (isRef(target[key]) && !isRef(value)) {
71 | return (target[key].value = value);
72 | } else {
73 | return Reflect.set(target, key, value);
74 | }
75 | },
76 | });
77 | }
78 |
--------------------------------------------------------------------------------
/src/reactivity/tests/computed.spec.ts:
--------------------------------------------------------------------------------
1 | import { computed } from "../computed";
2 | import { reactive } from "../reactive";
3 |
4 | describe("computed", () => {
5 | it("happy path", () => {
6 | const user = reactive({
7 | age: 1,
8 | });
9 | const age = computed(() => {
10 | return user.age;
11 | });
12 | expect(age.value).toBe(1);
13 | });
14 |
15 | it("缓存", () => {
16 | const user = reactive({
17 | age: 1,
18 | });
19 | const getter = jest.fn(() => {
20 | return user.age;
21 | });
22 | const age = computed(getter);
23 | // 没有调用age.value时不调用getter
24 | expect(getter).not.toHaveBeenCalled();
25 |
26 | expect(age.value).toBe(1);
27 | expect(getter).toHaveBeenCalledTimes(1);
28 | // 缓存 只应该调用一次
29 | expect(age.value).toBe(1);
30 | expect(getter).toHaveBeenCalledTimes(1);
31 |
32 | //依赖改变事 没有访问value 先不要计算
33 | user.age = 2;
34 | expect(getter).toHaveBeenCalledTimes(1);
35 | // 修改后第一次访问 现在计算
36 | expect(age.value).toBe(2);
37 | expect(getter).toHaveBeenCalledTimes(2);
38 |
39 | // 二次访问 不应该计算
40 | expect(age.value).toBe(2);
41 | expect(getter).toHaveBeenCalledTimes(2);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/reactivity/tests/effct.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from "../reactive";
2 | import { effect, stop } from "../effect";
3 | describe("effect", () => {
4 | it("happy path", () => {
5 | const user = reactive({
6 | age: 10,
7 | });
8 | let nextAge;
9 | effect(() => {
10 | nextAge = user.age + 1;
11 | });
12 | //init
13 | expect(nextAge).toBe(11);
14 | //update
15 | user.age++;
16 | expect(nextAge).toBe(12);
17 | });
18 |
19 | it("调用effect时返回runner", () => {
20 | // 调用effect返回runner runner执行 执行传入的fn 并且返回fn的返回值
21 | let foo = 10;
22 | const runner = effect(() => {
23 | foo++;
24 | });
25 | //effect 第一次执行
26 | expect(foo).toBe(11);
27 | //返回的runner运行 返回fn的值
28 | runner();
29 | expect(foo).toBe(12);
30 | });
31 |
32 | it("scheduler的实现 ", () => {
33 | /**
34 | * effect接受scheduler这个参数
35 | * 第一次scheduler不执行 执行fn
36 | * set update时 scheduler执行
37 | * runner时还是执行fn
38 | */
39 | let sum;
40 | let run;
41 | const scheduler = jest.fn(() => {
42 | run = runner;
43 | });
44 | const obj = reactive({ count: 1 });
45 | const runner = effect(
46 | () => {
47 | sum = obj.count;
48 | },
49 | { scheduler }
50 | );
51 | expect(scheduler).not.toHaveBeenCalled();
52 | expect(sum).toBe(1);
53 | obj.count++;
54 | expect(scheduler).toHaveBeenCalledTimes(1);
55 | run();
56 | expect(sum).toBe(2);
57 | });
58 |
59 | it("stop的实现", () => {
60 | /**
61 | * 执行stop时不去执行effect里的函数
62 | */
63 | let obj = reactive({
64 | count: 0,
65 | });
66 | let sum;
67 | const runner = effect(() => {
68 | sum = obj.count;
69 | });
70 | obj.count = 1;
71 | expect(sum).toBe(1);
72 |
73 | // 不然里面执行 sum应该没变
74 | stop(runner);
75 | // obj.count = 2;
76 | obj.count++;
77 | expect(sum).toBe(1);
78 |
79 | runner();
80 | expect(sum).toBe(2);
81 | });
82 |
83 | it("onStop的实现", () => {
84 | /**
85 | * 执行stop时不去执行effect里的函数
86 | * 但是执行onStop
87 | */
88 | let obj = reactive({
89 | count: 0,
90 | });
91 | let sum;
92 | const onStop = jest.fn(() => {
93 | console.log("onsetp执行");
94 | });
95 | const runner = effect(
96 | () => {
97 | sum = obj.count;
98 | },
99 | {
100 | onStop,
101 | }
102 | );
103 | stop(runner);
104 | obj.count = 1;
105 | expect(sum).toBe(0);
106 | //执行一次onStop方法
107 | expect(onStop).toHaveBeenCalledTimes(1);
108 | });
109 | });
110 |
--------------------------------------------------------------------------------
/src/reactivity/tests/reactive.spec.ts:
--------------------------------------------------------------------------------
1 | import { isProxy, isReactive, reactive } from "../reactive";
2 | describe("reactive", () => {
3 | it("happy path", () => {
4 | const original = { age: 10 };
5 | const observed = reactive(original);
6 | expect(observed).not.toBe(original);
7 | expect(observed.age).toBe(10);
8 | expect(isReactive(observed)).toBe(true);
9 | expect(isReactive(original)).toBe(false);
10 | expect(isProxy(observed)).toBe(true);
11 | });
12 |
13 | it("reactive 嵌套对象 ", () => {
14 | let raw = {
15 | foo: { a: 1 },
16 | arr: [{ age: 1 }],
17 | };
18 | let obj = reactive(raw);
19 | expect(isReactive(obj.foo)).toBe(true);
20 | expect(isReactive(obj.arr)).toBe(true);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/reactivity/tests/readonly.spec.ts:
--------------------------------------------------------------------------------
1 | import { isProxy, isReadonly, readonly } from "../reactive";
2 |
3 | describe("readonly", () => {
4 | it("happy path ", () => {
5 | //set不能执行
6 | let raw = { count: 1 };
7 | let obj = readonly(raw);
8 | obj.count = 2;
9 | expect(obj).not.toBe(raw);
10 | expect(obj.count).toBe(1);
11 | expect(isReadonly(obj)).toBe(true);
12 | expect(isReadonly(raw)).toBe(false);
13 | expect(isProxy(obj)).toBe(true);
14 | });
15 |
16 | it("set时报错", () => {
17 | // 方便查看该方法有没有被触发
18 | console.warn = jest.fn();
19 | let raw = { count: 1 };
20 | let obj = readonly(raw);
21 | obj.count = 2;
22 | expect(console.warn).toBeCalled();
23 | });
24 |
25 | it("readonly 嵌套对象 ", () => {
26 | let raw = {
27 | foo: { a: 1 },
28 | arr: [{ age: 1 }],
29 | };
30 | let obj = readonly(raw);
31 | expect(isReadonly(obj.foo)).toBe(true);
32 | expect(isReadonly(obj.arr)).toBe(true);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/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 |
5 | describe("ref", () => {
6 | it("happy path", () => {
7 | let a = ref(1);
8 | expect(a.value).toBe(1);
9 | });
10 | it("ref effect", () => {
11 | let a = ref(1);
12 | let b = 0;
13 | let c = 0;
14 | effect(() => {
15 | c++;
16 | //get时收集依赖
17 | b = a.value;
18 | });
19 | expect(c).toBe(1);
20 | expect(b).toBe(1);
21 | //set时去触发依赖
22 | a.value = 2;
23 | expect(c).toBe(2);
24 | expect(b).toBe(2);
25 | //值未改变时不执行effect
26 | a.value = 2;
27 | expect(c).toBe(2);
28 | expect(b).toBe(2);
29 | });
30 |
31 | it("ref传入object时的处理", () => {
32 | const a = ref({
33 | age: 1,
34 | });
35 | let tmp;
36 | effect(() => {
37 | tmp = a.value.age;
38 | });
39 | expect(tmp).toBe(1);
40 | a.value.age = 12;
41 | expect(tmp).toBe(12);
42 | });
43 |
44 | it("isRef", () => {
45 | const a = ref(1);
46 | const obj = reactive({ a: 1 });
47 | expect(isRef(a)).toBe(true);
48 | expect(isRef(1)).toBe(false);
49 | expect(isRef(obj)).toBe(false);
50 | });
51 |
52 | // 返回ref.value
53 | it("unRef", () => {
54 | const a = ref(1);
55 | const obj = reactive({ a: 1 });
56 | expect(unRef(a)).toBe(1);
57 | expect(unRef(1)).toBe(1);
58 | });
59 |
60 | //template中用到 直接访问ref
61 | it("proxyRefs ref省略.value访问", () => {
62 | const user = {
63 | age: ref(10),
64 | name: "tom",
65 | };
66 | const proxyUser = proxyRefs(user);
67 | expect(user.age.value).toBe(10);
68 | //get ref? ref.value: ref
69 | expect(proxyUser.age).toBe(10);
70 | expect(proxyUser.name).toBe("tom");
71 |
72 | //set 普通值
73 | proxyUser.age = 10;
74 | expect(proxyUser.age).toBe(10);
75 | expect(user.age.value).toBe(10);
76 | //set ref
77 | proxyUser.age = ref(20);
78 | expect(proxyUser.age).toBe(20);
79 | expect(user.age.value).toBe(20);
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/src/reactivity/tests/shallowReadonly.test.ts:
--------------------------------------------------------------------------------
1 | import { isReadonly, shallowReadonly } from "../reactive";
2 |
3 | // 表层readonly的实现 只是最外面一层为reaconly 里面对象为普通
4 | describe("shallowReadonly", () => {
5 | it("shallowReadonly 实现 ", () => {
6 | let raw = {
7 | a: 1,
8 | b: { age: 20 },
9 | };
10 | let obj = shallowReadonly(raw);
11 | expect(isReadonly(obj)).toBe(true);
12 | expect(isReadonly(obj.b)).toBe(false);
13 | });
14 | it("set时报错", () => {
15 | // 方便查看该方法有没有被触发
16 | console.warn = jest.fn();
17 | let raw = { count: 1 };
18 | let obj = shallowReadonly(raw);
19 | obj.count = 2;
20 | expect(console.warn).toBeCalled();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/runtime-core/apiInject.ts:
--------------------------------------------------------------------------------
1 | import { getCurrentInstance } from "./component";
2 |
3 | export function provide(key, value) {
4 | const currentInstance: any = getCurrentInstance();
5 | //可以存在组件的实例上
6 |
7 | if (currentInstance) {
8 | let { provides } = currentInstance;
9 | const parentProvides = currentInstance.parent?.provides;
10 | //为每个组件都建立自己的provides 使用原型链的方式向上查找 指定__proto__为父级provides
11 |
12 | if (provides === parentProvides) {
13 | provides = currentInstance.provides = Object.create(parentProvides);
14 | }
15 | //在当前组件的provides上添加数据
16 |
17 | provides[key] = value;
18 | }
19 | }
20 |
21 | export function inject(key, defaultValue) {
22 | const currentInstance: any = getCurrentInstance();
23 |
24 | if (currentInstance) {
25 | const parentProvides = currentInstance.parent.provides;
26 |
27 | if (key in parentProvides) {
28 | return parentProvides[key];
29 | } else if (defaultValue) {
30 | //处理默认值时
31 | if (typeof defaultValue === "function") {
32 | return defaultValue();
33 | }
34 | return defaultValue;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/runtime-core/component.ts:
--------------------------------------------------------------------------------
1 | import { shallowReadonly } from "../reactivity/reactive";
2 | import { proxyRefs } from "../reactivity/ref";
3 | import { emit } from "./componentEmit";
4 | import { initProps } from "./componentProps";
5 | import { PublicInstanceProxyHandlers } from "./componentPublicInstance";
6 | import { initSlots } from "./componentSlots";
7 | let currentInstance = null;
8 | //初始化一个组件实例
9 | export function createComponentInstance(vnode, parent) {
10 | const component = {
11 | vnode,
12 | type: vnode.type,
13 | setupState: {},
14 | props: {},
15 | //初始化provides
16 | provides: parent ? parent.provides : {},
17 | parent,
18 | slots: {},
19 | isMounted: false,
20 | subTree: {},
21 | emit: () => {},
22 | };
23 | //方便用户使用不用传入instance
24 | component.emit = emit.bind(null, component) as any;
25 |
26 | return component;
27 | }
28 |
29 | export function setupComponent(instance) {
30 | initProps(instance, instance.vnode.props);
31 | initSlots(instance, instance.vnode.children);
32 | setupStatefulComponent(instance);
33 | }
34 |
35 | //创建一个有状态的组件
36 | export function setupStatefulComponent(instance) {
37 | const Component = instance.type;
38 | //实现组件代理 this.$el...
39 | //代理从setupState中取值
40 | //优化重构
41 | instance.proxy = new Proxy({ instance }, PublicInstanceProxyHandlers);
42 |
43 | const { setup } = Component;
44 | if (setup) {
45 | //getCurrentInsance 必须在setup里才能用
46 | setCurrentInstance(instance);
47 | //props给到setup
48 | // props不可修改 shallowReadonly
49 | const setupResult = setup(shallowReadonly(instance.props), {
50 | emit: instance.emit,
51 | });
52 | setCurrentInstance(null);
53 | //把执行结果挂载到实例
54 | handleSetupResult(instance, setupResult);
55 | }
56 | }
57 |
58 | function handleSetupResult(instance, setupResult) {
59 | // function Object
60 | // TODO function
61 | if (typeof setupResult === "object") {
62 | //自动转化.value
63 | instance.setupState = proxyRefs(setupResult);
64 | }
65 | finishComponentSetup(instance);
66 | }
67 |
68 | //把render挂载到实例
69 | function finishComponentSetup(instance: any) {
70 | const Component = instance.type;
71 | if (Component.render) {
72 | instance.render = Component.render;
73 | }
74 | }
75 |
76 | export function getCurrentInstance() {
77 | return currentInstance;
78 | }
79 |
80 | function setCurrentInstance(instance) {
81 | currentInstance = instance;
82 | }
83 |
--------------------------------------------------------------------------------
/src/runtime-core/componentEmit.ts:
--------------------------------------------------------------------------------
1 | //event为用户传入 ...args 为附加参数
2 | //把event转换为onEvent 在props中寻找
3 |
4 | import { capitalize } from "../shared";
5 |
6 | //偷个懒 这里只处理了add->onAdd 没有处理add-foo->onAddFoo
7 | export const emit = (instance, event, ...args) => {
8 | const { props } = instance;
9 |
10 | const handler = props["on" + capitalize(event)];
11 | handler && handler(...args);
12 | };
13 |
--------------------------------------------------------------------------------
/src/runtime-core/componentProps.ts:
--------------------------------------------------------------------------------
1 | //初始化话props 把vnode.props挂载到实例
2 | export function initProps(instance, rawProps) {
3 | instance.props = rawProps || {}; //{}默认值
4 | }
5 |
--------------------------------------------------------------------------------
/src/runtime-core/componentPublicInstance.ts:
--------------------------------------------------------------------------------
1 | import { hsaOwn } from "../shared";
2 |
3 | const publicPropertiesMap = {
4 | $el: (instance) => instance.vnode.el,
5 | $slots: (instance) => instance.slots,
6 | };
7 |
8 | export const PublicInstanceProxyHandlers = {
9 | get({ instance }, key) {
10 | //判断setupState中有么
11 | const { setupState, props } = instance;
12 |
13 | if (hsaOwn(setupState, key)) {
14 | return setupState[key];
15 | } else if (hsaOwn(props, key)) {
16 | return props[key];
17 | }
18 | const publicGetter = publicPropertiesMap[key];
19 | if (publicGetter) {
20 | return publicGetter(instance);
21 | }
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/src/runtime-core/componentSlots.ts:
--------------------------------------------------------------------------------
1 | import { ShapeFlags } from "../shared";
2 |
3 | export function initSlots(instance, children) {
4 | //把单个的转为都转为数组进行处理 下面都进行了
5 | // instance.slots = Array.isArray(children) ? children : [children];
6 | const { vnode } = instance;
7 | //是slot的才执行
8 | if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
9 | //转换slots为对象 使用统一的数据结构
10 | normalizeObjectSlots(children, instance.slots);
11 | }
12 | }
13 | // 规范化对象插槽
14 | function normalizeObjectSlots(children, slots) {
15 | //for in 数组 对象都可遍历
16 | for (const key in children) {
17 | const value = children[key];
18 | //如果指定名称插槽存在 则判断他们是否符合条件
19 | if (value) {
20 | // 这里有点绕
21 | slots[key] = (props) => normalizeSlotValue(value(props));
22 | }
23 | }
24 | }
25 | // 规范化插槽返回值
26 | function normalizeSlotValue(value) {
27 | return Array.isArray(value) ? value : [value];
28 | }
29 |
--------------------------------------------------------------------------------
/src/runtime-core/createApp.ts:
--------------------------------------------------------------------------------
1 | import { createVNode } from "./vnode";
2 | //为了传入render
3 | export function createAppApi(render) {
4 | return function createApp(rootComponent) {
5 | return {
6 | mount(rootContainer) {
7 | // 先转换成vnode
8 | const vnode = createVNode(rootComponent);
9 | //然后渲染
10 | render(vnode, rootContainer);
11 | },
12 | };
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/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, Fragment } from "../vnode";
2 |
3 | export function renderSlots(slots, name, props) {
4 | const slot = slots[name];
5 | if (slot) {
6 | if (typeof slot === "function") {
7 | //实现数组渲染 把数组转换为一个虚拟节点 chilren支持传入数组
8 | return createVNode(Fragment, {}, slot(props));
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/runtime-core/index.ts:
--------------------------------------------------------------------------------
1 | export { h } from "./h";
2 | export { renderSlots } from "./helpers/renderSlots";
3 | export { createTextNode } from "./vnode";
4 | export { getCurrentInstance } from "./component";
5 | export { provide, inject } from "./apiInject";
6 | export { createRenderer } from "./renderer";
7 |
--------------------------------------------------------------------------------
/src/runtime-core/renderer.ts:
--------------------------------------------------------------------------------
1 | import { effect } from "../reactivity/effect";
2 | import { isObject, ShapeFlags } from "../shared";
3 | import { createComponentInstance, setupComponent } from "./component";
4 | import { createAppApi } from "./createApp";
5 | import { Fragment, Text } from "./vnode";
6 | //为了接受用户传递的自定义函数
7 | export function createRenderer(options) {
8 | //传入的操作方法
9 | const { createElement, patchProp, insert, remove, setElementText } = options;
10 |
11 | function render(vnode, container) {
12 | //初始化n1为null
13 | patch(null, vnode, container, null);
14 | }
15 |
16 | function patch(n1, n2, container, parentComponent) {
17 | // 判断是element还是component 进行区分处理
18 | const { shapeFlag, type } = n2;
19 |
20 | switch (type) {
21 | case Fragment:
22 | processFragment(n1, n2, container, parentComponent);
23 | break;
24 | case Text:
25 | processText(n1, n2, container);
26 | break;
27 | default:
28 | if (shapeFlag & ShapeFlags.ELEMENT) {
29 | processElement(n1, n2, container, parentComponent);
30 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
31 | processComponent(n1, n2, container, parentComponent);
32 | }
33 | break;
34 | }
35 | }
36 |
37 | //处理组件
38 | function processComponent(n1, n2, container, parentComponent) {
39 | mountComponent(n2, container, parentComponent);
40 | }
41 |
42 | //挂载组件
43 | function mountComponent(initVNode, container, parentComponent) {
44 | const instance = createComponentInstance(initVNode, parentComponent);
45 | setupComponent(instance);
46 | setupRenderEffect(instance, initVNode, container);
47 | }
48 | //执行render 进行patch
49 | function setupRenderEffect(instance, initVNode, container) {
50 | //触发render里的响应式对象
51 | effect(() => {
52 | if (!instance.isMounted) {
53 | console.log("init");
54 | //render返回虚拟节点树
55 | //上一步时已将render挂载到实例上
56 | const { proxy } = instance;
57 | //或得proxy对象 更改this指向取到state
58 | const subTree = instance.render.call(proxy);
59 | //挂载树 为后面diff准备
60 | instance.subTree = subTree;
61 |
62 | //渲染子树 所以当前ins就为父节点
63 | patch(null, subTree, container, instance);
64 | //mount完毕挂到el
65 | initVNode.el = subTree.el;
66 |
67 | instance.isMounted = true;
68 | } else {
69 | const { proxy } = instance;
70 | //执行render获取到新树
71 | const subTree = instance.render.call(proxy);
72 | const prevSubTree = instance.subTree;
73 | patch(prevSubTree, subTree, container, instance);
74 | }
75 | });
76 | }
77 | //处理element
78 | function processElement(n1, n2, container, parentComponent) {
79 | if (!n1) {
80 | mountELement(n2, container, parentComponent);
81 | } else {
82 | patchElement(n1, n2, container, parentComponent);
83 | }
84 | }
85 |
86 | function patchElement(n1, n2, container, parentComponent) {
87 | console.log("patchElement");
88 | console.log("n1", n1);
89 | console.log("n2", n2);
90 |
91 | const oldProps = n1.props || {};
92 | const nextProps = n2.props || {};
93 | const el = (n2.el = n1.el);
94 | patchChildren(n1, n2, el, parentComponent);
95 | patchProps(el, oldProps, nextProps);
96 | }
97 |
98 | //处理子节点
99 | function patchChildren(n1, n2, container, parentComponent) {
100 | const prevShapeFlag = n1.shapeFlag;
101 | const shapeFlag = n2.shapeFlag;
102 | const c1 = n1.children;
103 | const c2 = n2.children;
104 |
105 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
106 | //new text
107 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
108 | //old array
109 | //把原来的清空
110 | unmountChildren(c1);
111 | }
112 | //old array 和 newtext 不相等触发 oldtext和newtext不相等
113 | if (c1 !== c2) {
114 | setElementText(container, c2);
115 | }
116 | } else {
117 | //new array
118 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
119 | //old array diff算法进行实现
120 | } else {
121 | //old text
122 | setElementText(container, "");
123 | mountChildren(c2, container, parentComponent);
124 | }
125 | }
126 | }
127 | function unmountChildren(childrens) {
128 | for (let i = 0; i < childrens.length; i++) {
129 | const children = childrens[i];
130 | const { el } = children;
131 | remove(el);
132 | }
133 | }
134 | //处理props
135 | function patchProps(el, oldProps, nextProps) {
136 | /**
137 | * 1、值改变
138 | * 2、值无效 undefined null
139 | * 3、值删除
140 | */
141 | //遍历新的看看值是否改变
142 | if (oldProps !== nextProps) {
143 | for (const key in nextProps) {
144 | const oldProp = oldProps[key];
145 | const nextProp = nextProps[key];
146 | if (oldProp !== nextProps) {
147 | //处理
148 | patchProp(el, key, oldProp, nextProp);
149 | }
150 | }
151 | //判断新的不是空对象
152 | if (Object.keys(nextProps)) {
153 | //遍历老的看看是否删除
154 | for (const key in oldProps) {
155 | //删除的话则remove
156 | if (!(key in nextProps)) {
157 | patchProp(el, key, oldProps[key], null);
158 | }
159 | }
160 | }
161 | }
162 | }
163 |
164 | //u挂载元素
165 | function mountELement(vnode, container, parentComponent) {
166 | //这里的vnode是element类型的 要取到el需要挂载到component类型上去
167 | //tag
168 | //createElement 替代
169 | const el = createElement(vnode.type);
170 | vnode.el = el;
171 |
172 | //props
173 | const { props, children } = vnode;
174 | //遍历props赋值
175 | for (const key in props) {
176 | const value = props[key];
177 | //patchProp替代
178 | patchProp(el, key, null, value);
179 | }
180 |
181 | //children
182 | // 1、string 2、array
183 | const { shapeFlag } = vnode;
184 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
185 | el.innerHTML = children;
186 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
187 | //挂载到当前元素里
188 | mountChildren(children, el, parentComponent);
189 | }
190 |
191 | //添加至容器
192 | //添加元素 insert
193 | insert(el, container);
194 | }
195 | function mountChildren(children, container, parentComponent) {
196 | children.forEach((v) => {
197 | //初始化 则null
198 | patch(null, v, container, parentComponent);
199 | });
200 | }
201 |
202 | function processFragment(n1, n2, container, parentComponent) {
203 | //直接渲染子节点
204 | mountChildren(n2.children, container, parentComponent);
205 | }
206 | function processText(n1, n2: any, container: any) {
207 | const { children } = n2;
208 | const textNode = (n2.el = document.createTextNode(children));
209 | container.append(textNode);
210 | }
211 |
212 | return {
213 | createApp: createAppApi(render),
214 | };
215 | }
216 |
--------------------------------------------------------------------------------
/src/runtime-core/vnode.ts:
--------------------------------------------------------------------------------
1 | import { isObject, ShapeFlags } from "../shared/";
2 |
3 | export const Fragment = Symbol("Fragment");
4 | export const Text = Symbol("Text");
5 |
6 | export function createVNode(type, props?, children?) {
7 | const vnode = {
8 | type,
9 | props,
10 | children,
11 | shapeFlag: getShapeFlags(type),
12 | el: null,
13 | };
14 |
15 | if (typeof children === "string") {
16 | vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN;
17 | } else if (Array.isArray(children)) {
18 | vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.ARRAY_CHILDREN;
19 | }
20 |
21 | // 标识slot
22 | // 组件 + children(object)
23 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
24 | if (isObject(children)) {
25 | vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.SLOTS_CHILDREN;
26 | }
27 | }
28 |
29 | return vnode;
30 | }
31 | function getShapeFlags(type) {
32 | return typeof type === "string"
33 | ? ShapeFlags.ELEMENT
34 | : ShapeFlags.STATEFUL_COMPONENT;
35 | }
36 | export function createTextNode(text) {
37 | return createVNode(Text, {}, text);
38 | }
39 |
--------------------------------------------------------------------------------
/src/runtime-dom/index.ts:
--------------------------------------------------------------------------------
1 | //custom renderer的实现
2 | //custrender核心就是把原来创建元素、设置元素属性、添加元素的方法抽离出来
3 | import { createRenderer } from "../runtime-core";
4 |
5 | function createElement(type) {
6 | return document.createElement(type);
7 | }
8 | function patchProp(el, key, oldValue, nextValue) {
9 | //是否符合onXxx
10 | const isOn = (key) => /^on[A-Z]/.test(key);
11 | //处理注册事件
12 | if (isOn(key)) {
13 | const event = key.slice(2).toLocaleLowerCase();
14 | el.addEventListener(event, nextValue);
15 | } else {
16 | if (nextValue === undefined || nextValue === null) {
17 | el.removeAttribute(key);
18 | } else {
19 | el.setAttribute(key, nextValue);
20 | }
21 | }
22 | }
23 | function insert(el, container) {
24 | container.append(el);
25 | }
26 |
27 | function remove(child) {
28 | //把父级里的子节点都删除
29 | const parent = child.parentNode;
30 | if (parent) {
31 | parent.removeChild(child);
32 | }
33 | }
34 |
35 | function setElementText(container, text) {
36 | container.textContent = text;
37 | }
38 |
39 | //把处理方法注入 返回{crateApp}
40 | const renderer: any = createRenderer({
41 | createElement,
42 | patchProp,
43 | insert,
44 | remove,
45 | setElementText,
46 | });
47 |
48 | //包装一层方便用户调用
49 | export function createApp(...args) {
50 | //实际调用createAppApi方法返回的方法 用于创建初始化
51 | return renderer.createApp(...args);
52 | }
53 |
54 | export * from "../runtime-core";
55 |
--------------------------------------------------------------------------------
/src/shared/ShapeFlags.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * shapeFlags 实现
3 | * 巧妙的通过位运算实现标识的查找和赋值
4 | * eg:
5 | * element 0001
6 | * component 0010
7 | * text_children 0100
8 | * array_children 1000
9 | *
10 | * 查找时用 &
11 | * element & element > 0001 v
12 | * component & element > 0000 x
13 | *
14 | * 赋值使用 |
15 | * shape = 0001 //标识为elemnt
16 | * shape = shape | text_children shape 0101 //标识即为element又是textchildren
17 | *
18 | *
19 | * 这种实现可读性不如直接用对象实现 但是性能很高
20 | */
21 | export enum ShapeFlags {
22 | ELEMENT = 1, //0001
23 | STATEFUL_COMPONENT = 1 << 1, //左位移一位 0010
24 | TEXT_CHILDREN = 1 << 2, //0100
25 | ARRAY_CHILDREN = 1 << 3, // 1000
26 | SLOTS_CHILDREN = 1 << 4, // 10000
27 | }
28 |
--------------------------------------------------------------------------------
/src/shared/index.ts:
--------------------------------------------------------------------------------
1 | export { ShapeFlags } from "./ShapeFlags";
2 |
3 | export const extend = Object.assign;
4 |
5 | export const isObject = (val) => {
6 | return val !== null && typeof val === "object";
7 | };
8 |
9 | export const hasChanged = (value, newValue) => {
10 | return !Object.is(value, newValue);
11 | };
12 |
13 | export const hsaOwn = (target, key) => Reflect.has(target, key);
14 |
15 | export const capitalize = (str: string) => {
16 | //首字母大写
17 | return str ? str.charAt(0).toLocaleUpperCase() + str.slice(1) : "";
18 | };
19 |
--------------------------------------------------------------------------------
/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": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | "lib": ["DOM","ES6"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 |
26 | /* Modules */
27 | "module": "esnext", /* Specify what module code is generated. */
28 | // "rootDir": "./", /* Specify the root folder within your source files. */
29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
34 | "types": ["jest"], /* Specify type package names to be included without being referenced in a source file. */
35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
36 | // "resolveJsonModule": true, /* Enable importing .json files */
37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
38 |
39 | /* JavaScript Support */
40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
43 |
44 | /* Emit */
45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
49 | // "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. */
50 | // "outDir": "./", /* Specify an output folder for all emitted files. */
51 | // "removeComments": true, /* Disable emitting comments. */
52 | // "noEmit": true, /* Disable emitting files from a compilation. */
53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
61 | // "newLine": "crlf", /* Set the newline character for emitting files. */
62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
67 |
68 | /* Interop Constraints */
69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
71 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
73 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
74 |
75 | /* Type Checking */
76 | "strict": true, /* Enable all strict type-checking options. */
77 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
78 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
79 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
80 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
81 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
82 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
83 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
84 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
85 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
86 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
87 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
88 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
89 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
90 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
91 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
92 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
95 |
96 | /* Completeness */
97 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
98 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
99 | }
100 | }
101 |
--------------------------------------------------------------------------------