Monorepo介绍 Monorepo
是管理项目代码的一种方式,只在一个仓库中管理多个模块/包
一个仓库可以维护多个模块,不用到处找仓库
方便版本管理和依赖管理,模块之间的引用和调用都非常方便
缺点:仓库的体积变大,打包和安装依赖都会变慢
例如Vue3源码、element-plus 等都已经采用这种方式来管理项目
了解更多可以查看这个文档:https://www.modb.pro/db/626876
Vue3项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 +---------------------+ | | | @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
:用于测试
server-renderer
:用于服务器端渲染
compiler-core
:与平台无关的编译器核心
compiler-dom
: 针对浏览器的编译模块
compiler-ssr:
针对服务端渲染的编译模块
compiler-sfc
: 针对单文件解析
size-check
:用来测试代码体积
template-explorer
:用于调试编译器输出的开发工具
shared
:多个包之间共享的内容
vue
:完整版本,包括运行时和编译器
Vue3构建流程搭建 vue3采用 monorepo 的方式,目前只有 yarn 支持,所以我们需要使用 yarn 来构建项目
初始化 找一个空文件夹执行命令
然后修改生成的 package.json
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "private" :"true" , "workspaces" :[ "packages/*" ], "name" : "zf-vue3" , "version" : "1.0.0" , "main" : "index.ts" , "license" : "MIT" , "devDependencies" : { "@rollup/plugin-json" : "^4.1.0" , "@rollup/plugin-node-resolve" : "^13.0.0" , "execa" : "^5.1.1" , "rollup" : "^2.52.3" , "rollup-plugin-typescript2" : "^0.30.0" , "typescript" : "^4.3.4" } }
然后执行安装命令
依赖
typescript
支持typescript
rollup
打包工具
rollup-plugin-typescript2
rollup 和 ts的 桥梁
@rollup/plugin-node-resolve
解析node第三方模块
@rollup/plugin-json
支持引入json
execa
开启子进程方便执行命令
声明子文件 reactivity 新建 packages\reactivity\src\index.ts
1 2 3 4 5 6 const Reactivity = {} export { Reactivity }
然后在 packages\reactivity
位置执行 yarn init -y
初始化 package.json
文件
接着修改 package.json
文件
packages/reactivity/package.json
1 2 3 4 5 6 7 8 9 10 11 { "name" : "@vue/reactivity" , "version" : "1.0.0" , "main" : "index.ts" , "module" : "dist/reactivity.esm-bundler.js" , "buildOptions" :{ "name" : "VueReactivity" , "formats" : ["esm-bundler" ,"cjs" ,"global" ] }, "license" : "MIT" }
shared 新建 packages\shared\src\index.ts
1 2 3 4 5 6 const Shared = {} export { Shared }
然后在 packages\shared
位置执行 yarn init -y
初始化 package.json
文件
接着修改 package.json
文件
packages/shared/package.json
1 2 3 4 5 6 7 8 9 10 11 { "name" : "@vue/shared" , "version" : "1.0.0" , "main" : "index.ts" , "module" : "dist/shared.esm-bundler.js" , "buildOptions" :{ "name" : "VueShared" , "formats" : ["esm-bundler" ,"cjs" ] }, "license" : "MIT" }
编译脚本、rollup、ts配置 package.json 添加脚本配置
1 2 3 4 "scripts": { "dev":"node scripts/dev.js", "build":"node scripts/build.js" },
scripts/dev.js
这个文件只做某个包的打包,给 rollup 传入TARGET环境变量,生成 rollup 配置
1 2 3 4 5 6 7 8 9 10 const execa = require ('execa' );const target = "reactivity" ;build(target) async function build (target ) { await execa('rollup' , ['-cw' , `--environment` , `TARGET:${target} ` ], { stdio : 'inherit' }); }
scripts/build.js
这个文件用来打包 packages 文件夹下所有的包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const fs = require ('fs' );const execa = require ('execa' ); const targets = fs.readdirSync("packages" ).filter(f => { if (!fs.statSync(`packages/${f} ` ).isDirectory()){ return false }else { return true } }) async function build (target ) { await execa("rollup" ,["-c" ,`--environment` ,`TARGET:${target} ` ],{ stdio: "inherit" }) } function runParallel (targets,buildFn ) { const result = [] targets.forEach(item => { result.push(buildFn(item)) }) return Promise .all(result) } runParallel(targets,build).then(()=> { console .log("打包完成" ) })
rollup.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import path from "path" ;import json from "@rollup/plugin-json" ;import resolvePlugin from "@rollup/plugin-node-resolve" ;import ts from "rollup-plugin-typescript2" ;const packagesDir = path.resolve(__dirname, "packages" );const packageDir = path.resolve(packagesDir, process.env.TARGET);const resolve = (p ) => path.resolve(packageDir, p);const pkg = require (resolve("package.json" ));const outputConfig = { "esm-bundler" : { file: resolve(`dist/${process.env.TARGET} .esm-bundler.js` ), format: "es" , }, cjs: { file: resolve(`dist/${process.env.TARGET} .cjs.js` ), format: "cjs" , }, global : { file: resolve(`dist/${process.env.TARGET} .global.js` ), format: "iife" , }, }; const options = pkg.buildOptions;function createConfig (format, output ) { output.name = options.name; output.sourcemap = true ; return { input: resolve(`src/index.ts` ), output, plugins: [ json(), ts({ tsconfig:path.resolve(__dirname, "tsconfig.json" ), }), resolvePlugin(), ], }; } export default options.formats.map((format ) => createConfig(format, outputConfig[format]));
tsconfig.json
在根目录执行 npx tsc --init
先初始化一个 tsconfig.json
文件,然后添加如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "compilerOptions" : { "target" : "ESNEXT" , "module" : "ESNEXT" , "strict" : false , "esModuleInterop" : true , "skipLibCheck" : true , "forceConsistentCasingInFileNames" : true , "moduleResolution" : "node" , "baseUrl" : "." , "paths" : { "@vue/*" : ["packages/*/src" ] } } }
至此我们就可以来尝试打包一下,输入命令
对比打包后的文件内容可以发现两个包下面生成的打包文件是不一样的
包之间的互相引用 当我们执行 yarn install
后会自动的再 node_modules 中生成软连接
后面的箭头表示这是一个软连接。然后我们在代码里使用如下方式引入时
1 2 3 4 5 6 7 8 import { Shared } from "@vue/shared" const Reactivity = {} export { Reactivity }
@vue/shared 指向的就是这个包所在的真实文件地址
reactive Api实现 四个核心的API 1 import { reactive,shallowReactive,readonly ,shallowReadonly } from "vue"
reactive:深层次的响应对象
shallowReactive:浅层响应对象,只会吧对象的第一层变成响应式的
readonly:对象是只读的
shallowReadonly:浅层对象只读
开启sourceMap 将 tsconfig.json
文件中的 sourceMap 打开并且设置成true,方便我们调试源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "compilerOptions" : { "target" : "ESNEXT" , "module" : "ESNEXT" , "strict" : false , "esModuleInterop" : true , "skipLibCheck" : true , "forceConsistentCasingInFileNames" : true , "moduleResolution" : "node" , "baseUrl" : "." , "paths" : { "@vue/*" : ["packages/*/src" ] }, "sourceMap" : true } }
目录结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ├── example │ └── 1.reactivity-api.html // 测试文件 ├── package.json ├── packages │ ├── reactivity │ │ ├── package.json │ │ └── src │ │ ├── baseHandlers.ts │ │ ├── index.ts // reactive核心方法文件,只做导出操作 │ │ └── reactive.ts │ └── shared │ ├── package.json │ └── src │ └── index.ts // 公共功能 ├── rollup.config.js ├── scripts │ ├── build.js │ └── dev.js └── tsconfig.json
shared/src/index.ts 这个文件专门放置一些公共的方法
1 2 3 4 5 6 7 8 9 export function isObject (value ) { return typeof value === 'object' && value !== null ; } export function mergeObjects <T extends object , U extends object >(obj1: T, obj2: U ): T & U { return Object .assign(obj1, obj2); }
reactivity/src/index.ts 1 2 3 4 5 6 7 export { reactive, shallowReactive, readonly, shallowReadonly, } from "./reactive" ;
reactivity/src/reactive.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import { isObject } from "@vue/shared" ;import { mutableHandlers, readonlyHandlers, shallowReactiveHandlers, shallowReadonlyHandlers, } from "./baseHandlers" ; export function reactive (target ) { return createReactiveObject(target, false , mutableHandlers); } export function shallowReactive (target ) { return createReactiveObject(target, false , shallowReactiveHandlers); } export function readonly (target ) { return createReactiveObject(target, true , readonlyHandlers); } export function shallowReadonly (target ) { return createReactiveObject(target, true , shallowReadonlyHandlers); } const reactiveMap = new WeakMap ();const readlonlyMap = new WeakMap ();export function createReactiveObject (target, isReadonly, baseHandlers ) { if (!isObject(target)) return target; const proxyMap = isReadonly ? readlonlyMap : reactiveMap; const existProxy = proxyMap.get(target); if (existProxy) return existProxy; const proxy = new Proxy (target, baseHandlers); proxyMap.set(proxy, target); return proxy; }
reactivity/src/baseHandlers.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 import { isObject, mergeObjects } from "@vue/shared" ;import { reactive, readonly } from "./reactive" ;const readonlyObj = { set: (target, key ) => { console .warn(`key:${key} set 失败,因为这个对象是只读的` ); }, }; function createGetter (isReadonly = false , isShallow = false ) { return function (target, key, receiver ) { const res = Reflect .get(target, key, receiver); if (!isReadonly) { } if (isShallow) return res; if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res); } return res; }; } function createSetter (isShallow = false ) { return function (target, key, value, receiver ) { const res = Reflect .set(target, key, value, receiver); return res }; } const get = createGetter();const shallowGet = createGetter(false , true );const readonlyGet = createGetter(true );const shallowReadonlyGet = createGetter(true , true );const set = createSetter();const shallowSet = createSetter(true );export const mutableHandlers = { get, set, }; export const shallowReactiveHandlers = { get: shallowGet, set: shallowSet, }; export const readonlyHandlers = mergeObjects( { get: readonlyGet, }, readonlyObj ); export const shallowReadonlyHandlers = mergeObjects( { get: shallowReadonlyGet, }, readonlyObj );
测试reactive Api example/1.reactive.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > Document</title > </head > <body > <script src ="../node_modules/@vue/reactivity/dist/reactivity.global.js" > </script > <script > let { reactive, shallowReactive, readonly, shallowReadonly } = VueReactivity; let state = shallowReadonly({ name:"李四" , age:{ n:18 } }); state.name = 20 console .log(state.age); </script > </body > </html >
Effect依赖收集 effect.ts packages/reactivity/src/effect.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 export function effect (fn: Function , options: any = {} ) { const effect = createReactiveEffect(fn, options); if (!options.lazy) { effect(); } return effect; } let uid = 0 ;let activeEffect; const effectStack = []; function createReactiveEffect (fn, options ) { const effect = function reactiveEffect ( ) { try { activeEffect = effect; fn(); effectStack.push(effect); } finally { effectStack.pop(); activeEffect = effectStack[effectStack.length - 1 ]; } }; effect.id = uid++; effect._isEffect = true ; effect.row = fn; effect.options = options; return effect; } const targetMap = new WeakMap ()export function track (target, type, key ) { if (!activeEffect) return let depTarget = targetMap.get(target) if (!depTarget){ targetMap.set(target, depTarget = new Map ()) } let depMap = depTarget.get(key) if (!depMap){ depTarget.set(key, depMap = new Set ()) } depMap.add(activeEffect) }
baseHandlers.ts packages/reactivity/src/baseHandlers.ts
修改 createGetter 方法,在get时调用 effect.js 中的 track 方法,传入 target,type,key 进行响应式依赖收集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 + import { track } from "./effect"; + import { TrackOpTypes } from "./operators"; // 拦截get的方法,柯里化 function createGetter(isReadonly = false, isShallow = false) { /** * 这个内部的方法参数来自于读取某个对象是,proxy传递进来的 * target: 当前对象本身 * key: 当前读取的key * receiver: 当前的代理对象 */ return function (target, key, receiver) { // 这里使用Reflect.get来获取对象,相当于 target[key] // 后续Object上的方法,会被迁移到Reflect上去,Reflect.getProptotypeof() // target[key] = value 的方式可能会设置失败,但是并不会返回报错,也没有返回标识来表示是否成功 // Reflect 方法具备返回值,所以这里要使用 Reflect 来取值和set值 const res = Reflect.get(target, key, receiver); if (!isReadonly) { // 进行依赖收集,只有不是只读的对象才会进行依赖收集 + track(target,TrackOpTypes.GET,key) } // 如果是shallow则只返回拿到的值,不进行深层次代理 if (isShallow) return res; // 如果拿到的返回值仍然是一个对象,则进行递归响应式处理 // 这里和vue2不同,vue2是上来就进行对象的递归响应式处理 // vue3则是懒代理,只有到读取到这个对象的某个属性时,才会对这个对象进行响应式处理 if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res); } return res; }; }
operators.ts packages/reactivity/src/operators.ts
定义一个枚举,用于区分场景
1 2 3 4 export const enum TrackOpTypes { GET }
测试effect example/2.effect.html
1 2 3 4 5 6 7 8 9 <div id ="app" > </div > <script src ="../node_modules/@vue/reactivity/dist/reactivity.global.js" > </script > <script > let { effect,reactive } = VueReactivity let state = reactive({name :'张三' ,age :18 }) effect(()=> { app.innerText = `${state.name} + ${state.age} ` }) </script >
最终生成的 targetMap 结构
触发更新 修改createSetter方法 修改 /reactivity/src/baseHandlers.ts 文件中的 createSetter 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import { hasChange, hasOwn, isArray, isIntegerKey, isObject, mergeObjects, } from "@vue/shared" ; import { reactive, readonly } from "./reactive" ;import { track, trigger } from "./effect" ;import { TrackOpTypes, TriggerOpTypes } from "./operators" ;function createSetter (isShallow = false ) { return function (target, key, value, receiver ) { const oldValue = Reflect .get(target, key, receiver); let hadKey = isArray(target) && isIntegerKey(key) ? Number (key) < target.length : hasOwn(target, key); const res = Reflect .set(target, key, value, receiver); if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value); } else { if (hasChange(oldValue, value)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue); } } return res; }; }
reactivity/src/operators.ts operators 文件中增加了一个枚举类,用于区分时新增一个属性还是修改一个属性
1 2 3 4 5 export const enum TriggerOpTypes { ADD, SET, }
shared/src/index.ts 公共方法增加了一些方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 export function isObject (value ) { return typeof value === "object" && value !== null ; } export function mergeObjects <T extends object , U extends object >( obj1: T, obj2: U ): T & U { return Object .assign(obj1, obj2); } export function isArray <T >(value: unknown ): value is T [] { return Array .isArray(value); } export function isFunction (value: unknown ): value is Function { return typeof value === "function" ; } export function isNumber (value: unknown ): value is number { return typeof value === "number" ; } export function isString (value: unknown ): value is string { return typeof value === "string" ; } export function isIntegerKey (key ) { return parseInt (key) + "" === key; } export function hasOwn (obj, key ) { return Object .prototype.hasOwnProperty.call(obj, key); } export function hasChange (oldValue, newValue ) { return oldValue !== newValue; } export function isSymbol (value ) { return typeof value === "symbol" ; }
reactivity/src/effect.ts effect 中去增加一个 tagger 方法,此方法会根据传递进来的key和target,找到对应的 dep 进行更新,也就是会重新触发这个属性对应的 effect,从而实现页面更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import { isArray, isIntegerKey, isSymbol } from "@vue/shared" ;import { TriggerOpTypes } from "./operators" ;export function trigger (target, type, key?, newValue?, oldValue? ) { let depMap = targetMap.get(target); if (!depMap) return ; let effects = []; const add = (effectsToAdd ) => { if (effectsToAdd) { effectsToAdd.forEach((effect ) => { effects.push(effect); }); } }; if (key === "length" && Array .isArray(target)) { console .log(depMap, "depMapdepMap" ); depMap.forEach((deps, key ) => { if (!isSymbol(key) && (key === "length" || key > newValue)) { add(deps); } }); } else { if (key !== undefined ) { add(depMap.get(key)); } switch (type) { case TriggerOpTypes.ADD: if (isArray(target) && isIntegerKey(key)) { add(depMap.get("length" )); } } } effects.forEach((effect ) => effect()); }
测试触发更新 1 2 3 4 5 6 7 8 9 10 11 12 13 <div id ="app" > </div > <script src ="../node_modules/@vue/reactivity/dist/reactivity.global.js" > </script > <script > let { effect, reactive } = VueReactivity; let state = reactive({ name : "张三" , age : 18 , arr : [1 , 2 , 3 ] }); effect(() => { app.innerHTML = `${state.arr} + ${state.arr.length} ` ; }); setTimeout (() => { state.arr.length = 100; }, 1000); </script >
ref Api实现 四个API ref 有关的API共有四个
ref:将一个普通数据类型转成响应式的,如果穿过来的是一个对象,则会调用 reactive 进行响应式处理
shallowRef:浅层的ref
toRef:用法 let refKey =toRef(object,key)
将某个对象中的某一个属性变成响应式的,此时修改 refKey.value = xxx
相当于修改的就是 object.key = xxx
toRefs:用法 let {key1,key2} = toRefs(object)
将某个对象中的所有属性变成响应式的,在对象解构时可以用这个方法,否则解构出来的属性会丢失响应式
reactivity/src/ref.ts 下面是源码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import { hasChange, isArray, isObject } from "@vue/shared" ;import { track, trigger } from "./effect" ;import { TrackOpTypes, TriggerOpTypes } from "./operators" ;import { reactive } from "./reactive" ;export function ref (value ) { return createRef(value); } export function shallowRef (value ) { return createRef(value, true ); } function convert (value ) { return isObject(value) ? reactive(value) : value; } class RefImpl { public _value: any; public __v_isRef = true ; constructor (public rowValue, public shallow ) { this ._value = shallow ? rowValue : convert(rowValue); } get value () { track(this , TrackOpTypes.GET, "value" ); return this ._value; } set value (newValue ) { if (hasChange(this ._value, newValue)) { this .rowValue = newValue; this ._value = newValue; trigger(this , TriggerOpTypes.SET, "value" , newValue); } } } export function createRef (value, shallow = false ) { return new RefImpl(value, shallow); } class ObjectRefImpl { public __v_isRef = true ; constructor (public target, public key ) { } get value (){ return this .target[this .key]; } set value (newValue ){ this .target[this .key] = newValue; } } export function toRef (target, key ) { return new ObjectRefImpl(target, key); } export function toRefs (object ) { const res = isArray(object) ? new Array (object.length) : {} for (let key in object) { res[key] = toRef(object, key); } return res }
reactivity/src/index.ts 在 index 文件导入四个ref的四个方法
1 export {ref,shallowRef,toRef,toRefs} from "./ref"
测试ref 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <div id ="app" > </div > <div id ="app1" style ="width: 100px;height: 100px;" > </div > <script src ="../node_modules/@vue/reactivity/dist/reactivity.global.js" > </script > <script > let { ref,effect,reactive,toRef,toRefs } = VueReactivity; let color = ref("red" ) effect(()=> { app1.style.backgroundColor = color.value }) setTimeout (()=> { color.value = "blue" },1000) let state = reactive({ name:"王五" , age:12 }) let {name,age} = toRefs(state) effect(()=> { app.innerHTML = `${name.value} - ${age.value} ` }) setTimeout (()=> { name.value = "张三123" },1000) </script >
computed源码实现 实现流程
当访问计算属性的value时要把当前计算属性所依赖的effect收集起来
当计算属性中所依赖的属性发生变化时,会走set方法,会遍历所依赖属性收集的effect并执行
执行effect中如果发现effect的属性中存在scheduler方法,则会执行scheduler方法
在计算属性的scheduler方法中会重置_dirty的值,并执行trigger方法_
在trigger方法中会执行当前计算属性收集的effect,从而重新读取value属性触发get方法
执行get方法时因为已经重置了_dirty的值,所以会重新执行getter方法得到最新值并返回
触发get方法后会再次收集依赖等待下次调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import {isFunction} from "@vue/shared" ;import {effect, track, trigger} from "./effect" ;import {TrackOpTypes, TriggerOpTypes} from "./operators" ;class ComputedRef { public _dirty = true public effect public _value constructor (getter,public setter ) { this .effect = effect(getter,{ lazy:true , scheduler:()=> { if (!this ._dirty){ this ._dirty = true trigger(this ,TriggerOpTypes.SET,"value" ) } } }) } get value (){ if (this ._dirty){ this ._value = this .effect() this ._dirty = false } track(this ,TrackOpTypes.GET,"value" ) return this ._value } set value (newValue ){ this .setter(newValue) } } export function computed (getterOrOptions ) { let getter; let setter; if (isFunction(getterOrOptions)){ getter = getterOrOptions setter = ()=> { console .warn("computed value must be readonly" ) } }else { getter = getterOrOptions.get setter = getterOrOptions.set } return new ComputedRef(getter,setter) }
修改 packages/reactivity/src/effect.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 import { isArray, isIntegerKey, isSymbol } from "@vue/shared"; import { TriggerOpTypes } from "./operators"; export function effect(fn: Function, options: any = {}) { const effect = createReactiveEffect(fn, options); if (!options.lazy) { effect(); } return effect; } let uid = 0; let activeEffect; // 当前正在操作的effect const effectStack = []; // 使用一个栈来存储effect // 创建一个响应式的effect,根据不同的属性创建不同的effect方法 function createReactiveEffect(fn, options) { const effect = function reactiveEffect() { try { activeEffect = effect; // 当前正在操作的effect effectStack.push(effect); + return fn(); } finally { effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; } }; effect.id = uid++; // 制作一个effect标识,用于区分effect effect._isEffect = true; // 用于标识这个是响应式effect effect.row = fn; // 保留effect对应的原函数 effect.options = options; // 保留effect的属性 return effect; } // 依赖收集方法,在 effect 中读取属性时就会触发 get 方法 // 在get方法中将对象本身,以及当前的key传递过来 // 然后和effect进行对应关联 const targetMap = new WeakMap(); export function track(target, type, key) { if (!activeEffect) return; // 使用如下一个结构来进行属性和effect关联 // new WeakMap( target, new Map( key, [effect1,effect2] )) let depTarget = targetMap.get(target); if (!depTarget) { targetMap.set(target, (depTarget = new Map())); } // 拿到的是一个set,存放的是属性对应的多个effect let depMap = depTarget.get(key); if (!depMap) { depTarget.set(key, (depMap = new Set())); } depMap.add(activeEffect); } // 更新依赖的方法 export function trigger(target, type, key?, newValue?, oldValue?) { // 得到当前对象对应的map let depMap = targetMap.get(target); // 判断这个target是否收集过依赖 if (!depMap) return; // 将所有的effect放在一个数组中,最终一起执行 let effects = []; const add = (effectsToAdd) => { console.log(effectsToAdd,'effectsToAddeffectsToAdd') if (effectsToAdd) { effectsToAdd.forEach((effect) => { effects.push(effect); }); } }; // 判断当前操作的是否是数组 // 并且修改的key是legth,则需要更新数组所收集到的effect if (key // console.log(depMap, "depMapdepMap"); // Map类型数据进行forEach遍历是,第一个是键值对的值,第二个是键 depMap.forEach((deps, key) => { // key > newValue // 例如我effect中使用了 state.arr[2],则收集到的依赖就会有一个key是2 // 如果我更新时 state.arr.length = 1,则也要更新这个数组target所收集的依赖effect if (!isSymbol(key) && (key add(deps); } }); } else { // 这里可能是对象 if (key !== undefined) { add(depMap.get(key)); } // 如果是这种情况: state.arr[100] = 1 // 这种情况表示更新的是数组的某个索引,此时key就是100 // 但是100并不在原有数组的属性上,所以type是ADD // 去更新这个数组对应的length属性对应的effect switch (type) { case TriggerOpTypes.ADD: if (isArray(target) && isIntegerKey(key)) { add(depMap.get("length")); } } } + effects.forEach((effect) => { + if(effect.options.scheduler){ + effect.options.scheduler() + }else{ + effect() + } }); }
runtimeDom和runtimeCore
runtimeDom专门用来用来操作DOM,例如style属性的添加和删除,class类的添加和删除,属性、事件等等
runtimeCore专门用来生成虚拟节点并挂载,会调用runtimeDom中提供的方法
新建runtime-dom包 新建 packages/runtime-dom
文件夹
然后创建 src/index.ts
文件
1 2 3 export default { name:"runtime-dom" }
新建一个package.json
1 2 3 4 5 6 7 8 9 10 11 { "name" : "@vue/runtime-dom" , "version" : "1.0.0" , "main" : "index.js" , "module" : "dist/runtime-dom.esm-bundler.js" , "buildOptions" :{ "name" : "VueRuntimeDom" , "formats" : ["esm-bundler" ,"cjs" ,"global" ] }, "license" : "MIT" }
新建runtime-core包 新建 packages/runtime-core
文件夹
然后创建 src/index.ts
文件
1 2 3 export default { name:"runtime-core" }
新建一个package.json
1 2 3 4 5 6 7 8 9 10 11 { "name" : "@vue/runtime-core" , "version" : "1.0.0" , "main" : "index.js" , "module" : "dist/runtime-core.esm-bundler.js" , "buildOptions" :{ "name" : "VueRuntimeCore" , "formats" : ["esm-bundler" ,"cjs" ,"global" ] }, "license" : "MIT" }
添加依赖 执行 yarn install
将 runtime-dom
和 runtime-core
加入到 node_modules
中
1 2 3 yarn install yarn build
观察是否生成 dist 文件,有说明包创建成功
patchNode 新建 runtime-dom/src/nodeOpts.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export const nodeOpts = { createElement: (target ) => document .createElement(target), remove: (target ) => { const parent = target.parentNode if (parent) { parent.removeChild(target) } }, insert: (target, parent, anchor = null ) => { parent.insertBefore(target, anchor) }, querySelector: (selector ) => document .querySelector(selector), setElementText: (el, text ) => el.textContent = text, createText: (text ) => document .createTextNode(text), setText: (node, text ) => node.nodeValue = text }
patchProps 新建 runtime-dom/src/patchProps.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import {patchStyle} from "./modules/style" ;import {patchClass} from "./modules/class" ;import {patchEvent} from "./modules/event" ;import {patchAttr} from "./modules/attr" ;export const patchProps = (el, key, prevValue, newValue ) => { switch (key) { case "style" : patchStyle(el,prevValue,newValue) break ; case "class" : patchClass(el,newValue) break ; default : if (/on[^a-z]/ .test(key)){ patchEvent(el,key,newValue) }else { patchAttr(el,key,newValue) } break ; } }
runtime-dom/src/modules/style.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export function patchStyle (el, prev, next ) { let style = el.style if (next == null ){ el.removeAttribute("style" ) return } if (prev){ for (const key in prev) { if (!next[key]){ style[key] = '' } } } for (const key in next) { style[key] = next[key] } }
runtime-dom/src/modules/class.ts
1 2 3 4 5 6 export function patchClass (el, value ) { if (value == null ){ value = "" } el.classList = value }
runtime-dom/src/modules/event.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 export function patchEvent (el, key, value ) { const invokers = el._vei || (el._vei = {}) const exists = invokers[key] if (value && exists){ exists.value = value }else { const eventName = key.slice(2 ).toLowerCase() if (value){ let invoker = invokers[key] = createInvoker(value) el.addEventListener(eventName, invoker) }else { el.removeEventListener(eventName, exists) invokers[key] = undefined } } } function createInvoker (value ) { const invoker = (e )=> { invoker.value(e) } invoker.value = value return invoker }
runtime-dom/src/modules/attr.ts
1 2 3 4 5 6 7 export function patchAttr (el, key, value ) { if (value == null ){ el.removeAttribute(key) return } el.setAttribute(key,value) }
runtime-dom导出createApp方法 我们最终要使用 createApp 这个方法来挂载页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <div id ="app" > </div > <script src ="../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js" > </script > <script > let {createApp} = VueRuntimeDom let App = { render ( ) { console .log("render" ) } } let app = createApp(App) app.mount("#el" ) </script > </body > </html >
runtime-dom/src/index.ts
创建createApp方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import {patchProps} from "./patchProps" import {nodeOpts} from "./nodeOpts" import {mergeObjects} from "@vue/shared" ;import {createRender} from "@vue/runtime-core" ;const renderOptions = mergeObjects({patchProps}, nodeOpts)export function createApp (rootComponent, rootProps = null ) { const app: any = createRender(renderOptions).createApp(rootComponent, rootProps) const { mount } = app app.mount = (container ) => { const contain = nodeOpts.querySelector(container) contain.innerHTML = "" mount(contain) } return app }
runtime-core实现createRender方法 新建 runtime-core/src/renderer.ts
1 2 3 4 5 6 7 8 9 10 11 import {createAppApi} from "./apiCreateApp" ;export function createRender (renderOptions ) { const render = (vnode,container )=> { } return { createApp:createAppApi(render) } }
新建 runtime-core/src/apiCreateApp.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export function createAppApi (render ) { return function createApp (rootComponent, rootProps ) { return { mount (container ) { let vnode = {} render(vnode,container) console .log(container) console .log(rootComponent) console .log(rootProps) } } } }
导出 createRender
方法
runtime-core/src/index.ts
1 2 3 4 5 import {createRender} from "./renderer" ;export { createRender }
最后测试一下看看 mount 方法的打印是否成功
接下来我们专注实现如何创建虚拟节点
组件创建流程 runtime-core/src/apiCreateApp.ts 此方法返回mount方法,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import {createVNode} from "./vnode" ;export function createAppApi (render ) { return function createApp (rootComponent, rootProps ) { const app = { _props: rootProps, _component: rootComponent, _container: null , mount (container ) { let vnode = createVNode(rootComponent, rootProps) render(vnode, container) app._container = container } } return app } }
runtime-core/src/vnode.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import {isArray, isObject, isString} from "@vue/shared" ;import {ShapeFlags} from "@vue/shared/src/shapeFlags" ;export const createVNode = (type , props, children = null ) => { const shapeFlag = isString(type ) ? ShapeFlags.ELEMENT : (isObject(type ) ? ShapeFlags.STATEFUL_COMPONENT : 0 ) const vnode = { __v_isVnode: true , type , props, children, el: null , key: props && props.key, shapeFlag, } normalizeChildren(vnode, children) return vnode } function normalizeChildren (vnode, children ) { let type = 0 if (children === null ) { } else if (isArray(children)) { type = ShapeFlags.ARRAY_CHILDREN } else { type = ShapeFlags.TEXT_CHILDREN } vnode.shapeFlag |= type }
shared/src/shapeFlags.ts 使用位运算符来判断组件的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export const enum ShapeFlags { ELEMENT = 1 , FUNCTIONAL_COMPONENT = 1 << 1 , STATEFUL_COMPONENT = 1 << 2 , TEXT_CHILDREN = 1 << 3 , ARRAY_CHILDREN = 1 << 4 , SLOTS_CHILDREN = 1 << 5 , TELEPORT = 1 << 6 , SUSPENSE = 1 << 7 , COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8 , COMPONENT_KEPT_ALIVE = 1 << 9 , COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT }
然后再 shared/src/index.ts
文件中导出
1 import * as shapeFlags from "./shapeFlags"
runtime-core/src/renderer.ts 在mount函数中会调用下面的render方法,并传递过来vnode虚拟节点,拿到不同的虚拟节点,创建对应的真实节点,然后将真实节点渲染到容器中
然后判断是元素还是组件,进行组件挂载
判断有没有上一次的虚拟节点,如果没有,则就是初始化,否则是更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import {createAppApi} from "./apiCreateApp" ;import {ShapeFlags} from "@vue/shared/src/shapeFlags" ;import {createComponentInstance, setupComponent} from "./component" ;export function createRender (renderOptions ) { const setupRenderEffect = () => { } const mountComponent = (initialVNode, container ) => { const instance = (initialVNode.component = createComponentInstance(initialVNode)) setupComponent(instance) setupRenderEffect() } const processComponent = (n1, n2, container ) => { if (n1 === null ) { mountComponent(n2, container) } else { } } const patch = (n1, n2, container ) => { const {shapeFlag} = n2 if (shapeFlag & ShapeFlags.ELEMENT) { } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { processComponent(n1, n2, container) } } const render = (vnode, container ) => { patch(null , vnode, container) } return { createApp: createAppApi(render) } }
runtime-core/src/component.ts 在挂载组件是创建实例,并且调用组件的setup方法和render方法,都在此文件中进行
调用setup函数时,会传递两个参数:
定义的props属性数据
context上下文对象,包含:attrs、slots、emit、expose
调用render函数时,会传递一个proxy代理对象,通过这个代理对象,可以同时访问setupState、data、props上的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import {ShapeFlags} from "@vue/shared/src/shapeFlags" ;import {PublicInstanceProxyHandlers} from "./PublicInstanceProxyHandlers" ;export const createComponentInstance = (vnode ) => { const instance = { vnode, type : vnode.type, props: {}, attrs: {}, slots: {}, ctx: {}, data: { b: 2 }, render: null , setupState: { c: 3 }, isMounted: false } instance.ctx = {_ : instance} return instance } export const setupComponent = (instance ) => { let {props, children, shapeFlag} = instance.vnode instance.props = props instance.children = children let isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT if (isStateful) { setupStatefulComponent(instance) } } function setupStatefulComponent (instance ) { let Component = instance.type let {setup} = Component let setupContext = createSetupContext(instance) setup(instance.props, setupContext) instance.proxy = new Proxy (instance.ctx, PublicInstanceProxyHandlers as any ) Component.render(instance.proxy) } function createSetupContext (instance ) { return { attrs: instance.attrs, slots: instance.slots, emit: () => { }, expose: () => { } } }
runtime-core/src/PublicInstanceProxyHandlers.ts 对render函数的参数进行代理,访问一个属性时,可以从props,setupState,data中获取任意一个只要存在的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import {hasOwn} from "@vue/shared" ;export const PublicInstanceProxyHandlers = { get ({_: instance}, key ) { if (key[0 ] === '$' ) { return } let {props, data, setupState} = instance if (hasOwn(setupState, key)) { return setupState[key] } else if (hasOwn(data, key)) { return data[key] } else if (hasOwn(props, key)) { return props[key] } }, set ({_: instance}, key, value ) { let {props, data, setupState} = instance if (hasOwn(props, key)) { props[key] = value } else if (hasOwn(data, key)) { data[key] = value } else if (hasOwn(setupState, key)) { setupState[key] = value } } }
测试setup函数和render函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <div id ="app" > </div > <script src ="../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js" > </script > <script > let {createApp} = VueRuntimeDom let App = { setup (props, context ) { console .log("setup" ) console .log(props, context) }, render (proxy ) { console .log(proxy) console .log(proxy.name) console .log(proxy.b) console .log(proxy.c) } } let app = createApp(App, {name : "李四" , age : 18 }) app.mount("#app" ) </script > </body > </html >
运行结果
通过结果分析
李四 是从调用createApp函数时,传递的参数中获取的
2 是从data中获取的
3 是从setupState中获取的
setupState是setup函数的返回值,目前我们还没有处理,只是暂时写死的,后面我们会处理
处理setup的返回值 runtime-core/src/component.ts 修改 setupStatefulComponent
方法,增加判断是否存在setup,如果存在,则获取setup的返回值,交给 handleSetupResult
方法处理,最终调用 finishComponentSetup
方法处理render
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 import {ShapeFlags} from "@vue/shared/src/shapeFlags" ;import {PublicInstanceProxyHandlers} from "./PublicInstanceProxyHandlers" ;import {isFunction, isObject} from "@vue/shared" ;export const createComponentInstance = (vnode ) => { const instance = { vnode, type : vnode.type, props: {}, attrs: {}, slots: {}, ctx: {}, data: {}, render: null , setupState: {}, isMounted: false } instance.ctx = {_ : instance} return instance } export const setupComponent = (instance ) => { let {props, children, shapeFlag} = instance.vnode instance.props = props instance.children = children let isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT if (isStateful) { setupStatefulComponent(instance) } } function setupStatefulComponent (instance ) { let Component = instance.type let {setup} = Component if (setup) { let setupContext = createSetupContext(instance) let setupResult = setup(instance.props, setupContext) handleSetupResult(instance, setupResult) } else { finishComponentSetup(instance) } } function handleSetupResult (instance, setupResult ) { if (isFunction(setupResult)) { instance.render = setupResult } else if (isObject(setupResult)) { instance.setupState = setupResult } finishComponentSetup(instance) } function finishComponentSetup (instance ) { let Component = instance.type if (!instance.render) { if (!Component.render && Component.template) { } instance.render = Component.render instance.proxy = new Proxy (instance.ctx, PublicInstanceProxyHandlers as any ) } } function createSetupContext (instance ) { return { attrs: instance.attrs, slots: instance.slots, emit: () => { }, expose: () => { } } }
runtime-core/src/renderer.ts 修改 renderer
文件的 setupRenderEffect
方法。接收 instance
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import {createAppApi} from "./apiCreateApp" ;import {ShapeFlags} from "@vue/shared/src/shapeFlags" ;import {createComponentInstance, setupComponent} from "./component" ;export function createRender (renderOptions ) { const setupRenderEffect = (instance,container ) => { instance.render() } const mountComponent = (initialVNode, container ) => { const instance = (initialVNode.component = createComponentInstance(initialVNode)) setupComponent(instance) setupRenderEffect(instance,container) } const processComponent = (n1, n2, container ) => { if (n1 === null ) { mountComponent(n2, container) } else { } } const patch = (n1, n2, container ) => { const {shapeFlag} = n2 if (shapeFlag & ShapeFlags.ELEMENT) { } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { processComponent(n1, n2, container) } } const render = (vnode, container ) => { patch(null , vnode, container) } return { createApp: createAppApi(render) } }
组件渲染流程 在 setupRenderEffect 方法中创建effect函数
获取render函数的返回值 修改 runtime-core/src/renderer.ts 文件的 setupRenderEffect
方法
创建一个effect,在effect中调用render方法,这样render方法会拿到数据会收集这个effect,属性更新时effect会重新执行
只要effect重新执行,render方法就会重新调用,这样页面就会重新渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import {createAppApi} from "./apiCreateApp" ;import {ShapeFlags} from "@vue/shared/src/shapeFlags" ;import {createComponentInstance, setupComponent} from "./component" ;import {effect} from "@vue/reactivity" ;export function createRender (renderOptions ) { const setupRenderEffect = (instance, container ) => { effect(function componentEffect ( ) { if (!instance.isMounted) { let subTree = instance.render.call(instance.proxy, instance.proxy) console .log(subTree, container) patch(null , subTree, container) instance.isMounted = true } else { } }) } const mountComponent = (initialVNode, container ) => { const instance = (initialVNode.component = createComponentInstance(initialVNode)) setupComponent(instance) setupRenderEffect(instance, container) } const processComponent = (n1, n2, container ) => { if (n1 === null ) { mountComponent(n2, container) } else { } } const patch = (n1, n2, container ) => { const {shapeFlag} = n2 if (shapeFlag & ShapeFlags.ELEMENT) { } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { processComponent(n1, n2, container) } } const render = (vnode, container ) => { patch(null , vnode, container) } return { createApp: createAppApi(render) } }
编写h函数 render的返回值是一个h函数,h函数的返回值是虚拟节点,所以下面来实现h函数
新建 packages/runtime-core/src/h.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import {isArray, isObject} from "@vue/shared" ;import {createVNode, isVnode} from "./vnode" ;export function h (type , propsOrChildren, children ) { let l = arguments .length if (l === 2 ) { if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { if (isVnode(propsOrChildren)) { return createVNode(type , null , [propsOrChildren]) } return createVNode(type , propsOrChildren) } else { return createVNode(type , null , propsOrChildren) } } else { if (l > 3 ) { children = Array .prototype.slice.call(arguments , 2 ) } else if (l === 3 && isVnode(children)) { children = [children] } return createVNode(type , propsOrChildren, children) } }
然后导出
packages/runtime-core/src/index.ts文件导出h函数
1 2 3 4 5 6 7 import {createRender} from "./renderer" ;import {h} from "./h" export { createRender, h }
然后再 runtime-dom 中吧 runtime-core 的内容导出
packages/runtime-dom/src/index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import {patchProps} from "./patchProps" import {nodeOpts} from "./nodeOpts" import {mergeObjects} from "@vue/shared"; import {createRender} from "@vue/runtime-core"; + // runtime-dom中导出runtime-core中所有的内容 + export * from "@vue/runtime-core" // 将patchProps和nodeOpts合并为一个对象并导出 const renderOptions = mergeObjects({patchProps}, nodeOpts) // 导出 createApp 方法 export function createApp(rootComponent, rootProps = null) { const app: any = createRender(renderOptions).createApp(rootComponent, rootProps) const {mount} = app app.mount = (container) => { // 先清空容器内容 const contain = nodeOpts.querySelector(container) contain.innerHTML = "" mount(contain) } return app }
测试查看打印结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <div id ="app" > </div > <script src ="../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js" > </script > <script > let {createApp, h} = VueRuntimeDom let App = { setup (props, context ) { console .log("setup" ) console .log(props, context) return { b: 111, c: 333 } }, render (proxy ) { console .log(proxy) console .log(proxy.name) console .log(proxy.b) console .log(proxy.c) return h("div" , {style : {color : "red" }}, "hello world" ) } } let app = createApp(App, {name : "李四" , age : 18 }) app.mount("#app" ) </script > </body > </html >
下面我们来实现这个过程
实现组件渲染 修改 packages/runtime-core/src/renderer.ts 文件中的 patch 方法,添加针对元素的操作
增加 processElement
方法处理元素,并且将创建出来的真实DOM赋值给vnode的el属性
然后再 processElement
方法判断是否是初次挂载,如果是初次挂载则调用 mountElement
方法
mountElement
方法中开始创建真实DOM,并且添加属性和子元素等
如果判断子元素是一个数组,则调用 mountChildren
方法遍历子元素
mountChildren
方法中使用 normalizeVnode
来创建每一个子元素对应的虚拟节点
normalizeVnode
方法会判断当前元素是否是对象,如果是对象则直接返回,否则创建一个类型是 Symbol("Text")
的虚拟节点
然后重新调用 patch
方法进行子组件渲染,此时子组件挂载目标就是上面的el
递归调用 path 最终完成组件渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 import {createAppApi} from "./apiCreateApp" ;import {ShapeFlags} from "@vue/shared/src/shapeFlags" ;import {createComponentInstance, setupComponent} from "./component" ;import {effect} from "@vue/reactivity" ;import {normalizeVnode, Text} from "./vnode" ;export function createRender (renderOptions ) { const { patchProps: hostPatchProps, insert: hostInsert, remove: hostRemove, createElement: hostCreateElement, createText: hostCreateText, setText: hostSetText, setElementText: hostSetElementText, } = renderOptions const setupRenderEffect = (instance, container ) => { effect(function componentEffect ( ) { if (!instance.isMounted) { let subTree = instance.render.call(instance.proxy, instance.proxy) patch(null , subTree, container) instance.isMounted = true } else { } }) } const mountComponent = (initialVNode, container ) => { const instance = (initialVNode.component = createComponentInstance(initialVNode)) setupComponent(instance) setupRenderEffect(instance, container) } const processComponent = (n1, n2, container ) => { if (n1 === null ) { mountComponent(n2, container) } else { } } const mountElement = (vnode, container ) => { const {type , props, shapeFlag, children} = vnode let el = (vnode.el = hostCreateElement(type )) if (props) { for (const key in props) { hostPatchProps(el, key, null , props[key]) } } if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(el, children) } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren(children, el) } hostInsert(el, container) } const mountChildren = (children, container ) => { for (let i = 0 ; i < children.length; i++) { let child = (children[i] = normalizeVnode(children[i])) patch(null , child, container) } } const processElement = (n1, n2, container ) => { if (n1 === null ) { mountElement(n2, container) } else { } } const processText = (n1, n2, container ) => { if (n1 === null ) { hostInsert(hostCreateText(n2.children), container) } } const patch = (n1, n2, container ) => { const {shapeFlag, type } = n2 switch (type ) { case Text: processText(n1, n2, container) break default : if (shapeFlag & ShapeFlags.ELEMENT) { processElement(n1, n2, container) } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { processComponent(n1, n2, container) } } } const render = (vnode, container ) => { patch(null , vnode, container) } return { createApp: createAppApi(render) } }
packages/runtime-core/src/vnode.ts 新增 normalizeVnode
方法
1 2 3 4 5 6 7 export const Text = Symbol ("Text" )export function normalizeVnode (vnode ) { if (isObject(vnode)) return vnode return createVNode(Text, null , String (vnode)) }
测试查看渲染效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <div id ="app" > </div > <script src ="../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js" > </script > <script > let {createApp, h} = VueRuntimeDom let App = { render (proxy ) { return h( "div" , {style : {color : "red" }}, [ h( "h2" , {style : {color : "blue" }}, [ "hello world" , h( "b" , {style : {color : "pink" }}, "123" ) ] ), "world" ]) } } let app = createApp(App, {name : "李四" , age : 18 }) app.mount("#app" ) </script > </body > </html >
接下来实现数据更新,自动更新组件
组件更新流程 只触发一次更新 packages/runtime-core/src/index.ts 导入 reactive 内容
1 2 3 4 5 6 7 8 9 export { createRender, } from "./renderer" ; export { h } from "./h" export * from "@vue/reactivity"
然后修改测试的 html 文件,使用 reactive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <div id ="app" > </div > <script src ="../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js" > </script > <script > let {createApp, h, reactive} = VueRuntimeDom let App = { setup ( ) { let state = reactive({ name: "szx" }) function changeName ( ) { state.name = "songzx" state.name = "szx" state.name = "songzx" } return () => { return h( "div" , {style : {color : "red" }, onClick : changeName}, [ h( "h2" , {style : {color : "blue" }}, [ "hello world" , h( "b" , {style : {color : "pink" }}, "123" ) ] ), state.name ]) } }, } let app = createApp(App, {name : "李四" , age : 18 }) app.mount("#app" ) </script > </body > </html >
现在我们在 h 函数中用到了 state.name ,并且点击后出改变数据,此时会触发三次更新
此时,可以使用之前定义过的effect的 scheduler 属性,自定义更新事件
如果 effect 函数的参数中有 scheduler 属性,则会调用 scheduler(),并传入当前要执行的effect
新建 packages/runtime-core/src/scheduler.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 let queue = []export function queueJob (job ) { if (!queue.includes(job)) { queue.push(job) queueFlush() } } let isFlushPending = false function queueFlush ( ) { if (!isFlushPending) { isFlushPending = true Promise .resolve().then(flushJobs) } } function flushJobs ( ) { isFlushPending = false queue.sort((a, b ) => a.id - b.id) for (let i = 0 ; i < queue.length; i++) { const job = queue[i] job() } queue.length = 0 }
然后修改 packages/runtime-core/src/renderer.ts 文件的 setupRenderEffect
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const setupRenderEffect = (instance, container ) => { effect(function componentEffect ( ) { if (!instance.isMounted) { let subTree = instance.render.call(instance.proxy, instance.proxy) patch(null , subTree, container) instance.isMounted = true } else { console .log("更新" ) } }, { scheduler: (effect ) => { queueJob(effect) } }) }
此时再去点击就会触发一次更新
默认两个元素的比较 packages/runtime-dom/src/nodeOpts.ts
添加 nextSibling 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export const nodeOpts = { createElement: (target ) => document .createElement(target), remove: (target ) => { const parent = target.parentNode if (parent) { parent.removeChild(target) } }, insert: (target, parent, anchor = null ) => { parent.insertBefore(target, anchor) }, querySelector: (selector ) => document .querySelector(selector), setElementText: (el, text ) => el.textContent = text, createText: (text ) => document .createTextNode(text), setText: (node, text ) => node.nodeValue = text, nextSibling: (el ) => el.nextSibling }
有如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <div id ="app" > </div > <script src ="../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js" > </script > <script > let {createApp, h, reactive,ref} = VueRuntimeDom let App = { setup ( ) { let flag = ref(true ) setTimeout (()=> { flag.value = false },1000) return ()=> { return flag.value ? h("div" ,{style :{color :"red" }},h("div" ,"hello word" )) : h("div" ,{style :{color :"blue" }},h("div" ,"你好 世界" )) } } } const app = createApp(App) app.mount("#app" ) </script > </body > </html >
在倒计时一秒后,页面发生变化
flag.value
变化后,会重新触发 effect 方法,此时判断是否初次渲染
packages/runtime-core/src/renderer.ts
修改setupRenderEffect方法中的effect方法,从当前实例中获取上次的VNode,然后调用render函数拿到新的VNode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const setupRenderEffect = (instance, container ) => { effect(function componentEffect ( ) { if (!instance.isMounted) { let subTree = instance.subTree = instance.render.call(instance.proxy, instance.proxy) patch(null , subTree, container) instance.isMounted = true } else { let prevTree = instance.subTree let proxyToUse = instance.proxy let nextTree = instance.render.call(proxyToUse, proxyToUse) patch(prevTree, nextTree, container) } }, { scheduler: (effect ) => { queueJob(effect) } }) }
然后调用patch方法进行页面页面更新
packages/runtime-core/src/renderer.ts
patch方法中添加一个判断,判断n1和n2是否相同,如果不相同,表示本次更新的元素完全和上次不一样,所以直接吧上一次的删除掉,重新渲染新的元素,同时获取上一个元素的下一个兄弟元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const isSameVNodeType = (n1, n2 ) => { return n1.type === n2.type && n1.key === n2.key } const unmount = (n1 ) => { hostRemove(n1.el) } const patch = (n1, n2, container, anchor = null ) => { const {shapeFlag, type} = n2 if (n1 && !isSameVNodeType(n1, n2)) { anchor = hostNextSibling(n1.el) unmount(n1) n1 = null } switch (type) { case Text: processText(n1, n2, container) break default : if (shapeFlag & ShapeFlags.ELEMENT) { processElement(n1, n2, container, anchor) } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { processComponent(n1, n2, container) } } }
判断完成之后如果可以进入到processElement方法中,表示两个元素的类型相同,则要开始比较他们的属性和孩子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 const processElement = (n1, n2, container, anchor ) => { if (n1 === null ) { mountElement(n2, container) } else { patchElement(n1, n2, container) } } const patchProps = (el,oleProps,newProps ) => { for (const key in newProps) { let prev = oleProps[key] let next = newProps[key] if (prev !== next){ hostPatchProps(el,key,oleProps[key],newProps[key]) } } for (const key in oleProps) { if (!(key in newProps)){ hostPatchProps(el,key,oleProps[key],null ) } } } const patchChildren = (n1,n2,el ) => { console .log(n1,n2,el) } const patchElement = (n1, n2, container ) => { let el = (n2.el = n1.el) let oldProps = n1.props let newProps = n2.props patchProps(el, oldProps, newProps) patchChildren(n1,n2,el) }
测试比较两个不同的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let App = { setup ( ) { let flag = ref(true ) setTimeout (()=> { flag.value = false },1000 ) return ()=> { return flag.value ? h("div" ,{style :{color :"red" }},h("div" ,"hello word" )) : h("span" ,{style :{color :"blue" }},h("div" ,"你好 世界" )) } } }
此时页面会直接替换
如果元素类型相同,则会比较属性和比较孩子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let App = { setup ( ) { let flag = ref(true ) setTimeout (()=> { flag.value = false },1000 ) return ()=> { return flag.value ? h("div" ,{style :{color :"red" }},h("div" ,"hello word" )) : h("div" ,{style :{color :"blue" }},h("div" ,"你好 世界" )) } } }
此时页面只是吧属性更新了一下,内容没有替换
接下来开始实现比较孩子的操作
特殊比较和优化 packages/runtime-core/src/renderer.ts
实现 patchChildren 方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 const unmountChildren = (children ) => { for (let child of children) { unmount(child) } } const patchKeyedChildren = (c1, c2, el ) => { let i = 0 let e1 = c1.length - 1 let e2 = c2.length - 1 while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = c2[i] if (isSameVNodeType(n1, n2)) { patch(n1, n2, el) } else { break } i++ } while (i <= e1 && i <= e2){ const n1 = c1[e1] const n2 = c2[e2] if (isSameVNodeType(n1,n2)){ patch(n1, n2, el) }else { break } e1-- e2-- } if (i > e1){ if (i <= e2){ const nextPosition = e2 + 1 const anchor = nextPosition < c2.length ? c2[nextPosition].el : null while (i <= e2){ patch(null ,c2[i],el,anchor) i++ } } }else if (i > e2){ while (i <= e1){ unmount(c1[i]) i++ } }else { let s1 = i let s2 = i const keyToNewIndexMap = new Map () for (let j = s2; j <= e2; j++) { const childVNode = c2[j] keyToNewIndexMap.set(childVNode.key,j) } const toBePatched = e2 - s2 + 1 const newIndexToOldIndexMap = new Array (toBePatched).fill(0 ) for (let j = s1; j <= e1; j++) { const oldVNode = c1[j] const newIndex = keyToNewIndexMap.get(oldVNode.key) if (newIndex === undefined ){ unmount(oldVNode) }else { newIndexToOldIndexMap[newIndex - s2] = j + 1 patch(oldVNode,c2[newIndex],el) } } for (let i = toBePatched - 1 ; i >= 0 ; i--) { console .log(i,'565656' ) const currentIndex = i + s2 const child = c2[currentIndex] const anchor = currentIndex + 1 < c2.length ? c2[currentIndex + 1 ].el : null console .log(child,'555' ) if (newIndexToOldIndexMap[i] === 0 ){ patch(null ,child,el,anchor) }else { hostInsert(child.el,el,anchor) } } } } const patchChildren = (n1, n2, el ) => { let c1 = n1.children let c2 = n2.children const prevShapeFlag = n1.shapeFlag const shapeFlag = n2.shapeFlag if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { unmountChildren(c1) } if (c1 !== c2) { hostSetElementText(el, c2) } } else { if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { patchKeyedChildren(c1, c2, el) } else { unmountChildren(c1) } } else { if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(el, "" ) } if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren(c2, el) } } } }
前面相同,后面不同 前面相同,后面不同,从头开始比较
1 2 3 4 5 6 7 8 9 10 11 while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = c2[i] if (isSameVNodeType(n1, n2)) { patch(n1, n2, el) } else { break } i++ }
后面相同,前面不同
1 2 3 4 5 6 7 8 9 10 11 12 while (i <= e1 && i <= e2){ const n1 = c1[e1] const n2 = c2[e2] if (isSameVNodeType(n1,n2)){ patch(n1, n2, el) }else { break } e1-- e2-- }
尾部追加
前后对比完成后,如果发现新的多,则往后面追加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (i > e1){ if (i <= e2){ const nextPosition = e2 + 1 const anchor = nextPosition < c2.length ? c2[nextPosition].el : null while (i <= e2){ patch(null ,c2[i],el,anchor) i++ } } }
尾部删除
前后对比完成后,如果发现新的少,老的多,则删除老的多余节点
1 2 3 4 5 6 else if (i > e2){ while (i <= e1){ unmount(c1[i]) i++ } }
最终进入乱序比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 else { let s1 = i let s2 = i const keyToNewIndexMap = new Map () for (let j = s2; j <= e2; j++) { const childVNode = c2[j] keyToNewIndexMap.set(childVNode.key,j) } const toBePatched = e2 - s2 + 1 const newIndexToOldIndexMap = new Array (toBePatched).fill(0 ) for (let j = s1; j <= e1; j++) { const oldVNode = c1[j] const newIndex = keyToNewIndexMap.get(oldVNode.key) if (newIndex === undefined ){ unmount(oldVNode) }else { newIndexToOldIndexMap[newIndex - s2] = j + 1 patch(oldVNode,c2[newIndex],el) } } for (let i = toBePatched - 1 ; i >= 0 ; i--) { const currentIndex = i + s2 const child = c2[currentIndex] const anchor = currentIndex + 1 < c2.length ? c2[currentIndex + 1 ].el : null console .log(child,'555' ) if (newIndexToOldIndexMap[i] === 0 ){ patch(null ,child,el,anchor) }else { hostInsert(child.el,el,anchor) } } }
测试比较结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 let App = { setup ( ) { let flag = ref(true ) setTimeout (() => { flag.value = false }, 1000 ) return () => { return flag.value ? h("div" , {style : {color : "red" , "font-size" : "25px" }}, [ h("li" , {key : "a" }, "a" ), h("li" , {key : "b" }, "b" ), h("li" , {key : "e" }, "e" ), h("li" , {key : "f" }, "f" ), h("li" , {key : "g" }, "g" ), h("li" , {key : "l" }, "l" ), h("li" , {key : "k" }, "k" ), h("li" , {key : "c" }, "c" ), h("li" , {key : "d" }, "d" ), ]) : h("div" , {style : {color : "blue" }}, [ h("li" , {key : "a" }, "a" ), h("li" , {key : "b" }, "b" ), h("li" , {key : "g" }, "g" ), h("li" , {key : "e" }, "e" ), h("li" , {key : "f" }, "f" ), h("li" , {key : "h" }, "h" ), h("li" , {key : "c" }, "c" ), h("li" , {key : "d" }, "d" ), ]) } } } const app = createApp(App)app.mount("#app" )
观察动画发现虽然结果正确了,但是 g、e、f、h 四个节点都会发生一次变化。在老节点中,e、f 两个节点是连续在一起的,新的节点中 e、f 也是在一起的,我们只需要吧 g 直接插入到 e 的前面即可。
这个逻辑优化就是查找最长递增子序列。下面我们来实现这个优化
最长递增子序列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 function getSequence (arr ) { const p = arr.slice() const result = [0 ] let i, j, u, v, c const len = arr.length for (i = 0 ; i < len; i++) { const arrI = arr[i] if (arrI !== 0 ) { j = result[result.length - 1 ] if (arr[j] < arrI) { p[i] = j result.push(i) continue } u = 0 v = result.length - 1 while (u < v) { c = ((u + v) / 2 ) | 0 if (arr[result[c]] < arrI) { u = c + 1 } else { v = c } } if (arrI < arr[result[u]]) { if (u > 0 ) { p[i] = result[u - 1 ] } result[u] = i } } } u = result.length v = result[u - 1 ] while (u-- > 0 ) { result[u] = v v = p[v]; } return result }
以数组[2, 11, 6, 8, 1]为例:最终输出的结果为[0, 2, 3],表示最强增长序列的索引分别是0, 2 ,3;对应的值是2,6,8。换句话说,在这个数组中最长连续增长的值就是数组中的2,6,8三个元素。
费了这么大的力气,使用这个方法的目的是什么呢?
Vue2在DOM-Diff过程中,优先处理特殊场景的情况,即头头比对,头尾比对,尾头比对等。
而Vue3在DOM-Diff过程中,根据 newIndexToOldIndexMap 新老节点索引列表找到最长稳定序列,通过最长增长子序列的算法比对,找出新旧节点中不需要移动的节点,原地复用,仅对需要移动或已经patch的节点进行操作,最大限度地提升替换效率,相比于Vue2版本是质的提升!
有了这个方法后我们就可以利用这个算法得出那些节点是可以直接跳过的
修改 else 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 else { let s1 = i let s2 = i const keyToNewIndexMap = new Map () for (let j = s2; j <= e2; j++) { const childVNode = c2[j] keyToNewIndexMap.set(childVNode.key,j) } const toBePatched = e2 - s2 + 1 const newIndexToOldIndexMap = new Array (toBePatched).fill(0 ) for (let j = s1; j <= e1; j++) { const oldVNode = c1[j] const newIndex = keyToNewIndexMap.get(oldVNode.key) if (newIndex === undefined ){ unmount(oldVNode) }else { newIndexToOldIndexMap[newIndex - s2] = j + 1 patch(oldVNode,c2[newIndex],el) } } let increasingNewIndexSequence = getSequence(newIndexToOldIndexMap) let j = increasingNewIndexSequence.length - 1 for (let i = toBePatched - 1 ; i >= 0 ; i--) { const currentIndex = i + s2 const child = c2[currentIndex] const anchor = currentIndex + 1 < c2.length ? c2[currentIndex + 1 ].el : null if (newIndexToOldIndexMap[i] === 0 ){ patch(null ,child,el,anchor) }else { if (i !== increasingNewIndexSequence[j]){ hostInsert(child.el,el,anchor) }else { j-- } } } }
查看结果
仔细观察 e、f 节点,可以发现 e、f 并没有发生移动,这一点对比vue2的 diff 算法是质的变化,页面渲染效率提高很多。
组件渲染流程图
组件更新流程图
Vue3的模板编译过程的优化 blockTree和patchFlag 在这个网址中可以查看vue3代码编译后的结果
https://template-explorer.vuejs.org/
复制这段编译后的代码到vue3的项目中去运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" export function render (_ctx, _cache, $props, $setup, $data, $options ) { return (_openBlock(), _createElementBlock(_Fragment, null , [ _createElementVNode("div" , null , _toDisplayString(_ctx.name), 1 ), _createElementVNode("span" , null , [ _createElementVNode("a" , { href : _ctx.dyHref }, _toDisplayString(_ctx.age), 9 , ["href" ]) ]) ], 64 )) } console .log(render({ name:"李四" , age:18 , href:"http://baidu.com" }))
可以看到收集到了两个动态节点
vue3在模板更新时加入了 blockTree 和 patchFlag
blockTree 作用是收集了动态的节点,放在了 dynamicChildren 中
在进行diff对比的过程中去判断如果这个节点的dynamicChildren不为空,则遍历dynamicChildren中的节点进行比对,而不再进行全量diff
这样效率大大提高
编译过程
先将模板进行分析,生成对应的ast数。ast数是一个大对象,来描述语法的
然后做转化流程 transform -> 对动态节点做一些标记,例如:指令、插槽、事件、属性….,这些标记会被记录到 patchFlag 属性中,对应不同的值
然后diff过程中会根据 patchFlag 的值,知道这个节点具体是哪个东西发生了改变,从而进行精准diff
最后代码生成 codegen -> 生成最终代码
blokeTree
diff 算法的特点是递归遍历,每次比较同一层,然后深度遍历比较所有,这样效率并不好
block 的作用就是为了收集动态节点,将树的递归拍平成了一个数组,只遍历收集起来的节点
在 createVnode 的时候,就会判断这个节点是不是动态的,然外层的 block 收集起来
目的是为了 diff 时只 diff 动态节点
如果会影响DOM结构的,例如 v-if v-else 都会被标记成 block 节点
父亲也会收集儿子的 block
最终多个 block 构成一个 blockTree
patchFlags
性能优化
每次重新渲染时,都要创建一个虚拟节点,调用的是 createVnode 方法
当创建多个重复的静态节点时,会做静态提升,静态节点进行提取
Vue3和Vue2的对比
响应式原理:由 defineProperty 替换成了 proxy
defineProperty 会递归对所有属性进行代理,而 proxy 只会在访问到这个属性时返回代理结果
vue3 diff算法(可以根据 patchFlag 做 diff)vue2 的 diff 是全量 diff。其中 vue3的diff算法中用到了一个最长递增子序列的算法,如果有相邻的重复节点,则会跳过这些节点,提高性能
编码层面的变化:vue2采用 optionsAPI,vue3是 compositionAPI
vue3支持同时存在多个根节点,因为如果是多个根节点的情况,会自动的在最外面添加一个 Fragment
vue3源码全面采用ts编写,vue2是flow,类型退到不准确
vue3支持自定义渲染器 createRender() 传入自己的渲染方法,可以编译到不同平台
组件生命周期原理 实现原理 在组件的不同阶段会触发不同的钩子。当页面初次渲染和更新都会走 patch 方法,然而在初次渲染时,调用 patch 之前,会先获取当前组件的实例,去实例上获取声明的生命周期钩子函数,判断是否存在 onBeforeMount 钩子,如果有,就会先调用 onBeforeMount 方法,然后 patch 函数执行完毕后,调用 onMounted 钩子。然后组件更新时也会走 patch 方法,更新之前会判断是否存在 onBeforeUpdated 钩子,如果有,就会先调用 onBeforeUpdated 方法,更新完毕后,在调用 onUpdated 方法。
这里的核心是如何确保当前组件的声明周期钩子函数中获取到的实例是当前组件的。例如当父组件内包含一个子组件时,一定是先执行子组件的钩子,再执行父组件的钩子。
实现代码 下面来实现代码
找到 packages/runtime-core/src/component.ts 文件中调用 setup 的 setupStatefulComponent 函数
添加下面代码,分别导出
currentInstance
getCurrentInstance
setCurrentInstance
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 export let currentInstance = null export const getCurrentInstance = () => { return currentInstance } export const setCurrentInstance = (instance ) => { currentInstance = instance } function setupStatefulComponent (instance ) { let Component = instance.type let {setup} = Component if (setup) { currentInstance = instance let setupContext = createSetupContext(instance) let setupResult = setup(instance.props, setupContext) currentInstance = null handleSetupResult(instance, setupResult) } else { finishComponentSetup(instance) } }
在执行 setup 之前将当前的实例赋给 currentInstance,然后 setup 函数执行完毕后清空 currentInstance
然后新建 packages/runtime-core/src/apiLifecycle.ts 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import {currentInstance, setCurrentInstance} from "./component" ;const enum LifeCycleHooks { BEFORE_MOUNT = "bm" , MOUNTED = "m" , BEFORE_UPDATE = "bu" , UPDATED = "u" } const createHook = (lifeCycle ) => (hooke, target = currentInstance ) => { injectHook(lifeCycle, hooke, target) } function injectHook (type, hooke, target ) { if (!target) { console .warn("当前没有实例,无法执行钩子函数" ) } else { const hooks = target[type] || (target[type] = []) const wrap = () => { setCurrentInstance(target) hooke.call(target) setCurrentInstance(null ) } hooks.push(wrap) } } export const invokeArrayFns = (fns ) => { for (let i = 0 ; i < fns.length; i++) { fns[i]() } } export const onBeforeMount = createHook(LifeCycleHooks.BEFORE_MOUNT)export const onMounted = createHook(LifeCycleHooks.MOUNTED)export const onBeforeUpdate = createHook(LifeCycleHooks.BEFORE_UPDATE)export const onUpdated = createHook(LifeCycleHooks.UPDATED)
这个文件中的 injectHook 方法巧妙的利用闭包保存了每个组件自己的实例,并且在执行钩子函数之前会调用 setCurrentInstance 方法保存当前实例,这样组件钩子内去调用 getCurrentInstance 方法是会准确的获取到当前的实例
并且将每个钩子名称做了简化,以数组的形式保存到当前实例中
实例上了钩子函数后,下一步就是要在合适的时机去执行这些钩子
找到 packages/runtime-core/src/renderer.ts 文件的 createRender 方法中的 setupRenderEffect 方法,这个方法中包含了挂载前,挂载完成,更新前,更新完成这四个时机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 const setupRenderEffect = (instance, container ) => { effect(function componentEffect ( ) { if (!instance.isMounted) { let {bm, m} = instance if (bm) { invokeArrayFns(bm) } let subTree = instance.subTree = instance.render.call(instance.proxy, instance.proxy) patch(null , subTree, container) instance.isMounted = true if (m) { invokeArrayFns(m) } } else { let {bu, u} = instance if (bu) { invokeArrayFns(bu) } let prevTree = instance.subTree let proxyToUse = instance.proxy let nextTree = instance.render.call(proxyToUse, proxyToUse) patch(prevTree, nextTree, container) if (u) { invokeArrayFns(u) } } }, { scheduler: (effect ) => { queueJob(effect) } }) }
最后导出方法
packages/runtime-core/src/index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export { createRender, } from "./renderer" ; export { h } from "./h" export { onBeforeMount, onMounted, onBeforeUpdate, onUpdated } from "./apiLifecycle" export {getCurrentInstance} from "./component" export * from "@vue/reactivity"
测试生命周期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <div id ="app" > </div > </body > <script src ="../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js" > </script > <script > let { createApp, h, reactive, ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, getCurrentInstance } = VueRuntimeDom let flag = ref(true ) setTimeout (() => { flag.value = false }, 1000) let App = { setup ( ) { onBeforeMount(() => { console .log("onBeforeMount" ) }) onMounted(() => { const instance = getCurrentInstance() console .log(instance) console .log("onMounted" ) }) onBeforeUpdate(() => { console .log("onBeforeUpdate" ) }) onUpdated(() => { console .log("onUpdate" ) }) return () => { return h("div" , flag.value) } } } const app = createApp(App) app.mount("#app" ) </script > </html >
通过打印,可以发现组件的实例上已经添加了不同生命周期中的不同钩子