├── .gitignore ├── .idea ├── .gitignore ├── Be-Vue.iml └── modules.xml ├── babel.config.js ├── example ├── apiInject │ ├── App.js │ └── index.html ├── complie-base │ ├── App.js │ └── index.html ├── componentEmit │ ├── App.js │ ├── foo.js │ ├── index.html │ └── main.js ├── componentSlots │ ├── App.js │ ├── foo.js │ ├── index.html │ └── main.js ├── componentUpdate │ ├── App.js │ ├── Child.js │ ├── index.html │ └── main.js ├── customRenderer │ ├── App.js │ ├── game.js │ ├── index.html │ ├── main.js │ └── renderer.js ├── finite-state-machine │ └── index.js ├── getCurrentInstance │ ├── App.js │ ├── foo.js │ ├── index.html │ └── main.js ├── helloword │ ├── App.js │ ├── foo.js │ ├── index.html │ └── main.js ├── nextTick │ ├── App.js │ ├── index.html │ └── main.js ├── update │ ├── App.js │ ├── index.html │ └── main.js ├── updateChildren │ ├── App.js │ ├── ArrayToArray.js │ ├── ArrayToText.js │ ├── TextToArray.js │ ├── TextToText.js │ ├── index.html │ └── main.js └── updateProps │ ├── App.js │ ├── index.html │ └── main.js ├── img └── 2022-06-28_22-34-44.png ├── lib ├── be-vue.cjs.js └── be-vue.esm.js ├── package.json ├── reactive-set_map.md ├── rollup.config.js ├── src ├── compiler-core │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── codegen.test.ts.snap │ │ ├── codegen.test.ts │ │ ├── parse.test.ts │ │ └── transform.test.ts │ ├── index.ts │ └── src │ │ ├── ast.ts │ │ ├── codegen.ts │ │ ├── complie.ts │ │ ├── parse.ts │ │ ├── runtimeHelpers.ts │ │ ├── transform.ts │ │ ├── transform │ │ ├── transformElement.ts │ │ ├── transformExpression.ts │ │ └── transformText.ts │ │ └── utils.ts ├── index.ts ├── reactivity │ ├── __test__ │ │ ├── computed.test.ts │ │ ├── effect.test.ts │ │ ├── index.test.ts │ │ ├── reactive.test.ts │ │ ├── readonly.test.ts │ │ ├── ref.test.ts │ │ └── shallowReadonly.test.ts │ ├── baseHandlers.ts │ ├── computed.ts │ ├── effect.d.ts │ ├── effect.ts │ ├── index.ts │ ├── reactive.ts │ └── ref.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 │ ├── ShapeFlags.ts │ ├── index.ts │ └── toDisplayString.ts ├── tsconfig.json ├── vue总结.md ├── why should cleanupEffect in ReactiveEffect ├── yarn └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /node_modules/ -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/Be-Vue.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; -------------------------------------------------------------------------------- /example/apiInject/App.js: -------------------------------------------------------------------------------- 1 | // 组件 provide 和 inject 功能 2 | import { 3 | h, 4 | provide, 5 | inject, 6 | } from "../../lib/be-vue.esm.js"; 7 | 8 | const ProviderOne = { 9 | setup() { 10 | provide("foo", "foo"); 11 | provide("bar", "bar"); 12 | return () => h(ProviderTwo); 13 | }, 14 | }; 15 | 16 | const ProviderTwo = { 17 | setup() { 18 | // override parent value 19 | provide("foo", "fooOverride"); 20 | provide("baz", "baz"); 21 | const foo = inject("foo"); 22 | // 这里获取的 foo 的值应该是 "foo" 23 | // 这个组件的子组件获取的 foo ,才应该是 fooOverride 24 | if (foo !== "foo") { 25 | throw new Error("Foo should equal to foo"); 26 | } 27 | return () => h(Consumer); 28 | }, 29 | }; 30 | 31 | const Consumer = { 32 | setup() { 33 | const foo = inject("foo"); 34 | const bar = inject("bar"); 35 | const baz = inject("baz"); 36 | return () => { 37 | return h("div", {}, `${foo}-${bar}-${baz}`); 38 | }; 39 | }, 40 | }; 41 | 42 | export default { 43 | name: "App", 44 | setup() { 45 | return ()=>h("div", {}, [ 46 | h("p", {}, "apiInject"), 47 | h(ProviderOne) 48 | ]); 49 | }, 50 | }; -------------------------------------------------------------------------------- /example/apiInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 17 | 18 | -------------------------------------------------------------------------------- /example/complie-base/App.js: -------------------------------------------------------------------------------- 1 | // 最简单的情况 2 | // template 只有一个 interpolation 3 | // export default { 4 | // template: `{{msg}}`, 5 | // setup() { 6 | // return { 7 | // msg: "vue3 - compiler", 8 | // }; 9 | // }, 10 | // }; 11 | 12 | 13 | // 复杂一点 14 | // template 包含 element 和 interpolation 15 | export default { 16 | template: `

{{msg}}

