├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── README.md ├── babel.config.js ├── example ├── apiInject │ ├── App.js │ └── index.html ├── componentEmit │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── componentSlot │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── componentUpdate │ ├── App.js │ ├── Child.js │ ├── index.html │ └── main.js ├── currentInstance │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── customRenderer │ ├── App.js │ ├── index.html │ └── main.js ├── helloworld │ ├── App.js │ ├── Foo.js │ ├── index.html │ └── main.js ├── nextTick │ ├── App.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 ├── lib ├── guide-mini-vue.cjs.js └── guide-mini-vue.esm.js ├── package.json ├── rollup.config.js ├── src ├── compiler-core │ ├── src │ │ ├── ast.ts │ │ ├── codegen.ts │ │ ├── parse.ts │ │ └── transform.ts │ └── tests │ │ ├── __snapshots__ │ │ └── codegen.spec.ts.snap │ │ ├── codegen.spec.ts │ │ ├── parse.spec.ts │ │ └── transform.spec.ts ├── index.ts ├── reactivity │ ├── baseHandlers.ts │ ├── computed.ts │ ├── effect.ts │ ├── index.ts │ ├── reactive.ts │ ├── ref.ts │ └── tests │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ └── shallowReadonly.spec.ts ├── runtime-core │ ├── apiInject.ts │ ├── component.ts │ ├── componentEmit.ts │ ├── componentProps.ts │ ├── componentPublicInstance.ts │ ├── componentSlots.ts │ ├── componentUpdateUtils.ts │ ├── createApp.ts │ ├── h.ts │ ├── helpers │ │ └── renderSlots.ts │ ├── index.ts │ ├── renderer.ts │ ├── scheduler.ts │ └── vnode.ts ├── runtime-dom │ └── index.ts └── shared │ ├── index.ts │ └── shapeFlags.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies npm包文件 2 | /node_modules 3 | 4 | # production 打包文件 5 | /dist -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "args": ["--runInBand", "--watchAll=false"], 12 | "cwd": "${workspaceFolder}", 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "program": "${workspaceFolder}/node_modules/.bin/jest", 17 | "windows": { 18 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 19 | } 20 | }, 21 | { 22 | "name": "Debug Jest Tests", 23 | "type": "node", 24 | "request": "launch", 25 | "runtimeArgs": [ 26 | "--inspect-brk", 27 | "${workspaceRoot}/node_modules/jest/bin/jest.js", 28 | "--runInBand" 29 | ], 30 | //"sourceMaps": true, 31 | "trace": true, 32 | "console": "integratedTerminal", 33 | "internalConsoleOptions": "neverOpen", 34 | "port": 9229 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5503, 3 | "cSpell.words": [ 4 | "vnode" 5 | ], 6 | "editor.fontSize": 16, 7 | "editor.lineHeight": 24 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **guide-mini-vue** 2 | === 3 | **vue3**的简单实现,学习参考 **mini-vue** 4 | 5 | 目前已实现功能: 6 | 7 | + [x] effect、reactive、依赖收集、依赖触发 8 | + [x] effect 的 scheduler 功能 9 | + [x] readonly 功能 10 | + [x] isReactive、isReadonly 工具函数 11 | + [x] stop 功能 12 | + [x] reactive、readonly 的对象嵌套 13 | + [x] shallowReadonly 功能 14 | + [x] isProxy 工具函数 15 | + [x] ref 功能 16 | + [x] isRef、unRef 工具函数 17 | + [x] 实现 proxyRefs 功能 18 | + [x] 实现 computed 计算属性 19 | + [x] 实现初始化 component 主流程 20 | + [x] 实现组件代理对象 21 | + [x] 实现 shapeFlags 22 | + [x] 实现注册事件功能 23 | + [x] 实现组件 props 功能 24 | + [x] 实现组件 emit 功能 25 | + [x] 实现组件 slots 功能 26 | + [x] 实现 Fragment和Text类型 节点功能 27 | + [x] 实现 getCurrentInstance 28 | + [x] 实现 provide & inject 功能 29 | + [x] 实现 自定义渲染器 功能 30 | + [x] 更新 element 流程搭建 31 | + [x] 更新 element 的 props 32 | + [x] 更新 element 的 children 33 | + [x] 实现 双端对比 diff 算法 34 | + [x] 实现 组件更新功能 35 | + [x] 实现 nextTick 功能 36 | + [x] 实现 解析插值 功能 37 | + [x] 实现 解析element 功能 38 | + [x] 实现 解析text 功能 39 | + [x] 实现 解析三种联合类型 功能 40 | + [x] 实现 transform 功能 41 | + [x] 实现 代码生成 string 类型 42 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { 4 | targets: { 5 | node: 'current' 6 | } 7 | }], 8 | '@babel/preset-typescript', 9 | ], 10 | 11 | }; -------------------------------------------------------------------------------- /example/apiInject/App.js: -------------------------------------------------------------------------------- 1 | import {h,provide,inject} from '../../lib/guide-mini-vue.esm.js'; 2 | 3 | const Provider = { 4 | name:"Provider", 5 | setup(){ 6 | provide("foo","fooVal"); 7 | provide("bar","barVal"); 8 | }, 9 | render(){ 10 | return h("div", {}, [h("p", {}, "Provider"), h(ProviderTwo)]) 11 | } 12 | } 13 | const ProviderTwo = { 14 | name: "ProviderTwo", 15 | setup() { 16 | provide("foo",'fooTwo') 17 | const foo = inject("foo"); 18 | return { 19 | foo 20 | } 21 | }, 22 | render() { 23 | return h("div", {}, [h("p", {}, `ProviderTwo foo ${this.foo}`), h(Consumer)]) 24 | } 25 | } 26 | const Consumer = { 27 | name: "Consumer", 28 | setup(){ 29 | const foo = inject("foo"); 30 | const bar = inject("bar"); 31 | const baz = inject("baz",()=>'bazDefault'); 32 | return { 33 | foo, 34 | bar, 35 | baz 36 | } 37 | }, 38 | render(){ 39 | return h("div", {}, `Consumer:-${this.foo} - ${this.bar}- ${this.baz}`) 40 | } 41 | } 42 | export default { 43 | name:'App', 44 | render(){ 45 | return h("div", {}, [h("p", {}, "apiInject"), h(Provider)]) 46 | }, 47 | setup(){ 48 | 49 | } 50 | } -------------------------------------------------------------------------------- /example/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 | 22 | 23 | -------------------------------------------------------------------------------- /example/componentEmit/App.js: -------------------------------------------------------------------------------- 1 | import {h} from '../../lib/guide-mini-vue.esm.js'; 2 | import {Foo} from './Foo.js' 3 | export const App = { 4 | name: 'App', 5 | render(){ 6 | return h("div",{},[h("div",{},"App"),h(Foo,{ 7 | onAdd(a,b){ 8 | console.log('onAdd',a,b); 9 | }, 10 | onBarFoo(c,d){ 11 | console.log('onBarFoo', c, d); 12 | } 13 | })]) 14 | }, 15 | setup(){ 16 | return { 17 | 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /example/componentEmit/Foo.js: -------------------------------------------------------------------------------- 1 | import {h} from '../../lib/guide-mini-vue.esm.js'; 2 | export const Foo = { 3 | setup(props,{emit}){ 4 | const emitAdd = ()=>{ 5 | console.log("emit add") 6 | emit("add",1,2) 7 | emit("bar-foo",3,4) 8 | } 9 | return {emitAdd} 10 | }, 11 | render(){ 12 | const btn = h("button",{ 13 | onClick:this.emitAdd 14 | },"emitAdd") 15 | const foo = h("p",{},"foo"); 16 | return h("div",{},[foo,btn]) 17 | } 18 | } -------------------------------------------------------------------------------- /example/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /example/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from '../../lib/guide-mini-vue.esm.js' 2 | import {App} from './App.js' 3 | const rootContainer = document.querySelector('#app'); 4 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/componentSlot/App.js: -------------------------------------------------------------------------------- 1 | import {h,createTextVnode} from '../../lib/guide-mini-vue.esm.js'; 2 | import {Foo} from './Foo.js' 3 | export const App = { 4 | name:'App', 5 | render(){ 6 | const app = h("div",{},"App"); 7 | const foo = h(Foo, {}, 8 | { 9 | header:({age})=>[h("p", {}, "header"+age),createTextVnode('你好呀')], 10 | footer:()=>h("p", {}, "footer") 11 | } 12 | ); 13 | //const foo = h(Foo, {}, h("p", {}, "123")); 14 | return h("div",{},[app,foo]) 15 | }, 16 | setup(){ 17 | return {} 18 | } 19 | } -------------------------------------------------------------------------------- /example/componentSlot/Foo.js: -------------------------------------------------------------------------------- 1 | import {h,renderSlots} from '../../lib/guide-mini-vue.esm.js'; 2 | export const Foo = { 3 | setup(){ 4 | return {} 5 | }, 6 | render(){ 7 | const foo = h("p",{},"foo"); 8 | const age = 11; 9 | return h("div", {}, [ 10 | renderSlots(this.$slots, "header",{age}) , 11 | foo, 12 | renderSlots(this.$slots, "footer",{}) 13 | ]); 14 | } 15 | } -------------------------------------------------------------------------------- /example/componentSlot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /example/componentSlot/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from '../../lib/guide-mini-vue.esm.js' 2 | import {App} from './App.js' 3 | const rootContainer = document.querySelector('#app'); 4 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/componentUpdate/App.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/guide-mini-vue.esm.js' 2 | import Child from './Child.js'; 3 | export const App = { 4 | name:'App', 5 | setup(){ 6 | const msg = ref('123'); 7 | const count = ref(1); 8 | window.msg = msg; 9 | 10 | const changeChildProps = ()=>{ 11 | msg.value = '456' 12 | } 13 | const changeCount = ()=>{ 14 | count.value++; 15 | } 16 | return {msg,count,changeChildProps,changeCount} 17 | }, 18 | render(){ 19 | return h("div",{},[ 20 | h("div",{},'你好'), 21 | h( 22 | "button", 23 | { 24 | onClick: this.changeChildProps 25 | }, 26 | "change child props" 27 | ), 28 | h( 29 | Child,{ 30 | msg:this.msg 31 | } 32 | ), 33 | h( 34 | "button", 35 | { 36 | onClick: this.changeCount 37 | }, 38 | "change self count" 39 | ), 40 | h( 41 | "p",{},"count"+this.count 42 | ) 43 | ]) 44 | } 45 | } -------------------------------------------------------------------------------- /example/componentUpdate/Child.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/guide-mini-vue.esm.js"; 2 | export default { 3 | name: "Child", 4 | setup(props, { emit }) {}, 5 | render() { 6 | console.log(this.$props) 7 | return h("div", {}, [h("div", {}, "child" + this.$props.msg)]); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /example/componentUpdate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /example/componentUpdate/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from '../../lib/guide-mini-vue.esm.js' 2 | import {App} from './App.js' 3 | const rootContainer = document.querySelector('#app'); 4 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/currentInstance/App.js: -------------------------------------------------------------------------------- 1 | import {h,getCurrentInstance} from '../../lib/guide-mini-vue.esm.js'; 2 | import {Foo} from './Foo.js' 3 | export const App = { 4 | name:'App', 5 | render(){ 6 | return h("div",{},[h("p",{},"currentInstance Demo"),h(Foo)]) 7 | }, 8 | setup(){ 9 | const instance = getCurrentInstance(); 10 | console.log('App:', instance) 11 | return {} 12 | } 13 | } -------------------------------------------------------------------------------- /example/currentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import {h,getCurrentInstance} from '../../lib/guide-mini-vue.esm.js'; 2 | export const Foo = { 3 | name:"Foo", 4 | setup(){ 5 | const instance = getCurrentInstance(); 6 | console.log('foo',instance) 7 | return {} 8 | }, 9 | render(){ 10 | return h("div", {}, "Foo"); 11 | } 12 | } -------------------------------------------------------------------------------- /example/currentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /example/currentInstance/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from '../../lib/guide-mini-vue.esm.js' 2 | import {App} from './App.js' 3 | const rootContainer = document.querySelector('#app'); 4 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/customRenderer/App.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-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 | } -------------------------------------------------------------------------------- /example/customRenderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /example/customRenderer/main.js: -------------------------------------------------------------------------------- 1 | import {createRenderer} from '../../lib/guide-mini-vue.esm.js'; 2 | import {App} from './App.js'; 3 | console.log(PIXI) 4 | 5 | const game = new PIXI.Application({ 6 | width:500, 7 | height:500 8 | }) 9 | document.body.append(game.view); 10 | 11 | 12 | const renderer = createRenderer({ 13 | createElement(type){ 14 | if(type==='rect'){ 15 | const rect = new PIXI.Graphics(); 16 | rect.beginFill(0xff0000); 17 | rect.drawRect(0,0,100,100); 18 | rect.endFill(); 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 | renderer.createApp(App).mount(game.stage); -------------------------------------------------------------------------------- /example/helloworld/App.js: -------------------------------------------------------------------------------- 1 | import {h} from '../../lib/guide-mini-vue.esm.js'; 2 | import {Foo} from './Foo.js' 3 | window.self = null; 4 | export const App = { 5 | render(){ 6 | window.self = this; 7 | return h("div",{ 8 | name:'App', 9 | id:'root', 10 | class:['red','hard'], 11 | onClick(){ 12 | console.log('click') 13 | }, 14 | onMousedown(){ 15 | console.log('onMousedown') 16 | } 17 | }, 18 | [h("div",{},"hi,"+this.msg),h(Foo,{count:1})] 19 | //[h("p",{class:'red'},"hi"),h("h1",{class:"green"},'标题')] 20 | ) 21 | }, 22 | setup(){ 23 | return { 24 | msg:'mini-vue' 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /example/helloworld/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/guide-mini-vue.esm.js" 2 | 3 | export const Foo = { 4 | setup(props){ 5 | console.log(props) 6 | props.count++; 7 | }, 8 | render(){ 9 | return h("div",{},"foo "+this.count) 10 | } 11 | } -------------------------------------------------------------------------------- /example/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /example/helloworld/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from '../../lib/guide-mini-vue.esm.js' 2 | import {App} from './App.js' 3 | const rootContainer = document.querySelector('#app'); 4 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/nextTick/App.js: -------------------------------------------------------------------------------- 1 | import {h,ref,getCurrentInstance,nextTick} from '../../lib/guide-mini-vue.esm.js' 2 | export const App = { 3 | name:'App', 4 | setup(){ 5 | const count = ref(1); 6 | const instance = getCurrentInstance(); 7 | function onClick (){ 8 | for(let i = 0;i<100;i++){ 9 | console.log('update') 10 | count.value = i; 11 | } 12 | console.log(instance) 13 | nextTick(()=>{ 14 | console.log(instance) 15 | }) 16 | } 17 | 18 | return { 19 | count, 20 | onClick 21 | } 22 | }, 23 | render(){ 24 | const button = h("button",{onClick:this.onClick},"update") 25 | const p = h("p",{},"count:"+this.count); 26 | return h("div",{},[button,p]) 27 | } 28 | } -------------------------------------------------------------------------------- /example/nextTick/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /example/nextTick/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from '../../lib/guide-mini-vue.esm.js' 2 | import {App} from './App.js' 3 | const rootContainer = document.querySelector('#app'); 4 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/patchChildren/App.js: -------------------------------------------------------------------------------- 1 | import {h} from '../../lib/guide-mini-vue.esm.js' 2 | import ArrayToText from './ArrayToText.js' 3 | import TextToText from './TextToText.js'; 4 | import TextToArray from './TextToArray.js'; 5 | import ArrayToArray from './ArrayToArray.js'; 6 | export const App = { 7 | name:'App', 8 | setup(){}, 9 | render(){ 10 | return h( 11 | "div", 12 | {tId:1}, 13 | [ 14 | h("p",{},'主页'), 15 | //h(ArrayToText) 16 | //h(TextToText) 17 | //h(TextToArray) 18 | h(ArrayToArray) 19 | ] 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /example/patchChildren/ArrayToArray.js: -------------------------------------------------------------------------------- 1 | import {ref,h} from '../../lib/guide-mini-vue.esm.js' 2 | //1、左侧的对比 3 | /* const prevChildren = [ 4 | h("p",{key:'A'},'A'), 5 | h("p",{key:'B'},'B'), 6 | h("p",{key:'C'},'C'), 7 | ] 8 | const nextChildren = [ 9 | h("p",{key:'A'},'A'), 10 | h("p",{key:'B'},'B'), 11 | h("p",{key:'D'},'D'), 12 | h("p",{key:'E'},'E'), 13 | ] */ 14 | //2、右侧的对比 15 | /* const prevChildren = [ 16 | h("p",{key:'A'},'A'), 17 | h("p",{key:'B'},'B'), 18 | h("p",{key:'C'},'C'), 19 | ] 20 | const nextChildren = [ 21 | h("p",{key:'D'},'D'), 22 | h("p",{key:'E'},'E'), 23 | h("p",{key:'B'},'B'), 24 | h("p",{key:'C'},'C'), 25 | ] */ 26 | //3、新的比老的长 左侧相同 27 | /* const prevChildren = [ 28 | h("p",{key:'A'},'A'), 29 | h("p",{key:'B'},'B'), 30 | ] 31 | const nextChildren = [ 32 | h("p",{key:'A'},'A'), 33 | h("p",{key:'B'},'B'), 34 | h("p",{key:'C'},'C'), 35 | ] */ 36 | //4、新的比老的长 右侧相同 37 | /* const prevChildren = [ 38 | h("p",{key:'A'},'A'), 39 | h("p",{key:'B'},'B'), 40 | ] 41 | const nextChildren = [ 42 | h("p",{key:'D'},'D'), 43 | h("p",{key:'C'},'C'), 44 | h("p",{key:'A'},'A'), 45 | h("p",{key:'B'},'B'), 46 | ] */ 47 | //5、新的比老的短 删除 左侧相同 48 | /* const prevChildren = [ 49 | h("p",{key:'A'},'A'), 50 | h("p",{key:'B'},'B'), 51 | h("p",{key:'C'},'C'), 52 | ] 53 | const nextChildren = [ 54 | h("p",{key:'A'},'A'), 55 | h("p",{key:'B'},'B'), 56 | ] 57 | //6、新的比老的短 删除 右侧相同 58 | /* const prevChildren = [ 59 | h("p",{key:'A'},'A'), 60 | h("p",{key:'B'},'B'), 61 | h("p",{key:'C'},'C'), 62 | ] 63 | const nextChildren = [ 64 | h("p",{key:'B'},'B'), 65 | h("p",{key:'C'},'C'), 66 | ] */ 67 | //7、对比中间的部分 68 | /* const prevChildren = [ 69 | h("p",{key:'A'},'A'), 70 | h("p",{key:'B'},'B'), 71 | h("p",{key:'C',id:'prev-c'},'C'), 72 | h("p",{key:'D'},'D'), 73 | h("p",{key:'F'},'F'), 74 | h("p",{key:'G'},'G'), 75 | ] 76 | const nextChildren = [ 77 | h("p",{key:'A'},'A'), 78 | h("p",{key:'B'},'B'), 79 | h("p",{key:'E'},'E'), 80 | h("p",{key:'C',id:'next-c'},'C'), 81 | h("p",{key:'F'},'F'), 82 | h("p",{key:'G'},'G'), 83 | ] */ 84 | //8、老的比新的长 85 | /* const prevChildren = [ 86 | h("p",{key:'A'},'A'), 87 | h("p",{key:'B'},'B'), 88 | h("p",{key:'C',id:'prev-c'},'C'), 89 | h("p",{key:'E'},'E'), 90 | h("p",{key:'D'},'D'), 91 | h("p",{key:'F'},'F'), 92 | h("p",{key:'G'},'G'), 93 | ] 94 | const nextChildren = [ 95 | h("p",{key:'A'},'A'), 96 | h("p",{key:'B'},'B'), 97 | h("p",{key:'E'},'E'), 98 | h("p",{key:'C',id:'next-c'},'C'), 99 | h("p",{key:'F'},'F'), 100 | h("p",{key:'G'},'G'), 101 | ] */ 102 | //9、最长递增子序列 103 | /* const prevChildren = [ 104 | h("p",{key:'A'},'A'), 105 | h("p",{key:'B'},'B'), 106 | h("p",{key:'C'},'C'), 107 | h("p",{key:'D'},'D'), 108 | h("p",{key:'E'},'E'), 109 | h("p",{key:'F'},'F'), 110 | h("p",{key:'G'},'G'), 111 | ] 112 | const nextChildren = [ 113 | h("p",{key:'A'},'A'), 114 | h("p",{key:'B'},'B'), 115 | h("p",{key:'E'},'E'), 116 | h("p",{key:'C'},'C'), 117 | h("p",{key:'D'},'D'), 118 | h("p",{key:'F'},'F'), 119 | h("p",{key:'G'},'G'), 120 | ] */ 121 | /* const prevChildren = [ 122 | h("p",{key:'A'},'A'), 123 | h("p",{key:'B'},'B'), 124 | h("p",{key:'C'},'C'), 125 | h("p",{key:'E'},'E'), 126 | h("p",{key:'F'},'F'), 127 | h("p",{key:'G'},'G'), 128 | ] 129 | const nextChildren = [ 130 | h("p",{key:'A'},'A'), 131 | h("p",{key:'B'},'B'), 132 | h("p",{key:'E'},'E'), 133 | h("p",{key:'C'},'C'), 134 | h("p",{key:'D'},'D'), 135 | h("p",{key:'F'},'F'), 136 | h("p",{key:'G'},'G'), 137 | ] */ 138 | /* const prevChildren = [ 139 | h("p",{key:'A'},'A'), 140 | h("p",{key:'B'},'B'), 141 | h("p",{key:'C'},'C'), 142 | h("p",{key:'D'},'D'), 143 | h("p",{key:'E'},'E'), 144 | h("p",{key:'Z'},'Z'), 145 | h("p",{key:'F'},'F'), 146 | h("p",{key:'G'},'G'), 147 | ] 148 | const nextChildren = [ 149 | h("p",{key:'A'},'A'), 150 | h("p",{key:'B'},'B'), 151 | h("p",{key:'D'},'D'), 152 | h("p",{key:'C'},'C'), 153 | h("p",{key:'Y'},'Y'), 154 | h("p",{key:'E'},'E'), 155 | h("p",{key:'F'},'F'), 156 | h("p",{key:'G'},'G'), 157 | ] */ 158 | //fix 159 | const prevChildren = [ 160 | h("p",{key:'A'},'A'), 161 | h("p",{key:'B'},'B'), 162 | h("p",{},'C'), 163 | h("p",{key:'D'},'D'), 164 | ] 165 | const nextChildren = [ 166 | h("p",{key:'A'},'A'), 167 | h("p",{},'C'), 168 | h("p",{key:'B'},'B'), 169 | h("p",{key:'D'},'D'), 170 | ] 171 | export default { 172 | name: 'ArrayToArray', 173 | setup() { 174 | const isChange = ref(false); 175 | window.isChange = isChange; 176 | return { 177 | isChange 178 | } 179 | }, 180 | render() { 181 | const self = this; 182 | return self.isChange === true ? 183 | h("div", {}, nextChildren) : 184 | h("div", {}, prevChildren) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /example/patchChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/guide-mini-vue.esm.js'; 2 | const nextChildren = 'newChildren'; 3 | const prevChildren = [h("div",{},"A"),h("div",{},"B")]; 4 | export default { 5 | name:'ArrayToText', 6 | setup(){ 7 | const isChange = ref(false); 8 | window.isChange = isChange; 9 | return { 10 | isChange 11 | } 12 | }, 13 | render(){ 14 | const self = this; 15 | return self.isChange === true 16 | ? h("div",{},nextChildren) 17 | : h("div",{},prevChildren) 18 | } 19 | } -------------------------------------------------------------------------------- /example/patchChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/guide-mini-vue.esm.js'; 2 | const nextChildren = [h("div", {}, "A"), h("div", {}, "B")]; 3 | const prevChildren = 'newChildren'; 4 | export default { 5 | name:'ArrayToText', 6 | setup(){ 7 | const isChange = ref(false); 8 | window.isChange = isChange; 9 | return { 10 | isChange 11 | } 12 | }, 13 | render(){ 14 | const self = this; 15 | return self.isChange === true 16 | ? h("div",{},nextChildren) 17 | : h("div",{},prevChildren) 18 | } 19 | } -------------------------------------------------------------------------------- /example/patchChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/guide-mini-vue.esm.js'; 2 | const nextChildren = 'newChildren'; 3 | const prevChildren = 'oldChildren'; 4 | export default { 5 | name:'ArrayToText', 6 | setup(){ 7 | const isChange = ref(false); 8 | window.isChange = isChange; 9 | return { 10 | isChange 11 | } 12 | }, 13 | render(){ 14 | const self = this; 15 | return self.isChange === true 16 | ? h("div",{},nextChildren) 17 | : h("div",{},prevChildren) 18 | } 19 | } -------------------------------------------------------------------------------- /example/patchChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /example/patchChildren/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from '../../lib/guide-mini-vue.esm.js' 2 | import {App} from './App.js' 3 | const rootContainer = document.querySelector('#app'); 4 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/update/App.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/guide-mini-vue.esm.js' 2 | export const App = { 3 | name:'App', 4 | setup(){ 5 | const count = ref(0); 6 | const onClick = ()=>{ 7 | count.value++; 8 | } 9 | const props = ref({ 10 | foo:'foo', 11 | bar:'bar' 12 | }) 13 | const onChangePropsDemo1 = ()=>{ 14 | props.value.foo = 'new-foo'; 15 | } 16 | const onChangePropsDemo2 = ()=>{ 17 | props.value.foo = undefined; 18 | } 19 | const onChangePropsDemo3 = ()=>{ 20 | props.value = { 21 | foo:'foo' 22 | } 23 | } 24 | return { 25 | count, 26 | props, 27 | onClick, 28 | onChangePropsDemo1, 29 | onChangePropsDemo2, 30 | onChangePropsDemo3 31 | } 32 | }, 33 | render(){ 34 | return h( 35 | "div", 36 | { 37 | id:"root", 38 | ...this.props 39 | }, 40 | [ 41 | h("div",{},"count:"+this.count),//依赖收集 42 | h( 43 | "button", 44 | { 45 | onClick:this.onClick 46 | }, 47 | "click" 48 | ), 49 | h( 50 | "button", 51 | { 52 | onClick:this.onChangePropsDemo1 53 | }, 54 | "changeProps - 值改变了 - 修改" 55 | ), 56 | h( 57 | "button", 58 | { 59 | onClick:this.onChangePropsDemo2 60 | }, 61 | "changeProps - 值改变了 undefined - 删除" 62 | ), 63 | h( 64 | "button", { 65 | onClick: this.onChangePropsDemo3 66 | }, 67 | "changeProps - key在新的里面没有了 - 删除" 68 | ) 69 | ] 70 | ) 71 | } 72 | } -------------------------------------------------------------------------------- /example/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /example/update/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from '../../lib/guide-mini-vue.esm.js' 2 | import {App} from './App.js' 3 | const rootContainer = document.querySelector('#app'); 4 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /lib/guide-mini-vue.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | const Fragment = Symbol("Fragment"); 6 | const Text = Symbol("Text"); 7 | function createVnode(type, props, children) { 8 | const vnode = { 9 | type, 10 | props, 11 | children, 12 | el: null, 13 | key: props && props.key, 14 | component: null, 15 | shapeFlag: getShapeFlag(type) 16 | }; 17 | if (typeof children === 'string') { 18 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 19 | } 20 | else if (Array.isArray(children)) { 21 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 22 | } 23 | if (vnode.shapeFlag & 2 /* STATEFUL_COMPONENT */) { 24 | if (typeof children === 'object') { 25 | vnode.shapeFlag |= 16 /* SLOT_CHILDREN */; 26 | } 27 | } 28 | return vnode; 29 | } 30 | function createTextVnode(text) { 31 | return createVnode(Text, {}, text); 32 | } 33 | function getShapeFlag(type) { 34 | return typeof type === "string" ? 1 /* ELEMENT */ : 2 /* STATEFUL_COMPONENT */; 35 | } 36 | 37 | function h(type, props, children) { 38 | return createVnode(type, props, children); 39 | } 40 | 41 | function renderSlots(slots, name, props) { 42 | const slot = slots[name]; 43 | if (slot) { 44 | if (typeof slot === 'function') { 45 | return createVnode(Fragment, {}, slot(props)); 46 | } 47 | } 48 | } 49 | 50 | const extend = Object.assign; 51 | const isObject = (val) => { 52 | return val !== null && typeof val === "object"; 53 | }; 54 | const hasChanged = (newValue, value) => { 55 | return !Object.is(newValue, value); 56 | }; 57 | const camelize = (str) => { 58 | return str.replace(/-(\w)/g, (_, c) => { 59 | return c ? c.toUpperCase() : ""; 60 | }); 61 | }; 62 | const capitalize = (str) => { 63 | return str.charAt(0).toUpperCase() + str.slice(1); 64 | }; 65 | const toHandlerKey = (str) => { 66 | return str ? "on" + capitalize(str) : ""; 67 | }; 68 | 69 | let activeEffect; 70 | let shouldTrack; 71 | class ReactiveEffect { 72 | constructor(fn, scheduler) { 73 | this.scheduler = scheduler; 74 | this.deps = []; 75 | this.active = true; 76 | this._fn = fn; 77 | } 78 | run() { 79 | if (!this.active) { 80 | return this._fn(); 81 | } 82 | shouldTrack = true; 83 | activeEffect = this; 84 | const result = this._fn(); 85 | shouldTrack = false; 86 | return result; 87 | } 88 | stop() { 89 | if (this.active) { 90 | cleanupEffect(this); 91 | if (this.onStop) 92 | this.onStop(); 93 | this.active = false; 94 | } 95 | } 96 | } 97 | function cleanupEffect(effect) { 98 | effect.deps.forEach((dep) => { 99 | dep.delete(effect); 100 | }); 101 | effect.deps.length = 0; 102 | } 103 | let targetMap = new Map(); 104 | function track(target, key) { 105 | //targetMap target key 106 | if (!isTracking()) 107 | return; 108 | let depsMap = targetMap.get(target); 109 | if (!depsMap) { 110 | depsMap = new Map(); 111 | targetMap.set(target, depsMap); 112 | } 113 | let deps = depsMap.get(key); 114 | if (!deps) { 115 | deps = new Set(); 116 | depsMap.set(key, deps); 117 | } 118 | trackEffects(deps); 119 | } 120 | function trackEffects(deps) { 121 | if (deps.has(activeEffect)) 122 | return; 123 | deps.add(activeEffect); 124 | //反向收集 125 | activeEffect.deps.push(deps); 126 | } 127 | function isTracking() { 128 | return shouldTrack && activeEffect !== undefined; 129 | } 130 | function trigger(target, key) { 131 | let depsMap = targetMap.get(target); 132 | if (!depsMap) 133 | return; 134 | let deps = depsMap.get(key); 135 | triggerEffect(deps); 136 | } 137 | function triggerEffect(deps) { 138 | deps && deps.forEach((effect) => { 139 | if (effect.scheduler) { 140 | effect.scheduler(); 141 | } 142 | else { 143 | effect.run(); 144 | } 145 | }); 146 | } 147 | function effect(fn, options = {}) { 148 | const _effect = new ReactiveEffect(fn, options.scheduler); 149 | extend(_effect, options); 150 | _effect.onStop = options.onStop; 151 | _effect.run(); 152 | const runner = _effect.run.bind(_effect); 153 | runner.effect = _effect; 154 | return runner; 155 | } 156 | 157 | const get = createGetter(); 158 | const set = createSetter(); 159 | const readonlyGet = createGetter(true); 160 | const shallowReadonlyGet = createGetter(true, true); 161 | function createGetter(isReadonly = false, shallow = false) { 162 | return function get(target, key) { 163 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 164 | return !isReadonly; 165 | } 166 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 167 | return isReadonly; 168 | } 169 | const res = Reflect.get(target, key); 170 | if (shallow) 171 | return res; 172 | if (isObject(res)) { 173 | return isReadonly ? readonly(res) : reactive(res); 174 | } 175 | if (!isReadonly) { 176 | track(target, key); 177 | } 178 | return res; 179 | }; 180 | } 181 | function createSetter() { 182 | return function set(target, key, value) { 183 | const res = Reflect.set(target, key, value); 184 | trigger(target, key); 185 | return res; 186 | }; 187 | } 188 | const mutableHandlers = { 189 | get, 190 | set, 191 | }; 192 | const readonlyHandlers = { 193 | get: readonlyGet, 194 | set(target, key, value) { 195 | console.warn(`key:${key} set失败了,因为target是readonly的`, target); 196 | return true; 197 | }, 198 | }; 199 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 200 | get: shallowReadonlyGet 201 | }); 202 | 203 | function reactive(raw) { 204 | return createActiveObject(raw, mutableHandlers); 205 | } 206 | function readonly(raw) { 207 | return createActiveObject(raw, readonlyHandlers); 208 | } 209 | function shallowReadonly(raw) { 210 | return createActiveObject(raw, shallowReadonlyHandlers); 211 | } 212 | function createActiveObject(target, baseHandlers) { 213 | if (!isObject(target)) { 214 | console.warn(`target ${target} 必须是一个对象`); 215 | return target; 216 | } 217 | return new Proxy(target, baseHandlers); 218 | } 219 | 220 | class RefImpl { 221 | constructor(value) { 222 | this._v_ref = true; 223 | this._rawValue = value; 224 | this._value = convert(value); 225 | this.dep = new Set(); 226 | } 227 | get value() { 228 | trackRefEffects(this); 229 | return this._value; 230 | } 231 | set value(newValue) { 232 | if (hasChanged(newValue, this._rawValue)) { 233 | this._rawValue = newValue; 234 | this._value = convert(newValue); 235 | triggerEffect(this.dep); 236 | } 237 | } 238 | } 239 | function convert(value) { 240 | return isObject(value) ? reactive(value) : value; 241 | } 242 | function trackRefEffects(ref) { 243 | if (isTracking()) { 244 | trackEffects(ref.dep); 245 | } 246 | } 247 | function ref(value) { 248 | return new RefImpl(value); 249 | } 250 | function isRef(value) { 251 | return !!value._v_ref; 252 | } 253 | function unRef(ref) { 254 | return isRef(ref) ? ref.value : ref; 255 | } 256 | function proxyRefs(objectWithRefs) { 257 | return new Proxy(objectWithRefs, { 258 | get(target, key) { 259 | return unRef(Reflect.get(target, key)); 260 | }, 261 | set(target, key, value) { 262 | if (isRef(target[key]) && !isRef(value)) { 263 | return (target[key].value = value); 264 | } 265 | else { 266 | return Reflect.set(target, key, value); 267 | } 268 | } 269 | }); 270 | } 271 | 272 | function emit(instance, event, ...args) { 273 | const { props } = instance; 274 | const HandlerName = toHandlerKey(camelize(event)); 275 | const handler = props[HandlerName]; 276 | handler && handler(...args); 277 | } 278 | 279 | function initProps(instance, rawProps = {}) { 280 | instance.props = rawProps; 281 | } 282 | 283 | const publicPropertiesMap = { 284 | $el: (i) => i.vnode.el, 285 | $slots: (i) => i.slots, 286 | $props: (i) => i.props 287 | }; 288 | const PublicInstanceProxyHandlers = { 289 | get({ _: instance }, key) { 290 | const { setupState, props } = instance; 291 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 292 | if (hasOwn(setupState, key)) { 293 | return setupState[key]; 294 | } 295 | else if (hasOwn(props, key)) { 296 | return props[key]; 297 | } 298 | const publicGetter = publicPropertiesMap[key]; 299 | if (publicGetter) { 300 | return publicGetter(instance); 301 | } 302 | }, 303 | }; 304 | 305 | function initSlots(instance, children) { 306 | const { vnode } = instance; 307 | if (vnode.shapeFlag & 16 /* SLOT_CHILDREN */) { 308 | normalizeObjectSlots(children, instance.slots); 309 | } 310 | } 311 | function normalizeSlotValue(value) { 312 | return Array.isArray(value) ? value : [value]; 313 | } 314 | function normalizeObjectSlots(children, slots) { 315 | for (const key in children) { 316 | const value = children[key]; 317 | if (typeof value === "function") { 318 | slots[key] = (props) => normalizeSlotValue(value(props)); 319 | } 320 | } 321 | } 322 | 323 | function createComponentInstance(vnode, parent) { 324 | const component = { 325 | vnode, 326 | type: vnode.type, 327 | setupState: {}, 328 | props: {}, 329 | slots: {}, 330 | provides: parent ? parent.provides : {}, 331 | parent, 332 | isMounted: false, 333 | subTree: {}, 334 | next: null, 335 | emit: () => { }, 336 | }; 337 | component.emit = emit.bind(null, component); 338 | return component; 339 | } 340 | function setupComponent(instance) { 341 | initProps(instance, instance.vnode.props); 342 | initSlots(instance, instance.vnode.children); 343 | setupStatefulComponent(instance); 344 | } 345 | function setupStatefulComponent(instance) { 346 | const Component = instance.type; 347 | instance.proxy = new Proxy({ 348 | _: instance, 349 | }, PublicInstanceProxyHandlers); 350 | const { setup } = Component; 351 | if (setup) { 352 | setCurrentInstance(instance); 353 | const setupResult = setup(shallowReadonly(instance.props), { 354 | emit: instance.emit 355 | }); 356 | handleSetupResult(instance, setupResult); 357 | setCurrentInstance(null); 358 | } 359 | } 360 | function handleSetupResult(instance, setupResult) { 361 | if (typeof setupResult === "object") { 362 | instance.setupState = proxyRefs(setupResult); 363 | } 364 | finishComponentSetup(instance); 365 | } 366 | function finishComponentSetup(instance) { 367 | const Component = instance.type; 368 | instance.render = Component.render; 369 | } 370 | let currentInstance = null; 371 | function getCurrentInstance() { 372 | return currentInstance; 373 | } 374 | function setCurrentInstance(instance) { 375 | currentInstance = instance; 376 | } 377 | 378 | function provide(key, value) { 379 | const currentInstance = getCurrentInstance(); 380 | if (currentInstance) { 381 | let { provides } = currentInstance; 382 | const parentProvides = currentInstance.parent.provides; 383 | if (provides === parentProvides) { 384 | provides = currentInstance.provides = Object.create(parentProvides); 385 | } 386 | provides[key] = value; 387 | } 388 | } 389 | function inject(key, defaultValue) { 390 | const currentInstance = getCurrentInstance(); 391 | if (currentInstance) { 392 | const parentProvides = currentInstance.parent.provides; 393 | if (parentProvides[key]) { 394 | return parentProvides[key]; 395 | } 396 | else if (defaultValue) { 397 | if (typeof defaultValue === 'function') { 398 | return defaultValue(); 399 | } 400 | return defaultValue; 401 | } 402 | } 403 | } 404 | 405 | function shouldUpdateComponent(prevVnode, nextVnode) { 406 | const { props: prevProps } = prevVnode; 407 | const { props: nextProps } = nextVnode; 408 | for (const key in nextProps) { 409 | if (nextProps[key] !== prevProps[key]) { 410 | return true; 411 | } 412 | } 413 | return false; 414 | } 415 | 416 | function createAppAPI(render) { 417 | return function createApp(rootComponent) { 418 | return { 419 | mount(rootContainer) { 420 | const vnode = createVnode(rootComponent); 421 | render(vnode, rootContainer); 422 | }, 423 | }; 424 | }; 425 | } 426 | 427 | const queue = []; 428 | let isFlushPending = false; 429 | const p = Promise.resolve(); 430 | function nextTick(fn) { 431 | return fn ? p.then(fn) : p; 432 | } 433 | function queueJobs(job) { 434 | if (!queue.includes(job)) { 435 | queue.push(job); 436 | } 437 | queueFlush(); 438 | } 439 | function queueFlush() { 440 | if (isFlushPending) 441 | return; 442 | isFlushPending = true; 443 | nextTick(flushJobs); 444 | } 445 | function flushJobs() { 446 | isFlushPending = false; 447 | let job; 448 | while ((job = queue.shift())) { 449 | job && job(); 450 | } 451 | } 452 | 453 | function createRenderer(options) { 454 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 455 | function render(vnode, container, parentComponent) { 456 | patch(null, vnode, container, parentComponent, null); 457 | } 458 | //n1 old n2 new 459 | function patch(n1, n2, container, parentComponent, anchor) { 460 | const { type, shapeFlag } = n2; 461 | switch (type) { 462 | case Fragment: 463 | processFragment(n1, n2, container, parentComponent, anchor); 464 | break; 465 | case Text: 466 | processText(n1, n2, container); 467 | break; 468 | default: 469 | if (shapeFlag & 1 /* ELEMENT */) { 470 | processElement(n1, n2, container, parentComponent, anchor); 471 | } 472 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 473 | processComponent(n1, n2, container, parentComponent, anchor); 474 | } 475 | break; 476 | } 477 | } 478 | function processElement(n1, n2, container, parentComponent, anchor) { 479 | if (!n1) { 480 | mountElement(n1, n2, container, parentComponent, anchor); 481 | } 482 | else { 483 | patchElement(n1, n2, container, parentComponent, anchor); 484 | } 485 | } 486 | function patchElement(n1, n2, container, parentComponent, anchor) { 487 | console.log("patchComponent"); 488 | console.log("n1", n1); 489 | console.log("n2", n2); 490 | const oldProps = n1.props || EMPTY_OBJ; 491 | const newProps = n2.props || EMPTY_OBJ; 492 | const el = (n2.el = n1.el); 493 | patchChildren(n1, n2, el, parentComponent, anchor); 494 | patchProps(el, oldProps, newProps); 495 | } 496 | function patchChildren(n1, n2, container, parentComponent, anchor) { 497 | const prevShapeFlag = n1.shapeFlag; 498 | const { shapeFlag } = n2; 499 | const c1 = n1.children; 500 | const c2 = n2.children; 501 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 502 | if (prevShapeFlag & 8 /* ARRAY_CHILDREN */) { 503 | //1 把n1都清空 504 | unmountChildren(n1.children); 505 | } 506 | //2 设置text 507 | if (c1 !== c2) { 508 | hostSetElementText(container, c2); 509 | } 510 | } 511 | else { 512 | //text to array 513 | if (prevShapeFlag & 4 /* TEXT_CHILDREN */) { 514 | hostSetElementText(container, ""); 515 | mountChildren(c2, container, parentComponent, anchor); 516 | } 517 | else { 518 | //array 519 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 520 | } 521 | } 522 | } 523 | function patchKeyedChildren(c1, c2, container, parentComponent, parentAnchor) { 524 | let i = 0; 525 | const l2 = c2.length; 526 | let e1 = c1.length - 1; 527 | let e2 = l2 - 1; 528 | function isSomeVNodeType(n1, n2) { 529 | return n1.type === n2.type && n1.key === n2.key; 530 | } 531 | //左侧 532 | while (i <= e1 && i <= e2) { 533 | const n1 = c1[i]; 534 | const n2 = c2[i]; 535 | if (isSomeVNodeType(n1, n2)) { 536 | patch(n1, n2, container, parentComponent, parentAnchor); 537 | } 538 | else { 539 | break; 540 | } 541 | i++; 542 | } 543 | //右侧 544 | while (i <= e1 && i <= e2) { 545 | const n1 = c1[e1]; 546 | const n2 = c2[e2]; 547 | if (isSomeVNodeType(n1, n2)) { 548 | patch(n1, n2, container, parentComponent, parentAnchor); 549 | } 550 | else { 551 | break; 552 | } 553 | e1--; 554 | e2--; 555 | } 556 | //新的比老的多 创建 557 | if (i > e1) { 558 | if (i <= e2) { 559 | const nextPos = e2 + 1; 560 | const anchor = e2 + 1 < l2 ? c2[nextPos].el : null; 561 | while (i <= e2) { 562 | patch(null, c2[i], container, parentComponent, anchor); 563 | i++; 564 | } 565 | } 566 | } 567 | else if (i > e2) { 568 | while (i <= e1) { 569 | hostRemove(c1[i].el); 570 | i++; 571 | } 572 | } 573 | else { 574 | let s1 = i; 575 | let s2 = i; 576 | const toBePatched = e2 - s2 + 1; 577 | let patched = 0; 578 | const keyToNewIndexMap = new Map(); 579 | const newIndexToOldIndexMap = new Array(toBePatched); 580 | let moved = false; 581 | let maxNewIndexSoFar = 0; 582 | for (let i = 0; i < toBePatched; i++) 583 | newIndexToOldIndexMap[i] = 0; 584 | for (let i = s2; i <= e2; i++) { 585 | const nextChild = c2[i]; 586 | keyToNewIndexMap.set(nextChild.key, i); 587 | } 588 | for (let i = s1; i <= e1; i++) { 589 | const prevChild = c1[i]; 590 | if (patched >= toBePatched) { 591 | hostRemove(prevChild.el); 592 | continue; 593 | } 594 | let newIndex; 595 | if (prevChild.key != null) { 596 | newIndex = keyToNewIndexMap.get(prevChild.key); 597 | } 598 | else { 599 | for (let j = s2; j <= e2; j++) { 600 | if (isSomeVNodeType(prevChild, c2[j])) { 601 | newIndex = j; 602 | break; 603 | } 604 | } 605 | } 606 | if (newIndex === undefined) { 607 | hostRemove(prevChild.el); 608 | } 609 | else { 610 | if (newIndex >= maxNewIndexSoFar) { 611 | maxNewIndexSoFar = newIndex; 612 | } 613 | else { 614 | moved = true; 615 | } 616 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 617 | patch(prevChild, c2[newIndex], container, parentComponent, null); 618 | patched++; 619 | } 620 | } 621 | const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []; 622 | let j = increasingNewIndexSequence.length - 1; 623 | for (let i = toBePatched - 1; i >= 0; i--) { 624 | const nextIndex = i + s2; 625 | const nextChild = c2[nextIndex]; 626 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 627 | if (newIndexToOldIndexMap[i] === 0) { 628 | patch(null, nextChild, container, parentComponent, anchor); 629 | } 630 | else if (moved) { 631 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 632 | console.log("移动位置"); 633 | hostInsert(nextChild.el, container, anchor); 634 | } 635 | else { 636 | j--; 637 | } 638 | } 639 | } 640 | } 641 | } 642 | function getSequence(arr) { 643 | const p = arr.slice(); 644 | const result = [0]; 645 | let i, j, u, v, c; 646 | const len = arr.length; 647 | for (i = 0; i < len; i++) { 648 | const arrI = arr[i]; 649 | if (arrI !== 0) { 650 | j = result[result.length - 1]; 651 | if (arr[j] < arrI) { 652 | p[i] = j; 653 | result.push(i); 654 | continue; 655 | } 656 | u = 0; 657 | v = result.length - 1; 658 | while (u < v) { 659 | c = (u + v) >> 1; 660 | if (arr[result[c]] < arrI) { 661 | u = c + 1; 662 | } 663 | else { 664 | v = c; 665 | } 666 | } 667 | if (arrI < arr[result[u]]) { 668 | if (u > 0) { 669 | p[i] = result[u - 1]; 670 | } 671 | result[u] = i; 672 | } 673 | } 674 | } 675 | u = result.length; 676 | v = result[u - 1]; 677 | while (u-- > 0) { 678 | result[u] = v; 679 | v = p[v]; 680 | } 681 | return result; 682 | } 683 | function unmountChildren(children) { 684 | for (let i = 0; i < children.length; i++) { 685 | const el = children[i].el; 686 | hostRemove(el); 687 | } 688 | } 689 | const EMPTY_OBJ = {}; 690 | function patchProps(el, oldProps, newProps) { 691 | if (oldProps !== newProps) { 692 | for (let key in newProps) { 693 | const prevProp = oldProps[key]; 694 | const nextProp = newProps[key]; 695 | if (prevProp !== nextProp) { 696 | hostPatchProp(el, key, prevProp, nextProp); 697 | } 698 | } 699 | if (oldProps !== EMPTY_OBJ) 700 | for (let key in oldProps) { 701 | if (!(key in newProps)) { 702 | hostPatchProp(el, key, oldProps[key], null); 703 | } 704 | } 705 | } 706 | } 707 | function mountElement(n1, n2, container, parentComponent, anchor) { 708 | const el = (n2.el = hostCreateElement(n2.type)); 709 | const { children, shapeFlag } = n2; 710 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 711 | el.textContent = children; 712 | } 713 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 714 | mountChildren(n2.children, el, parentComponent, anchor); 715 | } 716 | const { props } = n2; 717 | for (const key in props) { 718 | const val = props[key]; 719 | hostPatchProp(el, key, null, val); 720 | } 721 | hostInsert(el, container, anchor); 722 | } 723 | function processFragment(n1, n2, container, parentComponent, anchor) { 724 | mountChildren(n2.children, container, parentComponent, anchor); 725 | } 726 | function processText(n1, n2, container) { 727 | const { children } = n2; 728 | const textNode = (n2.el = document.createTextNode(children)); 729 | container.append(textNode); 730 | } 731 | function mountChildren(children, container, parentComponent, anchor) { 732 | children.forEach((v) => { 733 | patch(null, v, container, parentComponent, anchor); 734 | }); 735 | } 736 | function processComponent(n1, n2, container, parentComponent, anchor) { 737 | if (!n1) { 738 | mountComponent(n2, container, parentComponent, anchor); 739 | } 740 | else { 741 | updateComponent(n1, n2); 742 | } 743 | } 744 | function updateComponent(n1, n2) { 745 | const instance = (n2.component = n1.component); 746 | if (shouldUpdateComponent(n1, n2)) { 747 | instance.next = n2; 748 | instance.update(); 749 | } 750 | else { 751 | n2.component = n1.component; 752 | n2.el = n1.el; 753 | instance.vnode = n2; 754 | } 755 | } 756 | function mountComponent(initialVnode, container, parentComponent, anchor) { 757 | const instance = (initialVnode.component = createComponentInstance(initialVnode, parentComponent)); 758 | setupComponent(instance); 759 | setupRenderEffect(instance, initialVnode, container, anchor); 760 | } 761 | function setupRenderEffect(instance, initialVnode, container, anchor) { 762 | instance.update = effect(() => { 763 | if (!instance.isMounted) { 764 | console.log("init"); 765 | const { proxy } = instance; 766 | const subTree = (instance.subTree = instance.render.call(proxy)); 767 | patch(null, subTree, container, instance, anchor); 768 | initialVnode.el = subTree.el; 769 | instance.isMounted = true; 770 | } 771 | else { 772 | console.log("update"); 773 | const { next, vnode } = instance; 774 | if (next) { 775 | next.el = vnode.el; 776 | updateComponentPreRender(instance, next); 777 | } 778 | const { proxy } = instance; 779 | const subTree = instance.render.call(proxy); 780 | const prevSubTree = instance.subTree; 781 | instance.subTree = subTree; 782 | patch(prevSubTree, subTree, container, instance, anchor); 783 | } 784 | }, { 785 | scheduler() { 786 | console.log('update--scheduler'); 787 | queueJobs(instance.update); 788 | } 789 | }); 790 | } 791 | return { 792 | createApp: createAppAPI(render) 793 | }; 794 | } 795 | function updateComponentPreRender(instance, nextVnode) { 796 | instance.vnode = nextVnode; 797 | instance.next = null; 798 | instance.props = nextVnode.props; 799 | } 800 | 801 | function createElement(type) { 802 | return document.createElement(type); 803 | } 804 | function patchProp(el, key, oldVal, newVal) { 805 | const isOn = (key) => /^on[A-Z]/.test(key); 806 | if (isOn(key)) { 807 | const event = key.slice(2).toLowerCase(); 808 | el.addEventListener(event, newVal); 809 | } 810 | else { 811 | if (newVal === undefined || newVal === null) { 812 | el.removeAttribute(key, newVal); 813 | } 814 | else { 815 | el.setAttribute(key, newVal); 816 | } 817 | } 818 | } 819 | function insert(child, parent, anchor) { 820 | parent.insertBefore(child, anchor || null); 821 | } 822 | function remove(child) { 823 | const parent = child.parentNode; 824 | if (parent) { 825 | parent.removeChild(child); 826 | } 827 | } 828 | function setElementText(container, text) { 829 | container.textContent = text; 830 | } 831 | const render = createRenderer({ 832 | createElement, 833 | patchProp, 834 | insert, 835 | remove, 836 | setElementText 837 | }); 838 | function createApp(...args) { 839 | return render.createApp(...args); 840 | } 841 | 842 | exports.createApp = createApp; 843 | exports.createRenderer = createRenderer; 844 | exports.createTextVnode = createTextVnode; 845 | exports.getCurrentInstance = getCurrentInstance; 846 | exports.h = h; 847 | exports.inject = inject; 848 | exports.nextTick = nextTick; 849 | exports.provide = provide; 850 | exports.proxyRefs = proxyRefs; 851 | exports.ref = ref; 852 | exports.renderSlots = renderSlots; 853 | -------------------------------------------------------------------------------- /lib/guide-mini-vue.esm.js: -------------------------------------------------------------------------------- 1 | const Fragment = Symbol("Fragment"); 2 | const Text = Symbol("Text"); 3 | function createVnode(type, props, children) { 4 | const vnode = { 5 | type, 6 | props, 7 | children, 8 | el: null, 9 | key: props && props.key, 10 | component: null, 11 | shapeFlag: getShapeFlag(type) 12 | }; 13 | if (typeof children === 'string') { 14 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 15 | } 16 | else if (Array.isArray(children)) { 17 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 18 | } 19 | if (vnode.shapeFlag & 2 /* STATEFUL_COMPONENT */) { 20 | if (typeof children === 'object') { 21 | vnode.shapeFlag |= 16 /* SLOT_CHILDREN */; 22 | } 23 | } 24 | return vnode; 25 | } 26 | function createTextVnode(text) { 27 | return createVnode(Text, {}, text); 28 | } 29 | function getShapeFlag(type) { 30 | return typeof type === "string" ? 1 /* ELEMENT */ : 2 /* STATEFUL_COMPONENT */; 31 | } 32 | 33 | function h(type, props, children) { 34 | return createVnode(type, props, children); 35 | } 36 | 37 | function renderSlots(slots, name, props) { 38 | const slot = slots[name]; 39 | if (slot) { 40 | if (typeof slot === 'function') { 41 | return createVnode(Fragment, {}, slot(props)); 42 | } 43 | } 44 | } 45 | 46 | const extend = Object.assign; 47 | const isObject = (val) => { 48 | return val !== null && typeof val === "object"; 49 | }; 50 | const hasChanged = (newValue, value) => { 51 | return !Object.is(newValue, value); 52 | }; 53 | const camelize = (str) => { 54 | return str.replace(/-(\w)/g, (_, c) => { 55 | return c ? c.toUpperCase() : ""; 56 | }); 57 | }; 58 | const capitalize = (str) => { 59 | return str.charAt(0).toUpperCase() + str.slice(1); 60 | }; 61 | const toHandlerKey = (str) => { 62 | return str ? "on" + capitalize(str) : ""; 63 | }; 64 | 65 | let activeEffect; 66 | let shouldTrack; 67 | class ReactiveEffect { 68 | constructor(fn, scheduler) { 69 | this.scheduler = scheduler; 70 | this.deps = []; 71 | this.active = true; 72 | this._fn = fn; 73 | } 74 | run() { 75 | if (!this.active) { 76 | return this._fn(); 77 | } 78 | shouldTrack = true; 79 | activeEffect = this; 80 | const result = this._fn(); 81 | shouldTrack = false; 82 | return result; 83 | } 84 | stop() { 85 | if (this.active) { 86 | cleanupEffect(this); 87 | if (this.onStop) 88 | this.onStop(); 89 | this.active = false; 90 | } 91 | } 92 | } 93 | function cleanupEffect(effect) { 94 | effect.deps.forEach((dep) => { 95 | dep.delete(effect); 96 | }); 97 | effect.deps.length = 0; 98 | } 99 | let targetMap = new Map(); 100 | function track(target, key) { 101 | //targetMap target key 102 | if (!isTracking()) 103 | return; 104 | let depsMap = targetMap.get(target); 105 | if (!depsMap) { 106 | depsMap = new Map(); 107 | targetMap.set(target, depsMap); 108 | } 109 | let deps = depsMap.get(key); 110 | if (!deps) { 111 | deps = new Set(); 112 | depsMap.set(key, deps); 113 | } 114 | trackEffects(deps); 115 | } 116 | function trackEffects(deps) { 117 | if (deps.has(activeEffect)) 118 | return; 119 | deps.add(activeEffect); 120 | //反向收集 121 | activeEffect.deps.push(deps); 122 | } 123 | function isTracking() { 124 | return shouldTrack && activeEffect !== undefined; 125 | } 126 | function trigger(target, key) { 127 | let depsMap = targetMap.get(target); 128 | if (!depsMap) 129 | return; 130 | let deps = depsMap.get(key); 131 | triggerEffect(deps); 132 | } 133 | function triggerEffect(deps) { 134 | deps && deps.forEach((effect) => { 135 | if (effect.scheduler) { 136 | effect.scheduler(); 137 | } 138 | else { 139 | effect.run(); 140 | } 141 | }); 142 | } 143 | function effect(fn, options = {}) { 144 | const _effect = new ReactiveEffect(fn, options.scheduler); 145 | extend(_effect, options); 146 | _effect.onStop = options.onStop; 147 | _effect.run(); 148 | const runner = _effect.run.bind(_effect); 149 | runner.effect = _effect; 150 | return runner; 151 | } 152 | 153 | const get = createGetter(); 154 | const set = createSetter(); 155 | const readonlyGet = createGetter(true); 156 | const shallowReadonlyGet = createGetter(true, true); 157 | function createGetter(isReadonly = false, shallow = false) { 158 | return function get(target, key) { 159 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 160 | return !isReadonly; 161 | } 162 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 163 | return isReadonly; 164 | } 165 | const res = Reflect.get(target, key); 166 | if (shallow) 167 | return res; 168 | if (isObject(res)) { 169 | return isReadonly ? readonly(res) : reactive(res); 170 | } 171 | if (!isReadonly) { 172 | track(target, key); 173 | } 174 | return res; 175 | }; 176 | } 177 | function createSetter() { 178 | return function set(target, key, value) { 179 | const res = Reflect.set(target, key, value); 180 | trigger(target, key); 181 | return res; 182 | }; 183 | } 184 | const mutableHandlers = { 185 | get, 186 | set, 187 | }; 188 | const readonlyHandlers = { 189 | get: readonlyGet, 190 | set(target, key, value) { 191 | console.warn(`key:${key} set失败了,因为target是readonly的`, target); 192 | return true; 193 | }, 194 | }; 195 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 196 | get: shallowReadonlyGet 197 | }); 198 | 199 | function reactive(raw) { 200 | return createActiveObject(raw, mutableHandlers); 201 | } 202 | function readonly(raw) { 203 | return createActiveObject(raw, readonlyHandlers); 204 | } 205 | function shallowReadonly(raw) { 206 | return createActiveObject(raw, shallowReadonlyHandlers); 207 | } 208 | function createActiveObject(target, baseHandlers) { 209 | if (!isObject(target)) { 210 | console.warn(`target ${target} 必须是一个对象`); 211 | return target; 212 | } 213 | return new Proxy(target, baseHandlers); 214 | } 215 | 216 | class RefImpl { 217 | constructor(value) { 218 | this._v_ref = true; 219 | this._rawValue = value; 220 | this._value = convert(value); 221 | this.dep = new Set(); 222 | } 223 | get value() { 224 | trackRefEffects(this); 225 | return this._value; 226 | } 227 | set value(newValue) { 228 | if (hasChanged(newValue, this._rawValue)) { 229 | this._rawValue = newValue; 230 | this._value = convert(newValue); 231 | triggerEffect(this.dep); 232 | } 233 | } 234 | } 235 | function convert(value) { 236 | return isObject(value) ? reactive(value) : value; 237 | } 238 | function trackRefEffects(ref) { 239 | if (isTracking()) { 240 | trackEffects(ref.dep); 241 | } 242 | } 243 | function ref(value) { 244 | return new RefImpl(value); 245 | } 246 | function isRef(value) { 247 | return !!value._v_ref; 248 | } 249 | function unRef(ref) { 250 | return isRef(ref) ? ref.value : ref; 251 | } 252 | function proxyRefs(objectWithRefs) { 253 | return new Proxy(objectWithRefs, { 254 | get(target, key) { 255 | return unRef(Reflect.get(target, key)); 256 | }, 257 | set(target, key, value) { 258 | if (isRef(target[key]) && !isRef(value)) { 259 | return (target[key].value = value); 260 | } 261 | else { 262 | return Reflect.set(target, key, value); 263 | } 264 | } 265 | }); 266 | } 267 | 268 | function emit(instance, event, ...args) { 269 | const { props } = instance; 270 | const HandlerName = toHandlerKey(camelize(event)); 271 | const handler = props[HandlerName]; 272 | handler && handler(...args); 273 | } 274 | 275 | function initProps(instance, rawProps = {}) { 276 | instance.props = rawProps; 277 | } 278 | 279 | const publicPropertiesMap = { 280 | $el: (i) => i.vnode.el, 281 | $slots: (i) => i.slots, 282 | $props: (i) => i.props 283 | }; 284 | const PublicInstanceProxyHandlers = { 285 | get({ _: instance }, key) { 286 | const { setupState, props } = instance; 287 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 288 | if (hasOwn(setupState, key)) { 289 | return setupState[key]; 290 | } 291 | else if (hasOwn(props, key)) { 292 | return props[key]; 293 | } 294 | const publicGetter = publicPropertiesMap[key]; 295 | if (publicGetter) { 296 | return publicGetter(instance); 297 | } 298 | }, 299 | }; 300 | 301 | function initSlots(instance, children) { 302 | const { vnode } = instance; 303 | if (vnode.shapeFlag & 16 /* SLOT_CHILDREN */) { 304 | normalizeObjectSlots(children, instance.slots); 305 | } 306 | } 307 | function normalizeSlotValue(value) { 308 | return Array.isArray(value) ? value : [value]; 309 | } 310 | function normalizeObjectSlots(children, slots) { 311 | for (const key in children) { 312 | const value = children[key]; 313 | if (typeof value === "function") { 314 | slots[key] = (props) => normalizeSlotValue(value(props)); 315 | } 316 | } 317 | } 318 | 319 | function createComponentInstance(vnode, parent) { 320 | const component = { 321 | vnode, 322 | type: vnode.type, 323 | setupState: {}, 324 | props: {}, 325 | slots: {}, 326 | provides: parent ? parent.provides : {}, 327 | parent, 328 | isMounted: false, 329 | subTree: {}, 330 | next: null, 331 | emit: () => { }, 332 | }; 333 | component.emit = emit.bind(null, component); 334 | return component; 335 | } 336 | function setupComponent(instance) { 337 | initProps(instance, instance.vnode.props); 338 | initSlots(instance, instance.vnode.children); 339 | setupStatefulComponent(instance); 340 | } 341 | function setupStatefulComponent(instance) { 342 | const Component = instance.type; 343 | instance.proxy = new Proxy({ 344 | _: instance, 345 | }, PublicInstanceProxyHandlers); 346 | const { setup } = Component; 347 | if (setup) { 348 | setCurrentInstance(instance); 349 | const setupResult = setup(shallowReadonly(instance.props), { 350 | emit: instance.emit 351 | }); 352 | handleSetupResult(instance, setupResult); 353 | setCurrentInstance(null); 354 | } 355 | } 356 | function handleSetupResult(instance, setupResult) { 357 | if (typeof setupResult === "object") { 358 | instance.setupState = proxyRefs(setupResult); 359 | } 360 | finishComponentSetup(instance); 361 | } 362 | function finishComponentSetup(instance) { 363 | const Component = instance.type; 364 | instance.render = Component.render; 365 | } 366 | let currentInstance = null; 367 | function getCurrentInstance() { 368 | return currentInstance; 369 | } 370 | function setCurrentInstance(instance) { 371 | currentInstance = instance; 372 | } 373 | 374 | function provide(key, value) { 375 | const currentInstance = getCurrentInstance(); 376 | if (currentInstance) { 377 | let { provides } = currentInstance; 378 | const parentProvides = currentInstance.parent.provides; 379 | if (provides === parentProvides) { 380 | provides = currentInstance.provides = Object.create(parentProvides); 381 | } 382 | provides[key] = value; 383 | } 384 | } 385 | function inject(key, defaultValue) { 386 | const currentInstance = getCurrentInstance(); 387 | if (currentInstance) { 388 | const parentProvides = currentInstance.parent.provides; 389 | if (parentProvides[key]) { 390 | return parentProvides[key]; 391 | } 392 | else if (defaultValue) { 393 | if (typeof defaultValue === 'function') { 394 | return defaultValue(); 395 | } 396 | return defaultValue; 397 | } 398 | } 399 | } 400 | 401 | function shouldUpdateComponent(prevVnode, nextVnode) { 402 | const { props: prevProps } = prevVnode; 403 | const { props: nextProps } = nextVnode; 404 | for (const key in nextProps) { 405 | if (nextProps[key] !== prevProps[key]) { 406 | return true; 407 | } 408 | } 409 | return false; 410 | } 411 | 412 | function createAppAPI(render) { 413 | return function createApp(rootComponent) { 414 | return { 415 | mount(rootContainer) { 416 | const vnode = createVnode(rootComponent); 417 | render(vnode, rootContainer); 418 | }, 419 | }; 420 | }; 421 | } 422 | 423 | const queue = []; 424 | let isFlushPending = false; 425 | const p = Promise.resolve(); 426 | function nextTick(fn) { 427 | return fn ? p.then(fn) : p; 428 | } 429 | function queueJobs(job) { 430 | if (!queue.includes(job)) { 431 | queue.push(job); 432 | } 433 | queueFlush(); 434 | } 435 | function queueFlush() { 436 | if (isFlushPending) 437 | return; 438 | isFlushPending = true; 439 | nextTick(flushJobs); 440 | } 441 | function flushJobs() { 442 | isFlushPending = false; 443 | let job; 444 | while ((job = queue.shift())) { 445 | job && job(); 446 | } 447 | } 448 | 449 | function createRenderer(options) { 450 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 451 | function render(vnode, container, parentComponent) { 452 | patch(null, vnode, container, parentComponent, null); 453 | } 454 | //n1 old n2 new 455 | function patch(n1, n2, container, parentComponent, anchor) { 456 | const { type, shapeFlag } = n2; 457 | switch (type) { 458 | case Fragment: 459 | processFragment(n1, n2, container, parentComponent, anchor); 460 | break; 461 | case Text: 462 | processText(n1, n2, container); 463 | break; 464 | default: 465 | if (shapeFlag & 1 /* ELEMENT */) { 466 | processElement(n1, n2, container, parentComponent, anchor); 467 | } 468 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 469 | processComponent(n1, n2, container, parentComponent, anchor); 470 | } 471 | break; 472 | } 473 | } 474 | function processElement(n1, n2, container, parentComponent, anchor) { 475 | if (!n1) { 476 | mountElement(n1, n2, container, parentComponent, anchor); 477 | } 478 | else { 479 | patchElement(n1, n2, container, parentComponent, anchor); 480 | } 481 | } 482 | function patchElement(n1, n2, container, parentComponent, anchor) { 483 | console.log("patchComponent"); 484 | console.log("n1", n1); 485 | console.log("n2", n2); 486 | const oldProps = n1.props || EMPTY_OBJ; 487 | const newProps = n2.props || EMPTY_OBJ; 488 | const el = (n2.el = n1.el); 489 | patchChildren(n1, n2, el, parentComponent, anchor); 490 | patchProps(el, oldProps, newProps); 491 | } 492 | function patchChildren(n1, n2, container, parentComponent, anchor) { 493 | const prevShapeFlag = n1.shapeFlag; 494 | const { shapeFlag } = n2; 495 | const c1 = n1.children; 496 | const c2 = n2.children; 497 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 498 | if (prevShapeFlag & 8 /* ARRAY_CHILDREN */) { 499 | //1 把n1都清空 500 | unmountChildren(n1.children); 501 | } 502 | //2 设置text 503 | if (c1 !== c2) { 504 | hostSetElementText(container, c2); 505 | } 506 | } 507 | else { 508 | //text to array 509 | if (prevShapeFlag & 4 /* TEXT_CHILDREN */) { 510 | hostSetElementText(container, ""); 511 | mountChildren(c2, container, parentComponent, anchor); 512 | } 513 | else { 514 | //array 515 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 516 | } 517 | } 518 | } 519 | function patchKeyedChildren(c1, c2, container, parentComponent, parentAnchor) { 520 | let i = 0; 521 | const l2 = c2.length; 522 | let e1 = c1.length - 1; 523 | let e2 = l2 - 1; 524 | function isSomeVNodeType(n1, n2) { 525 | return n1.type === n2.type && n1.key === n2.key; 526 | } 527 | //左侧 528 | while (i <= e1 && i <= e2) { 529 | const n1 = c1[i]; 530 | const n2 = c2[i]; 531 | if (isSomeVNodeType(n1, n2)) { 532 | patch(n1, n2, container, parentComponent, parentAnchor); 533 | } 534 | else { 535 | break; 536 | } 537 | i++; 538 | } 539 | //右侧 540 | while (i <= e1 && i <= e2) { 541 | const n1 = c1[e1]; 542 | const n2 = c2[e2]; 543 | if (isSomeVNodeType(n1, n2)) { 544 | patch(n1, n2, container, parentComponent, parentAnchor); 545 | } 546 | else { 547 | break; 548 | } 549 | e1--; 550 | e2--; 551 | } 552 | //新的比老的多 创建 553 | if (i > e1) { 554 | if (i <= e2) { 555 | const nextPos = e2 + 1; 556 | const anchor = e2 + 1 < l2 ? c2[nextPos].el : null; 557 | while (i <= e2) { 558 | patch(null, c2[i], container, parentComponent, anchor); 559 | i++; 560 | } 561 | } 562 | } 563 | else if (i > e2) { 564 | while (i <= e1) { 565 | hostRemove(c1[i].el); 566 | i++; 567 | } 568 | } 569 | else { 570 | let s1 = i; 571 | let s2 = i; 572 | const toBePatched = e2 - s2 + 1; 573 | let patched = 0; 574 | const keyToNewIndexMap = new Map(); 575 | const newIndexToOldIndexMap = new Array(toBePatched); 576 | let moved = false; 577 | let maxNewIndexSoFar = 0; 578 | for (let i = 0; i < toBePatched; i++) 579 | newIndexToOldIndexMap[i] = 0; 580 | for (let i = s2; i <= e2; i++) { 581 | const nextChild = c2[i]; 582 | keyToNewIndexMap.set(nextChild.key, i); 583 | } 584 | for (let i = s1; i <= e1; i++) { 585 | const prevChild = c1[i]; 586 | if (patched >= toBePatched) { 587 | hostRemove(prevChild.el); 588 | continue; 589 | } 590 | let newIndex; 591 | if (prevChild.key != null) { 592 | newIndex = keyToNewIndexMap.get(prevChild.key); 593 | } 594 | else { 595 | for (let j = s2; j <= e2; j++) { 596 | if (isSomeVNodeType(prevChild, c2[j])) { 597 | newIndex = j; 598 | break; 599 | } 600 | } 601 | } 602 | if (newIndex === undefined) { 603 | hostRemove(prevChild.el); 604 | } 605 | else { 606 | if (newIndex >= maxNewIndexSoFar) { 607 | maxNewIndexSoFar = newIndex; 608 | } 609 | else { 610 | moved = true; 611 | } 612 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 613 | patch(prevChild, c2[newIndex], container, parentComponent, null); 614 | patched++; 615 | } 616 | } 617 | const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []; 618 | let j = increasingNewIndexSequence.length - 1; 619 | for (let i = toBePatched - 1; i >= 0; i--) { 620 | const nextIndex = i + s2; 621 | const nextChild = c2[nextIndex]; 622 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 623 | if (newIndexToOldIndexMap[i] === 0) { 624 | patch(null, nextChild, container, parentComponent, anchor); 625 | } 626 | else if (moved) { 627 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 628 | console.log("移动位置"); 629 | hostInsert(nextChild.el, container, anchor); 630 | } 631 | else { 632 | j--; 633 | } 634 | } 635 | } 636 | } 637 | } 638 | function getSequence(arr) { 639 | const p = arr.slice(); 640 | const result = [0]; 641 | let i, j, u, v, c; 642 | const len = arr.length; 643 | for (i = 0; i < len; i++) { 644 | const arrI = arr[i]; 645 | if (arrI !== 0) { 646 | j = result[result.length - 1]; 647 | if (arr[j] < arrI) { 648 | p[i] = j; 649 | result.push(i); 650 | continue; 651 | } 652 | u = 0; 653 | v = result.length - 1; 654 | while (u < v) { 655 | c = (u + v) >> 1; 656 | if (arr[result[c]] < arrI) { 657 | u = c + 1; 658 | } 659 | else { 660 | v = c; 661 | } 662 | } 663 | if (arrI < arr[result[u]]) { 664 | if (u > 0) { 665 | p[i] = result[u - 1]; 666 | } 667 | result[u] = i; 668 | } 669 | } 670 | } 671 | u = result.length; 672 | v = result[u - 1]; 673 | while (u-- > 0) { 674 | result[u] = v; 675 | v = p[v]; 676 | } 677 | return result; 678 | } 679 | function unmountChildren(children) { 680 | for (let i = 0; i < children.length; i++) { 681 | const el = children[i].el; 682 | hostRemove(el); 683 | } 684 | } 685 | const EMPTY_OBJ = {}; 686 | function patchProps(el, oldProps, newProps) { 687 | if (oldProps !== newProps) { 688 | for (let key in newProps) { 689 | const prevProp = oldProps[key]; 690 | const nextProp = newProps[key]; 691 | if (prevProp !== nextProp) { 692 | hostPatchProp(el, key, prevProp, nextProp); 693 | } 694 | } 695 | if (oldProps !== EMPTY_OBJ) 696 | for (let key in oldProps) { 697 | if (!(key in newProps)) { 698 | hostPatchProp(el, key, oldProps[key], null); 699 | } 700 | } 701 | } 702 | } 703 | function mountElement(n1, n2, container, parentComponent, anchor) { 704 | const el = (n2.el = hostCreateElement(n2.type)); 705 | const { children, shapeFlag } = n2; 706 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 707 | el.textContent = children; 708 | } 709 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 710 | mountChildren(n2.children, el, parentComponent, anchor); 711 | } 712 | const { props } = n2; 713 | for (const key in props) { 714 | const val = props[key]; 715 | hostPatchProp(el, key, null, val); 716 | } 717 | hostInsert(el, container, anchor); 718 | } 719 | function processFragment(n1, n2, container, parentComponent, anchor) { 720 | mountChildren(n2.children, container, parentComponent, anchor); 721 | } 722 | function processText(n1, n2, container) { 723 | const { children } = n2; 724 | const textNode = (n2.el = document.createTextNode(children)); 725 | container.append(textNode); 726 | } 727 | function mountChildren(children, container, parentComponent, anchor) { 728 | children.forEach((v) => { 729 | patch(null, v, container, parentComponent, anchor); 730 | }); 731 | } 732 | function processComponent(n1, n2, container, parentComponent, anchor) { 733 | if (!n1) { 734 | mountComponent(n2, container, parentComponent, anchor); 735 | } 736 | else { 737 | updateComponent(n1, n2); 738 | } 739 | } 740 | function updateComponent(n1, n2) { 741 | const instance = (n2.component = n1.component); 742 | if (shouldUpdateComponent(n1, n2)) { 743 | instance.next = n2; 744 | instance.update(); 745 | } 746 | else { 747 | n2.component = n1.component; 748 | n2.el = n1.el; 749 | instance.vnode = n2; 750 | } 751 | } 752 | function mountComponent(initialVnode, container, parentComponent, anchor) { 753 | const instance = (initialVnode.component = createComponentInstance(initialVnode, parentComponent)); 754 | setupComponent(instance); 755 | setupRenderEffect(instance, initialVnode, container, anchor); 756 | } 757 | function setupRenderEffect(instance, initialVnode, container, anchor) { 758 | instance.update = effect(() => { 759 | if (!instance.isMounted) { 760 | console.log("init"); 761 | const { proxy } = instance; 762 | const subTree = (instance.subTree = instance.render.call(proxy)); 763 | patch(null, subTree, container, instance, anchor); 764 | initialVnode.el = subTree.el; 765 | instance.isMounted = true; 766 | } 767 | else { 768 | console.log("update"); 769 | const { next, vnode } = instance; 770 | if (next) { 771 | next.el = vnode.el; 772 | updateComponentPreRender(instance, next); 773 | } 774 | const { proxy } = instance; 775 | const subTree = instance.render.call(proxy); 776 | const prevSubTree = instance.subTree; 777 | instance.subTree = subTree; 778 | patch(prevSubTree, subTree, container, instance, anchor); 779 | } 780 | }, { 781 | scheduler() { 782 | console.log('update--scheduler'); 783 | queueJobs(instance.update); 784 | } 785 | }); 786 | } 787 | return { 788 | createApp: createAppAPI(render) 789 | }; 790 | } 791 | function updateComponentPreRender(instance, nextVnode) { 792 | instance.vnode = nextVnode; 793 | instance.next = null; 794 | instance.props = nextVnode.props; 795 | } 796 | 797 | function createElement(type) { 798 | return document.createElement(type); 799 | } 800 | function patchProp(el, key, oldVal, newVal) { 801 | const isOn = (key) => /^on[A-Z]/.test(key); 802 | if (isOn(key)) { 803 | const event = key.slice(2).toLowerCase(); 804 | el.addEventListener(event, newVal); 805 | } 806 | else { 807 | if (newVal === undefined || newVal === null) { 808 | el.removeAttribute(key, newVal); 809 | } 810 | else { 811 | el.setAttribute(key, newVal); 812 | } 813 | } 814 | } 815 | function insert(child, parent, anchor) { 816 | parent.insertBefore(child, anchor || null); 817 | } 818 | function remove(child) { 819 | const parent = child.parentNode; 820 | if (parent) { 821 | parent.removeChild(child); 822 | } 823 | } 824 | function setElementText(container, text) { 825 | container.textContent = text; 826 | } 827 | const render = createRenderer({ 828 | createElement, 829 | patchProp, 830 | insert, 831 | remove, 832 | setElementText 833 | }); 834 | function createApp(...args) { 835 | return render.createApp(...args); 836 | } 837 | 838 | export { createApp, createRenderer, createTextVnode, getCurrentInstance, h, inject, nextTick, provide, proxyRefs, ref, renderSlots }; 839 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GUIDE-MINI-VUE", 3 | "version": "1.0.0", 4 | "main": "lib/guide-mini-vue.cjs.js", 5 | "module":"lib/guide-mini-vue.esm.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "jest", 9 | "build": "rollup -c rollup.config.js" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.17.7", 13 | "@babel/preset-env": "^7.16.11", 14 | "@babel/preset-typescript": "^7.16.7", 15 | "@rollup/plugin-typescript": "^8.3.1", 16 | "@types/jest": "^27.4.1", 17 | "babel-jest": "^27.5.1", 18 | "jest": "^27.5.1", 19 | "rollup": "^2.70.1", 20 | "tslib": "^2.3.1", 21 | "typescript": "^4.6.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript 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:[ 16 | typescript() 17 | ] 18 | } -------------------------------------------------------------------------------- /src/compiler-core/src/ast.ts: -------------------------------------------------------------------------------- 1 | export const enum NodeTypes{ 2 | INTERPOLATION, 3 | SIMPLE_EXPRESSION, 4 | ELEMENT, 5 | TEXT 6 | } -------------------------------------------------------------------------------- /src/compiler-core/src/codegen.ts: -------------------------------------------------------------------------------- 1 | export function generate(ast){ 2 | const context = createCodegenContext(); 3 | const { push } = context; 4 | push(`return `); 5 | const functionName = "render"; 6 | const args = ["_ctx", "_cache"]; 7 | const signature = args.join(","); 8 | push(`function ${functionName} (${signature}){`); 9 | push(`return `); 10 | genNode(ast.codegenNode, context); 11 | push(`}`); 12 | return { 13 | code:context.code, 14 | }; 15 | } 16 | function createCodegenContext(){ 17 | const context = { 18 | code:"", 19 | push(source){ 20 | context.code += source; 21 | } 22 | } 23 | return context; 24 | } 25 | function genNode(node,context){ 26 | const {push} = context; 27 | push(`'${node.content}'`); 28 | } -------------------------------------------------------------------------------- /src/compiler-core/src/parse.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "./ast"; 2 | 3 | const enum TagType{ 4 | Start, 5 | End 6 | } 7 | 8 | export function baseParse(content:string){ 9 | const context = createParserContext(content); 10 | return createRoot(parseChildren(context,[])); 11 | } 12 | function parseChildren(context,ancestors){ 13 | const nodes:any = []; 14 | while(!isEnd(context,ancestors)){ 15 | let node; 16 | const s = context.source; 17 | if(s.startsWith("{{")){ 18 | node = parseInterpolation(context); 19 | }else if(s[0]==='<'){ 20 | if(/[a-z]/i.test(s[1])){ 21 | node = parseElement(context,ancestors); 22 | } 23 | } 24 | if(!node){ 25 | console.log('parse text'); 26 | node = parseText(context); 27 | } 28 | nodes.push(node); 29 | } 30 | return nodes; 31 | } 32 | function isEnd(context,ancestors){ 33 | const s = context.source; 34 | if(s.startsWith("=0;i--){ 36 | const tag = ancestors[i].tag; 37 | if(startsWithEndTagOpen(s,tag)){ 38 | return true; 39 | } 40 | } 41 | } 42 | return !s; 43 | } 44 | function parseText(context:any){ 45 | let endIndex = context.source.length; 46 | let endTokens = ["<","{{"]; 47 | for(let i = 0;i < endTokens.length;i++){ 48 | const index = context.source.indexOf(endTokens[i]); 49 | if(index!==-1 && endIndex > index){ 50 | endIndex = index; 51 | } 52 | } 53 | const content = parseTextData(context,endIndex); 54 | console.log('content---',content); 55 | return{ 56 | type:NodeTypes.TEXT, 57 | content 58 | } 59 | } 60 | function parseTextData(context:any,length){ 61 | const content = context.source.slice(0,length); 62 | advanceBy(context,length); 63 | return content; 64 | } 65 | function parseElement(context:any,ancestors){ 66 | const element:any = parseTag(context,TagType.Start); 67 | ancestors.push(element); 68 | element.children = parseChildren(context,ancestors); 69 | ancestors.pop(); 70 | if(startsWithEndTagOpen(context.source,element.tag)){ 71 | parseTag(context,TagType.End); 72 | }else{ 73 | throw new Error(`缺少结束标签${element.tag}`); 74 | } 75 | return element; 76 | } 77 | function startsWithEndTagOpen(source,tag){ 78 | return source.slice(2,2+tag.length).toLowerCase() === tag.toLowerCase(); 79 | } 80 | function parseTag(context:any,type:TagType){ 81 | const match:any = /^<\/?([a-z]*)/i.exec(context.source); 82 | const tag = match[1]; 83 | advanceBy(context,match[0].length); 84 | advanceBy(context,1); 85 | if(type === TagType.End) return; 86 | return { 87 | type:NodeTypes.ELEMENT, 88 | tag 89 | } 90 | } 91 | function parseInterpolation(context){ 92 | const openDelimiter = "{{"; 93 | const closeDelimiter = "}}"; 94 | const closeIndex = context.source.indexOf(closeDelimiter,openDelimiter.length); 95 | advanceBy(context,openDelimiter.length); 96 | const rawContentLength = closeIndex-openDelimiter.length; 97 | const rawcontent = parseTextData(context,rawContentLength); 98 | const content = rawcontent.trim(); 99 | advanceBy(context,closeDelimiter.length); 100 | return { 101 | type:NodeTypes.INTERPOLATION, 102 | content:{ 103 | type:NodeTypes.SIMPLE_EXPRESSION, 104 | content 105 | } 106 | } 107 | } 108 | function advanceBy(context:any,length:number){ 109 | context.source = context.source.slice(length); 110 | } 111 | function createRoot(children){ 112 | return { 113 | children 114 | } 115 | } 116 | function createParserContext(content){ 117 | return { 118 | source:content 119 | } 120 | } -------------------------------------------------------------------------------- /src/compiler-core/src/transform.ts: -------------------------------------------------------------------------------- 1 | export function transform(root,options = {}){ 2 | //深度优先遍历 3 | const context = createTransformContext(root, options); 4 | traverseNode(root, context); 5 | createRootCodegen(root); 6 | } 7 | function createRootCodegen(root){ 8 | root.codegenNode = root.children[0]; 9 | } 10 | function createTransformContext(root, options) { 11 | const context = { 12 | root, 13 | nodeTransforms: options.nodeTransforms||[] 14 | }; 15 | return context; 16 | } 17 | function traverseNode(node:any,context){ 18 | const nodeTransforms = context.nodeTransforms; 19 | for(let i = 0;i{ 5 | it("string",()=>{ 6 | const ast = baseParse("hi"); 7 | transform(ast) 8 | const {code} = generate(ast); 9 | expect(code).toMatchSnapshot(); 10 | }) 11 | it("interpolation",()=>{ 12 | 13 | }) 14 | }) -------------------------------------------------------------------------------- /src/compiler-core/tests/parse.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../src/ast'; 2 | import {baseParse} from '../src/parse'; 3 | describe("Parse",()=>{ 4 | describe("interpolation",()=>{ 5 | test("simple interpolation",()=>{ 6 | const ast = baseParse("{{ message }}"); 7 | expect(ast.children[0]).toStrictEqual({ 8 | type:NodeTypes.INTERPOLATION, 9 | content:{ 10 | type:NodeTypes.SIMPLE_EXPRESSION, 11 | content:"message" 12 | } 13 | }) 14 | }) 15 | }) 16 | }) 17 | describe("element",()=>{ 18 | it("simple element div",()=>{ 19 | const ast = baseParse("
"); 20 | expect(ast.children[0]).toStrictEqual({ 21 | type:NodeTypes.ELEMENT, 22 | tag:"div", 23 | children:[] 24 | }) 25 | }) 26 | }) 27 | describe("element",()=>{ 28 | it("simple text",()=>{ 29 | const ast = baseParse("some text"); 30 | expect(ast.children[0]).toStrictEqual({ 31 | type:NodeTypes.TEXT, 32 | content:"some text" 33 | }) 34 | }) 35 | }) 36 | test("hello world",()=>{ 37 | const ast = baseParse("

