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