`, 17 | setup() { 18 | return { 19 | msg: "vue3 - compiler", 20 | }; 21 | }, 22 | }; -------------------------------------------------------------------------------- /example/complie-base/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 19 | 20 | -------------------------------------------------------------------------------- /example/componentEmit/App.js: -------------------------------------------------------------------------------- 1 | import {h,createTextVNode} from '../../lib/be-vue.esm.js' 2 | import foo from "./foo.js"; 3 | console.log(foo) 4 | window.self = null 5 | export const App = { 6 | // .vue 7 | setup() { 8 | return {} 9 | }, 10 | render() { 11 | window.self = this 12 | return h( 13 | 'div', 14 | { 15 | id: `hi be-vue${this.msg}`, 16 | class: ['aaa', 'qwdqwdasssssss'], 17 | }, 18 | [ 19 | createTextVNode('ComponentEmit'), 20 | h(foo,{ 21 | onAdd(...data){ 22 | console.log('emit onAdd',...data) 23 | }, 24 | onAddFoo(...data){ 25 | console.log('emit onAddFoo',...data) 26 | } 27 | }), 28 | 29 | ] 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /example/componentEmit/foo.js: -------------------------------------------------------------------------------- 1 | import {h} from "../../lib/be-vue.esm.js"; 2 | 3 | export default { 4 | setup(props, {emit}) { 5 | const handleClick = () => { 6 | emit('add', 1, 2) 7 | emit('add-foo', 1, 2) 8 | } 9 | return { 10 | handleClick 11 | } 12 | }, 13 | render() { 14 | const _this = this 15 | const renderBtn = h( 16 | 'button', 17 | { 18 | onClick() { 19 | _this.handleClick() 20 | } 21 | },'test emit') 22 | return h( 23 | 'div', 24 | { 25 | class: 'foo', 26 | }, 27 | [renderBtn]) 28 | } 29 | } -------------------------------------------------------------------------------- /example/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /example/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | // be-vue 2 | import {createApp} from "../../lib/be-vue.esm.js"; 3 | import {App} from "./App.js"; 4 | const rootContainer = document.getElementById('app') 5 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/componentSlots/App.js: -------------------------------------------------------------------------------- 1 | import {createTextVNode, h} from '../../lib/be-vue.esm.js' 2 | import foo from "./foo.js"; 3 | 4 | console.log(foo) 5 | window.self = null 6 | export const App = { 7 | // .vue 8 | setup() { 9 | return {} 10 | }, 11 | render() { 12 | window.self = this 13 | return h( 14 | 'div', 15 | { 16 | id: `hi be-vue${this.msg}`, 17 | class: ['aaa', 'qwdqwdasssssss'], 18 | }, 19 | [ 20 | createTextVNode('ComponentEmit'), 21 | h(foo, 22 | {}, 23 | { 24 | // 具名插槽 25 | header: () => { 26 | return h('p', {}, 'slot-name--header') 27 | }, 28 | body: () => { 29 | return [h('p', {}, 'slot-name--body1'), h('p', {}, 'slot-name--body2')] 30 | }, 31 | // 作用域插槽 32 | footer: ({name}) => { 33 | return h('p', {}, `slot-name--footer__${name}`) 34 | } 35 | } 36 | ), 37 | 38 | ] 39 | ) 40 | } 41 | } -------------------------------------------------------------------------------- /example/componentSlots/foo.js: -------------------------------------------------------------------------------- 1 | import {h,renderSlots} from "../../lib/be-vue.esm.js"; 2 | 3 | export default { 4 | setup(props, {emit}) { 5 | const handleClick = () => { 6 | emit('add', 1, 2) 7 | emit('add-foo', 1, 2) 8 | } 9 | return { 10 | handleClick 11 | } 12 | }, 13 | render() { 14 | const _this = this 15 | const renderP = h( 16 | 'p', 17 | {}, 18 | 'test slot') 19 | return h( 20 | 'div', 21 | { 22 | class: 'foo', 23 | }, 24 | [ 25 | renderP, 26 | renderSlots(_this.$slots,'header', {}), 27 | renderSlots(_this.$slots,'body', {}), 28 | renderSlots(_this.$slots,'footer', {name:'baiwusanyu'})]) 29 | } 30 | } -------------------------------------------------------------------------------- /example/componentSlots/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /example/componentSlots/main.js: -------------------------------------------------------------------------------- 1 | // be-vue 2 | import {createApp} from "../../lib/be-vue.esm.js"; 3 | import {App} from "./App.js"; 4 | const rootContainer = document.getElementById('app') 5 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/componentUpdate/App.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/be-vue.esm.js' 2 | import {Child} from './Child.js' 3 | export const App = { 4 | // .vue 5 | setup() { 6 | const msg = ref('123') 7 | const count = ref(1) 8 | const changeChildProps = () =>{ 9 | msg.value = '456' 10 | } 11 | window.msg = msg 12 | const changeCount = () =>{ 13 | count.value++ 14 | } 15 | return { 16 | changeChildProps, 17 | msg, 18 | count, 19 | changeCount 20 | } 21 | }, 22 | render() { 23 | return h( 24 | 'div', 25 | { 26 | id: `hi be-vue`, 27 | }, 28 | [ 29 | h( 30 | 'button', 31 | { 32 | onClick:this.changeChildProps 33 | }, 34 | `changeChildProps` 35 | ), 36 | h( 37 | Child, 38 | { 39 | msg:this.msg 40 | }, 41 | ), 42 | h( 43 | 'button', 44 | { 45 | onClick:this.changeCount 46 | }, 47 | `changeCount` 48 | ), 49 | h( 50 | 'p', 51 | { 52 | 53 | }, 54 | `${this.count}` 55 | ), 56 | ] 57 | ) 58 | } 59 | } -------------------------------------------------------------------------------- /example/componentUpdate/Child.js: -------------------------------------------------------------------------------- 1 | import {h} from '../../lib/be-vue.esm.js' 2 | export const Child = { 3 | // .vue 4 | setup() { 5 | 6 | }, 7 | render() { 8 | return h( 9 | 'div', 10 | { 11 | 12 | }, 13 | `hi be-vue ${this.$props.msg}`, 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /example/componentUpdate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /example/componentUpdate/main.js: -------------------------------------------------------------------------------- 1 | // be-vue 2 | import {createApp} from "../../lib/be-vue.esm.js"; 3 | import {App} from "./App.js"; 4 | const rootContainer = document.getElementById('app') 5 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/customRenderer/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from "../../lib/be-vue.esm.js"; 2 | import { game } from "./game.js"; 3 | 4 | export default { 5 | name: "App", 6 | setup() { 7 | // 通过 ticker 来去更新 x 的值 8 | 9 | const x = ref(0); 10 | const y = ref(0); 11 | let dir = 1; 12 | const speed = 2; 13 | 14 | game.ticker.add(() => { 15 | if (x.value > 400) { 16 | dir = -1; 17 | } else if (x.value < 0) { 18 | dir = 1; 19 | } 20 | 21 | x.value += speed * dir; 22 | }); 23 | 24 | return { 25 | x, 26 | y, 27 | }; 28 | }, 29 | 30 | render() { 31 | return h("rect", { x: this.x, y: this.y }); 32 | }, 33 | }; -------------------------------------------------------------------------------- /example/customRenderer/game.js: -------------------------------------------------------------------------------- 1 | export const game = new PIXI.Application({ 2 | width: 500, 3 | height: 500, 4 | }); 5 | 6 | document.body.append(game.view); 7 | 8 | export function createRootContainer() { 9 | return game.stage; 10 | } -------------------------------------------------------------------------------- /example/customRenderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/customRenderer/main.js: -------------------------------------------------------------------------------- 1 | import App from "./App.js"; 2 | import { createApp } from "./renderer.js"; 3 | import { createRootContainer } from "./game.js"; 4 | 5 | createApp(App).mount(createRootContainer()); -------------------------------------------------------------------------------- /example/customRenderer/renderer.js: -------------------------------------------------------------------------------- 1 | import { createRenderer } from "../../lib/be-vue.esm.js"; 2 | 3 | // 给基于 pixi.js 的渲染函数 4 | const renderer = createRenderer({ 5 | createElement(type) { 6 | const rect = new PIXI.Graphics(); 7 | rect.beginFill(0xff0000); 8 | rect.drawRect(0, 0, 100, 100); 9 | rect.endFill(); 10 | 11 | return rect; 12 | }, 13 | 14 | patchProp(el, key, prevValue, nextValue) { 15 | el[key] = nextValue; 16 | }, 17 | 18 | insert(el, parent,anchor) { 19 | parent.insertBefore(el,anchor || null); 20 | }, 21 | }); 22 | 23 | export function createApp(rootComponent) { 24 | return renderer.createApp(rootComponent); 25 | } -------------------------------------------------------------------------------- /example/finite-state-machine/index.js: -------------------------------------------------------------------------------- 1 | // /abc/.test('abc) 2 | // 有限状态机 模拟实现 正则表达式 3 | function test(str) { 4 | let i = 0; 5 | let currentState = stateA 6 | function stateA(c) { 7 | if(c === 'a'){ 8 | return stateB 9 | } 10 | return stateA 11 | } 12 | function stateB(c) { 13 | if(c === 'b'){ 14 | return stateCD 15 | } 16 | return stateA 17 | } 18 | function stateCD(c) { 19 | if(c === 'c'){ 20 | return stateEnd 21 | } 22 | return stateA 23 | } 24 | function stateEnd() { 25 | return stateEnd 26 | } 27 | for(;i { 8 | emit('add', 1, 2) 9 | emit('add-foo', 1, 2) 10 | } 11 | return { 12 | handleClick 13 | } 14 | }, 15 | render() { 16 | const _this = this 17 | const renderBtn = h( 18 | 'button', 19 | { 20 | onClick() { 21 | _this.handleClick() 22 | } 23 | },'test emit') 24 | return h( 25 | 'div', 26 | { 27 | class: 'foo', 28 | }, 29 | [renderBtn]) 30 | } 31 | } -------------------------------------------------------------------------------- /example/getCurrentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /example/getCurrentInstance/main.js: -------------------------------------------------------------------------------- 1 | // be-vue 2 | import {createApp} from "../../lib/be-vue.esm.js"; 3 | import {App} from "./App.js"; 4 | const rootContainer = document.getElementById('app') 5 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/helloword/App.js: -------------------------------------------------------------------------------- 1 | import {h} from '../../lib/be-vue.esm.js' 2 | import foo from "./foo.js"; 3 | console.log(foo) 4 | window.self = null 5 | export const App = { 6 | // .vue 7 | setup() { 8 | 9 | return { 10 | msg: '-baiwusanyu' 11 | } 12 | }, 13 | render() { 14 | window.self = this 15 | return h( 16 | 'div', 17 | { 18 | id: `hi be-vue${this.msg}`, 19 | class: ['aaa', 'qwdqwdasssssss'], 20 | onClick() { 21 | console.log('click') 22 | } 23 | }, 24 | [ 25 | h( 26 | 'div', 27 | { 28 | id: `hi be-vue${this.msg}c`, class: ['ccc'] 29 | }, 30 | 'tesa' 31 | ), 32 | h(foo,{count:1}) 33 | ] 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /example/helloword/foo.js: -------------------------------------------------------------------------------- 1 | import {h} from "../../lib/be-vue.esm.js"; 2 | 3 | export default { 4 | setup(props){ 5 | console.log(props.count) 6 | // props is shallowReadonly 7 | props.count++ 8 | }, 9 | render(){ 10 | return h('div', { 11 | class:'foo' 12 | },`foo:${this.count}`) 13 | } 14 | } -------------------------------------------------------------------------------- /example/helloword/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /example/helloword/main.js: -------------------------------------------------------------------------------- 1 | // be-vue 2 | import {createApp} from "../../lib/be-vue.esm.js"; 3 | import {App} from "./App.js"; 4 | const rootContainer = document.getElementById('app') 5 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/nextTick/App.js: -------------------------------------------------------------------------------- 1 | import {h,ref,nextTick,getCurrentInstance} from '../../lib/be-vue.esm.js' 2 | export const App = { 3 | // .vue 4 | setup() { 5 | const count = ref(0) 6 | let inst = getCurrentInstance() 7 | const handleClick = () =>{ 8 | for(let i = 0;i < 100; i++){ 9 | count.value++ 10 | } 11 | nextTick(()=>{ 12 | console.log(inst) 13 | }) 14 | } 15 | return { 16 | handleClick, 17 | count 18 | } 19 | }, 20 | render() { 21 | return h( 22 | 'div', 23 | { 24 | id: `hi be-vue${this.msg}`, 25 | class: ['aaa', 'qwdqwdasssssss'], 26 | }, 27 | [ 28 | h( 29 | 'div', 30 | { 31 | id: `hi be-vue${this.msg}c`, class: ['ccc'] 32 | }, 33 | `${this.count}` 34 | ), 35 | h( 36 | 'button', 37 | { 38 | id: `hi be-vue${this.msg}c`, class: ['ccc'], 39 | onClick:this.handleClick 40 | }, 41 | `$click` 42 | ), 43 | ] 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /example/nextTick/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /example/nextTick/main.js: -------------------------------------------------------------------------------- 1 | // be-vue 2 | import {createApp} from "../../lib/be-vue.esm.js"; 3 | import {App} from "./App.js"; 4 | const rootContainer = document.getElementById('app') 5 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/update/App.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/be-vue.esm.js' 2 | export const App = { 3 | // .vue 4 | setup() { 5 | const count = ref(0) 6 | const handleClick = () =>{ 7 | count.value++ 8 | } 9 | return { 10 | handleClick, 11 | count 12 | } 13 | }, 14 | render() { 15 | return h( 16 | 'div', 17 | { 18 | id: `hi be-vue${this.msg}`, 19 | class: ['aaa', 'qwdqwdasssssss'], 20 | }, 21 | [ 22 | h( 23 | 'div', 24 | { 25 | id: `hi be-vue${this.msg}c`, class: ['ccc'] 26 | }, 27 | `${this.count}` 28 | ), 29 | h( 30 | 'button', 31 | { 32 | id: `hi be-vue${this.msg}c`, class: ['ccc'], 33 | onClick:this.handleClick 34 | }, 35 | `$click` 36 | ), 37 | ] 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /example/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /example/update/main.js: -------------------------------------------------------------------------------- 1 | // be-vue 2 | import {createApp} from "../../lib/be-vue.esm.js"; 3 | import {App} from "./App.js"; 4 | const rootContainer = document.getElementById('app') 5 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/updateChildren/App.js: -------------------------------------------------------------------------------- 1 | import {h} from '../../lib/be-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 | // .vue 8 | setup() {}, 9 | render() { 10 | return h( 11 | 'div', 12 | { 13 | id: `hi be-vue${this.msg}`, 14 | }, 15 | [ 16 | h( 17 | 'p', 18 | { 19 | }, 20 | `home` 21 | ), 22 | h(ArrayToArray), 23 | //h(TextToText), 24 | //h(TextToArray), 25 | ] 26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /example/updateChildren/ArrayToArray.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/be-vue.esm.js"; 2 | import { ref } from "../../lib/be-vue.esm.js"; 3 | 4 | const isChange = ref(false); 5 | /************************************************ 扫描头部与扫描尾部 demo **********************************/ 6 | // 1. 头部扫描:左侧的对比 7 | // (a b) c 8 | // (a b) d e 9 | /* const prevChildren = [ 10 | h("p", { key: "A" }, "A"), 11 | h("p", { key: "B" }, "B"), 12 | h("p", { key: "C" }, "C"), 13 | ]; 14 | const nextChildren = [ 15 | h("p", { key: "A" }, "A"), 16 | h("p", { key: "B" }, "B"), 17 | h("p", { key: "D" }, "D"), 18 | h("p", { key: "E" }, "E"), 19 | ];*/ 20 | 21 | // 2. 尾部扫描:右侧的对比 22 | // a (b c) 23 | // d e (b c) 24 | /* const prevChildren = [ 25 | h("p", { key: "A" }, "A"), 26 | h("p", { key: "B" }, "B"), 27 | h("p", { key: "C" }, "C"), 28 | ]; 29 | const nextChildren = [ 30 | h("p", { key: "D" }, "D"), 31 | h("p", { key: "E" }, "E"), 32 | h("p", { key: "B" }, "B"), 33 | h("p", { key: "C" }, "C"), 34 | ];*/ 35 | 36 | // 3. 新的比老的长 37 | // 创建新的 38 | // 新节点在 左侧 39 | // (a b) 40 | // (a b) c 41 | // i = 2, e1 = 1, e2 = 2 42 | /*const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 43 | const nextChildren = [ 44 | h("p", { key: "A" }, "A"), 45 | h("p", { key: "B" }, "B"), 46 | h("p", { key: "C" }, "C"), 47 | ];*/ 48 | 49 | // 新节点在 右侧 50 | // (a b) 51 | // c (a b) 52 | // i = 0, e1 = -1, e2 = 0 53 | /*const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")]; 54 | const nextChildren = [ 55 | h("p", { key: "C" }, "C"), 56 | h("p", { key: "A" }, "A"), 57 | h("p", { key: "B" }, "B"), 58 | ];*/ 59 | 60 | // 4. 老的比新的长 61 | // 删除老的 62 | // 老节点在左侧 63 | // (a b) c 64 | // (a b) 65 | // i = 2, e1 = 2, e2 = 1 66 | /* const prevChildren = [ 67 | h("p", { key: "A" }, "A"), 68 | h("p", { key: "B" }, "B"), 69 | h("p", { key: "C" }, "C"), 70 | ]; 71 | const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];*/ 72 | 73 | // 老节点在右侧 74 | // a (b c) 75 | // (b c) 76 | // i = 0, e1 = 0, e2 = -1 77 | 78 | /* 79 | const prevChildren = [ 80 | h("p", { key: "A" }, "A"), 81 | h("p", { key: "B" }, "B"), 82 | h("p", { key: "C" }, "C"), 83 | ]; 84 | const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")]; 85 | */ 86 | 87 | 88 | 89 | 90 | 91 | /************************************************ 处理头尾部扫描后 中间节点部分 demo **********************************/ 92 | // 5. 对比中间的部分 93 | // 删除老的 (在老的里面存在,新的里面不存在) 94 | // 5.1 95 | // a,b,(c,d),f,g 96 | // a,b,(e,c),f,g 97 | // D 节点在新的里面是没有的 - 需要删除掉 98 | // C 节点 props 也发生了变化 99 | 100 | /*const prevChildren = [ 101 | h("p", { key: "A" }, "A"), 102 | h("p", { key: "B" }, "B"), 103 | h("p", { key: "C", id: "c-prev" }, "C"), 104 | h("p", { key: "D" }, "D"), 105 | h("p", { key: "F" }, "F"), 106 | h("p", { key: "G" }, "G"), 107 | ]; 108 | 109 | const nextChildren = [ 110 | h("p", { key: "A" }, "A"), 111 | h("p", { key: "B" }, "B"), 112 | h("p", { key: "E" }, "E"), 113 | h("p", { key: "C", id:"c-next" }, "C"), 114 | h("p", { key: "F" }, "F"), 115 | h("p", { key: "G" }, "G"), 116 | ];*/ 117 | 118 | // 5.1.1 119 | // a,b,(c,e,d),f,g 120 | // a,b,(e,c),f,g 121 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑) 122 | /*const prevChildren = [ 123 | h("p", { key: "A" }, "A"), 124 | h("p", { key: "B" }, "B"), 125 | h("p", { key: "C", id: "c-prev" }, "C"), 126 | h("p", { key: "E" }, "E"), 127 | h("p", { key: "D" }, "D"), 128 | h("p", { key: "F" }, "F"), 129 | h("p", { key: "G" }, "G"), 130 | ]; 131 | 132 | const nextChildren = [ 133 | h("p", { key: "A" }, "A"), 134 | h("p", { key: "B" }, "B"), 135 | h("p", { key: "E" }, "E"), 136 | h("p", { key: "C", id:"c-next" }, "C"), 137 | h("p", { key: "F" }, "F"), 138 | h("p", { key: "G" }, "G"), 139 | ];*/ 140 | 141 | // 2 移动 (节点存在于新的和老的里面,但是位置变了) 142 | 143 | // 2.1 144 | // a,b,(c,d,e),f,g 145 | // a,b,(e,c,d),f,g 146 | // 最长子序列: [1,2] 147 | 148 | /* const prevChildren = [ 149 | h("p", { key: "A" }, "A"), 150 | h("p", { key: "B" }, "B"), 151 | h("p", { key: "C" }, "C"), 152 | h("p", { key: "D" }, "D"), 153 | h("p", { key: "E" }, "E"), 154 | h("p", { key: "F" }, "F"), 155 | h("p", { key: "G" }, "G"), 156 | ]; 157 | 158 | const nextChildren = [ 159 | h("p", { key: "A" }, "A"), 160 | h("p", { key: "B" }, "B"), 161 | h("p", { key: "E" }, "E"), 162 | h("p", { key: "C" }, "C"), 163 | h("p", { key: "D" }, "D"), 164 | h("p", { key: "F" }, "F"), 165 | h("p", { key: "G" }, "G"), 166 | ];*/ 167 | 168 | // 2.2 169 | // a,b,(c,d,e,z),f,g 170 | // a,b,(d,c,y,e),f,g 171 | // 最长子序列: [1,3] 172 | 173 | /* const prevChildren = [ 174 | h("p", { key: "A" }, "A"), 175 | h("p", { key: "B" }, "B"), 176 | h("p", { key: "C" }, "C"), 177 | h("p", { key: "D" }, "D"), 178 | h("p", { key: "E" }, "E"), 179 | h("p", { key: "Z" }, "Z"), 180 | h("p", { key: "F" }, "F"), 181 | h("p", { key: "G" }, "G"), 182 | ]; 183 | 184 | const nextChildren = [ 185 | h("p", { key: "A" }, "A"), 186 | h("p", { key: "B" }, "B"), 187 | h("p", { key: "D" }, "D"), 188 | h("p", { key: "C" }, "C"), 189 | h("p", { key: "Y" }, "Y"), 190 | h("p", { key: "E" }, "E"), 191 | h("p", { key: "F" }, "F"), 192 | h("p", { key: "G" }, "G"), 193 | ];*/ 194 | 195 | // 3. 创建新的节点 196 | // a,b,(c,e),f,g 197 | // a,b,(e,c,d),f,g 198 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建 199 | const prevChildren = [ 200 | h("p", { key: "A" }, "A"), 201 | h("p", { key: "B" }, "B"), 202 | h("p", { key: "C" }, "C"), 203 | h("p", { key: "E" }, "E"), 204 | h("p", { key: "F" }, "F"), 205 | h("p", { key: "G" }, "G"), 206 | ]; 207 | 208 | const nextChildren = [ 209 | h("p", { key: "A" }, "A"), 210 | h("p", { key: "B" }, "B"), 211 | h("p", { key: "E" }, "E"), 212 | h("p", { key: "C" }, "C"), 213 | h("p", { key: "D" }, "D"), 214 | h("p", { key: "F" }, "F"), 215 | h("p", { key: "G" }, "G"), 216 | ]; 217 | 218 | export default { 219 | name: "PatchChildren", 220 | setup() {}, 221 | render() { 222 | return h("div", {}, [ 223 | h( 224 | "button", 225 | { 226 | onClick: () => { 227 | isChange.value = !isChange.value; 228 | }, 229 | }, 230 | "测试子组件之间的 patch 逻辑" 231 | ), 232 | h("children", {}, isChange.value === true ? nextChildren : prevChildren), 233 | ]); 234 | }, 235 | }; -------------------------------------------------------------------------------- /example/updateChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/be-vue.esm.js' 2 | const nextC = 'newC' 3 | const prevC = [ 4 | h('div',{},'A'), 5 | h('div',{},'B') 6 | ] 7 | 8 | export default { 9 | name:'ArrayToText', 10 | setup(){ 11 | const isChange = ref(false) 12 | window.isChange = isChange 13 | return{ 14 | isChange 15 | } 16 | }, 17 | render(){ 18 | const self = this 19 | return self.isChange === true ? h('div',{},nextC) :h('div',{},prevC) 20 | } 21 | } -------------------------------------------------------------------------------- /example/updateChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/be-vue.esm.js' 2 | const prevC = 'newC' 3 | const nextC = [ 4 | h('div',{},'A'), 5 | h('div',{},'B') 6 | ] 7 | 8 | export default { 9 | name:'TextToArray', 10 | setup(){ 11 | const isChange = ref(false) 12 | window.isChange = isChange 13 | return{ 14 | isChange 15 | } 16 | }, 17 | render(){ 18 | const self = this 19 | return self.isChange === true ? 20 | h('div',{},nextC) 21 | :h('div',{},prevC) 22 | } 23 | } -------------------------------------------------------------------------------- /example/updateChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/be-vue.esm.js' 2 | const nextC = 'newC' 3 | const prevC = 'prevC' 4 | 5 | export default { 6 | name:'TextToText', 7 | setup(){ 8 | const isChange = ref(false) 9 | window.isChange = isChange 10 | return{ 11 | isChange 12 | } 13 | }, 14 | render(){ 15 | const self = this 16 | return self.isChange === true ? 17 | h('div',{},nextC) 18 | :h('div',{},prevC) 19 | } 20 | } -------------------------------------------------------------------------------- /example/updateChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /example/updateChildren/main.js: -------------------------------------------------------------------------------- 1 | // be-vue 2 | import {createApp} from "../../lib/be-vue.esm.js"; 3 | import {App} from "./App.js"; 4 | const rootContainer = document.getElementById('app') 5 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /example/updateProps/App.js: -------------------------------------------------------------------------------- 1 | import {h,ref} from '../../lib/be-vue.esm.js' 2 | export const App = { 3 | // .vue 4 | setup() { 5 | const count = ref(0) 6 | const data = ref({ 7 | foo:'foo', 8 | bar:'bar' 9 | }) 10 | const handleClick = () =>{ 11 | count.value++ 12 | } 13 | const setNewFoo = () =>{ 14 | data.value.foo = 'new Foo' 15 | } 16 | const setNullFoo = () =>{ 17 | data.value.foo = undefined 18 | } 19 | const setRemoveBar = () =>{ 20 | data.value = { 21 | foo:'foo', 22 | } 23 | } 24 | return { 25 | setNullFoo, 26 | handleClick, 27 | setNewFoo, 28 | setRemoveBar, 29 | data, 30 | count 31 | } 32 | }, 33 | render() { 34 | return h( 35 | 'div', 36 | { 37 | id: `hi be-vue${this.msg}`, 38 | foo:this.data.foo, 39 | bar:this.data.bar, 40 | class: ['aaa', 'qwdqwdasssssss'], 41 | }, 42 | [ 43 | h( 44 | 'button', 45 | { 46 | id: `hi be-vue${this.msg}c`, class: ['ccc'], 47 | onClick:this.handleClick 48 | }, 49 | `$click` 50 | ), 51 | h( 52 | 'button', 53 | { 54 | id: `hi be-vue${this.msg}cc`, class: ['ccc'], 55 | onClick:this.setNewFoo 56 | }, 57 | `the Value is changed` 58 | ), 59 | h( 60 | 'button', 61 | { 62 | id: `hi be-vue${this.msg}ac`, class: ['ccc'], 63 | onClick:this.setNullFoo 64 | }, 65 | `the Value is Null or Undefined` 66 | ), 67 | h( 68 | 'button', 69 | { 70 | id: `hi be-vue${this.msg}sc`, class: ['ccc'], 71 | onClick:this.setRemoveBar 72 | }, 73 | `the old Value is removed` 74 | ), 75 | ] 76 | ) 77 | } 78 | } -------------------------------------------------------------------------------- /example/updateProps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /example/updateProps/main.js: -------------------------------------------------------------------------------- 1 | // be-vue 2 | import {createApp} from "../../lib/be-vue.esm.js"; 3 | import {App} from "./App.js"; 4 | const rootContainer = document.getElementById('app') 5 | createApp(App).mount(rootContainer) -------------------------------------------------------------------------------- /img/2022-06-28_22-34-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baiwusanyu-c/Be-Vue/28c9465ddaa8589d3d86aca9d4ce4856da6b9968/img/2022-06-28_22-34-44.png -------------------------------------------------------------------------------- /lib/be-vue.esm.js: -------------------------------------------------------------------------------- 1 | // 生成ast 上下文对象 2 | function createParserContext(content) { 3 | return { 4 | source: content 5 | }; 6 | } 7 | // 生成ast root 8 | function createRoot(parseChildren) { 9 | return { 10 | children: parseChildren, 11 | type: 4 /* nodeTypes.ROOT */ 12 | }; 13 | } 14 | // 解析 {{}} 15 | function parseInterpolation(context) { 16 | // {{ message }} 17 | let startDelimiter = '{{'; 18 | let closeDelimiter = '}}'; 19 | // 获取 '}}' 位置 20 | let closeIndex = context.source.indexOf(closeDelimiter, startDelimiter.length); 21 | // 消费 '{{' 22 | advanceBy(context, startDelimiter.length); 23 | // 截取 content && 消费 content 24 | let rawContent = parseTextData(context, closeIndex - startDelimiter.length); 25 | // 消费 '}}' 26 | advanceBy(context, closeDelimiter.length); 27 | let content = rawContent.trim(); 28 | return { 29 | type: 0 /* nodeTypes.INTERPOLATION */, 30 | content: { 31 | type: 1 /* nodeTypes.SIMPLE_EXPRESSION */, 32 | content: content 33 | } 34 | }; 35 | } 36 | // 消费字符串 37 | function advanceBy(context, index) { 38 | context.source = context.source.slice(index); 39 | } 40 | function startsWithEndTagOpen(source, tag) { 41 | return source.startsWith('' 63 | advanceBy(context, 1); 64 | if (type === 1 /* TagType.end */) 65 | return; 66 | return { 67 | type: 2 /* nodeTypes.ELEMENT */, 68 | tag: tag, 69 | children: [] 70 | }; 71 | } 72 | function parseText(context) { 73 | let s = context.source; 74 | let endIndex = s.length; 75 | let endToken = ['<', '{{']; 76 | // 找到 source中 <','{{' 的位置 并进一步解析出文本 77 | for (let i = 0; i < endToken.length; i++) { 78 | const index = s.indexOf(endToken[i]); 79 | if (index !== -1 && index < endIndex) { 80 | endIndex = index; 81 | } 82 | } 83 | const content = parseTextData(context, endIndex); 84 | return { 85 | type: 3 /* nodeTypes.TEXT */, 86 | content 87 | }; 88 | } 89 | function parseTextData(context, length) { 90 | const content = context.source.slice(0, length); 91 | // 消费 92 | advanceBy(context, length); 93 | return content; 94 | } 95 | // 解析 children,基于有限状态机模式 96 | function parseChildren(context, ancestors) { 97 | const nodes = []; 98 | // 循环遍历children 字符串 99 | while (!isEnd(context, ancestors)) { 100 | let node; 101 | let s = context.source; 102 | // 以 {{ 则走解析插值逻辑 103 | if (s.startsWith('{{')) { 104 | node = parseInterpolation(context); 105 | } 106 | else if (s[0] === '<') { 107 | // 以 < 开头 且第二个字母为a-z 就当做element的标签解析 108 | if (/[a-z]/i.test(s[1])) { 109 | node = parseElement(context, ancestors); 110 | } 111 | } 112 | else { 113 | // 默认当做文本解析 114 | node = parseText(context); 115 | } 116 | nodes.push(node); 117 | } 118 | return nodes; 119 | } 120 | // 是否标签闭合 121 | function isEnd(context, ancestors) { 122 | let s = context.source; 123 | if (s.startsWith(`= 0; i--) { 126 | const tag = ancestors[i].tag; 127 | if (startsWithEndTagOpen(s, tag)) { 128 | return true; 129 | } 130 | } 131 | } 132 | return !context.source; 133 | } 134 | function baseParse(content) { 135 | const context = createParserContext(content); 136 | return createRoot(parseChildren(context, [])); 137 | } 138 | 139 | const TO_DISPLAY_STRING = Symbol('toDisplayString'); 140 | const CREATE_ELEMENT_VNODE = Symbol('createElementVNode'); 141 | const helperMapName = { 142 | [TO_DISPLAY_STRING]: 'toDisplayString', 143 | [CREATE_ELEMENT_VNODE]: 'createElementVNode' 144 | }; 145 | 146 | function createRootCodegen(root) { 147 | const child = root.children[0]; 148 | if (child.type === 2 /* nodeTypes.ELEMENT */) { 149 | root.codegenNode = child.codegenNode; 150 | } 151 | else { 152 | root.codegenNode = root.children[0]; 153 | } 154 | } 155 | function transform(root, option = {}) { 156 | const context = createTransFormContext(root, option); 157 | traverseNode(root, context); 158 | createRootCodegen(root); 159 | root.helpers = [...context.helpers.keys()]; 160 | } 161 | function traverseChildren(root, context) { 162 | const children = root.children; 163 | for (let i = 0; i < children.length; i++) { 164 | traverseNode(children[i], context); 165 | } 166 | } 167 | // 遍历ast,并调用 转换函数 168 | function traverseNode(root, context) { 169 | // 执行传入的转换函数 170 | const nodeTransforms = context.nodeTransforms; 171 | // 先传入 后调用 插件转换方法(闭包实现) 172 | const exitFns = []; 173 | if (nodeTransforms) { 174 | for (let i = 0; i < nodeTransforms.length; i++) { 175 | const onExit = nodeTransforms[i](root, context); 176 | onExit && exitFns.push(onExit); 177 | } 178 | } 179 | switch (root.type) { 180 | case 0 /* nodeTypes.INTERPOLATION */: 181 | context.helper(TO_DISPLAY_STRING); 182 | break; 183 | case 2 /* nodeTypes.ELEMENT */: 184 | case 4 /* nodeTypes.ROOT */: 185 | // 深度优先遍历ast 186 | traverseChildren(root, context); 187 | break; 188 | } 189 | let i = exitFns.length; 190 | while (i--) { 191 | exitFns[i](); 192 | } 193 | } 194 | // 生成 transform 上下文 195 | function createTransFormContext(root, option) { 196 | const context = { 197 | root, 198 | nodeTransforms: option.nodeTransforms || [], 199 | helpers: new Map(), 200 | helper: (key) => { 201 | context.helpers.set(key, 1); 202 | } 203 | }; 204 | return context; 205 | } 206 | 207 | function transformExpression(node) { 208 | if (node.type === 0 /* nodeTypes.INTERPOLATION */) { 209 | node.content = processExpression(node.content); 210 | } 211 | } 212 | function processExpression(node) { 213 | node.content = `_ctx.${node.content}`; 214 | return node; 215 | } 216 | 217 | function createVNodeCall(context, tag, props, children) { 218 | context.helper(CREATE_ELEMENT_VNODE); 219 | return { 220 | type: 2 /* nodeTypes.ELEMENT */, 221 | tag, 222 | props, 223 | children, 224 | }; 225 | } 226 | 227 | function transformElement(node, context) { 228 | if (node.type === 2 /* nodeTypes.ELEMENT */) { 229 | return () => { 230 | // 处理tag 231 | let vnodeTag = `'${node.tag}'`; 232 | // 处理 props 233 | let vnodeProps; 234 | const children = node.children; 235 | let vnodeChildren = children[0]; 236 | node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren); 237 | }; 238 | } 239 | } 240 | 241 | function isText(node) { 242 | return node.type === 3 /* nodeTypes.TEXT */ || node.type === 0 /* nodeTypes.INTERPOLATION */; 243 | } 244 | 245 | function transformText(node, context) { 246 | if (node.type === 2 /* nodeTypes.ELEMENT */) { 247 | return () => { 248 | const { children } = node; 249 | let currentContainer; 250 | // 遍历节点 children,把所有子节点全部收集到 COMPOUND_EXPRESSION 类型节点中(还 添加了 + ) 251 | // 收集完成后 并把 COMPOUND_EXPRESSION 类型节点 替换成 children 252 | for (let i = 0; i < children.length; i++) { 253 | let child = children[i]; 254 | if (isText(child)) { 255 | for (let j = i + 1; j < children.length; j++) { 256 | const next = children[j]; 257 | if (isText(next)) { 258 | if (!currentContainer) { 259 | currentContainer = children[i] = { 260 | type: 5 /* nodeTypes.COMPOUND_EXPRESSION */, 261 | children: [child] 262 | }; 263 | } 264 | currentContainer.children.push(' + '); 265 | currentContainer.children.push(next); 266 | children.splice(j, 1); 267 | j--; 268 | } 269 | else { 270 | currentContainer = undefined; 271 | break; 272 | } 273 | } 274 | } 275 | } 276 | }; 277 | } 278 | } 279 | 280 | function toDisplayString(value) { 281 | return String(value); 282 | } 283 | 284 | const extend = Object.assign; 285 | const isObject = (raw) => { 286 | return raw !== null && typeof raw === 'object'; 287 | }; 288 | const isString = (raw) => typeof raw === "string"; 289 | const isArray = (raw) => { 290 | return Array.isArray(raw); 291 | }; 292 | const hasChanged = (val, nVal) => { 293 | return !(Object.is(val, nVal)); 294 | }; 295 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 296 | const toHandlerKey = (str) => { 297 | return str ? `on${capitalize(str)}` : ''; 298 | }; 299 | const capitalize = (str) => { 300 | return str.charAt(0).toUpperCase() + str.slice(1); 301 | }; 302 | const camelize = (str) => { 303 | return str.replace(/-(\w)/g, (_, c) => { 304 | return c ? c.toUpperCase() : ''; 305 | }); 306 | }; 307 | const EMPTY_OBJ = {}; 308 | 309 | function generate(ast) { 310 | const context = createCodegenContext(); 311 | const { push } = context; 312 | genFunctionPreamble(ast, context); 313 | push('function '); 314 | const argList = ['_ctx', '_cache']; 315 | const signature = argList.join(', '); 316 | const functionName = 'render'; 317 | push(`${functionName}( ${signature}){`); 318 | push('return '); 319 | genNode(ast.codegenNode, context); 320 | push(`}`); 321 | return { 322 | code: context.code 323 | }; 324 | } 325 | // 生成 文本类型 326 | function genText(node, context) { 327 | const { push } = context; 328 | push(`'${node.content}'`); 329 | } 330 | // 生成 差值 {{}} 331 | function genInterpolation(node, context) { 332 | const { push, helper } = context; 333 | push(`${helper(helperMapName[TO_DISPLAY_STRING])}`); 334 | push(`(`); 335 | genNode(node.content, context); 336 | push(`)`); 337 | } 338 | // 生成 简单表达式 _ctx.foo 339 | function genSimpleExpression(node, context) { 340 | const { push } = context; 341 | push(`${node.content}`); 342 | } 343 | function genNullable(param) { 344 | return param.map(val => val ? val : 'null'); 345 | } 346 | function genNodeList(nodes, context) { 347 | const { push } = context; 348 | for (let i = 0; i < nodes.length; i++) { 349 | const node = nodes[i]; 350 | if (isString(node)) { 351 | push(node); 352 | } 353 | else { 354 | genNode(node, context); 355 | } 356 | if (i < nodes.length - 1) { 357 | push(', '); 358 | } 359 | } 360 | } 361 | function genElement(node, context) { 362 | const { push, helper } = context; 363 | const { tag, children, props } = node; 364 | push(`${helper(helperMapName[CREATE_ELEMENT_VNODE])}(`); 365 | genNodeList(genNullable([tag, props, children]), context); 366 | push(`)`); 367 | } 368 | function genCompoundExpression(node, context) { 369 | const { children } = node; 370 | const { push } = context; 371 | for (let i = 0; i < children.length; i++) { 372 | const child = children[i]; 373 | if (isString(child)) { 374 | push(child); 375 | } 376 | else { 377 | genNode(child, context); 378 | } 379 | } 380 | } 381 | function genNode(node, context) { 382 | switch (node.type) { 383 | case 3 /* nodeTypes.TEXT */: 384 | genText(node, context); 385 | break; 386 | case 0 /* nodeTypes.INTERPOLATION */: 387 | genInterpolation(node, context); 388 | break; 389 | case 1 /* nodeTypes.SIMPLE_EXPRESSION */: 390 | genSimpleExpression(node, context); 391 | break; 392 | case 2 /* nodeTypes.ELEMENT */: 393 | genElement(node, context); 394 | break; 395 | case 5 /* nodeTypes.COMPOUND_EXPRESSION */: 396 | genCompoundExpression(node, context); 397 | break; 398 | } 399 | } 400 | // 生成上下文对象 401 | function createCodegenContext(ast) { 402 | const context = { 403 | code: '', 404 | push: (source) => { 405 | context.code += source; 406 | }, 407 | helper: (key) => `_${key}` 408 | }; 409 | return context; 410 | } 411 | // 生成导入语句 412 | function genFunctionPreamble(node, context) { 413 | const { push } = context; 414 | const bindging = 'vue'; 415 | const aliasHelpers = ((s) => s = `${helperMapName[s]}: _${helperMapName[s]}`); 416 | if (node.helpers.length > 0) { 417 | push(`const { ${node.helpers.map(aliasHelpers).join(', ')} } = ${bindging}`); 418 | } 419 | push('\n'); 420 | push('return '); 421 | } 422 | 423 | function baseCompile(template) { 424 | const ast = baseParse(template); 425 | transform(ast, { 426 | nodeTransforms: [transformExpression, transformElement, transformText] 427 | }); 428 | return generate(ast); 429 | } 430 | 431 | const publicPropertiesMap = { 432 | $el: (i) => { return i.vnode.el; }, 433 | $slots: (i) => { return i.slots; }, 434 | $props: (i) => { return i.props; } 435 | }; 436 | const PublicInstanceProxyHandlers = { 437 | // @ts-ignore 438 | get({ _: instance }, key, receiver) { 439 | const { setupState, props } = instance; 440 | if (hasOwn(setupState, key)) { 441 | return setupState[key]; 442 | } 443 | if (hasOwn(props, key)) { 444 | return props[key]; 445 | } 446 | const publicGetter = publicPropertiesMap[key]; 447 | if (publicGetter) { 448 | return publicGetter(instance); 449 | } 450 | } 451 | }; 452 | 453 | function initProps(instance, rawProps) { 454 | instance.props = rawProps || {}; 455 | } 456 | 457 | // 当前激活的副作用函数对象 458 | let activeEffect; 459 | // 是否应该收集依赖 stop功能会用到 460 | let shouldTrack; 461 | class ReactiveEffect { 462 | constructor(fn, scheduler) { 463 | this.scheduler = scheduler; 464 | this.deps = []; 465 | this.active = true; 466 | this._fn = fn; 467 | } 468 | run() { 469 | // stop 后 this.active = false,直接return 470 | // 由于 shouldTrack = false 所以不会 再 track 471 | if (!this.active) { 472 | return this._fn(); 473 | } 474 | shouldTrack = true; 475 | activeEffect = this; 476 | const res = this._fn(); 477 | shouldTrack = false; 478 | return res; 479 | } 480 | stop() { 481 | if (this.active) { 482 | if (this.onStop) { 483 | this.onStop(); 484 | } 485 | cleanupEffect(this); 486 | this.active = false; 487 | } 488 | } 489 | } 490 | function cleanupEffect(effect) { 491 | effect.deps.forEach(value => { 492 | value.delete(effect); 493 | }); 494 | } 495 | const effect = (fn, options = {}) => { 496 | const _effect = new ReactiveEffect(fn, options.scheduler); 497 | // 降配置传递给 _effect 498 | extend(_effect, options); 499 | _effect.run(); 500 | // 给 runner 上挂上自己,stop时使用 501 | const runner = _effect.run.bind(_effect); 502 | runner.effect = _effect; 503 | return runner; 504 | }; 505 | let targetMap = new Map(); 506 | /** 507 | * 依赖收集 508 | * @param target 509 | * @param key 510 | */ 511 | const track = (target, key) => { 512 | if (!isTracking()) { 513 | return; 514 | } 515 | let depsMap = targetMap.get(target); 516 | if (!depsMap) { 517 | depsMap = new Map(); 518 | targetMap.set(target, depsMap); 519 | } 520 | let dep = depsMap.get(key); 521 | if (!dep) { 522 | dep = new Set(); 523 | depsMap.set(key, dep); 524 | } 525 | trackEffects(dep); 526 | }; 527 | function trackEffects(dep) { 528 | if (!dep.has(activeEffect)) { 529 | // 当前激活的副作用函数对象作为依赖收集起来 530 | dep.add(activeEffect); 531 | // 反向收集activeEffect的dep,使得stop时可以找到对应副作用函数 532 | activeEffect.deps.push(dep); 533 | } 534 | } 535 | function isTracking() { 536 | return shouldTrack && activeEffect !== undefined; 537 | } 538 | /** 539 | * 通知派发 540 | * @param target 541 | * @param key 542 | * @param value 543 | */ 544 | const trigger = (target, key, value) => { 545 | let depsMap = targetMap.get(target); 546 | let dep = depsMap.get(key); 547 | triggerEffects(dep); 548 | }; 549 | function triggerEffects(dep) { 550 | // 将 target 某个key的所有依赖全部执行一遍 551 | for (let effect of dep) { 552 | if (effect.scheduler) { 553 | effect.scheduler(); 554 | } 555 | else { 556 | effect.run(); 557 | } 558 | } 559 | } 560 | 561 | const createGetter = (isReadonly = false, isShallow = false) => { 562 | return function get(target, key, receiver) { 563 | // 根据参数确定是否为readonly 564 | if (key === ReactiveFlags.IS_REACTIVE) { 565 | return !isReadonly; 566 | } 567 | if (key === ReactiveFlags.IS_READONLY) { 568 | return isReadonly; 569 | } 570 | const res = Reflect.get(target, key); 571 | // readonly 只能读,不会set 不trigger 也就不需要 track 572 | if (!isReadonly) { 573 | // 依赖收集 574 | track(target, key); 575 | } 576 | // shallowReadonly 或 shallowReactive 都直接返回 577 | if (isShallow) { 578 | return res; 579 | } 580 | // 处理存在子对象获子数组情况,递归调用 581 | // 深readonly和深reactive 582 | if (isObject(res)) { 583 | return isReadonly ? readonly(res) : reactive(res); 584 | } 585 | return res; 586 | }; 587 | }; 588 | const createSetter = () => { 589 | return function set(target, key, value, receiver) { 590 | Reflect.set(target, key, value); 591 | // 派发通知 592 | trigger(target, key); 593 | return true; 594 | }; 595 | }; 596 | const get = createGetter(); 597 | const set = createSetter(); 598 | const readonlyGetter = createGetter(true); 599 | const shallowReadonlyGetter = createGetter(true, true); 600 | const mutableHandlers = { 601 | get, 602 | set 603 | }; 604 | const readonlyHandlers = { 605 | get: readonlyGetter, 606 | set(target, key, value, receiver) { 607 | console.warn('readonlyHandlers'); 608 | return true; 609 | } 610 | }; 611 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 612 | get: shallowReadonlyGetter, 613 | }); 614 | 615 | var ReactiveFlags; 616 | (function (ReactiveFlags) { 617 | ReactiveFlags["IS_REACTIVE"] = "__v_reactive"; 618 | ReactiveFlags["IS_READONLY"] = "__v_readonly"; 619 | })(ReactiveFlags || (ReactiveFlags = {})); 620 | const reactive = (raw) => { 621 | return createActiveObject(raw, mutableHandlers); 622 | }; 623 | const readonly = (raw) => { 624 | return createActiveObject(raw, readonlyHandlers); 625 | }; 626 | const shallowReadonly = (raw) => { 627 | return createActiveObject(raw, shallowReadonlyHandlers); 628 | }; 629 | const createActiveObject = (raw, baseHandlers) => { 630 | return new Proxy(raw, baseHandlers); 631 | }; 632 | 633 | function emit(instance, event, ...arg) { 634 | const { props } = instance; 635 | const handlerName = toHandlerKey(camelize(event)); 636 | const handler = props[handlerName]; 637 | handler && handler(...arg); 638 | } 639 | 640 | function initSlots(instance, children) { 641 | if (instance.vnode.shapeFlag & 16 /* shapeFlags.SLOTS_CHILDREN */) { 642 | normalizeObjectSlots(children, instance.slots); 643 | } 644 | } 645 | function normalizeObjectSlots(children, slot) { 646 | for (let key in children) { 647 | const value = children[key]; 648 | slot[key] = (props) => normalizeSlots(value(props)); 649 | } 650 | } 651 | function normalizeSlots(value) { 652 | return isArray(value) ? value : [value]; 653 | } 654 | 655 | class refImpl { 656 | constructor(value) { 657 | this.dep = new Set(); 658 | this.__v_isRef = true; 659 | this._value = convert(value); 660 | this._rawValue = this._value; 661 | } 662 | get value() { 663 | if (isTracking()) { 664 | // 依赖收集 665 | trackEffects(this.dep); 666 | } 667 | return this._value; 668 | } 669 | set value(newValue) { 670 | if (hasChanged(this._rawValue, newValue)) { 671 | this._value = convert(newValue); 672 | this._rawValue = this._value; 673 | // 派发更新 674 | triggerEffects(this.dep); 675 | } 676 | } 677 | } 678 | const convert = (value) => { 679 | return isObject(value) ? reactive(value) : value; 680 | }; 681 | function ref(raw) { 682 | return new refImpl(raw); 683 | } 684 | function isRef(ref) { 685 | return !!ref.__v_isRef; 686 | } 687 | function proxyRefs(rawWithRefs) { 688 | return new Proxy(rawWithRefs, { 689 | get(target, key) { 690 | // 当访问的值是ref对象,返回.value,否则直接返回 691 | const res = Reflect.get(target, key); 692 | return isRef(res) ? res.value : res; 693 | }, 694 | set(target, key, value, receiver) { 695 | // 当设置的值是ref,且新值不是ref 696 | let targetVal = Reflect.get(target, key); 697 | if (isRef(targetVal) && !isRef(value)) { 698 | targetVal.value = value; 699 | } 700 | else { 701 | targetVal = value; 702 | } 703 | return true; 704 | } 705 | }); 706 | } 707 | 708 | let compiler; 709 | function createComponentInstance(vnode, parent) { 710 | const instance = { 711 | vnode, 712 | type: vnode.type, 713 | setupState: {}, 714 | props: {}, 715 | next: null, 716 | provides: parent ? parent.provides : {}, 717 | parent, 718 | isMounted: false, 719 | subTree: {}, 720 | emit: (event, ...arg) => { }, 721 | slots: {} 722 | }; 723 | instance.emit = emit.bind(null, instance); 724 | return instance; 725 | } 726 | function setupComponent(instance) { 727 | // 初始化处理 props 728 | // initProps 729 | initProps(instance, instance.vnode.props); 730 | // 初始化处理插槽 731 | initSlots(instance, instance.vnode.children); 732 | // 创建一个有状态的组件 733 | setStatefulComponent(instance); 734 | } 735 | function setStatefulComponent(instance) { 736 | // 获取原始组件对象(注意这里并不是组件实例) 737 | const component = instance.type; 738 | // 创建要给组件实例代理,使得render方法内能够通过this访问组件实例,如this.$el等 739 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 740 | // 获取原始组件对象的 setup 方法 741 | const setup = component.setup; 742 | if (setup) { 743 | // diao yong setup qian she zhi instance 744 | setCurrentInstance(instance); 745 | const setupResult = setup(shallowReadonly(instance.props), { 746 | emit: instance.emit 747 | }); 748 | setCurrentInstance(null); 749 | // 处理setup结果 750 | handleSetupResult(instance, setupResult); 751 | } 752 | } 753 | function handleSetupResult(instance, setupResult) { 754 | // 如果setup结果,是一个对象,就把结果挂载到instance上 755 | if (isObject(setupResult)) { 756 | instance.setupState = setupResult; 757 | } 758 | if (typeof setupResult === "function") { 759 | // 如果返回的是 function 的话,那么绑定到 render 上 760 | // 认为是 render 逻辑 761 | // setup(){ return ()=>(h("div")) } 762 | instance.render = setupResult; 763 | } 764 | else if (typeof setupResult === "object") { 765 | // 返回的是一个对象的话 766 | // 先存到 setupState 上 767 | // 先使用 @vue/reactivity 里面的 proxyRefs 768 | // 后面我们自己构建 769 | // proxyRefs 的作用就是把 setupResult 对象做一层代理 770 | // 方便用户直接访问 ref 类型的值 771 | // 比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了,而不需要在 count.value 772 | // 这里也就是官网里面说到的自动结构 Ref 类型 773 | instance.setupState = proxyRefs(setupResult); 774 | } 775 | // 处理渲染函数render具体方法,渲染函数可能来自于模板编译、setup返回、render的option 776 | finishComponentSetup(instance); 777 | } 778 | // 最后处理渲染函数方法,自此组件setup相关初始化流程结束 779 | function finishComponentSetup(instance) { 780 | // 组件原始对象 781 | const component = instance.type; 782 | // render 优先级 : setup的返回render,组件内option的render,template 783 | // 如果组件 instance上没有render(setup返回渲染函数) 784 | if (!instance.render) { 785 | // 如果 compile 有值 并且当然组件没有 render 函数(组件内option的render), 786 | // 那么就需要把 template 编译成 render 函数 787 | if (compiler && !component.render) { 788 | if (component.template) { 789 | // 这里就是 runtime 模块和 compile 模块结合点 790 | const template = component.template; 791 | component.render = compiler(template); 792 | } 793 | } 794 | instance.render = component.render; 795 | } 796 | } 797 | let currentInstance = null; 798 | function getCurrentInstance() { 799 | return currentInstance; 800 | } 801 | function setCurrentInstance(instance) { 802 | currentInstance = instance; 803 | } 804 | function registryRuntimeCompiler(_compiler) { 805 | compiler = _compiler; 806 | } 807 | 808 | const TEXT = Symbol('TEXT'); 809 | const FRAGMENT = Symbol('FRAGMENT'); 810 | function createVNode(type, props, children) { 811 | const vnode = { 812 | __is_VNode: true, 813 | type: type, 814 | props, 815 | children, 816 | components: null, 817 | shapeFlag: getShapeFlag(type), // 设置初始时点的 shapeFlag 818 | }; 819 | if (isString(vnode.children)) { 820 | vnode.shapeFlag |= 4 /* shapeFlags.TEXT_CHILDREN */; 821 | } 822 | if (isArray(vnode.children)) { 823 | vnode.shapeFlag |= 8 /* shapeFlags.ARRAY_CHILDREN */; 824 | } 825 | // 判断slots 826 | if (vnode.shapeFlag & 2 /* shapeFlags.STATEFUL_COMPONENT */) { 827 | if (isObject(vnode.children)) { 828 | vnode.shapeFlag |= 16 /* shapeFlags.SLOTS_CHILDREN */; 829 | } 830 | } 831 | return vnode; 832 | } 833 | function getShapeFlag(type) { 834 | if (isString(type)) { 835 | return 1 /* shapeFlags.ELEMENT */; 836 | } 837 | if (isObject(type)) { 838 | return 2 /* shapeFlags.STATEFUL_COMPONENT */; 839 | } 840 | } 841 | function createTextVNode(children) { 842 | return createVNode(TEXT, {}, children); 843 | } 844 | 845 | function createAppApi(render) { 846 | return function createApp(rootComponent) { 847 | return { 848 | mount(rootContainer) { 849 | // 根据根组件 rootComponent ,创建vnode 850 | const vnode = createVNode(rootComponent); 851 | // 调用 render 开始处理 vnode 和 rootContainer 直至最终渲染 852 | render(null, vnode, rootContainer); 853 | } 854 | }; 855 | }; 856 | } 857 | 858 | function shouldUpdateComponent(n1, n2) { 859 | const { props: nextProps } = n2; 860 | const { props: prevProps } = n1; 861 | for (let key in nextProps) { 862 | if (nextProps[key] !== prevProps[key]) { 863 | return true; 864 | } 865 | } 866 | return false; 867 | } 868 | 869 | let p = Promise.resolve(); 870 | let queue = new Array(); 871 | let isFlushPending = false; 872 | function nextTick(fn) { 873 | return fn ? p.then(fn) : p; 874 | } 875 | function queueJobs(job) { 876 | if (!queue.includes(job)) { 877 | queue.push(job); 878 | queueFlush(); 879 | } 880 | } 881 | function queueFlush() { 882 | // 首次触发时,isFlushPending 设置为true 883 | // 多次触发时,只添加到任务队列,然后到这里旧直接返回 884 | // 任务执行完后再设置为false,避免多次调用 flushJobs 885 | if (isFlushPending) 886 | return; 887 | isFlushPending = true; 888 | nextTick(flushJobs); 889 | } 890 | function flushJobs() { 891 | isFlushPending = false; 892 | let jobs; 893 | while ((jobs = queue.shift())) { 894 | jobs && jobs(); 895 | } 896 | } 897 | 898 | function createRenderer(option) { 899 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, setElementText: hostSetElementText, remove: hostRemove } = option; 900 | function render(n1, n2, container) { 901 | patch(n1, n2, container, null); 902 | } 903 | /** 904 | * patch方法 905 | * @param n1 旧的虚拟节点 906 | * @param n2 新的虚拟节点 907 | * @param container 908 | * @param parent 909 | */ 910 | function patch(n1, n2, container, parent, anchor) { 911 | if (!n2) { 912 | return; 913 | } 914 | // 根据 vnode 类型不同,进行不同处理 915 | // 处理 element类型 916 | const { shapeFlag, type } = n2; 917 | switch (type) { 918 | case TEXT: 919 | processText(n1, n2, container); 920 | break; 921 | case FRAGMENT: 922 | processFragment(n1, n2, container, parent); 923 | break; 924 | default: { 925 | if (shapeFlag & 1 /* shapeFlags.ELEMENT */) { 926 | processElement(n1, n2, container, parent); 927 | } 928 | // 处理组件类型 929 | if (shapeFlag & 2 /* shapeFlags.STATEFUL_COMPONENT */) { 930 | processComponent(n1, n2, container, parent); 931 | } 932 | } 933 | } 934 | } 935 | /** 936 | * 处理元素方法 937 | * @param n1 旧的虚拟节点 938 | * @param n2 新的虚拟节点 939 | * @param container 940 | * @param parent 941 | */ 942 | function processElement(n1, n2, container, parent, anchor) { 943 | if (!n1) { 944 | mountElement(n2, container, parent); 945 | } 946 | else { 947 | patchElement(n1, n2, container, parent); 948 | } 949 | } 950 | /** 951 | * 更新元素方法 952 | * @param n1 旧的虚拟节点 953 | * @param n2 新的虚拟节点 954 | * @param container 955 | */ 956 | function patchElement(n1, n2, container, parent, anchor) { 957 | console.log('patch Element'); 958 | console.log('n1'); 959 | console.log(n1); 960 | console.log('n2'); 961 | console.log(n2); 962 | const el = (n2.el = n1.el); 963 | const oldProps = n1.props || EMPTY_OBJ; 964 | const newProps = n2.props || EMPTY_OBJ; 965 | patchChildren(n1, n2, el, parent); 966 | patchProps(oldProps, newProps, el); 967 | } 968 | function patchChildren(n1, n2, container, parent, anchor) { 969 | const prevShapeFlag = n1.shapeFlag; 970 | const c1 = n1.children; 971 | const shapeFlag = n2.shapeFlag; 972 | const c2 = n2.children; 973 | // 新子节点是文本 974 | if (shapeFlag & 4 /* shapeFlags.TEXT_CHILDREN */) { 975 | // 老的子节点是数组,则卸载 976 | if (prevShapeFlag & 8 /* shapeFlags.ARRAY_CHILDREN */) { 977 | unmountChildren(c1); 978 | } 979 | // 无论老的子节点是数组,还是文本,都替换新文本 980 | if (c1 !== c2) { 981 | hostSetElementText(container, c2); 982 | } 983 | } 984 | else { 985 | // 新的是数组,老的是文本,就清空文本,挂载新子节点 986 | if (prevShapeFlag & 4 /* shapeFlags.TEXT_CHILDREN */) { 987 | hostSetElementText(container, ''); 988 | mountChildren(c2, container, parent); 989 | } 990 | else { 991 | //新老子节点都是数组 开始diff 992 | patchKeyedChildren(c1, c2, container, parent); 993 | } 994 | } 995 | } 996 | function patchKeyedChildren(c1, c2, container, parent, anchor) { 997 | let indexStart = 0; 998 | let oldIndexEnd = c1.length - 1; 999 | let newIndexEnd = c2.length - 1; 1000 | let newChildLen = c2.length; 1001 | // 头部扫描,当 oldIndexEnd 大于于 newIndexEnd 或 newChildLen 停止 1002 | // 当节点不同,停止 1003 | while (indexStart <= oldIndexEnd && indexStart <= newIndexEnd) { 1004 | // indexStart 指向的新旧虚拟节点相同,则递归patch 1005 | if (isSameVNode(c1[indexStart], c2[indexStart])) { 1006 | patch(c1[indexStart], c2[indexStart], container, parent); 1007 | } 1008 | else { 1009 | break; 1010 | } 1011 | // 移动指针 1012 | indexStart++; 1013 | } 1014 | // 头尾部扫描,当 oldIndexEnd 大于于 newIndexEnd 或 newChildLen 停止 1015 | // 当节点不同,停止 1016 | while (indexStart <= oldIndexEnd && indexStart <= newIndexEnd) { 1017 | // indexStart 指向的新旧虚拟节点相同,则递归patch 1018 | if (isSameVNode(c1[indexStart], c2[indexStart])) { 1019 | patch(c1[indexStart], c2[indexStart], container, parent); 1020 | } 1021 | else { 1022 | break; 1023 | } 1024 | // 移动指针 1025 | oldIndexEnd--; 1026 | newIndexEnd--; 1027 | } 1028 | // 头部扫描 与 尾部扫描结束后,根据指针指向情况 1029 | // 处理头部节点序列、头部节点序列的新增或修改情况 1030 | // 从逻辑上来看 头部扫描 与 尾部扫描 是为了达到将那些 1031 | // 没有发生移动的节点预先处理的目的,此时处理过后 1032 | // 新旧虚拟节点数组中,没有被扫描的中间部分,才包含着移动节点的情况 1033 | // 头部扫描 与 尾部扫描 节点新增 1034 | // 此时 indexStart > oldIndexEnd && indexStart <= newIndexEnd 1035 | // 此时 indexStart ~ newIndexEnd 之间为新增节点 1036 | // oE oE 1037 | // (a b) 或 (a b) 1038 | // (a b) c (a b) c d 1039 | // s/nE s nE 1040 | if (indexStart > oldIndexEnd) { 1041 | if (indexStart <= newIndexEnd) { 1042 | // 挂载新的节点 1043 | let nextPos = indexStart + 1; 1044 | nextPos < newChildLen ? c2[nextPos].el : null; 1045 | while (indexStart <= newIndexEnd) { 1046 | patch(null, c2[indexStart], container, parent); 1047 | indexStart++; 1048 | } 1049 | } 1050 | } 1051 | else if (indexStart > newIndexEnd) { 1052 | // 头部扫描 与 尾部扫描 旧节点删除 1053 | // 此时 indexStart > newIndexEnd && indexStart <= oldIndexEnd 1054 | // 此时 indexStart ~ oldIndexEnd 之间为需要删除节点 1055 | // oE oE 1056 | // (a b) c 或 (a b) c d 1057 | // (a b) (a b) 1058 | // nE s nE s 1059 | if (indexStart <= oldIndexEnd) { 1060 | while (indexStart <= oldIndexEnd) { 1061 | hostRemove(c1[indexStart].el); 1062 | indexStart++; 1063 | } 1064 | } 1065 | } 1066 | else { 1067 | // 处理中间部分节点序列 1068 | // 1069 | let s1 = indexStart; 1070 | let s2 = indexStart; 1071 | // 新的中间部分节点序列数量 1072 | let newChildNum = newIndexEnd - indexStart + 1; 1073 | // 已经处理过的中间节点序列数量 1074 | let patched = 0; 1075 | // 新的中间部分节点序列映射表 1076 | let newToIndexMap = new Map(); 1077 | // 建立映射 1078 | for (let i = 0; i < s2; i++) { 1079 | if (c2[i].key) { 1080 | newToIndexMap.set(c2.key, i); 1081 | } 1082 | } 1083 | // 建立映射表。该映射表的索引对应新的中间部分节点序列 1084 | // 值对于旧的中间部分节点序列 1085 | let newIndexToOldIndexMap = new Array(newChildNum); 1086 | // 初始化 newIndexToOldIndexMap, newIndexToOldIndexMap[i] === 0 表示节点新增 1087 | for (let i = 0; i <= newChildNum; i++) 1088 | newIndexToOldIndexMap[i] = 0; 1089 | // 节点是否移动 1090 | let move = false; 1091 | let newIndexSoFar = 0; 1092 | // 遍历旧中间部分节点序列 1093 | for (let i = 0; i < s1; i++) { 1094 | const prevChild = c1[i]; 1095 | // 新的节点序列比旧的先处理完,旧的剩余的统统删除 1096 | if (patched >= newChildNum) { 1097 | hostRemove(prevChild.el); 1098 | continue; 1099 | } 1100 | // 当前旧节点在新序列中索引 1101 | // 这里是用旧的节点去新的节点序列中查找,看是否找到 1102 | let oldInNewIndex; 1103 | // 如果传了key,就用映射表,否则就要遍历新的序列 1104 | if (prevChild.key) { 1105 | oldInNewIndex = newToIndexMap.get(prevChild.key); 1106 | } 1107 | else { 1108 | for (let j = 0; j <= s2; j++) { 1109 | if (isSameVNode(prevChild, c2[j])) { 1110 | oldInNewIndex = j; 1111 | break; 1112 | } 1113 | } 1114 | } 1115 | // 旧的节点不在新的节点序列中,删除旧节点 1116 | if (oldInNewIndex === null) { 1117 | hostRemove(prevChild.el); 1118 | } 1119 | else { 1120 | if (oldInNewIndex >= newIndexSoFar) { 1121 | newIndexSoFar = oldInNewIndex; 1122 | } 1123 | else { 1124 | move = true; 1125 | } 1126 | // 将节点在旧的序列中位置记录到对应的‘新的’映射表中, 1127 | // 这里+1 是为了避免 第一个就匹配到,和初始值作区分 1128 | newIndexToOldIndexMap[oldInNewIndex - s2] = i + 1; 1129 | // 旧的节点在新的节点序列中,递归patch 1130 | patch(prevChild, c2[oldInNewIndex], container, parent); 1131 | // 记录已处理的新节点数量 1132 | patched++; 1133 | } 1134 | } 1135 | // 根据 newIndexToOldIndexMap 计算最长递增子序列(返回值是新中间节点序列中最长递增子序列的索引组成的数组) 1136 | // 2 3 4 1137 | // a,b,(c,d,e),f,g 1138 | // a,b,(e,c,d),f,g 1139 | // 0 1 2 1140 | // 最长子序列: [1,2] 1141 | // newIndexToOldIndexMap:[5,3,4](加了1) 1142 | let increasingNewIndexSequence = move ? getSequence(newIndexToOldIndexMap) : []; 1143 | let j = increasingNewIndexSequence.length - 1; 1144 | // 倒序遍历新的中间节点序列,倒序是为了保证移动时锚点是稳定的,正序遍历无法确定锚点是否稳定 1145 | for (let i = newChildNum; i >= 0; i--) { 1146 | let nextIndex = i + s2; 1147 | let nexChild = c2[nextIndex]; 1148 | let anchor = nextIndex + 1 > newChildLen ? null : c2[nextIndex + 1].el; 1149 | // 新增的节点 1150 | if (newIndexToOldIndexMap[i] === 0) { 1151 | patch(null, nexChild, container, parent); 1152 | } 1153 | // 新的节点 在递增子序列中找得到 就不需要移动 1154 | if (move) { 1155 | if (j < 0 || increasingNewIndexSequence[j] !== i) { 1156 | // 移动节点 1157 | hostInsert(nexChild.el, container, anchor); 1158 | } 1159 | else { 1160 | j--; 1161 | } 1162 | } 1163 | } 1164 | } 1165 | } 1166 | function isSameVNode(n1, n2) { 1167 | return n1.type === n2.type && n1.key === n2.key; 1168 | } 1169 | function unmountChildren(children) { 1170 | for (let i = 0; i < children.length; i++) { 1171 | const el = children[i].el; 1172 | hostRemove(el); 1173 | } 1174 | } 1175 | /** 1176 | * 处props 1177 | */ 1178 | function patchProps(oldProps, newProps, el) { 1179 | if (oldProps !== newProps) { 1180 | for (let key in newProps) { 1181 | const prevProps = oldProps[key]; 1182 | const nextProps = newProps[key]; 1183 | // props 被修改为了 null 或 undefined,我们需要删除 1184 | if (nextProps === null || nextProps === undefined) { 1185 | hostPatchProp(el, key, prevProps, null); 1186 | } 1187 | // props 发生了改变 'foo' => 'new foo',我们需要修改 1188 | if (prevProps !== nextProps) { 1189 | hostPatchProp(el, key, prevProps, nextProps); 1190 | } 1191 | } 1192 | if (EMPTY_OBJ !== oldProps) { 1193 | for (let key in oldProps) { 1194 | const prevProps = oldProps[key]; 1195 | const nextProps = newProps[key]; 1196 | // props 在新的VNode中不存在,而旧的VNode中还存在,则删除 1197 | if (!nextProps) { 1198 | hostPatchProp(el, key, prevProps, nextProps); 1199 | } 1200 | } 1201 | } 1202 | } 1203 | } 1204 | /** 1205 | * 挂载元素方法 1206 | * @param vnode 1207 | * @param container 1208 | * @param parent 1209 | */ 1210 | function mountElement(vnode, container, parent, anchor) { 1211 | // const el = (vnode.el = document.createElement(vnode.type)) 1212 | const el = (vnode.el = hostCreateElement(vnode.type)); 1213 | let { children, shapeFlag } = vnode; 1214 | // 如果是文本元素就插入 1215 | if (shapeFlag & 4 /* shapeFlags.TEXT_CHILDREN */) { 1216 | el.textContent = children; 1217 | } 1218 | // 是数组就递归 patch 子节点 1219 | if (shapeFlag & 8 /* shapeFlags.ARRAY_CHILDREN */) { 1220 | mountChildren(vnode.children, el, parent); 1221 | } 1222 | // 处理属性 1223 | const { props } = vnode; 1224 | for (let key in props) { 1225 | let val = props[key]; 1226 | hostPatchProp(el, key, null, val); 1227 | } 1228 | // insert the el to container 1229 | hostInsert(el, container); 1230 | } 1231 | function mountChildren(children, container, parent, anchor) { 1232 | children.forEach((elm) => { 1233 | patch(null, elm, container, parent); 1234 | }); 1235 | } 1236 | /** 1237 | * 处理组件方法 1238 | * @param n1 旧的虚拟节点 1239 | * @param n2 新的虚拟节点 1240 | * @param container 1241 | * @param parent 1242 | */ 1243 | function processComponent(n1, n2, container, parent, anchor) { 1244 | if (!n1) { 1245 | mountComponent(n2, container, parent); 1246 | } 1247 | else { 1248 | // 更新组件 1249 | updateComponent(n1, n2); 1250 | } 1251 | } 1252 | function updateComponent(n1, n2) { 1253 | const instance = (n2.components = n1.components); 1254 | // 对比props 看是否需要更新 1255 | if (shouldUpdateComponent(n1, n2)) { 1256 | instance.next = n2; 1257 | // 更新组件 1258 | instance.update(); 1259 | } 1260 | else { 1261 | // 不更新,也要跟新实力上的el、vnode 1262 | n2.el = n1.el; 1263 | instance.vnode = n2; 1264 | } 1265 | } 1266 | /** 1267 | * 挂载组件方法 1268 | * @param vnode 1269 | * @param container 1270 | * @param parent 1271 | */ 1272 | function mountComponent(vnode, container, parent, anchor) { 1273 | // 创建组件实例 1274 | const instance = (vnode.components = createComponentInstance(vnode, parent)); 1275 | // 开始 处理组件setup 1276 | setupComponent(instance); 1277 | // 开始处理 setup 运行完成后内涵的子节点 1278 | // 可以理解初始化时,我们处理的是根节点组件与容器 1279 | // 这里就是处理根组件下的子组件了 1280 | setupRenderEffect(instance, vnode, container); 1281 | } 1282 | function setupRenderEffect(instance, vnode, container, anchor) { 1283 | // 缓存runner,在组件更新时调用 1284 | // @ts-ignore 1285 | // @ts-ignore 1286 | instance.update = effect(() => { 1287 | // 调用render函数,拿到子树vnode,这个值可能是组件也可能是元素或其他, 1288 | // 但是他一定是上一轮的子树 1289 | // 初始化逻辑 1290 | if (!instance.isMounted) { 1291 | const subTree = (instance.subTree = instance.render.call(instance.proxy, instance.proxy)); 1292 | // 再次 patch,处理子树 1293 | patch(null, subTree, container, instance); 1294 | // 记录根组件对应的el 1295 | vnode.el = subTree.el; 1296 | instance.isMounted = true; 1297 | } 1298 | else { 1299 | // 更新el、props、vnode 1300 | // 父组件初次触发更新时,父组件更新它的element,再走到更新children逻辑 1301 | // 最后在updateComponent中会判断props是否改变 1302 | // 再次调用instance.update进入这里更新子组件 1303 | const { next } = instance; 1304 | if (next) { 1305 | // 更新el 1306 | next.el = vnode.el; 1307 | updateComponentPreRender(instance, next); 1308 | } 1309 | const subTree = instance.render.call(instance.proxy, instance.proxy); 1310 | // 更新逻辑 1311 | const prevSubTree = instance.subTree; 1312 | instance.subTree = subTree; 1313 | // 再次 patch,处理子树 1314 | patch(prevSubTree, subTree, container, instance); 1315 | // 记录根组件对应的el 1316 | vnode.el = subTree.el; 1317 | } 1318 | }, { 1319 | // @ts-ignore 1320 | scheduler: () => { 1321 | console.log('scheduler update'); 1322 | queueJobs(instance.update); 1323 | } 1324 | }); 1325 | } 1326 | function updateComponentPreRender(instance, nextVNode) { 1327 | instance.vnode = nextVNode; 1328 | instance.next = null; 1329 | instance.props = nextVNode.props; 1330 | } 1331 | /** 1332 | * 处理fragment 1333 | * @param n1 旧的虚拟节点 1334 | * @param n2 新的虚拟节点 1335 | * @param container 1336 | * @param parent 1337 | */ 1338 | function processFragment(n1, n2, container, parent, anchor) { 1339 | mountChildren(n2.children, container, parent); 1340 | } 1341 | /** 1342 | * 处理文本方法 1343 | * @param n1 旧的虚拟节点 1344 | * @param n2 新的虚拟节点 1345 | * @param container 1346 | */ 1347 | function processText(n1, n2, container) { 1348 | const { children } = n2; 1349 | const text = n2.el = document.createTextNode(children); 1350 | container.append(text); 1351 | } 1352 | return { 1353 | createApp: createAppApi(render) 1354 | }; 1355 | } 1356 | function getSequence(arr) { 1357 | const p = arr.slice(); 1358 | const result = [0]; 1359 | let i, j, u, v, c; 1360 | const len = arr.length; 1361 | for (i = 0; i < len; i++) { 1362 | const arrI = arr[i]; 1363 | if (arrI !== 0) { 1364 | j = result[result.length - 1]; 1365 | if (arr[j] < arrI) { 1366 | p[i] = j; 1367 | result.push(i); 1368 | continue; 1369 | } 1370 | u = 0; 1371 | v = result.length - 1; 1372 | while (u < v) { 1373 | c = (u + v) >> 1; 1374 | if (arr[result[c]] < arrI) { 1375 | u = c + 1; 1376 | } 1377 | else { 1378 | v = c; 1379 | } 1380 | } 1381 | if (arrI < arr[result[u]]) { 1382 | if (u > 0) { 1383 | p[i] = result[u - 1]; 1384 | } 1385 | result[u] = i; 1386 | } 1387 | } 1388 | } 1389 | u = result.length; 1390 | v = result[u - 1]; 1391 | while (u-- > 0) { 1392 | result[u] = v; 1393 | v = p[v]; 1394 | } 1395 | return result; 1396 | } 1397 | 1398 | function h(rootComponent, props, children) { 1399 | return createVNode(rootComponent, props, children); 1400 | } 1401 | 1402 | function renderSlots(slots, name, props) { 1403 | const slot = slots[name]; 1404 | if (slot) { 1405 | return createVNode(FRAGMENT, {}, slot(props)); 1406 | } 1407 | } 1408 | 1409 | function provide(key, val) { 1410 | var _a; 1411 | const instance = getCurrentInstance(); 1412 | if (instance) { 1413 | let { provides } = instance; 1414 | let parentProvides = (_a = instance.parent) === null || _a === void 0 ? void 0 : _a.provides; 1415 | if (provides === parentProvides) { 1416 | // 这里要解决一个问题 1417 | // 当父级 key 和 爷爷级别的 key 重复的时候,对于子组件来讲,需要取最近的父级别组件的值 1418 | // 那这里的解决方案就是利用原型链来解决 1419 | // provides 初始化的时候是在 createComponent 时处理的,当时是直接把 parent.provides 赋值给组件的 provides 的 1420 | // 所以,如果说这里发现 provides 和 parentProvides 相等的话,那么就说明是第一次做 provide(对于当前组件来讲) 1421 | // 我们就可以把 parent.provides 作为 currentInstance.provides 的原型重新赋值 1422 | // 至于为什么不在 createComponent 的时候做这个处理,可能的好处是在这里初始化的话,是有个懒执行的效果(优化点,只有需要的时候在初始化) 1423 | provides = instance.provides = Object.create(parentProvides); 1424 | } 1425 | provides[key] = val; 1426 | } 1427 | } 1428 | function inject(key, defaultVal) { 1429 | const instance = getCurrentInstance(); 1430 | if (instance) { 1431 | let provides = instance.parent.provides; 1432 | if (key in provides) { 1433 | return provides[key]; 1434 | } 1435 | else if (defaultVal) { 1436 | if (typeof defaultVal === 'function') { 1437 | return defaultVal(); 1438 | } 1439 | else { 1440 | return defaultVal; 1441 | } 1442 | } 1443 | } 1444 | } 1445 | 1446 | function createElement(type) { 1447 | return document.createElement(type); 1448 | } 1449 | /** 1450 | * 渲染props 1451 | * @param el 1452 | * @param props 1453 | */ 1454 | function patchProp(el, key, oldVal, newVal) { 1455 | const isOn = (key) => { 1456 | return /on[A-z]/.test(key); 1457 | }; 1458 | // 处理事件 1459 | if (isOn(key)) { 1460 | const eventName = key.slice(2).toLowerCase(); 1461 | el.addEventListener(eventName, newVal); 1462 | } 1463 | else { 1464 | if (newVal === null || newVal === undefined) { 1465 | el.removeAttribute(key); 1466 | } 1467 | else { 1468 | el.setAttribute(key, newVal); 1469 | } 1470 | } 1471 | } 1472 | function insert(el, container) { 1473 | container.append(el); 1474 | } 1475 | function setElementText(container, text) { 1476 | container.textContent = text; 1477 | } 1478 | function remove(el) { 1479 | const parent = el.parentNode; 1480 | parent && parent.removeChild(el); 1481 | } 1482 | const renderer = createRenderer({ 1483 | createElement, 1484 | patchProp, 1485 | insert, 1486 | remove, 1487 | setElementText, 1488 | }); 1489 | function createApp(...args) { 1490 | return renderer.createApp(...args); 1491 | } 1492 | 1493 | var runtimeDom = /*#__PURE__*/Object.freeze({ 1494 | __proto__: null, 1495 | createApp: createApp, 1496 | createAppApi: createAppApi, 1497 | h: h, 1498 | renderSlots: renderSlots, 1499 | createTextVNode: createTextVNode, 1500 | createElementVNode: createVNode, 1501 | getCurrentInstance: getCurrentInstance, 1502 | registryRuntimeCompiler: registryRuntimeCompiler, 1503 | inject: inject, 1504 | provide: provide, 1505 | createRenderer: createRenderer, 1506 | nextTick: nextTick, 1507 | toDisplayString: toDisplayString 1508 | }); 1509 | 1510 | function compileToFunction(template) { 1511 | const { code } = baseCompile(template); 1512 | const render = new Function('vue', code)(runtimeDom); 1513 | return render; 1514 | } 1515 | registryRuntimeCompiler(compileToFunction); 1516 | 1517 | export { createApp, createAppApi, createVNode as createElementVNode, createRenderer, createTextVNode, effect, getCurrentInstance, h, inject, nextTick, provide, ref, registryRuntimeCompiler, renderSlots, toDisplayString }; 1518 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Be-Vue", 3 | "version": "1.0.0", 4 | "main": "lib/be-vue.cjs.js", 5 | "module": "lib/be-vue.esm.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "jest", 9 | "build": "rollup -c rollup.config.js", 10 | "build:watch": "rollup -c rollup.config.js --watch" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.17.8", 14 | "@babel/preset-env": "^7.16.11", 15 | "@babel/preset-typescript": "^7.16.7", 16 | "@rollup/plugin-typescript": "^8.3.1", 17 | "@types/jest": "^27.4.1", 18 | "babel-jest": "^27.5.1", 19 | "jest": "^27.5.1", 20 | "rollup": "^2.70.1", 21 | "typescript": "^4.6.2", 22 | "tslib": "^2.3.1" 23 | }, 24 | "dependencies": { 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /reactive-set_map.md: -------------------------------------------------------------------------------- 1 | `Set` 数据类型与 `Map` 数据类型和普通的对象不同,它们有自己的get和set等属性访问或修改方法; 2 | 我们使用两种数据类型时,必须是通过这些内置方法来使用的,所以这里的数据劫持和常规对象不同,在我们使用过程中, 3 | 可以拆解为两个步骤 4 | 1.访问对对应的方法,例如 `x.size`,此时会触发 `getter` 钩子,并把对应的方法返回 5 | 2.再调用返回的方法 即 `x.size()`,故而我们的依赖收集 `track` 与派发 `trigger`实际上应当在对应的方法执行时进行。 6 | 然后我们需要根据一些方法的性质和功能不同,分别来处理。 7 | ps: 这里还需要注意的时,在 `size` 方法中会访问 `this.size`,而其他的一些方法例如 `x.set(1)`,他是一个方法, 8 | 我们在使用 `proxy` 进行代理时,操作的是 `proxy`,所以这些方法内部的 `this` 指向永远是代理对象,会导致这些方法使用异常 9 | 处理方法就是针对 `size` 我们在 `getter` 中返回 `reflect.get(target,key,target)` 而其他方法则使用 `bind`, 10 | 返回 `target[key].bind(target)`。 11 | ### 访问方法 12 | set:size()√、has、forEach() √ 13 | map:get()√、size()√、has、forEach() √ 14 | ### 修改方法 15 | set:add()、clear()、delete() 16 | map:set()、clear()、delete() 17 | ### 迭代器方法 18 | values()、keys()、entries() 19 | #### 其实要实现在方法运行时劫持,vue就是把这些方法都改写了 20 | 对于那些`访问方法`,类似于普通对象的 `get` ,他们会对数据进行访问,因此,我们应当在这些方法运行时,`track` 收集那些调用这些方法的副作用函数依赖。 21 | 在 `track` 时 则是 `track(target,IERATE_KEY)`. 22 | 而对于那些`修改方法`,他们会涉及到值得修改,那么对于的读取方法的副作用也应该运行,这就类似于普通对象的 `set` 因此我们在修改方法中进行 `trigger` 派发 23 | 此外,由于 `forEach` 方法的执行,和键值对数量,`set` 大小有关,所以一切可能改变大小的操作的都应当触发 `trigger`,所以在 `forEach` 中也要 `track` 24 | 对于那些迭代器方法、或 `for....of` 迭代,他们同样是访问性质的,不用在于他们访问的是对应的迭代器方法,因此这些方法中都应该进行 `track` 25 | 具体来说 `entries` 劫持 `entries` 方法,`for....of` 劫持的是迭代器属性 `[Symbol.iterator]`,他和 `entries` 是同一个迭代方法,而 `values()`、`keys()`则各自有自己的方法来实现迭代 26 | 同样也是劫持这些对应方法的运行。 27 | `track` 时,`keys()` 只关心 `key` 变化因此用不同于 `IERATE_KEY` 的另一个 `MAP_KEY_IERATE_KEY` 来进行依赖收集,这样就可以避免当只发生 `key` 变化时,不会触发那些和 `value` 变化相关的依赖 28 | 29 | 30 | # map 中以下场景会出现数据污染,vue是如何处理的? 31 | ```` 32 | const m = new Map() 33 | 34 | const p1 = reactive(m) 35 | const p2 = ractive(new Map()) 36 | p1.set('p2',p2) 37 | effect(()=>{ 38 | console.log(m.get('p2').size) 39 | }) 40 | m.get('p2').set('foo',1) 41 | ```` 42 | 上述代码,运行时发现对原始数据进行修改时,`effect` 的副作用运行了, 43 | 这是因为在改写 `set` 时, 调用原生 `set` 方法获取结果 `target.set(key,value)` 44 | 这里是直接用传递进来的 `value` 进行操作处理,当用户传递进来的是 响应式对象的话, 45 | 会导致原始对象 `m.get('p2')` 拿到的是代理对象,此时在 `set` 则会触发 `p2` 的副作用。 46 | 解决方法 响应式对象上有一个属性(假设是 `raw` ),而原始对象上是没有的,那么就判断是否有 `raw` 47 | 即可即 `target.set(key,value.raw || value)` 48 | 49 | ### vue3 如何拦截对象的 `in` 操作符 和 `for...in` 循环 50 | `x has obj` 根据es规范,对应的操作函数是 `has` 函数,因此拦截的是 `has` 方法 而非 `get`, 51 | `for...in` 其内部迭代器的生成器方法中使用了 `ownKeys` 方法 因此拦截的是 `ownKeys` 方法 52 | ### vue3 如何对数组拦截 53 | 1.数组 常规的拦截 索引访问 拦截是 `get`,索引的设置、`length` 的修改 是在 `set` 触发, 54 | 这里 `set` 还做了细分,比如区分索引设置值是修改还是新增,新增则会修改 `length`;`length` 的修改,那些大于或等与新 `length` 索引的元素也会受到影响 55 | 这些,都是需要根据情况来 `trigger` 的 56 | 2.数组的 `for...in` 访问也是拦截 `ownKeys` ,只不过,数组的 这个 `for...in` 与 `length` 有关,所以 `for...in` 的 `track` 是作为 `length` 相关的副作用收集 57 | 2.数组的 `for...of` 劫持的是迭代器属性 `[Symbol.iterator]`,这里面访问会访问 索引 和 长度,所以 所以 `for...of` 的 `track` 是作为 `索引 和 长度 相关的副作用收集 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /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:'esm', 12 | file:pkg.module 13 | } 14 | ], 15 | plugins:[typescript()] 16 | } -------------------------------------------------------------------------------- /src/compiler-core/__test__/__snapshots__/codegen.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`codegen compound 1`] = ` 4 | Object { 5 | "code": "const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = vue 6 | return function render( _ctx, _cache){return _createElementVNode('div', null, 'czh,' + _toDisplayString(_ctx.message))}", 7 | } 8 | `; 9 | 10 | exports[`codegen element 1`] = ` 11 | Object { 12 | "code": "const { createElementVNode: _createElementVNode } = vue 13 | return function render( _ctx, _cache){return _createElementVNode('div', null, null)}", 14 | } 15 | `; 16 | 17 | exports[`codegen interpolation 1`] = ` 18 | Object { 19 | "code": "const { toDisplayString: _toDisplayString } = vue 20 | return function render( _ctx, _cache){return _toDisplayString(_ctx.czh)}", 21 | } 22 | `; 23 | 24 | exports[`codegen string 1`] = ` 25 | Object { 26 | "code": " 27 | return function render( _ctx, _cache){return 'czh'}", 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /src/compiler-core/__test__/codegen.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import {baseParse} from '../src/parse' 3 | import {generate} from '../src/codegen' 4 | import {transform} from "../src/transform"; 5 | import {transformExpression} from "../src/transform/transformExpression"; 6 | import {transformElement} from "../src/transform/transformElement"; 7 | import {transformText} from "../src/transform/transformText"; 8 | describe('generate',()=>{ 9 | test('string',()=>{ 10 | const ast = baseParse('czh') 11 | transform(ast) 12 | const node = generate(ast) 13 | // 快照测试 14 | expect(node).toMatchSnapshot() 15 | }) 16 | test('interpolation',()=>{ 17 | const ast = baseParse('{{czh}}') 18 | transform(ast,{ 19 | nodeTransforms:[transformExpression] 20 | }) 21 | const node = generate(ast) 22 | // 快照测试 23 | expect(node).toMatchSnapshot() 24 | }) 25 | test('element',()=>{ 26 | const ast = baseParse('
') 27 | transform(ast,{ 28 | nodeTransforms:[transformElement] 29 | }) 30 | const node = generate(ast) 31 | // 快照测试 32 | expect(node).toMatchSnapshot() 33 | }) 34 | 35 | 36 | test('compound',()=>{ 37 | const ast = baseParse('
czh,{{message}}
') 38 | transform(ast,{ 39 | nodeTransforms:[transformExpression,transformElement,transformText] 40 | }) 41 | const node = generate(ast) 42 | // 快照测试 43 | expect(node).toMatchSnapshot() 44 | }) 45 | }) -------------------------------------------------------------------------------- /src/compiler-core/__test__/parse.test.ts: -------------------------------------------------------------------------------- 1 | import {baseParse} from '../src/parse' 2 | import {nodeTypes} from "../src/ast"; 3 | 4 | describe('Parse',()=>{ 5 | describe('interpolation',()=>{ 6 | test('simple interpolation',()=>{ 7 | const ast = baseParse('{{message }}') 8 | expect(ast.children[0]).toStrictEqual({ 9 | type:nodeTypes.INTERPOLATION, 10 | content:{ 11 | type:nodeTypes.SIMPLE_EXPRESSION, 12 | content:'message' 13 | } 14 | }) 15 | }) 16 | }) 17 | describe('parse Element',()=>{ 18 | test('simple Element',()=>{ 19 | const ast = baseParse('
') 20 | expect(ast.children[0]).toStrictEqual({ 21 | type:nodeTypes.ELEMENT, 22 | tag:'div', 23 | children:[] 24 | }) 25 | }) 26 | }) 27 | describe('parse Text',()=>{ 28 | test('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 | describe('happy path',()=>{ 37 | test('three case one',()=>{ 38 | const ast = baseParse('

czh,{{message}}

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

czh,

{{message}}
') 60 | expect(ast.children[0]).toStrictEqual({ 61 | type:nodeTypes.ELEMENT, 62 | tag:'div', 63 | children:[ 64 | { 65 | type:nodeTypes.ELEMENT, 66 | tag:'p', 67 | children:[ 68 | { 69 | type:nodeTypes.TEXT, 70 | content:'czh,' 71 | } 72 | ] 73 | }, 74 | { 75 | type:nodeTypes.INTERPOLATION, 76 | content:{ 77 | type:nodeTypes.SIMPLE_EXPRESSION, 78 | content:'message' 79 | } 80 | } 81 | ] 82 | 83 | }) 84 | }) 85 | 86 | test('should throw err when lack end tag',()=>{ 87 | 88 | expect(()=>{ 89 | baseParse('

') 90 | }).toThrow(`lack the end tag p`) 91 | }) 92 | }) 93 | }) -------------------------------------------------------------------------------- /src/compiler-core/__test__/transform.test.ts: -------------------------------------------------------------------------------- 1 | import {baseParse} from '../src/parse' 2 | import {nodeTypes} from "../src/ast"; 3 | import {transform} from "../src/transform"; 4 | describe('transform',()=>{ 5 | test('happy path',()=>{ 6 | const ast = baseParse('
czh {{message}}
') 7 | const plugin = { 8 | nodeTransforms:[(node:any)=>{ 9 | if(node.type === nodeTypes.TEXT){ 10 | node.content = node.content + 'cool' 11 | } 12 | }] 13 | } 14 | transform(ast,plugin) 15 | const textContent = ast.children[0].children[0].content 16 | expect(textContent).toBe('czh cool') 17 | }) 18 | 19 | }) -------------------------------------------------------------------------------- /src/compiler-core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/complie' -------------------------------------------------------------------------------- /src/compiler-core/src/ast.ts: -------------------------------------------------------------------------------- 1 | import {CREATE_ELEMENT_VNODE} from "./runtimeHelpers"; 2 | 3 | export const enum nodeTypes { 4 | INTERPOLATION, 5 | SIMPLE_EXPRESSION, 6 | ELEMENT, 7 | TEXT, 8 | ROOT, 9 | COMPOUND_EXPRESSION, 10 | } 11 | export function createVNodeCall(context:any,tag:any,props:any,children:any){ 12 | context.helper(CREATE_ELEMENT_VNODE) 13 | return { 14 | type: nodeTypes.ELEMENT, 15 | tag, 16 | props, 17 | children, 18 | } 19 | } -------------------------------------------------------------------------------- /src/compiler-core/src/codegen.ts: -------------------------------------------------------------------------------- 1 | import {CREATE_ELEMENT_VNODE, helperMapName, TO_DISPLAY_STRING} from "./runtimeHelpers"; 2 | import {nodeTypes} from "./ast"; 3 | import {isString} from "../../shared"; 4 | 5 | 6 | export function generate(ast: any) { 7 | const context = createCodegenContext(ast) 8 | const {push} = context 9 | genFunctionPreamble(ast,context); 10 | push('function ') 11 | const argList = ['_ctx','_cache'] 12 | const signature = argList.join(', ') 13 | const functionName = 'render' 14 | push(`${functionName}( ${signature}){`) 15 | push('return ') 16 | genNode(ast.codegenNode,context) 17 | push(`}`) 18 | return { 19 | code:context.code 20 | } 21 | } 22 | // 生成 文本类型 23 | function genText(node:any,context:any) { 24 | const {push} = context 25 | push(`'${node.content}'`) 26 | } 27 | 28 | // 生成 差值 {{}} 29 | function genInterpolation(node:any,context:any) { 30 | const {push,helper} = context 31 | push(`${helper(helperMapName[TO_DISPLAY_STRING])}`) 32 | push(`(`) 33 | genNode(node.content, context) 34 | push(`)`) 35 | } 36 | // 生成 简单表达式 _ctx.foo 37 | function genSimpleExpression(node: any, context: any) { 38 | const {push} = context 39 | push(`${node.content}`) 40 | } 41 | 42 | function genNullable(param: any[]) { 43 | return param.map(val=>val ? val :'null') 44 | } 45 | 46 | function genNodeList(nodes: any[],context: any) { 47 | const {push} = context 48 | for (let i = 0; i < nodes.length; i++) { 49 | const node = nodes[i] 50 | if(isString(node)){ 51 | push(node) 52 | }else{ 53 | genNode(node,context) 54 | } 55 | if(i < nodes.length - 1){ 56 | push(', ') 57 | } 58 | 59 | } 60 | } 61 | 62 | function genElement(node: any, context: any) { 63 | const {push,helper} = context 64 | const {tag,children,props} = node 65 | push(`${helper(helperMapName[CREATE_ELEMENT_VNODE])}(`) 66 | genNodeList(genNullable([tag,props,children]),context) 67 | push(`)`) 68 | } 69 | 70 | function genCompoundExpression(node: any, context: any) { 71 | const {children} = node 72 | const {push} = context 73 | for (let i = 0; i < children.length; i++) { 74 | const child = children[i] 75 | if(isString(child)){ 76 | push(child) 77 | }else{ 78 | genNode(child,context) 79 | } 80 | 81 | } 82 | } 83 | 84 | function genNode(node:any,context:any){ 85 | switch (node.type) { 86 | case nodeTypes.TEXT: 87 | genText(node, context); 88 | break; 89 | case nodeTypes.INTERPOLATION: 90 | genInterpolation(node, context); 91 | break; 92 | case nodeTypes.SIMPLE_EXPRESSION: 93 | genSimpleExpression(node, context); 94 | break; 95 | case nodeTypes.ELEMENT: 96 | genElement(node, context); 97 | break; 98 | case nodeTypes.COMPOUND_EXPRESSION: 99 | genCompoundExpression(node, context); 100 | break; 101 | default: 102 | break; 103 | } 104 | 105 | } 106 | // 生成上下文对象 107 | function createCodegenContext(ast:any){ 108 | const context = { 109 | code:'', 110 | push:(source:string)=>{ 111 | context.code += source 112 | }, 113 | helper:(key:string)=>`_${key}` 114 | } 115 | return context 116 | } 117 | // 生成导入语句 118 | function genFunctionPreamble(node:any,context:any) { 119 | const { push } = context 120 | const bindging = 'vue' 121 | const aliasHelpers = ((s:any) => s = `${helperMapName[s as keyof typeof helperMapName]}: _${helperMapName[s as keyof typeof helperMapName]}`) 122 | if(node.helpers.length > 0){ 123 | push(`const { ${node.helpers.map(aliasHelpers).join(', ')} } = ${bindging}`) 124 | } 125 | push('\n') 126 | push('return ') 127 | } -------------------------------------------------------------------------------- /src/compiler-core/src/complie.ts: -------------------------------------------------------------------------------- 1 | import {baseParse} from "./parse"; 2 | import {transform} from "./transform"; 3 | import {transformExpression} from "./transform/transformExpression"; 4 | import {transformElement} from "./transform/transformElement"; 5 | import {transformText} from "./transform/transformText"; 6 | import {generate} from "./codegen"; 7 | 8 | export function baseCompile(template:string){ 9 | const ast = baseParse(template) 10 | transform(ast,{ 11 | nodeTransforms:[transformExpression,transformElement,transformText] 12 | }) 13 | return generate(ast) 14 | } -------------------------------------------------------------------------------- /src/compiler-core/src/parse.ts: -------------------------------------------------------------------------------- 1 | // 生成上下文,用于后面的解析 2 | import {nodeTypes} from "./ast"; 3 | const enum TagType { 4 | start, 5 | end 6 | } 7 | // 生成ast 上下文对象 8 | function createParserContext(content: string) { 9 | return { 10 | source:content 11 | } 12 | } 13 | // 生成ast root 14 | function createRoot(parseChildren: any) { 15 | return { 16 | children:parseChildren, 17 | type: nodeTypes.ROOT 18 | } 19 | } 20 | 21 | // 解析 {{}} 22 | function parseInterpolation(context: any) { 23 | // {{ message }} 24 | let startDelimiter = '{{' 25 | let closeDelimiter = '}}' 26 | // 获取 '}}' 位置 27 | let closeIndex = context.source.indexOf(closeDelimiter,startDelimiter.length) 28 | // 消费 '{{' 29 | advanceBy(context,startDelimiter.length) 30 | // 截取 content && 消费 content 31 | let rawContent = parseTextData(context,closeIndex - startDelimiter.length) 32 | // 消费 '}}' 33 | advanceBy(context,closeDelimiter.length) 34 | let content = rawContent.trim() 35 | return { 36 | type:nodeTypes.INTERPOLATION, 37 | content:{ 38 | type:nodeTypes.SIMPLE_EXPRESSION, 39 | content:content 40 | } 41 | } 42 | } 43 | // 消费字符串 44 | function advanceBy(context:any,index:any) { 45 | context.source = context.source.slice(index) 46 | } 47 | 48 | 49 | function startsWithEndTagOpen(source: any, tag: any) { 50 | return source.startsWith('' 73 | advanceBy(context,1) 74 | if(type === TagType.end) return 75 | return { 76 | type: nodeTypes.ELEMENT, 77 | tag:tag, 78 | children:[] 79 | } 80 | } 81 | 82 | function parseText(context: any) { 83 | let s = context.source 84 | let endIndex = s.length 85 | let endToken = ['<','{{'] 86 | // 找到 source中 <','{{' 的位置 并进一步解析出文本 87 | for (let i = 0;i < endToken.length;i++){ 88 | const index = s.indexOf(endToken[i]) 89 | if(index !== -1 && index < endIndex){ 90 | endIndex = index 91 | } 92 | } 93 | const content = parseTextData(context,endIndex) 94 | return { 95 | type: nodeTypes.TEXT, 96 | content 97 | } 98 | } 99 | function parseTextData(context: any,length:number) { 100 | const content = context.source.slice(0,length) 101 | // 消费 102 | advanceBy(context,length) 103 | return content 104 | } 105 | 106 | // 解析 children,基于有限状态机模式 107 | function parseChildren(context: any,ancestors:any) { 108 | const nodes = [] 109 | // 循环遍历children 字符串 110 | while(!isEnd(context,ancestors)){ 111 | let node 112 | let s:string = context.source 113 | // 以 {{ 则走解析插值逻辑 114 | if(s.startsWith('{{')){ 115 | node = parseInterpolation(context) 116 | }else if(s[0] === '<'){ 117 | // 以 < 开头 且第二个字母为a-z 就当做element的标签解析 118 | if(/[a-z]/i.test(s[1])){ 119 | node = parseElement(context,ancestors) 120 | } 121 | }else{ 122 | // 默认当做文本解析 123 | node = parseText(context) 124 | } 125 | nodes.push(node) 126 | } 127 | 128 | return nodes 129 | } 130 | // 是否标签闭合 131 | function isEnd(context:any,ancestors:any) { 132 | let s = context.source 133 | if(s.startsWith(`= 0;i--){ 136 | const tag = ancestors[i].tag 137 | if(startsWithEndTagOpen(s,tag)){ 138 | return true 139 | } 140 | 141 | } 142 | 143 | } 144 | return !context.source 145 | } 146 | 147 | export function baseParse(content:string) { 148 | const context = createParserContext(content) 149 | return createRoot(parseChildren(context,[])) 150 | } -------------------------------------------------------------------------------- /src/compiler-core/src/runtimeHelpers.ts: -------------------------------------------------------------------------------- 1 | export const TO_DISPLAY_STRING = Symbol('toDisplayString') 2 | export const CREATE_ELEMENT_VNODE = Symbol('createElementVNode') 3 | export const helperMapName = { 4 | [TO_DISPLAY_STRING]:'toDisplayString', 5 | [CREATE_ELEMENT_VNODE]:'createElementVNode' 6 | 7 | } -------------------------------------------------------------------------------- /src/compiler-core/src/transform.ts: -------------------------------------------------------------------------------- 1 | import {nodeTypes} from "./ast"; 2 | import {TO_DISPLAY_STRING} from "./runtimeHelpers"; 3 | 4 | function createRootCodegen(root:any) { 5 | const child = root.children[0] 6 | if(child.type === nodeTypes.ELEMENT){ 7 | root.codegenNode = child.codegenNode 8 | }else{ 9 | root.codegenNode = root.children[0] 10 | } 11 | 12 | } 13 | 14 | export function transform(root:any,option:any = {}){ 15 | const context = createTransFormContext(root,option) 16 | traverseNode(root,context) 17 | createRootCodegen(root) 18 | root.helpers = [...context.helpers.keys()] 19 | } 20 | 21 | function traverseChildren(root: any, context: any) { 22 | const children = root.children 23 | for (let i = 0; i < children.length; i++) { 24 | traverseNode(children[i], context) 25 | } 26 | 27 | } 28 | // 遍历ast,并调用 转换函数 29 | export function traverseNode(root:any,context:any){ 30 | // 执行传入的转换函数 31 | const nodeTransforms = context.nodeTransforms 32 | // 先传入 后调用 插件转换方法(闭包实现) 33 | const exitFns = [] 34 | if(nodeTransforms){ 35 | for (let i = 0;i < nodeTransforms.length;i++){ 36 | const onExit = nodeTransforms[i](root,context) 37 | onExit && exitFns.push(onExit) 38 | } 39 | } 40 | switch (root.type) { 41 | case nodeTypes.INTERPOLATION: 42 | context.helper(TO_DISPLAY_STRING) 43 | break; 44 | case nodeTypes.ELEMENT: 45 | case nodeTypes.ROOT: 46 | // 深度优先遍历ast 47 | traverseChildren(root, context); 48 | break; 49 | default: 50 | break; 51 | } 52 | let i = exitFns.length 53 | while (i--){ 54 | exitFns[i]() 55 | } 56 | 57 | } 58 | // 生成 transform 上下文 59 | export function createTransFormContext(root:any,option:any){ 60 | const context = { 61 | root, 62 | nodeTransforms:option.nodeTransforms || [], 63 | helpers:new Map(), 64 | helper:(key:string)=>{ 65 | context.helpers.set(key,1) 66 | } 67 | } 68 | return context 69 | } -------------------------------------------------------------------------------- /src/compiler-core/src/transform/transformElement.ts: -------------------------------------------------------------------------------- 1 | import {createVNodeCall, nodeTypes} from "../ast"; 2 | import {CREATE_ELEMENT_VNODE} from "../runtimeHelpers"; 3 | 4 | export function transformElement(node: any, context: any) { 5 | if (node.type === nodeTypes.ELEMENT) { 6 | return () => { 7 | // 处理tag 8 | let vnodeTag = `'${node.tag}'` 9 | // 处理 props 10 | let vnodeProps 11 | const children = node.children 12 | let vnodeChildren = children[0] 13 | node.codegenNode = createVNodeCall(context,vnodeTag,vnodeProps,vnodeChildren) 14 | } 15 | } 16 | 17 | 18 | } -------------------------------------------------------------------------------- /src/compiler-core/src/transform/transformExpression.ts: -------------------------------------------------------------------------------- 1 | import {nodeTypes} from "../ast"; 2 | 3 | export function transformExpression(node:any){ 4 | if (node.type === nodeTypes.INTERPOLATION){ 5 | node.content = processExpression(node.content) 6 | } 7 | } 8 | function processExpression(node:any){ 9 | node.content = `_ctx.${node.content}` 10 | return node 11 | } -------------------------------------------------------------------------------- /src/compiler-core/src/transform/transformText.ts: -------------------------------------------------------------------------------- 1 | import {nodeTypes} from "../ast"; 2 | import {isText} from "../utils"; 3 | 4 | export function transformText(node: any, context: any) { 5 | if (node.type === nodeTypes.ELEMENT) { 6 | return () => { 7 | const {children} = node 8 | let currentContainer 9 | // 遍历节点 children,把所有子节点全部收集到 COMPOUND_EXPRESSION 类型节点中(还 添加了 + ) 10 | // 收集完成后 并把 COMPOUND_EXPRESSION 类型节点 替换成 children 11 | for (let i = 0; i < children.length; i++) { 12 | let child = children[i] 13 | if (isText(child)) { 14 | for (let j = i + 1; j < children.length; j++) { 15 | const next = children[j] 16 | if (isText(next)) { 17 | if (!currentContainer) { 18 | currentContainer = children[i] = { 19 | type: nodeTypes.COMPOUND_EXPRESSION, 20 | children: [child] 21 | } 22 | } 23 | currentContainer.children.push(' + ') 24 | currentContainer.children.push(next) 25 | children.splice(j, 1) 26 | j-- 27 | 28 | } else { 29 | currentContainer = undefined 30 | break; 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/compiler-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import {nodeTypes} from "./ast"; 2 | 3 | export function isText(node:any){ 4 | return node.type === nodeTypes.TEXT || node.type === nodeTypes.INTERPOLATION 5 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {baseCompile} from "./compiler-core/"; 2 | import * as runtimeDom from "./runtime-dom"; 3 | export * from "./runtime-dom"; 4 | export * from "./reactivity"; 5 | import {registryRuntimeCompiler} from "./runtime-dom"; 6 | 7 | function compileToFunction(template:string){ 8 | const {code} = baseCompile(template) 9 | const render = new Function('vue',code)(runtimeDom) 10 | return render 11 | } 12 | registryRuntimeCompiler(compileToFunction) -------------------------------------------------------------------------------- /src/reactivity/__test__/computed.test.ts: -------------------------------------------------------------------------------- 1 | import {reactive} from "../reactive"; 2 | import {computed} from "../computed"; 3 | 4 | describe('test-computed', () => { 5 | test('happy path', () => { 6 | const foo = reactive({ 7 | age:18 8 | }) 9 | const computeFoo = computed(()=>{ 10 | return foo.age 11 | }) 12 | expect(computeFoo.value).toBe(18) 13 | }) 14 | 15 | test('should compute lazily', () => { 16 | const foo = reactive({ 17 | age:18 18 | }) 19 | const getter = jest.fn(()=>{ 20 | return foo.age 21 | }) 22 | const computeFoo = computed(getter) 23 | // 没访问 computeFoo.value 的话,getter 不执行 24 | expect(getter).not.toHaveBeenCalled() 25 | 26 | // 访问了 就会执行一次 getter 27 | expect(computeFoo.value).toBe(18) 28 | expect(getter).toHaveBeenCalledTimes(1) 29 | 30 | // foo.age 没变,访问了只会会执行一次 getter 31 | computeFoo.value 32 | expect(getter).toHaveBeenCalledTimes(1) 33 | 34 | // foo.age 变,访问了,会执行一次 getter 35 | foo.age = 20 36 | expect(getter).toHaveBeenCalledTimes(1) 37 | expect(computeFoo.value).toBe(20) 38 | expect(getter).toHaveBeenCalledTimes(2) 39 | 40 | computeFoo.value 41 | // foo.age 没变,访问了只会会执行一次 getter 42 | expect(getter).toHaveBeenCalledTimes(2) 43 | }) 44 | }) -------------------------------------------------------------------------------- /src/reactivity/__test__/effect.test.ts: -------------------------------------------------------------------------------- 1 | import {effect, stop} from "../effect"; 2 | import {reactive} from "../reactive"; 3 | 4 | describe('test-effect', () => { 5 | test('happy path', async () => { 6 | const user = reactive({ 7 | age:10 8 | }) 9 | let nextAge; 10 | effect(()=>{ 11 | nextAge = user.age + 1 12 | }) 13 | expect(nextAge).toBe(11) 14 | user.age++ 15 | expect(nextAge).toBe(12) 16 | }) 17 | test('effect runner', async () => { 18 | // 执行effect,能够返回一个runner的,runner是一个fn 19 | // 当执行runner 时,能够直接执行传入给effect的fn(即依赖),并拿到fn的执行结果 20 | // 在计算属性compute中会用到 21 | const test = reactive({ 22 | foo:1 23 | }) 24 | let effectTest:number = 0; 25 | const runner = effect(()=>{ 26 | effectTest = effectTest + test.foo + 1 27 | return 'runner' 28 | }) 29 | expect(effectTest).toBe(2) 30 | let res = runner() 31 | expect(effectTest).toBe(4) 32 | expect(res).toBe('runner') 33 | }) 34 | 35 | test('effect scheduler', async () => { 36 | // effect 的调度执行,在计算属性compute中会用到 37 | // 1. effect 支持传入一个包含名为 scheduler 函数的 options 38 | // 2.effect 首次执行时,传入给effect的fn(即依赖) 39 | // 3.当对应响应式对象 set 并trigger时,不执行 fn(即依赖) 而执行 options 的 scheduler 函数 40 | // 4.当执行runner时(effect的返回) 能够执行fn(即依赖) 41 | 42 | const test = reactive({ 43 | foo:1 44 | }) 45 | let run:any 46 | const scheduler = jest.fn(()=>{ 47 | run = runner 48 | }) 49 | let testScheduler = 0 50 | const runner = effect(()=>{ 51 | testScheduler = test.foo 52 | return 'scheduler' 53 | },{scheduler}) 54 | expect(scheduler).not.toHaveBeenCalled() 55 | expect(testScheduler).toBe(1) 56 | test.foo++ 57 | expect(scheduler).toHaveBeenCalledTimes(1) 58 | expect(testScheduler).toBe(1) 59 | expect(run === runner).toBeTruthy() 60 | let res = run() 61 | expect(res).toBe('scheduler') 62 | expect(testScheduler).toBe(2) 63 | 64 | }) 65 | test('effect stop', async () => { 66 | // stop方法,传入一个runner,当trigger后 ,不执行副作用函数,需要手动调用runner 67 | // 通过在effect对象上挂载stop方法,在方法内部清空对应deps依赖实现 68 | const test = reactive({ 69 | foo:1 70 | }) 71 | let testStop:number = 0 72 | const runner = effect(()=>{ 73 | testStop = test.foo 74 | return 'stop' 75 | }) 76 | test.foo = 2 77 | expect(test.foo).toBe(2) 78 | expect(testStop).toBe(2) 79 | stop(runner) 80 | test.foo ++ 81 | expect(testStop).toBe(2) 82 | runner() 83 | expect(testStop).toBe(3) 84 | 85 | }) 86 | test('effect onStop', async () => { 87 | // onStop 调用stop时的钩子函数 88 | const test = reactive({ 89 | foo:1 90 | }) 91 | const onStop = jest.fn() 92 | let testOnStop:number = 0 93 | const runner = effect(()=>{ 94 | testOnStop = test.foo 95 | return 'onStop' 96 | },{onStop}) 97 | stop(runner) 98 | expect(onStop).toBeCalledTimes(1) 99 | }) 100 | }) -------------------------------------------------------------------------------- /src/reactivity/__test__/index.test.ts: -------------------------------------------------------------------------------- 1 | describe('test-be-dialog-props', () => { 2 | test('props-isShow', async () => { 3 | expect(true).toBeTruthy() 4 | }) 5 | }) -------------------------------------------------------------------------------- /src/reactivity/__test__/reactive.test.ts: -------------------------------------------------------------------------------- 1 | import {isProxy, isReactive, reactive} from "../reactive"; 2 | describe('reactive',()=>{ 3 | test('happy path',()=>{ 4 | const original = {foo:1} 5 | const observed = reactive(original) 6 | expect(original).not.toBe(observed) 7 | expect(observed.foo).toBe(1) 8 | expect(isReactive(observed)).toBeTruthy() 9 | expect(isProxy(observed)).toBeTruthy() 10 | expect(isProxy(original)).not.toBeTruthy() 11 | }) 12 | test('nested reactive',()=>{ 13 | const original = { 14 | foo:1, 15 | child:{ 16 | foo:1 17 | }, 18 | arr:[{foo:1}] 19 | } 20 | const observed = reactive(original) 21 | expect(original).not.toBe(observed) 22 | expect(observed.foo).toBe(1) 23 | expect(isReactive(observed)).toBeTruthy() 24 | expect(isReactive(observed.child)).toBeTruthy() 25 | expect(isReactive(observed.arr)).toBeTruthy() 26 | expect(isReactive(observed.arr[0])).toBeTruthy() 27 | }) 28 | }) -------------------------------------------------------------------------------- /src/reactivity/__test__/readonly.test.ts: -------------------------------------------------------------------------------- 1 | import {isProxy, isReadonly, readonly} from "../reactive"; 2 | 3 | describe('test-readonly', () => { 4 | // readonly 不可以 set 5 | test('happy path', async () => { 6 | const originVal:any = {foo:1} 7 | let readonlyVal = readonly(originVal) 8 | expect(readonlyVal).not.toBe(originVal) 9 | expect(readonlyVal.foo).toBe(1) 10 | expect(isReadonly(readonlyVal)).toBeTruthy() 11 | expect(isProxy(readonlyVal)).toBeTruthy() 12 | expect(isProxy(originVal)).not.toBeTruthy() 13 | }) 14 | test('warn then call set', async () => { 15 | const originVal:any = {foo:1} 16 | let readonlyVal = readonly(originVal) 17 | console.warn = jest.fn() 18 | expect(readonlyVal).not.toBe(originVal) 19 | expect(readonlyVal.foo).toBe(1) 20 | readonlyVal.foo = 2 21 | expect(console.warn).toBeCalled() 22 | }) 23 | test('nested readonly',()=>{ 24 | const original = { 25 | foo:1, 26 | child:{ 27 | foo:1 28 | }, 29 | arr:[{foo:1}] 30 | } 31 | const observed = readonly(original) 32 | expect(original).not.toBe(observed) 33 | expect(observed.foo).toBe(1) 34 | expect(isReadonly(observed)).toBeTruthy() 35 | expect(isReadonly(observed.child)).toBeTruthy() 36 | expect(isReadonly(observed.arr)).toBeTruthy() 37 | expect(isReadonly(observed.arr[0])).toBeTruthy() 38 | }) 39 | }) -------------------------------------------------------------------------------- /src/reactivity/__test__/ref.test.ts: -------------------------------------------------------------------------------- 1 | import {isRef, proxyRefs, ref, unRef} from "../ref"; 2 | import {effect} from "../effect"; 3 | import {reactive} from "../reactive"; 4 | 5 | describe('test-effect', () => { 6 | test('happy path', () => { 7 | const foo = ref(1) 8 | expect(foo.value).toBe(1) 9 | }) 10 | test('ref is reactive', () => { 11 | const foo = ref(1) 12 | let exp:number = 0 13 | let exmp 14 | effect(()=>{ 15 | exp++ 16 | exmp = foo.value 17 | }) 18 | expect(exp).toBe(1) 19 | expect(exmp).toBe(1) 20 | foo.value = 2 21 | expect(exmp).toBe(2) 22 | expect(exp).toBe(2) 23 | // 同样值不触发trigger 24 | foo.value = 2 25 | expect(exmp).toBe(2) 26 | expect(exp).toBe(2) 27 | }) 28 | // 处理 接受对象情况 29 | test('ref nested reactvie', () => { 30 | const foo = ref({asd:1}) 31 | 32 | let exmp 33 | effect(()=>{ 34 | 35 | exmp = foo.value.asd 36 | }) 37 | expect(exmp).toBe(1) 38 | foo.value.asd = 2 39 | expect(exmp).toBe(2) 40 | 41 | }) 42 | 43 | test('isRef',()=>{ 44 | const foo = ref(1) 45 | const reactiveFoo = reactive({foo:1}) 46 | expect(isRef(foo)).toBe(true) 47 | expect(isRef(1)).toBe(false) 48 | expect(isRef(reactiveFoo)).toBe(false) 49 | }) 50 | test('unRef',()=>{ 51 | const foo = ref(1) 52 | expect(unRef(foo)).toBe(1) 53 | expect(unRef(1)).toBe(1) 54 | }) 55 | 56 | test('proxyRefs',()=>{ 57 | // proxyRefs 能够代理 一个对象中的 ref对象,在template模板中会用到 58 | // 1.proxyRefs方法返回一个对象,该对象不需要.value即可访问ref对象值 59 | // 2.通过对proxyRefs方法返回对象进行修改,能够映射到对应ref对象上进行修改 60 | let foo = { 61 | age:ref(10), 62 | name:'baisan', 63 | } 64 | let proxyRef = proxyRefs(foo) 65 | expect(foo.age.value).toBe(10) 66 | expect(proxyRef.age).toBe(10) 67 | expect(proxyRef.name).toBe('baisan') 68 | proxyRef.age = 20 69 | expect(foo.age.value).toBe(20) 70 | expect(proxyRef.age).toBe(20) 71 | proxyRef.age = ref(18) 72 | expect(foo.age.value).toBe(20) 73 | expect(proxyRef.age).toBe(20) 74 | 75 | }) 76 | }) -------------------------------------------------------------------------------- /src/reactivity/__test__/shallowReadonly.test.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly,shallowReadonly} from "../reactive"; 2 | 3 | describe('test-shallowReadonly', () => { 4 | // shallow 是指表层的处理 例如只讲表层的对象或数组做 readonly 化或 reactive 化 5 | test('shallowReadonly', async () => { 6 | const originVal:any = { 7 | foo:{ 8 | fooC:1 9 | } 10 | } 11 | let shallowReadonlyVal = shallowReadonly(originVal) 12 | expect(shallowReadonlyVal).not.toBe(originVal) 13 | expect(isReadonly(shallowReadonlyVal)).toBe(true) 14 | expect(isReadonly(shallowReadonlyVal.foo)).toBe(false) 15 | }) 16 | }) -------------------------------------------------------------------------------- /src/reactivity/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import {track, trigger} from "./effect"; 2 | import {reactive, ReactiveFlags, readonly} from "./reactive"; 3 | import {extend, isObject} from "../shared"; 4 | 5 | const createGetter = (isReadonly = false,isShallow = false) =>{ 6 | return function get(target: any, key: string | symbol, receiver: any): any { 7 | // 根据参数确定是否为readonly 8 | if(key === ReactiveFlags.IS_REACTIVE){ 9 | return !isReadonly 10 | } 11 | if(key === ReactiveFlags.IS_READONLY){ 12 | return isReadonly 13 | } 14 | const res = Reflect.get(target,key) 15 | // readonly 只能读,不会set 不trigger 也就不需要 track 16 | if(!isReadonly){ 17 | // 依赖收集 18 | track(target,key) 19 | } 20 | // shallowReadonly 或 shallowReactive 都直接返回 21 | if(isShallow){ 22 | return res 23 | } 24 | // 处理存在子对象获子数组情况,递归调用 25 | // 深readonly和深reactive 26 | if(isObject(res)){ 27 | return isReadonly ? readonly(res) : reactive(res) 28 | } 29 | 30 | return res 31 | } 32 | } 33 | const createSetter = () =>{ 34 | return function set(target: any, key: string | symbol, value: any, receiver: any): boolean { 35 | 36 | const res = Reflect.set(target,key,value) 37 | // 派发通知 38 | trigger(target,key,value) 39 | return true 40 | } 41 | } 42 | const get = createGetter() 43 | const set = createSetter() 44 | const readonlyGetter = createGetter(true) 45 | const shallowReadonlyGetter = createGetter(true,true) 46 | export const mutableHandlers = { 47 | get, 48 | set 49 | } 50 | export const readonlyHandlers = { 51 | get:readonlyGetter, 52 | set(target: any, key: string | symbol, value: any, receiver: any): boolean { 53 | console.warn('readonlyHandlers') 54 | return true 55 | } 56 | } 57 | export const shallowReadonlyHandlers = extend({},readonlyHandlers,{ 58 | get:shallowReadonlyGetter, 59 | }) -------------------------------------------------------------------------------- /src/reactivity/computed.ts: -------------------------------------------------------------------------------- 1 | import {ReactiveEffect} from "./effect"; 2 | 3 | /** 4 | * 1.computed 接受一个方法,该方法内部应该具有访问响应式对象的语句 5 | * 2.computed 返回一个通过 .value访问的对象,.value会触发 是 computed接受方法,并拿到返回值 6 | * 3.computed 具有惰性,多次访问 .value,在对应响应式对象值不改变的时候,不会多次触发接受的方法 7 | * 4.computed 在对应响应式对象值改变的时候,才触发接受的方法 8 | */ 9 | 10 | class computedRefsImpl { 11 | private _effect:ReactiveEffect 12 | private _getter:Function 13 | private _isDirty = true // 惰性,控制多次访问时,响应式对象没有改变的话直接返回 _value 14 | private _value:any 15 | constructor(getter:Function) { 16 | this._getter = getter 17 | // 由于当getter中响应式对象改变时,我们要运行getter, 18 | // 所以我们使用响应式 effect 对象做依赖收集 19 | // 并且我们使用effect的调度执行 scheduler 进行调度执行 20 | // 实现在getter中响应式对象改变时,我们重置 _isDirty 21 | // 在 .value访问时,真正调度执行 getter 22 | this._effect = new ReactiveEffect(getter,()=>{ 23 | if(!this._isDirty){ 24 | this._isDirty = true 25 | } 26 | }) 27 | } 28 | get value(){ 29 | if(this._isDirty){ 30 | this._isDirty = false 31 | this._value = this._effect.run() 32 | } 33 | return this._value 34 | } 35 | } 36 | 37 | export function computed (getter:Function):computedRefsImpl{ 38 | return new computedRefsImpl(getter) 39 | } -------------------------------------------------------------------------------- /src/reactivity/effect.d.ts: -------------------------------------------------------------------------------- 1 | export declare interface IEffectOption { 2 | scheduler?:()=>void 3 | onStop?:()=>void 4 | } -------------------------------------------------------------------------------- /src/reactivity/effect.ts: -------------------------------------------------------------------------------- 1 | import {IEffectOption} from './effect.d' 2 | import {extend} from "../shared"; 3 | 4 | // 当前激活的副作用函数对象 5 | let activeEffect:ReactiveEffect; 6 | // 是否应该收集依赖 stop功能会用到 7 | let shouldTrack:boolean; 8 | 9 | 10 | export class ReactiveEffect { 11 | private _fn:any 12 | deps:Array = [] 13 | active:boolean = true 14 | onStop?:()=>void 15 | constructor(fn:Function,public scheduler?:Function) { 16 | this._fn = fn 17 | } 18 | run(){ 19 | // stop 后 this.active = false,直接return 20 | // 由于 shouldTrack = false 所以不会 再 track 21 | if(!this.active){ 22 | return this._fn() 23 | } 24 | 25 | shouldTrack = true 26 | activeEffect = this 27 | const res = this._fn() 28 | shouldTrack = false 29 | return res 30 | } 31 | stop(){ 32 | if(this.active){ 33 | if(this.onStop){ 34 | this.onStop() 35 | } 36 | cleanupEffect(this); 37 | this.active = false 38 | } 39 | } 40 | 41 | } 42 | function cleanupEffect(effect:ReactiveEffect) { 43 | effect.deps.forEach(value => { 44 | value.delete(effect) 45 | }) 46 | } 47 | export const effect = (fn:Function,options:IEffectOption={}):Function =>{ 48 | const _effect = new ReactiveEffect(fn,options.scheduler) 49 | // 降配置传递给 _effect 50 | extend(_effect,options) 51 | _effect.run() 52 | // 给 runner 上挂上自己,stop时使用 53 | const runner:any = _effect.run.bind(_effect) 54 | runner.effect = _effect 55 | return runner 56 | } 57 | 58 | export const stop = (runner:any):void =>{ 59 | runner.effect.stop() 60 | } 61 | let targetMap = new Map() 62 | /** 63 | * 依赖收集 64 | * @param target 65 | * @param key 66 | */ 67 | export const track = (target: any, key: string | symbol) :void =>{ 68 | if(!isTracking()){ 69 | return; 70 | } 71 | let depsMap = targetMap.get(target) 72 | if(!depsMap){ 73 | depsMap = new Map() 74 | targetMap.set(target,depsMap) 75 | } 76 | let dep = depsMap.get(key) 77 | if(!dep){ 78 | dep = new Set() 79 | depsMap.set(key,dep) 80 | } 81 | trackEffects(dep) 82 | } 83 | export function trackEffects(dep:any):void { 84 | if(!dep.has(activeEffect)) { 85 | // 当前激活的副作用函数对象作为依赖收集起来 86 | dep.add(activeEffect) 87 | // 反向收集activeEffect的dep,使得stop时可以找到对应副作用函数 88 | activeEffect.deps.push(dep) 89 | } 90 | } 91 | export function isTracking(){ 92 | return shouldTrack && activeEffect !== undefined 93 | } 94 | /** 95 | * 通知派发 96 | * @param target 97 | * @param key 98 | * @param value 99 | */ 100 | export const trigger = (target: any, key: string | symbol, value: any) :void =>{ 101 | let depsMap = targetMap.get(target) 102 | let dep = depsMap.get(key) 103 | triggerEffects(dep) 104 | } 105 | export function triggerEffects(dep:any):void { 106 | // 将 target 某个key的所有依赖全部执行一遍 107 | for (let effect of dep){ 108 | if(effect.scheduler){ 109 | effect.scheduler() 110 | }else{ 111 | effect.run() 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export {ref} from './ref' 2 | export {effect} from './effect' -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | import {mutableHandlers, readonlyHandlers,shallowReadonlyHandlers} from "./baseHandlers"; 2 | 3 | export enum ReactiveFlags { 4 | IS_REACTIVE = '__v_reactive', 5 | IS_READONLY = '__v_readonly', 6 | } 7 | export const reactive = (raw:any) =>{ 8 | return createActiveObject(raw,mutableHandlers) 9 | } 10 | export const readonly = (raw:any) =>{ 11 | return createActiveObject(raw,readonlyHandlers) 12 | } 13 | export const shallowReadonly = (raw:any) =>{ 14 | return createActiveObject(raw,shallowReadonlyHandlers) 15 | } 16 | export const isReactive = (raw:any) =>{ 17 | return !!raw[ReactiveFlags.IS_REACTIVE] 18 | } 19 | export const isReadonly = (raw:any) =>{ 20 | return !!raw[ReactiveFlags.IS_READONLY] 21 | } 22 | // 返回是否为 Reactive 或 Readonly 处理过的对象判断 23 | export const isProxy = (raw:any) =>{ 24 | return isReactive(raw) || isReadonly(raw) 25 | } 26 | const createActiveObject = (raw:any,baseHandlers:any) =>{ 27 | return new Proxy(raw,baseHandlers) 28 | } 29 | -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | import {isTracking, trackEffects, triggerEffects} from "./effect"; 2 | import {hasChanged, isObject} from "../shared/index"; 3 | import {reactive} from "./reactive"; 4 | 5 | 6 | class refImpl { 7 | private _value:any 8 | private _rawValue:any 9 | public dep = new Set() 10 | public __v_isRef = true 11 | constructor(value:any) { 12 | this._value = convert(value) 13 | this._rawValue = this._value 14 | } 15 | get value(){ 16 | if(isTracking()){ 17 | // 依赖收集 18 | trackEffects(this.dep) 19 | } 20 | return this._value 21 | } 22 | set value(newValue:any){ 23 | if(hasChanged(this._rawValue,newValue)) { 24 | this._value = convert(newValue) 25 | this._rawValue = this._value 26 | // 派发更新 27 | triggerEffects(this.dep) 28 | } 29 | } 30 | } 31 | export const convert = (value:any) =>{ 32 | return isObject(value) ? reactive(value) : value 33 | } 34 | export function ref(raw:any){ 35 | return new refImpl(raw) 36 | } 37 | export function isRef(ref:any){ 38 | return !!ref.__v_isRef 39 | } 40 | export function unRef(ref:any){ 41 | return isRef(ref) ? ref.value : ref 42 | } 43 | export function proxyRefs(rawWithRefs:any){ 44 | return new Proxy(rawWithRefs,{ 45 | get(target: any, key: string | symbol): any { 46 | // 当访问的值是ref对象,返回.value,否则直接返回 47 | const res = Reflect.get(target,key) 48 | return isRef(res) ? res.value : res 49 | }, 50 | set(target: any, key: string | symbol, value: any, receiver: any): boolean { 51 | // 当设置的值是ref,且新值不是ref 52 | let targetVal = Reflect.get(target,key) 53 | if(isRef(targetVal) && !isRef(value)){ 54 | targetVal.value = value 55 | }else{ 56 | targetVal = value 57 | } 58 | return true 59 | } 60 | }) 61 | } -------------------------------------------------------------------------------- /src/runtime-core/apiInject.ts: -------------------------------------------------------------------------------- 1 | import {getCurrentInstance} from "./component"; 2 | 3 | export function provide(key:string,val:any) { 4 | const instance = getCurrentInstance() 5 | if(instance){ 6 | let {provides} = instance 7 | let parentProvides = instance.parent?.provides 8 | if(provides === parentProvides){ 9 | // 这里要解决一个问题 10 | // 当父级 key 和 爷爷级别的 key 重复的时候,对于子组件来讲,需要取最近的父级别组件的值 11 | // 那这里的解决方案就是利用原型链来解决 12 | // provides 初始化的时候是在 createComponent 时处理的,当时是直接把 parent.provides 赋值给组件的 provides 的 13 | // 所以,如果说这里发现 provides 和 parentProvides 相等的话,那么就说明是第一次做 provide(对于当前组件来讲) 14 | // 我们就可以把 parent.provides 作为 currentInstance.provides 的原型重新赋值 15 | // 至于为什么不在 createComponent 的时候做这个处理,可能的好处是在这里初始化的话,是有个懒执行的效果(优化点,只有需要的时候在初始化) 16 | provides = instance.provides = Object.create(parentProvides) 17 | } 18 | provides[key] = val 19 | } 20 | 21 | } 22 | export function inject(key:string,defaultVal:any) { 23 | const instance = getCurrentInstance() 24 | if(instance){ 25 | let provides = instance.parent.provides; 26 | if(key in provides){ 27 | return provides[key] 28 | }else if(defaultVal){ 29 | if(typeof defaultVal === 'function'){ 30 | return defaultVal() 31 | }else{ 32 | return defaultVal 33 | } 34 | } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import {isObject} from "../shared/index"; 2 | import {PublicInstanceProxyHandlers} from "./componentPublicInstance"; 3 | import {initProps} from "./componentProps"; 4 | import {shallowReadonly} from "../reactivity/reactive"; 5 | import {emit} from "./componentEmit"; 6 | import {initSlots} from "./componentSlots"; 7 | import {proxyRefs} from "../reactivity/ref"; 8 | 9 | let compiler: any; 10 | export function createComponentInstance(vnode:any,parent:any){ 11 | const instance = { 12 | vnode, 13 | type:vnode.type,// 这个是原始组件对象 14 | setupState:{}, // setup的返回结果对象 15 | props:{}, 16 | next:null,// 待更新的新虚拟节点 17 | provides:parent ? parent.provides : {}, 18 | parent, 19 | isMounted:false,// 标记是否初始化过 20 | subTree:{},// 子虚拟节点树 21 | emit:(event: string, ...arg: any[])=>{}, 22 | slots:{} 23 | } 24 | instance.emit = emit.bind(null,instance) 25 | return instance 26 | } 27 | export function setupComponent(instance:any){ 28 | // 初始化处理 props 29 | // initProps 30 | initProps(instance,instance.vnode.props) 31 | // 初始化处理插槽 32 | initSlots(instance,instance.vnode.children) 33 | // 创建一个有状态的组件 34 | setStatefulComponent(instance) 35 | } 36 | 37 | export function setStatefulComponent(instance:any){ 38 | // 获取原始组件对象(注意这里并不是组件实例) 39 | const component = instance.type 40 | // 创建要给组件实例代理,使得render方法内能够通过this访问组件实例,如this.$el等 41 | instance.proxy = new Proxy({_:instance},PublicInstanceProxyHandlers) 42 | // 获取原始组件对象的 setup 方法 43 | const setup = component.setup 44 | if(setup){ 45 | // diao yong setup qian she zhi instance 46 | setCurrentInstance(instance) 47 | const setupResult = setup(shallowReadonly(instance.props),{ 48 | emit:instance.emit 49 | }) 50 | setCurrentInstance(null); 51 | // 处理setup结果 52 | handleSetupResult(instance,setupResult) 53 | } 54 | 55 | } 56 | export function handleSetupResult(instance:any,setupResult:any){ 57 | // 如果setup结果,是一个对象,就把结果挂载到instance上 58 | if(isObject(setupResult)){ 59 | instance.setupState = setupResult 60 | } 61 | 62 | if (typeof setupResult === "function") { 63 | // 如果返回的是 function 的话,那么绑定到 render 上 64 | // 认为是 render 逻辑 65 | // setup(){ return ()=>(h("div")) } 66 | instance.render = setupResult; 67 | } else if (typeof setupResult === "object") { 68 | // 返回的是一个对象的话 69 | // 先存到 setupState 上 70 | // 先使用 @vue/reactivity 里面的 proxyRefs 71 | // 后面我们自己构建 72 | // proxyRefs 的作用就是把 setupResult 对象做一层代理 73 | // 方便用户直接访问 ref 类型的值 74 | // 比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了,而不需要在 count.value 75 | // 这里也就是官网里面说到的自动结构 Ref 类型 76 | instance.setupState = proxyRefs(setupResult); 77 | } 78 | // 处理渲染函数render具体方法,渲染函数可能来自于模板编译、setup返回、render的option 79 | finishComponentSetup(instance) 80 | 81 | } 82 | // 最后处理渲染函数方法,自此组件setup相关初始化流程结束 83 | export function finishComponentSetup(instance:any){ 84 | // 组件原始对象 85 | const component = instance.type 86 | // render 优先级 : setup的返回render,组件内option的render,template 87 | 88 | // 如果组件 instance上没有render(setup返回渲染函数) 89 | if (!instance.render) { 90 | // 如果 compile 有值 并且当然组件没有 render 函数(组件内option的render), 91 | // 那么就需要把 template 编译成 render 函数 92 | if (compiler && !component.render) { 93 | if (component.template) { 94 | // 这里就是 runtime 模块和 compile 模块结合点 95 | const template = component.template; 96 | component.render = compiler(template); 97 | } 98 | } 99 | 100 | instance.render = component.render; 101 | } 102 | 103 | } 104 | let currentInstance:any = null 105 | export function getCurrentInstance() { 106 | return currentInstance 107 | } 108 | export function setCurrentInstance(instance:any) { 109 | currentInstance = instance 110 | } 111 | 112 | export function registryRuntimeCompiler(_compiler:any){ 113 | compiler = _compiler 114 | } -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import {camelize, toHandlerKey} from "../shared/index"; 2 | 3 | export function emit(instance:any,event:string,...arg:any):void { 4 | const { props } = instance 5 | const handlerName = toHandlerKey(camelize(event)) 6 | const handler = props[handlerName] 7 | handler && handler(...arg) 8 | } -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance:any,rawProps:any){ 2 | instance.props = rawProps ||{} 3 | } -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import {hasOwn} from "../shared/index"; 2 | 3 | const publicPropertiesMap:any = { 4 | $el:(i:any)=>{return i.vnode.el}, 5 | $slots:(i:any)=>{return i.slots}, 6 | $props:(i:any)=>{return i.props} 7 | } 8 | 9 | 10 | export const PublicInstanceProxyHandlers = { 11 | // @ts-ignore 12 | get({_:instance}, key:any, receiver: any): any { 13 | const {setupState,props} = instance 14 | if(hasOwn(setupState,key)){ 15 | return setupState[key] 16 | } 17 | if(hasOwn(props,key)){ 18 | return props[key] 19 | } 20 | const publicGetter = publicPropertiesMap[key] 21 | if(publicGetter){ 22 | return publicGetter(instance) 23 | } 24 | 25 | } 26 | } -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import {isArray} from "../shared/index"; 2 | import {shapeFlags} from "../shared/ShapeFlags"; 3 | 4 | export function initSlots(instance:any,children:any){ 5 | if(instance.vnode.shapeFlag & shapeFlags.SLOTS_CHILDREN){ 6 | normalizeObjectSlots(children,instance.slots) 7 | } 8 | } 9 | function normalizeObjectSlots(children:any,slot:any){ 10 | for(let key in children){ 11 | const value = children[key] 12 | slot[key] = (props:any)=>normalizeSlots(value(props)) 13 | } 14 | } 15 | function normalizeSlots(value:any){ 16 | return isArray(value) ? value : [value] 17 | } -------------------------------------------------------------------------------- /src/runtime-core/componentUpdateUtils.ts: -------------------------------------------------------------------------------- 1 | export function shouldUpdateComponent(n1: any, n2: any):boolean{ 2 | 3 | const {props:nextProps} = n2 4 | const {props:prevProps} = n1 5 | for(let 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:Function){ 4 | return function createApp(rootComponent:any){ 5 | return{ 6 | mount(rootContainer:string){ 7 | // 根据根组件 rootComponent ,创建vnode 8 | const vnode = createVNode(rootComponent) 9 | // 调用 render 开始处理 vnode 和 rootContainer 直至最终渲染 10 | render(null,vnode,rootContainer) 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/runtime-core/h.ts: -------------------------------------------------------------------------------- 1 | import {createVNode} from "./vnode"; 2 | export function h(rootComponent:any,props?:any,children?:any){ 3 | return createVNode(rootComponent,props,children) 4 | } -------------------------------------------------------------------------------- /src/runtime-core/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import {createVNode, FRAGMENT} from "../vnode"; 2 | 3 | export function renderSlots(slots:any,name:string,props:any){ 4 | const slot = slots[name] 5 | if(slot){ 6 | return createVNode(FRAGMENT,{},slot(props)) 7 | } 8 | 9 | } -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | export {createAppApi} from "./createApp"; 2 | export {h} from "./h"; 3 | export {renderSlots} from "./helpers/renderSlots"; 4 | export {createTextVNode,createElementVNode} from "./vnode" 5 | export {getCurrentInstance,registryRuntimeCompiler} from "./component" 6 | export {inject,provide} from "./apiInject" 7 | export {createRenderer} from "./renderer" 8 | export {nextTick} from "./scheduler" 9 | export {toDisplayString} from "../shared"; -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | import {createComponentInstance, setupComponent} from "./component"; 2 | import {shapeFlags} from "../shared/ShapeFlags"; 3 | import {FRAGMENT, TEXT} from "./vnode"; 4 | import {createAppApi} from "./createApp"; 5 | import {effect} from "../reactivity"; 6 | import {EMPTY_OBJ} from "../shared"; 7 | import {shouldUpdateComponent} from "./componentUpdateUtils"; 8 | import {queueJobs} from "./scheduler"; 9 | 10 | export function createRenderer(option: any) { 11 | const { 12 | createElement: hostCreateElement, 13 | patchProp: hostPatchProp, 14 | insert: hostInsert, 15 | setElementText: hostSetElementText, 16 | remove: hostRemove 17 | } = option 18 | 19 | function render(n1: any, n2: any, container: any) { 20 | patch(n1, n2, container, null, null) 21 | } 22 | 23 | /** 24 | * patch方法 25 | * @param n1 旧的虚拟节点 26 | * @param n2 新的虚拟节点 27 | * @param container 28 | * @param parent 29 | */ 30 | function patch(n1: any, n2: any, container: any, parent: any, anchor: any) { 31 | if (!n2) { 32 | return 33 | } 34 | // 根据 vnode 类型不同,进行不同处理 35 | // 处理 element类型 36 | const {shapeFlag, type} = n2 37 | switch (type) { 38 | case TEXT: 39 | processText(n1, n2, container) 40 | break; 41 | case FRAGMENT: 42 | processFragment(n1, n2, container, parent, anchor) 43 | break; 44 | default: { 45 | if (shapeFlag & shapeFlags.ELEMENT) { 46 | processElement(n1, n2, container, parent, anchor) 47 | } 48 | // 处理组件类型 49 | if (shapeFlag & shapeFlags.STATEFUL_COMPONENT) { 50 | processComponent(n1, n2, container, parent, anchor) 51 | } 52 | } 53 | } 54 | 55 | 56 | } 57 | 58 | /** 59 | * 处理元素方法 60 | * @param n1 旧的虚拟节点 61 | * @param n2 新的虚拟节点 62 | * @param container 63 | * @param parent 64 | */ 65 | 66 | function processElement(n1: any, n2: any, container: any, parent: any, anchor: any) { 67 | if (!n1) { 68 | mountElement(n2, container, parent, anchor) 69 | } else { 70 | patchElement(n1, n2, container, parent, anchor) 71 | } 72 | 73 | } 74 | 75 | /** 76 | * 更新元素方法 77 | * @param n1 旧的虚拟节点 78 | * @param n2 新的虚拟节点 79 | * @param container 80 | */ 81 | function patchElement(n1: any, n2: any, container: any, parent: any, anchor: any) { 82 | console.log('patch Element') 83 | console.log('n1') 84 | console.log(n1) 85 | console.log('n2') 86 | console.log(n2) 87 | const el = (n2.el = n1.el) 88 | const oldProps = n1.props || EMPTY_OBJ 89 | const newProps = n2.props || EMPTY_OBJ 90 | patchChildren(n1, n2, el, parent, anchor) 91 | patchProps(oldProps, newProps, el) 92 | } 93 | 94 | function patchChildren(n1: any, n2: any, container: any, parent: any, anchor: any) { 95 | const prevShapeFlag = n1.shapeFlag 96 | const c1 = n1.children 97 | const shapeFlag = n2.shapeFlag 98 | const c2 = n2.children 99 | // 新子节点是文本 100 | if (shapeFlag & shapeFlags.TEXT_CHILDREN) { 101 | // 老的子节点是数组,则卸载 102 | if (prevShapeFlag & shapeFlags.ARRAY_CHILDREN) { 103 | unmountChildren(c1) 104 | } 105 | // 无论老的子节点是数组,还是文本,都替换新文本 106 | if (c1 !== c2) { 107 | hostSetElementText(container, c2) 108 | } 109 | } else { 110 | // 新的是数组,老的是文本,就清空文本,挂载新子节点 111 | if (prevShapeFlag & shapeFlags.TEXT_CHILDREN) { 112 | 113 | hostSetElementText(container, '') 114 | mountChildren(c2, container, parent, anchor) 115 | } else { 116 | //新老子节点都是数组 开始diff 117 | patchKeyedChildren(c1, c2, container, parent, anchor) 118 | } 119 | } 120 | } 121 | 122 | function patchKeyedChildren(c1: any, c2: any, container: any, parent: any, anchor: any) { 123 | let indexStart = 0 124 | let oldIndexEnd = c1.length - 1 125 | let newIndexEnd = c2.length - 1 126 | let newChildLen = c2.length 127 | // 头部扫描,当 oldIndexEnd 大于于 newIndexEnd 或 newChildLen 停止 128 | // 当节点不同,停止 129 | while (indexStart <= oldIndexEnd && indexStart <= newIndexEnd) { 130 | // indexStart 指向的新旧虚拟节点相同,则递归patch 131 | if (isSameVNode(c1[indexStart], c2[indexStart])) { 132 | patch(c1[indexStart], c2[indexStart], container, parent, anchor) 133 | } else { 134 | break; 135 | } 136 | // 移动指针 137 | indexStart++ 138 | } 139 | // 头尾部扫描,当 oldIndexEnd 大于于 newIndexEnd 或 newChildLen 停止 140 | // 当节点不同,停止 141 | while (indexStart <= oldIndexEnd && indexStart <= newIndexEnd) { 142 | // indexStart 指向的新旧虚拟节点相同,则递归patch 143 | if (isSameVNode(c1[indexStart], c2[indexStart])) { 144 | patch(c1[indexStart], c2[indexStart], container, parent, anchor) 145 | } else { 146 | break; 147 | } 148 | // 移动指针 149 | oldIndexEnd-- 150 | newIndexEnd-- 151 | } 152 | // 头部扫描 与 尾部扫描结束后,根据指针指向情况 153 | // 处理头部节点序列、头部节点序列的新增或修改情况 154 | // 从逻辑上来看 头部扫描 与 尾部扫描 是为了达到将那些 155 | // 没有发生移动的节点预先处理的目的,此时处理过后 156 | // 新旧虚拟节点数组中,没有被扫描的中间部分,才包含着移动节点的情况 157 | 158 | // 头部扫描 与 尾部扫描 节点新增 159 | // 此时 indexStart > oldIndexEnd && indexStart <= newIndexEnd 160 | // 此时 indexStart ~ newIndexEnd 之间为新增节点 161 | // oE oE 162 | // (a b) 或 (a b) 163 | // (a b) c (a b) c d 164 | // s/nE s nE 165 | if (indexStart > oldIndexEnd) { 166 | if (indexStart <= newIndexEnd) { 167 | // 挂载新的节点 168 | let nextPos = indexStart + 1 169 | let anchor = nextPos < newChildLen ? c2[nextPos].el : null 170 | while (indexStart <= newIndexEnd) { 171 | patch(null, c2[indexStart], container, parent, anchor) 172 | indexStart++ 173 | } 174 | } 175 | } else if (indexStart > newIndexEnd) { 176 | // 头部扫描 与 尾部扫描 旧节点删除 177 | // 此时 indexStart > newIndexEnd && indexStart <= oldIndexEnd 178 | // 此时 indexStart ~ oldIndexEnd 之间为需要删除节点 179 | // oE oE 180 | // (a b) c 或 (a b) c d 181 | // (a b) (a b) 182 | // nE s nE s 183 | if (indexStart <= oldIndexEnd) { 184 | while (indexStart <= oldIndexEnd) { 185 | hostRemove(c1[indexStart].el) 186 | indexStart++ 187 | } 188 | } 189 | } else { 190 | // 处理中间部分节点序列 191 | // 192 | let s1 = indexStart; 193 | let s2 = indexStart 194 | // 新的中间部分节点序列数量 195 | let newChildNum = newIndexEnd - indexStart + 1 196 | // 已经处理过的中间节点序列数量 197 | let patched = 0 198 | // 新的中间部分节点序列映射表 199 | let newToIndexMap = new Map() 200 | // 建立映射 201 | for (let i = 0; i < s2; i++) { 202 | if (c2[i].key) { 203 | newToIndexMap.set(c2.key, i) 204 | } 205 | } 206 | // 建立映射表。该映射表的索引对应新的中间部分节点序列 207 | // 值对于旧的中间部分节点序列 208 | let newIndexToOldIndexMap = new Array(newChildNum) 209 | // 初始化 newIndexToOldIndexMap, newIndexToOldIndexMap[i] === 0 表示节点新增 210 | for(let i = 0;i <= newChildNum;i++) newIndexToOldIndexMap[i] = 0 211 | // 节点是否移动 212 | let move = false 213 | let newIndexSoFar = 0 214 | // 遍历旧中间部分节点序列 215 | for (let i = 0; i < s1; i++) { 216 | const prevChild = c1[i] 217 | // 新的节点序列比旧的先处理完,旧的剩余的统统删除 218 | if (patched >= newChildNum) { 219 | hostRemove(prevChild.el) 220 | continue 221 | } 222 | // 当前旧节点在新序列中索引 223 | // 这里是用旧的节点去新的节点序列中查找,看是否找到 224 | let oldInNewIndex 225 | // 如果传了key,就用映射表,否则就要遍历新的序列 226 | if (prevChild.key) { 227 | oldInNewIndex = newToIndexMap.get(prevChild.key) 228 | } else { 229 | for (let j = 0; j <= s2; j++) { 230 | if (isSameVNode(prevChild, c2[j])) { 231 | oldInNewIndex = j 232 | break; 233 | } 234 | } 235 | } 236 | // 旧的节点不在新的节点序列中,删除旧节点 237 | if (oldInNewIndex === null) { 238 | hostRemove(prevChild.el) 239 | } else { 240 | 241 | if(oldInNewIndex >= newIndexSoFar){ 242 | newIndexSoFar = oldInNewIndex 243 | }else{ 244 | move = true 245 | } 246 | 247 | // 将节点在旧的序列中位置记录到对应的‘新的’映射表中, 248 | // 这里+1 是为了避免 第一个就匹配到,和初始值作区分 249 | newIndexToOldIndexMap[oldInNewIndex - s2] = i + 1 250 | // 旧的节点在新的节点序列中,递归patch 251 | patch(prevChild, c2[oldInNewIndex], container, parent, null) 252 | // 记录已处理的新节点数量 253 | patched++ 254 | } 255 | 256 | } 257 | 258 | // 根据 newIndexToOldIndexMap 计算最长递增子序列(返回值是新中间节点序列中最长递增子序列的索引组成的数组) 259 | // 2 3 4 260 | // a,b,(c,d,e),f,g 261 | // a,b,(e,c,d),f,g 262 | // 0 1 2 263 | // 最长子序列: [1,2] 264 | // newIndexToOldIndexMap:[5,3,4](加了1) 265 | let increasingNewIndexSequence = move ? getSequence(newIndexToOldIndexMap) : [] 266 | let j = increasingNewIndexSequence.length - 1 267 | // 倒序遍历新的中间节点序列,倒序是为了保证移动时锚点是稳定的,正序遍历无法确定锚点是否稳定 268 | for(let i = newChildNum;i>=0;i--){ 269 | let nextIndex = i + s2 270 | let nexChild = c2[nextIndex] 271 | let anchor = nextIndex + 1 > newChildLen ? null : c2[nextIndex + 1].el 272 | // 新增的节点 273 | if(newIndexToOldIndexMap[i] === 0){ 274 | patch(null,nexChild, container,parent,anchor) 275 | } 276 | // 新的节点 在递增子序列中找得到 就不需要移动 277 | if(move){ 278 | if(j < 0 || increasingNewIndexSequence[j] !== i){ 279 | // 移动节点 280 | hostInsert(nexChild.el, container,anchor) 281 | }else{ 282 | j-- 283 | } 284 | } 285 | 286 | } 287 | 288 | 289 | 290 | } 291 | } 292 | 293 | function isSameVNode(n1: any, n2: any) { 294 | return n1.type === n2.type && n1.key === n2.key 295 | } 296 | 297 | function unmountChildren(children: Array) { 298 | for (let i: number = 0; i < children.length; i++) { 299 | const el = children[i].el 300 | hostRemove(el) 301 | } 302 | 303 | } 304 | 305 | /** 306 | * 处props 307 | */ 308 | function patchProps(oldProps: any, newProps: any, el: any) { 309 | if (oldProps !== newProps) { 310 | for (let key in newProps) { 311 | const prevProps = oldProps[key] 312 | const nextProps = newProps[key] 313 | // props 被修改为了 null 或 undefined,我们需要删除 314 | if (nextProps === null || nextProps === undefined) { 315 | hostPatchProp(el, key, prevProps, null) 316 | } 317 | // props 发生了改变 'foo' => 'new foo',我们需要修改 318 | if (prevProps !== nextProps) { 319 | hostPatchProp(el, key, prevProps, nextProps) 320 | } 321 | } 322 | if (EMPTY_OBJ !== oldProps) { 323 | for (let key in oldProps) { 324 | const prevProps = oldProps[key] 325 | const nextProps = newProps[key] 326 | // props 在新的VNode中不存在,而旧的VNode中还存在,则删除 327 | if (!nextProps) { 328 | hostPatchProp(el, key, prevProps, nextProps) 329 | } 330 | } 331 | } 332 | } 333 | } 334 | 335 | /** 336 | * 挂载元素方法 337 | * @param vnode 338 | * @param container 339 | * @param parent 340 | */ 341 | function mountElement(vnode: any, container: any, parent: any, anchor: any) { 342 | 343 | // const el = (vnode.el = document.createElement(vnode.type)) 344 | const el = (vnode.el = hostCreateElement(vnode.type)) 345 | let {children, shapeFlag} = vnode 346 | // 如果是文本元素就插入 347 | if (shapeFlag & shapeFlags.TEXT_CHILDREN) { 348 | el.textContent = children 349 | } 350 | // 是数组就递归 patch 子节点 351 | if (shapeFlag & shapeFlags.ARRAY_CHILDREN) { 352 | mountChildren(vnode.children, el, parent, anchor) 353 | } 354 | // 处理属性 355 | 356 | const {props} = vnode 357 | for (let key in props) { 358 | let val = props[key] 359 | hostPatchProp(el, key, null, val) 360 | } 361 | // insert the el to container 362 | hostInsert(el, container) 363 | } 364 | 365 | function mountChildren(children: any, container: any, parent: any, anchor: any) { 366 | children.forEach((elm: any) => { 367 | patch(null, elm, container, parent, anchor) 368 | }) 369 | } 370 | 371 | /** 372 | * 处理组件方法 373 | * @param n1 旧的虚拟节点 374 | * @param n2 新的虚拟节点 375 | * @param container 376 | * @param parent 377 | */ 378 | function processComponent(n1: any, n2: any, container: any, parent: any, anchor: any) { 379 | if(!n1){ 380 | mountComponent(n2, container, parent, anchor) 381 | }else { 382 | // 更新组件 383 | updateComponent(n1, n2) 384 | } 385 | 386 | } 387 | function updateComponent(n1: any, n2: any){ 388 | const instance = (n2.components = n1.components) 389 | // 对比props 看是否需要更新 390 | if(shouldUpdateComponent(n1,n2)){ 391 | instance.next = n2 392 | // 更新组件 393 | instance.update() 394 | }else{ 395 | // 不更新,也要跟新实力上的el、vnode 396 | n2.el = n1.el 397 | instance.vnode = n2 398 | } 399 | } 400 | /** 401 | * 挂载组件方法 402 | * @param vnode 403 | * @param container 404 | * @param parent 405 | */ 406 | function mountComponent(vnode: any, container: any, parent: any, anchor: any) { 407 | // 创建组件实例 408 | const instance = (vnode.components = createComponentInstance(vnode, parent)) 409 | // 开始 处理组件setup 410 | setupComponent(instance) 411 | // 开始处理 setup 运行完成后内涵的子节点 412 | // 可以理解初始化时,我们处理的是根节点组件与容器 413 | // 这里就是处理根组件下的子组件了 414 | setupRenderEffect(instance, vnode, container, anchor) 415 | } 416 | 417 | function setupRenderEffect(instance: any, vnode: any, container: any, anchor: any) { 418 | // 缓存runner,在组件更新时调用 419 | // @ts-ignore 420 | // @ts-ignore 421 | instance.update = effect(() => { 422 | // 调用render函数,拿到子树vnode,这个值可能是组件也可能是元素或其他, 423 | // 但是他一定是上一轮的子树 424 | // 初始化逻辑 425 | if (!instance.isMounted) { 426 | const subTree = (instance.subTree = instance.render.call(instance.proxy,instance.proxy)) 427 | 428 | // 再次 patch,处理子树 429 | patch(null, subTree, container, instance, anchor) 430 | // 记录根组件对应的el 431 | vnode.el = subTree.el 432 | instance.isMounted = true 433 | } else { 434 | // 更新el、props、vnode 435 | // 父组件初次触发更新时,父组件更新它的element,再走到更新children逻辑 436 | // 最后在updateComponent中会判断props是否改变 437 | // 再次调用instance.update进入这里更新子组件 438 | const {next} = instance 439 | if(next){ 440 | // 更新el 441 | next.el = vnode.el 442 | updateComponentPreRender(instance,next) 443 | } 444 | 445 | 446 | const subTree = instance.render.call(instance.proxy,instance.proxy) 447 | // 更新逻辑 448 | const prevSubTree = instance.subTree 449 | instance.subTree = subTree 450 | // 再次 patch,处理子树 451 | patch(prevSubTree, subTree, container, instance, anchor) 452 | // 记录根组件对应的el 453 | vnode.el = subTree.el 454 | } 455 | },{ 456 | // @ts-ignore 457 | scheduler:()=>{ 458 | console.log('scheduler update') 459 | queueJobs(instance.update) 460 | } 461 | }) 462 | 463 | } 464 | function updateComponentPreRender(instance:any,nextVNode:any){ 465 | instance.vnode = nextVNode 466 | instance.next = null 467 | instance.props = nextVNode.props 468 | } 469 | /** 470 | * 处理fragment 471 | * @param n1 旧的虚拟节点 472 | * @param n2 新的虚拟节点 473 | * @param container 474 | * @param parent 475 | */ 476 | function processFragment(n1: any, n2: any, container: any, parent: any, anchor: any) { 477 | mountChildren(n2.children, container, parent, anchor) 478 | } 479 | 480 | /** 481 | * 处理文本方法 482 | * @param n1 旧的虚拟节点 483 | * @param n2 新的虚拟节点 484 | * @param container 485 | */ 486 | function processText(n1: any, n2: any, container: any) { 487 | const {children} = n2 488 | const text = n2.el = document.createTextNode(children) 489 | container.append(text) 490 | 491 | } 492 | 493 | return { 494 | createApp: createAppApi(render) 495 | } 496 | } 497 | function getSequence(arr: number[]): number[] { 498 | const p = arr.slice(); 499 | const result = [0]; 500 | let i, j, u, v, c; 501 | const len = arr.length; 502 | for (i = 0; i < len; i++) { 503 | const arrI = arr[i]; 504 | if (arrI !== 0) { 505 | j = result[result.length - 1]; 506 | if (arr[j] < arrI) { 507 | p[i] = j; 508 | result.push(i); 509 | continue; 510 | } 511 | u = 0; 512 | v = result.length - 1; 513 | while (u < v) { 514 | c = (u + v) >> 1; 515 | if (arr[result[c]] < arrI) { 516 | u = c + 1; 517 | } else { 518 | v = c; 519 | } 520 | } 521 | if (arrI < arr[result[u]]) { 522 | if (u > 0) { 523 | p[i] = result[u - 1]; 524 | } 525 | result[u] = i; 526 | } 527 | } 528 | } 529 | u = result.length; 530 | v = result[u - 1]; 531 | while (u-- > 0) { 532 | result[u] = v; 533 | v = p[v]; 534 | } 535 | return result; 536 | } 537 | -------------------------------------------------------------------------------- /src/runtime-core/scheduler.ts: -------------------------------------------------------------------------------- 1 | let p = Promise.resolve() 2 | let queue = new Array() 3 | let isFlushPending = false 4 | 5 | export function nextTick(fn:any) { 6 | return fn ? p.then(fn) : p 7 | } 8 | export function queueJobs(job:any):void { 9 | if(!queue.includes(job)){ 10 | queue.push(job) 11 | queueFlush(); 12 | } 13 | 14 | } 15 | export function queueFlush() { 16 | // 首次触发时,isFlushPending 设置为true 17 | // 多次触发时,只添加到任务队列,然后到这里旧直接返回 18 | // 任务执行完后再设置为false,避免多次调用 flushJobs 19 | if(isFlushPending) return 20 | isFlushPending = true 21 | nextTick(flushJobs) 22 | } 23 | export function flushJobs(){ 24 | isFlushPending = false 25 | let jobs 26 | while ((jobs = queue.shift())){ 27 | jobs && jobs() 28 | } 29 | } -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import {isArray, isObject, isString} from "../shared/index"; 2 | import {shapeFlags} from "../shared/ShapeFlags"; 3 | export const TEXT = Symbol('TEXT') 4 | export const FRAGMENT = Symbol('FRAGMENT') 5 | export { 6 | createVNode as createElementVNode 7 | } 8 | export function createVNode(type: any, props?: any, children?: any) { 9 | const vnode = { 10 | __is_VNode: true, 11 | type: type, 12 | props, 13 | children, 14 | components:null,// 组件实例 15 | shapeFlag: getShapeFlag(type),// 设置初始时点的 shapeFlag 16 | } 17 | if (isString(vnode.children)) { 18 | vnode.shapeFlag! |= shapeFlags.TEXT_CHILDREN 19 | } 20 | if (isArray(vnode.children)) { 21 | vnode.shapeFlag! |= shapeFlags.ARRAY_CHILDREN 22 | } 23 | // 判断slots 24 | if(vnode.shapeFlag! & shapeFlags.STATEFUL_COMPONENT){ 25 | if (isObject(vnode.children)) { 26 | vnode.shapeFlag! |= shapeFlags.SLOTS_CHILDREN 27 | } 28 | } 29 | 30 | return vnode 31 | } 32 | 33 | function getShapeFlag(type: any) { 34 | if (isString(type)) { 35 | return shapeFlags.ELEMENT 36 | } 37 | if (isObject(type)) { 38 | return shapeFlags.STATEFUL_COMPONENT 39 | } 40 | } 41 | export function createTextVNode(children:string){ 42 | return createVNode(TEXT,{},children) 43 | } 44 | export function isVNode(target: any) { 45 | if (isObject(target)){ 46 | return false 47 | } 48 | return !!target.__is_VNode 49 | } -------------------------------------------------------------------------------- /src/runtime-dom/index.ts: -------------------------------------------------------------------------------- 1 | import {createRenderer} from "../runtime-core/renderer"; 2 | 3 | function createElement(type:string) { 4 | return document.createElement(type) 5 | } 6 | 7 | /** 8 | * 渲染props 9 | * @param el 10 | * @param props 11 | */ 12 | function patchProp(el:any,key:string,oldVal:any,newVal:any) { 13 | const isOn = (key: string) => { 14 | return /on[A-z]/.test(key) 15 | } 16 | // 处理事件 17 | if (isOn(key)) { 18 | const eventName = key.slice(2).toLowerCase() 19 | el.addEventListener(eventName, newVal) 20 | } else { 21 | if(newVal === null || newVal === undefined){ 22 | el.removeAttribute(key) 23 | }else{ 24 | el.setAttribute(key, newVal) 25 | } 26 | } 27 | 28 | } 29 | function insert(el:any, container:any) { 30 | container.append(el) 31 | } 32 | function setElementText(container:any,text:string) { 33 | container.textContent = text 34 | } 35 | function remove(el:any) { 36 | const parent = el.parentNode 37 | parent && parent.removeChild(el) 38 | } 39 | const renderer:any = createRenderer({ 40 | createElement, 41 | patchProp, 42 | insert, 43 | remove, 44 | setElementText, 45 | }) 46 | 47 | export function createApp(...args:any[]){ 48 | return renderer.createApp(...args) 49 | } 50 | export * from '../runtime-core' -------------------------------------------------------------------------------- /src/shared/ShapeFlags.ts: -------------------------------------------------------------------------------- 1 | export const enum shapeFlags { 2 | ELEMENT = 1, // 0001 3 | STATEFUL_COMPONENT = 1 << 1, // 0010 4 | TEXT_CHILDREN = 1 << 2, // 0100 5 | ARRAY_CHILDREN = 1 << 3, // 1000 6 | SLOTS_CHILDREN = 1 << 4, 7 | } 8 | 9 | // shapeFlags 是vnode的类型标记 10 | // 通过位运算操作 11 | // 查找 使用 & 12 | // 修改使用 | 13 | // & (都为 1 才得 1 ) 14 | // | (都为 0 才得 0 ) -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toDisplayString' 2 | export const extend = Object.assign 3 | export const isObject = (raw:any):boolean =>{ 4 | return raw !== null && typeof raw === 'object' 5 | } 6 | export const isString = (raw:any)=> typeof raw === "string" 7 | export const isArray = (raw:any):boolean =>{ 8 | return Array.isArray(raw) 9 | } 10 | 11 | 12 | export const hasChanged = (val:any,nVal:any):boolean =>{ 13 | return !(Object.is(val,nVal)) 14 | } 15 | 16 | export const hasOwn = (val:any,key:string):boolean => Object.prototype.hasOwnProperty.call(val,key) 17 | 18 | export const toHandlerKey = (str:string) =>{ 19 | return str ? `on${capitalize(str)}`:''; 20 | } 21 | export const capitalize = (str:string) =>{ 22 | return str.charAt(0).toUpperCase() + str.slice(1) 23 | } 24 | export const camelize = (str:string) =>{ 25 | return str.replace(/-(\w)/g,(_,c):string=>{ 26 | return c ? c.toUpperCase() : '' 27 | }) 28 | } 29 | export const EMPTY_OBJ = {} -------------------------------------------------------------------------------- /src/shared/toDisplayString.ts: -------------------------------------------------------------------------------- 1 | export function toDisplayString(value:any){ 2 | return String(value) 3 | } -------------------------------------------------------------------------------- /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 s to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where 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": true, /* 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 | -------------------------------------------------------------------------------- /vue总结.md: -------------------------------------------------------------------------------- 1 | # vue3知识点总结 2 | ## reactive 响应式系统 3 | `vue3` 的响应式系统是基于订阅发布者模式,通过 `proxy` 实现对数据的响应式化, 4 | 而依赖副作用的收集就是根据响应式化的数据来进行的。 5 | 因为 `Proxy` 本质上是对某个对象的劫持,这样它不仅仅可以监听对象某个属性值的变化,还可以监听对象属性的新增和删除; 6 | 而 `Object.defineProperty` 是给对象的某个已存在的属性添加对应的 `getter` 和 `setter`,所以它只能监听这个属性值的变化,而不能去监听对象属性的新增和删除。 7 | 8 |

注意

9 | 这里需要明确的一点,`reactive` 只是做了数据的响应式化处理,使用 `proxy` 代理数据对象,并在 `get`、`set` 中完成了数据访问劫持和数据设置触发依赖。 10 | `get` 和 `set` 是在有访问或设置操作进行是才会触发的, 11 | 而 `vue` 的依赖收集(track)实际上是要结合 `effect Api` 进行的。 12 | 例如: 13 | 14 | ```javascript 15 | let foo = reactive('foo') 16 | let consoleFoo = ()=>{ 17 | console.log(foo) 18 | } 19 | effect(consoleFoo) 20 | ``` 21 | 22 | 在上述代码中,首先通过 `reactive` 对 `foo` 做了代理,而 `consoleFoo` 方法则访问了 `foo` 并打印, 23 | 然后在 `effect` 中传入的函数 `consoleFoo`,在这个过程中,`effect` 会运行一遍传入的函数 `consoleFoo`, 24 | 此时 `consoleFoo` 会被当做当前激活依赖存放在全局变量 `activeEffect` 上,而 `consoleFoo` 运行时访问了 `foo`,此时会触发 `get`,从而将当前的 25 | `activeEffect`(也就是 `consoleFoo`),当做 `foo` 的依赖进行收集。 26 | ### reactive 的基本实现 27 | `reactive API` 的实现其实就是创建并返回了一个 `proxy` 对象, 28 | 将原始数据对象 `raw` 和 `mutableHandlers` 传递给 `proxy` 构造函数, 29 | 完成对象的响应式代理。 30 | 其中 `mutableHandlers` 位于 `baseHandlers.ts` 中,其具体实现中 31 | `Getter` 实现数据访问劫持: 32 | `Getter` 接受两个参数 `isReadonly = false`,`isShallow = false`,用于标记是否创建的是 `readonly` 或 `shallow` 33 | 并且 返回了一个 `get` 方法,在 `get` 方法内部 34 | * 首先判断访问的 `key` 是否是 `__v_reactive` 或 `__v_readonly`,并返回(`isReadonly` 和 `isReactive` 实现), 35 | * 然后根据 `Getter` 接受两个参数判断 `isReadonly = false`,则调用 `track` 做依赖收集。 36 | * 判断 `isShallow = true`,直接返回访问目标值 `res` 37 | * 如果访问目标值 `res`是对象,则根据 `isReadonly` 判断递归调用 `readonly Api` 或 `reactive Api` 并传入 `res` 38 | 39 | `setter` 实现数据设置派发更新: 40 | 将访问的目标对象 `target`、设置的 `key`,设置的值传递 `trigger` 方法 派发啊更新 41 | ### effect的基本实现(runner、scheduler、stop)以及依赖收集与触发依赖 42 | #### effect 主流程 43 | `effect` 接受参数 `effect` 接受依赖函数 `fn` 和配置对象(内含调度执行函数 `scheduler` 和 `onStop方法`), 44 | * 方法内部会通过 `ReactiveEffect` 创建 `_effect` 对象(`_effect` 接受依赖函数fn,和调度执行函数 `scheduler` ), 45 | * 然后会将 `onStop` 通过 `Object.assign` 拷贝到 `_effect` 46 | * 执行 `_effect.run` (其实这里就是执行了 依赖函数 `fn`,因为创建 `_effect` 时已经传递进去了),此时如果 `fn `内部访问了 响应式对象,fn则会当做依赖被收集到对应 响应式对象依赖中。 47 | * 创建 `runner` `_effect.run.bind(_effect)`),并把 `_effect` 存在 `runner` 上(实现 `stop` 方法) 48 | * 返回 `runner`(`_effect.run.bind(_effect)`),用户可以手动通过 `runner` 触发依赖, 49 | #### ReactiveEffect 对象 50 | ```javascript 51 | _fn // 依赖函数 52 | deps = [] // 依赖函数集合 53 | active = true // 是否激活,stop方法执行后用于判断 54 | onStop = ()=>{} // stop 钩子方法 55 | scheduler 56 | ``` 57 | 首先依赖收集有全局变量 `activeEffect`(当前激活的 `effec` t对象,内含副作用依赖),`shouldTrack`(是否需要收集) 58 | `ReactiveEffect` 对象还包含一个 `run` 方法 和 `stop` 方法, 59 | 在构造方法时,`_fn` 会缓存当前传入的依赖函数 `fn`, 60 | 当外部调用 `run` 方法时,`run` 方法内部会先判断 `active === false`, 61 | 命中则直接返回 依赖函数执行结果 `this._fn()`; 62 | 否则 `shouldTrack` 设置为 `true` 63 | 就把 `activeEffect` 指向当前 `ReactiveEffect` 对象(用于 `track` 时收集), 64 | 执行结果 `res = this._fn();` 65 | `shouldTrack` 设置为 `false` 66 | 最后返回 `res` 67 | 注意,实际上 `activeEffect` 指向当前 `ReactiveEffect` 对象这一步是通过effectStack 栈 维护的 68 | ````javascript 69 | const counter = reactive({ 70 | num: 0, 71 | num2: 0 72 | }) 73 | function logCount() { 74 | effect(logCount2) 75 | console.log('num:', counter.num) 76 | } 77 | function count() { 78 | counter.num++ 79 | } 80 | function logCount2() { 81 | console.log('num2:', counter.num2) 82 | } 83 | effect(logCount) 84 | count() 85 | ```` 86 | 上述代码中,如果单纯的把`activeEffect` 指向当前 `ReactiveEffect` 对象,`effect(logCount2)` 87 | 执行完后 activeEffect 指向的是 `logCount2`,而后续的 `console.log('num:', counter.num)` , 88 | 会导致错误的将 `logCount2`作为 `num`的依赖收集, 89 | 此时我们`count()`,触发的依赖却是 `logCount2`,在 `fn` 执行完毕后出栈,再把 `activeEffect` 指向 `effectStack` 最后一个元素, 90 | 也就是外层 `effect` 函数对应的 `reactiveEffect` 91 | 92 | 93 | 94 | 当外部调用 `stop` 方法时,`stop` 方法内部会先判断 `active === true`, 95 | 然后判断是否传入了 `onStop` 钩子函数,有就运行 `onStop`。然后清除依赖,并把 `active = false`, 96 |

注意

97 | `stop` 方法想要实现的效果其实是 `stop` 后,清空收集的依赖并不在收集、不执行副作用函数,需要手动调用 `runner`, 98 | 这个效果实际上是通过 `shouldTrack` 和 `this.active` 实现的 99 | 正常情况下 `run` 方法运行,`shouldTrack` 设置为 `true` ,然后执行 `this._fn()`,在这个 `this._fn()` 过程中会触发 `track` 做依赖收集, 100 | 而 `track` 会判断 `shouldTrack`,为 `false` 直接返回不收集, 101 | 正常流程 `shouldTrack = true =》this._fn() =》 track 收集 =》shouldTrack = false`, 102 | 注意此流程走完后依赖收集完毕是 `false` 的, 而在调用 `stop` 方法后因为 `this.active` 变为 `false`,`run` 方法运行他会直接返回 `this._fn()` 的结果,不会在设置 103 | `shouldTrack = true`,于是在 `track` 时会被直接返回捕收剂。 104 | #### 为什么effect.run 每次运行都要清空effect对象上的依赖? 105 | 见 `why should cleanupEffect in ReactiveEffect` 106 | ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 107 | #### stop方法Api 108 | `stop`方法`Api`,传入一个 `runner`,当 `trigger` 后 ,不执行副作用函数,需要手动调用 `runner`, 109 | 其内部就是通过传入的 `runner` 访问都 `effect` 对象,并调用 `effect` 对象上的 `stop` 方法 110 | 111 | #### track 112 | 先判断 `shouldTrack`,为 `false` 直接返回不收集 113 | 全局维护了一个 `targetMap`,`key` 是响应式原始对象 `target`,值是 `depsMap`。 114 | `depsMap`的 `key` 是 `target` 中的各个 `key`,值是一个 `deps`(本质是 `Set` ), 115 | `deps` 中每个元素是都对应这这个 `key` 的一个 `activeEffect`( `ReactiveEffect` 对象,依赖函数 `fn` 实际上就存储在 `ReactiveEffect` 的 `_fn`上) 116 | `track` 方法主要是对上述数据结构的一些维护,比如没有就创建,有就获取,最终创建或找到对应 `target` 的对应 `key` 的依赖集合 `deps`,然后调用 `trackEffects` 把 `activeEffect` 收集到 `deps` 中, 117 | 值得注意的时 `trackEffects` 还反向收集 `activeEffect.deps.push(dep)`,在清除依赖时使用 118 | #### trigger 119 | 从 `depsMap` 中找到对应 `target` 对应 `key` 的 `deps`,并传递给 `triggerEffects` 调用 120 | `triggerEffects` 循环遍历 `dep` 拿到每个 `effect` 对象 调用 `run` 或 `scheduler` 121 | #### scheduler 的调度执行 122 | `scheduler` 的调度执行,通常会在一些调度优化、计算属性 `compute` 中会使用到,其表现出来的效果是 123 | 1.`effect` 支持传入一个包含名为 `scheduler` 函数的 `options` 124 | 2.`effect` 首次执行时,传入给 `effect` 的`fn`(即依赖)执行 125 | 3.当对应响应式对象 `set` 并 `trigger` 时,不执行 `fn`(即依赖) 而执行 `options` 的 `scheduler` 函数 126 | 4.当执行 `runner` 时(`effect` 的返回) 能够执行 `fn`(即依赖) 127 | 128 | 其实现就是在 `trigger` 时 `triggerEffects` 变量依赖集合 `dep` 并调用里面依赖对象时,判断依赖对象 `effect` 是否 129 | 有 `scheduler` 有就执行否则就执行run来执行依赖函数 `fn` 130 |

注意

131 | `scheduler` 调度执行只是没有执行 `fn` 而是执行了 `scheduler`,但是响应式数据对象的值是改变了的 132 | 133 | ### readonly 的基本实现 134 | 与 `reactive` 相似,只是创建 `proxy` 时传入的 `Getter` 的 `isReadonly` 为 `true` 135 | 这使得在触发 `get` 做依赖收集时,不再执行 `track` , 136 | `set` 直接返回 `true`,不修改值,实现 只读 与 不收集依赖。 137 | ### shallowReadonly 的基本实现 138 | 与 `reactive` 相似,只是创建 `proxy` 时传入的 `Getter` 的 `isReadonly` 为 `true`,`isShallow` 为 `true` 139 | 这使得在触发 `get` 做依赖收集时,不再执行 `track` ,然后先判断 `isShallow` 命中 直接返回,不做递归执行 `reactive` 或 `readonly` 140 | ### isReadonly 的基本实现 141 | 接受一个 `target` 作为参数,访问 `target` 上的 `__v_readonly` 属性,这会触发 `get` 142 | 在 `get` 中 判断是否访问的 key 是 `__v_readonly`,命中 则返回 `isReadonly`(`readonly(obj)` 时,传入给 `Getter` 为 `true`) 143 | ### isReactive 的基本实现 144 | 接受一个 `target` 作为参数,访问 `target` 上的 `__v_reactive` 属性,这会触发 `get` 145 | 在 `get` 中 判断是否访问的 key 是 `__v_reactive`,命中 则返回 `!isReadonly`(`readonly(obj)` 时,传入给 `Getter` 为 `true`) 146 | ### isProxy 的基本实现 147 | 接受一个 `target` 作为参数,返回 `isReactive(target) || isReadonly(target)` 148 | ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 149 | 150 | ### ref 的基本实现 151 | ref 的出现与设计 是因为 reactive 是基于 proxy 实现 get 、set 来实现数据的访问劫持与设置派发更新,这是针对对象而言的,而对于基本数据对象类型 152 | String、Boolean、Number等,则需要对包装一层对象,再通过 proxy 来实现数据的访问劫持与设置派发更新。 153 | ref 实际上是 创建并返回了一个 refImpl 对象 154 | #### refImpl 155 | ```javascript 156 | _value // 当前值 157 | _rawValue // 原始值 158 | dep = new Set() // 依赖集合 159 | __v_isRef = true // 是否是 ref 对象标识 160 | ``` 161 | 构造函数运行时,会先判断传入是否为对象,是则先用 `reactive` 处理一遍,然后把他们存在 `_value` 上,`_rawValue` 存储一开始的 `_value`, 162 | `refImpl` 的 `get` 方法 实现数据劫持,判断 `shouldTrack` 和 `activeEffect` 直接调用 `trackEffects` 依赖收集,并返回value 163 | `refImpl` 的 `set` 方法 实现数据设置派发更新,判断根据 `_rawValue` 数据是否变化, 164 | 判断新值是否为对象,是则先用 `reactive` 处理一遍, 165 | 然后把他们存在 `_value` 上,`_rawValue` 存储一开始的 `_value`, 166 | `triggerEffects` 派发更新 167 | ### proxyRefs 的基本实现,实现ref的访问代理 168 | `proxyRefs` 能够代理 一个对象中的 `ref` 对象,在 `template` 模板中会用到 169 | 1.`proxyRefs` 方法返回一个对象,该对象不需要`.value`即可访问 `ref` 对象值 170 | 2.通过对 `proxyRefs` 方法返回对象进行修改,能够映射到对应 `ref` 对象上进行修改 171 | 其本质是返回一个 代理 `ref` 的 `proxy` 代理对象 172 | 在 `get` 中 判断访问的 `target` 是否是 `ref`,是这返回 `res.value`,否则返回 `res` 173 | 在 `set` 中 判断访问的 `target` 是否是 `ref` 且 新值不是 `ref`,则直接 `targetVal.value = 新值`,否则直接 `targetVal = 新值` 174 | ### compute计算属性的基本实现 175 | 1.`computed` 接受一个方法 `fn` ,该方法内部应该具有访问响应式对象的语句 176 | 2.`computed` 返回一个通过 `.value`访问的对象,`.value`会触发 `computed` 接受方法 `fn`,并拿到返回值 177 | 3.`computed` 具有惰性,多次访问 `.value`,在对应响应式对象值不改变的时候,不会多次触发接受的方法 178 | 4.`computed` 在对应响应式对象值改变的时候,访问 `.value`,才触发接受的方法 `fn` 179 | 其本质是对effect方法的封装,并利用了effect和scheduler配置,它内部创建了一个 `computedRefsImpl` 对象,把 `fn` 传递给构造函数 180 | 在构造时,会把 `fn` 通过 `ReactiveEffect` 创建一个 `effect` 对象 放在 `this.effect` 中,并传入调度执行方法 `scheduler`。 181 | 在 `scheduler` 方法内部会修改 `computedRefsImpl` 对象 上的属性 `this._isDirty = true`,这样响应式值改变时不会触发更新,而在 `.value` 访问时 182 | 又可以获取到新的值 183 | 在 `get` 方法内部 `this._isDirty = true`,则重置 `this._isDirty = false`,并调用 `this.effect.run()` 触发更新,返回更新后的值 184 |

注意

185 | `computed` 返回的值,在 `.value` 访问时,由于没有进行 `set` 操作,故 `_isDirty = false`,不会在 `get` 中执行 `this._effect.run()`, 186 | 而由于是调度执行,在给 `fn` 里的响应式对象赋值后,会触发 `scheduler`,`_isDirty = true`,在在 `.value` 访问时get中执行 `this._effect.run()` 187 | 从而实现惰性, 188 | 此外这里是 直接调用 `ReactiveEffect` 而不是 `effect Api`,所以 `fn` 不会先执行一次 , 189 | 而且还会在 `computeRefImpl` 的构造函数和 `get` 方法上分别 `trigger` 和 `track` 一下计算属性对象,因为它也具有响应式,某些依赖方法会访问它。 190 | `isDirty` 初始为 `true` ,这样在首次访问`.value`时就可以`run`,收集依赖 191 | 192 | ### watch 监听属性的基本实现 193 | `watch` 是用来监听响应式对象的。 194 | `watch` 与 `watchEffect` 都是通过调用 `doWatch` 来实现的 195 | 其实他们本质还是基于对 `effect` 方法的封装,并利用了 `effect` 的 `lazy` 和 `scheduler` 配置, 196 | 在 `doWatch` 方法中,会根据传递进来的第一个参数,也就是监听目标,根据它的类型去封装成一个 `getter` 函数 做不同的处理,例如是`ref`,则`getter = (s)=>s.value`, 197 | 是数组则 `getter` 内部会遍历这个数组,是`reactive`,则会递归的访问每一个键等等,这个封装的`getter`是为了能够拿到监听目标的最新值并返回, 198 | 所以会作为`effect`的第一个参数传递个`effect`,如此则实现了对监听目标的监听;然后设置了 199 | `effect` 为`lazy`配置为true,防止`effect`会运行一次`getter`;而在`watch`内部还封装了一个方法 `job`,这个`job`方法会作为`effect`的`scheduler`方法传递给`effect`, 200 | 在 `watch` 内维护着 `nVal` 和 `oVal`,在`job`内部会通过执行`effect`的返回值`effectFn`(通过`runner`最终执行的是`getter`),拿到新值, 201 | 并调用用户传递的`cb`,将新值和旧值传递给用户(最后会更新旧值),这样就实现了监听目标改变,能够相应的运行 202 | 用户传递个`cb`,并拿到新旧值功能。 203 | 关于配置项 `immediate`,如果传了,`watch`内部会立即执行一次`job`。 204 | 关于 `onInvalidate`,`watch`内部有一个 `onInvalidate` 方法,会在`job`执行是作为第三个参数通过`cb`传递给用户,而用户传递给`onInvalidate`的`cb`会放在全局变量`cleanup`上,每次执行`job` 205 | 前都会判断执行一次`cleanup`,这个属性可以用于异步场景中监听目标多次改变引发的过期处理。 206 | ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 207 |
208 | 209 | ## vue3.2中 对依赖收集与清空的优化 210 | 在vue3.2以前,effect每次run都会对对应的effect对象的deps中依赖进行清除, 211 | 这个过程涉及大量的set,delete等操作,而在实际使用场景中依赖关系是很少改变的, 212 | 因此这里存在一定的优化空间。 213 | 1.首先设置了一个`effect`最大递归层数30,与当前`effect`递归层数 `effectTrackDepth` 214 | 当 `effectTrackDepth` 达到30,每次run才对对应的对应的effect对象的deps中依赖进行清除。 215 | 并在track时做全依赖收集。 216 | 2.当递归层数小于30时,在依赖收集的数据结构中,dep原先是一个set, 217 | 现在增加了一个w属性(dep依赖最先被搜集层数), 218 | wasTracked的缩写,采用二进制格式,每一位表示不同effect嵌套层级中,该依赖是否已被跟踪过(即在上一轮副作用函数执行时已经被访问过) 219 | 一个n属性用于标识依赖最新出现的层数, 220 | newTracked的缩写,采用二进制格式,每一位表示不同effect嵌套层级中,该依赖是否为新增(即在本轮副作用函数执行中被访问过) 221 | 在trigger触发effect的run时,会将deps上的w属性设置为当前层数, 222 | 而再次 track 时,有一个shouldTrack变量初始为false,它为true时才真正把activeEffect放入deps 223 | 然后判断deps的n属性(初始为0,初始比进标记逻辑),若 `n` 层数与当前层数 `trackOpBit` 224 | 位运算为0,则用 `trackOpBit` 更新n属性,这样在嵌套场景中,若某个依赖在多个层级出现,则能够更新n属性, 225 | 若在同层出现,则表示已经在同层收集过,不在进行收集,更新n属性后,会根基w属性与当前层数 `trackOpBit` 226 | 来判断是否需要收集。 227 | n与w属性的设计使得在某些嵌套场景中,依赖重复收集的问题得到优化,结合最大递归数,使得减少了依赖清除操作,使得 228 | 性能得到提升。 229 | 而最后effect的run方法还有一个finally,在其中调用了`finalizeDepMarkers`方法,对曾经标记收集过的依赖, 230 | 而本次副作用没有进行标记跟踪的依赖进行删除。 231 | ````javascript 232 | const a = ref(0) 233 | effect(()=>{ 234 | console.log('1 ---- ',a) 235 | console.log('2 ---- ',a) 236 | }) 237 | a.value = 1 238 | ```` 239 | 初始化, 240 | 第一个console 241 | trackOpBit = 1 << ++effectTrackDepth trackOpBit = 2 242 | effectTrackDepth < 30 进入initDepMarker设置w,由于deps初始化没有length = 0,跳过设置w,w依旧为0 243 | 进入track,!newTracked => !(n & trackOpBit) => !(0 & 2) => !0 => true,是本层新收集的 244 | n |= trackOpBit => n = 2 245 | !wasTracked => !(w & trackOpBit) => !(0 & 2) => !0 => true,是没有收集过的,进行收集 246 | 第二个console 247 | 前面一样,只是进入 !newTracked 时 n为2,!(2 & 2) => !2 => false,不是本层新收集的,不收集 248 | finally 249 | 重置 w = 0;n = 0; 250 | 更新 251 | 前面一样,只是进入进入initDepMarker设置w,由于deps再上一轮收集时有收集了,length = 1, 252 | w |= trackOpBit =》 0 |= 2 =》w = 2 253 | track时,前面都一样,只是!wasTracked => !(w & trackOpBit) => !(2 & 2) => !2 => false, 254 | 这个依赖在上一轮effect运行时收集过了,不再收集 255 | 256 | ````javascript 257 | const a = ref(0) 258 | const show = ref(true) 259 | effect(()=>{ 260 | if(show.value){ 261 | console.log(a.value) 262 | } 263 | }) 264 | show.value = false 265 | ```` 266 | 初始化都一样 267 | 更新 268 | 此时deps里面[DepShow,DepA] 269 | 更新先访问show,触发show的trigger, 270 | trackOpBit = 1 << ++effectTrackDepth trackOpBit = 2 271 | initDepMarker设置w,由于deps再上一轮收集时有收集了,length = 2, 272 | 他会遍历所有dep挨个设置w 273 | DepShow.w |= trackOpBit =》 0 |= 2 =》w = 2 274 | DepA.w |= trackOpBit =》 0 |= 2 =》w = 2 275 | 进入track,!newTracked => !(n & trackOpBit) => !(0 & 2) => !0 => true,是本层新收集的 276 | n |= trackOpBit => n = 2 277 | !newTracked 时 n为2,!(2 & 2) => !2 => false,不是本层新收集的,不收集 278 | 而由于if逻辑,不再访问a,所以a的trigger不触发,effect.run执行完 279 | 此时 DepShow.w = 2,DepA.w = 2,DepA.n = 0 DepShow.n = 2 280 | finally 281 | 遍历 deps,判断删除依赖 282 | wasTracked:DepA.w & trackOpBit => 2 & 2 =>true; DepShow.w & trackOpBit => 2 & 2 =>true; 283 | !newTracked:!(DepA.n & trackOpBit) => !(0 & 2) =>true; !(DepShow.n & trackOpBit) => !(2 & 2) =>false; 284 | DepA 会被删除 285 | 286 | ## runtime-core 运行时核心-初始化 287 | createRenderer 方法创建渲染器对象,他是可拔插设计,接受支持传入参数包括创建节点方法、节点传入方法、节点移动方法、节点删除方法等, 288 | 这种可拔插设计使得具体渲染流程与具体的元素操作逻辑解耦,实现不同平台渲染器的支持。 289 | createApp 方法实际上内部返回的就是 createRenderer方法创建渲染器对象的createApp属性的调用运行结果,这个属性的值是createAppApi方法的返回值, 290 | createAppApi实际上是一个闭包,他接受一个渲染参数render(这个参数是定义在createRenderer中的,上下文关系createAppApi调用时是在createRenderer的返回值中) 291 | createAppApi返回的函数中接受参数为rootComponent 即入口根组件,createAppApi返回的函数中的返回值则是包含mount方法的对象,这样我们就可以调用mount方法进行挂载了 292 | mount方法接受根节点,可以是dom选择器字符串,也可以是dom节点。 293 | mount 方法内部会根据 ,根据根组件 rootComponent ,创建vnode,并把rootContainer、vnode传递给render方法 294 | render方法,就开始了正式的初始化流程。 295 | ### component组件的基本初始化流程 296 | 在render方法内部实际上会调用patch方法来处理,patch方法来会根据vnode的type以及patchFlag进行判断然后走不同的分支逻辑, 297 | 例如Type是文本就会走processText,是Fragment就会走processFragment, 298 | 组件则会走 299 | processComponent -> n1为 undefined -> mountComponent ( 300 | 创建组件实例 createComponentInstance 301 | 处理setup setupComponent -> initProps -> initSlots -> setStatefulComponent->handleSetupResult->finishComponentSetup 302 | 调用处理render具体渲染 setupRenderEffect 303 | ) 304 | setStatefulComponent:创建组件代理使得render方法内能够通过this访问组件实例,如this.$el等、在setup调用前创建currentInstance、调用setup拿到setupResult 305 | handleSetupResult:根据setupResult结果做处理,setupResult是对象则挂载 instance.setupState上,这里用proxyRefs做了ref的解包 306 | 是方法则作为渲染方法挂载 instance.render上。 307 | finishComponentSetup:处理render,渲染方法挂载 instance.render上。 308 | render来源 309 | 1.setup返回render,优先级最高,在handleSetupResult中先挂到instance.render上 310 | 2.组件内option的render,优先级第二 在finishComponentSetup中挂到instance.render上 311 | 2.template编译生成的render,优先级最低 在finishComponentSetup中,当有编译结果compiler && !组件内option的render && !instance.render 312 | compiler才挂到instance.render上 313 | setupRenderEffect: 314 | 主要是逻辑调用组件实例上的渲染方法 instance.render 拿到子树虚拟节点,并递归的调用patch递归处理子节点,并把组件实例上instance.isMounted设置为true, 315 | 下次组件就根据isMounted 走更新逻辑。 316 | 这些逻辑是放在effect中调度执行的,这样就实现了视图个更新对比,effect的返回值会存储在instance.update上,当组件或元素更新时,在effect调度执行 317 | scheduler,把instance.update放入微任务队列中执行,实现视图更新patch。 318 | ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 319 | ### 普通dom的Element元素基本初始化流程 320 | 在render方法内部实际上会调用patch方法来处理,patch方法来会根据vnode的type以及patchFlag进行判断然后走不同的分支逻辑, 321 | 例如Type是文本就会走processText,是Fragment就会走processFragment, 322 | dom元素则会走 323 | processElement -> n1为 undefined -> mountElement( 324 | 调用传入给 createRenderer 的 createElement 根据vnode创建真实节点 el; 325 | 根据 children 和 patchFlag分别处理,children是文本就直接给el,是数组则遍历递归走patch; 326 | 遍历vnode的props,调用传入给 createRenderer 的 patchProp 给el添加属性或添加事件; 327 | (beforeMounted) 328 | 调用传入给 createRenderer 的 insert将el插入都真实节点容器中 329 | (onMounted) 330 | ) 331 | ### 组件代理对象的基本实现 332 | 在render函数运行时,能够通过this,访问到组件实例 333 | 原理是在组件初始流程时,setStatefulComponent中创建了组件代理对象,并存储都instance.proxy上 334 | 在setupRenderEffect时,调用instance.render 获取subTree时,通过call将instance.proxy作为render运行时 335 | 的this实现。 336 | 组件代理对象的handler实现就是根据访问的key做判断,返回组件实例上对应的值 337 | 访问的key在instance.props中就从props取值返回,在instance.setupState中就取值返回 338 | 访问的是 $el 就返回 instance.el,是$slots 就返回instance.slots,是$props 就返回instance.props 339 | ### shapeFlags的基本实现,使用二进制来做判断标志 340 | shapeFlags 是 vnode的类型标记 341 | 通过位运算操作 342 | 查找 使用 & 343 | 修改使用 | 344 | & (都为 1 才得 1 ) 345 | | (都为 0 才得 0 ) 346 | ### 实现组件的事件注册、props 347 | 无论是组件还是element的属性、事件,对于事件和作用于元素的props(例如class),都是作用于dom元素的, 348 | 组件的事件和props 都会在setupComponent时,被存储到instance.props上, 349 | 在mountElement时,会遍历instance.props并调用props处理方法, 350 | 事件会被编译成以on开头的props,如果props被正则匹配到on开头,则会当做事件被添加到el上, 351 | 否则则会作为el的属性被添加上去,另外,在更新时还会传入旧的props做对比,实现更新或删除。 352 | 然而element的属性设置是根据具体情况来调用不同的API的 这是因为HTML Attribute 与 Dom Properties的表现不同导致的 353 | ```` 354 | 355 | 356 | ```` 357 | 会分别被解析成 disabled:'' 和 disabled:false,调用setAttribute 会一直被禁用,因为 false 会被解析为 'false', 358 | 使用 el.disabled 会被一直开启,因为 el.disabled = '' 等价 el.disabled = false, 359 | 所以正确的设置这里还是做了很多边界条件的判断的。 360 | ### 实现组件的emit功能,与setup内props 361 | 在setStatefulComponent时,调用setup方法时,会把组件实例的props作为第一个参数,传递给setup, 362 | emit功能则是放在一个对象里,作为第二个参数传递给setup,这样用户就可以在setup中获取props以及emit方法 363 | emit方法具体实现其实是,将第一个参数,事件名进行驼峰表转化处理,使得其格式与props的事件格式统一, 364 | (props的事件会被转化为on开头),第一个参数处理后去props中取对应的事件,并把参数传递给它触发用户的事件运行。 365 | 366 | ### 更新 element的 props 367 | element的 props 的更新在patchElement流程中的patchProps中进行,对元素属性、事件的增删改依旧是 368 | 使用的createRenderer方法传递进来的patchProp方法,它会根据传递进来的props值,来判断事件、属性的增删改 369 | patchProps中,先对新的props做了遍历,在每次遍历中根据新key,到新旧props中取值 370 | 1.若旧的取不到,则表明要添加 371 | 2.若新旧不同,则表明要更新 372 | 然后对旧的props做遍历,在每次遍历中根据旧key,到新旧props中取值,新的取不到,则说明哟删除` 373 | ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 374 | ### 实现组件slot插槽 375 | 插槽节点的传入时 376 | 在编译时会被解析成一个对象,对象的键就是插槽名,值是一个返回虚拟节点或虚拟节点list的方法, 377 | 其中default键就是默认插槽 ,这个对象在创建虚拟节点时会作为children传入, 378 | 在创建虚拟节点时,当vnode节点的类型为组件且children是一个对象,则vnode的shapeFlag则会标记成插槽 379 | 在setupComponent方法流程里,对slot做初始化,会判断vnode的shapeFlag是否被标记为插槽类型, 380 | 遍历children(此时是个对象),符合则将其挨个存储到 381 | 组件实例的instance.slots上,这里有个小细节就是对返回值做了一层包装,使得插槽对象返回值都是数组 382 | 而插槽节点在熏染时,则是通过编译成renderSlots方法实现,其内部根据插槽名,在instance.slots上获取都对应插槽的渲染方法 383 | 并使用fragment进行包装渲染 384 | ### 实现 Fragment 片段与Text文本类型节点 385 | Text纯文本在编译时会被编译成使用createTextVNode来创建vnode, 386 | 此时patch时则走processText,processText则是拿到children(文本子节点),直接插入都容器container中 387 | Fragment 片段 在编译时,如果是组件类型却没有单一根子节点,则会将Fragment作为类型来创建vnode, 388 | 此时patch时则走processFragment,processFragment内部则是调用mountChildren,遍历children 挨个patch 389 | ### 实现getCurrentInstance 390 | 原理是在组件初始流程时,setStatefulComponent中在调用setup方法之前,将组件实例缓存在一个全局变量中,用户 391 | 在setup运行时,通过调用getCurrentInstance获取全局变量从而拿到组件实例instance。 392 | ### 实现组件的provide-inject 393 | provide 提供一个值使得后代组件能够访问 394 | inject 在后代组件中能够访问provide 395 | 二者一句话概括就是用了getCurrentInstance获取组件实例,provide 值挂载实例上,在子组件中instance.parent再去取 396 | ### 实现自定义渲染器custom renderer 397 | 实际上就是基于createRenderer的可拔插式的插件设计,实现了渲染的主流程与具体节点操作逻辑分离,用户可以传入自定义的 398 | 节点操作逻辑实现自定义渲染器。例如将dom节点操作方法,移动、创建、删除等全部替换成canvas的操作方法,那么就会实现基于 399 | canvas的渲染器。 400 | ### 对class的解析与处理 401 | class在编译时做了归一化处理,字符串不作处理,对象取值为true的键名拼接,二者混合数组同理 402 | ## runtime-core 运行时核心-更新 403 | ### element更新基本流程 404 | element更新基本流程原理是基于响应式系统的,在初始化流程中 最后会走到setupRenderEffect方法中, 405 | 在这个方法中,会把组件的初始化与更新逻辑作为依赖,传递给effect,并将effect返回的runner又重新挂在组件实例上, 406 | 结合effect的调度执行scheduler,当响应式数据变动时,会触发scheduler执行,scheduler执行时,会将挂在组件实例上的effect的runner 407 | 加入到微任务队列中,进行执行,这样就实现了数据变动,相应组件更新的功能。 408 | 而组件的初始化逻辑与更新逻辑,具体是通过组件实例上的变量isMounted来区分的,初始化时,为false,初始化结束后为true, 409 | 再次更新触发时,就会走更新逻辑。 410 | 411 | ### 更新 element 的 children 基本场景 412 | 元素的更新 processElement -> patchElement -> patchChildren ->patchProps 413 | patchChildren中会先获取新旧虚拟节点的子节点,并根据虚拟节点的shapeFlag进行判断,做一些基本处理 414 | 415 | 新children 为文本, 416 | 老的是空 -> 替换为新文本 417 | 老的是文本 -> 替换为新文本 418 | 老的为数组,则变量老数组挨个删除元素 -> 替换为新文本 419 | 420 | 如果新的是 空 421 | 老的是空 -> 不操作 422 | 老的是文本 -> 删除文本 423 | 老的为数组,则变量老数组挨个删除元素 424 | 425 | 如果新的是 数组 426 | 老的是空 -> mountChildren插入新数组 427 | 老的是文本 -> 删除文本 -> mountChildren插入新数组 428 | 老的为数组 -> diff 算法 429 | 430 | ### 更新 element 的 children 的新旧数组场景 - 双端快速diff算法 431 | 双端快速 `diff` 算法 维护了三个指针头部指针 `indexStart`,旧序列尾部指针 `oldIndexEnd`,新序列尾部指针 `newIndexEnd`, 432 | 1.头部扫描与尾部扫描预处理,移动头部指针,遍历新旧节点序列, 433 | 通过 `isSameVNode` 方法(对比 `type` 与 `key`)对比指针指向的节点,如果相同则递归 `patch`, 434 | 当指针指向新旧节点不同,或头部指针大于新或旧指针时,头部扫描结束 435 | 436 | 2.头部扫描与尾部扫描预处理,移动尾部指针,遍历新旧节点序列, 437 | 通过 `isSameVNode` 方法(对比 `type` 与 `key`)对比指针指向的节点,如果相同则递归 `patch`, 438 | 当指针指向新旧节点不同,或头部指针大于新或旧指针时,尾部扫描结束 439 | 440 | 头部扫描与尾部扫描预处理是借鉴了纯文本 `diff` 算法的思路,能够排除序列两端相同的节点,从而减少 `diff` 的节点数量。 441 | 以及能够轻松判断序列两端的节点删除与增加。 442 | 头部扫描 与 尾部扫描结束后,根据指针指向情况 443 | 处理头部节点序列、头部节点序列的新增或修改情况 444 | 从逻辑上来看 头部扫描 与 尾部扫描 是为了达到将那些 445 | 没有发生移动的节点预先处理的目的,此时处理过后 446 | 新旧虚拟节点数组中,没有被扫描的中间部分,才包含着移动节点的情况 447 | 448 | 3.指针指向情况处理头部节点序列、头部节点序列的新增或修改情况 449 | 450 | 3.1尾部节点新增 451 | 头大于旧尾,头针小等新尾,头到新尾新增 452 | 此时 indexStart > oldIndexEnd && indexStart <= newIndexEnd 453 | 此时 indexStart ~ newIndexEnd 之间为新增节点 454 | oE oE 455 | (a b) 或 (a b) 456 | (a b) c (a b) c d 457 | s/nE s nE 458 | 459 | 3.2旧节点删除 460 | 头大于新尾,头针小等旧尾,头到旧尾删除 461 | 此时 indexStart > newIndexEnd && indexStart <= oldIndexEnd 462 | 此时 indexStart ~ oldIndexEnd 之间为需要删除节点 463 | oE oE 464 | (a b) c 或 (a b) c d 465 | (a b) (a b) 466 | nE s nE s 467 | 468 | 3.3 剩余指针的情况,则是包含节点移动的情况,需要进一步处理 469 | 3.3.1 470 | 构建一个 newIndexMap,他是一个Map对象,根据新序列每个节点的key,建立key与节点在序列中的index索引映射 471 | 实现:遍历新序列填充Map对象 472 | 3.3.2 473 | 构建一个数组 newIndexToOldIndexMap,其长度是新序列剩余中间序列长度,初始值每个都是 0 474 | newIndexToOldIndexMap的是为了建立起新序列每个元素在旧序列中的映射关系, 475 | 其每个元素的索引本身对应新序列每个元素的索引,而元素值,则是新序列元素在旧序列中的索引。 476 | 他将后续辅助计算最大递增子序列。 477 | 478 | 实现:遍历旧序列,在遍历时 479 | 如果旧节点有 `key`,则根据 `key` 到 `newIndexMap` 中找新节点索引 `oldInNewIndex`, 480 | 如果没有 `key`,则只能遍历新序列找到对应节点的索引 `oldInNewIndex`, 481 | 这也是为什么设置key能够提高diff速度的原因, 482 | 如果旧节点没有在新的序列中找到对应的节点索引,则说明节点需要删除,调用 `hostRemove` 删除节点, 483 | 否则说明旧的节点在新的节点序列中依然存在,则根据 `oldInNewIndex` 和遍历迭代变量 `i` , 484 | 填充 `newIndexToOldIndexMap`,并将旧节点,与新节点`c2[oldInNewIndex]`进行 `patch` 485 | 486 | 当遍历结束后 newIndexToOldIndexMap 就建立完毕了,值得一提的是,在这个遍历过程中, 487 | 还做了其他两件事 488 | 1).每当旧节点在新序列中找到了 `oldInNewIndex`并进行 `patch`,还需要记录一下更新节点数量 patched, 489 | 在下次迭代循环进入时,判断patched是否大于等于新序列数量,如果命中,则说明新序列更新完了,旧序列剩余 490 | 的节点都需要删除。 491 | 2).循环外维护两个变量 move=false 和 pos=0,每当旧节点在新序列中找到了 `oldInNewIndex`并进行 `patch`, 492 | 将 oldInNewIndex 与 pos 对比,oldInNewIndex 大于等于 pos时,则更新pos等于 oldInNewIndex,否则move = true, 493 | 这一步是为了判断节点是否存在移动,如果新旧序列中,节点不存在移动,那么在遍历过程中节点在新旧序列中的索引是单调递增的。 494 | 495 | 3.3.3 496 | 此时我们拿到了是否要移动节点以及新旧节点映射关系表 `newIndexToOldIndexMap`, 497 | 根据 `newIndexToOldIndexMap` 计算最大递增子序列seq,seq返回的值在vue中是最大递增子序列位于 498 | `newIndexToOldIndexMap` 的索引值序列,他表示对应索引位置的原始不需要移动 499 | 根据最大递增子序列,使用`patched`(新剩余节点数量)开启一个for循环遍历新节点序列,并维护指针s指向seq尾部,i指向新序列(中间部分)尾部 500 | 每一次循环, 501 | 先判断i位于 `newIndexToOldIndexMap` 值是否为 0,是则patch创建新元素,并 --i移动指针,进入下一轮循环, 502 | 否则 判断 指针i是否等于指针s指向的递增子序列`seq[s]`,等于,则s--,进入下一轮循环,--i移动指针, 503 | 否则,根据 i + newStart(就是头部的索引),和下一个节点索引 i + newStart + 1,获取锚点,调用insert实现移动节点,--i移动指针 504 | 至此 双端快速diff算法结束 505 | ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 506 | ### 最大递增子序列算法 507 | 最大递增子序列算法核心思路 508 | 从给定序列第一个元素开始遍历,依次计算元素的递增序列长度,递增序列长度最大的,就是最大递增子序列 509 | 而给定序列的某一个元素,它的子序列长度,为 【1(该元素本身)】 + 【在该元素之前的其他元素中,小于该元素的,且元素x的子序列长度x.len最大】 510 | exp: 511 | 原始输入序列: 1 7 2 5 6 4 3 512 | 递增序列长度: 1 1+1(1.len)=2 1+1(1.len)=2 1+2(2.len)=3 1+3(5.len)=4 1+2(2.len)=3 1+2(2.len)=3 513 | 可得 最大递增子序列元素为 6 ,最大递增子序列长度为4 结果为 【1 2 5 6】 514 | ```` 515 | // [4,10,4,3,8,9] 516 | // 1,2,1, 1,2,3 517 | var lengthOfLIS = function(nums) { 518 | if(nums.length === 1){ 519 | return 1 520 | } 521 | if([...(new Set(nums))].length === 1){ 522 | return 1 523 | } 524 | let dep = [] 525 | dep.length = nums.length 526 | dep.fill(1) 527 | let resNum = 1 528 | let resIndex = 0 529 | for(let i = 0;i < nums.length ; i++){ 530 | if(i === 0){ 531 | dep[i] = 1 532 | continue 533 | }else{ 534 | for(let j = 0; j < i; j++){ 535 | if(nums[j] < nums[i]){ 536 | dep[i] = Math.max(1 + dep[j],dep[i]) 537 | resNum = Math.max(dep[i],resNum) 538 | if(resNum === dep[i]){ 539 | resIndex = i 540 | } 541 | } 542 | } 543 | } 544 | } 545 | console.log(nums[resIndex],resIndex) 546 | return resNum 547 | }; 548 | ```` 549 | 550 | ### vue2的diff算法基本原理 551 | vue2采用的是双端diff算法,对于新旧虚拟节点序列,维护了四个指针 552 | newStart 指向新节点序列头部 553 | newEnd 指向新节点序列尾部 554 | oldStart 指向旧节点序列头部 555 | oldEnd 指向就旧节点序列尾部 556 | 算法第一步是开启一个循环移动指针,从序列两端开始遍历序列,找相同的节点,进行移动和patch 557 | 这个循环的终止条件是新头和新尾相遇,或旧头与旧尾相遇, 558 | 而在循环中会根据指针指向的节点情况分为四种情况, 559 | o :1 2 3 4 560 | n :4 2 1 3 561 | 1.newStart === oldStart,此时表示当前头部指针指向节点相同,不需要移动,进行patch,深度优先递归patch,并移动头部指针 562 | 2.newEnd === oldEnd,此时表示当前尾部指针指向节点相同,不需要移动,进行patch,深度优先递归patch,并移动尾部指针 563 | 3.newEnd === oldStart,此时表示oldStart指向旧头节点被移动到了newEnd指向的位置, 564 | 即需要将oldStart节点insert到`oldEnd`(nextSibling)后面,进行patch,深度优先递归patch,并移动newEnd、oldStart指针 565 | 4.newStart === oldEnd,此时表示oldEnd指向的尾节点被移动到了newStart指向的位置, 566 | o :1 2 3 4 567 | n :2 4 1 3 568 | 即需要将oldEnd节点insert到`oldStart`前面,进行patch,深度优先递归patch,并移动oldEnd、newStart指针。 569 | 570 | 这四种情况之外,就是非理想情况的处理,此时处理逻辑是拿新头newStart到旧节点序列中找相同的节点,并记录索引indexOnOld, 571 | 当它大于0,则模拟情况1的逻辑,去区别时移动过后,要把indexOnOld的旧序列对应节点置为undefined,且只移动newStart指针, 572 | 这样其实就是构造了四种理想情况序列,继续遍历算法执行。 573 | o :4 1 3 2 574 | n :1 2 3 575 | 当它不满足大于0,说明newStart的节点时新增头节点,直接插入到oldStart前,并移动newStart即可。 576 | o :4 1 2 3 577 | n :1 2 3 578 | 当这个循环结束后,其实还会有其他遗漏的新增节点,此时newStart <= newEnd,而 oldEnd < oldStart,则需要遍历新序列, 579 | 将newStart ~ newEnd间的节点插入到头部。 580 | 最后则是删除节点的情况 此时 newStart > newEnd,而 oldEnd >= oldStart 581 | oldStart ~ oldEnd间的节点删除 582 | o :1 2 3 583 | n :1 3 584 | ### 组件类型的更新 585 | 对于组件或元素节点,更新逻辑最主要的是再次调用render 获取新的subTree,并将旧的subTree,与新的subTree 作为参数去patch。 586 | 在patch时再根据新的vnode类型,做不同的分支逻辑,元素类型则需要diff,这里特别说明的是组件类型的更新,实际上组件类型vnode的更新场景 587 | 是从父组件角度来看待的,因此组件类型vnode更新更关注与传递给组件的props的变化,因此在updateComponent方法中,主要是对新旧vnode的props变化进行 588 | 遍历比较,如果确定需要更新,则会将新的vnode存储在组件实例上instance.next,并调用组件实例instance上的update方法(也就是runner)来更新组件, 589 | 这里更新的组件是指更新组件的内容,例如subTree,而非组件vnode;在组件实例instance上的update方法中, 590 | 会判断是否存在next,存在就会在对subTree进行patch前,更新组件实例上的vnode、props等,使得组件的subTree在patch时是最新的props。 591 | ### 更新组件的 props 592 | 参见 `组件类型的更新` 593 | ### nextTick原理 594 | 场景:当用户短时间内多次触发响应式更新逻辑,比如循环改变一个响应式对象,视图更新。 595 | 在这个场景中,视图更新逻辑会被多次触发,而这可以优化为等待循环结束,再触发更新逻辑。 596 | 此时就需要将视图更新逻辑作为一个微任务来调用,等待循环结束,再调用视图更新逻辑,一次性更新视图。 597 | 这个优化的逻辑基本实现思路是基于`effect`的`scheduler`实现和`Promise`的,通过创建一个微任务队列 `queue`, 598 | 在执行视图更新逻辑时候,调用 `effect` 并传入 `scheduler`,在 `scheduler中` 将 `effect` 返回的 `runner` 作为任务存入微任务队列 599 | `queue`,然后再`Promise`中循环队列 `queue`,执行 `runner` 更新视图。 600 | 这种优化使得视图更新逻辑变为了异步,但是在某些场景下用户需要拿到更新后的一些组件实例或者做一些其他操作,此时就诞生了 `nextTick` 601 | ,它的基本原理其实就是把传入的回调函数放在微任务或者宏任务中执行,但是它内部做了`Api`的使用判断,判断当前浏览器是否支持`Promise`然后降级调用 `Api`,比如不支持就调用 `setTimeout`,`messageChannel` 这种。 602 | 队列的逻辑是,有一个进队方法,每次调用会把传递进来的任务(如果不存在于队列中)存入队列,然后调用flush方法, 603 | flush方法内部会通过nextTick以微任务的形式执行队列执行方法,此时有一个开关变量 isFlushPending 604 | 当它为true则直接返回,只有nextTick执行时才设置为false,这样避免了短时间内重复调用nextTick的问题 605 | 队列执行方法内部就是循环队列,挨个执行任务 606 | 607 | ## compiler-core 608 | ### 主要流程 609 | template -》parse(str){ `词法分析 -》语法分析` } =》 模板AST -》 Transformer -》 JavaScript Ast -》代码生成 (generate JSAST)-》渲染函数 610 | vue的编译模块如上所示,主要是由sfc输入文件内容字符串,然后通过parse对字符串做词法分析和语法分析,将文件内容字符串抽象成模板AST, 611 | 在通过transform模块对模板AST做转化,比如一些优化block、hoist都在这一阶段进行,生成js AST, 612 | 这个js AST则用于描述js代码,比如模板中有一个div,那么js AST中就要描述需要调用creatVNode 来创建div的虚拟节点, 613 | 然后通过generate模块将 js AST解析生成对应的javaScript字符串,用于描述sfc文件。并在运行时被调用 614 | ### 基于有限状态机的`parse`基本理解 615 | 以模板解析为例 `parse` 时逐个字符对模板的内容字符串进行解析,对字符串进行切割,获取到dom的标签开始、标签名称、标签上属性、文本标签开始等信息, 616 | 其实他是基于有限状态机的原理去实现解析的,它对字符串进行遍历,通过各种状态迁移(标签开始、标签名称、标签文本、标签结束状态), 617 | 字符内容判断等操作,将dom字符串解析成一个个token,并根据token构造出AST 618 | ```` 619 |

foo

620 | 初始状态 621 | < ----------- 标签开始状态 622 | p ----------- 标签名称状态 623 | > ----------- 初始状态 624 | {type:'tagStart',name:'p'} 625 | foo ----------- 文本状态 626 | type:'text',content:'foo'} 627 | < ----------- 标签开始状态 628 | / ----------- 结束标签状态 629 | p ----------- 结束标签名称状态 630 | > ----------- 初始状态 631 | {type:'tagEnd',name:'p'} 632 | 633 | => token:[ 634 | {type:'tagStart',name:'p'}, 635 | {type:'text',content:'foo'}, 636 | {type:'tagEnd',name:'p'}, 637 | ] 638 | ```` 639 | 在html中,标签是一个树,通过扫描token列表,配合一个elementStack即可创建一个AST 640 | 扫描element时,当遇到一个开始标签,就在elementStack压入一个对应element对应类型的AST节点, 641 | 并在AST中创建一个对应element对应类型的AST节点,往后扫描中,所有的token都作为elementStack栈顶元素的子节点进行创建 642 | 直到遇到结束标签token,把elementStack出栈,这样就实现了根据token,创建对应父子关系的AST。 643 | ### Transformer 基本流程原理 644 | `Transformer` 被设计为只控制主流程,具体的转换实现,由传入的转换方法实现,这是一种可拔插的插件设计模式,能够使得 `Transformer` 足够解耦灵活,实现不同环境下的转换 645 | ### generate 基本流程原理 646 | `generate` 的本质,其实就是根据`transform`处理过的`ast`进行解析,生成对应的`JavaScript`代码字符串 647 | 648 | ### compiler 编译模块如何在 runtime 运行时模块中使用 649 | `runtime` 运行时模块(runtime-dom) 导出了一个注册编译函数`registerRuntimeCompiler`, 650 | 在`vue`的入口文件(vue文件夹) `index` 中调用 `registerRuntimeCompiler` 并传入一个方法 `compileToFunction` 651 | 通过注册编译函数 `registryRuntimeCompiler`,传入的`compileToFunction` 将被存储在运行时全局变量 652 | `compile`上,`compile`则会在 `finishComponentSetup` 中被调用,最后获得 `render` 方法挂载在组件实例 653 | `instance.render`上。 654 | 在`compileToFunction` 它在调用时,内部将传递进来的 `template` 传递给编译模块 的 `compile` 方法最终会得到 `code`, 655 | 在使用 `new Function('vue',code)(runtimeDom) `得到 `render` 函数。最终返回给运行时调用 656 | 其中 `baseCompile` 函数由 编译模块 `index` 导出,其内部分别调用 `baseParse`、`transform`、`generate`. 657 | ## vue3中 block的处理、patchFlag的优化 658 | 在实际场景中,模板的结构大多数情况下是稳定的,所以在编译阶段能够分析出模板的很多信息用于优化; 659 | vue3的block优化,实际上就是在编译阶段,分析出那些包含有动态信息的动态节点(例如字节点或孙子节点中包含有只指令、响应式变量等的节点), 660 | 调用对应的方法(openBlock、createElementBlock),并把节点信息、patchFlags等信息传递进去,创建block节点的过程。 661 | 通过block的创建,实际上思想就是将节点的动态信息传递给render渲染器,使得渲染器在运行时能够根据这些信息做优化运行。 662 | 一个block节点,它通常是模板中的根节点、带有节点指令等情况的节点。在一个block节点中,会有一个dynamicChildren属性,里面记录了该block节点 663 | 的后代节点中,属于动态节点的对应vnode、block节点,在渲染器运行时,更新逻辑就只会从dynamicChildren中进行更新, 664 | 这样就减少了diff对比的节点数量,从而达到优化。 665 | ### 如何收集后代block与动态节点 666 | 由于在组件的渲染函数render中createVNode是嵌套调用的, 子节点createVNode会作为上层节点createVNode的参数, 667 | 因此createVNode的调用是深度优先的,为了能够让根节点收集到后代的动态节点, 668 | 需要用一个栈来存储后代的动态节点(dynamicChildrenStack(currentDynamicChildren = []))也就是openBlock方法的主要作用; 669 | 而在渲染器中,判断是否有dynamicChildren,有就在更新时遍历新旧dynamicChildren进行更新,而单个节点, 670 | 由于dynamicChildren的vnode存在patchFlag(标记出element更新是class、style这些),所以可以准确的靶向更新。 671 | 值得注意的是,若子代存在block,根节点收集的是子代block,但是子block后有动态节点,根节点不会收集 672 | 因为他们已经存在与子代block中了。 673 | ### 结构化指令带来的block不稳定 674 | block不稳定通常是指dynamicChildren更新前后的数量或顺序不一致,在patchBlockChildren中是遍历新的dynamicChildren, 675 | 取索引拿新旧dynamicChildren对应元素来patchElement的过程 676 | 在v-if中,收集的block是v-if下的动态节点,这就导致在v-if变化时,其自生后代节点没变,不会响应更新, 677 | 因此需要将v-if/v-else等结构化指令也作为block,并收集到根节点block中 678 | 在v-for中,会使用fragment 充当v-for的block,因为v-for的循环数据可能在更新后长度上一致,导致了dynamicChildren的结构不稳定, 679 | 而dynamicChildren的节点不一定是同级的,因此无法使用diff, 因此需要fragment来包裹v-for,这样v-for的节点就能限定为同级 680 | 但是在fragment中,v-for依旧可能是不稳定的(fragment稳定的只有模板多个根节点和常量v-for) 681 | 因此渲染器对不稳定的fragment还是使用的常规diff。 682 | ## vue3的静态提升优化 、预字符串化 683 | 在render函数内调用createElementBlock,createVNode时,只要更新逻辑触发,render函数运行, 684 | createElementBlock,createVNode会被再次调用,而如果一个节点,它所使用的变量是静态常量,非响应式的,它在更新时也会被重复创建 685 | (createElementBlock,createVNode),而实际上不需要更新,所以可以把创建静态常量的vnode逻辑提升到render函数外面,把创建的vnode放在一个变量hoist上, 686 | render函数内部使用hoist,这样在更新时就可以避免重复调用方法创建vnode了。 687 | 预字符串化,是静态提升的一种优化策略,当提升变量到底20个,这些变量对应的节点会直接被转化为字符串, 688 | 通过innerHTML来创建,这里由于不涉及更新,用innerHTML来创建大量dom在性能上有优势,同时又减少了 689 | 内存消耗、创建vnode开销。 690 | ## 内置组件实现原理 691 | ### Keep-alive 692 | vue3的keepalive组件使用了setup进行了重写, 693 | keepalive组件的组件对象中 694 | setup中通过getCurrentInstance获取到了keepalive组件实例, 695 | 从上面获取到了由渲染器注入的move方法、createElement方法,节点卸载方法等, 696 | 其中会使用createElement方法创建存储元素storageContainer,在缓存隐藏逻辑时,缓存的vnode节点将被挂在元素下 697 | 698 | 在组件实例上,添加一个active方法,这个方法将在patch时将被调用,用于处理keepalive的激活逻辑, 699 | 在这个 activate 方法中,主要是将传递进来的缓存节点vnode,根据容器参数container调用move方法将缓存节点移动到对应容器内渲染, 700 | 并且重新将node和组件实例上旧的vnode进行patch,以此来更新props,最后在把组件的isDeactivated状态设置为false,即激活状态。 701 | 702 | 在组件实例上,添加一个deactivate方法,这个方法将在patch时的unmount方法中被调用,用于处理keepalive的非激活逻辑, 703 | 而在deactivate方法中主要是将传递进来的缓存节点vnode,根据storageContainer调用move方法将缓存节点移动到storageContainer元素内隐藏, 704 | 把组件的isDeactivated状态设置为true,即非激活状态。 705 | 706 | 可以看到keepalive组件内卸载时并不是真的卸载而是被隐藏到storageContainer中,当激活时也是将组件从storageContainer移动到对应容器下。 707 | 708 | keepalive使用的缓存策略时缓存最一次此缓存的,它内部维护了一个keys,他是一个set,按照顺序存储缓存的vnode的key, 709 | 还维护了一个cache,它是一个map,key是vnode的type,值是vnode , 710 | 711 | 在keepalive组件中会先获取插槽,拿到插槽对应组件的vnode,然后获取key、组件的名称等,其主要时做先做了一个判断,根据组件的name, 712 | 去看是不是不存在include里或存在exclude里,满足条件就直接返回组件的vnode, 713 | 不满足就根据vnode的key去cache拿缓存cahceVnode, 714 | 拿到了,就把cacheVnode的el和component赋值vnode上,并把vnode存在变量current上,并且先删除keys上对应key,再添加key(`实现更新最近访问效果`); 715 | 如果拿不到,就给keys添加key,然后判断是否大于max,大于就处理溢出逻辑 716 | 溢出逻辑,获取keys的第一个元素,根据这个元素key从cache中获取vnode,把这个vnode和current对比,类型不一样就要卸载, 717 | 然后还要key根据删除cache和keys 718 | ### teleport 719 | 内置组件teleport会导出一个teleport对象,他会在编译结束后作为第一个参数节点类型传递给CreateBlock创建shapeFlag为teleport类型的虚拟节点 720 | teleport对象实际上就是TeleportImpl对象 721 | 在编译后创建的teleport的vnode,在patch过程中会被识别出来,并用vnode上的type(就是teleport-》TeleportImpl)来调用process方法来完成teleport组件的逻辑实现。 722 | process内部对普通元素的主要逻辑实现 723 | 初始化时,n1不存在,则获取n2的props.to,根据to,获取到对应的dom(如果禁用dom就是container), 724 | 使用mountChildren并遍历n2的children,挨个将其进行patch,最终将dom插入到目标容器下。 725 | 726 | 在更新时,如果n1与n2的props.to不同,则获取n2的props.to,根据to,获取到对应的dom, 727 | 调用moveTeleport方法直接将整个child插入到对应dom下 728 | 729 | 如果n1与n2的props.disabled 不同,则获取n2的props.disabled,根据disabled 730 | 获取到对应的dom(true-》false,dom是to的目标元素,false-》true,dom就是container), 731 | 调用moveTeleport方法,并遍历n2的children然后调用渲染器导出的move方法(方法内最终用的hostInsert) 732 | ,将最终将dom移动到目标容器下。 733 | 734 | 如果如果n1于n2的props.to一样,直接patchChildren 或 patchBlockChildren 735 | 并且TeleportImpl还提供了对外的Api给框架操作,比如remove方法,在unmount时能够卸载对应的teleport 736 | 737 | ### 提交的 pr 738 | ### bug 5675 739 | 问题描述:在动画组件`transition`里,如果添加注释,会爆出警告,不支持多个子节点 740 | 问题定位于分析:在`BaseTransition.ts`中150行左右会获取组件的默认插槽`slot.default`赋值到变量`children`上,也就是子节点; 741 | 代码中只对 `children` 的长度进行了判断,大于 `1` 就直接报出警告; 742 | 我的解决方法:对`children`进行`filter`过滤,过滤出非注释类型的节点,当过滤结果长度大于1,则说明存在多个非注释子节点,报出警告; 743 | 我的问题:仅仅只是解决了警告的报出问题,而并没有修复 `transition` 对于`child`的获取,在后续逻辑中, 744 | 会获取`child = children[0]`来进行处理; 745 | 正确的解决方式:当`children`大于1时,遍历 `children`,找到非注释节点的第一个子节点,赋值给`child`变量,并标记找到`isFound`, 746 | 往后循环如果还有非注释节点,则根据 `isFound` 直接报出警告,并 `break`; 747 | ### bug 5675 748 | 问题描述: 749 | ````html 750 |
753 | test 754 |
755 | ```` 756 | 无法被正确编译转换,直接报错 757 | 问题定位于分析:在编译模块时,transform步骤会将模板ast转化为JavaScript代码, 758 | 其中会根据ast节点类型、props等,调用各种节点创建函数、转化函数等,上述给style绑定的数组是一个常量数组(非响应式), 759 | 它编译时会被静态提升, 760 | 我的解决方法:在transformExpression.ts加入检测来避免识别为静态提升,从而能够被正确编译 761 | 我的问题:虽然解决了问题,但是这与静态提升的设计违背,会导致这些静态变量无法被提升。 762 | 正确的解决方式:在transformElement.ts中,即element 转换节点检测这个问题,最终生成的静态提升变量会被 `normalizeStyle`处理成合法的格式,; 763 | ### 杂项 764 | 虚拟 `dom` 的性能到底如何?为什么不使用 `innerHTML`? 765 | 要理解这一点首先要清楚我们对dom的操作代码,如果是命令式的框架,例如jq或原生js,假设我需要给div设置文本并添加点击事件 766 | 一般要三部,找到dom,填充文字,添加事件,这个过程通常需要三行代码实现,而在vue这在声明式框架中,则只需要一行模板代码即可, 767 | 声明式框架能够有自己的语法,它可以有更高的自由度,它使得开发者更加的关注结果,而非过程,因为过程框架已经帮我们处理了。 768 | 但是这其实在框架内会比原生做更多的事情,从而打带来了更多的性能消耗,虚拟dom就是为了在便利开发和性能权衡间找到最优平衡而出现的。 769 | 首先在项目中,dom页面更新的性能消耗可以概括的理解为更新的性能消耗 = 找出更新差异的消耗 + 直接修改更新差异的消耗。这个消耗在简单的业务逻辑中 770 | 我们通过原生js去实现,比如出之前div设置文本并添加点击事件,这个消耗原生绝对是小于使用虚拟dom的;但是在现实中,我们的业务要更加多,页面更加复杂, 771 | 我们很难去写出或者要耗费很大的精力去写出这种代码,而这种复杂的代码也难以维护这投入和产出比显然是不对等的,所以回去使用虚拟dom, 772 | 同时配合框架的diff算法去得到更新最小解,实现我们最终的效果,这其实是一种性能与开发投入、可维护性的一种权衡。 773 | 所以对于虚拟dom性能优于原生或原生优于虚拟dom其实是不准确的。例如我要创建1000个div, 774 | 通过innerHTML去实现,他需要在dom层面去解析字符串,那么性能消耗是dom层面计算1000个div的消耗 + 创建消耗 775 | 而js层面的计算消耗是小于dom层面的,那么虚拟dom实现的则是js层面计算1000个div的消耗 + 创建消耗,在这个例子中,如果有div发生更新, 776 | 虚拟dom能精准的实现目标元素的更新,而innerHTML需要重新计算这1000个div,这样虚拟dom的性能就高于innerHTML 777 | 那如果用纯js原生去实现,消耗则更小,但是实现复杂心智负担大,而虚拟dom 对于原生js、innerHTML,是心智负担最小,性能又不错的一种选择。 778 | ### Vue2 与 Vue3 是如何对数组实现数据劫持的 ? 779 | vue3中对数组的数据劫持还是通过proxy来实现,只不过数组存在大量的访问方法,设置方法,遍历方法,都需要对这些方法做单独出来 780 | ```` 781 | let a = reactive([1]) 782 | effect(()=>{ 783 | console.log(a.length) 784 | }) 785 | ```` 786 | 上述代码中,如果我们设置a[0] = 2设置,和a[1] = 0 设置是不一样的,因此在setter中还需要根据设置的索引值和数组长度去区分操作类型 787 | 时 ‘ADD’ 还是 ‘SET’,针对类型的不同在trigger中还需要做处理,比如‘ADD’类型,就需要在trigger中取出该数组length相关的所有副作用, 788 | 遍历挨个触发。 789 | 在比如对数组的遍历方法 `for...of`,`values`方法,他们的迭代器访问了length 与 索引,因此在数据访问劫持做依赖收集时,是收集的length属性与索引 790 | 当数据的length,索引对应元素变化,需要触发这些遍历方法再次执行。 791 | 而在vue2中 792 | 有一个改写数组原生方法的列表 `methodsToPatch` 793 | 然后以`Array.prototype`为原型创建一个对象 `arrayMethods` 794 | 改写时,遍历 `methodsToPatch`,在每趟遍历中,缓存原始方法 `original = arrayProto[method]` 795 | 并调用 `definedProperty` 来对`arrayMethods`上数组相关方法的调用做劫持,具体的劫持方法中,先使用缓存的原始方法 `original`那个结果 796 | 对那些可以新增数组元素的方法比如 `push`,`shift`,会拿到新增的数据,并把这些数据传递给 `observe`,做响应式处理, 797 | 最后通过`notify`派发更新并返回结果。至此就完成了数组方法的改写对象 `arrayMethods` 798 | 799 | 在应用时,判断浏览器是否支持隐式原型 `__proto__`,如果支持,则直接将当前数据数组的_proto_指向 `arrayMethods`, 800 | 如果浏览器不支持`__proto__`,则直接遍历`arrayMethods`,将上面重写的方法直接定义到当前数据对象上 801 | 802 | ### vue3如何处理多个v-mode? 803 | 在vue2中`v-model`实现需要开发者在子组件中通过`model`选项定义绑定值和触发事件名称 804 | ```` 805 | model: { 806 | prop: "number", // 默认为 value 807 | event: "change", // 默认为 input 808 | }, 809 | ```` 810 | 相应的在父组件就等效于 811 | ```` 812 | 813 | 814 | 815 | ```` 816 | 在组件数据修改时需要触发 `this.$emit("change", val);` 817 | 818 | 在`vue3`中,我们实现`v-model`需要在子组件中定义emit事件`update:modelValue`,通过`props.modelValue`访问绑定值 819 | 并且在数据更改时触发这个事件,实现双向绑定 820 | 在模板编译阶段 `v-model`会被编译为props传递来创建 `vnode` 821 | ```` 822 | modelValue: msg.value, 823 | "onUpdate:modelValue":$event => ((msg).value = $event)), 824 | ```` 825 | 而如果我们实现多个v-model,则语法规则是`v-model:msg1 = "msg1" v-model:msg2 = "msg2"` 826 | 会被编译成 827 | ```` 828 | msg1: msg1.value, 829 | "onUpdate:msg1":$event => ((msg1).value = $event)), 830 | msg2: msg2.value, 831 | "onUpdate:msg2":$event => ((msg2).value = $event)), 832 | ```` 833 | 834 | ### vue3中一些常用生命周期在那个阶段调用 TODO 835 | -------------------------------------------------------------------------------- /why should cleanupEffect in ReactiveEffect: -------------------------------------------------------------------------------- 1 | 为什么需要 cleanup 呢?如果遇到这种场景: 2 | 12 | 38 | 39 | 结合代码可以知道,这个组件的视图会根据 showMsg 变量的控制显示 msg 或者一个随机数,当我们点击 Switch View 的按钮时,就会修改这个变量值。 40 | 假设没有 cleanup,在第一次渲染模板的时候,activeEffect 是组件的副作用渲染函数,因为模板 render 的时候访问了 state.msg, 41 | 所以会执行依赖收集,把副作用渲染函数作为 state.msg 的依赖,我们把它称作 render effect。然后我们点击 Switch View 按钮,视图切换为显示随机数,此时我们再点击 Toggle Msg 按钮,由于修改了 state.msg 就会派发通知,找到了 render effect 并执行,就又触发了组件的重新渲染。 42 | 但这个行为实际上并不符合预期,因为当我们点击 Switch View 按钮,视图切换为显示随机数的时候,也会触发组件的重新渲染,但这个时候视图并没有渲染 state.msg,所以对它的改动并不应该影响组件的重新渲染。 43 | 因此在组件的 render effect 执行之前,如果通过 cleanup 清理依赖,我们就可以删除之前 state.msg 收集的 render effect 依赖。这样当我们修改 state.msg 时,由于已经没有依赖了就不会触发组件的重新渲染,符合预期。 44 | 45 | 换句话说,render方法中即使访问了多个响应式变量,但实际对应的是一个reactiveEffect对象,这个对象会被收集到各个响应式变量的依赖map中, 46 | 但是如果某个响应式变量导致render方法中访问的响应式变量(数目)发生了改变(例如上述的v-if),此时如果不清空,会导致修改响应式变量( 47 | 这个响应式变量由于v-if,不在render中了,但是之前被收集过)时,触发依赖 48 | -------------------------------------------------------------------------------- /yarn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baiwusanyu-c/Be-Vue/28c9465ddaa8589d3d86aca9d4ce4856da6b9968/yarn --------------------------------------------------------------------------------