hi,{{message}}

"); 38 | expect(ast.children[0]).toStrictEqual({ 39 | type:NodeTypes.ELEMENT, 40 | tag:"p", 41 | children:[ 42 | { 43 | type:NodeTypes.TEXT, 44 | content:"hi," 45 | }, 46 | { 47 | type:NodeTypes.INTERPOLATION, 48 | content:{ 49 | type:NodeTypes.SIMPLE_EXPRESSION, 50 | content:"message" 51 | } 52 | } 53 | ] 54 | }) 55 | }) 56 | test("Nested element",()=>{ 57 | const ast = baseParse("

hi

{{message}}
"); 58 | expect(ast.children[0]).toStrictEqual({ 59 | type:NodeTypes.ELEMENT, 60 | tag:"div", 61 | children:[ 62 | { 63 | type:NodeTypes.ELEMENT, 64 | tag:"p", 65 | children:[ 66 | { 67 | type:NodeTypes.TEXT, 68 | content:"hi", 69 | } 70 | ] 71 | }, 72 | { 73 | type:NodeTypes.INTERPOLATION, 74 | content:{ 75 | type:NodeTypes.SIMPLE_EXPRESSION, 76 | content:"message" 77 | } 78 | } 79 | ] 80 | }) 81 | }) 82 | test("should throw error when lack end tag",()=>{ 83 | expect(()=>{ 84 | baseParse("
") 85 | }).toThrow('缺少结束标签span'); 86 | }) -------------------------------------------------------------------------------- /src/compiler-core/tests/transform.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from "../src/ast"; 2 | import { baseParse } from "../src/parse"; 3 | import {transform} from '../src/transform'; 4 | 5 | describe("transform",()=>{ 6 | it("happy path",()=>{ 7 | const ast = baseParse("
hi,{{message}}
"); 8 | const plugin = (node)=>{ 9 | if (node.type === NodeTypes.TEXT) { 10 | node.content = node.content + "mini-vue"; 11 | } 12 | } 13 | transform(ast,{ 14 | nodeTransforms:[plugin] 15 | }); 16 | const nodeText = ast.children[0].children[0]; 17 | expect(nodeText.content).toBe("hi,mini-vue") 18 | }) 19 | }) -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './runtime-dom'; 2 | export * from './reactivity'; -------------------------------------------------------------------------------- /src/reactivity/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { extend, isObject } from "../shared"; 2 | import { track, trigger } from "./effect"; 3 | import { reactive, ReactiveFlags, readonly } from "./reactive"; 4 | const get = createGetter(); 5 | const set = createSetter(); 6 | const readonlyGet = createGetter(true); 7 | const shallowReadonlyGet = createGetter(true,true); 8 | function createGetter(isReadonly = false,shallow = false) { 9 | return function get(target, key) { 10 | if (key === ReactiveFlags.IS_REACTIVE) { 11 | return !isReadonly; 12 | }else if(key === ReactiveFlags.IS_READONLY){ 13 | return isReadonly; 14 | } 15 | const res = Reflect.get(target, key); 16 | if(shallow) return res; 17 | if (isObject(res)) { 18 | return isReadonly ? readonly(res) : reactive(res); 19 | } 20 | if (!isReadonly) { 21 | track(target, key); 22 | } 23 | return res; 24 | }; 25 | } 26 | function createSetter() { 27 | return function set(target, key, value) { 28 | const res = Reflect.set(target, key, value); 29 | trigger(target, key); 30 | return res; 31 | }; 32 | } 33 | 34 | export const mutableHandlers = { 35 | get, 36 | set, 37 | }; 38 | export const readonlyHandlers = { 39 | get: readonlyGet, 40 | set(target, key, value) { 41 | console.warn(`key:${key} set失败了,因为target是readonly的`,target) 42 | return true; 43 | }, 44 | }; 45 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 46 | get: shallowReadonlyGet 47 | }); -------------------------------------------------------------------------------- /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: any){ 9 | this._getter = getter; 10 | this._effect = new ReactiveEffect(getter,()=>{ 11 | if (!this._dirty) this._dirty = true; 12 | }); 13 | } 14 | get value(){ 15 | if(this._dirty){ 16 | this._dirty = false; 17 | this._value = this._effect.run(); 18 | } 19 | return this._value; 20 | } 21 | } 22 | export function computed(getter){ 23 | return new ComputedRefImpl(getter); 24 | } -------------------------------------------------------------------------------- /src/reactivity/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from "../shared/index"; 2 | 3 | let activeEffect: any; 4 | let shouldTrack; 5 | export class ReactiveEffect{ 6 | private _fn: any; 7 | deps = []; 8 | active = true; 9 | onStop?:()=>void; 10 | constructor(fn: any,public scheduler?: any){ 11 | this._fn = fn; 12 | } 13 | run(){ 14 | if(!this.active){ 15 | return this._fn(); 16 | } 17 | shouldTrack = true; 18 | 19 | activeEffect = this; 20 | const result = this._fn(); 21 | 22 | shouldTrack = false; 23 | return result; 24 | } 25 | stop(){ 26 | if(this.active){ 27 | cleanupEffect(this) 28 | if(this.onStop) this.onStop(); 29 | this.active = false; 30 | } 31 | } 32 | } 33 | function cleanupEffect(effect){ 34 | effect.deps.forEach((dep:any)=>{ 35 | dep.delete(effect) 36 | }) 37 | effect.deps.length = 0; 38 | } 39 | let targetMap = new Map(); 40 | export function track(target: any, key: any) { 41 | //targetMap target key 42 | if(!isTracking()) return; 43 | let depsMap = targetMap.get(target); 44 | if (!depsMap) { 45 | depsMap = new Map(); 46 | targetMap.set(target, depsMap); 47 | } 48 | let deps = depsMap.get(key); 49 | if (!deps) { 50 | deps = new Set(); 51 | depsMap.set(key, deps); 52 | } 53 | trackEffects(deps); 54 | } 55 | export function trackEffects(deps){ 56 | if (deps.has(activeEffect)) return; 57 | deps.add(activeEffect); 58 | //反向收集 59 | activeEffect.deps.push(deps); 60 | } 61 | export function isTracking() { 62 | return shouldTrack && activeEffect!==undefined; 63 | } 64 | export function trigger(target:any,key:any){ 65 | let depsMap = targetMap.get(target); 66 | if(!depsMap) return; 67 | let deps = depsMap.get(key); 68 | triggerEffect(deps); 69 | } 70 | export function triggerEffect(deps){ 71 | deps && deps.forEach((effect)=>{ 72 | if(effect.scheduler){ 73 | effect.scheduler() 74 | }else{ 75 | effect.run() 76 | } 77 | }) 78 | } 79 | export function effect(fn: any,options:any = {}){ 80 | const _effect = new ReactiveEffect(fn,options.scheduler); 81 | extend(_effect,options); 82 | _effect.onStop = options.onStop; 83 | _effect.run(); 84 | const runner:any = _effect.run.bind(_effect); 85 | runner.effect = _effect; 86 | return runner; 87 | } 88 | export function stop(runner){ 89 | runner.effect.stop() 90 | } -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export { ref, proxyRefs } from "./ref"; -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from './../shared/index'; 2 | import { mutableHandlers, readonlyHandlers,shallowReadonlyHandlers } from "./baseHandlers"; 3 | export const enum ReactiveFlags{ 4 | IS_REACTIVE='__v_isReactive', 5 | IS_READONLY='__v_isReadonly' 6 | } 7 | export function reactive(raw: any) { 8 | return createActiveObject(raw,mutableHandlers); 9 | } 10 | export function readonly(raw:any){ 11 | return createActiveObject(raw, readonlyHandlers); 12 | } 13 | export function shallowReadonly(raw:any){ 14 | return createActiveObject(raw, shallowReadonlyHandlers); 15 | } 16 | export function isReactive(value){ 17 | return !!value[ReactiveFlags.IS_REACTIVE]; 18 | } 19 | export function isReadonly(value){ 20 | return !!value[ReactiveFlags.IS_READONLY]; 21 | } 22 | export function isProxy(value){ 23 | return isReactive(value) || isReadonly(value); 24 | } 25 | function createActiveObject(target: any,baseHandlers) { 26 | if (!isObject(target)) { 27 | console.warn(`target ${target} 必须是一个对象`); 28 | return target; 29 | } 30 | return new Proxy(target, baseHandlers); 31 | } 32 | -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged, isObject } from "../shared"; 2 | import { isTracking, trackEffects, triggerEffect } from "./effect"; 3 | import { reactive } from "./reactive"; 4 | 5 | class RefImpl{ 6 | private _value:any; 7 | public dep; 8 | private _rawValue: any; 9 | private _v_ref: boolean; 10 | constructor(value){ 11 | this._v_ref = true; 12 | this._rawValue = value; 13 | this._value = convert(value); 14 | this.dep = new Set(); 15 | } 16 | get value(){ 17 | trackRefEffects(this); 18 | return this._value; 19 | } 20 | set value(newValue){ 21 | if (hasChanged(newValue, this._rawValue)){ 22 | this._rawValue = newValue; 23 | this._value = convert(newValue); 24 | triggerEffect(this.dep); 25 | } 26 | } 27 | } 28 | function convert(value){ 29 | return isObject(value) ? reactive(value) : value; 30 | } 31 | function trackRefEffects(ref){ 32 | if (isTracking()) { 33 | trackEffects(ref.dep); 34 | } 35 | } 36 | export function ref(value){ 37 | return new RefImpl(value); 38 | } 39 | export function isRef(value){ 40 | return !!value._v_ref; 41 | } 42 | export function unRef(ref){ 43 | return isRef(ref) ? ref.value: ref; 44 | } 45 | export function proxyRefs(objectWithRefs){ 46 | return new Proxy(objectWithRefs,{ 47 | get(target,key){ 48 | return unRef(Reflect.get(target,key)); 49 | }, 50 | set(target,key,value){ 51 | if(isRef(target[key]) && !isRef(value)){ 52 | return (target[key].value = value); 53 | }else{ 54 | return Reflect.set(target, key, value); 55 | } 56 | } 57 | }); 58 | } -------------------------------------------------------------------------------- /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 | it("should computed lazily",()=>{ 15 | const value = reactive({ 16 | foo:1 17 | }) 18 | const getter = jest.fn(()=>{ 19 | return value.foo; 20 | }) 21 | const cValue = computed(getter); 22 | //lazy 23 | expect(getter).not.toHaveBeenCalled(); 24 | expect(cValue.value).toBe(1); 25 | expect(getter).toHaveBeenCalledTimes(1); 26 | //触发get 只调用一次 27 | cValue.value; 28 | expect(getter).toHaveBeenCalledTimes(1); 29 | 30 | value.foo = 2; 31 | expect(getter).toHaveBeenCalledTimes(1); 32 | 33 | expect(cValue.value).toBe(2); 34 | expect(getter).toHaveBeenCalledTimes(2); 35 | 36 | cValue.value; 37 | expect(getter).toHaveBeenCalledTimes(2); 38 | }) 39 | }) -------------------------------------------------------------------------------- /src/reactivity/tests/effect.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 | expect(nextAge).toBe(11); 13 | user.age++; 14 | expect(nextAge).toBe(12); 15 | }) 16 | it('should return runner when call effect',()=>{ 17 | let foo = 10; 18 | const runner = effect(()=>{ 19 | foo++; 20 | return 'foo'; 21 | }) 22 | expect(foo).toBe(11); 23 | const r = runner(); 24 | expect(foo).toBe(12); 25 | expect(r).toBe('foo'); 26 | }) 27 | it('scheduler',()=>{ 28 | let dummy; 29 | let run:any; 30 | const scheduler = jest.fn(()=>{ 31 | run = runner; 32 | }) 33 | const obj = reactive({foo:1}); 34 | const runner = effect( 35 | ()=>{ 36 | dummy = obj.foo; 37 | },{ 38 | scheduler 39 | } 40 | ) 41 | expect(scheduler).not.toHaveBeenCalled(); 42 | expect(dummy).toBe(1); 43 | obj.foo++; 44 | expect(dummy).toBe(1); 45 | expect(scheduler).toHaveBeenCalledTimes(1); 46 | run(); 47 | expect(dummy).toBe(2); 48 | }) 49 | it("stop",()=>{ 50 | let dummy; 51 | const obj = reactive({prop:1}); 52 | const runner = effect(()=>{ 53 | dummy = obj.prop; 54 | }) 55 | obj.prop = 2; 56 | expect(dummy).toBe(2); 57 | stop(runner); 58 | // obj.prop = 3; 59 | //get set obj.prop = obj.prop+1; 60 | obj.prop++; 61 | expect(dummy).toBe(2); 62 | runner(); 63 | expect(dummy).toBe(3); 64 | }) 65 | it('onStop',()=>{ 66 | const obj = reactive({foo:1}); 67 | let dummy; 68 | const onStop = jest.fn(); 69 | const runner = effect( 70 | ()=>{ 71 | dummy = obj.foo; 72 | },{ 73 | onStop 74 | } 75 | ) 76 | stop(runner); 77 | expect(onStop).toBeCalledTimes(1); 78 | }) 79 | }) -------------------------------------------------------------------------------- /src/reactivity/tests/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import {isProxy, isReactive, reactive} from '../reactive' 2 | describe('reactive',()=>{ 3 | it('happy path',()=>{ 4 | const original = {foo:1}; 5 | const observed = reactive(original); 6 | expect(observed).not.toBe(original); 7 | expect(observed.foo).toBe(1); 8 | expect(isReactive(observed)).toBe(true); 9 | expect(isReactive(original)).toBe(false); 10 | expect(isProxy(observed)).toBe(true); 11 | }) 12 | test('nested reactive',()=>{ 13 | const original = { 14 | nested:{ 15 | foo:1 16 | }, 17 | array:[{bar:2}] 18 | } 19 | const observed = reactive(original); 20 | expect(isReactive(observed.nested)).toBe(true); 21 | expect(isReactive(observed.array)).toBe(true); 22 | expect(isReactive(observed.array[0])).toBe(true); 23 | }) 24 | }) -------------------------------------------------------------------------------- /src/reactivity/tests/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReadonly, readonly } from "../reactive"; 2 | 3 | describe('readonly',()=>{ 4 | it('happy path',()=>{ 5 | const original = {foo:1,bar:{baz:2}}; 6 | const wrapped = readonly(original); 7 | expect(isReadonly(wrapped)).toBe(true); 8 | expect(isReadonly(original)).toBe(false); 9 | expect(isReadonly(wrapped.bar)).toBe(true); 10 | expect(wrapped).not.toBe(original); 11 | expect(isProxy(wrapped)).toBe(true); 12 | expect(wrapped.foo).toBe(1); 13 | }) 14 | it('warn then call set',()=>{ 15 | console.warn = jest.fn(); 16 | const user = readonly({age:10}); 17 | user.age = 11; 18 | expect(console.warn).toBeCalled(); 19 | }) 20 | }) -------------------------------------------------------------------------------- /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 | const a = ref(1); 8 | expect(a.value).toBe(1); 9 | }) 10 | it("should be reactive",()=>{ 11 | const a = ref(1); 12 | let dummy; 13 | let calls = 0; 14 | effect(()=>{ 15 | calls++; 16 | dummy = a.value; 17 | }) 18 | expect(calls).toBe(1); 19 | expect(dummy).toBe(1); 20 | a.value = 2; 21 | expect(calls).toBe(2); 22 | expect(dummy).toBe(2); 23 | // same value should not trigger 24 | a.value = 2; 25 | expect(calls).toBe(2); 26 | expect(dummy).toBe(2); 27 | }) 28 | it("should make nested properties reactive",()=>{ 29 | const a = ref({ 30 | count:1 31 | }); 32 | let dummy; 33 | effect(()=>{ 34 | dummy = a.value.count; 35 | }) 36 | expect(dummy).toBe(1); 37 | a.value.count = 2; 38 | expect(dummy).toBe(2); 39 | }) 40 | it("isRef",()=>{ 41 | const a = ref(1); 42 | const user = reactive({age:1}); 43 | expect(isRef(a)).toBe(true); 44 | expect(isRef(1)).toBe(false); 45 | expect(isRef(user)).toBe(false); 46 | }) 47 | it("unRef", () => { 48 | const a = ref(1); 49 | expect(unRef(a)).toBe(1); 50 | expect(unRef(1)).toBe(1); 51 | }); 52 | it("proxyRefs",()=>{ 53 | const user = { 54 | age:ref(10), 55 | name:"xiaohong" 56 | } 57 | const proxyUser = proxyRefs(user); 58 | expect(user.age.value).toBe(10); 59 | expect(proxyUser.age).toBe(10); 60 | expect(proxyUser.name).toBe("xiaohong"); 61 | proxyUser.age = 20; 62 | expect(proxyUser.age).toBe(20); 63 | expect(user.age.value).toBe(20); 64 | 65 | proxyUser.age = ref(10); 66 | expect(proxyUser.age).toBe(10); 67 | expect(user.age.value).toBe(10); 68 | 69 | }) 70 | }) -------------------------------------------------------------------------------- /src/reactivity/tests/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, shallowReadonly } from "../reactive"; 2 | 3 | describe('shallowReadonly',()=>{ 4 | test("should not make non-reactive properties reactive",()=>{ 5 | const props = shallowReadonly({n:1,foo:1}); 6 | expect(isReadonly(props)).toBe(true); 7 | expect(isReadonly(props.n)).toBe(false); 8 | }) 9 | it("should call console.warn when set",()=>{ 10 | console.warn = jest.fn(); 11 | const user = shallowReadonly({ 12 | age:10 13 | }) 14 | user.age = 11; 15 | expect(console.warn).toHaveBeenCalled(); 16 | }) 17 | }) -------------------------------------------------------------------------------- /src/runtime-core/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from "./component"; 2 | export function provide(key,value){ 3 | const currentInstance:any = getCurrentInstance(); 4 | if(currentInstance){ 5 | let {provides} = currentInstance; 6 | const parentProvides = currentInstance.parent.provides; 7 | if(provides===parentProvides){ 8 | provides = currentInstance.provides = Object.create(parentProvides); 9 | } 10 | provides[key] = value; 11 | } 12 | } 13 | export function inject(key,defaultValue){ 14 | const currentInstance:any = getCurrentInstance(); 15 | if (currentInstance) { 16 | const parentProvides = currentInstance.parent.provides; 17 | if (parentProvides[key]){ 18 | return parentProvides[key]; 19 | } else if(defaultValue){ 20 | if(typeof defaultValue === 'function'){ 21 | return defaultValue() 22 | } 23 | return defaultValue; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import { proxyRefs } from "../reactivity"; 2 | import { shallowReadonly } from "../reactivity/reactive"; 3 | import { emit } from "./componentEmit"; 4 | import { initProps } from "./componentProps"; 5 | import { PublicInstanceProxyHandlers } from "./componentPublicInstance"; 6 | import { initSlots } from "./componentSlots"; 7 | 8 | export function createComponentInstance(vnode,parent){ 9 | const component = { 10 | vnode, 11 | type: vnode.type, 12 | setupState: {}, 13 | props: {}, 14 | slots: {}, 15 | provides: parent ? parent.provides : {}, 16 | parent, 17 | isMounted: false, 18 | subTree:{}, 19 | next:null, 20 | emit: () => {}, 21 | }; 22 | component.emit = emit.bind(null,component) as any; 23 | return component; 24 | } 25 | export function setupComponent(instance){ 26 | initProps(instance,instance.vnode.props); 27 | initSlots(instance,instance.vnode.children); 28 | setupStatefulComponent(instance); 29 | } 30 | function setupStatefulComponent(instance) { 31 | const Component = instance.type; 32 | instance.proxy = new Proxy( 33 | { 34 | _: instance, 35 | }, 36 | PublicInstanceProxyHandlers 37 | ); 38 | const { setup } = Component; 39 | if(setup){ 40 | setCurrentInstance(instance); 41 | const setupResult = setup(shallowReadonly(instance.props),{ 42 | emit:instance.emit 43 | }); 44 | handleSetupResult(instance,setupResult); 45 | setCurrentInstance(null); 46 | } 47 | } 48 | function handleSetupResult(instance,setupResult) { 49 | if (typeof setupResult === "object") { 50 | instance.setupState = proxyRefs(setupResult); 51 | } 52 | finishComponentSetup(instance); 53 | } 54 | function finishComponentSetup(instance){ 55 | const Component = instance.type; 56 | instance.render = Component.render; 57 | } 58 | let currentInstance = null; 59 | export function getCurrentInstance(){ 60 | return currentInstance; 61 | } 62 | export function setCurrentInstance(instance){ 63 | currentInstance = instance; 64 | } -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from "../shared/index"; 2 | 3 | export function emit(instance,event,...args){ 4 | const { props } = instance; 5 | const HandlerName = toHandlerKey(camelize(event)); 6 | const handler = props[HandlerName]; 7 | handler && handler(...args) 8 | } -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance,rawProps = {}){ 2 | instance.props = rawProps; 3 | } -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | const publicPropertiesMap = { 2 | $el: (i) => i.vnode.el, 3 | $slots:(i)=> i.slots, 4 | $props:(i)=> i.props 5 | }; 6 | export const PublicInstanceProxyHandlers = { 7 | get({ _: instance }, key) { 8 | const { setupState,props } = instance; 9 | const hasOwn = (val,key)=>Object.prototype.hasOwnProperty.call(val,key); 10 | if (hasOwn(setupState, key)) { 11 | return setupState[key]; 12 | } else if (hasOwn(props, key)) { 13 | return props[key]; 14 | } 15 | const publicGetter = publicPropertiesMap[key]; 16 | if (publicGetter) { 17 | return publicGetter(instance); 18 | } 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from "../shared/shapeFlags"; 2 | export function initSlots(instance, children) { 3 | const { vnode } = instance; 4 | if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN){ 5 | normalizeObjectSlots(children, instance.slots); 6 | } 7 | } 8 | function normalizeSlotValue(value) { 9 | return Array.isArray(value) ? value : [value]; 10 | } 11 | function normalizeObjectSlots(children, slots) { 12 | for (const key in children) { 13 | const value = children[key]; 14 | if (typeof value === "function") { 15 | slots[key] = (props) => normalizeSlotValue(value(props)); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/runtime-core/componentUpdateUtils.ts: -------------------------------------------------------------------------------- 1 | export function shouldUpdateComponent(prevVnode,nextVnode){ 2 | const { props: prevProps } = prevVnode; 3 | const { props: nextProps } = nextVnode; 4 | 5 | for (const key in nextProps) { 6 | if (nextProps[key] !== prevProps[key]) { 7 | return true; 8 | } 9 | } 10 | return false; 11 | } -------------------------------------------------------------------------------- /src/runtime-core/createApp.ts: -------------------------------------------------------------------------------- 1 | import { createVnode } from "./vnode"; 2 | 3 | export function createAppAPI(render){ 4 | return function createApp(rootComponent) { 5 | return { 6 | mount(rootContainer) { 7 | const vnode = createVnode(rootComponent); 8 | render(vnode, rootContainer); 9 | }, 10 | }; 11 | } 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/runtime-core/h.ts: -------------------------------------------------------------------------------- 1 | import { createVnode } from "./vnode"; 2 | export function h(type,props?,children?){ 3 | return createVnode(type,props,children) 4 | } -------------------------------------------------------------------------------- /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 | return createVnode(Fragment, {}, slot(props)); 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | export {h} from './h'; 2 | export {renderSlots} from './helpers/renderSlots' 3 | export { createTextVnode } from './vnode'; 4 | export { getCurrentInstance} from './component'; 5 | export { provide, inject } from "./apiInject"; 6 | export {createRenderer} from './renderer'; 7 | export {nextTick} from './scheduler'; -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../reactivity/effect'; 2 | import { ShapeFlags } from '../shared/shapeFlags'; 3 | import { createComponentInstance, setupComponent } from "./component"; 4 | import { shouldUpdateComponent } from './componentUpdateUtils'; 5 | import { createAppAPI } from './createApp'; 6 | import { queueJobs } from './scheduler'; 7 | import { Fragment,Text } from './vnode'; 8 | 9 | export function createRenderer(options){ 10 | const { 11 | createElement: hostCreateElement, 12 | patchProp: hostPatchProp, 13 | insert: hostInsert, 14 | remove: hostRemove, 15 | setElementText:hostSetElementText, 16 | } = options; 17 | function render(vnode, container, parentComponent) { 18 | patch(null, vnode, container, parentComponent, null); 19 | } 20 | //n1 old n2 new 21 | function patch(n1, n2, container, parentComponent, anchor) { 22 | const { type, shapeFlag } = n2; 23 | switch (type) { 24 | case Fragment: 25 | processFragment(n1, n2, container, parentComponent, anchor); 26 | break; 27 | case Text: 28 | processText(n1, n2, container); 29 | break; 30 | default: 31 | if (shapeFlag & ShapeFlags.ELEMENT) { 32 | processElement(n1, n2, container, parentComponent, anchor); 33 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 34 | processComponent(n1, n2, container, parentComponent, anchor); 35 | } 36 | break; 37 | } 38 | } 39 | function processElement(n1, n2, container, parentComponent, anchor) { 40 | if (!n1) { 41 | mountElement(n1, n2, container, parentComponent, anchor); 42 | } else { 43 | patchElement(n1, n2, container, parentComponent, anchor); 44 | } 45 | } 46 | function patchElement(n1, n2, container, parentComponent, anchor) { 47 | console.log("patchComponent"); 48 | console.log("n1", n1); 49 | console.log("n2", n2); 50 | const oldProps = n1.props || EMPTY_OBJ; 51 | const newProps = n2.props || EMPTY_OBJ; 52 | const el = (n2.el = n1.el); 53 | patchChildren(n1, n2, el, parentComponent, anchor); 54 | patchProps(el, oldProps, newProps); 55 | } 56 | function patchChildren(n1, n2, container, parentComponent, anchor) { 57 | const prevShapeFlag = n1.shapeFlag; 58 | const { shapeFlag } = n2; 59 | const c1 = n1.children; 60 | const c2 = n2.children; 61 | 62 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 63 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 64 | //1 把n1都清空 65 | unmountChildren(n1.children); 66 | } 67 | //2 设置text 68 | if (c1 !== c2) { 69 | hostSetElementText(container, c2); 70 | } 71 | } else { 72 | //text to array 73 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 74 | hostSetElementText(container, ""); 75 | mountChildren(c2, container, parentComponent, anchor); 76 | } else { 77 | //array 78 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 79 | } 80 | } 81 | } 82 | 83 | function patchKeyedChildren(c1, c2, container, parentComponent,parentAnchor) { 84 | let i = 0; 85 | const l2 = c2.length; 86 | let e1 = c1.length-1; 87 | let e2 = l2 - 1; 88 | function isSomeVNodeType(n1,n2){ 89 | return n1.type === n2.type && n1.key === n2.key; 90 | } 91 | //左侧 92 | while(i<=e1 &&i<=e2){ 93 | const n1 = c1[i]; 94 | const n2 = c2[i]; 95 | if(isSomeVNodeType(n1,n2)){ 96 | patch(n1, n2, container, parentComponent, parentAnchor); 97 | }else{ 98 | break; 99 | } 100 | i++; 101 | } 102 | //右侧 103 | while (i <= e1 && i <= e2) { 104 | const n1 = c1[e1]; 105 | const n2 = c2[e2]; 106 | if (isSomeVNodeType(n1, n2)) { 107 | patch(n1, n2, container, parentComponent, parentAnchor); 108 | } else { 109 | break; 110 | } 111 | 112 | e1--; 113 | e2--; 114 | } 115 | //新的比老的多 创建 116 | if(i > e1){ 117 | if(i <= e2){ 118 | const nextPos = e2 + 1; 119 | const anchor = e2 + 1 < l2 ? c2[nextPos].el : null; 120 | while(i<=e2){ 121 | patch(null, c2[i], container, parentComponent, anchor); 122 | i++; 123 | } 124 | } 125 | }else if(i > e2){ 126 | while(i <= e1){ 127 | hostRemove(c1[i].el); 128 | i++; 129 | } 130 | }else{ 131 | let s1 = i; 132 | let s2 = i; 133 | 134 | const toBePatched = e2 - s2 + 1; 135 | let patched = 0; 136 | 137 | const keyToNewIndexMap = new Map(); 138 | const newIndexToOldIndexMap = new Array(toBePatched); 139 | let moved = false; 140 | let maxNewIndexSoFar = 0; 141 | for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0; 142 | 143 | for(let i = s2;i<=e2;i++){ 144 | const nextChild = c2[i]; 145 | keyToNewIndexMap.set(nextChild.key,i); 146 | } 147 | for(let i = s1;i<=e1;i++){ 148 | const prevChild = c1[i]; 149 | if(patched>=toBePatched){ 150 | hostRemove(prevChild.el); 151 | continue; 152 | } 153 | let newIndex; 154 | if(prevChild.key!=null){ 155 | newIndex = keyToNewIndexMap.get(prevChild.key); 156 | }else{ 157 | for(let j = s2;j<=e2;j++){ 158 | if(isSomeVNodeType(prevChild,c2[j])){ 159 | newIndex = j; 160 | break; 161 | } 162 | 163 | } 164 | } 165 | if(newIndex===undefined){ 166 | hostRemove(prevChild.el); 167 | }else{ 168 | if(newIndex>=maxNewIndexSoFar){ 169 | maxNewIndexSoFar = newIndex 170 | }else{ 171 | moved = true; 172 | } 173 | newIndexToOldIndexMap[newIndex-s2] = i+1; 174 | patch(prevChild,c2[newIndex],container,parentComponent,null); 175 | patched++; 176 | } 177 | 178 | } 179 | const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap):[]; 180 | let j = increasingNewIndexSequence.length-1; 181 | for(let i = toBePatched-1;i>=0;i--){ 182 | const nextIndex = i + s2; 183 | const nextChild = c2[nextIndex]; 184 | const anchor = nextIndex+1> 1; 219 | if (arr[result[c]] < arrI) { 220 | u = c + 1; 221 | } else { 222 | v = c; 223 | } 224 | } 225 | if (arrI < arr[result[u]]) { 226 | if (u > 0) { 227 | p[i] = result[u - 1]; 228 | } 229 | result[u] = i; 230 | } 231 | } 232 | } 233 | u = result.length; 234 | v = result[u - 1]; 235 | while (u-- > 0) { 236 | result[u] = v; 237 | v = p[v]; 238 | } 239 | return result; 240 | } 241 | 242 | function unmountChildren(children){ 243 | for(let i = 0;i { 295 | patch(null, v, container, parentComponent, anchor); 296 | }); 297 | } 298 | function processComponent(n1, n2, container, parentComponent, anchor) { 299 | if(!n1){ 300 | mountComponent(n2, container, parentComponent, anchor); 301 | }else{ 302 | updateComponent(n1,n2); 303 | } 304 | } 305 | 306 | function updateComponent(n1,n2){ 307 | const instance = (n2.component = n1.component); 308 | if(shouldUpdateComponent(n1,n2)){ 309 | instance.next = n2; 310 | instance.update(); 311 | }else{ 312 | n2.component = n1.component; 313 | n2.el = n1.el; 314 | instance.vnode = n2; 315 | } 316 | 317 | } 318 | 319 | function mountComponent(initialVnode, container, parentComponent, anchor) { 320 | const instance = (initialVnode.component = createComponentInstance( 321 | initialVnode, 322 | parentComponent 323 | )); 324 | setupComponent(instance); 325 | setupRenderEffect(instance, initialVnode, container, anchor); 326 | } 327 | function setupRenderEffect(instance, initialVnode, container, anchor) { 328 | instance.update = effect(() => { 329 | if (!instance.isMounted) { 330 | console.log("init"); 331 | const { proxy } = instance; 332 | const subTree = (instance.subTree = instance.render.call(proxy)); 333 | patch(null, subTree, container, instance, anchor); 334 | initialVnode.el = subTree.el; 335 | instance.isMounted = true; 336 | } else { 337 | console.log("update"); 338 | const {next,vnode} = instance; 339 | 340 | if(next){ 341 | next.el = vnode.el; 342 | updateComponentPreRender(instance,next); 343 | } 344 | 345 | const { proxy } = instance; 346 | const subTree = instance.render.call(proxy); 347 | const prevSubTree = instance.subTree; 348 | instance.subTree = subTree; 349 | patch(prevSubTree, subTree, container, instance, anchor); 350 | } 351 | },{ 352 | scheduler(){ 353 | console.log('update--scheduler') 354 | queueJobs(instance.update); 355 | } 356 | }); 357 | } 358 | return { 359 | createApp:createAppAPI(render) 360 | } 361 | } 362 | 363 | function updateComponentPreRender(instance, nextVnode) { 364 | instance.vnode = nextVnode; 365 | instance.next = null; 366 | instance.props = nextVnode.props; 367 | } 368 | 369 | -------------------------------------------------------------------------------- /src/runtime-core/scheduler.ts: -------------------------------------------------------------------------------- 1 | const queue:any[] = []; 2 | let isFlushPending = false; 3 | const p = Promise.resolve(); 4 | export function nextTick(fn){ 5 | return fn ? p.then(fn):p; 6 | } 7 | 8 | export function queueJobs(job){ 9 | if(!queue.includes(job)){ 10 | queue.push(job) 11 | } 12 | queueFlush(); 13 | } 14 | function queueFlush(){ 15 | if (isFlushPending) return; 16 | isFlushPending = true; 17 | nextTick(flushJobs); 18 | } 19 | function flushJobs(){ 20 | isFlushPending = false; 21 | let job; 22 | while ((job = queue.shift())) { 23 | job && job(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from "../shared/shapeFlags"; 2 | 3 | export const Fragment = Symbol("Fragment"); 4 | export const Text = Symbol("Text"); 5 | export function createVnode(type,props?,children?){ 6 | const vnode = { 7 | type, 8 | props, 9 | children, 10 | el:null, 11 | key:props && props.key, 12 | component:null, 13 | shapeFlag:getShapeFlag(type) 14 | } 15 | if(typeof children ==='string'){ 16 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN 17 | }else if(Array.isArray(children)){ 18 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN; 19 | } 20 | if(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT){ 21 | if(typeof children === 'object'){ 22 | vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN; 23 | } 24 | } 25 | return vnode; 26 | } 27 | export function createTextVnode(text:string){ 28 | return createVnode(Text,{},text) 29 | } 30 | function getShapeFlag(type){ 31 | return typeof type === "string" ? ShapeFlags.ELEMENT : ShapeFlags.STATEFUL_COMPONENT 32 | } -------------------------------------------------------------------------------- /src/runtime-dom/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "../runtime-core"; 2 | function createElement(type) { 3 | return document.createElement(type); 4 | } 5 | function patchProp(el, key, oldVal,newVal) { 6 | const isOn = (key: string) => /^on[A-Z]/.test(key); 7 | if (isOn(key)) { 8 | const event = key.slice(2).toLowerCase(); 9 | el.addEventListener(event, newVal); 10 | } else { 11 | if(newVal === undefined || newVal === null){ 12 | el.removeAttribute(key, newVal); 13 | }else{ 14 | el.setAttribute(key, newVal); 15 | } 16 | } 17 | } 18 | function insert(child, parent,anchor) { 19 | parent.insertBefore(child,anchor || null) 20 | } 21 | function remove(child){ 22 | const parent = child.parentNode; 23 | if(parent){ 24 | parent.removeChild(child); 25 | } 26 | } 27 | function setElementText(container,text){ 28 | container.textContent = text; 29 | } 30 | const render: any = createRenderer({ 31 | createElement, 32 | patchProp, 33 | insert, 34 | remove, 35 | setElementText 36 | }); 37 | export function createApp(...args){ 38 | return render.createApp(...args) 39 | } 40 | export * from "../runtime-core"; -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export const extend = Object.assign; 2 | export const isObject = (val) => { 3 | return val !== null && typeof val === "object"; 4 | }; 5 | export const hasChanged = (newValue,value) => { 6 | return !Object.is(newValue,value); 7 | }; 8 | export const camelize = (str: string) => { 9 | return str.replace(/-(\w)/g, (_, c: string) => { 10 | return c ? c.toUpperCase() : ""; 11 | }); 12 | }; 13 | const capitalize = (str: string) => { 14 | return str.charAt(0).toUpperCase() + str.slice(1); 15 | }; 16 | export const toHandlerKey = (str: string) => { 17 | return str ? "on" + capitalize(str) : ""; 18 | }; -------------------------------------------------------------------------------- /src/shared/shapeFlags.ts: -------------------------------------------------------------------------------- 1 | export const enum ShapeFlags { 2 | ELEMENT = 1, 3 | STATEFUL_COMPONENT = 1 << 1, 4 | TEXT_CHILDREN = 1 << 2, 5 | ARRAY_CHILDREN = 1 << 3, 6 | SLOT_CHILDREN = 1 << 4, 7 | } -------------------------------------------------------------------------------- /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","ES2016"], /* 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 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | --------------------------------------------------------------------------------