├── md
├── Vue3 中关于集合类型数据的响应式方案.md
├── 从 npm 源码分析 npm run 背后到底做了什么.md
├── 为什么React里的异步更新是宏任务,而Vue3里的是微任务.md
├── study-mini-vue.jpg
├── images
│ ├── prototype.png
│ ├── weakmap-set.png
│ ├── Proxy-Reflect-01.png
│ ├── Proxy-Reflect-02.png
│ ├── Proxy-Reflect-03.png
│ ├── Proxy-Reflect-04.png
│ ├── Proxy-Reflect-05.png
│ ├── Proxy-Reflect-06.png
│ ├── Vue3-proxy-arry-01.png
│ └── vue2-arry-defineProperty-01.png
├── Vue3 模板引用 ref 的实现原理.md
├── 关于 Vue3 源码当中的 Proxy 和 Reflect 的那些事儿.md
├── Vue3生命周期Hooks的原理及其与调度器(Scheduler)的关系.md
└── Vue3的effect、watch、watchEffect API的实现原理.md
├── src
├── compiler-core
│ ├── src
│ │ ├── index.ts
│ │ ├── runtimeHelpers.ts
│ │ ├── transforms
│ │ │ ├── transformExpression.ts
│ │ │ ├── transformElement.ts
│ │ │ └── transformText.ts
│ │ ├── ast.ts
│ │ ├── compile.ts
│ │ ├── transform.ts
│ │ ├── codegen.ts
│ │ └── parse.ts
│ └── tests
│ │ ├── __snapshots__
│ │ └── codegen.spec.ts.snap
│ │ ├── transform.spec.ts
│ │ ├── codegen.spec.ts
│ │ └── parse.spec.ts
├── reactivity
│ ├── index.ts
│ ├── tests
│ │ ├── index.spec.ts
│ │ ├── shallowReadonly.spec.ts
│ │ ├── readonly.spec.ts
│ │ ├── reactive.spec.ts
│ │ ├── computed.spec.ts
│ │ ├── ref.spec.ts
│ │ └── effect.spec.ts
│ ├── computed.ts
│ ├── reactive.ts
│ ├── baseHandler.ts
│ ├── ref.ts
│ └── effect.ts
├── shared
│ ├── toDisplayString.ts
│ ├── ShapeFlags.ts
│ ├── looseEqual.ts
│ └── index.ts
├── runtime-core
│ ├── componentProps.ts
│ ├── h.ts
│ ├── componentRenderContext.ts
│ ├── helpers
│ │ └── renderSlots.ts
│ ├── componentUpdateUtils.ts
│ ├── componentEmit.ts
│ ├── createApp.ts
│ ├── componentRenderUtils.ts
│ ├── index.ts
│ ├── componentSlots.ts
│ ├── componentPublicInstance.ts
│ ├── apiInject.ts
│ ├── directives.ts
│ ├── apiLifecycle.ts
│ ├── rendererTemplateRef.ts
│ ├── vnode.ts
│ ├── scheduler.ts
│ ├── component.ts
│ ├── apiWatch2.ts
│ ├── apiWatch.ts
│ └── renderer.ts
├── runtime-dom
│ ├── modules
│ │ └── events.ts
│ ├── index.ts
│ └── directives
│ │ └── vModel.ts
└── index.ts
├── babel.config.js
├── example
├── apiInject
│ ├── main.js
│ ├── index.html
│ └── App.js
├── TemplateRefs
│ ├── main.js
│ ├── Foo.js
│ ├── index.html
│ ├── App.js
│ ├── App1.js
│ └── App2.js
├── apiWatch
│ ├── main.js
│ ├── index.html
│ └── App.js
├── helloworld
│ ├── main.js
│ ├── index.html
│ ├── Foo.js
│ └── App.js
├── lifecycle
│ ├── main.js
│ ├── Foo.js
│ ├── index.html
│ └── App.js
├── compiler-base
│ ├── main.js
│ ├── App.js
│ └── index.html
├── componentEmit
│ ├── main.js
│ ├── index.html
│ ├── Foo.js
│ └── App.js
├── componentSlot
│ ├── main.js
│ ├── Foo.js
│ ├── index.html
│ └── App.js
├── componentUpdate
│ ├── main.js
│ ├── Child.js
│ ├── index.html
│ └── App.js
├── currentInstance
│ ├── main.js
│ ├── App.js
│ ├── Foo.js
│ └── index.html
├── nextTicker
│ ├── main.js
│ ├── index.html
│ └── App.js
├── patchChildren
│ ├── main.js
│ ├── index.html
│ ├── TextToText.js
│ ├── TextToArray.js
│ ├── ArrayToText.js
│ ├── App.js
│ └── ArrayToArray.js
├── update
│ ├── main.js
│ ├── index.html
│ └── App.js
└── customRenderer
│ ├── App.js
│ ├── index.html
│ └── main.js
├── .gitignore
├── rollup.config.js
├── package.json
├── README.md
└── tsconfig.json
/md/Vue3 中关于集合类型数据的响应式方案.md:
--------------------------------------------------------------------------------
1 | # Vue3 中关于集合类型数据的响应式方案
--------------------------------------------------------------------------------
/src/compiler-core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './compile'
--------------------------------------------------------------------------------
/md/从 npm 源码分析 npm run 背后到底做了什么.md:
--------------------------------------------------------------------------------
1 | # 从 npm 源码分析 npm run 背后到底做了什么
--------------------------------------------------------------------------------
/md/为什么React里的异步更新是宏任务,而Vue3里的是微任务.md:
--------------------------------------------------------------------------------
1 | # 为什么 React 中的异步更新是宏任务,而 Vue3 中的是微任务
--------------------------------------------------------------------------------
/md/study-mini-vue.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/study-mini-vue.jpg
--------------------------------------------------------------------------------
/src/reactivity/index.ts:
--------------------------------------------------------------------------------
1 | export { ref, proxyRefs } from './ref'
2 | export { reactive } from './reactive'
--------------------------------------------------------------------------------
/md/images/prototype.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/images/prototype.png
--------------------------------------------------------------------------------
/md/images/weakmap-set.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/images/weakmap-set.png
--------------------------------------------------------------------------------
/src/shared/toDisplayString.ts:
--------------------------------------------------------------------------------
1 | export function toDisplayString(value) {
2 | return String(value)
3 | }
--------------------------------------------------------------------------------
/md/images/Proxy-Reflect-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/images/Proxy-Reflect-01.png
--------------------------------------------------------------------------------
/md/images/Proxy-Reflect-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/images/Proxy-Reflect-02.png
--------------------------------------------------------------------------------
/md/images/Proxy-Reflect-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/images/Proxy-Reflect-03.png
--------------------------------------------------------------------------------
/md/images/Proxy-Reflect-04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/images/Proxy-Reflect-04.png
--------------------------------------------------------------------------------
/md/images/Proxy-Reflect-05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/images/Proxy-Reflect-05.png
--------------------------------------------------------------------------------
/md/images/Proxy-Reflect-06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/images/Proxy-Reflect-06.png
--------------------------------------------------------------------------------
/md/images/Vue3-proxy-arry-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/images/Vue3-proxy-arry-01.png
--------------------------------------------------------------------------------
/src/runtime-core/componentProps.ts:
--------------------------------------------------------------------------------
1 |
2 | export function initProps(instance, rawProps) {
3 | instance.props = rawProps || {}
4 | }
--------------------------------------------------------------------------------
/md/images/vue2-arry-defineProperty-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amebyte/mini-vue3-plus/HEAD/md/images/vue2-arry-defineProperty-01.png
--------------------------------------------------------------------------------
/src/reactivity/tests/index.spec.ts:
--------------------------------------------------------------------------------
1 | // import { add } from '../index'
2 | // it('init', () => {
3 | // expect(add(1, 1)).toBe(2)
4 | // })
--------------------------------------------------------------------------------
/src/runtime-core/h.ts:
--------------------------------------------------------------------------------
1 | import { createVNode } from './vnode'
2 | export function h(type, props?, children?) {
3 | return createVNode(type, props, children)
4 | }
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', { targets: { node: 'current' } }],
4 | '@babel/preset-typescript',
5 | ],
6 | }
7 |
--------------------------------------------------------------------------------
/src/runtime-dom/modules/events.ts:
--------------------------------------------------------------------------------
1 | export function addEventListener(
2 | el,
3 | event,
4 | handler,
5 | options?
6 | ) {
7 | el.addEventListener(event, handler, options)
8 | }
--------------------------------------------------------------------------------
/example/apiInject/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import App from './App.js'
3 | const rootContainer = document.querySelector("#app")
4 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/example/TemplateRefs/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from './App.js'
3 | const rootContainer = document.querySelector("#app")
4 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/example/apiWatch/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from './App.js'
3 | const rootContainer = document.querySelector("#app")
4 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/example/helloworld/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from './App.js'
3 | const rootContainer = document.querySelector("#app")
4 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/example/lifecycle/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from './App.js'
3 | const rootContainer = document.querySelector("#app")
4 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/example/compiler-base/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from './App.js'
3 | const rootContainer = document.querySelector("#app")
4 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/example/componentEmit/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from './App.js'
3 | const rootContainer = document.querySelector("#app")
4 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/example/componentSlot/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from './App.js'
3 | const rootContainer = document.querySelector("#app")
4 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/example/componentUpdate/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from './App.js'
3 | const rootContainer = document.querySelector("#app")
4 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/example/currentInstance/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from './App.js'
3 | const rootContainer = document.querySelector("#app")
4 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/example/nextTicker/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import App from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#app");
5 | createApp(App).mount(rootContainer);
--------------------------------------------------------------------------------
/example/patchChildren/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import App from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#app");
5 | createApp(App).mount(rootContainer);
--------------------------------------------------------------------------------
/example/update/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/mini-vue.esm.js"
2 | import { App } from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#app");
5 | createApp(App).mount(rootContainer);
--------------------------------------------------------------------------------
/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 | SLOT_CHILDREN = 1 << 4
7 | }
--------------------------------------------------------------------------------
/src/runtime-core/componentRenderContext.ts:
--------------------------------------------------------------------------------
1 | export let currentRenderingInstance = null
2 |
3 | export function setCurrentRenderingInstance(instance) {
4 | const prev = currentRenderingInstance
5 | currentRenderingInstance = instance
6 | return prev
7 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/example/customRenderer/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/mini-vue.esm.js"
2 |
3 | export const App = {
4 | setup() {
5 | return {
6 | x: 100,
7 | y: 100
8 | }
9 | },
10 | render() {
11 | return h("rect", { x: this.x, y: this.y })
12 | }
13 | }
--------------------------------------------------------------------------------
/example/compiler-base/App.js:
--------------------------------------------------------------------------------
1 | import { ref } from '../../lib/mini-vue.esm.js'
2 | window.self = null
3 | export const App = {
4 | name: 'App',
5 | template: `
卡颂读书会,{{count}}
`,
6 | setup() {
7 | const count = window.count = ref(1)
8 | return {
9 | count,
10 | }
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/src/runtime-core/helpers/renderSlots.ts:
--------------------------------------------------------------------------------
1 | import { createVNode, Fragment } from '../vnode'
2 |
3 | export function renderSlots(slots, name, props) {
4 | const slot = slots[name]
5 | if (slot) {
6 | if (typeof slot === 'function') {
7 | return createVNode(Fragment, {}, slot(props))
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/src/runtime-core/componentUpdateUtils.ts:
--------------------------------------------------------------------------------
1 | export function shouldUpdateComponent(prevVNode, nextVNode) {
2 | const { props: prevProps } = prevVNode
3 | const { props: nextProps } = nextVNode
4 |
5 | for(const key in nextProps) {
6 | if(nextProps[key] !== prevProps[key]) {
7 | return true
8 | }
9 | }
10 |
11 | return false
12 | }
--------------------------------------------------------------------------------
/src/runtime-core/componentEmit.ts:
--------------------------------------------------------------------------------
1 | import { camelize, toHandlerKey } from "../shared"
2 |
3 | export function emit(instance, event, ...args) {
4 | const { props } = instance
5 | // TPP
6 | // 先去写一个特定的行为 =》 重构成通用的行为
7 |
8 | const handlerName = toHandlerKey(camelize(event))
9 |
10 | const handler = props[handlerName]
11 | handler && handler(...args)
12 | }
--------------------------------------------------------------------------------
/src/compiler-core/src/transforms/transformExpression.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "../ast";
2 |
3 | export function transformExpression(node) {
4 | if(node.type === NodeTypes.INTERPOLATION) {
5 | node.content = processExpression(node.content)
6 | }
7 | }
8 |
9 | function processExpression(node) {
10 | node.content = `_ctx.${node.content}`
11 | return node
12 | }
--------------------------------------------------------------------------------
/example/TemplateRefs/Foo.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/mini-vue.esm.js"
2 | export const Foo = {
3 | setup() {
4 | const emitAdd = () => {
5 | console.log('template ref')
6 | }
7 | return {
8 | emitAdd
9 | }
10 | },
11 | render() {
12 | const foo = h("p", {}, "foot")
13 | return h("div", {}, [foo])
14 | }
15 | }
--------------------------------------------------------------------------------
/src/runtime-core/createApp.ts:
--------------------------------------------------------------------------------
1 | import { createVNode } from "./vnode"
2 |
3 | export function createAppAPI(render) {
4 | return function createApp(rootComponent) {
5 | return {
6 | mount(rootContainer) {
7 | const vnode = createVNode(rootComponent)
8 | render(vnode, rootContainer)
9 | }
10 | }
11 | }
12 | }
13 |
14 |
15 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import pkg from './package.json'
2 | import typescript from "@rollup/plugin-typescript"
3 | export default {
4 | input: "./src/index.ts",
5 | output: [
6 | {
7 | format: "cjs",
8 | file: pkg.main
9 | },
10 | {
11 | format: "es",
12 | file: pkg.module
13 | }
14 | ],
15 | plugins:[typescript()]
16 | }
--------------------------------------------------------------------------------
/example/componentSlot/Foo.js:
--------------------------------------------------------------------------------
1 | import { h, renderSlots } from "../../lib/mini-vue.esm.js"
2 | export const Foo = {
3 | setup() {
4 | return {}
5 | },
6 | render() {
7 | const foo = h("p", {}, "foot")
8 | const age = 18
9 | console.log('this.$slots', this.$slots)
10 | return h("div", {}, [renderSlots(this.$slots, 'header', { age }), foo, renderSlots(this.$slots, 'footer')])
11 | }
12 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./runtime-dom"
2 | import { baseCompile } from './compiler-core/src'
3 | import * as runtimeDom from './runtime-dom'
4 | import { registerRuntimeCompiler } from './runtime-dom'
5 |
6 | function compileToFunction(template) {
7 | const { code } = baseCompile(template)
8 | const render = new Function('Vue', code)(runtimeDom)
9 | return render
10 | }
11 |
12 | registerRuntimeCompiler(compileToFunction)
--------------------------------------------------------------------------------
/example/currentInstance/App.js:
--------------------------------------------------------------------------------
1 | import { h, getCurrentInstance } from '../../lib/mini-vue.esm.js'
2 | import { Foo } from './Foo.js'
3 | window.self = null
4 | export const App = {
5 | name: 'App',
6 | render() {
7 |
8 | return h('div', {}, [h('div', {}, 'currentInstance demo'), h(Foo)])
9 | },
10 | setup() {
11 | const instance = getCurrentInstance()
12 | console.log('App Instance', instance)
13 | return {
14 | msg: 'mini-vue',
15 | }
16 | },
17 | }
18 |
--------------------------------------------------------------------------------
/src/runtime-core/componentRenderUtils.ts:
--------------------------------------------------------------------------------
1 | import { setCurrentRenderingInstance } from "./componentRenderContext";
2 |
3 | export function renderComponentRoot(
4 | instance
5 | ) {
6 | const { proxy, render } = instance
7 | let result
8 | // 返回上一个实例对象
9 | const prev = setCurrentRenderingInstance(instance)
10 | result = render.call(proxy, proxy)
11 | // 再设置当前的渲染对象上一个,具体场景是嵌套循环渲染的时候,渲染完子组件,再去渲染父组件
12 | setCurrentRenderingInstance(prev)
13 | return result
14 | }
--------------------------------------------------------------------------------
/example/currentInstance/Foo.js:
--------------------------------------------------------------------------------
1 | import { h, renderSlots, getCurrentInstance } from "../../lib/mini-vue.esm.js"
2 | export const Foo = {
3 | name: 'Foo',
4 | setup() {
5 | const instance = getCurrentInstance()
6 | console.log('Foo instance', instance)
7 | return {}
8 | },
9 | render() {
10 | const foo = h("p", {}, "foot")
11 | const age = 18
12 | console.log('this.$slots', this.$slots)
13 | return h("div", {}, 'footer instance')
14 | }
15 | }
--------------------------------------------------------------------------------
/example/componentUpdate/Child.js:
--------------------------------------------------------------------------------
1 |
2 | import { h, onUpdated, onBeforeUpdate } from "../../lib/mini-vue.esm.js";
3 | export default {
4 | name: "Child",
5 | setup(props, { emit }) {
6 | onBeforeUpdate(() => {
7 | console.log('onBeforeUpdate by child')
8 | })
9 | onUpdated(() => {
10 | console.log('onUpdated by child')
11 | })
12 | },
13 | render(proxy) {
14 | return h("div", {}, [h("div", {}, "child - props - msg: " + this.$props.msg + this.$props.count)]);
15 | },
16 | };
--------------------------------------------------------------------------------
/src/runtime-core/index.ts:
--------------------------------------------------------------------------------
1 | export { h } from './h'
2 | export { renderSlots } from "./helpers/renderSlots"
3 | export { createTextVNode, createElementVNode } from './vnode'
4 | export { getCurrentInstance, registerRuntimeCompiler } from './component'
5 | export { provide, inject } from './apiInject'
6 | export { createRenderer } from './renderer'
7 | export { nextTick } from './scheduler'
8 | export { toDisplayString } from '../shared'
9 | export * from "../reactivity"
10 | export * from './apiLifecycle'
11 | export * from './apiWatch2'
--------------------------------------------------------------------------------
/example/lifecycle/Foo.js:
--------------------------------------------------------------------------------
1 | import { h, onMounted, onUpdated } from "../../lib/mini-vue.esm.js"
2 | export const Foo = {
3 | setup() {
4 | console.log('child')
5 | const emitAdd = () => {
6 | console.log('template ref')
7 | }
8 | onMounted(() => {
9 | console.log('onMounted by child')
10 | })
11 | return {
12 | emitAdd
13 | }
14 | },
15 | render() {
16 | const foo = h("p", {}, "foot")
17 | return h("div", {}, [foo])
18 | }
19 | }
--------------------------------------------------------------------------------
/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 | ATTRIBUTE // 属性
11 | }
12 |
13 | export function createVNodeCall(context, tag, props, children) {
14 | context.helper(CREATE_ELEMENT_VNODE)
15 | return {
16 | type: NodeTypes.ELEMENT,
17 | tag,
18 | props,
19 | children
20 | }
21 | }
--------------------------------------------------------------------------------
/example/update/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/example/apiWatch/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/nextTicker/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/example/TemplateRefs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/apiInject/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/compiler-base/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/componentEmit/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/componentSlot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/helloworld/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/lifecycle/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/patchChildren/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/example/componentUpdate/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/currentInstance/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/componentSlot/App.js:
--------------------------------------------------------------------------------
1 | import { h, createTextVNode } from '../../lib/mini-vue.esm.js'
2 | import { Foo } from './Foo.js'
3 | window.self = null
4 | export const App = {
5 | render() {
6 | window.self = this
7 | const app = h('div', {}, 'App')
8 | const foo = h(
9 | Foo,
10 | {},
11 | { header: ({ age }) => [h('p', {}, 'header' + age), createTextVNode('cobyte')], footer: () => h('p', {}, 'footer') }
12 | )
13 | return h('div', {}, [app, foo])
14 | },
15 | setup() {
16 | return {
17 | msg: 'mini-vue',
18 | }
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/example/patchChildren/TextToText.js:
--------------------------------------------------------------------------------
1 | // 新的是 text
2 | // 老的是 text
3 | import { ref, h } from "../../lib/mini-vue.esm.js";
4 |
5 | const prevChildren = "oldChild";
6 | const nextChildren = "newChild";
7 |
8 | export default {
9 | name: "TextToText",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
--------------------------------------------------------------------------------
/src/compiler-core/src/compile.ts:
--------------------------------------------------------------------------------
1 | import { generate } from "./codegen"
2 | import { baseParse } from "./parse"
3 | import { transform } from "./transform"
4 | import { transformElement } from "./transforms/transformElement"
5 | import { transformExpression } from "./transforms/transformExpression"
6 | import { transformText } from "./transforms/transformText"
7 |
8 | export function baseCompile(template) {
9 | const ast = baseParse(template)
10 | transform(ast, {
11 | nodeTransforms: [transformExpression, transformElement, transformText]
12 | })
13 | return generate(ast)
14 | }
--------------------------------------------------------------------------------
/example/TemplateRefs/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref, nextTick } from '../../lib/mini-vue.esm.js'
2 | import { Foo } from './Foo.js'
3 |
4 | const refKey = ref()
5 | // string
6 | export const App = {
7 | render() {
8 |
9 | return h(
10 | 'div',
11 | {
12 | ref: refKey
13 | },
14 | [
15 | h(Foo, { })
16 | ]
17 | )
18 | },
19 | setup() {
20 |
21 |
22 | nextTick(() => {
23 | console.log('refKey', refKey)
24 | })
25 |
26 | return {
27 | // refKey
28 | }
29 | },
30 | }
31 |
--------------------------------------------------------------------------------
/example/patchChildren/TextToArray.js:
--------------------------------------------------------------------------------
1 | // 新的是 array
2 | // 老的是 text
3 | import { ref, h } from "../../lib/mini-vue.esm.js";
4 |
5 | const prevChildren = "oldChild";
6 | const nextChildren = [h("div", {}, "A"), h("div", {}, "B")];
7 |
8 | export default {
9 | name: "TextToArray",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
--------------------------------------------------------------------------------
/src/runtime-core/componentSlots.ts:
--------------------------------------------------------------------------------
1 | import { ShapeFlags } from '../shared/ShapeFlags'
2 |
3 | export function initSlots(instance, children) {
4 | const { vnode } = instance
5 | if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) {
6 | normalizeObjectSlots(children, instance.slots)
7 | }
8 | }
9 |
10 | function normalizeObjectSlots(children, slots) {
11 | for (const key in children) {
12 | const value = children[key]
13 | slots[key] = (props) => normalizeSlotValue(value(props))
14 | }
15 | }
16 |
17 | function normalizeSlotValue(value) {
18 | return Array.isArray(value) ? value : [value]
19 | }
20 |
--------------------------------------------------------------------------------
/example/patchChildren/ArrayToText.js:
--------------------------------------------------------------------------------
1 | // 老的是 array
2 | // 新的是 text
3 |
4 | import { ref, h } from "../../lib/mini-vue.esm.js";
5 | const nextChildren = "newChildren";
6 | const prevChildren = [h("div", {}, "A"), h("div", {}, "B")];
7 |
8 | export default {
9 | name: "ArrayToText",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
--------------------------------------------------------------------------------
/src/compiler-core/src/transforms/transformElement.ts:
--------------------------------------------------------------------------------
1 | import { createVNodeCall, NodeTypes } from "../ast";
2 |
3 | export function transformElement(node, context) {
4 | if(node.type === NodeTypes.ELEMENT) {
5 | return () => {
6 | // 中间处理层
7 | //tag
8 | const vnodeTag = `'${node.tag}'`
9 | // props
10 | let vnodeProps
11 | // children
12 | const children = node.children
13 | let vnodeChildren = children[0]
14 | node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren)
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/example/helloworld/Foo.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/mini-vue.esm.js"
2 | export const Foo = {
3 | setup(props, { emit }) {
4 | console.log(props)
5 | props.count ++
6 | const emitAdd = () => {
7 | console.log('emit++ add')
8 | emit("add", 1, 2)
9 | emit("add-foo", 1, 2)
10 | }
11 | return {
12 | emitAdd
13 | }
14 | },
15 | render() {
16 | const btn = h("button", {
17 | onClick: this.emitAdd
18 | }, "add")
19 | const foo = h("p", {}, "foot")
20 | return h("div", {}, [foo, btn])
21 | }
22 | }
--------------------------------------------------------------------------------
/example/componentEmit/Foo.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../lib/mini-vue.esm.js"
2 | export const Foo = {
3 | setup(props, { emit }) {
4 | console.log(props)
5 | props.count ++
6 | const emitAdd = () => {
7 | console.log('emit++ add')
8 | emit("add", 1, 2)
9 | emit("add-foo", 1, 2)
10 | }
11 | return {
12 | emitAdd
13 | }
14 | },
15 | render() {
16 | const btn = h("button", {
17 | onClick: this.emitAdd
18 | }, "add")
19 | const foo = h("p", {}, "foot")
20 | return h("div", {}, [foo, btn])
21 | }
22 | }
--------------------------------------------------------------------------------
/example/customRenderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | mini-vue
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/example/apiWatch/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref, reactive, nextTick, onMounted, watch } from '../../lib/mini-vue.esm.js'
2 | const refKey = ref()
3 |
4 | export const App = {
5 | render() {
6 |
7 | return h(
8 | 'div',
9 | {
10 | ref: refKey
11 | },
12 | 'coboy'
13 | )
14 | },
15 | setup() {
16 | const obj = reactive({name: 'coboy~'})
17 | watch(() => obj.name, () => {
18 | console.log('数据变化了')
19 | })
20 | setTimeout(() => {
21 | obj.name = 'cobyte'
22 | }, 1000)
23 | return {
24 | // refKey
25 | }
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/src/compiler-core/tests/__snapshots__/codegen.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`codegen element 1`] = `
4 | "const { toDisplayString:_toDisplayString, createElementVnode:_createElementVnode } = Vue
5 | return function render(_ctx, _cache){return _createElementVnode('div', null, 'hi,' + _toDisplayString(_ctx.message))}"
6 | `;
7 |
8 | exports[`codegen interpolation 1`] = `
9 | "const { toDisplayString:_toDisplayString } = Vue
10 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}"
11 | `;
12 |
13 | exports[`codegen string 1`] = `
14 | "
15 | return function render(_ctx, _cache){return 'hi'}"
16 | `;
17 |
--------------------------------------------------------------------------------
/src/reactivity/tests/shallowReadonly.spec.ts:
--------------------------------------------------------------------------------
1 | import { isReadonly, shallowReadonly } from "../reactive";
2 |
3 | describe('shallowReadonly', () => {
4 | test("should not make non-reactive properties reactive", () => {
5 | const props = shallowReadonly({ n: { foo: 1 } });
6 | expect(isReadonly(props)).toBe(true);
7 | expect(isReadonly(props.n)).toBe(false);
8 | });
9 | it("should call console.warn when set", () => {
10 | console.warn = jest.fn();
11 | const user = shallowReadonly({
12 | age: 10,
13 | });
14 |
15 | user.age = 11;
16 | expect(console.warn).toHaveBeenCalled();
17 | });
18 | })
19 |
--------------------------------------------------------------------------------
/example/patchChildren/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from "../../lib/mini-vue.esm.js";
2 |
3 | import ArrayToText from "./ArrayToText.js";
4 | import TextToText from "./TextToText.js";
5 | import TextToArray from "./TextToArray.js";
6 | import ArrayToArray from "./ArrayToArray.js";
7 |
8 | export default {
9 | name: "App",
10 | setup() {},
11 |
12 | render() {
13 | return h("div", { tId: 1 }, [
14 | h("p", {}, "主页"),
15 | // 老的是 array 新的是 text
16 | // h(ArrayToText),
17 | // 老的是 text 新的是 text
18 | // h(TextToText),
19 | // 老的是 text 新的是 array
20 | // h(TextToArray)
21 | // 老的是 array 新的是 array
22 | h(ArrayToArray)
23 | ]);
24 | },
25 | };
--------------------------------------------------------------------------------
/src/compiler-core/tests/transform.spec.ts:
--------------------------------------------------------------------------------
1 | import { transform } from "../src/transform"
2 | import { baseParse } from "../src/parse"
3 | import { NodeTypes } from "../src/ast"
4 |
5 | describe('transform', () => {
6 | it("happy path", () => {
7 | const ast = baseParse('hi,{{message}}
')
8 | const plugin = (node) => {
9 | if(node.type === NodeTypes.TEXT) {
10 | node.content = node.content + " mini-vue"
11 | }
12 | }
13 | transform(ast, {
14 | nodeTransforms: [plugin]
15 | })
16 | const nodeText = ast.children[0].children[0]
17 | expect(nodeText.content).toBe("hi, mini-vue")
18 | })
19 | })
--------------------------------------------------------------------------------
/example/lifecycle/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref, nextTick, onMounted } from '../../lib/mini-vue.esm.js'
2 | import { Foo } from './Foo.js'
3 | const refKey = ref()
4 | // string
5 | export const App = {
6 | render() {
7 |
8 | return h(
9 | 'div',
10 | {
11 | ref: refKey
12 | },
13 | [
14 | h(Foo, { }, 'coboy')
15 | ]
16 | )
17 | },
18 | setup() {
19 | console.log('parent')
20 | const mount1 = () => {
21 | console.log('onMounted by parent')
22 | }
23 | onMounted(mount1)
24 |
25 | nextTick(() => {
26 | // console.log('refKey', refKey)
27 | })
28 |
29 | return {
30 | // refKey
31 | }
32 | },
33 | }
34 |
--------------------------------------------------------------------------------
/src/runtime-core/componentPublicInstance.ts:
--------------------------------------------------------------------------------
1 | import { hasOwn } from "../shared"
2 |
3 | const publicPropertiesMap = {
4 | $el: i => i.vnode.el,
5 | $slots: i => i.slots,
6 | $props: i => i.props
7 | }
8 |
9 | export const publicInstanceProxyHandlers = {
10 | get({ _: instance}, key) {
11 | const { setupState, props } = instance
12 |
13 | if(hasOwn(setupState, key)) {
14 | return setupState[key]
15 | } else if(hasOwn(props, key)) {
16 | return props[key]
17 | }
18 |
19 | const publicGetter = publicPropertiesMap[key]
20 | if(publicGetter) {
21 | return publicGetter(instance)
22 | } else {
23 | console.warn(`non-existent ${key} publicGetter`)
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/example/customRenderer/main.js:
--------------------------------------------------------------------------------
1 | import { createRenderer } from "../../lib/mini-vue.esm.js"
2 | import { App } from './App.js'
3 | console.log(PIXI)
4 | const game = new PIXI.Application({
5 | with: 500,
6 | height: 500
7 | })
8 | document.body.append(game.view)
9 | const renderer = createRenderer({
10 | createElement(type) {
11 | if(type === 'rect') {
12 | const rect = new PIXI.Graphics()
13 | rect.beginFill(0xff0000)
14 | rect.drawRect(0, 0, 100, 100)
15 | rect.endFill()
16 | return rect
17 | }
18 | },
19 | patchProp(el, key, val){
20 | el[key] = val
21 | },
22 | insert(el, parent) {
23 | parent.addChild(el)
24 | }
25 | })
26 |
27 | renderer.createApp(App).mount(game.stage)
--------------------------------------------------------------------------------
/src/reactivity/computed.ts:
--------------------------------------------------------------------------------
1 |
2 | import { ReactiveEffect } from "./effect"
3 |
4 | class ComputedImpl {
5 | private _getter: any
6 | private _dirty: boolean = true
7 | private _value: any
8 | private _effect: any
9 | constructor(getter) {
10 | this._getter = getter
11 | this._effect = new ReactiveEffect(getter, () => {
12 | // 只要触发了这个函数说明响应式对象的值发生改变了
13 | // 那么就解锁,后续在调用 get 的时候就会重新执行,所以会得到最新的值
14 | if(!this._dirty) {
15 | this._dirty = true
16 | }
17 | })
18 | }
19 |
20 | get value() {
21 | if(this._dirty) {
22 | this._dirty = false
23 | this._value = this._effect.run()
24 | }
25 | return this._value
26 | }
27 | }
28 |
29 | export function computed(getter) {
30 | return new ComputedImpl(getter)
31 | }
--------------------------------------------------------------------------------
/src/reactivity/tests/readonly.spec.ts:
--------------------------------------------------------------------------------
1 | import { readonly, isReadonly, isProxy } from "../reactive";
2 |
3 | describe("readonly", () => {
4 | it("should make nested values readonly", () => {
5 | const original = { foo: 1, bar: { baz: 2 } };
6 | const wrapped = readonly(original);
7 | expect(wrapped).not.toBe(original);
8 | expect(wrapped.foo).toBe(1);
9 | expect(isReadonly(wrapped)).toBe(true)
10 | expect(isReadonly(wrapped.bar)).toBe(true)
11 | expect(isProxy(wrapped.bar)).toBe(true)
12 | expect(isReadonly(original)).toBe(false)
13 | expect(isReadonly(original.bar)).toBe(false)
14 | });
15 |
16 | it("should call console.warn when set", () => {
17 | console.warn = jest.fn();
18 | const user = readonly({
19 | age: 10,
20 | });
21 |
22 | user.age = 11;
23 | expect(console.warn).toHaveBeenCalled();
24 | });
25 | });
--------------------------------------------------------------------------------
/example/TemplateRefs/App1.js:
--------------------------------------------------------------------------------
1 | import { h, ref, nextTick } from '../../lib/mini-vue.esm.js'
2 | import { Foo } from './Foo.js'
3 |
4 | string
5 | export const App = {
6 | render() {
7 |
8 | return h(
9 | 'div',
10 | {
11 | onClick: this.refHandler,
12 | ref:'elmentRef'
13 | },
14 | [
15 | h(Foo, { ref: 'fooRef'})
16 | ]
17 | )
18 | },
19 | setup() {
20 | const fooRef = ref(null)
21 | const elmentRef = ref(null)
22 | const refHandler = () => {
23 | console.log('fooRef', fooRef)
24 | fooRef.value.emitAdd()
25 | console.log('elmentRef', elmentRef.value)
26 | }
27 | nextTick(() => {
28 | console.log('nextTickfooRef', fooRef)
29 | fooRef.value.emitAdd()
30 | console.log('nextTickelmentRef', elmentRef.value)
31 | })
32 | return {
33 | fooRef,
34 | refHandler,
35 | elmentRef
36 | }
37 | },
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/example/TemplateRefs/App2.js:
--------------------------------------------------------------------------------
1 | import { h, ref, nextTick } from '../../lib/mini-vue.esm.js'
2 | import { Foo } from './Foo.js'
3 |
4 | // string
5 | export const App = {
6 | render() {
7 |
8 | return h(
9 | 'div',
10 | {
11 | onClick: this.refHandler,
12 | ref: this.refKey
13 | },
14 | [
15 | h(Foo, { })
16 | ]
17 | )
18 | },
19 | setup() {
20 | const fooEl = ref(null)
21 | const barEl = ref(null)
22 | const refKey = ref('foo')
23 | const refHandler = () => {
24 | }
25 | nextTick(() => {
26 | console.log('fooEl', fooEl, 'barEl', barEl)
27 | refKey.value = 'bar'
28 | })
29 | nextTick(() => {
30 | console.log('fooEl', fooEl, 'barEl', barEl)
31 | })
32 | return {
33 | foo:fooEl,
34 | bar: barEl,
35 | refHandler,
36 | refKey
37 | }
38 | },
39 | }
40 |
--------------------------------------------------------------------------------
/example/nextTicker/App.js:
--------------------------------------------------------------------------------
1 | import {
2 | h,
3 | ref,
4 | getCurrentInstance,
5 | // nextTick,
6 | } from "../../lib/mini-vue.esm.js";
7 |
8 | export default {
9 | name: "App",
10 | setup() {
11 | const count = ref(1);
12 | const instance = getCurrentInstance();
13 |
14 | function onClick() {
15 | for (let i = 0; i < 100; i++) {
16 | console.log("update");
17 | count.value = i;
18 | }
19 |
20 | // debugger;
21 | console.log(instance);
22 | // nextTick(() => {
23 | // console.log(instance);
24 | // });
25 |
26 | // await nextTick()
27 | // console.log(instance)
28 | }
29 |
30 | return {
31 | onClick,
32 | count,
33 | };
34 | },
35 | render() {
36 | const button = h("button", { onClick: this.onClick }, "update");
37 | const p = h("p", {}, "count:" + this.count);
38 |
39 | return h("div", {}, [button, p]);
40 | },
41 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-vue3",
3 | "version": "1.0.0",
4 | "description": "a mini vue3.0",
5 | "main": "lib/mini-vue.cjs.js",
6 | "module": "lib/mini-vue.esm.js",
7 | "scripts": {
8 | "test": "jest",
9 | "build": "rollup -c rollup.config.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/amebyte/mini-vue3.git"
14 | },
15 | "keywords": [],
16 | "author": "",
17 | "license": "ISC",
18 | "bugs": {
19 | "url": "https://github.com/amebyte/mini-vue3/issues"
20 | },
21 | "homepage": "https://github.com/amebyte/mini-vue3#readme",
22 | "devDependencies": {
23 | "@babel/core": "^7.15.5",
24 | "@babel/preset-env": "^7.15.6",
25 | "@babel/preset-typescript": "^7.15.0",
26 | "@types/jest": "^27.0.1",
27 | "babel-jest": "^27.1.1",
28 | "jest": "^27.1.1",
29 | "rollup": "^2.58.0",
30 | "tslib": "^2.3.1",
31 | "typescript": "^4.4.3"
32 | },
33 | "dependencies": {
34 | "@rollup/plugin-typescript": "^8.2.5"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/example/helloworld/App.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../lib/mini-vue.esm.js'
2 | import { Foo } from './Foo.js'
3 | window.self = null
4 | export const App = {
5 | render() {
6 | window.self = this
7 | // return h("div",{ id: 'root', class: ['red', 'green']}, "hi," + this.msg)
8 | return h(
9 | 'div',
10 | {
11 | id: 'root',
12 | class: ['red', 'green'],
13 | onClick() {
14 | console.log('click')
15 | },
16 | onMousedown() {
17 | console.log("mousedown")
18 | }
19 | },
20 | [
21 | h('p', { class: 'red' }, 'hi'),
22 | h('span', { class: 'green' }, 'this is a ' + this.msg),
23 | h(Foo, { count: 1,
24 | onAdd(a, b) {
25 | console.log("onAdd", a, b)
26 | },
27 | onAddFoo(a, b) {
28 | console.log("onAddFoo", a, b)
29 | }
30 | })
31 | ]
32 | )
33 | },
34 | setup() {
35 | return {
36 | msg: 'mini-vue',
37 | }
38 | },
39 | }
40 |
--------------------------------------------------------------------------------
/example/componentEmit/App.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../lib/mini-vue.esm.js'
2 | import { Foo } from './Foo.js'
3 | window.self = null
4 | export const App = {
5 | render() {
6 | window.self = this
7 | // return h("div",{ id: 'root', class: ['red', 'green']}, "hi," + this.msg)
8 | return h(
9 | 'div',
10 | {
11 | id: 'root',
12 | class: ['red', 'green'],
13 | onClick() {
14 | console.log('click')
15 | },
16 | onMousedown() {
17 | console.log("mousedown")
18 | }
19 | },
20 | [
21 | h('p', { class: 'red' }, 'hi'),
22 | h('span', { class: 'green' }, 'this is a ' + this.msg),
23 | h(Foo, { count: 1,
24 | onAdd(a, b) {
25 | console.log("onAdd", a, b)
26 | },
27 | onAddFoo(a, b) {
28 | console.log("onAddFoo", a, b)
29 | }
30 | })
31 | ]
32 | )
33 | },
34 | setup() {
35 | return {
36 | msg: 'mini-vue',
37 | }
38 | },
39 | }
40 |
--------------------------------------------------------------------------------
/src/runtime-core/apiInject.ts:
--------------------------------------------------------------------------------
1 | import { getCurrentInstance } from "./component"
2 |
3 | export function provide(key, value) {
4 | const currentInstance: any = getCurrentInstance()
5 | if(currentInstance) {
6 | let { provides } = currentInstance
7 | const parentProvides = currentInstance.parent.provides
8 | if(provides === parentProvides) {
9 | // Object.create() es6创建对象的另一种方式,可以理解为继承一个对象, 添加的属性是在原型下。
10 | provides = currentInstance.provides = Object.create(parentProvides)
11 | }
12 | provides[key] = value
13 | }
14 | }
15 |
16 | export function inject(key, defaultValue) {
17 | const currentInstance: any = getCurrentInstance()
18 | if(currentInstance) {
19 | const parentProvides = currentInstance.parent.provides
20 | if(key in parentProvides) {
21 | return parentProvides[key]
22 | } else if(defaultValue) {
23 | if(typeof defaultValue === 'function') {
24 | return defaultValue()
25 | }
26 | return defaultValue
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/src/reactivity/reactive.ts:
--------------------------------------------------------------------------------
1 | import { isObject } from "../shared"
2 | import { mutableHanders, readonlyHanders, shallowReadonlyHanders } from "./baseHandler"
3 |
4 | export const enum ReactiveFlags {
5 | IS_REACTIVE = '_v_isReactive',
6 | IS_READONLY = '_v_isReadonly'
7 | }
8 |
9 | export function reactive(raw) {
10 | return createActiveObject(raw, mutableHanders)
11 | }
12 |
13 | export function readonly(raw) {
14 | return createActiveObject(raw, readonlyHanders)
15 | }
16 |
17 | export function shallowReadonly(raw) {
18 | return createActiveObject(raw, shallowReadonlyHanders)
19 | }
20 |
21 | export function isReactive(value) {
22 | return !!value[ReactiveFlags.IS_REACTIVE]
23 | }
24 |
25 | export function isReadonly(value) {
26 | return !!value[ReactiveFlags.IS_READONLY]
27 | }
28 |
29 | export function isProxy(value) {
30 | return isReactive(value) || isReadonly(value)
31 | }
32 |
33 | function createActiveObject(raw: any, baseHandlers) {
34 | if(!isObject(raw)) {
35 | console.warn("raw ${raw} 必须是一个对象")
36 | return raw
37 | }
38 | return new Proxy(raw, baseHandlers)
39 | }
--------------------------------------------------------------------------------
/src/reactivity/tests/reactive.spec.ts:
--------------------------------------------------------------------------------
1 | import { effect } from '../effect';
2 | import { reactive, isReactive, isProxy } from '../reactive';
3 | describe('reactive', () => {
4 | it('happy path', () => {
5 | const original = {foo: 1}
6 | const observed = reactive(original)
7 | expect(observed).not.toBe(original)
8 | expect(observed.foo).toBe(1)
9 | expect(isReactive(observed)).toBe(true)
10 | expect(isProxy(observed)).toBe(true)
11 | })
12 | test("nested reactives", () => {
13 | const original = {
14 | nested: {
15 | foo: 1,
16 | },
17 | array: [{ bar: 2 }],
18 | };
19 | const observed = reactive(original);
20 | expect(isReactive(observed.nested)).toBe(true);
21 | expect(isReactive(observed.array)).toBe(true);
22 | expect(isReactive(observed.array[0])).toBe(true);
23 | });
24 | it("test", () => {
25 | const obj = reactive({
26 | a: {
27 | b: "b",
28 | },
29 | });
30 |
31 | effect(() => {
32 | console.log(obj.a);
33 | });
34 |
35 | obj.a = { b: "bb" };
36 | });
37 | })
--------------------------------------------------------------------------------
/src/compiler-core/tests/codegen.spec.ts:
--------------------------------------------------------------------------------
1 | import { generate } from "../src/codegen"
2 | import { baseParse } from "../src/parse"
3 | import { transform } from "../src/transform"
4 | import { transformElement } from "../src/transforms/transformElement"
5 | import { transformExpression } from "../src/transforms/transformExpression"
6 | import { transformText } from "../src/transforms/transformText"
7 |
8 | describe('codegen', () => {
9 | it('string', () => {
10 | const ast = baseParse('hi')
11 | transform(ast)
12 | const {code} = generate(ast)
13 | expect(code).toMatchSnapshot()
14 | })
15 | it('interpolation', () => {
16 | const ast = baseParse('{{message}}')
17 | transform(ast, {
18 | nodeTransforms: [transformExpression]
19 | })
20 | const {code} = generate(ast)
21 | expect(code).toMatchSnapshot()
22 | })
23 | it('element', () => {
24 | const ast = baseParse('hi,{{message}}
')
25 | transform(ast, {
26 | nodeTransforms: [transformExpression,transformElement,transformText]
27 | })
28 | const {code} = generate(ast)
29 | expect(code).toMatchSnapshot()
30 | })
31 | })
--------------------------------------------------------------------------------
/example/componentUpdate/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref, onUpdated, onBeforeUpdate } from "../../lib/mini-vue.esm.js";
2 | import Child from "./Child.js";
3 |
4 | export const App = {
5 | name: "App",
6 | setup() {
7 | onBeforeUpdate(() => {
8 | console.log('onBeforeUpdate by parent')
9 | })
10 | onUpdated(() => {
11 | console.log('onUpdated by parent')
12 | })
13 | const msg = ref("123");
14 | const count = ref(1);
15 |
16 | window.msg = msg;
17 |
18 | const changeChildProps = () => {
19 | msg.value = "456";
20 | };
21 |
22 | const changeCount = () => {
23 | count.value++;
24 | };
25 |
26 | return { msg, changeChildProps, changeCount, count };
27 | },
28 |
29 | render() {
30 | return h("div", {}, [
31 | h("div", {}, "你好"),
32 | h(
33 | "button",
34 | {
35 | onClick: this.changeChildProps,
36 | },
37 | "change child props"
38 | ),
39 | h(Child, {
40 | msg: this.msg,
41 | count: this.count
42 | }),
43 | h(
44 | "button",
45 | {
46 | onClick: this.changeCount,
47 | },
48 | "change self count"
49 | ),
50 | h("p", {}, "count: " + this.count),
51 | ]);
52 | },
53 | };
--------------------------------------------------------------------------------
/src/runtime-dom/index.ts:
--------------------------------------------------------------------------------
1 | import { createRenderer } from '../runtime-core'
2 |
3 | function createElement(type) {
4 | return document.createElement(type)
5 | }
6 |
7 | function patchProp(el, key, prevVal, nextVal) {
8 | const isOn = (key: string) => /^on[A-Z]/.test(key)
9 | if(isOn(key)) {
10 | const event = key.slice(2).toLowerCase()
11 | el.addEventListener(event, nextVal)
12 | } else {
13 | if(nextVal === undefined || nextVal === null) {
14 | el.removeAttribute(key, nextVal)
15 | } else {
16 | el.setAttribute(key, nextVal)
17 | }
18 | }
19 | }
20 |
21 | function insert(child, parent, anchor) {
22 | // parent.append(el)
23 | parent.insertBefore(child, anchor || null)
24 | }
25 |
26 | function remove(child) {
27 | const parent = child.parentNode
28 | if(parent) {
29 | parent.removeChild(child)
30 | }
31 | }
32 |
33 | function setElementText(el, text) {
34 | el.textContent = text
35 | }
36 |
37 | const renderer: any = createRenderer({
38 | createElement,
39 | patchProp,
40 | insert,
41 | remove,
42 | setElementText
43 | })
44 |
45 | export function createApp(...args) {
46 | return renderer.createApp(...args)
47 | }
48 |
49 | export * from "../runtime-core"
--------------------------------------------------------------------------------
/example/apiInject/App.js:
--------------------------------------------------------------------------------
1 | import { h, provide, inject } from "../../lib/mini-vue.esm.js"
2 |
3 | const Provider = {
4 | name: "Provider",
5 | setup() {
6 | provide("foo", "fooVal")
7 | provide("bar", "barVal")
8 | },
9 | render() {
10 | return h("div", {}, [h("p", {}, "Provider"), h(ProviderTwo)])
11 | }
12 | }
13 |
14 | const ProviderTwo = {
15 | name: "ProviderTwo",
16 | setup() {
17 | provide("foo", "fooTwoVal")
18 | const foo = inject("foo")
19 | return {
20 | foo
21 | }
22 | },
23 | render() {
24 | return h("div", {}, [h("p", {}, `ProviderTwo foo: ${this.foo}`), h(Consumer)])
25 | }
26 | }
27 |
28 | const Consumer = {
29 | name: "Consumer",
30 | setup() {
31 | const foo = inject("foo")
32 | const bar = inject("bar")
33 | const barz = inject("barz", () => "barzDefault")
34 | return {
35 | foo,
36 | bar,
37 | barz
38 | }
39 | },
40 | render() {
41 | return h("div", {}, `Consumer: -${this.foo} - ${this.bar} - ${this.barz}`)
42 | }
43 | }
44 |
45 | export default {
46 | name: "App",
47 | setup() {},
48 | render() {
49 | return h("div", {}, [h("p", {}, "apiInject"), h(Provider)])
50 | }
51 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 基于崔效瑞老师的mini-vue而来,用于深入学习 vue3 源码
2 |
3 | 为什么叫 `mini-vue3-plus`,因为它是基于崔效瑞老师的开源库mini-vue而来,在mini-vue的基础上实现更多的vue3核心功能,用于深入学习 vue3, 让你更轻松地理解 vue3 的核心逻辑。
4 |
5 | 目前已经实现:
6 |
7 | - [x] 模板引用ref:[Vue3 模板引用 ref 的实现原理](https://juejin.cn/post/7087227183613083678)
8 | - [x] 生命周期:[Vue3生命周期Hooks的原理及其与调度器(Scheduler)的关系](https://juejin.cn/post/7093880734246502414)
9 | - [x] 调度器Scheduler:[Vue3生命周期Hooks的原理及其与调度器(Scheduler)的关系](https://juejin.cn/post/7093880734246502414)
10 | - [x] watch watchEffect [Vue3 的 effect、 watch、watchEffect 的实现原理](https://juejin.cn/post/7098303741278814221)
11 |
12 |
13 |
14 | 崔效瑞老师还把他的mini-vue库的实现做成了视频教程,大家如果想学的话,也可以扫码进行学习。
15 |
16 | 读懂 Vue3 源码可能是 2022 年要进大厂的必经之路了。
17 |
18 | 但是直接阅读源码的难度非常大,因为除了核心逻辑以外框架本身还要处理很多 edge case(边缘情况) 、错误处理、热更新等一系列工程问题。
19 |
20 | 因此我向大家推荐大崔哥的 mini-vue 来简化学习 vue3 源码的难度 ,这个库把 vue3 源码中最核心的逻辑剥离出来,只留下核心逻辑,以供大家学习,带有详细的中文注释,以及完善的输出,帮助你理解运行时流程。
21 |
22 | mini-vue 这个库在 github 上已经获的 6.2k 的 star,完成了 reactivity、 runtime-core、 compiler 三个大模块的简化。
23 |
24 | 现在大崔哥基于 mini-vue 推出了视频课程,他会带你手把手的实现同级别的 mini-vue 库,授课方式非常有特点,他会先带你写一个单元测试(目标),然后写代码让单测通过(实现),最后在使用重构手法让代码变的更可读。 代码组织方式和代码的命名也完全遵照 vue3 源码。课程里面没有跳过一行代码。同时还融入了 TDD 、 小步骤的开发思想、 TPP 从具体到抽象等编程思想层面的内容。
25 |
26 | 想掌握源码的话,最好的方式就是手写一遍,而这套课程的教学理念就是展现出编程问题背后的思考全过程,对于想掌握 vue3 原理、冲击大厂、进阶中高级前端的同学非常适合,是目前市场上少见的好课。
27 |
28 | **购买之后,报是从 coboy 的海报过来的,还有优惠返款 ~**
29 |
30 | 
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/compiler-core/src/transforms/transformText.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "../ast";
2 |
3 | export function transformText(node) {
4 |
5 | function isText(node) {
6 | return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION
7 | }
8 | let currentContainer
9 | if(node.type === NodeTypes.ELEMENT) {
10 | return () => {
11 | const { children } = node
12 | for(let i = 0; i < children.length; i++) {
13 | const child = children[i]
14 | if(isText(child)){
15 | for(let j = i + 1; j < children.length; j++) {
16 | const next = children[j]
17 | if(isText(next)) {
18 | if(!currentContainer) {
19 | currentContainer = children[i] = {
20 | type: NodeTypes.COMPOUND_EXPRESSION,
21 | children: [child]
22 | }
23 | }
24 | currentContainer.children.push(' + ')
25 | currentContainer.children.push(next)
26 | children.splice(j, 1)
27 | j--
28 | } else {
29 | currentContainer = undefined
30 | break;
31 | }
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/reactivity/baseHandler.ts:
--------------------------------------------------------------------------------
1 | import { extend, isObject } from "../shared"
2 | import { track, trigger } from "./effect"
3 | import { reactive, ReactiveFlags, readonly } from "./reactive"
4 |
5 | const get = createGetter()
6 | const set = createSetter()
7 | const readonlyGet = createGetter(true)
8 | const shallowReadonlyGet = createGetter(true, true)
9 |
10 | function createGetter(isReadonly = false, shallow = false) {
11 | return function get(target, key) {
12 | if(key === ReactiveFlags.IS_REACTIVE) {
13 | return !isReadonly
14 | } else if(key === ReactiveFlags.IS_READONLY) {
15 | return isReadonly
16 | }
17 | const res = Reflect.get(target, key)
18 |
19 | if(shallow) {
20 | return res
21 | }
22 |
23 | if(!isReadonly) track(target, key)
24 |
25 | if(isObject(res)) {
26 | return isReadonly ? readonly(res) : reactive(res)
27 | }
28 | return res
29 | }
30 | }
31 |
32 | function createSetter() {
33 | return function set(target, key, val){
34 | const res = Reflect.set(target, key, val)
35 | trigger(target, key)
36 | return res
37 | }
38 | }
39 |
40 | export const mutableHanders = {
41 | get,
42 | set
43 | }
44 |
45 | export const readonlyHanders = {
46 | get: readonlyGet,
47 | set(target, key, val) {
48 | console.warn('readonly数据不能被set')
49 | return true
50 | }
51 | }
52 |
53 | export const shallowReadonlyHanders = extend({}, readonlyHanders, { get: shallowReadonlyGet })
--------------------------------------------------------------------------------
/src/reactivity/tests/computed.spec.ts:
--------------------------------------------------------------------------------
1 | import { computed } from "../computed";
2 | import { reactive } from "../reactive";
3 |
4 | describe("computed", () => {
5 | it("happy path", () => {
6 | const user = reactive({
7 | age: 1,
8 | });
9 |
10 | const age = computed(() => {
11 | return user.age;
12 | });
13 |
14 | expect(age.value).toBe(1);
15 | });
16 |
17 | it("should compute lazily", () => {
18 | const value = reactive({
19 | foo: 1,
20 | });
21 | const getter = jest.fn(() => {
22 | return value.foo;
23 | });
24 | const cValue = computed(getter);
25 |
26 | // lazy
27 | expect(getter).not.toHaveBeenCalled();
28 |
29 | expect(cValue.value).toBe(1);
30 | expect(getter).toHaveBeenCalledTimes(1);
31 |
32 | // should not compute again
33 | cValue.value; // get
34 | expect(getter).toHaveBeenCalledTimes(1);
35 |
36 | // should not compute until needed
37 | value.foo = 2;
38 | expect(getter).toHaveBeenCalledTimes(1);
39 |
40 | // now it should compute
41 | expect(cValue.value).toBe(2);
42 | expect(getter).toHaveBeenCalledTimes(2);
43 |
44 | // should not compute again
45 | cValue.value;
46 | // expect(getter).toHaveBeenCalledTimes(2);
47 | });
48 |
49 | it("hao compute run", () => {
50 | const value = reactive({
51 | foo: 1,
52 | });
53 | const getter = jest.fn(() => {
54 | return value.foo;
55 | });
56 | const cValue = computed(getter);
57 | cValue.value
58 | value.foo = 3
59 | expect(cValue.value).toBe(3);
60 | })
61 | });
62 |
--------------------------------------------------------------------------------
/src/runtime-core/directives.ts:
--------------------------------------------------------------------------------
1 | import { EMPTY_OBJ, isFunction } from "../shared"
2 | import { currentRenderingInstance } from "./componentRenderContext"
3 |
4 | export function withDirectives(
5 | vnode,
6 | directives
7 | ) {
8 | const internalInstance = currentRenderingInstance as any
9 | if (internalInstance === null) {
10 | console.warn(`withDirectives can only be used inside render functions.`)
11 | return vnode
12 | }
13 | const instance = internalInstance.proxy
14 | const bindings = vnode.dirs || (vnode.dirs = [])
15 | for (let i = 0; i < directives.length; i++) {
16 | let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
17 | if (isFunction(dir)) {
18 | dir = {
19 | mounted: dir,
20 | updated: dir
21 | }
22 | }
23 | bindings.push({
24 | dir,
25 | instance,
26 | value,
27 | oldValue: void 0,
28 | arg,
29 | modifiers
30 | })
31 | }
32 | return vnode
33 | }
34 |
35 | export function invokeDirectiveHook(
36 | vnode,
37 | prevVNode,
38 | instance,
39 | name
40 | ) {
41 | const bindings = vnode.dirs!
42 | const oldBindings = prevVNode && prevVNode.dirs!
43 | for (let i = 0; i < bindings.length; i++) {
44 | const binding = bindings[i]
45 | if (oldBindings) {
46 | binding.oldValue = oldBindings[i].value
47 | }
48 | let hook = binding.dir[name]
49 | if (hook) {
50 | const args = [
51 | vnode.el,
52 | binding,
53 | vnode,
54 | prevVNode
55 | ]
56 | hook(...args)
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/src/runtime-core/apiLifecycle.ts:
--------------------------------------------------------------------------------
1 | import { currentInstance, LifecycleHooks, setCurrentInstance, unsetCurrentInstance } from "./component"
2 |
3 | // injectHook是一个闭包函数,通过闭包缓存绑定对应生命周期Hooks到对应的组件实例上
4 | export function injectHook(type, hook, target) {
5 | if(target) {
6 | // 把各个生命周期的Hooks函数挂载到组件实例上,并且是一个数组,因为可能你会多次调用同一个组件的同一个生命周期函数
7 | const hooks = target[type] || (target[type] = [])
8 | // 把生命周期函数进行包装并且把包装函数缓存在__weh上
9 | const wrappedHook =
10 | hook.__weh ||
11 | (hook.__weh = (...args: unknown[]) => {
12 | if (target.isUnmounted) {
13 | return
14 | }
15 | // 当生命周期调用时 保证currentInstance是正确的
16 | setCurrentInstance(target)
17 | // 执行生命周期Hooks函数
18 | const res = args ? hook(...args) : hook()
19 | unsetCurrentInstance()
20 | return res
21 | })
22 | // 把生命周期的包装函数绑定到组件实例对应的hooks上
23 | hooks.push(wrappedHook)
24 | // 返回包装函数
25 | return wrappedHook
26 | }
27 | }
28 |
29 | // 创建生命周期函数,target,表示该生命周期Hooks函数被绑定到哪个组件实例上,默认是当前工作的组件实例。
30 | // createHook是一个闭包函数,通过闭包缓存当前是属于哪个生命周期的Hooks
31 | export const createHook = (lifecycle) => (hook, target = currentInstance) => injectHook(lifecycle, hook, target)
32 |
33 | export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
34 | export const onMounted = createHook(LifecycleHooks.MOUNTED)
35 | export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
36 | export const onUpdated = createHook(LifecycleHooks.UPDATED)
37 | export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
38 | export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
39 |
40 |
--------------------------------------------------------------------------------
/src/shared/looseEqual.ts:
--------------------------------------------------------------------------------
1 | import { isArray, isDate, isObject } from './'
2 |
3 | function looseCompareArrays(a: any[], b: any[]) {
4 | if (a.length !== b.length) return false
5 | let equal = true
6 | for (let i = 0; equal && i < a.length; i++) {
7 | equal = looseEqual(a[i], b[i])
8 | }
9 | return equal
10 | }
11 |
12 | export function looseEqual(a: any, b: any): boolean {
13 | if (a === b) return true
14 | let aValidType = isDate(a)
15 | let bValidType = isDate(b)
16 | if (aValidType || bValidType) {
17 | return aValidType && bValidType ? a.getTime() === b.getTime() : false
18 | }
19 | aValidType = isArray(a)
20 | bValidType = isArray(b)
21 | if (aValidType || bValidType) {
22 | return aValidType && bValidType ? looseCompareArrays(a, b) : false
23 | }
24 | aValidType = isObject(a)
25 | bValidType = isObject(b)
26 | if (aValidType || bValidType) {
27 | /* istanbul ignore if: this if will probably never be called */
28 | if (!aValidType || !bValidType) {
29 | return false
30 | }
31 | const aKeysCount = Object.keys(a).length
32 | const bKeysCount = Object.keys(b).length
33 | if (aKeysCount !== bKeysCount) {
34 | return false
35 | }
36 | for (const key in a) {
37 | const aHasKey = a.hasOwnProperty(key)
38 | const bHasKey = b.hasOwnProperty(key)
39 | if (
40 | (aHasKey && !bHasKey) ||
41 | (!aHasKey && bHasKey) ||
42 | !looseEqual(a[key], b[key])
43 | ) {
44 | return false
45 | }
46 | }
47 | }
48 | return String(a) === String(b)
49 | }
50 |
51 | export function looseIndexOf(arr: any[], val: any): number {
52 | return arr.findIndex(item => looseEqual(item, val))
53 | }
54 |
--------------------------------------------------------------------------------
/src/runtime-core/rendererTemplateRef.ts:
--------------------------------------------------------------------------------
1 | import { isRef } from "../reactivity/ref"
2 | import { EMPTY_OBJ, hasOwn, isString } from "../shared"
3 | import { ShapeFlags } from "../shared/ShapeFlags"
4 |
5 | export function setRef(
6 | rawRef,
7 | oldRawRef,
8 | vnode,
9 | isUnmount = false
10 | ) {console.log('setRef')
11 | // 判断如果是组件实例,则把改组件实例作为ref的值,否则就是把该元素作为ref值
12 | const refValue =
13 | vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
14 | ? vnode.component!.proxy
15 | : vnode.el
16 | // 如果n2不存在则是卸载
17 | const value = isUnmount ? null : refValue
18 | // 把在创建虚拟DOM的时候设置保存的组件渲染实例解构出来
19 | const { i: owner, r: ref } = rawRef
20 |
21 | const oldRef = oldRawRef && oldRawRef.r
22 | const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
23 | const setupState = owner.setupState
24 |
25 | // 动态ref,如果ref更改,就删除旧ref的值
26 | if (oldRef != null && oldRef !== ref) {
27 | if (isString(oldRef)) {
28 | refs[oldRef] = null
29 | if (hasOwn(setupState, oldRef)) {
30 | setupState[oldRef] = null
31 | }
32 | } else if (isRef(oldRef)) {
33 | oldRef.value = null
34 | }
35 | }
36 |
37 | // happy path中我们只考虑最简单的情况
38 | const _isString = isString(ref)
39 | console.log('setRef', ref)
40 | if (_isString) {
41 | console.log('rawRef', rawRef, ref)
42 | refs[ref] = value
43 | // 如果在对应于渲染上下文中存在ref键值,则 VNode 的相应元素或组件实例将被分配给该 ref 的值
44 | if (hasOwn(setupState, ref)) {
45 | setupState[ref] = value
46 | }
47 | } else if (isRef(ref)) {
48 | ref.value = value
49 | if (rawRef.k) refs[rawRef.k] = value
50 | }
51 | }
--------------------------------------------------------------------------------
/src/reactivity/ref.ts:
--------------------------------------------------------------------------------
1 | import { hasChange, isObject } from "../shared"
2 | import { isTacking, trackEffect, triggerEffect } from "./effect"
3 | import { reactive } from "./reactive"
4 |
5 | class RefImpl{
6 | private _value
7 | public dep
8 | private _rawValue: any
9 | private _v_isRef = true
10 | constructor(value) {
11 | this._rawValue = value
12 | this._value = convert(value)
13 | this.dep = new Set()
14 | }
15 |
16 | get value() {
17 | trackRefValue(this)
18 | return this._value
19 | }
20 |
21 | set value(newVal) {
22 | if(hasChange(this._rawValue, newVal)){
23 | this._rawValue = newVal
24 | this._value = convert(newVal)
25 | triggerEffect(this.dep)
26 | }
27 | }
28 | }
29 |
30 | function convert(value) {
31 | return isObject(value) ? reactive(value) : value
32 | }
33 |
34 | function trackRefValue(ref) {
35 | if(isTacking()) trackEffect(ref.dep)
36 | }
37 |
38 | export function ref(value) {
39 | return new RefImpl(value)
40 | }
41 |
42 | export function isRef(ref) {
43 | return ref && !!ref._v_isRef
44 | }
45 |
46 | export function unRef(ref) {
47 | return isRef(ref) ? ref.value : ref
48 | }
49 |
50 | export function proxyRefs(objectWithRefs) {
51 | return new Proxy(objectWithRefs, {
52 | get(target, key) {
53 | return unRef(Reflect.get(target, key))
54 | },
55 | set(target, key, val) {
56 | if(isRef(target[key]) && !isRef(val)){
57 | target[key].value = val
58 | // 一定要显式地返回true,不然会报错
59 | return true
60 | } else {
61 | return Reflect.set(target, key, val)
62 | }
63 | }
64 | })
65 | }
--------------------------------------------------------------------------------
/example/update/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from "../../lib/mini-vue.esm.js";
2 |
3 | export const App = {
4 | name: "App",
5 |
6 | setup() {
7 | const count = ref(0);
8 |
9 | const onClick = () => {
10 | count.value++;
11 | };
12 |
13 | const props = ref({
14 | foo: "foo",
15 | bar: "bar",
16 | });
17 | const onChangePropsDemo1 = () => {
18 | props.value.foo = "new-foo";
19 | };
20 |
21 | const onChangePropsDemo2 = () => {
22 | props.value.foo = undefined;
23 | };
24 |
25 | const onChangePropsDemo3 = () => {
26 | props.value = {
27 | foo: "foo",
28 | };
29 | };
30 |
31 | return {
32 | count,
33 | onClick,
34 | onChangePropsDemo1,
35 | onChangePropsDemo2,
36 | onChangePropsDemo3,
37 | props,
38 | };
39 | },
40 | render() {
41 | return h(
42 | "div",
43 | {
44 | id: "root",
45 | ...this.props,
46 | },
47 | [
48 | h("div", {}, "count:" + this.count), // 依赖收集
49 | h(
50 | "button",
51 | {
52 | onClick: this.onClick,
53 | },
54 | "click"
55 | ),
56 | h(
57 | "button",
58 | {
59 | onClick: this.onChangePropsDemo1,
60 | },
61 | "changeProps - 值改变了 - 修改"
62 | ),
63 |
64 | h(
65 | "button",
66 | {
67 | onClick: this.onChangePropsDemo2,
68 | },
69 | "changeProps - 值变成了 undefined - 删除"
70 | ),
71 |
72 | h(
73 | "button",
74 | {
75 | onClick: this.onChangePropsDemo3,
76 | },
77 | "changeProps - key 在新的里面没有了 - 删除"
78 | ),
79 | ]
80 | );
81 | },
82 | };
--------------------------------------------------------------------------------
/src/runtime-core/vnode.ts:
--------------------------------------------------------------------------------
1 | import { isRef } from "../reactivity/ref"
2 | import { isFunction, isString } from "../shared"
3 | import { ShapeFlags } from "../shared/ShapeFlags"
4 | import { currentRenderingInstance } from "./componentRenderContext"
5 |
6 | export const Fragment = Symbol('Fragment')
7 | export const Text = Symbol('Text')
8 |
9 | const normalizeRef = ({
10 | ref,
11 | ref_key,
12 | ref_for,
13 | }) => {
14 | return (
15 | ref != null
16 | // 从这里我们可以知道ref值可以是字符串,Ref数据,函数
17 | ? isString(ref) || isRef(ref) || isFunction(ref)
18 | ? { i: currentRenderingInstance, r: ref, k: ref_key, f: !!ref_for}
19 | : ref
20 | : null
21 | ) as any
22 | }
23 |
24 | export { createVNode as createElementVNode }
25 |
26 | export function createVNode(type, props?, children?) {
27 | const vnode = {
28 | type,
29 | props,
30 | ref: props && normalizeRef(props),
31 | children,
32 | component: null,
33 | key: props && props.key,
34 | shapeFlag: getShapeFlag(type),
35 | el: null
36 | }
37 |
38 | if( typeof children === 'string') {
39 | // vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN
40 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
41 | } else if(Array.isArray(children)) {
42 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
43 | }
44 |
45 | // 组件 + children object
46 | if(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
47 | if(typeof children === 'object') {
48 | vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN
49 | }
50 | }
51 |
52 | return vnode
53 | }
54 |
55 | export function createTextVNode(text: string) {
56 | return createVNode(Text, {}, text)
57 | }
58 |
59 | function getShapeFlag(type) {
60 | return typeof type === 'string' ? ShapeFlags.ELEMENT : ShapeFlags.STATEFUL_COMPONENT
61 | }
--------------------------------------------------------------------------------
/src/shared/index.ts:
--------------------------------------------------------------------------------
1 | export * from './toDisplayString'
2 | export const extend = Object.assign
3 | export const isObject = (val) => {
4 | return val !== null && typeof val === 'object'
5 | }
6 | export const hasChange = (newVal, val) => {
7 | return !Object.is(newVal, val)
8 | }
9 |
10 | export const EMPTY_OBJ: { readonly [key: string]: any } = {}
11 |
12 | export const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)
13 |
14 | export const camelize = (str: string) => {
15 | return str.replace(/-(\w)/g, (_, c: string) => {
16 | return c ? c.toUpperCase() : ""
17 | })
18 | }
19 |
20 | export const capitalize = (str: string) => {
21 | return str.charAt(0).toUpperCase() + str.slice(1)
22 | }
23 |
24 | export const toHandlerKey = (str: string) => {
25 | return str ? 'on' + capitalize(str) : ''
26 | }
27 |
28 | export const isArray = Array.isArray
29 | export const isMap = (val: unknown): val is Map =>
30 | toTypeString(val) === '[object Map]'
31 | export const isSet = (val: unknown): val is Set =>
32 | toTypeString(val) === '[object Set]'
33 |
34 | export const isDate = (val: unknown): val is Date => val instanceof Date
35 | export const isFunction = (val: unknown): val is Function =>
36 | typeof val === 'function'
37 | export const isString = (val: unknown): val is string => typeof val === 'string'
38 |
39 | export const invokeArrayFns = (fns: Function[], arg?: any) => {
40 | for (let i = 0; i < fns.length; i++) {
41 | fns[i](arg)
42 | }
43 | }
44 |
45 | export const objectToString = Object.prototype.toString
46 | export const toTypeString = (value: unknown): string =>
47 | objectToString.call(value)
48 |
49 | export const isPlainObject = (val: unknown): val is object =>
50 | toTypeString(val) === '[object Object]'
51 |
52 | export const toNumber = (val: any): any => {
53 | const n = parseFloat(val)
54 | return isNaN(n) ? val : n
55 | }
--------------------------------------------------------------------------------
/src/reactivity/tests/ref.spec.ts:
--------------------------------------------------------------------------------
1 | import { effect } from "../effect";
2 | import { reactive } from "../reactive";
3 | import { isRef, proxyRefs, ref, unRef } from "../ref";
4 | describe("ref", () => {
5 | it("happy path", () => {
6 | const a = ref(1);
7 | expect(a.value).toBe(1);
8 | });
9 |
10 | it("should be reactive", () => {
11 | const a = ref(1);
12 | let dummy;
13 | let calls = 0;
14 | effect(() => {
15 | calls++;
16 | dummy = a.value;
17 | });
18 | expect(calls).toBe(1);
19 | expect(dummy).toBe(1);
20 | a.value = 2;
21 | expect(calls).toBe(2);
22 | expect(dummy).toBe(2);
23 | // same value should not trigger
24 | a.value = 2;
25 | expect(calls).toBe(2);
26 | expect(dummy).toBe(2);
27 | });
28 |
29 | it("should make nested properties reactive", () => {
30 | const a = ref({
31 | count: 1,
32 | });
33 | let dummy;
34 | effect(() => {
35 | dummy = a.value.count;
36 | });
37 | expect(dummy).toBe(1);
38 | a.value.count = 2;
39 | expect(dummy).toBe(2);
40 | });
41 |
42 | it("isRef", () => {
43 | const a = ref(1)
44 | const user = reactive({ age: 1 })
45 | expect(isRef(a)).toBe(true)
46 | expect(isRef(1)).toBe(false)
47 | expect(isRef(user)).toBe(false)
48 | })
49 |
50 | it("unRef", () => {
51 | const a = ref(1)
52 | expect(unRef(a)).toBe(1)
53 | expect(unRef(1)).toBe(1)
54 | })
55 |
56 | it("proxyRefs", () => {
57 | const user = {
58 | age: ref(10),
59 | name: 'coboy'
60 | }
61 | const proxyUser = proxyRefs(user)
62 | expect(user.age.value).toBe(10)
63 | expect(proxyUser.age).toBe(10)
64 | expect(proxyUser.name).toBe('coboy')
65 |
66 | proxyUser.age = 20
67 | expect(proxyUser.age).toBe(20)
68 | expect(user.age.value).toBe(20)
69 |
70 | proxyUser.age = ref(10)
71 | expect(proxyUser.age).toBe(10)
72 | expect(user.age.value).toBe(10)
73 | })
74 | });
--------------------------------------------------------------------------------
/src/compiler-core/src/transform.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "./ast"
2 | import { TO_DISPLAY_STRING } from "./runtimeHelpers"
3 |
4 | export function transform(root, options = {}) {
5 | const context = createTransformContext(root, options)
6 | traverseNode(root, context)
7 | createRootCodegen(root)
8 | root.helpers = [...context.helpers.keys()]
9 | }
10 |
11 | function createRootCodegen(root: any) {
12 | const child = root.children[0]
13 | if(child.type === NodeTypes.ELEMENT) {
14 | root.codegenNode = child.codegenNode
15 | } else {
16 | root.codegenNode = child
17 | }
18 | }
19 |
20 | function traverseNode(node: any, context) {
21 | const nodeTransforms = context.nodeTransforms
22 | const exitFns: any = []
23 | for(let i = 0; i < nodeTransforms.length; i++) {
24 | const transform = nodeTransforms[i]
25 | const onExit = transform(node, context)
26 | if(onExit) exitFns.push(onExit)
27 | }
28 |
29 | switch(node.type) {
30 | case NodeTypes.INTERPOLATION:
31 | context.helper(TO_DISPLAY_STRING)
32 | break;
33 | case NodeTypes.ROOT:
34 | case NodeTypes.ELEMENT:
35 | traverseChildren(node, context)
36 | break;
37 | default:
38 | break;
39 | }
40 | let i = exitFns.length
41 | while(i--) {
42 | exitFns[i]()
43 | }
44 | }
45 |
46 | function traverseChildren(node: any, context: any) {
47 | const children = node.children
48 |
49 | if(children) {
50 | for(let i = 0; i < children.length; i++){
51 | const node = children[i]
52 | traverseNode(node, context)
53 | }
54 | }
55 | }
56 |
57 | function createTransformContext(root: any, options: any) {
58 | const context = {
59 | root,
60 | nodeTransforms: options.nodeTransforms || [],
61 | helpers: new Map(),
62 | helper(key){
63 | context.helpers.set(key, 1)
64 | }
65 | }
66 | return context
67 | }
68 |
--------------------------------------------------------------------------------
/src/runtime-core/scheduler.ts:
--------------------------------------------------------------------------------
1 |
2 | let activePostFlushCbs: any = null
3 | let postFlushIndex = 0
4 | const pendingPostFlushCbs: any[] = []
5 | let activePreFlushCbs: any[] | null = null
6 | const queue: any[] = []
7 | let isFlushPending = false
8 | const p = Promise.resolve()
9 | export function nextTick(fn) {
10 | return fn ? p.then(fn) : p
11 | }
12 |
13 | const getId = (job): number =>
14 | job.id == null ? Infinity : job.id
15 |
16 | export function queueJobs(job) {
17 | if(!queue.includes(job)) {
18 | queue.push(job)
19 | }
20 | queueFlush()
21 | }
22 |
23 | export function queuePreFlushCb(cb) {
24 | queueCb(cb, activePreFlushCbs)
25 | }
26 |
27 | export function queuePostFlushCb(cb) {
28 | queueCb(cb, pendingPostFlushCbs)
29 | }
30 |
31 | export function flushPostFlushCbs(seen?) {
32 | if (pendingPostFlushCbs.length) {
33 | const deduped = [...new Set(pendingPostFlushCbs)]
34 | pendingPostFlushCbs.length = 0
35 | activePostFlushCbs = deduped
36 | activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
37 |
38 | for (
39 | postFlushIndex = 0;
40 | postFlushIndex < activePostFlushCbs.length;
41 | postFlushIndex++
42 | ) {
43 | activePostFlushCbs[postFlushIndex]()
44 | }
45 | activePostFlushCbs = null
46 | postFlushIndex = 0
47 | }
48 | }
49 |
50 | function queueCb(
51 | cb,
52 | pendingQueue
53 | ) {
54 | pendingQueue.push(...cb)
55 | queueFlush()
56 | }
57 |
58 | function queueFlush() {
59 | if(isFlushPending) return
60 | isFlushPending = true
61 | nextTick(flushJobs)
62 | }
63 |
64 | function flushJobs(seen?) {
65 | isFlushPending = false
66 | // 组件更新前队列执行
67 | // flushPreFlushCbs(seen)
68 | try{
69 | queue.sort((a, b) => getId(a) - getId(b))
70 | // 组件更新队列执行
71 | let job
72 | while (job = queue.shift()) {
73 | job && job()
74 | }
75 | } finally {
76 | // 组件更新后队列执行
77 | flushPostFlushCbs(seen)
78 |
79 | // 如果在执行异步任务的过程中又产生了新的队列,那么则继续回调执行
80 | if (
81 | queue.length ||
82 | // pendingPreFlushCbs.length ||
83 | pendingPostFlushCbs.length
84 | ) {
85 | flushJobs(seen)
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/src/reactivity/tests/effect.spec.ts:
--------------------------------------------------------------------------------
1 | import { effect, stop } from "../effect"
2 | import { reactive } from "../reactive"
3 |
4 | describe('effect', () => {
5 | it('happy path', () => {
6 | const user = reactive({
7 | age: 10
8 | })
9 | let nextAge
10 | effect(() => {
11 | nextAge = user.age + 1
12 | })
13 | expect(nextAge).toBe(11)
14 | user.age++
15 | expect(nextAge).toBe(12)
16 | })
17 | it('should return runner when call effect', () => {
18 | let foo = 10
19 | const runner = effect(() => {
20 | foo++
21 | return 'foo'
22 | })
23 |
24 | expect(foo).toBe(11)
25 | const r = runner()
26 | expect(foo).toBe(12)
27 | expect(r).toBe('foo')
28 | })
29 | it('scheduler', () => {
30 | // 1. 通过effect的第二个参数给定一个scheduler的fn
31 | // 2. effect第一次执行的时候,还会执行fn
32 | // 3. 当响应式对象set update 不会执行fn而是执行scheduler
33 | // 4. 如果说当执行runner的时候,会再次执行fn
34 | let dummy
35 | let run: any
36 | const scheduler = jest.fn(() => {
37 | run = runner
38 | })
39 | const obj = reactive({ foo: 1})
40 | const runner = effect(() => {
41 | dummy = obj.foo
42 | }, {scheduler})
43 | expect(scheduler).not.toHaveBeenCalled()
44 | expect(dummy).toBe(1)
45 | obj.foo++
46 | expect(scheduler).toHaveBeenCalledTimes(1)
47 | expect(dummy).toBe(1)
48 | run()
49 | expect(dummy).toBe(2)
50 | })
51 | it("stop", () => {
52 | let dummy;
53 | const obj = reactive({ prop: 1 });
54 | const runner = effect(() => {
55 | dummy = obj.prop;
56 | });
57 | obj.prop = 2;
58 | expect(dummy).toBe(2);
59 | obj.prop++;
60 | expect(dummy).toBe(3);
61 | stop(runner);
62 | // obj.prop = 3
63 | obj.prop++;
64 | expect(dummy).toBe(3);
65 |
66 | // stopped effect should still be manually callable
67 | runner();
68 | expect(dummy).toBe(4);
69 | });
70 |
71 | it("onStop", () => {
72 | const obj = reactive({ foo: 1 })
73 | const onStop = jest.fn()
74 | let dummy
75 | const runner = effect(() => {
76 | dummy = obj.foo
77 | }, {
78 | onStop
79 | })
80 |
81 | stop(runner)
82 | expect(onStop).toBeCalledTimes(1)
83 | })
84 | })
--------------------------------------------------------------------------------
/src/compiler-core/tests/parse.spec.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "../src/ast"
2 | import { baseParse } from "../src/parse"
3 | describe('Parse', () => {
4 | describe('interpolation', () => {
5 | test('simple interpolation', () => {
6 | const ast = baseParse("{{ message }}")
7 | // root
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('element', () => {
18 | it('simple element div', () => {
19 | const ast = baseParse("")
20 | // root
21 | expect(ast.children[0]).toStrictEqual({
22 | type: NodeTypes.ELEMENT,
23 | tag: "div",
24 | children: []
25 | })
26 | })
27 | })
28 | describe('text', () => {
29 | it('simple text', () => {
30 | const ast = baseParse("some text")
31 | // root
32 | expect(ast.children[0]).toStrictEqual({
33 | type: NodeTypes.TEXT,
34 | content: "some text"
35 | })
36 | })
37 | })
38 |
39 | test("hello world", () => {
40 | const ast = baseParse("hi,{{message}}
")
41 | expect(ast.children[0]).toStrictEqual({
42 | type: NodeTypes.ELEMENT,
43 | tag: "div",
44 | children: [
45 | {
46 | type: NodeTypes.TEXT,
47 | content: "hi,"
48 | },
49 | {
50 | type: NodeTypes.INTERPOLATION,
51 | content: {
52 | type: NodeTypes.SIMPLE_EXPRESSION,
53 | content: "message"
54 | }
55 | }
56 | ]
57 | })
58 | })
59 |
60 | test.only("Nested element", () => {
61 | const ast = baseParse("")
62 | expect(ast.children[0]).toStrictEqual({
63 | type: NodeTypes.ELEMENT,
64 | props: {
65 | type: NodeTypes.ATTRIBUTE,
66 | name: 'id',
67 | value: 'kasong'
68 | },
69 | tag: "div",
70 | children: [
71 | {
72 | type: NodeTypes.ELEMENT,
73 | tag: "p",
74 | children: [
75 | {
76 | type: NodeTypes.TEXT,
77 | content: "卡颂读书会"
78 | }
79 | ]
80 | },
81 | {
82 | type: NodeTypes.INTERPOLATION,
83 | content: {
84 | type: NodeTypes.SIMPLE_EXPRESSION,
85 | content: "message"
86 | }
87 | }
88 | ]
89 | })
90 | })
91 |
92 | test("should throw error when lack end tag", () => {
93 | expect(() => {
94 | const ast = baseParse("
")
95 | }).toThrow('缺少结束标签:span')
96 | })
97 | })
--------------------------------------------------------------------------------
/src/runtime-core/component.ts:
--------------------------------------------------------------------------------
1 | import { proxyRefs } from "../reactivity"
2 | import { shallowReadonly } from "../reactivity/reactive"
3 | import { EMPTY_OBJ } from "../shared"
4 | import { emit } from "./componentEmit"
5 | import { initProps } from "./componentProps"
6 | import { publicInstanceProxyHandlers } from "./componentPublicInstance"
7 | import { initSlots } from "./componentSlots"
8 |
9 | export let currentInstance = null
10 |
11 | export const enum LifecycleHooks {
12 | BEFORE_CREATE = 'bc', // 创建之前
13 | CREATED = 'c', // 创建
14 | BEFORE_MOUNT = 'bm', // 挂载之前
15 | MOUNTED = 'm', // 挂载之后
16 | BEFORE_UPDATE = 'bu', // 更新之前
17 | UPDATED = 'u', // 更新之后
18 | BEFORE_UNMOUNT = 'bum', // 卸载之前
19 | UNMOUNTED = 'um', // 卸载之后
20 | DEACTIVATED = 'da',
21 | ACTIVATED = 'a',
22 | RENDER_TRIGGERED = 'rtg',
23 | RENDER_TRACKED = 'rtc',
24 | ERROR_CAPTURED = 'ec',
25 | SERVER_PREFETCH = 'sp'
26 | }
27 |
28 | export function createComponentInstance(vnode: any, parent) {
29 | let component = {
30 | vnode,
31 | next: null, // 需要更新的 vnode,用于更新 component 类型的组件
32 | type: vnode.type,
33 | setupState: {}, // 存储 setup 的返回值
34 | props: {},
35 | slots: {}, // 存放插槽的数据
36 | refs: EMPTY_OBJ,
37 | provides: parent ? parent.provides : {}, // 获取 parent 的 provides 作为当前组件的初始化值 这样就可以继承 parent.provides 的属性了
38 | parent,
39 | isMounted: false,
40 | subTree: {},
41 | emit: () => {},
42 | m: null,
43 | }
44 | component.emit = emit.bind(null, component) as any
45 | return component
46 | }
47 |
48 | export function setupComponent(instance) {
49 | // TODO
50 | initProps(instance, instance.vnode.props)
51 | // initSlots
52 | initSlots(instance, instance.vnode.children)
53 | setupStatefulComponent(instance)
54 | }
55 |
56 | function setupStatefulComponent(instance: any ) {
57 | const Component = instance.type
58 | instance.proxy = new Proxy({ _:instance }, publicInstanceProxyHandlers)
59 | const { setup } = Component
60 | if(setup) {
61 | setCurrentInstance(instance)
62 | const setupResult = setup(shallowReadonly(instance.props), {
63 | emit: instance.emit
64 | })
65 | setCurrentInstance(null)
66 | handleSetupResult(instance, setupResult)
67 | }
68 | }
69 | function handleSetupResult(instance, setupResult: any) {
70 | if(typeof setupResult === 'object') {
71 | instance.setupState = proxyRefs(setupResult)
72 | }
73 | finishComponentSetup(instance)
74 | }
75 |
76 | function finishComponentSetup(instance: any) {
77 | const Component = instance.type
78 | if(compiler && !Component.render) {
79 | if(Component.template) {
80 | Component.render = compiler(Component.template)
81 | }
82 | }
83 | instance.render = Component.render
84 | }
85 |
86 | export function getCurrentInstance() {
87 | return currentInstance
88 | }
89 |
90 | export function setCurrentInstance(instance) {
91 | currentInstance = instance
92 | }
93 |
94 | export function unsetCurrentInstance() {
95 | currentInstance = null
96 | }
97 |
98 | let compiler
99 |
100 | export function registerRuntimeCompiler(_compiler) {
101 | compiler = _compiler
102 | }
103 |
--------------------------------------------------------------------------------
/src/runtime-core/apiWatch2.ts:
--------------------------------------------------------------------------------
1 | import { ReactiveEffect } from "../reactivity/effect"
2 | import { isReactive } from "../reactivity/reactive"
3 | import { isRef } from "../reactivity/ref"
4 | import { isFunction, isObject, isPlainObject } from "../shared"
5 | import { currentInstance } from "./component"
6 | import { queuePostFlushCb, queuePreFlushCb } from "./scheduler"
7 |
8 | export function watch(
9 | source,
10 | cb,
11 | { immediate, deep, flush }: any
12 | ) {
13 |
14 | const instance = currentInstance as any
15 | let getter
16 | if (isRef(source)) {
17 | // 如果是ref类型
18 | getter = () => source.value
19 | } else if (isReactive(source)) {
20 | // 如果是reactive类型
21 | getter = () => source
22 | // 深度监听为true
23 | deep = true
24 | } else if (Array.isArray(source)) {
25 | // 如果是数组,进行循环处理
26 | getter = () =>
27 | source.map(s => {
28 | if (isRef(s)) {
29 | return s.value
30 | } else if (isReactive(s)) {
31 | return traverse(s)
32 | } else if (isFunction(s)) {
33 | return s()
34 | }
35 | })
36 | } else if (isFunction(source)) {
37 | // 如果是函数
38 | getter = () => source()
39 | }
40 |
41 | if (cb && deep) {
42 | // 如果有回调函数并且深度监听为true,那么就通过traverse函数进行深度递归监听
43 | const baseGetter = getter
44 | getter = () => traverse(baseGetter())
45 | }
46 |
47 | // 定义老值
48 | let oldValue
49 | // 提取 scheduler 调度函数为一个独立的 job 函数
50 | const job = () => {
51 | // 在scheduler中重新执行effect实例对象的run方法,得到的是新值
52 | const newValue = effect.run()
53 | // 将新值和旧值作为回调函数的参数
54 | cb(newValue, oldValue)
55 | // 更新旧值,不然下一次会得到错误的旧值
56 | oldValue = newValue
57 | }
58 |
59 | let scheduler
60 | if (flush === 'sync') {
61 | scheduler = job // 同步执行
62 | } else if (flush === 'post') {
63 | // 将job函数放到微任务队列中,从而实现异步延迟执行,注意 post 是在 DOM 更新之后再执行
64 | scheduler = () => queuePostFlushCb(job)
65 | } else {
66 | // flush默认为:'pre'
67 | scheduler = () => {
68 | if (!instance || instance.isMounted) {
69 | // 在组件更新之前执行
70 | queuePreFlushCb(job)
71 | } else {
72 | // 使用“pre”选项,第一次调用必须在安装组件之前进行,以便同步调用。
73 | job()
74 | }
75 | }
76 | }
77 |
78 | const effect = new ReactiveEffect(getter, scheduler)
79 |
80 | if (immediate) {
81 | // 当 immediate 为 true 时立即执行 job,从而触发回调函数执行
82 | job()
83 | } else {
84 | // 手动执行effect实例对象的run方法,拿到的值就是旧值
85 | oldValue = effect.run()
86 | }
87 | }
88 |
89 | export function traverse(value: unknown, seen?: Set) {
90 | // 如果是普通类型或者不是响应式的对象就直接返回
91 | if (!isObject(value)) {
92 | return value
93 | }
94 | seen = seen || new Set()
95 | if (seen.has(value)) {
96 | // 如果已经读取过就返回
97 | return value
98 | }
99 | // 读取了就添加到集合中,代表遍历地读取过了,避免循环引用引起死循环
100 | seen.add(value)
101 | if (isRef(value)) {
102 | // 如果是ref类型,继续递归执行.value值
103 | // traverse(value.value, seen)
104 | } else if (Array.isArray(value)) {
105 | // 如果是数组类型
106 | for (let i = 0; i < value.length; i++) {
107 | // 递归调用traverse进行处理
108 | traverse(value[i], seen)
109 | }
110 | } else if (isPlainObject(value)) {
111 | // 如果是对象,使用for in 读取对象的每一个值,并递归调用traverse进行处理
112 | for (const key in value) {
113 | traverse((value as any)[key], seen)
114 | }
115 | }
116 | return value
117 | }
--------------------------------------------------------------------------------
/src/compiler-core/src/codegen.ts:
--------------------------------------------------------------------------------
1 | import { isString } from "../../shared"
2 | import { NodeTypes } from "./ast"
3 | import { CREATE_ELEMENT_VNODE, helperMapName, TO_DISPLAY_STRING } from "./runtimeHelpers"
4 |
5 | export function generate(ast) {
6 | const context = createCodegenContext()
7 | const { push } = context
8 | genFunctionPreamble(ast, context)
9 | const functionName = 'render'
10 | const args = ['_ctx', '_cache']
11 | const signature = args.join(', ')
12 | push(`function ${functionName}(${signature}){`)
13 | push(`return`)
14 | genNode(ast.codegenNode, context)
15 | push(`}`)
16 | return {
17 | code: context.code
18 | }
19 | }
20 |
21 | function genFunctionPreamble(ast, context) {
22 | const { push } = context
23 | const VueBinging = "Vue"
24 | const aliasHelper = (s) => `${helperMapName[s]}:_${helperMapName[s]}`
25 | if(ast.helpers.length > 0) {
26 | push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinging}`)
27 | }
28 | push(`\r\n`)
29 | push('return ')
30 | }
31 |
32 | function genNode(node, context) {
33 | switch(node.type) {
34 | // 文本
35 | case NodeTypes.TEXT:
36 | genText(node, context)
37 | break;
38 | // 插值
39 | case NodeTypes.INTERPOLATION:
40 | genInterpolation(node, context)
41 | break;
42 | // 表达式
43 | case NodeTypes.SIMPLE_EXPRESSION:
44 | genExpression(node, context)
45 | break;
46 | // 元素
47 | case NodeTypes.ELEMENT:
48 | genElement(node, context)
49 | break;
50 | // 复合类型
51 | case NodeTypes.COMPOUND_EXPRESSION:
52 | genCompoundExpression(node, context)
53 | break;
54 | default:
55 | break;
56 | }
57 | }
58 |
59 | function genCompoundExpression(node, context) {
60 | const { push } = context
61 | const children = node.children
62 | for(let i = 0; i < children.length; i++) {
63 | const child = children[i]
64 | if(isString(child)) {
65 | push(child)
66 | } else {
67 | genNode(child, context)
68 | }
69 | }
70 | }
71 |
72 | function genElement(node, context) {
73 | const { push, helper } = context
74 | const { tag, children, props } = node
75 | push(`${helper(CREATE_ELEMENT_VNODE)}(`)
76 | genNodeList(genNullable([tag, props, children]), context)
77 | push(')')
78 | }
79 |
80 | function genNodeList(nodes, context) {
81 | const { push } = context
82 | for(let i = 0; i < nodes.length; i++) {
83 | const node = nodes[i]
84 | if(isString(node)) {
85 | push(node)
86 | } else {
87 | genNode(node, context)
88 | }
89 | if(i < nodes.length -1) {
90 | push(', ')
91 | }
92 | }
93 | }
94 |
95 | function genNullable(args: any) {
96 | return args.map(arg => arg || 'null')
97 | }
98 |
99 | function genExpression(node, context) {
100 | const { push } = context
101 | push(`${node.content}`)
102 | }
103 |
104 | function genInterpolation(node, context) {
105 | const { push, helper } = context
106 | push(`${helper(TO_DISPLAY_STRING)}(`)
107 | genNode(node.content, context)
108 | push(')')
109 | }
110 |
111 | function genText(node, context) {
112 | const { push } = context
113 | push(` '${node.content}'`)
114 | }
115 |
116 | function createCodegenContext() {
117 | const context = {
118 | code: '',
119 | push(source){
120 | context.code += source
121 | },
122 | helper(key){
123 | return ` _${helperMapName[key]}`
124 | }
125 | }
126 | return context
127 | }
128 |
--------------------------------------------------------------------------------
/src/reactivity/effect.ts:
--------------------------------------------------------------------------------
1 | import { extend } from "../shared"
2 |
3 | // 记录当前活跃的对象
4 | let activeEffect
5 | // 标记是否追踪
6 | let shouldTrack
7 | // 用于依赖收集
8 | export class ReactiveEffect{
9 | private _fn: any
10 | deps = [] // 所有依赖这个 effect 的响应式对象
11 | active = true // 是否为激活状态
12 | onStop?: () => void
13 | // public scheduler? 显式声明类可选属性
14 | constructor(fn, public scheduler?) {
15 | this._fn = fn
16 | }
17 | run() {
18 | // 运行 run 的时候,可以控制 要不要执行后续收集依赖的一步
19 | // 目前来看的话,只要执行了 fn 那么就默认执行了收集依赖
20 | // 这里就需要控制了
21 |
22 | // 是不是收集依赖的变量
23 |
24 | // 执行 fn 但是不收集依赖
25 | if(!this.active) {
26 | return this._fn()
27 | }
28 | // 执行 fn 收集依赖
29 | // 可以开始收集依赖了
30 | shouldTrack = true
31 | // 执行的时候给全局的 activeEffect 赋值
32 | // 利用全局属性来获取当前的 effect
33 | activeEffect = this
34 | // 执行用户传入的 fn
35 | const result = this._fn()
36 | // 重置
37 | shouldTrack = false
38 | return result
39 | }
40 | stop() {
41 | if(this.active) {
42 | // 如果第一次执行 stop 后 active 就 false 了
43 | // 这是为了防止重复的调用,执行 stop 逻辑
44 | cleanupEffect(this)
45 | if(this.onStop) {
46 | this.onStop()
47 | }
48 | this.active = false
49 | }
50 | }
51 | }
52 |
53 | function cleanupEffect(effect) {
54 | // 找到所有依赖这个 effect 的响应式对象
55 | // 从这些响应式对象里面把 effect 给删除掉
56 | effect.deps.forEach(dep => {
57 | dep.delete(effect)
58 | })
59 | }
60 |
61 | const targetMap = new Map()
62 | export function track(target, key) {
63 | if(!isTacking()) return
64 |
65 | // 1. 先基于 target 找到对应的 dep
66 | // 如果是第一次的话,那么就需要初始化
67 | let depsMap = targetMap.get(target)
68 | if(!depsMap) {
69 | // 初始化 depsMap 的逻辑
70 | depsMap = new Map()
71 | targetMap.set(target, depsMap)
72 | }
73 | let dep = depsMap.get(key)
74 | if(!dep) {
75 | dep = new Set()
76 | depsMap.set(key, dep)
77 | }
78 |
79 | if(dep.has(activeEffect)) return
80 | trackEffect(dep)
81 | }
82 |
83 | export function trackEffect(dep) {
84 | // 单纯reactive触发依赖收集,不会有Effect实例
85 |
86 | // 用 dep 来存放所有的 effect
87 |
88 | // TODO
89 | // 这里是一个优化点
90 | // 先看看这个依赖是不是已经收集了,
91 | // 已经收集的话,那么就不需要在收集一次了
92 | // 可能会影响 code path change 的情况
93 | // 需要每次都 cleanupEffect
94 | dep.add(activeEffect)
95 | activeEffect.deps.push(dep)
96 | }
97 |
98 | export function isTacking() {
99 | return activeEffect && shouldTrack
100 | }
101 |
102 | export function trigger(target, key) {
103 | const depsMap = targetMap.get(target)
104 | const deps = depsMap && depsMap.get(key)
105 | triggerEffect(deps)
106 | }
107 |
108 | export function triggerEffect(deps) {
109 | if(deps) {
110 |
111 | deps.forEach(effect => {
112 | if(effect.scheduler){
113 | effect.scheduler()
114 | } else {
115 | effect.run()
116 | }
117 | });
118 |
119 | // for(const effect of deps){console.log('effect', effect)
120 | // if(effect.scheduler){
121 | // effect.scheduler()
122 | // } else {
123 | // effect.run()
124 | // }
125 | // }
126 |
127 | }
128 | }
129 |
130 | export function effect(fn, options: any = {}) {
131 | const scheduler = options.scheduler
132 | const _effect = new ReactiveEffect(fn, scheduler)
133 | // 把用户传过来的值合并到 _effect 对象上去
134 | // 缺点就是不是显式的,看代码的时候并不知道有什么值
135 | extend(_effect, options)
136 | // _effect.onStop = options.onStop
137 | _effect.run()
138 | // 把 _effect.run 这个方法返回
139 | // 让用户可以自行选择调用的时机(调用 fn)
140 | const runner: any = _effect.run.bind(_effect)
141 | runner.effect = _effect
142 | return runner
143 | }
144 |
145 | export function stop(runner) {
146 | runner.effect.stop()
147 | }
--------------------------------------------------------------------------------
/src/runtime-core/apiWatch.ts:
--------------------------------------------------------------------------------
1 | import { ReactiveEffect } from "../reactivity/effect";
2 | import { isReactive } from "../reactivity/reactive";
3 | import { isRef } from "../reactivity/ref";
4 | import { isFunction, isObject, isPlainObject } from "../shared";
5 | import { currentInstance } from "./component";
6 | import { queuePostFlushCb, queuePreFlushCb } from "./scheduler";
7 |
8 | export function watchEffect(
9 | effect,
10 | options?
11 | ) {
12 | return doWatch(effect, null, options)
13 | }
14 |
15 | export function watchPostEffect(
16 | effect,
17 | options?
18 | ) {
19 | return doWatch(effect, null, { flush: 'post' })
20 | }
21 |
22 | export function watchSyncEffect(
23 | effect,
24 | options?
25 | ) {
26 | return doWatch(effect, null, { flush: 'sync' })
27 | }
28 |
29 | export function watch(
30 | source,
31 | cb,
32 | options
33 | ) {
34 | return doWatch(source as any, cb, options)
35 | }
36 |
37 | function doWatch(
38 | source,
39 | cb,
40 | { immediate, deep, flush }: any
41 | ) {
42 | const instance = currentInstance as any
43 | let getter
44 | if (isRef(source)) {
45 | // 如果是ref类型
46 | getter = () => source.value
47 | } else if (isReactive(source)) {
48 | // 如果是reactive类型
49 | getter = () => source
50 | // 深度监听为true
51 | deep = true
52 | } else if (Array.isArray(source)) {
53 | // 如果是数组
54 |
55 | } else if (isFunction(source)) {
56 | if (cb) {
57 | // 如果是数组并且有回调函数
58 | getter = () => source()
59 | } else {
60 | // 没有回调函数
61 | getter = () => {
62 | return source()
63 | }
64 | }
65 | }
66 |
67 | if (cb && deep) {
68 | // 如果有回调函数并且深度监听为true,那么就通过traverse函数进行深度递归监听
69 | const baseGetter = getter
70 | getter = () => traverse(baseGetter())
71 | }
72 |
73 | // oldValue默认值处理,如果watch的第一个参数是数组,那么oldValue也是一个数组
74 | let oldValue
75 | const job = () => {
76 | // 如果effect已经失效则什么都不做
77 | if (!effect.active) {
78 | return
79 | }
80 | if (cb) {
81 | // 如果有回调函数
82 | // 执行effect.run获取新值
83 | const newValue = effect.run()
84 | if (deep) {
85 | // 执行回调函数
86 | // 第一次执行的时候,旧值是undefined,这是符合预期的
87 | cb(newValue, oldValue)
88 | // 把新值赋值给旧值
89 | oldValue = newValue
90 | }
91 | } else {
92 | // 没有回调函数则是watchEffect走的分支
93 | effect.run()
94 | }
95 | }
96 |
97 | let scheduler
98 | if (flush === 'sync') {
99 | scheduler = job as any // 同步执行
100 | } else if (flush === 'post') {
101 | // 将job函数放到微任务队列中,从而实现异步延迟执行,注意post是在DOM更新之后再执行
102 | scheduler = () => queuePostFlushCb(job)
103 | } else {
104 | // flush默认为:'pre'
105 | scheduler = () => {
106 | if (!instance || instance.isMounted) {
107 | // 在组件更新之后执行
108 | queuePreFlushCb(job)
109 | } else {
110 | // 使用“pre”选项,第一次调用必须在安装组件之前进行,以便同步调用
111 | job()
112 | }
113 | }
114 | }
115 |
116 | const effect = new ReactiveEffect(getter, scheduler)
117 |
118 | // 初始化
119 | if (cb) {
120 | if (immediate) {
121 | job()
122 | } else {
123 | oldValue = effect.run()
124 | }
125 | } else if (flush === 'post') {
126 | queuePostFlushCb(effect.run.bind(effect))
127 | } else {
128 | effect.run()
129 | }
130 |
131 | return () => {
132 | effect.stop()
133 | }
134 | }
135 |
136 | export function traverse(value: unknown, seen?: Set) {
137 | // 如果是普通类型或者不是响应式的对象就直接返回,ReactiveFlags.SKIP表示不需要响应式的对象
138 | if (!isObject(value)) {
139 | return value
140 | }
141 | seen = seen || new Set()
142 | if (seen.has(value)) {
143 | // 如果已经读取过就返回
144 | return value
145 | }
146 | // 读取了就添加到集合中,代表遍历地读取过了,避免循环引用引起死循环
147 | seen.add(value)
148 | if (isRef(value)) {
149 | // 如果是ref类型,继续递归执行.value值
150 | // traverse(value.value, seen)
151 | } else if (Array.isArray(value)) {
152 | // 如果是数组类型
153 | for (let i = 0; i < value.length; i++) {
154 | // 递归调用traverse进行处理
155 | traverse(value[i], seen)
156 | }
157 | } else if (isPlainObject(value)) {
158 | // 如果是对象,使用for in 读取对象的每一个值,并递归调用traverse进行处理
159 | for (const key in value) {
160 | traverse((value as any)[key], seen)
161 | }
162 | }
163 | return value
164 | }
--------------------------------------------------------------------------------
/md/Vue3 模板引用 ref 的实现原理.md:
--------------------------------------------------------------------------------
1 | # Vue3 模板引用 ref 的实现原理
2 |
3 | ### 什么是模板引用 `ref` ?
4 |
5 | 有时候我们可以使用 `ref` attribute 为子组件或 HTML 元素指定引用 ID。
6 |
7 | ```html
8 |
9 |
10 |
11 |
25 | ```
26 |
27 | 这里我们在渲染上下文中暴露 `input`,并通过 `ref="input"`,将其绑定到 input 作为其 ref。在虚拟 DOM 补丁算法中,如果 VNode 的 `ref` 键对应于渲染上下文中的 ref,则 VNode 的相应元素或组件实例将被分配给该 ref 的值。这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。
28 |
29 | ### 设置当前渲染的实例对象
30 |
31 | 我们知道我们写的这个组件在运行的时候,先会创建一个组件实例对象`instance`,再通过运行这个组件实例对象的`render`方法获取这个组件的虚拟DOM,然后再进行`patch`,渲染出真实DOM。
32 |
33 | 在运行组件实例对象的render方法之前,会先设置保存正在渲染的组件实例对象`currentRenderingInstance`
34 |
35 | renderComponentRoot方法
36 |
37 | ```javascript
38 | import { setCurrentRenderingInstance } from "./componentRenderContext";
39 |
40 | export function renderComponentRoot(
41 | instance
42 | ) {
43 | const { proxy, render } = instance
44 | let result
45 | // 返回上一个实例对象
46 | const prev = setCurrentRenderingInstance(instance)
47 | result = render.call(proxy)
48 | // 再设置当前的渲染对象上一个,具体场景是嵌套循环渲染的时候,渲染完子组件,再去渲染父组件
49 | setCurrentRenderingInstance(prev)
50 | return result
51 | }
52 | ```
53 |
54 | setCurrentRenderingInstance方法
55 |
56 | ```javascript
57 | export let currentRenderingInstance = null
58 |
59 | export function setCurrentRenderingInstance(instance) {
60 | const prev = currentRenderingInstance
61 | currentRenderingInstance = instance
62 | return prev
63 | }
64 | ```
65 |
66 | ### 设置元素或者组件的props中的ref
67 |
68 | 在获取组件的虚拟DOM的时候,其实是通过createVNode来创建的虚拟DOM,在创建的虚拟DOM的时候会保存当前前渲染的实例对象到当前元素或者组件的props中的ref中。
69 |
70 | ```javascript
71 | export function createVNode(type, props?, children?) {
72 | const vnode = {
73 | type,
74 | props,
75 | ref: props && normalizeRef(props), // 创建虚拟DOM的时候设置ref
76 | children,
77 | component: null,
78 | key: props && props.key,
79 | shapeFlag: getShapeFlag(type),
80 | el: null
81 | }
82 | return vnode
83 | }
84 | ```
85 |
86 | 我们来看看normalizeRef函数做了什么
87 |
88 | ```javascript
89 | import { currentRenderingInstance } from "./componentRenderContext"
90 | const normalizeRef = ({
91 | ref
92 | }) => {
93 | return (
94 | ref != null
95 | ? isString(ref) || isRef(ref) || isFunction(ref)
96 | ? { i: currentRenderingInstance, r: ref}
97 | : ref
98 | : null
99 | ) as any
100 | }
101 | ```
102 |
103 | 我们可以看到normalizeRef函数最主要是把当前的渲染实例对象`currentRenderingInstance`保存起来了。
104 |
105 | ### 模板引用的赋值
106 |
107 | 我们在上面开头的时候已经说了,模板引用ref只会在初始渲染之后获得。那么具体在源码中的位置是patch函数的底部,也就是把虚拟DOM进行patch渲染之后,再设置模版引用ref。
108 |
109 | ```javascript
110 | function patch(n1, n2, container: any, parentComponent, anchor) {
111 | // 基于 n2 的类型来判断
112 | // 因为 n2 是新的 vnode
113 | const { type, shapeFlag, ref } = n2
114 |
115 | // Fragment => 只渲染 children
116 | switch (type) {
117 | // 其中还有几个类型比如: static fragment comment
118 | case Fragment:
119 | processFragment(n1, n2, container, parentComponent, anchor)
120 | break
121 | case Text:
122 | processText(n1, n2, container)
123 | break
124 | default:
125 | // 这里就基于 shapeFlag 来处理
126 | if (shapeFlag & ShapeFlags.ELEMENT) {
127 | // 处理 element
128 | processElement(n1, n2, container, parentComponent, anchor)
129 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
130 | // 处理 component
131 | processComponent(n1, n2, container, parentComponent, anchor)
132 | }
133 | break
134 | }
135 |
136 | // 模板引用ref只会在初始渲染之后获得
137 | if (ref != null && parentComponent) {
138 | setRef(ref, n2 || n1, !n2)
139 | }
140 | }
141 | ```
142 |
143 | 我们再看看setRef函数中干了什么事情。
144 |
145 | ```javascript
146 | export function setRef(
147 | rawRef,
148 | vnode,
149 | isUnmount = false
150 | ) {
151 | // 判断如果是组件实例,则把改组件实例作为ref的值,否则就是把该元素作为ref值
152 | const refValue =
153 | vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
154 | ? vnode.component!.proxy
155 | : vnode.el
156 | // 如果n2不存在则是卸载
157 | const value = isUnmount ? null : refValue
158 | // 把在创建虚拟DOM的时候设置保存的组件渲染实例和ref键值解构出来
159 | const { i: owner, r: ref } = rawRef
160 |
161 | const setupState = owner.setupState
162 | // happy path中我们只考虑最简单的情况
163 | const _isString = isString(ref)
164 |
165 | if (_isString) {
166 | // 如果在对应于渲染上下文中存在ref键值,则 VNode 的相应元素或组件实例将被分配给该 ref 的值
167 | if (hasOwn(setupState, ref)) {
168 | setupState[ref] = value
169 | }
170 | }
171 | }
172 | ```
173 |
174 | 模版引用ref的赋值具体就是在setRef函数中实现的。判断如果是组件实例,则把改组件实例作为ref的值,否则就是把该元素作为ref值,再把在创建虚拟DOM的时候设置保存的组件渲染实例和ref键值解构出来,再判断如果在对应于渲染上下文中存在ref键值,则 VNode 的相应元素或组件实例将被分配给该 ref 的值。
--------------------------------------------------------------------------------
/example/patchChildren/ArrayToArray.js:
--------------------------------------------------------------------------------
1 | // TODO
2 |
3 | import { ref, h } from "../../lib/mini-vue.esm.js";
4 | // 1. 左侧的对比
5 | // (a b) c
6 | // (a b) d e
7 | // const prevChildren = [
8 | // h("p", { key: "A" }, "A"),
9 | // h("p", { key: "B" }, "B"),
10 | // h("p", { key: "C" }, "C"),
11 | // ];
12 | // const nextChildren = [
13 | // h("p", { key: "A" }, "A"),
14 | // h("p", { key: "B" }, "B"),
15 | // h("p", { key: "D" }, "D"),
16 | // h("p", { key: "E" }, "E"),
17 | // ];
18 |
19 | // 2. 右侧的对比
20 | // a (b c)
21 | // d e (b c)
22 | // const prevChildren = [
23 | // h("p", { key: "A" }, "A"),
24 | // h("p", { key: "B" }, "B"),
25 | // h("p", { key: "C" }, "C"),
26 | // ];
27 | // const nextChildren = [
28 | // h("p", { key: "D" }, "D"),
29 | // h("p", { key: "E" }, "E"),
30 | // h("p", { key: "B" }, "B"),
31 | // h("p", { key: "C" }, "C"),
32 | // ];
33 |
34 | // 3. 新的比老的长
35 | // 创建新的
36 | // 左侧
37 | // (a b)
38 | // (a b) c
39 | // i = 2, e1 = 1, e2 = 2
40 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
41 | // const nextChildren = [
42 | // h("p", { key: "A" }, "A"),
43 | // h("p", { key: "B" }, "B"),
44 | // h("p", { key: "C" }, "C"),
45 | // h("p", { key: "D" }, "D"),
46 | // ];
47 |
48 | // 右侧
49 | // (a b)
50 | // c (a b)
51 | // i = 0, e1 = -1, e2 = 0
52 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
53 | // const nextChildren = [
54 | // h("p", { key: "C" }, "C"),
55 | // h("p", { key: "A" }, "A"),
56 | // h("p", { key: "B" }, "B"),
57 | // ];
58 |
59 | // 4. 老的比新的长
60 | // 删除老的
61 | // 左侧
62 | // (a b) c
63 | // (a b)
64 | // i = 2, e1 = 2, e2 = 1
65 | // const prevChildren = [
66 | // h("p", { key: "A" }, "A"),
67 | // h("p", { key: "B" }, "B"),
68 | // h("p", { key: "C" }, "C"),
69 | // ];
70 | // const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
71 |
72 | // 右侧
73 | // a (b c)
74 | // (b c)
75 | // i = 0, e1 = 0, e2 = -1
76 |
77 | // const prevChildren = [
78 | // h("p", { key: "A" }, "A"),
79 | // h("p", { key: "B" }, "B"),
80 | // h("p", { key: "C" }, "C"),
81 | // ];
82 | // const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")];
83 |
84 | // 5. 对比中间的部分
85 | // 1. 创建新的 (在老的里面不存在,新的里面存在)
86 | // 2. 删除老的 (在老的里面存在,新的里面不存在)
87 | // 3. 移动 (节点存在于新的和老的里面,但是位置变了)
88 | // - 使用最长子序列来优化
89 |
90 | // 5.1
91 | // a,b,(c,d),f,g
92 | // a,b,(e,c),f,g
93 | // D 节点在新的里面是没有的 - 需要删除掉
94 | // C 节点 props 也发生了变化
95 |
96 | // const prevChildren = [
97 | // h("p", { key: "A" }, "A"),
98 | // h("p", { key: "B" }, "B"),
99 | // h("p", { key: "C", id: "c-prev" }, "C"),
100 | // h("p", { key: "D" }, "D"),
101 | // h("p", { key: "F" }, "F"),
102 | // h("p", { key: "G" }, "G"),
103 | // ];
104 |
105 | // const nextChildren = [
106 | // h("p", { key: "A" }, "A"),
107 | // h("p", { key: "B" }, "B"),
108 | // h("p", { key: "E" }, "E"),
109 | // h("p", { key: "C", id:"c-next" }, "C"),
110 | // h("p", { key: "F" }, "F"),
111 | // h("p", { key: "G" }, "G"),
112 | // ];
113 |
114 | // 5.1.1
115 | // a,b,(c,e,d),f,g
116 | // a,b,(e,c),f,g
117 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑)
118 | // const prevChildren = [
119 | // h("p", { key: "A" }, "A"),
120 | // h("p", { key: "B" }, "B"),
121 | // h("p", { key: "C", id: "c-prev" }, "C"),
122 | // h("p", { key: "E" }, "E"),
123 | // h("p", { key: "D" }, "D"),
124 | // h("p", { key: "F" }, "F"),
125 | // h("p", { key: "G" }, "G"),
126 | // ];
127 | // const nextChildren = [
128 | // h("p", { key: "A" }, "A"),
129 | // h("p", { key: "B" }, "B"),
130 | // h("p", { key: "E" }, "E"),
131 | // h("p", { key: "C", id:"c-next" }, "C"),
132 | // h("p", { key: "F" }, "F"),
133 | // h("p", { key: "G" }, "G"),
134 | // ];
135 |
136 | // 2 移动 (节点存在于新的和老的里面,但是位置变了)
137 |
138 | // 2.1
139 | // a,b,(c,d,e),f,g
140 | // a,b,(e,c,d),f,g
141 | // 最长子序列: [1,2]
142 |
143 | // const prevChildren = [
144 | // h("p", { key: "A" }, "A"),
145 | // h("p", { key: "B" }, "B"),
146 | // h("p", { key: "C" }, "C"),
147 | // h("p", { key: "D" }, "D"),
148 | // h("p", { key: "E" }, "E"),
149 | // h("p", { key: "F" }, "F"),
150 | // h("p", { key: "G" }, "G"),
151 | // ];
152 |
153 | // const nextChildren = [
154 | // h("p", { key: "A" }, "A"),
155 | // h("p", { key: "B" }, "B"),
156 | // h("p", { key: "E" }, "E"),
157 | // h("p", { key: "C" }, "C"),
158 | // h("p", { key: "D" }, "D"),
159 | // h("p", { key: "F" }, "F"),
160 | // h("p", { key: "G" }, "G"),
161 | // ];
162 |
163 | // 3. 创建新的节点
164 | // a,b,(c,e),f,g
165 | // a,b,(e,c,d),f,g
166 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建
167 | // const prevChildren = [
168 | // h("p", { key: "A" }, "A"),
169 | // h("p", { key: "B" }, "B"),
170 | // h("p", { key: "C" }, "C"),
171 | // h("p", { key: "E" }, "E"),
172 | // h("p", { key: "F" }, "F"),
173 | // h("p", { key: "G" }, "G"),
174 | // ];
175 |
176 | // const nextChildren = [
177 | // h("p", { key: "A" }, "A"),
178 | // h("p", { key: "B" }, "B"),
179 | // h("p", { key: "E" }, "E"),
180 | // h("p", { key: "C" }, "C"),
181 | // h("p", { key: "D" }, "D"),
182 | // h("p", { key: "F" }, "F"),
183 | // h("p", { key: "G" }, "G"),
184 | // ];
185 |
186 | // 综合例子
187 | // a,b,(c,d,e,z),f,g
188 | // a,b,(d,c,y,e),f,g
189 |
190 | const prevChildren = [
191 | h("p", { key: "A" }, "A"),
192 | h("p", { key: "B" }, "B"),
193 | h("p", { key: "C" }, "C"),
194 | h("p", { key: "D" }, "D"),
195 | h("p", { key: "E" }, "E"),
196 | h("p", { key: "Z" }, "Z"),
197 | h("p", { key: "F" }, "F"),
198 | h("p", { key: "G" }, "G"),
199 | ];
200 |
201 | const nextChildren = [
202 | h("p", { key: "A" }, "A"),
203 | h("p", { key: "B" }, "B"),
204 | h("p", { key: "D" }, "D"),
205 | h("p", { key: "C" }, "C"),
206 | h("p", { key: "Y" }, "Y"),
207 | h("p", { key: "E" }, "E"),
208 | h("p", { key: "F" }, "F"),
209 | h("p", { key: "G" }, "G"),
210 | ];
211 |
212 | export default {
213 | name: "ArrayToArray",
214 | setup() {
215 | const isChange = ref(false);
216 | window.isChange = isChange;
217 |
218 | return {
219 | isChange,
220 | };
221 | },
222 | render() {
223 | const self = this;
224 |
225 | return self.isChange === true
226 | ? h("div", {}, nextChildren)
227 | : h("div", {}, prevChildren);
228 | },
229 | };
--------------------------------------------------------------------------------
/src/compiler-core/src/parse.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "./ast"
2 |
3 | const enum TagType {
4 | Start,
5 | End
6 | }
7 |
8 | export function baseParse (content: string) {
9 | const context = createParserContext(content)
10 | return createRoot(parseChildren(context, []))
11 | }
12 |
13 | function parseChildren(context, ancestors) {
14 | const nodes: any = []
15 | while(!isEnd(context, ancestors)) {
16 | let node
17 | const s = context.source
18 | if(s.startsWith("{{")) {
19 | node = parseInterpolation(context)
20 | } else if(s[0] === '<') {
21 | if(/[a-z]/i.test(s[1])) {
22 | node = parseElement(context, ancestors)
23 | }
24 |
25 | // 注释
26 | }
27 |
28 | if(!node) {
29 | node = parseText(context)
30 | }
31 |
32 | nodes.push(node)
33 | }
34 | return nodes
35 | }
36 |
37 | function isEnd(context, ancestors) {
38 | const s = context.source
39 | if(s.startsWith('')) {
40 | for(let i = ancestors.length -1; i >= 0; i--) {
41 | const tag = ancestors[i].tag
42 | if(startsWithEndTagOpen(s, tag)) {
43 | return true
44 | }
45 | }
46 | }
47 |
48 | return !s
49 | }
50 |
51 | function parseText(context: any): any {
52 | let endIndex = context.source.length
53 | let endTokens = ["<","{{"]
54 | for(let i = 0; i < endTokens.length; i++) {
55 | const index = context.source.indexOf(endTokens[i])
56 | if(index !== -1 && endIndex > index) {
57 | endIndex = index
58 | }
59 | }
60 |
61 | const content = parseTextData(context, endIndex)
62 | return {
63 | type: NodeTypes.TEXT,
64 | content
65 | }
66 | }
67 |
68 | function parseTextData(context: any, length) {
69 | const content = context.source.slice(0, length)
70 | advanceBy(context, length)
71 | return content
72 | }
73 |
74 | function parseElement(context: any, ancestors) {
75 | const element: any = parseTag(context, TagType.Start)
76 | ancestors.push(element)
77 | element.children = parseChildren(context, ancestors)
78 | ancestors.pop()
79 | if(startsWithEndTagOpen(context.source, element.tag)) {
80 | parseTag(context, TagType.End)
81 | } else {
82 | throw new Error(`缺少结束标签:${element.tag}`)
83 | }
84 | return element
85 | }
86 |
87 | function startsWithEndTagOpen(source, tag) {
88 | return source.startsWith('') && source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
89 | }
90 |
91 | function parseTag(context, type: TagType) {
92 | // 解析 tag
93 | const match: any = /^<\/?([a-z]*)/i.exec(context.source)
94 | const tag = match[1]
95 | // 删除处理完的代码
96 | advanceBy(context, match[0].length)
97 |
98 | // 解析属性
99 | const props = parseAttributes(context)
100 |
101 | advanceBy(context, 1)
102 |
103 |
104 |
105 | if(type === TagType.End) {
106 | return
107 | }
108 |
109 | return {
110 | type: NodeTypes.ELEMENT,
111 | props,
112 | tag
113 | }
114 | }
115 |
116 | function parseAttributes(context) {
117 | const props: any = []
118 | while(!context.source.startsWith('>') && !context.source.startsWith('/>')) {
119 | // 该正则用于匹配属性名称
120 | const match: any = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)
121 | // 得到属性名称
122 | const name = match[0]
123 | // 消费属性名称
124 | advanceBy(context, name.length)
125 | // 消费属性名称与等于号之间的空白字符
126 | advanceSpaces(context)
127 | // 消费等于号
128 | advanceBy(context, 1)
129 | // 消费等于号与属性值之间的空白字符
130 | advanceSpaces(context)
131 | // 属性值
132 | let value = ''
133 | // 获取当前模板内容的第一个字符
134 | const quote = context.source[0]
135 | // 判断属性值是否被引号引用
136 | const isQuoted = quote === '"' || quote === "'"
137 | if(isQuoted) {
138 | // 属性值被引用号引用,消费引号
139 | advanceBy(context, 1)
140 | // 获取下一个引号的索引
141 | const endQuoteIndex = context.source.indexOf(quote)
142 | if(endQuoteIndex > -1) {
143 | // 获取下一个引号之前的内容作为属性值
144 | value = context.source.slice(0, endQuoteIndex)
145 | // 消费属性值
146 | advanceBy(context, value.length)
147 | // 消费引号
148 | advanceBy(context, 1)
149 | } else {
150 | // 缺少引号
151 | throw new Error(`缺少引号`)
152 | }
153 | } else {
154 | // 代码运行到这里,说明属性值没有被引号引用
155 | // 下一个空白字符之前的内容全部作为属性值
156 | const match: any = /^[^\t\r\n\f >]+/.exec(context.source)
157 | value = match[0]
158 | // 消费属性值
159 | advanceBy(context, value.length)
160 | }
161 | // 消费属性值后面空白字符
162 | advanceSpaces(context)
163 | // 使用属性名称 + 属性值创建一个属性节点,添加到 props 数组中
164 | props.push({
165 | type: NodeTypes.ATTRIBUTE,
166 | name,
167 | value
168 | })
169 | }
170 | return props
171 | }
172 |
173 | function parseInterpolation(context) {
174 | const openDelimiter = '{{'
175 | const closeDelimiter = '}}'
176 | const closeIndex = context.source.indexOf(closeDelimiter, openDelimiter.length)
177 | advanceBy(context, openDelimiter.length)
178 | const rawContentLength = closeIndex - openDelimiter.length
179 | const rawContent = parseTextData(context, rawContentLength)
180 | const content = rawContent.trim()
181 | advanceBy(context, closeDelimiter.length)
182 |
183 | return {
184 | type: NodeTypes.INTERPOLATION,
185 | content: {
186 | type: NodeTypes.SIMPLE_EXPRESSION,
187 | content: content
188 | }
189 | }
190 | }
191 |
192 | function advanceBy(context: any, length: number) {
193 | context.source = context.source.slice(length)
194 | }
195 |
196 | function advanceSpaces(context: any) {
197 | // 匹配空白字符
198 | const match = /^[\t\r\n\f ]+/.exec(context.source)
199 | if(match) {
200 | // 调用 advanceBy 函数消费空白字符
201 | advanceBy(context, match[0].length)
202 | }
203 | }
204 |
205 | function createRoot(children) {
206 | return {
207 | children,
208 | type: NodeTypes.ROOT
209 | }
210 | }
211 |
212 | function createParserContext(content: string) {
213 | return {
214 | source: content
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/runtime-dom/directives/vModel.ts:
--------------------------------------------------------------------------------
1 | import { addEventListener } from '../modules/events'
2 | import { invokeArrayFns, isArray, isSet, toNumber } from "../../shared"
3 | import { looseEqual, looseIndexOf } from '../../shared/looseEqual'
4 |
5 | const getModelAssigner = (vnode) => {
6 | const fn = vnode.props!['onUpdate:modelValue']
7 | // 有可能存在多个更新函数,如果是多个更新函数则则封装一个高级函数进行调用处理
8 | return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
9 | }
10 |
11 | function onCompositionStart(e: Event) {
12 | // 设置 Composition 监听开关
13 | ;(e.target as any).composing = true
14 | }
15 |
16 | function onCompositionEnd(e: Event) {
17 | const target = e.target as any
18 | if (target.composing) {
19 | target.composing = false
20 | // 手动触发自定义事件
21 | trigger(target, 'input')
22 | }
23 | }
24 |
25 | function trigger(el: HTMLElement, type: string) {
26 | // 创建一个指定类型的事件
27 | const e = document.createEvent('HTMLEvents')
28 | // 初始化createEvent('HTMLEvents')返回的事件
29 | e.initEvent(type, true, true)
30 | // 触发自定义事件
31 | el.dispatchEvent(e)
32 | }
33 |
34 | export const vModelText = {
35 | created(el, { modifiers: { lazy, trim, number } }, vnode) {
36 | // 获取当前节点 props 中的 onUpdate:modelValue 更新函数
37 | el._assign = getModelAssigner(vnode)
38 | // 判断是否数字
39 | const castToNumber = number || el.type === 'number'
40 | // 监听当前节点,如果存在 lazy 修饰符则监听 change 事件否则就监听 input 事件。
41 | addEventListener(el, lazy ? 'change' : 'input', e => {
42 | // 如果存在 e.target.composing 存在则返回
43 | if ((e.target as any).composing) return
44 | let domValue = el.value
45 | if (trim) {
46 | // 如果存在 trim 修饰符则执行 trim() 方法去除字符串的头尾空格
47 | domValue = domValue.trim()
48 | } else if (castToNumber) {
49 | // 如果存在 number 修饰符或者是 number 类型的 Input 表单则把值转换成数字
50 | domValue = toNumber(domValue)
51 | }
52 | // 更新状态值,也就是用户操作 DOM 后是通过此来反向影响状态值的变化
53 | el._assign(domValue)
54 | })
55 | if (trim) {
56 | // 如果存在 trim 修饰符则监听 change 事件并且把值通过 trim 方法去除字符串的头尾空格
57 | addEventListener(el, 'change', () => {
58 | el.value = el.value.trim()
59 | })
60 | }
61 | if (!lazy) {
62 | // 利用 compositionstart 和 compositionend 监听控制中文输入的开始和结束动作
63 | addEventListener(el, 'compositionstart', onCompositionStart)
64 | addEventListener(el, 'compositionend', onCompositionEnd)
65 | addEventListener(el, 'change', onCompositionEnd)
66 | }
67 | },
68 | mounted(el, { value }) {
69 | // 更新当前节点真实 DOM 的值
70 | el.value = value == null ? '' : value
71 | },
72 | beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
73 | // 获取当前节点 props 中的 onUpdate:modelValue 更新函数
74 | el._assign = getModelAssigner(vnode)
75 | // 如果处于中文输入法的控制状态则不进行更新
76 | if ((el as any).composing) return
77 | // 通过 document.activeElement 可以获取哪个元素获取到了焦点
78 | // focus() 方法可以使某个元素获取焦点
79 | // 如果当前节点是正在被操作也就是获得了焦点就进行相关操作,主要是如果新旧值如果一样则不进行更新操作以节省性能开销
80 | if (document.activeElement === el) {
81 | if (trim && el.value.trim() === value) {
82 | return
83 | }
84 | if ((number || el.type === 'number') && toNumber(el.value) === value) {
85 | return
86 | }
87 | }
88 | const newValue = value == null ? '' : value
89 | // 判断新老值是否相同
90 | if (el.value !== newValue) {
91 | // 将状态值更新到真实 DOM 中
92 | el.value = newValue
93 | }
94 | }
95 | }
96 |
97 | export const vModelCheckbox = {
98 | created(el, _, vnode) {
99 | // 获取当前节点 props 中的 onUpdate:modelValue 更新函数
100 | el._assign = getModelAssigner(vnode)
101 | addEventListener(el, 'change', () => {
102 | // _modelValue 就是 v-model 绑定的状态数据
103 | const modelValue = (el as any)._modelValue
104 | // 获取 DOM 实例上 value 值
105 | const elementValue = getValue(el)
106 | // 选中状态
107 | const checked = el.checked
108 | const assign = el._assign
109 | // 处理 modelValue 是数组的情况
110 | if (isArray(modelValue)) {
111 | // 获取当前选项在 modelValue 数组中的位置
112 | const index = looseIndexOf(modelValue, elementValue)
113 | const found = index !== -1
114 | if (checked && !found) {
115 | // 如果是选中状态且 modelValue 里不存在当前 DOM 实例上 value 值,就往 modelValue 上添加,并且更新状态数据
116 | assign(modelValue.concat(elementValue))
117 | } else if (!checked && found) {
118 | // 如果是不是选中状态,又在 modelValue 中找到当前选项的值,则需要把当前选项的值从 modelValue 中删除,并且更新状态数据
119 | const filtered = [...modelValue]
120 | filtered.splice(index, 1)
121 | assign(filtered)
122 | }
123 | } else if (isSet(modelValue)) {
124 | // 如果是 Set 的数据类型的处理方案
125 | const cloned = new Set(modelValue)
126 | if (checked) {
127 | // 如果是选中状态则添加
128 | cloned.add(elementValue)
129 | } else {
130 | // 如果是未选中状态则删除
131 | cloned.delete(elementValue)
132 | }
133 | assign(cloned)
134 | } else {
135 | // 不是多个复选项的情况,处理的过程就跟单项选择框 Radio 一样。
136 | assign(getCheckboxValue(el, checked))
137 | }
138 | })
139 | },
140 | // 这里需要在 mounted 生命周期里初始化是因为需要等 true-value/false-value 的 props 设置完毕
141 | mounted: setChecked,
142 | beforeUpdate(el, binding, vnode) {
143 | // 获取当前节点 props 中的 onUpdate:modelValue 更新函数
144 | el._assign = getModelAssigner(vnode)
145 | // 更新过程跟初始化过程一样
146 | setChecked(el, binding, vnode)
147 | }
148 | }
149 |
150 | export const vModelRadio = {
151 | created(el, { value }, vnode) {
152 | // 给真实 DOM 的 checked 属性赋值
153 | el.checked = looseEqual(value, vnode.props!.value)
154 | // 获取当前节点 props 中的 onUpdate:modelValue 更新函数
155 | el._assign = getModelAssigner(vnode)
156 | // 单项选择只需要监听 change 事件
157 | addEventListener(el, 'change', () => {
158 | // 更新状态值,也就是用户操作 DOM 后是通过此来反向影响状态值的变化
159 | el._assign(getValue(el))
160 | })
161 | },
162 | beforeUpdate(el, { value, oldValue }, vnode) {
163 | // 获取当前节点 props 中的 onUpdate:modelValue 更新函数
164 | el._assign = getModelAssigner(vnode)
165 | // 新老值是否相等
166 | if (value !== oldValue) {
167 | // 将状态值更新到真实 DOM 中
168 | el.checked = looseEqual(value, vnode.props!.value)
169 | }
170 | }
171 | }
172 |
173 | export const vModelSelect = {
174 | created(el, { value, modifiers: { number } }, vnode) {
175 | // 判断 v-model 绑定的状态数据是否 Set 类型
176 | const isSetModel = isSet(value)
177 | addEventListener(el, 'change', () => {
178 | // 通过 Array.prototype.filter.call 方法筛选选中的选项数据,返回值是数组
179 | const selectedVal = Array.prototype.filter
180 | .call(el.options, (o: HTMLOptionElement) => o.selected)
181 | .map(
182 | (o: HTMLOptionElement) =>
183 | // 如果存在 number 修饰器则对返回值进行数字化处理
184 | number ? toNumber(getValue(o)) : getValue(o)
185 | )
186 | // 更新 v-model 绑定的状态数据
187 | el._assign(
188 | el.multiple
189 | ? isSetModel
190 | ? new Set(selectedVal) // 如果多选且是 Set 类型则返回 Set 类型数据
191 | : selectedVal // 如果是多选其是数组
192 | : selectedVal[0] // 因为上面经过处理返回的数据是数组
193 | )
194 | })
195 | // 获取当前节点 props 中的 onUpdate:modelValue 更新函数
196 | el._assign = getModelAssigner(vnode)
197 | },
198 | // 设置 value 值需要在 mounted 方法和 updated 方法中,因为需要等待子元素 option 也渲染完毕
199 | mounted(el, { value }) {
200 | setSelected(el, value)
201 | },
202 | beforeUpdate(el, _binding, vnode) {
203 | // 更新当前节点 props 中的 onUpdate:modelValue 更新函数
204 | el._assign = getModelAssigner(vnode)
205 | },
206 | updated(el, { value }) {
207 | setSelected(el, value)
208 | }
209 | }
210 |
211 | function setSelected(el: HTMLSelectElement, value: any) {
212 | // 是否多选
213 | const isMultiple = el.multiple
214 | for (let i = 0, l = el.options.length; i < l; i++) {
215 | const option = el.options[i]
216 | // 通过 getValue 函数获取 value 值,因为 select 的 option 选项的 value 也会被设置为 _value
217 | const optionValue = getValue(option)
218 | if (isMultiple) { // 多选的情况
219 | if (isArray(value)) {
220 | // 数组的情况处理
221 | option.selected = looseIndexOf(value, optionValue) > -1
222 | } else {
223 | // Set 类型的处理
224 | option.selected = value.has(optionValue)
225 | }
226 | } else { // 单选的情况
227 | if (looseEqual(getValue(option), value)) {
228 | // selectIndex 为被选中 option 元素的索引值,通过 selectIndex 可设置选中项、获取索引值、删除指定项和修改指定项文本
229 | el.selectedIndex = i
230 | return
231 | }
232 | }
233 | }
234 | // 单选时且没有选中任何 options 则把 select.selectedIndex 置为 -1
235 | if (!isMultiple) {
236 | // selectedIndex 为 -1 则没有选项被选中
237 | el.selectedIndex = -1
238 | }
239 | }
240 |
241 | function setChecked(el, { value, oldValue }, vnode) {
242 | // 把 v-model 的状态变量设置到 el._modelValue 上,相当于是一个全局变量
243 | ;(el as any)._modelValue = value
244 | if (isArray(value)) {
245 | // 如果是数组则判断 v-model 绑定是状态数据中是否存在当前复选框中的设置的 value 值
246 | el.checked = looseIndexOf(value, vnode.props!.value) > -1
247 | } else if (isSet(value)) {
248 | // 如果是 Set 数据则判断 v-model 绑定是状态数据中是否存在当前复选框中的设置的 value 值
249 | el.checked = value.has(vnode.props!.value)
250 | } else if (value !== oldValue) {
251 | // 如果是单一的复选框的情况,还需要处理使用 true-value 和 false-value 自定义 checkbox 的布尔绑定值的情况
252 | el.checked = looseEqual(value, getCheckboxValue(el, true))
253 | }
254 | }
255 |
256 | // retrieve raw value set via :value bindings
257 | function getValue(el) {
258 | return '_value' in el ? (el as any)._value : el.value
259 | }
260 |
261 | function getCheckboxValue(el, checked) {
262 | const key = checked ? '_trueValue' : '_falseValue'
263 | // 如果 _trueValue 或者 _falseValue 存在 el 实例中则使用 _trueValue 或 _falseValue 的值
264 | return key in el ? el[key] : checked
265 | }
266 |
267 |
268 |
--------------------------------------------------------------------------------
/md/关于 Vue3 源码当中的 Proxy 和 Reflect 的那些事儿.md:
--------------------------------------------------------------------------------
1 | # 关于 Vue3 源码当中的 Proxy 和 Reflect 的那些事儿
2 |
3 | ### 前言
4 |
5 | 什么是 Proxy 呢? 简单来说,使用 Proxy 可以创建一个代理对象,它允许我们拦截并重新定义对一个对象的基本操作。
6 |
7 | Proxy 只能够拦截对一个对象的基本操作,不能拦截对一个对象的复合操作。
8 |
9 | 任何在 Proxy 的拦截器中能够找到的方法,都能够在 Reflect 中找到同名函数。
10 |
11 | Reflect.get 函数还能接收第三个参数,即指定接收者 receiver,你可以把它理解为函数调用过程中的 this。
12 |
13 | 单纯 Reflect 很容易被原始的方法代替,目前也并不一定要使用 Reflect,但 Reflect + Proxy 则可以产生 1 + 1 > 2 的效果。
14 |
15 | 修改某些 Object 方法的返回结果,让其变得更规范化。如 Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false 。
16 |
17 | ### Reflect 的基本操作
18 |
19 | 对象读取操作
20 |
21 | 普通读取方式
22 |
23 | ```javascript
24 | const obj = { name: 'coboy', age: 25 }
25 | console.log(obj.name) // 'coboy'
26 | ```
27 |
28 | 使用 Reflect.get 的读取方式
29 |
30 | ```javascript
31 | console.log(Reflect.get(obj, 'name')) // coboy
32 | ```
33 |
34 | 对象设置操作
35 |
36 | 普通方式设置
37 |
38 | ```javascript
39 | obj.sex = 'boy'
40 | console.log(obj.sex) // 'boy'
41 | ```
42 |
43 | 使用 Reflect.set 的设置方式
44 |
45 | ```javascript
46 | Reflect.set(obj, 'address', '广东')
47 | console.log(obj.address) // '广东'
48 | ```
49 |
50 | 这么一看,Reflect 好像没什么特别,甚至有点画蛇添足,不急,这只是冰山一角。
51 |
52 | ### Reflect 修改某些 Object 方法的返回结果,让其变得更合理
53 |
54 | 有一些场景我们需要监测对象的属性的设置是否成功,我们在 Vue2 的源码中看到有这么一段代码:
55 |
56 | ```javascript
57 | export let supportsPassive = false
58 | if (inBrowser) {
59 | try {
60 | const opts = {}
61 | Object.defineProperty(opts, 'passive', ({
62 | get () {
63 | /* istanbul ignore next */
64 | supportsPassive = true
65 | }
66 | }: Object)) // https://github.com/facebook/flow/issues/285
67 | window.addEventListener('test-passive', null, opts)
68 | } catch (e) {}
69 | }
70 | ```
71 |
72 | 这段代码是什么意思,我们可以不用管,我们只需要注意到它使用了 `try catch` 来监听代码是否运行正常,这里主要监测的是 Object.defineProperty 的设置是否成功。那么为什么要监测 Object.defineProperty 是否设置成功呢?
73 |
74 | 是因为 `Object.defineProperty(obj, name, desc)` 在无法定义属性时,会抛出一个错误,并且会阻塞后面的代码运行。
75 |
76 | ```javascript
77 | Object.defineProperty(obj, 'like', {
78 | get() {
79 | return 'coboy'
80 | }
81 | })
82 |
83 | Object.defineProperty(obj, 'like', {
84 | get() {
85 | return 'cobyte'
86 | }
87 | })
88 | console.log('被阻塞了')
89 | ```
90 |
91 | 上面这段代码就会报错,并且后面的打印也不输出了,被阻塞了。
92 |
93 | ```javascript
94 | VM103:8 Uncaught TypeError: Cannot redefine property: like
95 | at Function.defineProperty ()
96 | at :8:8
97 | ```
98 |
99 | 那么想要不被阻塞呢就要通过 `try catch` 来捕获异常。
100 |
101 | ```javascript
102 | try {
103 | Object.defineProperty(obj, 'like', {
104 | get() {
105 | return 'coboy'
106 | }
107 | })
108 | } catch (error) {
109 | console.log(error)
110 | }
111 |
112 | try {
113 | Object.defineProperty(obj, 'like', {
114 | get() {
115 | return 'cobyte'
116 | }
117 | })
118 | } catch (error) {
119 | console.log(error)
120 | }
121 | ```
122 |
123 | 很明显这样写太不雅观了,如果使用 Reflect 则不存在这个问题了。
124 |
125 | ```javascript
126 | Reflect.defineProperty(obj, 'like', {
127 | get() {
128 | return '中国'
129 | }
130 | })
131 |
132 | Reflect.defineProperty(obj, 'like', {
133 | get() {
134 | return '中国'
135 | }
136 | })
137 |
138 | console.log('没有阻塞')
139 | ```
140 |
141 | 那么 Reflect 的这些到底有什么用呢?接下来我们看看 Reflect 跟 Proxy 结合的威力。
142 |
143 | ### Reflect 跟 Proxy 结合的威力
144 |
145 | 我们先来看看下面的例子:
146 |
147 | ```javascript
148 | const obj = {}
149 | Object.defineProperty(obj,"name",{
150 | value:"coboy",
151 | writable: false //当设置为 false 的时候当前对象的属性值不允许被修改
152 | })
153 |
154 | obj.name = 'cobyte'
155 | console.log(obj.name) // 'coboy'
156 | ```
157 |
158 | 我们创建了一个对象,并且通过 Object.defineProperty 设置了它的**属性描述符** value 值为: coboy,并且该属性值设置不允许修改,然后我们尝试修改它的 name 属性值,发现并没有成功,最终还是打印了最初的定义的 `coboy`。
159 |
160 | 我们将上面的代码和 Proxy 结合一下:
161 |
162 | ```javascript
163 | const proxy = new Proxy(obj, {
164 | get(target, key,) {
165 | // 注意,这里我们没有使用 Reflect 来进行读取
166 | return target[key]
167 | },
168 | set(target, key, value) {
169 | // 注意,这里同样没有使用 Reflect 来进行设置
170 | return target[key] = value
171 | }
172 | })
173 | proxy.name = '王五'
174 | console.log('阻塞了')
175 | ```
176 |
177 | 我们通过 Proxy 代理了上面设置的对象 obj,然后通过代理对象去修改 name 的属性值发现报错了,并且后面的代码也不执行,被阻塞了。
178 |
179 | ```
180 | VM258:10 Uncaught TypeError: 'set' on proxy: trap returned truish for property 'name' which exists in the proxy target as a non-configurable and non-writable data property with a different value
181 | ```
182 |
183 | 接下来我们使用 Reflect 对 Proxy 代理设置的代码进行改造一下:
184 |
185 | ```javascript
186 | const proxy = new Proxy(obj, {
187 | get(target, key,) {
188 | return Reflect.get(target, key)
189 | },
190 | set(target, key, value) {
191 | return Reflect.set(target, key, value)
192 | }
193 | })
194 | proxy.name = '王五'
195 | console.log('正常运行')
196 | ```
197 |
198 | 使用了 Reflect 进行设置之后,居然不报错了,代码正常运行了。是因为 Reflect.get()、Reflect.set() 具有返回值,并且 Proxy 的 handler 的 get、set 也要求有返回值,所以这时使用 Reflect 再合适不过了。
199 |
200 | 我们再看看上面的例子:
201 |
202 | ```javascript
203 | const obj = {}
204 | Object.defineProperty(obj,"name",{
205 | value:"coboy",
206 | writable: false //当设置为 false 的时候当前对象的属性值不允许被修改
207 | })
208 |
209 | console.log(Reflect.set(obj, 'name', 'cobyte')) // false
210 | console.log('不阻塞了') // '不阻塞了'
211 | ```
212 |
213 | 我们发现通过 Object.defineProperty 设置了不可修改的属性之后,我们使用 Reflect.set() 去修改的时候,它是有返回值的,并且返回值是 false。
214 |
215 |
216 | ### Proxy 只能够拦截对一个对象的基本操作,不能拦截对一个对象的复合操作
217 |
218 | ```javascript
219 | const obj = {
220 | name: 'coboy',
221 | fn() {
222 | console.log(this.name, this === p)
223 | }
224 | }
225 |
226 | const p = new Proxy(obj, {
227 | get(target, key, receiver) {
228 | return Reflect.get(target, key)
229 | }
230 | })
231 |
232 | p.fn() // 打印 'coboy', true
233 | ```
234 |
235 | Proxy 只能够拦截对一个对象的基本操作,不能拦截对一个对象的复合操作。调用对象下的方法就是典型的非基本操作,如上面的:p.fn() 实际上调用一个对象下的方法,是由两个基本语义组成的。第一个基本语义是 get,即先通过 get 操作得到 p.fn 属性。第二个基本语义是函数调用,即通过 get 得到 p.fn 的值后再调用它。p 是代理对象,而函数中的 this 是谁调用它,它就指向谁,所以此时 fn 中的 this 就指向了代理对象 p。由于 Proxy 不能拦截对一个对象的复合操作,所以 p.fn() 执行这一动作是无法捕捉的。
236 |
237 |
238 | ### 访问器属性中的 this 的指向问题
239 |
240 | 接下来再看下面的例子:
241 |
242 | ```javascript
243 | const obj = {
244 | name: 'coboy',
245 | get value() {
246 | console.log('value 中的 this:', this, this === obj)
247 | return this.name
248 | }
249 | }
250 | obj.value
251 | ```
252 |
253 | 打印出的结果是:
254 |
255 | 
256 |
257 | 我们可以看到,对象 obj 的 value 属性是一个访问器属性,它返回了 this.name 属性值。我们在打印台中可以看到,this.name 的值就是 'coboy',同时也可以得知 value 属性的访问器里面的 this 就是指向对象 obj 本身,所以 this.name 的返回值,其实就是 obj.name,自然 this.name 的返回值就是 ‘coboy’ 了。
258 |
259 | 接下来我们使用 Proxy 对对象 obj 进行代理:
260 |
261 | ```javascript
262 | const proxy = new Proxy(obj, {
263 | get(target, key) {
264 | return Reflect.get(target, key)
265 | }
266 | })
267 | proxy.value
268 | ```
269 |
270 | 打印出的结果是:
271 |
272 | 
273 |
274 | 我们通过代理对象 proxy 去访问 value 属性,最终还是返回了 'coboy',但我们看到 obj 的 value 属性访问器中的 this 仍然指向对象 obj。这是正确的吗?我们设想一下,将来当 effect 注册的副作用函数执行时,读取 proxy.value 属性,发现 proxy.value 是一个访问器属性,因此执行 getter 函数。由于在 getter 函数中通过 this.name 读取了 name 的属性值,那么副作用函数将要和属性 name 之间建立联系。但要建立联系,必须是响应式数据的读取才能发生,而上面的 this 是指向了 obj,obj对象是一个原始数据,并不是响应式对象,所以将无法和副作用函数建立联系。
275 |
276 | ### Proxy 和 Reflect 中的 receiver 参数
277 |
278 | 这个时候,就要说一下 Reflect.get() 的第三个参数了,先给出解决问题的代码:
279 |
280 | ```javascript
281 | const proxy = new Proxy(obj, {
282 | // 拦截读取操作,接收第三个参数 receiver
283 | get(target, key, receiver) {
284 | // 使用 Reflect.get 返回读取到的属性值
285 | return Reflect.get(target, key, receiver)
286 | }
287 | })
288 | proxy.value
289 | ```
290 |
291 | 我们再来看此时打印的结果:
292 |
293 | 
294 |
295 | 这个时候,我们发现 obj 里的 value 访问器里的 this 就已经指向了代理对象 proxy 了。很显然这时通过响应式对象读取 name 属性,便会在副作用函数与响应式数据之间建立响应式联系,从而达到依赖收集的效果。
296 |
297 | 那么这其实的奥秘到底在哪呢?接下我们再了解一下代理对象的 get 拦截函数接收第三个参数 receiver 是个啥东西。
298 |
299 | ```javascript
300 | const proxy = new Proxy(obj, {
301 | // 拦截读取操作,接收第三个参数 receiver
302 | get(target, key, receiver) {
303 | console.log('receiver:', receiver, receiver === proxy)
304 | // 使用 Reflect.get 返回读取到的属性值
305 | return Reflect.get(target, key, receiver)
306 | }
307 | })
308 | proxy.value
309 | ```
310 |
311 | 我们来看看打印结果:
312 |
313 | 
314 |
315 | 代理对象的 get 拦截函数接收第三个参数 receiver 就是 响应式对象 proxy,所以 Reflect.get(target, key, receiver) 就像 Reflect.get(target, key).call(receiver) [ 模拟,伪代码 ],改变 this 的指向。
316 |
317 | 如上面的代码所示,代理对象的 get 拦截函数接收第三个参数 receiver,它代表谁在读取属性,例如:
318 |
319 | ```javascript
320 | proxy.value // 代理对象 proxy 在读取 value 属性
321 | ```
322 | ### Proxy 实例对象的 get 陷阱上的 receiver 参数到底指向谁?
323 |
324 | 请看下面的例子:
325 |
326 | ```javascript
327 | const obj1 = { name: 'coboy' }
328 | const p = new Proxy(obj1, {
329 | get(target, key, receiver) {
330 | console.log(receiver === p, receiver === obj2);
331 | return target[key];
332 | },
333 | });
334 | const obj2 = {};
335 | // 设置 obj2 继承代理对象 p,而代理对象 p 则是对象 obj1 的代理
336 | obj2.__proto__ = p
337 | obj2.name
338 | ```
339 |
340 | 打印的结果:
341 |
342 | 
343 |
344 | 我们可以看到 proxy 对象的 get 陷阱上打印 `receiver === p` 是为 false 的,即 Proxy 实例对象的 get 陷阱上的 receiver 参数不一定是指向代理对象实例本身,而 打印 `receiver === obj2` 则为 true,所以我们可以得知,通过 obj2.name 的访问触发了属性访问器,是 obj2 对象触发的,**所以谁触发了 get 陷阱,receiver 就指向谁**。
345 |
346 | 所以为了印证这个说法,我们再看一个例子:
347 |
348 | ```javascript
349 | const obj = {
350 | name: 'coboy',
351 | get value() {
352 | console.log('value 中的 this:', this, this === obj2)
353 | return this.name
354 | }
355 | }
356 |
357 | const proxy = new Proxy(obj, {
358 | // 拦截读取操作,接收第三个参数 receiver
359 | get(target, key, receiver) {
360 | // 使用 Reflect.get 返回读取到的属性值
361 | return Reflect.get(target, key, receiver)
362 | }
363 | })
364 |
365 | const obj2 = {};
366 | // 设置 obj2 继承代理对象 p,而代理对象 p 则是对象 obj1 的代理
367 | obj2.__proto__ = proxy
368 | obj2.value
369 | ```
370 |
371 | 打印结果:
372 |
373 | 
374 |
375 | 由于是 obj2 触发了 value 的属性访问器,从而触发了 Proxy 中的 get 陷阱,所以此时 get 陷阱的 receiver 参数就是 obj2, 然后通过 Reflect.get() 的第三个参数改变 this 的指向,所以 obj 对象中的 value 属性访问器中的 this 就指向了 get 陷阱的 receiver 参数,也就是 obj2,而最终的打印结果也证明了这一点,所以更加证明了**谁触发了 get 陷阱,receiver 就指向谁** 。
376 |
377 |
378 |
379 | ### 总结
380 |
381 | 在本文章中,我们了解了 Proxy 与 Reflect 的一些使用方法。Vue3 的响应式数据是基于 Proxy 实现的,Proxy 可以为其他对象创建一个代理对象,它允许我们拦截并重新定义对一个对象的基本操作。在实现代理的过程中,我们遇到了访问器属性的 this 指向问题,这需要使用 Reflect 来和 Proxy 一起使用并指定正确的 receiver 来解决。
382 |
383 |
384 |
385 | 最后推荐一个学习vue3源码的库,它是基于崔效瑞老师的开源库mini-vue而来,在mini-vue的基础上实现更多的vue3核心功能,用于深入学习 vue3, 让你更轻松地理解 vue3 的核心逻辑。
386 |
387 | Github地址:[mini-vue3-plus](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Famebyte%2Fmini-vue3-plus)
388 |
389 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Enable incremental compilation */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | "lib": ["DOM", "ES6", "ES2016"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 |
26 | /* Modules */
27 | "module": "esnext", /* Specify what module code is generated. */
28 | // "rootDir": "./", /* Specify the root folder within your source files. */
29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
34 | "types": ["jest"], /* Specify type package names to be included without being referenced in a source file. */
35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
36 | // "resolveJsonModule": true, /* Enable importing .json files */
37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
38 |
39 | /* JavaScript Support */
40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
43 |
44 | /* Emit */
45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
50 | // "outDir": "./", /* Specify an output folder for all emitted files. */
51 | // "removeComments": true, /* Disable emitting comments. */
52 | // "noEmit": true, /* Disable emitting files from a compilation. */
53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
55 | "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
61 | // "newLine": "crlf", /* Set the newline character for emitting files. */
62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
67 |
68 | /* Interop Constraints */
69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
71 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
73 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
74 |
75 | /* Type Checking */
76 | "strict": true, /* Enable all strict type-checking options. */
77 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
78 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
79 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
80 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
81 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
82 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
83 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
84 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
85 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
86 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
87 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
88 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
89 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
90 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
91 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
92 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
95 |
96 | /* Completeness */
97 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
98 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/md/Vue3生命周期Hooks的原理及其与调度器(Scheduler)的关系.md:
--------------------------------------------------------------------------------
1 | # Vue3生命周期Hooks的原理及其与调度器(Scheduler)的关系
2 | ### 写在最前:本文章的目标
3 | Vue3的生命周期的实现原理是比较简单的,但要理解整个Vue3的生命周期则还要结合整个Vue的运行原理,又因为Vue3的一些生命周期的执行机制是通过Vue3的调度器来完成的,所以想要彻底了解Vue3的生命周期原理还必须要结合Vue3的调度器的实现原理来理解。同时通过对Vue3的调度器的理解,从而加深对Vue底层的一些设计原理和规则的理解,所以本文章的目标是理解Vue3生命周期Hooks的原理以及通过Vue3生命周期Hooks的运行了解Vue3调度器(Scheduler)的原理。
4 |
5 | ### Vue3生命周期的实现原理
6 | Vue3的生命周期Hooks函数的实现原理还是比较简单的,就是把各个生命周期的函数挂载或者叫注册到组件的实例上,然后等到组件运行到某个时刻,再去组件实例上把相应的生命周期的函数取出来执行。
7 |
8 | 下面来看看具体代码的实现
9 |
10 | #### 生命周期类型
11 |
12 | ```javascript
13 | // packages/runtime-core/src/component.ts
14 | export const enum LifecycleHooks {
15 | BEFORE_CREATE = 'bc', // 创建之前
16 | CREATED = 'c', // 创建
17 | BEFORE_MOUNT = 'bm', // 挂载之前
18 | MOUNTED = 'm', // 挂载之后
19 | BEFORE_UPDATE = 'bu', // 更新之前
20 | UPDATED = 'u', // 更新之后
21 | BEFORE_UNMOUNT = 'bum', // 卸载之前
22 | UNMOUNTED = 'um', // 卸载之后
23 | // ...
24 | }
25 | ```
26 |
27 | #### 各个生命周期Hooks函数的创建
28 |
29 | ```javascript
30 | // packages/runtime-core/src/apiLifecycle.ts
31 | export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
32 | export const onMounted = createHook(LifecycleHooks.MOUNTED)
33 | export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
34 | export const onUpdated = createHook(LifecycleHooks.UPDATED)
35 | export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
36 | export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
37 | ```
38 |
39 | 可以看到各个生命周期的Hooks函数是通过createHook这个函数创建的
40 |
41 | #### 创建生命周期函数createHook
42 |
43 | ```javascript
44 | // packages/runtime-core/src/apiLifecycle.ts
45 | export const createHook = (lifecycle) => (hook, target = currentInstance) => injectHook(lifecycle, hook, target)
46 | ```
47 |
48 | createHook是一个闭包函数,通过闭包缓存当前是属于哪个生命周期的Hooks,target表示该生命周期Hooks函数被绑定到哪个组件实例上,默认是当前工作的组件实例。createHook底层又调用了一个injectHook的函数,那么下面我们继续来看看这个injectHook函数。
49 |
50 | #### injectHook函数
51 |
52 | injectHook是一个闭包函数,通过闭包缓存绑定对应生命周期Hooks到对应的组件实例上。
53 |
54 | ```javascript
55 | // packages/runtime-core/src/apiLifecycle.ts
56 | export function injectHook(type, hook, target) {
57 | if(target) {
58 | // 把各个生命周期的Hooks函数挂载到组件实例上,并且是一个数组,因为可能你会多次调用同一个组件的同一个生命周期函数
59 | const hooks = target[type] || (target[type] = [])
60 | // 把生命周期函数进行包装并且把包装函数缓存在__weh上
61 | const wrappedHook =
62 | hook.__weh ||
63 | (hook.__weh = (...args: unknown[]) => {
64 | if (target.isUnmounted) {
65 | return
66 | }
67 | // 当生命周期调用时 保证currentInstance是正确的
68 | setCurrentInstance(target)
69 | // 执行生命周期Hooks函数
70 | const res = args ? hook(...args) : hook()
71 | unsetCurrentInstance()
72 | return res
73 | })
74 | // 把生命周期的包装函数绑定到组件实例对应的hooks上
75 | hooks.push(wrappedHook)
76 | // 返回包装函数
77 | return wrappedHook
78 | }
79 | }
80 | ```
81 | #### 生命周期Hooks的调用
82 |
83 | ```javascript
84 | instance.update = effect(() => {
85 | if (!instance.isMounted) {
86 | const { bm, m } = instance
87 | // 生命周期:beforeMount hook
88 | if (bm) {
89 | invokeArrayFns(bm)
90 | }
91 | // 组件初始化的时候会执行这里
92 | // 为什么要在这里调用 render 函数呢
93 | // 是因为在 effect 内调用 render 才能触发依赖收集
94 | // 等到后面响应式的值变更后会再次触发这个函数
95 | const subTree = (instance.subTree = renderComponentRoot(instance))
96 | patch(null, subTree, container, instance, anchor)
97 | instance.vnode.el = subTree.el
98 | instance.isMounted = true
99 | // 生命周期:mounted
100 | if(m) {
101 | // mounted需要通过Scheduler的函数来调用
102 | queuePostFlushCb(m)
103 | }
104 | } else {
105 | // 响应式的值变更后会从这里执行逻辑
106 | // 主要就是拿到新的 vnode ,然后和之前的 vnode 进行对比
107 |
108 | // 拿到最新的 subTree
109 | const { bu, u, next, vnode } = instance
110 | // 如果有 next 的话, 说明需要更新组件的数据(props,slots 等)
111 | // 先更新组件的数据,然后更新完成后,在继续对比当前组件的子元素
112 | if(next) {
113 | next.el = vnode.el
114 | updateComponentPreRender(instance, next)
115 | }
116 |
117 | // 生命周期:beforeUpdate hook
118 | if (bu) {
119 | invokeArrayFns(bu)
120 | }
121 |
122 | const subTree = renderComponentRoot(instance)
123 | // 替换之前的 subTree
124 | const prevSubTree = instance.subTree
125 | instance.subTree = subTree
126 | // 用旧的 vnode 和新的 vnode 交给 patch 来处理
127 | patch(prevSubTree, subTree, container, instance, anchor)
128 |
129 | // 生命周期:updated hook
130 | if (u) {
131 | // updated 需要通过Scheduler的函数来调用
132 | queuePostFlushCb(u)
133 | }
134 | }
135 | }, {
136 | scheduler() {
137 | queueJobs(instance.update)
138 | }
139 | })
140 | ```
141 |
142 | 上面这个是Vue3组件实例化之后,通过effect包装一个更新的副作用函数来和响应式数据进行依赖收集。在这个副作用函数里面有两个分支,第一个是组件挂载之前执行的,也就是生命周期函数beforeMount和mount调用的地方,第二个分支是组件挂载之后更新的时候执行的,在这里就是生命周期函数beforeUpdate和updated调用的地方。
143 | 具体就是在挂载之前,还没生成虚拟DOM之前就执行beforeMount函数,之后则去生成虚拟DOM经过patch之后,组件已经被挂载到页面上了,也就是页面上显示视图了,这个时候就去执行mount函数;在更新的时候,还没获取更新之后的虚拟DOM之前执行beforeUpdate,然后去获取更新之后的虚拟DOM,然后再去patch,更新视图,之后就执行updated。
144 | 需要注意的是beforeMount和beforeUpdate是同步执行的,都是通过invokeArrayFns来调用的。
145 | invokeArrayFns函数
146 |
147 | ```javascript
148 | export const invokeArrayFns = (fns: Function[], arg?: any) => {
149 | for (let i = 0; i < fns.length; i++) {
150 | fns[i](arg)
151 | }
152 | }
153 | ```
154 |
155 | 组件挂载和更新则是异步的,需要通过Scheduler来处理。
156 |
157 | ### Vue3调度器(Scheduler)原理
158 | 在Vue3的一些API,例如:组件的生命周期API、watch API、组件更新的回调函数都不是立即执行的,而是放到异步任务队列里面,然后按一定的规则进行执行的,比如说任务队列里面同时存在,watch的任务,组件更新的任务,生命周期的任务,它的执行顺序是怎么样的呢?这个就是由调度器的调度算法决定,同时调度算法只调度执行的顺序,不负责具体的执行。这样设计的好处就是即便将来Vue3增加新的异步回调API,也不需要修改调度算法,可以极大的减少 Vue API 和 队列间耦合。
159 | Vue3的Scheduler提供了三个入列方式的API:
160 |
161 | queuePreFlushCb API: 加入 Pre 队列 组件更新前执行
162 |
163 | ```javascript
164 | export function queuePreFlushCb(cb: SchedulerJob) {
165 | queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
166 | }
167 | ```
168 |
169 | queueJob API: 加入 queue 队列 组件更新执行
170 |
171 | ```javascript
172 | export function queueJob(job: SchedulerJob) {
173 |
174 | }
175 | ```
176 |
177 | queuePostFlushCb API: 加入 Post 队列 组件更新后执行
178 |
179 | ```javascript
180 | export function queuePostFlushCb(cb: SchedulerJobs) {
181 | queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
182 | }
183 | ```
184 | 由于Vue3只提供了入列方式的API并没有提供出列方式的API,所以我们只能控制何时入列,而何时出列则由Vue3调度器本身控制。
185 |
186 | 那么Vue3调度器如何控制出列方式呢?其实也很简单。
187 |
188 | ```javascript
189 | function flushJobs(seen?) {
190 | isFlushPending = false
191 | // 组件更新前队列执行
192 | flushPreFlushCbs(seen)
193 | try{
194 | // 组件更新队列执行
195 | let job
196 | while (job = queue.shift()) {
197 | job && job()
198 | }
199 | } finally {
200 | // 组件更新后队列执行
201 | flushPostFlushCbs(seen)
202 | // 如果在执行异步任务的过程中又产生了新的队列,那么则继续回调执行
203 | if (
204 | queue.length ||
205 | pendingPreFlushCbs.length ||
206 | pendingPostFlushCbs.length
207 | ) {
208 | flushJobs(seen)
209 | }
210 | }
211 | }
212 | ```
213 |
214 |
215 |
216 | ### Vue父子组件的生命周期的执行顺序
217 | 这里有两个概念需要厘清的概念,一:父子组件的执行顺序,二:父子组件生命周期的执行顺序。这两个是不一样的
218 |
219 | #### 父子组件的执行顺序
220 |
221 | 这个是先执行父组件再执行子组件,先父组件实例化,然后去获取父组件的虚拟DOM之后在patch的过程中,如果父组件的虚拟DOM中存在组件类型的虚拟DOM也就是子组件,那么在patch的分支中就会去走组件初始化的流程,如此循环。
222 |
223 | #### 父子组件生命周期的执行顺序
224 |
225 | 父子组件生命周期的执行顺序是在父子组件的执行顺序下通过调度算法按Vue的规则进行执行的。首先父组件先实例化进行执行,通过上面的生命周期的调用说明,我们可以知道,父组件在更新函数update第一次执行,也就是组件初始化的时候,先执行父组件的beforeMount,然后去获取父组件的虚拟DOM,然后在patch的过程中遇到虚拟节点是组件类型的时候,就又会去走组件初始化的流程,这个时候其实就是子组件初始化,那么之后子组件也需要走一遍组件的所有流程,子组件在更新update第一次执行的时候,先执行子组件的beforeMount,再去获取子组件的虚拟DOM,然后patch子组件的虚拟DOM,如果过程中又遇到节点是组件类型的话,又去走一遍组件初始化的流程,直到子组件patch完成,然后执行子组件的mounted生命周期函数,接着回到父组件的执行栈,执行父组件的mounted生命周期。
226 |
227 | 所以在初始化创建的时候,是深度递归创建子组件的过程,父子组件的生命周期的执行顺序是:
228 |
229 | 1. 父组件 -> beforeMount
230 | 2. 子组件 -> beforeMount
231 | 3. 子组件 -> mounted
232 | 4. 父组件 -> mounted
233 |
234 | 父子组件更新顺序同样是深度递归执行的过程:
235 |
236 | 1. 如果父子组件没通过props传递数据,那么更新的时候,就各自执行各自的更新生命周期函数。
237 | 2. 如果父子组件存在通过props传递数据的话,就必须先更新父组件,才能更新子组件。因为父组件 DOM 更新前,需要修改子组件的 props,子组件的 props 才是正确的值。
238 |
239 | 下面我们来看源码
240 |
241 | ```javascript
242 | if (next) {
243 | next.el = vnode.el
244 | // 在组件更新前,先更新一些数据
245 | updateComponentPreRender(instance, next, optimized)
246 | } else {
247 | next = vnode
248 | }
249 | ```
250 |
251 | 例如更新props,更新slots
252 |
253 | ```javascript
254 | const updateComponentPreRender = (
255 | instance: ComponentInternalInstance,
256 | nextVNode: VNode,
257 | optimized: boolean
258 | ) => {
259 | nextVNode.component = instance
260 | const prevProps = instance.vnode.props
261 | instance.vnode = nextVNode
262 | instance.next = null
263 | // 更新props
264 | updateProps(instance, nextVNode.props, prevProps, optimized)
265 | // 更新slots
266 | updateSlots(instance, nextVNode.children, optimized)
267 | // ...
268 | }
269 | ```
270 |
271 | 所以在父子组件更新的时候,父子组件的生命周期执行顺序是:
272 |
273 | 1. 父组件 -> beforeUpdate
274 | 2. 子组件 -> beforeUpdate
275 | 3. 子组件 -> updated
276 | 4. 父组件 -> updated
277 |
278 | 同样卸载的时候父子组件也是深度递归遍历执行的过程:
279 |
280 | 1. 父组件 -> beforeUnmount
281 | 2. 子组件 -> beforeUnmount
282 | 3. 子组件 -> unmounted
283 | 4. 父组件 -> unmounted
284 |
285 | ### 组件卸载的时候,是在卸载些什么呢?
286 |
287 | 组件卸载的时候主要是卸载模版引用,清除effect里面保存的相关组件的更新函数的副作用函数,如果是缓存组件,则清除相关缓存,最后去移除真实DOM上相关节点。
288 |
289 | 另外组件 DOM 更新(instance.update)是有保存在调度器的任务队列中的,组件卸载的时候,也需要把相关的组件更新(instance.update)设置失效。
290 |
291 | 在源码的unmountComponent函数中,有这么一段:
292 |
293 | ```javascript
294 | if (update) {
295 | // 把组件更新函数的active设置false
296 | update.active = false
297 | unmount(subTree, instance, parentSuspense, doRemove)
298 | }
299 | ```
300 |
301 | 然后在Scheduler执行queue队列任务的时候,那些job的active为false的则不执行
302 |
303 | ```javascript
304 | const job = queue[flushIndex]
305 | // 那些job的active为false的则不执行
306 | if (job && job.active !== false) {
307 | callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
308 | }
309 | ```
310 |
311 | 那么组件 DOM 更新(instance.update)什么时候会被删除呢?
312 |
313 | 在源码的updateComponent函数可以找到删除instance.update的设置
314 |
315 | ```javascript
316 | invalidateJob(instance.update)
317 | // 立即执行更新任务
318 | instance.update()
319 | ```
320 |
321 | 调度器删除任务
322 |
323 | ```javascript
324 | export function invalidateJob(job: SchedulerJob) {
325 | // 找到 job 的索引
326 | const i = queue.indexOf(job)
327 | if (i > flushIndex) {
328 | // 删除 Job
329 | queue.splice(i, 1)
330 | }
331 | }
332 | ```
333 |
334 | 由此我们可以得知在一个组件更新的时候,会先把该组件在调度器里的更新任务先删除。因为组件更新也是一个递归执行更新的过程,在递归的过程中执行了子组件的更新,那么调度器的任务队列里面的子组件更新任务就不需要再执行了,所以就要删除掉,将来子组件依赖的响应式数据发生了更新,那么则重新把子组件的更新任务放到调度器的任务队列里去。
335 |
336 | ### 组件更新的调度器里的队列任务的失效与删除的区别
337 |
338 | 通过上述组件卸载的介绍我们可以总结一下组件更新的调度器里的队列任务的失效与删除的区别
339 |
340 | 失效
341 |
342 | - 组件卸载时,将 Job 设置为失效,Job 从队列中取出时,不再执行
343 | - 不能再次加入队列,因为会被去重
344 | - 被卸载的组件,无论它依赖的响应式变量如何更新,该组件都不会更新了
345 |
346 | 删除
347 |
348 | - 组件更新时,删除该组件在调度器任务队列中的 Job
349 | - 可以再次加入队列
350 | - 删除任务,因为已经更新过了,不需要重复更新。 如果依赖的响应式变量再次被修改,仍然需要加入调度器的任务队列,等待更新
351 |
352 | ### 父子组件执行顺序与调度器的关系
353 |
354 | 假设有有这样一个场景,有一对父子组件,子组件使用watch API监听某个子组件的响应式数据发生改变之后,然后去修改了N个父组件的响应式数据。那么N个父组件的更新函数都将被放到调度器的任务队列中等待执行。这种情况调度器怎么确保最顶层的父组件的更新函数最先执行呢?
355 |
356 | 我们先看看调度器的任务队列里的Job的数据结构
357 |
358 | ```javascript
359 | export interface SchedulerJob extends Function {
360 | id?: number // 用于对队列中的 job 进行排序,id 小的先执行
361 | active?: boolean
362 | computed?: boolean
363 | allowRecurse?: boolean
364 | ownerInstance?: ComponentInternalInstance
365 | }
366 | ```
367 |
368 | Job是一个函数,并且带有一些属性。其中id,表示优先级,用于实现队列插队,id 小的先执行,active通过上文我们可以知道active表示 Job 是否有效,失效的 Job 不执行,如组件卸载会导致 Job 失效。
369 |
370 | 调度器任务队列的数据结构
371 |
372 | ```javascript
373 | const queue: SchedulerJob[] = []
374 | ```
375 |
376 | 是一个数组
377 |
378 | 调度器任务队列的执行
379 |
380 | ```javascript
381 | // 按任务id大小排序
382 | queue.sort((a, b) => getId(a) - getId(b))
383 | try {
384 | for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
385 | const job = queue[flushIndex]
386 | if (job && job.active !== false) {
387 | // 使用带有 Vue 内部的错误处理函数执行job
388 | callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
389 | }
390 | }
391 | } finally {
392 | // 清空 queue 队列
393 | flushIndex = 0
394 | queue.length = 0
395 | }
396 | ```
397 |
398 | 那么又怎么确保父组件的更新函数的任务id是最小的呢?
399 |
400 | 通过查看源码我们可以看在创建组件实例的createComponentInstance函数中有一个uid的属性,并且它的初始值为0,后续则++
401 |
402 | ```javascript
403 | let uid = 0 // 初始化为0
404 | export function createComponentInstance(
405 | vnode
406 | parent
407 | suspense
408 | ) {
409 | const instance: ComponentInternalInstance = {
410 | uid: uid++,
411 | // ...
412 | }
413 | ```
414 |
415 | 然后在创建组件更新函数的时候可以看到,组件更新函数的id就是该组件实例的uid
416 |
417 | ```javascript
418 | const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
419 | update.id = instance.uid
420 | ```
421 |
422 | 组件创建的过程是深度递归创建子组件的过程,所以最先的父组件是0,后面的子组件则一路++上去,这样就确保了子组件的更新函数的任务id是一定大于父组件更新函数的id的。所以当调度器的任务队列里面同时存在很多组件的更新函数的时候,通过优先级排序,就可以确保一定父组件的更新函数最先执行了。
423 |
424 | 当前中途也可以进行插队
425 |
426 | ```javascript
427 | export function queueJob(job: SchedulerJob) {
428 | // 没有id的则push到最后
429 | if (job.id == null) {
430 | queue.push(job)
431 | } else {
432 | // 进行插队处理
433 | queue.splice(findInsertionIndex(job.id), 0, job)
434 | }
435 | queueFlush()
436 | }
437 | ```
438 |
439 |
440 |
441 |
442 | ### Hooks的本质
443 | 最后探讨一下Hooks的本质
444 |
445 | Vue的Hooks设计是从React的Hooks那里借鉴过来的,React的Hooks的本质就是把状态变量、副作用函数存到函数组件的fiber对象上,等到将来状态变量发生改变的时候,相关的函数组件fiber就重新进行更新。Vue3这边的实现原理也类似,通过上面的生命周期的Hooks实现原理,我们可以知道Vue3的生命周期的Hooks是绑定到具体的组件实例上,而状态变量,则因为Vue的变量是响应式的,状态变量会通过effect和具体的组件更新函数进行依赖收集,然后进行绑定,将来状态变量发生改变的时候,相应的组件更新函数会重新进入调度器的任务队列进行调度执行。
446 |
447 | 所以Hooks的本质就是让那些状态变量或生命周期函数和组件绑定起来,组件运行到相应时刻执行相应绑定的生命周期函数,那些绑定的变量发生改变的时候,相应的组件也重新进行更新。
448 |
449 |
450 |
451 | ### 最后
452 |
453 | 下一篇准备写一下watch API的实现原理,同时watch API也需要和调度器结合进行理解,只有相互串联理解才可以把Vue3底层设计和实现原理理解得更加透切一些。
454 |
455 |
456 |
457 |
458 |
459 | 最后推荐一个学习 vue3 源码的库,它是基于崔效瑞老师的开源库 mini-vue 而来,在 mini-vue 的基础上实现更多的 vue3 核心功能,用于深入学习 vue3, 让你更轻松地理解 vue3 的核心逻辑。
460 |
461 | Github 地址:[mini-vue3-plus](https://github.com/amebyte/mini-vue3-plus)
--------------------------------------------------------------------------------
/src/runtime-core/renderer.ts:
--------------------------------------------------------------------------------
1 | import { effect } from '../reactivity/effect'
2 | import { EMPTY_OBJ, invokeArrayFns, isObject } from '../shared'
3 | import { ShapeFlags } from '../shared/ShapeFlags'
4 | import { createComponentInstance, setupComponent } from './component'
5 | import { renderComponentRoot } from './componentRenderUtils'
6 | import { shouldUpdateComponent } from './componentUpdateUtils'
7 | import { createAppAPI } from './createApp'
8 | import { invokeDirectiveHook } from './directives'
9 | import { setRef } from './rendererTemplateRef'
10 | import { queueJobs, queuePostFlushCb } from './scheduler'
11 | import { Fragment, Text } from './vnode'
12 |
13 | export function createRenderer(options) {
14 | const {
15 | createElement: hostCreateElement,
16 | patchProp: hostPatchProp,
17 | insert: hostInsert,
18 | remove: hostRemove,
19 | setElementText: hostSetElementText
20 | } = options
21 |
22 | function render(vnode: any, container: any) {
23 | patch(null, vnode, container, null, null)
24 | }
25 |
26 | function patch(n1, n2, container: any, parentComponent, anchor) {
27 | // 基于 n2 的类型来判断
28 | // 因为 n2 是新的 vnode
29 | const { type, shapeFlag, ref } = n2
30 |
31 | // Fragment => 只渲染 children
32 | switch (type) {
33 | // 其中还有几个类型比如: static fragment comment
34 | case Fragment:
35 | processFragment(n1, n2, container, parentComponent, anchor)
36 | break
37 | case Text:
38 | processText(n1, n2, container)
39 | break
40 | default:
41 | // 这里就基于 shapeFlag 来处理
42 | if (shapeFlag & ShapeFlags.ELEMENT) {
43 | // 处理 element
44 | processElement(n1, n2, container, parentComponent, anchor)
45 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
46 | // 处理 component
47 | processComponent(n1, n2, container, parentComponent, anchor)
48 | }
49 | break
50 | }
51 | console.log('refxxx', ref)
52 | // 模板引用ref只会在初始渲染之后获得
53 | if (ref != null && parentComponent) {
54 | setRef(ref, n1 && n1.ref, n2 || n1, !n2)
55 | }
56 | }
57 |
58 | function processComponent(n1, n2, container: any, parentComponent, anchor) {
59 | // 如果 n1 没有值的话,那么就是 mount
60 | if(!n1) {
61 | // 初始化 component
62 | mountComponent(n2, container, parentComponent, anchor)
63 | } else {
64 | updateComponent(n1, n2)
65 | }
66 | }
67 | // 组件的更新
68 | function updateComponent(n1, n2) {
69 | // 更新组件实例引用
70 | const instance = (n2.component = n1.component)
71 | // 先看看这个组件是否应该更新
72 | if(shouldUpdateComponent(n1, n2)) {
73 | // 那么 next 就是新的 vnode 了(也就是 n2)
74 | instance.next = n2
75 | // 这里的 update 是在 setupRenderEffect 里面初始化的,update 函数除了当内部的响应式对象发生改变的时候会调用
76 | // 还可以直接主动的调用(这是属于 effect 的特性)
77 | // 调用 update 再次更新调用 patch 逻辑
78 | // 在update 中调用的 next 就变成了 n2了
79 | // ps:可以详细的看看 update 中 next 的应用
80 | // TODO 需要在 update 中处理支持 next 的逻辑
81 | instance.update()
82 | } else {
83 | // 不需要更新的话,那么只需要覆盖下面的属性即可
84 | n2.component = n1.component;
85 | n2.el = n1.el
86 | n2.vnode = n2
87 | }
88 | }
89 |
90 | function mountComponent(initialVNode: any, container, parentComponent, anchor) {
91 | // 1. 先创建一个 component instance
92 | const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent))
93 | // 2. 给 instance 加工加工
94 | setupComponent(instance)
95 | setupRenderEffect(instance, initialVNode, container, anchor)
96 | }
97 |
98 | function setupRenderEffect(instance: any, vnode, container, anchor) {
99 | // 调用 render
100 | // 应该传入 ctx 也就是 proxy
101 | // ctx 可以选择暴露给用户的 api
102 | // 源代码里面是调用的 renderComponentRoot 函数
103 | // 这里为了简化直接调用 render
104 |
105 | // obj.name = "111"
106 | // obj.name = "2222"
107 | // 从哪里做一些事
108 | // 收集数据改变之后要做的事 (函数)
109 | // 依赖收集 effect 函数
110 | // 触发依赖
111 | instance.update = effect(() => {
112 | if (!instance.isMounted) {
113 | const { bm, m } = instance
114 | // beforeMount hook
115 | if (bm) {
116 | invokeArrayFns(bm)
117 | }
118 | // 组件初始化的时候会执行这里
119 | // 为什么要在这里调用 render 函数呢
120 | // 是因为在 effect 内调用 render 才能触发依赖收集
121 | // 等到后面响应式的值变更后会再次触发这个函数
122 | const subTree = (instance.subTree = renderComponentRoot(instance))
123 | // 这里基于 subTree 再次调用 patch
124 | // 基于 render 返回的 vnode ,再次进行渲染
125 | // 这里我把这个行为隐喻成开箱
126 | // 一个组件就是一个箱子
127 | // 里面有可能是 element (也就是可以直接渲染的)
128 | // 也有可能还是 component
129 | // 这里就是递归的开箱
130 | // 而 subTree 就是当前的这个箱子(组件)装的东西
131 | // 箱子(组件)只是个概念,它实际是不需要渲染的
132 | // 要渲染的是箱子里面的 subTree
133 | patch(null, subTree, container, instance, anchor)
134 | // 把 root element 赋值给 组件的vnode.el ,为后续调用 $el 的时候获取值
135 | instance.vnode.el = subTree.el // 这样显式赋值会不会好理解一点呢
136 | instance.isMounted = true
137 | if(m) {
138 | queuePostFlushCb(m)
139 | }
140 | } else {
141 | // 响应式的值变更后会从这里执行逻辑
142 | // 主要就是拿到新的 vnode ,然后和之前的 vnode 进行对比
143 |
144 | // 拿到最新的 subTree
145 | const { bu, u, next, vnode } = instance
146 | // 如果有 next 的话, 说明需要更新组件的数据(props,slots 等)
147 | // 先更新组件的数据,然后更新完成后,在继续对比当前组件的子元素
148 | if(next) {
149 | // 问题是 next 和 vnode 的区别是什么
150 | next.el = vnode.el
151 | updateComponentPreRender(instance, next)
152 | }
153 |
154 | // beforeUpdate hook
155 | if (bu) {
156 | invokeArrayFns(bu)
157 | }
158 |
159 | const subTree = renderComponentRoot(instance)
160 | // 替换之前的 subTree
161 | const prevSubTree = instance.subTree
162 | instance.subTree = subTree
163 | // 用旧的 vnode 和新的 vnode 交给 patch 来处理
164 | patch(prevSubTree, subTree, container, instance, anchor)
165 |
166 | // updated hook
167 | if (u) {
168 | queuePostFlushCb(u)
169 | }
170 | }
171 | }, {
172 | scheduler() {
173 | console.log('update - scheduler')
174 | queueJobs(instance.update)
175 | }
176 | })
177 | }
178 |
179 | function updateComponentPreRender(instance, nextVNode) {
180 | // 更新 nextVNode 的组件实例
181 | // 现在 instance.vnode 是组件实例更新前的
182 | // 所以之前的 props 就是基于 instance.vnode.props 来获取
183 | // 接着需要更新 vnode ,方便下一次更新的时候获取到正确的值
184 | instance.vnode = nextVNode
185 | instance.next = null
186 | instance.props = nextVNode.props
187 | }
188 |
189 | function processElement(n1, n2, container: any, parentComponent, anchor) {
190 | if (!n1) {
191 | mountElement(n2, container, parentComponent, anchor)
192 | } else {
193 | patchElement(n1, n2, parentComponent, anchor)
194 | }
195 | }
196 |
197 | function patchElement(n1, n2, parentComponent, anchor) {
198 | const { dirs } = n2
199 | // 执行指令 beforeUpdate 钩子函数
200 | if (dirs) {
201 | invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
202 | }
203 | const oldProps = n1.props || {}
204 | const newProps = n2.props || {}
205 | // 需要把 el 挂载到新的 vnode
206 | const el = (n2.el = n1.el)
207 | // 对比 children
208 | patchChildren(n1, n2, el, parentComponent, anchor)
209 | // 对比 props
210 | patchProps(el, oldProps, newProps)
211 |
212 | if (dirs) {
213 | queuePostFlushCb(() => {
214 | // 执行指令 updated 钩子函数
215 | dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
216 | })
217 | }
218 | }
219 |
220 | function patchChildren(n1, n2, container, parentComponent, anchor) {
221 | const prevShapeFlag = n1.shapeFlag
222 | const c1 = n1.children
223 | const { shapeFlag } = n2
224 | const c2 = n2.children
225 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
226 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
227 | // 1. 把老的 children 清空
228 | unmountChildren(n1.children)
229 | }
230 | if (c1 !== c2) {
231 | hostSetElementText(container, c2)
232 | }
233 | } else {
234 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
235 | hostSetElementText(container, '')
236 | mountChildren(c2, container, parentComponent, anchor)
237 | } else {
238 | // array diff array
239 | patchKeyedChildren(c1, c2, container, parentComponent, anchor)
240 | }
241 | }
242 | }
243 |
244 | function patchKeyedChildren(
245 | c1,
246 | c2,
247 | container,
248 | parentComponent,
249 | parentAnchor
250 | ) {
251 | const l2 = c2.length
252 | let i = 0
253 | let e1 = c1.length - 1
254 | let e2 = l2 - 1
255 |
256 | function isSomeVNodeType(n1, n2) {
257 | return n1.type === n2.type && n1.key === n2.key
258 | }
259 |
260 | // 左侧
261 | while (i <= e1 && i <= e2) {
262 | const n1 = c1[i]
263 | const n2 = c2[i]
264 |
265 | if (isSomeVNodeType(n1, n2)) {
266 | patch(n1, n2, container, parentComponent, parentAnchor)
267 | } else {
268 | break
269 | }
270 | i++
271 | }
272 |
273 | // 右侧
274 | while (i <= e1 && i <= e2) {
275 | const n1 = c1[e1]
276 | const n2 = c2[e2]
277 | if (isSomeVNodeType(n1, n2)) {
278 | patch(n1, n2, container, parentComponent, parentAnchor)
279 | } else {
280 | break
281 | }
282 | e1--
283 | e2--
284 | }
285 |
286 | // 新的比老的多,创建
287 | if (i > e1) {
288 | if (i <= e2) {
289 | // debugger
290 | const nextPos = i + 1
291 | const anchor = nextPos < l2 ? c2[nextPos].el : null
292 | while (i <= e2) {
293 | patch(null, c2[i], container, parentComponent, anchor)
294 | i++
295 | }
296 | }
297 | } else if (i > e2) {
298 | while (i <= e1) {
299 | hostRemove(c1[i].el)
300 | i++
301 | }
302 | } else {
303 | // 中间对比
304 | let s1 = i
305 | let s2 = i
306 | // 剩下需要比对的长度
307 | let toBePatched = e2 - s2 + 1
308 | // 当前处理的数量
309 | let patched = 0
310 | // 剩下新数组的 key - 索引 映射表
311 | const keyToNewIndexMap = new Map()
312 | const newIndexToOldIndexMap = new Array(toBePatched)
313 | let moved = false
314 | let maxNewIndexSoFar = 0
315 | for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
316 |
317 | for (let i = s2; i <= e2; i++) {
318 | const nextChild = c2[i]
319 | keyToNewIndexMap.set(nextChild.key, i)
320 | }
321 |
322 | for (let i = s1; i <= e1; i++) {
323 | const prevChild = c1[i]
324 | if (patched >= toBePatched) {
325 | hostRemove(prevChild.el)
326 | continue
327 | }
328 | let newIndex
329 | if (prevChild.key !== null) {
330 | newIndex = keyToNewIndexMap.get(prevChild.key)
331 | } else {
332 | for (let j = s2; j < e2; j++) {
333 | if (isSomeVNodeType(prevChild, c2[j])) {
334 | newIndex = j
335 | break
336 | }
337 | }
338 | }
339 |
340 | if (newIndex === undefined) {
341 | hostRemove(prevChild.el)
342 | } else {
343 |
344 | if(newIndex >= maxNewIndexSoFar) {
345 | maxNewIndexSoFar = newIndex
346 | } else {
347 | moved = true
348 | }
349 |
350 | newIndexToOldIndexMap[newIndex - s2] = i + 1
351 | patch(prevChild, c2[newIndex], container, parentComponent, null)
352 | patched++
353 | }
354 | }
355 | const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []
356 | let j = increasingNewIndexSequence.length - 1
357 | for(let i = toBePatched - 1; i >= 0; i--) {
358 | const nextIndex = i + s2
359 | const nextChild = c2[nextIndex]
360 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null
361 | if(newIndexToOldIndexMap[i] === 0) {
362 | patch(null, nextChild, container, parentComponent, anchor)
363 | } else if(moved) {
364 | // 如果不在最长递增子序列里面则要进行移动
365 | if(j < 0 || i !== increasingNewIndexSequence[j]) {
366 | hostInsert(nextChild.el, container, anchor)
367 | } else {
368 | j--
369 | }
370 | }
371 | }
372 | }
373 | }
374 |
375 | function getSequence(arr) {
376 | const p = arr.slice()
377 | const result = [0]
378 | let i, j, u, v, c
379 | const len = arr.length
380 | for (i = 0; i < len; i++) {
381 | const arrI = arr[i]
382 | if (arrI !== 0) {
383 | j = result[result.length - 1]
384 | if (arr[j] < arrI) {
385 | p[i] = j
386 | result.push(i)
387 | continue
388 | }
389 | u = 0
390 | v = result.length - 1
391 | while (u < v) {
392 | c = (u + v) >> 1
393 | if (arr[result[c]] < arrI) {
394 | u = c + 1
395 | } else {
396 | v = c
397 | }
398 | }
399 | if (arrI < arr[result[u]]) {
400 | if (u > 0) {
401 | p[i] = result[u - 1]
402 | }
403 | result[u] = i
404 | }
405 | }
406 | }
407 | u = result.length
408 | v = result[u - 1]
409 | while (u-- > 0) {
410 | result[u] = v
411 | v = p[v]
412 | }
413 | return result
414 | }
415 |
416 | function unmountChildren(children) {
417 | for (let i = 0; i < children.length; i++) {
418 | const el = children[i].el
419 | hostRemove(el)
420 | }
421 | }
422 |
423 | function patchProps(el, oldProps, newProps) {
424 | if (oldProps !== newProps) {
425 | // 对比 props 有以下几种情况
426 | // 1. oldProps 有,newProps 也有,但是 val 值变更了
427 | // 举个栗子
428 | // 之前: oldProps.id = 1 ,更新后:newProps.id = 2
429 |
430 | // key 存在 oldProps 里 也存在 newProps 内
431 | // 以 newProps 作为基准
432 | for (const key in newProps) {
433 | const prevProp = oldProps[key]
434 | const nextProp = newProps[key]
435 | if (prevProp !== nextProp) {
436 | // 对比属性
437 | // 需要交给 host 来更新 key
438 | hostPatchProp(el, key, prevProp, nextProp)
439 | }
440 | }
441 | if (oldProps !== EMPTY_OBJ) {
442 | for (const key in oldProps) {
443 | if (!(key in newProps)) {
444 | // 2. oldProps 有,而 newProps 没有了
445 | // 之前: {id:1,tId:2} 更新后: {id:1}
446 | // 这种情况下我们就应该以 oldProps 作为基准,因为在 newProps 里面是没有的 tId 的
447 | // 还需要注意一点,如果这个 key 在 newProps 里面已经存在了,说明已经处理过了,就不要在处理了
448 | hostPatchProp(el, key, oldProps[key], null)
449 | }
450 | }
451 | }
452 | }
453 | }
454 |
455 | function mountElement(vnode: any, container: any, parentComponent, anchor) {
456 | // 创建 DOM 元素节点
457 | const el = (vnode.el = hostCreateElement(vnode.type))
458 | const { props, children, shapeFlag, dirs } = vnode
459 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
460 | // 处理子节点是纯文本的情况
461 | el.textContent = children
462 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
463 | // 处理子节点是数组的情况
464 | mountChildren(vnode.children, el, parentComponent, anchor)
465 | }
466 | if (dirs) {
467 | // 执行指令的 created 生命周期的函数
468 | invokeDirectiveHook(vnode, null, parentComponent, 'created')
469 | }
470 | // 处理 props,比如 class、style、event 等属性
471 | if (props) {
472 | for (const key in props) {
473 | const val = props[key]
474 | hostPatchProp(el, key, null, val)
475 | }
476 | }
477 | if (dirs) {
478 | // 执行指令的 beforeMount 生命周期的函数
479 | invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
480 | }
481 | // container.append(el)
482 | // 把创建的 DOM 元素挂载到对应的根节点 container 上
483 | hostInsert(el, container, anchor)
484 |
485 | if (dirs) {
486 | queuePostFlushCb(() => {
487 | // 执行指令的 mounted 生命周期的函数
488 | dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
489 | })
490 | }
491 | }
492 |
493 | function mountChildren(children, container, parentComponent, anchor) {
494 | children.forEach((v) => {
495 | patch(null, v, container, parentComponent, anchor)
496 | })
497 | }
498 |
499 | function processFragment(n1, n2, container: any, parentComponent, anchor) {
500 | // 只需要渲染 children ,然后给添加到 container 内
501 | if (!n1) {
502 | // 初始化 Fragment 逻辑点
503 | mountChildren(n2.children, container, parentComponent, anchor)
504 | }
505 | }
506 |
507 | function processText(n1, n2, container: any) {
508 | // 处理 Text 节点
509 | const { children } = n2
510 | const textNode = (n2.el = document.createTextNode(children))
511 | container.append(textNode)
512 | }
513 | return {
514 | createApp: createAppAPI(render)
515 | }
516 | }
517 |
--------------------------------------------------------------------------------
/md/Vue3的effect、watch、watchEffect API的实现原理.md:
--------------------------------------------------------------------------------
1 | # Vue3 的 effect、 watch、watchEffect 的实现原理
2 |
3 | 所谓 watch,就是观测一个响应式数据或者监测一个副作用函数里面的响应式数据,当数据发生变化的时候通知并执行相应的回调函数。 Vue3 最新的 watch 实现是通过最底层的响应式类 ReactiveEffect 的实例化一个 reactive effect 对象来实现的。它的创建过程跟 effect API 的实现类似,所以在了解 watch API 之前,我们先要了解一下 effect 这个 API。
4 |
5 | ### effect 函数
6 |
7 | Vue3 里面 effect 函数 API 是用于注册副作用函数的函数,也是 Vue3 响应式系统最重要的 API 之一。通过 effect 注册了一个副作用函数之后,当这个副作用函数当中的响应式数据发生了读取操作之后,通过 Proxy 的 get,set 拦截,从而在副作用函数与响应式数据之间建立了联系。具体就是当响应式数据的 ”读取” 操作发生时,将当前执行的副作用函数存储起来;当响应式数据的 “设置” 操作发生时,再将储起来的副作用函数取出来执行。
8 |
9 | #### 什么是副作用函数?
10 |
11 | 副作用函数指的是会产生副作用的函数,如下面的代码所示:
12 |
13 | ```javascript
14 | function effect() {
15 | document.body.innerText = 'hello coboy ~'
16 | }
17 | ```
18 |
19 | 当 effect 函数执行时,它会设置 body 的文本内容,但除了 effect 函数之外的任何函数都可以读取或者设置 body 的文本内容。也就是说,effect 函数的执行会直接或间接影响其他函数的执行,这时我们说effect 函数产生了副作用。副作用很容易产生,例如一个函数修改了全局变量,这其实也是一个副作用,如下面的代码所示:
20 |
21 | ```javascript
22 | // 全局变量
23 | let val = 1
24 | function effect() {
25 | val = 2 // 修改全局变量,产生副作用
26 | }
27 | ```
28 |
29 | 例子说明来自 《vue.js 设计与实现》
30 |
31 | 我们在最文章最开头说到 Vue3 最新的 watch 实现是通过最底层的响应式类 ReactiveEffect 的实例化一个 reactive effect 对象来实现的,那么我们先来了解一下 ReactiveEffect 类。
32 |
33 | #### ReactiveEffect 类
34 |
35 | 相信很多关注 Vue3 源码的同学都知道,Vue3 先前的响应式系统版本中是没有 ReactiveEffect 这个类的,最新版本用面向对象的编程方式把变量当成对象进行操作,让编程思路更加清晰简洁,而且减少了很多冗余变量的出现,使用 ReactiveEffect 这个类封装了 effect 相关的数据和方法,方便了函数、变量、数据的管理。
36 |
37 | 下面我们来看看 ReactiveEffect 这个类的源码:
38 |
39 | ```javascript
40 | // 记录当前活跃的对象
41 | let activeEffect
42 | // 标记是否追踪
43 | let shouldTrack
44 | // 用于依赖收集
45 | export class ReactiveEffect{
46 | private _fn: any
47 | deps = [] // 所有依赖这个 effect 的响应式对象
48 | active = true // 是否为激活状态
49 | onStop?: () => void
50 | constructor(fn, public scheduler?) {
51 | // 用户传进来的副作用函数。
52 | this._fn = fn
53 | }
54 | run() {
55 | // 执行 fn 但是不收集依赖
56 | if(!this.active) {
57 | return this._fn()
58 | }
59 | // 执行 fn 收集依赖
60 | // 可以开始收集依赖了
61 | shouldTrack = true
62 | // 执行的时候给全局的 activeEffect 赋值
63 | // 利用全局属性来获取当前的 effect
64 | activeEffect = this
65 | // 执行用户传入的 fn
66 | const result = this._fn()
67 | // 重置
68 | shouldTrack = false
69 | return result
70 | }
71 | stop() {
72 | if(this.active) {
73 | // 如果第一次执行 stop 后 active 就 false 了
74 | // 这是为了防止重复的调用,执行 stop 逻辑
75 | cleanupEffect(this)
76 | // 如果用户往 effect 实例对象设置了 onStop 函数,那么在清除 effect 对象的时候,也会执行用户设置的 onStop 方法
77 | if(this.onStop) {
78 | this.onStop()
79 | }
80 | this.active = false
81 | }
82 | }
83 | }
84 | ```
85 |
86 | 通过 ReactiveEffect 这个类的实例化相当于是实现了响应式系统里面的一个大管家,这个大管家管理着用户设置的副作用函数,调度函数 scheduler,所有依赖这个 reactive effect 的响应式对象 deps 参数,还实现了两个方法,run 和 stop,在 run 方法里面执行副作用函数,触发依赖收集,并返回副作用函数执行的结果,stop 方法则是清除当前的 reactive effect 实例对象。
87 |
88 | 通过 ReactiveEffect 这个类我们可以很清晰地看到大管家 reactive effect 实例对象在干些什么工作。
89 |
90 | #### effect 函数解析
91 |
92 | 接下来我们看看effect函数的具体代码:
93 |
94 | ```javascript
95 | // packages/reactivity/src/effect.ts
96 | export function effect(
97 | // 副作用函数
98 | fn: () => T,
99 | // 配置选项
100 | options?: ReactiveEffectOptions
101 | ): ReactiveEffectRunner {
102 | // 如果当前 fn 已经是收集函数包装后的函数,则获取监听函数当做入参
103 | if ((fn as ReactiveEffectRunner).effect) {
104 | fn = (fn as ReactiveEffectRunner).effect.fn
105 | }
106 | // 创建effect对象
107 | const _effect = new ReactiveEffect(fn)
108 | // 把用户传过来的值合并到 _effect 对象上去
109 | if (options) {
110 | extend(_effect, options)
111 | if (options.scope) recordEffectScope(_effect, options.scope)
112 | }
113 | // 有些场景下,我们并不希望它立即执行,而是希望它在需要的时候才执行,例如计算属性。
114 | // 这个时候我们可以通过 optins 中添加 lazy 属性来达到目的,当 options.lazy 为 true 时,则不立即执行副作用函数
115 | if (!options || !options.lazy) {
116 | _effect.run() // 执行run,响应式数据将与副作用函数之间建立联系
117 | }
118 | // 把 _effect.run 这个方法返回,也就是等于将辅助函数作为返回值返回
119 | // 让用户可以自行选择调用的时机(调用 fn)
120 | const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
121 | runner.effect = _effect
122 | return runner
123 | }
124 | ```
125 |
126 | 简单总结一下effect
127 |
128 | - 接收一个副作用函数和 options 参数
129 | - 判断传入的副作用函数是不是 effect,如果是取出原始值
130 | - 调用 createReactiveEffect 创建 reactive effect 实例对象
131 | - 把用户传过来的 options 参数合并到创建的 reactive effect 对象上
132 | - 如果传入的 options 参数中的 lazy 为 false 则立即执行 effect 包装之后的副作用函数
133 | - 最后返回 reactive effect 实例对象上的 run 方法让用户可以自行选择调用的时机
134 |
135 | 简单来说 effect API 的实现就是实例化 ReactiveEffect 类获得一个 reactive effect 的实例对象,在实例化的时候通过传参把副作用函数和当前的 reactive effect 实例对象进行了绑定,当运行 reactive effect 实例对象上的 run 方法的时候就把响应式对象和 reactive effect 实例对象进行了绑定。在后续如果响应式对象发生了改变,就会把和响应式对象绑定的那些 reactive effect 实例对象取出来执行 reactive effect 实例对象上的 run 方法,run 方法里面就会执行最初传进来的副作用函数。
136 |
137 | #### 可调度执行
138 |
139 | 所谓可调度,指的是当 trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。
140 |
141 | effect 函数的第二个参数 options,允许用户指定调度器。当用户在调用 effect 函数注册副作用函数时,可以传递第二个参数 options。可以在 options 中指定 scheduler 调度函数。
142 |
143 | 这里,我们顺便介绍一下 effect 的 options 参数:
144 |
145 | ```javascript
146 | export interface ReactiveEffectOptions {
147 | lazy?: boolean //是否懒执行副作用函数
148 | scheduler?: (job: ReactiveEffect) => void //调度函数
149 | onTrack?: (event: DebuggerEvent) => void //追踪时触发
150 | onTrigger?: (event: DebuggerEvent) => void //触发回调时触发
151 | onStop?: () => void //停止监听时触发
152 | allowRecurse?: boolean //是否允许递归调用
153 | }
154 | ```
155 |
156 | #### 组件更新函数的最新实现
157 |
158 | 我们来看看组件更新函数最新的实现是怎么实现的。
159 |
160 | ```javascript
161 | const setupRenderEffect = () => {
162 | const componentUpdateFn = () => {
163 | // ...
164 | }
165 | // 通过 ReactiveEffect 类来创建渲染 reactive effect 实例对象
166 | const effect = (instance.effect = new ReactiveEffect(
167 | componentUpdateFn,
168 | () => queueJob(instance.update),
169 | instance.scope
170 | ))
171 | // 组件更新函数就是 reactive effect 实例对象上 run 方法
172 | const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
173 | update.id = instance.uid
174 |
175 | update()
176 | }
177 | ```
178 |
179 | 我们可以看到最新的组件更新函数也是通过 ReactiveEffect 类来实现的,把组件更新的副作用函数和调度函数传到 ReactiveEffect 类,再实例化出一个 reactive effect 实例对象,再把 reactive effect 实例对象中的 run 方法赋值给组件更新函数属性。
180 |
181 | 通过上面前奏简单了解 effect 函数 API 之后,再进行了解 watch 的实现原理就好理解了。
182 |
183 | ### watch 的实现原理
184 |
185 | 所谓 watch,其实本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。
186 |
187 | 接下来我们简单实现如下响应式数据的监测:
188 |
189 | ```javascript
190 | watch(() => obj.name, () => {
191 | console.log('数据变化了')
192 | })
193 | ```
194 |
195 | 当响应式数据 obj.name 发生更改的时候,就会执行回调函数。
196 |
197 | 在最新的 Vue3.2 版本中,watch API 是通过 ReactiveEffect 类来实现相关功能的。
198 |
199 | #### 最简单的 watch 实现
200 |
201 | ```javascript
202 | export function watch(
203 | source,
204 | cb,
205 | options
206 | ) {
207 | // 副作用函数
208 | const getter = source
209 | // 调度函数
210 | const scheduler = () => cb()
211 | // 通过 ReactiveEffect 类实例化出一个 effect 实例对象
212 | const effect = new ReactiveEffect(getter, scheduler)
213 | // 立即执行实例对象上的 run 方法,执行副作用函数,触发依赖收集
214 | effect.run()
215 | }
216 | ```
217 |
218 | 跟 effect API 的实现类似,通过 ReactiveEffect 类实例化出一个 reactive effect 实例对象,然后执行实例对象上的 run 方法就会执行 getter 副作用函数,getter 副作用函数里的响应式数据发生了读取的 get 操作之后触发了依赖收集,通过依赖收集将 reactive effect 实例对象和响应式数据之间建立了联系,当响应式数据变化的时候,会触发副作用函数的重新执行,但又因为传入了 scheduler 调度函数,所以会执行调度函数,而调度函数里是执行了回调函数 cb,从而实现了监测。
219 |
220 | #### 副作用函数的封装
221 |
222 | 因为第一个参数 source 可以是一个:
223 | - ref 类型的变量
224 | - reactive 类型的变量
225 | - Array 类型的变量,数组里面的元素可以是 ref 类型的变量、reactive 类型的变量、Function 函数
226 | - Function 函数
227 |
228 | 所以需要对第一个参数处理封装成一个通用的副作用函数。
229 |
230 | ```javascript
231 | let getter: () => any
232 | if (isRef(source)) {
233 | // 如果是 ref 类型
234 | getter = () => source.value
235 | } else if (isReactive(source)) {
236 | // 如果是 reactive 类型
237 | getter = () => source
238 | // 深度监听为 true
239 | deep = true
240 | } else if (isArray(source)) {
241 | // 如果是数组,进行循环处理
242 | getter = () =>
243 | source.map(s => {
244 | if (isRef(s)) {
245 | return s.value
246 | } else if (isReactive(s)) {
247 | return traverse(s)
248 | } else if (isFunction(s)) {
249 | return s()
250 | }
251 | })
252 | } else if (isFunction(source)) {
253 | // 如果是函数
254 | getter = () => source()
255 | }
256 |
257 | if (cb && deep) {
258 | // 如果有回调函数并且深度监听为 true,那么就通过 traverse 函数进行深度递归监听
259 | const baseGetter = getter
260 | getter = () => traverse(baseGetter())
261 | }
262 | ```
263 |
264 | #### deep 的实现原理
265 |
266 | 所谓 deep 其实就是深度递归执行响应式对象里的每个值,让每一个 key 值与副作用函数建立联系,以便后续任何一个响应式的 key 值发生了变化都会触发副作用函数的执行。其实就是上面 traverse 函数主要做的事情。值得注意的时候,reactvie 创建的响应式对象,默认 deep 为 true,并且内部是强制性设置 deep 为 true,其实这也是合理的。
267 |
268 | ##### 通用读取操作函数 traverse
269 |
270 | 接下来我们看看traverse函数如何实现进行深度递归监听的:
271 |
272 | ```javascript
273 | export function traverse(value: unknown, seen?: Set) {
274 | // 如果是普通类型或者不是响应式的对象就直接返回
275 | if (!isObject(value)) {
276 | return value
277 | }
278 | seen = seen || new Set()
279 | if (seen.has(value)) {
280 | // 如果已经读取过就返回
281 | return value
282 | }
283 | // 读取了就添加到集合中,代表遍历地读取过了,避免循环引用引起死循环
284 | seen.add(value)
285 | if (isRef(value)) {
286 | // 如果是 ref 类型,继续递归执行 .value值
287 | // traverse(value.value, seen)
288 | } else if (Array.isArray(value)) {
289 | // 如果是数组类型
290 | for (let i = 0; i < value.length; i++) {
291 | // 递归调用 traverse 进行处理
292 | traverse(value[i], seen)
293 | }
294 | } else if (isPlainObject(value)) {
295 | // 如果是对象,使用 for in 读取对象的每一个值,并递归调用 traverse 进行处理
296 | for (const key in value) {
297 | traverse((value as any)[key], seen)
298 | }
299 | }
300 | return value
301 | }
302 | ```
303 | traverse 函数主要处理各种类型数据递归读取操作,从而当任意属性发生变化时都能够触发回调函数执行。
304 |
305 | #### 新值与旧值的实现原理
306 |
307 | 我们知道在 watch API 的第二参数的回调函数的参数中可以拿到被检测的响应式变量的新值和旧值。
308 |
309 | ```javascript
310 | watch(() => obj.name, (newValue, oldValue) => {
311 | console.log('新值:', newValue, '旧值:', oldValue)
312 | })
313 | ```
314 |
315 | 那么如何获得新值与旧值呢?
316 |
317 | ```javascript
318 | // 定义新值和老值
319 | let oldValue, newValue
320 | const scheduler = () => {
321 | // 在 scheduler 中重新执行 reactive effect 实例对象的 run 方法,得到的是新值
322 | newValue = effect.run()
323 | // 将新值和旧值作为回调函数的参数
324 | cb(newValue, oldValue)
325 | // 更新旧值,不然下一次会得到错误的旧值
326 | oldValue = newValue
327 | }
328 | const effect = new ReactiveEffect(getter, scheduler)
329 | // 手动执行 reactive effect 实例对象的 run 方法,拿到的值就是旧值
330 | oldValue = effect.run()
331 | ```
332 |
333 | 跟 effect API 的实现很类似,通过实例化 ReactiveEffect 类得到实例对象 reactive effect,然后执行 reactive effect 实例对象的 run 方法,拿到的值就是旧值。在执行 reactive effect 实例对象 run 方法的时候,就让副作用函数 getter 中的响应式变量和实例对象 reactive effect 建立了联系,当其中的响应式变量发生更新的时候,会触发 scheduler 调度函数的执行,在调度函数里面重新执行 reactive effect 实例对象的 run 方法得到的值则是新值,然后在执行 watch API 中的回调函数,并把新值与旧值作为回调函数的参数传递给回调函数 cb,再使用新值更新旧值,否则在下一次变更的时候会得到错误的旧值。
334 |
335 | #### 参数 immediate 如何让回调函数立即执行
336 |
337 | 默认情况下,一个 watch 的回调只会在响应式数据发生变化时才执行,但可以通过选项参数 immediate 来指定回调是否需要立即执行。
338 |
339 | ```javascript
340 | watch(() => obj.name, () => {
341 | console.log('数据变化了')
342 | }, {
343 | // 回调函数会在 watch 创建的时候立即执行一次
344 | immediate: true
345 | })
346 | ```
347 |
348 | 在 Vue3 源码当中是把 scheduler 调度函数封装为一个通用函数 job,分别在初始化和变更时执行它。
349 |
350 | ```javascript
351 | // 定义老值
352 | let oldValue
353 | // 提取 scheduler 调度函数为一个独立的 job 函数
354 | const job = () => {
355 | // 在 scheduler 中重新执行 reactive effect 实例对象的run方法,得到的是新值
356 | const newValue = effect.run()
357 | // 将新值和旧值作为回调函数的参数
358 | cb(newValue, oldValue)
359 | // 更新旧值,不然下一次会得到错误的旧值
360 | oldValue = newValue
361 | }
362 |
363 | const scheduler = () => {
364 | // 使用 job 函数作为调度器函数
365 | job()
366 | }
367 | const effect = new ReactiveEffect(getter, scheduler)
368 | if (immediate) {
369 | // 当 immediate 为 true 时立即执行 job,从而触发回调函数执行
370 | job()
371 | } else {
372 | // 手动执行 reactive effect 实例对象的 run 方法,拿到的值就是旧值
373 | oldValue = effect.run()
374 | }
375 | ```
376 |
377 | 回调函数的立即执行和后续的执行本质上没有任何差别。把回调函数包装成一个通用函数 job,当 immediate 为 true 时,就立即执行,因为是立即执行所以是没有旧值的,所以旧值是 undefined,当 immediate 为 false 时,则先运行 reactive effect 实例对象的 run 方法拿到的值保存起来,其实就是旧值,等到响应式数据变更时触发调度函数执行时,就是执行 job 函数,在 job 函数内部再次执行 reactive effect 实例对象的 run 方法再次拿到的值就是新值。
378 |
379 | #### 如何控制调度函数的执行时机
380 |
381 | Vue3 中的 watch API 可以通过其他选项的 flush 参数来指定回调函数的执行时机,例如:
382 |
383 | ```javascript
384 | watch(() => obj.name, () => {
385 | console.log('数据变化了')
386 | }, {
387 | // 回调函数会在 watch 创建的时候立即执行一次
388 | flush: 'pre' // 还可以指定为 'post' 、'sync'
389 | })
390 | ```
391 |
392 | 接下来我们看看 Vue3 源码当中是如何实现指定调度函数的执行时机。
393 |
394 | ```javascript
395 | let scheduler
396 | if (flush === 'sync') {
397 | scheduler = job // 同步执行
398 | } else if (flush === 'post') {
399 | // 将 job 函数放到微任务队列中,从而实现异步延迟执行,注意 post 是在 DOM 更新之后再执行
400 | scheduler = () => queuePostFlushCb(job)
401 | } else {
402 | // flush默认为:'pre'
403 | scheduler = () => {
404 | if (!instance || instance.isMounted) {
405 | // 加入 Pre 队列 组件更新前执行
406 | queuePreFlushCb(job)
407 | } else {
408 | // 使用 'pre' 选项,第一次调用必须在安装组件之前进行,以便同步调用。
409 | job()
410 | }
411 | }
412 | }
413 | ```
414 |
415 | 参数 sync 和 pre 第一次调用都是相同的,都是同步执行,区别是 pre 在组件创建之后则需要使用 Vue3 内部的独立调度器(Scheduler)的API - queuePreFlushCb来执行,通过queuePreFlushCb的执行,回调函数会被放到Vue3内部独立调度器(Scheduler)的 pre 任务队列中,将在组件更新之前执行。
416 |
417 | 使用 post 参数则通过Vue3内部独立调度器(Scheduler)的 API - queuePostFlushCb 来执行,回调函数会被放到 Vue3 内部独立调度器(Scheduler)的 post 任务队列中,讲在组件更新之后执行。
418 |
419 | 那么 Vue3 的调度器,也说是任务队列具体是怎么执行的呢?接下来我们来了解一下Vue3的调度器。
420 |
421 | ### watch 与调度器(scheduler)的关系
422 |
423 | **组件更新队列**
424 |
425 | 对 Vue 有一定了解的都应该知道,Vue 的组件更新是异步的,就是等组件的所有数据更新完成后再执行UI的更新。我们通常写一个 Vue 应用都会用到很多组件,那么多组件进行异步更新的话,就需要一个异步任务队列。
426 |
427 | **组件更新之前的队列**
428 |
429 | 在组件更新队列执行前,需要确保组件数据已经更新到最新,这就需要在异步任务队列之前还要有一个任务队列,也就是组件更新之前的队列。这也是 watch 默认的加入的任务队列,也就是 flush 值为 pre 时加入的异步任务队列。
430 |
431 | **组件更新之后的队列**
432 |
433 | 在组件更新队列执行之后,也是需要执行一些回调的,比如生命周期的Hook函数,所以在组件更新队列执行之后也要有一个任务队列。这个就是 watch 的 flush 参数值为 post 时回调函数加入的异步任务队列。
434 |
435 | ### watchEffect的实现原理
436 |
437 |
438 | 我们来看看源码中的watch API:
439 |
440 | ```javascript
441 | // packages/runtime-core/src/apiWatch.ts
442 | export function watch(
443 | source,
444 | cb,
445 | options
446 | ) {
447 | return doWatch(source, cb, options)
448 | }
449 | ```
450 |
451 | 我们可以看到 watch API 最终调取了 doWatch 这个函数,而 doWatch 所做的事情就是我们上面分析 watch 实现原理的那些代码。
452 |
453 | 继续看看 watchEffect API 的源码:
454 |
455 | ```javascript
456 | export function watchEffect(
457 | effect: WatchEffect,
458 | options?: WatchOptionsBase
459 | ): WatchStopHandle {
460 | return doWatch(effect, null, options)
461 | }
462 | ```
463 |
464 | 可以看到 watchEffect 和 watch 的区别就是 watch 有回调函数而 watchEffect 没有。通过查看它的第一个参数 effect 的类型我们可以知道 watchEffect 的第一个参数是个副作用函数,并且这个副作用函数有一个回调参数 onCleanup。
465 |
466 | ```javascript
467 | export type WatchEffect = (onCleanup: OnCleanup) => void
468 | ```
469 |
470 | 我们再来看这个 onCleanup 参数:
471 |
472 | ```javascript
473 | let cleanup: () => void
474 | let onCleanup: OnCleanup = (fn: () => void) => {
475 | cleanup = effect.onStop = () => {
476 | callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
477 | }
478 | }
479 | ```
480 |
481 | 我们可以看到这个 onCleanup 参数也是一个函数,并且还可以传一个函数作为参数,在 onCleanup 函数的内部再封装一个函数,并且把它赋值给 cleanup 变量和 reactive effect 实例对象上的 onStop 属性, 在封装的这个函数里面再去执行用户传进来的函数。
482 |
483 | 然后我们可以在 getter 的副作用函数可以看到以下代码:
484 |
485 | ```javascript
486 | getter = () => {
487 | // 如果存在 cleanup 则执行 cleanup 函数
488 | if (cleanup) {
489 | cleanup()
490 | }
491 | return callWithAsyncErrorHandling(
492 | source,
493 | instance,
494 | ErrorCodes.WATCH_CALLBACK,
495 | [onCleanup] // 把 onCleanup 传进去供给用户使用
496 | )
497 | }
498 | ```
499 |
500 | 如果存在 cleanup 则执行 cleanup 函数,相当于执行了用户设置的回调函数,在返回的函数中已经把内部定义好的 onCleanup 传进去供给用户使用。
501 |
502 | **为什么要把封装的函数也赋值给 effect 实例对象上的 onStop 属性?**
503 |
504 | 我们回忆一下上面的分析,watch 的实现就是通过 ReactiveEffect 这类的实例化创建了一个 reactive effect 的实例对象。
505 |
506 | ```javascript
507 | const effect = new ReactiveEffect(getter, scheduler)
508 |
509 | // ...
510 |
511 | return () => {
512 | effect.stop()
513 | if (instance && instance.scope) {
514 | remove(instance.scope.effects!, effect)
515 | }
516 | }
517 | ```
518 |
519 | 在最后返回了一个函数,在这个函数里面执行了 reactive effect 实例对象的 stop 方法,也就是清除了这个副作用函数的依赖跟踪。
520 |
521 | 在 ReactiveEffect 类的 stop 方法里面:
522 |
523 | ```javascript
524 | class ReactiveEffect{
525 | stop() {
526 | if (this.active) {
527 | // 清除实例对象本身
528 | cleanupEffect(this)
529 | // 如果实例对象上存在 onStop 方法则执行 onStop 方法
530 | if (this.onStop) {
531 | this.onStop()
532 | }
533 | this.active = false
534 | }
535 | }
536 | }
537 | ```
538 |
539 | 我们可以看到在清除 reactive effect 实例对象的同时,如果存在 onStop 方法则执行 onStop 方法,故 watchEffect 里面把封装的函数也赋值给 reactive effect 实例对象上的 onStop 属性之后,当用户在手动停止 watchEffect 的监听时也会执行用户设置的回调函数 onCleanup。
540 |
541 | ### 如何解决 watch 新老值相同的问题
542 |
543 | ```javascript
544 | const proxy = reactive({ name: 'coboy' })
545 | watch(() => cloneDeep(proxy), (newVal, oldVal) => {
546 |
547 | })
548 | ```
549 | 在克隆的时候,就等于深度遍历了 proxy 的每一个 key,然后每一个 key 就和当前 watch 的 reactive effect 实例对象进行了绑定。这个时候和 reactive effect 实例对象绑定的是原响应式对象,所以原响应式对象发生更改的时候,依然会触发当前 watch 的 reactive effect 实例对象的 scheduler 方法执行。watch 里面新老值返回的都是克隆之后的响应式对象。
550 |
551 | ### 总结
552 |
553 | 副作用函数和响应式数据之间的联系 Vue3 的最新源码是通过 ReactiveEffect 类来实现的,最新的 effect API、watch API、组件更新函数都是通过 ReactiveEffect 类来实现的。都是利用了 ReactiveEffect 类中的 run 方法执行副作用函数及可以通过调度函数控制副作用函数的执行时机。这调度时机的实现本质上是利用了 Vue3 源码中的调度器(Scheduler)和异步的微任务队列。
--------------------------------------------------------------------------------