# mini-vue3-2.0 **Repository Path**: Vennasia/mini-vue3-2.0 ## Basic Information - **Project Name**: mini-vue3-2.0 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-06-04 - **Last Updated**: 2024-11-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Monorepo >NPM7.x以前不支持Monorepo, So 我们这里使用yarn ```shell yarn init -y ``` 会生成 ```json5 //package.json { "name": "mini-vue3", "version": "1.0.0", "main": "index.js", "author": "Chris <757541811@qq.com>", "license": "MIT" } ``` 在生成的`package.json`里添加如下设置 ```json5 { ... "private": true, "workspaces": [ "packages/*" ], ... } ``` + `"private"`: 根包是不需要发布的(`npm publish`) + `"workspaces"`: 指定所有的(子)包都放在哪里 - 这样配置后, 在我们执行`yarn install`或`yarn add`任意包后, 我们`workspaces`里配置的所有包都会在`./node_modules`下生成一个软链接, 这样packages下的所有包都能相互引用 - 但需要注意的是如果搭配ts, ts下即使你package.json中使用了workspaces, 也不能正确自动解析, 需要你tsconfig.json中进行如下搭配配置: ```json5 "moduleResolution": "node", "baseUrl": "./", "paths": { "@vue/*": [ "packages/*/src" ] } ``` 这样待会让rollup配合时开发时,rollup也能根据这个tsconfig配置文件正确解析到package包入口文件进行打包 创建两个包 `packages/reactivity` 和 `packages/shared` 然后分别在它们目录下运行`yarn init -y` ```json5 { "name": "@vue/reactivity", "version": "1.0.0", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", "buildOptions": { "name": "VueReactivity", //只有formats里存在global时会用到, 作为全局变量引入时的名字 "formats": [ "cjs", "esm-bundler", "global" ] }, "license": "MIT" } ``` + 将其各自生成的 `package.json` 中的 `name`字段的值 添加上 `@vue/` 前缀, 方便我们后面像 `import "@vue/reactivity"` 这样引用 + `"module": "dist/reactivity.esm-bundler.js"`, 主要是为了别人在配合打包工具(rollup)用我们这个包的时候, rollup解析包入口时会从这里找(rollup不安装插件是不支持解析commonJS模块的) - `main`字段是node解析包入口时用的,打包工具rollup、webpack等会优先解析`module`字段作为包入口 + `"buildOptions"`字段里设置的信息主要供我们等会打包工具(rollup)打包时用 给根模块安装如下依赖(`--ignore-workspace-root-check`) ```shell yarn add typescript rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-json @rollup/plugin-replace execa --save-dev --ignore-workspace-root-check ``` > 如果要给指定的子包安装依赖 需要这样运行命令: > > yarn workspace @vue/reactivity add @vue/shared@1.0.0 给根包的package.json添加如下scripts ```shell "scripts": { "dev": "node scripts/dev.js", "build": "node scripts/build.js" }, ``` 编写 `scripts/build.js` 脚本 运行脚本 ```shell yarn run build ``` 可以发现每个子包根据不同的`"buildOptions"`配置在每个子包下的`dist`目录生成了对应版本的打包文件 比如根据上面的reactivity包的package.json配置, 我们生成如下 ``` //packages/reactivity/dist reactivity.cjs.js reactivity.esm-bundler.js reactivity.global.js ``` 除了build.js, vue@next 中的script还有很多, 比如 release.js (如果不想手动写发布的脚本,可以使用lerna,它能帮我们管理子包,从创建到发布) >https://github.com/vuejs/vue-next/tree/master/scripts ### 如何测试和debug 在 `packages/reactivity/src/index.ts` 打上断点 用浏览器打开 `example/01.reactive-api-柯里化.html`  发现正确跳到了源码断点处 这是因为: + 我们开启了 tsconfig.json 和 rollup.config.js 的sourcemap选项 + yarn workspaces 让我们能直接通过 `` 软链的方式访问到 `npm run dev` 开发环境rollup打包出来的 `reactivity.global.js` 文件 - rollup之所以能正确解析打包reactivity package中依赖到的 shared package的内容 是因为我们这里rollup集成了ts插件, tsconfig 的path选项配置的映射起到了作用 - 而package子包中`modules`字段是给 其它开发者 在开发它们的包时要引用我们的子包时用的, 这样他们在使用他们的rollup等打包工具进行开发时才能正确解析到适合es6module开发模式下的入口文件 ## vue3 和 vue2对比 响应式原理: proxy <---> defineProperty diff:Vue2基本是全量diff(虽然内部也会标记静态节点), Vue3可以根据patchFlag做diff(能知道哪个属性具体变了、哪个类名具体变了),Vue3还采用了最长递增子序列算法 options Api / compositionApi (支持tree shaking,因为都是一个个函数了) Fragment (如果发现根有多个节点 会自动用Fragment包裹, 甚至可以跟是个文本节点 然后会被Fragment自动包裹) Teleport、Suspense ... ts / flow 自定义渲染器 Monorepo ## 关于reactive reactive 只接受对象 普通值想要变成响应式的 需要使用 ref reactive想要解构 ---> toRefs 只想解构某一个 ---> toRef effect、track、trigger getter时对数组类型的hack处理 对ref的处理 setter设置值时对ref的处理 ## 关于代理 + 重复代理的处理 + 对不同类型的代理 + 做缓存 ## 关于computed computed本身就是一个effect, 当computed接受的getter中用到的响应式属性发生变化, 会将computed属性身上的dirty指标变为true, 这样在重新访问这个computed属性时 会重新计算最新的值 而不是从缓存中拿取 另外vue2 和 vue3 computed原理是不一样的 ,vue2里面计算属性是不会具备搜集依赖的能力的, 但vue3具备 ```tsx /** case2: 在effect中使用计算属性*/ //vue2 和 vue3 computed原理是不一样的 //vue2里面计算属性是不会具备搜集依赖的能力的 effect(()=>{ console.log('renderer'); console.log(myAge.value); }) /* renderer 28 */ age.value = 200; /* renderer 210 */ ``` ## vue项目结构 ``` +---------------------+ | | | @vue/compiler-sfc | | | +-----+--------+------+ | | v v +---------------------+ +----------------------+ | | | | +-------->| @vue/compiler-dom +--->| @vue/compiler-core | | | | | | +----+----+ +---------------------+ +----------------------+ | | | vue | | | +----+----+ +---------------------+ +----------------------+ +-------------------+ | | | | | | | +-------->| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity | | | | | | | +---------------------+ +----------------------+ +-------------------+ ``` + `reactivity`:响应式系统 + `runtime-core`:与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器) + `runtime-dom`: 针对浏览器的运行时。包括DOM API,属性,事件处理等 + `runtime-test`:用于测试的 基于runtime-core + `server-renderer`:用于服务器端渲染 + `compiler-core`:与平台无关的编译器核心 + `compiler-dom`: 针对浏览器的编译模块 + `compiler-ssr`: 针对服务端渲染的编译模块 + `compiler-sfc`: 针对单文件解析 + `size-check`:用来测试代码体积 + `template-explorer`:用于调试编译器输出的开发工具 + `shared`:多个包之间共享的内容 + `vue`:完整版本,包括运行时和编译器 ## 根组件挂载流程  ```jsx const app = createApp(App/*rootComponent*/, {name:'ahhh'/*rootProps*/,age:123}) const p = app.mount('#app') ``` + createApp是封装过的不是真正的那个和平台无关的createApp(`packages/runtime-core/src/apiCreateApp.ts`) + 这个封装过后的createApp是和平台相关的, 会往`runtime-core/renderer.ts/createRenderer`注入平台相关的`nodeOps`和`patchProp`, 这样在和平台无关的`runtime-core/renderer.ts/createRenderer`调用内部的render->patch时就会拿传入对应平台`nodeOps`和`patchProp`进行渲染和更新 以达到 适配不同平台 用户调用的app.mount内部底层调用的其实是 `runtime-core/apiCreateApp` 里的app.mount, 它会先根据传入的 `rootComponent`和`rootProps` 生成组件的虚拟节点 然后会将这个 组件的虚拟节点 渲染到我们 app.mount(`container标识`) 传入的`container标识`生成的container容器中 + `container标识`会被外层的 `runtime-dom/index.ts/createApp` 里的app.mount拦截, 根据不同平台的api和传入的`container标识`创建对应平台的容器 app.mount ---> `runtime-core/renderer.ts/createRenderer中render方法` ---> `runtime-core/renderer.ts/createRenderer中的patch方法` ---> `runtime-core/renderer.ts/createRenderer中的processComponent方法` ---> `runtime-core/renderer.ts/createRenderer中的mountComponent方法` ```jsx /** 1.1 先有实例(instance:ComponentInternalInstance): 初始化组件实例该有的属性, 上面有我们的props、attrs(initialVNodeのprops拆分而来(未赋值,会在setupComponentのinitProps中赋值))、slots等其它属性 * 1.2 建立组件的父子关系instance.parent=父ComponentInternalInstance, * 1.3 instance.root永远指向根组件 * 1.4 instance.appContext指向parent(即父ComponentInternalInstance)的appContext,如果没有parent则说明是根,使用自己vnode.appContext - appContext包括以下: config、mixins、components 、directives、 provides、 app(即那个可以app.mount的app) - appContext包括createApp返回的app,app._context也能逆向访问到appContext * */ const instance/*: ComponentInternalInstance*/ = (initialVNode.component = createComponentInstance( initialVNode, parentComponent, // parentSuspense )) /** 2.1 主要是确保 instance.render 渲染函数是有的(setup执行、Component.render、Component.template), 我们会在后面的setupRenderEffect调用中用来生成真正要渲染的虚拟dom节点 * 2.2 用户传递的props、data等 以及 setup在这里执行后返回的对象(如果返回的是对象而不是renderFn的话)会放在instance,即componentInstance上 * 2.3 会通过proxy代理componentInstance(即在instance上挂载一个instance.proxy代理对象属性),等会会把它作为参数传给 instance.render(instance.proxy) */ setupComponent(instance); /**3. 给instance.update上挂载 渲染effect 并会执行一次*/ setupRenderEffect(instance, initialVNode, container/*, anchor, parentSuspense, isSVG, optimized*/); //↑ 会调用instance.render // 得到subTree 根组件真正要渲染的元素对应的虚拟节点 // patch(null, subTree, container/*,anchor, instance, parentSuspense, isSVG*/) // 此时再次调用patch 如果subTree是一个普通元素(不是组件) 就会走到ProcessElement进行真正的dom(或则其它平台下的元素)渲染了 (调用的api是我们 runtime-dom/nodeOps.ts、patchProp.ts 或则其它平台下的对应的nodeOps和patchProp) ``` ### 组件渲染流程 组件 ---> vnode --> 真实dom ---> 插入到页面 ## 关于文本虚拟节点(文本vnode) 根虚拟节点不可能是文本虚拟节点, `packages/runtime-core/src/h.ts` 并不支持这种形式 ```jsx // type only h('div') // type + props h('div', {}) // type + omit props + children // Omit props does NOT support named slots h('div', []) // array h('div', 'foo') // text h('div', h('br')) // vnode h(Component, () => {}) // default slot // type + props + children h('div', {}, []) // array h('div', {}, 'foo') // text h('div', {}, h('br')) // vnode h(Component, {}, () => {}) // default slot h(Component, {}, {}) // named slots // named slots without props requires explicit `null` to avoid ambiguity h(Component, null, {}) ``` 文本虚拟节点只可能是一个普通元素虚拟节点的孩子(.children) 如果一个普通元素有多个子节点,那么从属于这个普通元素的文本节点会在它所属普通元素虚拟节点"挂载"的时候的 `mountChildren` 过程中被 `normalizeVNode`, 由 `strxxx` 文本字符串 变成真正的文本虚拟节点 `{type:TEXT,children:String('strxxx')}` ```jsx { type: ShapeFlags.ELEMENT|ShapeFlags.ARRAY_CHILDREN, children:[ {type:TEXT,children:String('strxxx')}, ...其它虚拟节点 ] } ``` 但如果一个普通元素只有一个子节点,即这个文本节点的话,那会在这个普通元素被 `createVNode` --- `normalizeChildren`(非normalizeVNode) 的时候, 将这个普通元素的虚拟节点的type设置为 `type = ShapeFlags.TEXT_CHILDREN`(这个普通元素的.shapeFlag会`|`上该type), 并将文本节点经过 `String()` 包裹返回作为.children, 这样这个虚拟节点并不会包裹成文本虚拟节点, 它此时更像是和其父(普通元素虚拟节点)合为一体, 看成一个文本节点元素 ```jsx { type: ShapeFlags.ELEMENT|ShapeFlags.TEXT_CHILDREN, children: String('strxxx') } ``` + 注意此时 `typeof String('strxxx')` 仍然为 string, 而不是 object ## 组件更新  组件更新, 如果 `subtree` 中不含组件节点类型的孩子 是不会走到 `updateComponent` 中去的 组件由于自身声明的响应式属性发生变化而触发的更新是 直接走的 `instance.update`, 也就是 `renderEffect`, 和`updateComponent`无关 So `updateComponent` 只会由父组件更新 走diff流程时 导致子组件更新时会调用 ### 孩子可能有三种情况 patchChildren children has 3 possibilities: text, array or no children. ### full diff --- patchKeyedChildren 优化1: 头和头比  ```jsx /** 优化1:头和头比*/// 1. sync from start // (a b) c // (a b) d e while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = (/*c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) :*/ normalizeVNode(c2[i])) if (isSameVNodeType(n1, n2)) { patch( n1, n2, container, null, // parentComponent, // parentSuspense, // isSVG, // optimized ) } else { break } i++ } ``` 优化2: 尾和尾比  ```jsx /** 优化2:尾和尾比*/// 2. sync from end // a (b c) // d e (b c) while (i <= e1 && i <= e2) { const n1 = c1[e1] const n2 = (/*c2[e2] = optimized ? cloneIfMounted(c2[e2] as VNode) : */normalizeVNode(c2[e2])) if (isSameVNodeType(n1, n2)) { patch( n1, n2, container, null, // parentComponent, // parentSuspense, // isSVG, // optimized ) } else { break } e1-- e2-- } ``` ---- 结余 老的比对完了, 有新的需要添加   ```jsx /** 结余1: 正常序列(有一方已经比对完成了), 老的已经比对完了, 有新增的节点 * 参看 09.full-diff---patchKeyedChildren.html case1-2 */ // 3. common sequence + mount // (a b) // (a b) c // i = 2, e1 = 1, e2 = 2 // (a b) // c (a b) // i = 0, e1 = -1, e2 = 0 if (i > e1) { //老的少 新的多 if (i <= e2) { // i 和 e2之间的就需要插入 const nextPos = e2 + 1 const anchor = nextPos < l2 ? (c2[nextPos]/* as VNode*/).el /** 往前追加 */ : parentAnchor/*现在的流程里只可能是null(由patchElement走到这里的anchor只会为null)*/ /** 往后追加 */ while (i <= e2) { console.log('patch'); patch( null, (/*c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : */normalizeVNode(c2[i])), container, anchor, // parentComponent, // parentSuspense, // isSVG ) i++ } } } ``` 或 新的比对完了, 老的需要删除   ```jsx /** 结余2: 正常序列(有一方已经比对完成了), 新的已经比对完了, 老的有要删除的节点 * 参看 09.full-diff---patchKeyedChildren.html case3.1-3.2 */ // 4. common sequence + unmount // (a b) c // (a b) // i = 2, e1 = 2, e2 = 1 // a (b c) // (b c) // i = 0, e1 = 0, e2 = -1 else if (i > e2) { while (i <= e1) { unmount(c1[i]/*, parentComponent, parentSuspense, true*/) i++ } } ``` 或是乱序的   #### 最长递归子序列优化 > https://en.wikipedia.org/wiki/Longest_increasing_subsequence 当我们乱序比对时 ``` ab [cdeq] fg ab [ecdh] fg ``` 上面的 cd 是连着的 其实不需要额外操作 ``` // 记录的要乱序比对的新的元素 e c d h // newIndexToOldMapIndex [0, 0, 0, 0] // 新元素在老的里的索引位置+1 [4+1,2+1,3+1,0] // [5, 3, 4, 0] ``` + 最长的子序列是3、4, 即老索引为2和3的位置的元素不用动 ### 嵌套组件的更新 ```tsx instance.update = effect(function () { if (!instance.isMounted) { ... }else { console.log('组件更新: ',instance); // updateComponent // This is triggered by mutation of component's own state (next: null) // OR parent calling processComponent (next: VNode) } }) ``` ## 模板编译 >https://vue-next-template-explorer.netlify.app/#%7B%22src%22%3A%22%3Cdiv%3EHello%20World!%3C%2Fdiv%3E%22%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22filename%22%3A%22Foo.vue%22%2C%22prefixIdentifiers%22%3Afalse%2C%22hoistStatic%22%3Afalse%2C%22cacheHandlers%22%3Afalse%2C%22scopeId%22%3Anull%2C%22inline%22%3Afalse%2C%22ssrCssVars%22%3A%22%7B%20color%20%7D%22%2C%22compatConfig%22%3A%7B%22MODE%22%3A3%7D%2C%22whitespace%22%3A%22condense%22%2C%22bindingMetadata%22%3A%7B%22TestComponent%22%3A%22setup-const%22%2C%22setupRef%22%3A%22setup-ref%22%2C%22setupConst%22%3A%22setup-const%22%2C%22setupLet%22%3A%22setup-let%22%2C%22setupMaybeRef%22%3A%22setup-maybe-ref%22%2C%22setupProp%22%3A%22props%22%2C%22vMySetupDir%22%3A%22setup-const%22%7D%2C%22optimizeImports%22%3Afalse%7D%7D ```tsx
{{age}}
``` + div 和 span 是动态的 ```jsx export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock(_Fragment, null, [ _createVNode("div", null, _toDisplayString(_ctx.name), 1 /* TEXT */), _createVNode("p", null, [ _createVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */) ]) ], 64 /* STABLE_FRAGMENT */)) } ``` + `_createVNode` 的第四个参数 就是所谓的 `patchFlag`, 在 `transform` 的过程中帮我们自动加上的, 动态节点都会有 + `1 /* TEXT */`: 表示文本是动态的 + 在 `_createVNode` 时候, 会判断这个节点是否是动态的, 是的话就会让外层的block收集起来 上面之所以用`_createBlock`包一层, 是为了搜集动态节点, 这样我们在diff时只需要diff这些动态节点即可 调用上面codegen生成的`render`, 查看生成的虚拟节点, 会多一个 `dynamicChildren` 属性 ```jsx console.log(render({name: 'ahhh', age: 123})); //可以发现多一个 dynamicChildren 的属性, 它就是搜集到的动态节点数组, 这是一个扁平化的, 会将树的递归拍平成一个数组 ``` ```jsx children: Array(2) 0: {__v_isVNode: true, __v_skip: true, type: "div", props: null, key: null, …} 1: {__v_isVNode: true, __v_skip: true, type: "p", props: null, key: null, …} ``` ```jsx dynamicChildren: Array(2) 0: {__v_isVNode: true, __v_skip: true, type: "div", props: null, key: null, …} 1: {__v_isVNode: true, __v_skip: true, type: "span", props: null, key: null, …} ``` + 会发现 span 直接从 p 里拿了出来直接放在 `dynamicChildren` 中, 这样就不用每次都递归进去比对 - 而vue2.x是标识的静态节点,这样仍然需要递归,只是递归的时候跳过了 todo diff比对的是block里的dynamicChildren 把一个树的比对简化成了数组的比对 ### patchFlags 对不同的动态节点进行描述 ### 静态节点 ```jsx