keywords: Proxy、懒代理、编译时优化、对比、 启动流程
此文是余初习vue3源码所记,可谓走马观花。虽不常用vue,但看下来也是颇有所得,遂感其称三大框架之一,岂曰仅靠国人力捧邪?初学至此,已是殚精竭虑,头皮发麻,不敢深究,故文中仅梳理枝干,于海量细节精妙之大千世界尚少有探究。然力之所限,深以为憾,乃应为后之续习也。
vue3整体的思路和vue2基本一致,都是基于发布订阅模式的MVVM的实现,不一样的点在于主要是:vue2做响应式数据是:Object.definedPorperty + Dep + Watcher串联整体流程,而vue3是Proxy + Weakmap + effect;另外在模版编译阶段也有很多不同,主要是vue3摒弃了with,对模版做了分析之后,引入ctx来代替原本with的功能。
_
代码结构
多包单仓库架构:
Monorepo-多包单仓库的开发模式、知乎:monorepo
github: lerna
所以我们看到vue3有一个packages,就是用lerna做的monorepo:
比如一下都是packages下的包:
- compiler-core:平台无关编译器;
- compiler-dom:浏览器而言的编译器;
- compiler-sfc:vue模拆分
- compiler-ssr:SSR toString
- runtime-core:运行时和平台无关;
- runtime-dom:运行时基于浏览器平台;
- …
响应式数据
从defineProperty到Proxy
(reactivity包)
响应式数据一直是vue中最基础且重要的知识,所以,讨论vue3还是从双向数据绑定开始。
vue3用了Proxy和Reflect代替了vue2中的Object.defineProperty。
先简单看下proxy:
let a = { name: '123'};let proxy_a = new Proxy(a, {get(target, key) {console.log('in proxy:');console.log('-- target:',target);console.log('-- key:', key);return target[key];},set(target, key, value) {console.log('in proxy:');console.log('-- target:',target);console.log('-- key:', key);console.log('-- value:', value);return Reflect.set(target, key, value)}})
我们看到proxy_a是a对象的一个代理对象,相当于包裹的一层,所以,借由proxy对象,对被代理对象的操作可以先被拦截,执行固定的逻辑后,再通过反射,作用在被拦截对象上。
proxy比Object.defindProperty好在哪里呢?Object.defindProperty的机制是对每一个属性进行重写,是从属性的视角来出发做一些动作,但是proxy是从对象本身的角度出发。Object.defindProperty对于新增属性,那就要重新对这个属性进行监听,但是proxy就不用,因为对象就已经被监听(代理)过了。
可是proxy依旧存在问题:
- 属性是对象的引用问题,比如data.arr[3] = ‘ccc’;这个赋值,只写个proxy能监听到吗?监听不到…
- 和Object.defindProperty一样,对数组的可能操作index的操作,有可能会触发多次方法…
那么看下vue3中的实现,这里面最重要的就是reactive.ts 和 effect.ts:功能上来说,reactive可以把数据变成响应式数据,返回代理对象,而effect接受一个函数,这个函数会在响应式数据变化的时候被触发, 大致就是这个意思:
const { reactive, effect } = VueReactivity;const data = { count: 0 };// 返回一个监听的新数据 proxyconst state = reactive(data);const fn = () => {const count = state.count;// ==>get count:fn();console.log('当前的count:', count);};effect(fn);
reactive.ts:
export function reactive(target: object) {//只读数据,直接返回 readonly ==>只读数据if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {return target}//调用创建响应式数据方法return createReactiveObject(target,false,mutableHandlers,//不同逻辑的处理情况mutableCollectionHandlers)}
其核心逻辑是调用了createReactiveObject函数, mutableHandlers、mutableCollectionHandlers涉及更细致的具体的代理过程:
createReactiveObject代码如下:
function createReactiveObject(target: Target,isReadonly: boolean,baseHandlers: ProxyHandler<any>,collectionHandlers: ProxyHandler<any>) {if (!isObject(target)) {if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)}return target}// 已经是响应式数据,并且不是只读的 proxyif (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target}// 原始数据已经有代理数据,直接找到之前代理后的数据const proxyMap = isReadonly ? readonlyMap : reactiveMapconst existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}// 判断数据是否可以被代理,比如VNode对象,component实例等const targetType = getTargetType(target)if (targetType === TargetType.INVALID) {return target}// 处理代理逻辑,内部有对应的get、setconst proxy = new Proxy(target,targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers);//代理之后,直接设置,避免下次重复代理一样的原始数据proxyMap.set(target, proxy);//返回数据return proxy}
看到在这个函数中,首先先进行了几个判断:target已经是响应式数据吗?已经有代理数据吗?数据是否可以被代理?都通过之后,构建了代理对象,这时候具体的方法:
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
对于集合走collectionHandlers、对于数组和对象,走baseHandlers,我们重点关注下baseHandlers,这个东西其实是引入进来的mutableHandlers,位于包中baseHandlers.ts文件中,这里才是核心逻辑:
export const mutableHandlers: ProxyHandler<object> = {get, // get逻辑,收集依赖set, // set逻辑,发布订阅deleteProperty,has,ownKeys}
- 先看下get: ```typescript const get = /#PURE/ createGetter();
function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { //对特殊的key,做了一层处理,判断对应数据是否什么响应类型数据,只读与响应式。或者获取原始数据 if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if ( key === ReactiveFlags.RAW && receiver === (isReadonly ? readonlyMap : reactiveMap).get(target) ) { return target }
// 是数组吗?const targetIsArray = isArray(target)// 数组则借助此对象; arrayInstrumentationsif (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)}//其他情况,直接获取const res = Reflect.get(target, key, receiver)//symbol不处理,直接返回if (isSymbol(key)? builtInSymbols.has(key as symbol): key === `__proto__` || key === `__v_isRef`) {return res}// 不是只读数据,开始收集依赖:trackif (!isReadonly) {track(target, TrackOpTypes.GET, key)}if (shallow) {return res}if (isRef(res)) { // 如果获取到的值是ref数据,直接返回对应的value情况// ref unwrapping - does not apply for Array + integer key.const shouldUnwrap = !targetIsArray || !isIntegerKey(key)return shouldUnwrap ? res.value : res}
// 如果是对象,那就做一个懒代理,避免一开始就深度嵌套 // 这里很有点意思 if (isObject(res)) { // Convert returned value into a proxy as well. we do the isObject check // here to avoid invalid value warning. Also need to lazy access readonly // and reactive here to avoid circular dependency. return isReadonly ? readonly(res) : reactive(res) }
return res
} }
这个get函数就很有点东西了~首先也是先做了一堆判断,然后判断了下是不是数组,数组暂且不表。如果不是数组,Reflect.get(target, key, receiver)通过反射拿到原始对象的值,res,之后,做了两件事情,1)track,收集依赖;2)判断res是不是object;<br />为什么要判断res是不是object,这里其实就是vue3对于对象嵌套监听不到的问题的答案。当获取的值res,经过isObject(res)判断之后又是对象,这种情况下对res也代理下,再调用reactive(res),这样解决了对象嵌套监听不到的问题,同时,这个是**懒代理**,懒在哪里?懒在没有像vue2那样对所有属性就深度遍历,而是对属性真正访问的时候,才去做的这样的代理,这样必然节约计算成本,性能甚好,岂不美哉?回头看下数组:```typescript// 重写数组,因为数组还是会存在修改一次数据,触发多次修改的情况,直接重写方法const arrayInstrumentations: Record<string, Function> = {}// values,可能会触发多次get的情况;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {const method = Array.prototype[key] as anyarrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {//获取到数据的原始数据const arr = toRaw(this)for (let i = 0, l = this.length; i < l; i++) {track(arr, TrackOpTypes.GET, i + ''); // 收集一次依赖}// we run the method using the original args first (which may be reactive)const res = method.apply(arr, args);// 第一次执行,可能参数是响应式数据的情况if (res === -1 || res === false) {// 可能是数据被代理的情况,查找原始数据,重新处理// if that didn't work, run it again using raw values.return method.apply(arr, args.map(toRaw))} else {return res}}})// 改变数组的情况,可能导致多次get、set,特定处理;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {const method = Array.prototype[key] as anyarrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {pauseTracking()const res = method.apply(this, args) // 执行方法resetTracking()return res}});
可以看到,和vue2思路是相同的,重新数组方法。但是这个重写的逻辑都是借用原始的方法,const method = Array.prototype[key] as any,然后拿到原始方法的值 const res = method.apply(this, args) // 执行方法,在拿到值的前后做依赖收集的之类的行为。
看下set:
const set = /*#__PURE__*/ createSetter()function createSetter(shallow = false) {return function set(target: object,key: string | symbol,value: unknown,receiver: object): boolean {// 获取到原来的值const oldValue = (target as any)[key]if (!shallow) {// 处理对应ref数据的情况value = toRaw(value)if (!isArray(target) && isRef(oldValue) && !isRef(value)) {oldValue.value = value; // ref的时候,需要修改value来触发对应数据的内部逻辑return true}} else {// in shallow mode, objects are set as-is regardless of reactive or not}// 判断这个key是新添加的,还是修改的const hadKey =isArray(target) && isIntegerKey(key)? Number(key) < target.length: hasOwn(target, key)// 触发修改逻辑const result = Reflect.set(target, key, value, receiver)// don't trigger if target is something up in the prototype chain of original// receiver 为Proxy或者继承Proxy的对象,这里需要处理原型链的情况,因为如果原型链继承的也是一个proxy,通过Reflect.set修改原型链上的属性会触发两次setterif (target === toRaw(receiver)) {if (!hadKey) {//trigger 派发通知trigger(target, TriggerOpTypes.ADD, key, value)} else if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key, value, oldValue)}}return result}}
set的核心逻辑就是要得到被代理对象的值,并且通知值已经产生变化,关注核心逻辑,const result = Reflect.set(target, key, value, receiver),得到result值,通过trigger派发通知(新增key和已存在的key调用trigger不太一样)
至此,响应式数据的代理过程基本了解了。
总结下来:
- 在get阶段,vue3通过懒代理的方式解决object嵌套问题、通过重写数组方法解决调用数组方法后多次触发的问题;这个阶段涉及track收集依赖;
- 在set阶段,涉及trigger,派发通知的过程;
effect、track、trigger
- effect
可以这么理解,vue3中这一套响应式数据系统,最初的激励就是依靠effect完成的。同时,effect还是后续系统流转起来的重要角色。那么effect是什么呢?(reactivity/src/effect.ts)
export function effect<T = any>(fn: () => T,options: ReactiveEffectOptions = EMPTY_OBJ): ReactiveEffect<T> {if (isEffect(fn)) {// 如果fn已经是一个effect函数了,那么就直接指向原始函数fn = fn.raw}// 包装const effect = createReactiveEffect(fn, options)//判断是否需要lazy,如果不是lazy,直接执行一次if (!options.lazy) {// ----> 执行effect()}//返回函数return effect}
可以看到effect函数内,执行了一次effect,并返回了effect,而这个effect是createReactiveEffect的返回值:
function createReactiveEffect<T = any>(fn: () => T,options: ReactiveEffectOptions): ReactiveEffect<T> {const effect = function reactiveEffect(): unknown {if (!effect.active) {// 1. 在effect没有激活的状态之下,调用effect的时候,没有调度,直接执行fnreturn options.scheduler ? undefined : fn()}if (!effectStack.includes(effect)) {cleanup(effect)try {// 2. 开启允许依赖收集enableTracking()// 3. effect压栈effectStack.push(effect)// 4. 设置激活的effectactiveEffect = effect// 5. 执行fn逻辑,触发内部数据依赖的收集,开始收集activeEffectreturn fn();// targetMap} finally {// 6. 出栈effectStack.pop()// 恢复之前的状态resetTracking()// 指向栈最后一个effectactiveEffect = effectStack[effectStack.length - 1]}}} as ReactiveEffect// 这里effect.id = uid++effect.allowRecurse = !!options.allowRecurseeffect._isEffect = true // 表示一个effect函数effect.active = true // 激活状态effect.raw = fn // 原始的函数effect.deps = [] // effect对应的依赖 ,一个effect里面,有多个响应式数据的使用,可能这些数据会触发其他依赖的修改//其他配置项effect.options = optionsreturn effect}
看上面的代码中,逻辑大致可以整理成:1. 在effect没有激活的状态之下,调用effect的时候,没有调度,直接执行fn;2. 检查当前的effect栈中,如果不存在effect就把effect压入栈中,然后标记为激活,执行下fn并返回;简而言之,effect中是要执行fn的。
- track
然后看下get阶段要利用track来收集依赖,是怎么收集的?track函数也是在此文件中。 ```typescript
export function track(target: object, type: TrackOpTypes, key: unknown) { if (!shouldTrack || activeEffect === undefined) { return }
//targetMap 维护了全局的依赖情况 let depsMap = targetMap.get(target) if (!depsMap) { //每一个target,对应一个depsMap targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { //depsMap中维护了对应的key到dep的集合 depsMap.set(key, (dep = new Set())) } if (!dep.has(activeEffect)) { //收集当前激活的effect作为依赖 dep.add(activeEffect) activeEffect.deps.push(dep) //当前激活的effect,收集dep作为依赖,当前effect内部还有触发其他effect的情况 if (DEV && activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type, key }) } } }
track是当data中的key被get的时候触发的,首先我们要知道,这里要做的事情是收集依赖关系,维护“电话本”。<br />然后看函数,target是原始数据,首先,用targetMap.get(target)来获得对target的依赖关系,targetMap是一个Weakmap,所以key可以不是基本类型。<br />这里targetMap是对target的依赖,target上的key的依赖是:let dep = depsMap.get(key),dep是一个set,当前激活状态下的activeEffect最终被add到这个set中了,dep.add(activeEffect),这个数据结构见定义:```typescripttype Dep = Set<ReactiveEffect>type KeyToDepMap = Map<any, Dep>// targetMapconst targetMap = new WeakMap<any, KeyToDepMap>();
ok,这就是track的过程,简单说维护了一个Weakmap,存放依赖关系,所谓的关系,就是key和effect发生了关系,一对多的关系…(没有开车)
trigger
当响应式数据被赋值,那自然是触发set方法,而在set方法中会触发trigger,函数也是在这个文件里面:export function trigger(target: object,type: TriggerOpTypes,key?: unknown,newValue?: unknown,oldValue?: unknown,oldTarget?: Map<unknown, unknown> | Set<unknown>) {// 获取到对应数据原始数据的依赖集合const depsMap = targetMap.get(target)if (!depsMap) {// 没有被收集,直接返回return}// 创建需要运行的effect集合const effects = new Set<ReactiveEffect>()// 定义遍历添加effects的函数const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {if (effectsToAdd) {effectsToAdd.forEach(effect => {if (effect !== activeEffect || effect.allowRecurse) {effects.add(effect)}})}}// 触发了对应的操作,修改、删除、添加,都添加effect依赖到effects中if (type === TriggerOpTypes.CLEAR) {// collection being cleared// trigger all effects for targetdepsMap.forEach(add)} else if (key === 'length' && isArray(target)) {depsMap.forEach((dep, key) => {if (key === 'length' || key >= (newValue as number)) {add(dep)}})} else {// schedule runs for SET | ADD | DELETEif (key !== void 0) {add(depsMap.get(key))}// also run for iteration key on ADD | DELETE | Map.SETswitch (type) {case TriggerOpTypes.ADD:if (!isArray(target)) {add(depsMap.get(ITERATE_KEY))if (isMap(target)) {add(depsMap.get(MAP_KEY_ITERATE_KEY))}} else if (isIntegerKey(key)) {// new index added to array -> length changesadd(depsMap.get('length'))}breakcase TriggerOpTypes.DELETE:if (!isArray(target)) {add(depsMap.get(ITERATE_KEY))if (isMap(target)) {add(depsMap.get(MAP_KEY_ITERATE_KEY))}}breakcase TriggerOpTypes.SET:if (isMap(target)) {add(depsMap.get(ITERATE_KEY))}break}}//定义执行函数const run = (effect: ReactiveEffect) => {if (__DEV__ && effect.options.onTrigger) {effect.options.onTrigger({effect,target,key,type,newValue,oldValue,oldTarget})}// 如果有调度函数,就先执行调度函数if (effect.options.scheduler) {effect.options.scheduler(effect)} else {//没有的话,直接执行effect()}}effects.forEach(run);// 遍历执行effect}
这段代码看似长,但是实际上逻辑不难,主要流程就是在找到target和key有关的依赖—-effect,把这些effect最终遍历执行。
所以,上述过程就接上了:当我们访问数据,会将数据和effect建立关系维护起来;当我们修改数据,修改的时候要找到维护起来的关系,依次执行这些相关的effect。
再对比下Vue2,vue3中的双向数据绑定,或者说响应式数据流程,从v2的Object.definedPorperty() + Dep + Watcher 变成了v3时代Proxy + Weakmap + effect~
vue3 编译和优化
vue2编译的问题主要有2点:1. 正则; 2. with;
- 正则匹配,回溯的算法问题。所以用正则匹配回溯次数完全不可控;
vue3用了状态机(经典方案,很多编译器都是AST + 状态机) 。
以前是用字符串正则匹配出指令,现在改成解析成AST。
在vue3中,编译最终生成的是这样,astexplorer,这是个在线编译的工具,可以看到编译后带render函数,以及在console中输出了AST树对象:
<div>Hello World!{{ msg }}</div>
编译成:
import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createBlock("div", null, "Hello World! " + _toDisplayString(_ctx.msg), 1 /* TEXT */))}
- with的问题
with,不会立即释放内存,导致性能问题,vue3中离线编译出来的render已经不需要with了,而是有一个ctx作为参数,代替以前的with的功能;
问题来了:vue2为什么不把数据挂在ctx上,这样不是也可以不用with 只要把变量都变成ctx.变量名 ?
我的理解,因为vue2中不能对模版中的动态语句,没有做,分析不了。所以无法变成 ctx.变量名形式??
这里逻辑是不是这样,因为with性能不好,所以需要优化,但是没有with又拿不到数据,所以把数据放到ctx上了,所以需要分析语句,因为至少需要在变量前面加上 ctx.变量名??
{{ name + familyName }} 比如在vue2时代,没有分析语句的话,要变成 ctx.name和ctx.familyName,因为编译程序无法识别这个字符串从哪里到那里是变量,所以就简单粗暴的在最外层加一个with,这语句原封不动拿过来就好。 提升动态VNode
<div><section v-if="foo"><p>{{ a }}</p></section><div v-else><p>{{ a }}</p></div></div>
这个节点生成的Vnode可能会是这样:
const block = {tag: 'div',dynamicChildren: [{ tag: 'p', children: ctx.a, patchFlag: 1 }]}
dynamicChildren会把当前的组件的动态节点向上提升,这样的好处就是,更新的时候不用遍历到更深的内部了。但是提升到什么高度呢,总不能提升到root吧。
这里这个机制对事件也适用。
- 连续的静态节点生成字符串
诸如这样的一些列静态节点:
<div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div> ... n ... <div></div><div></div><div></div>
在编译时就直接会生成静态节点的字符串:
_createStaticVnode('<div></div><div>......</div>')
启动流程和挂载
看下vue整体的启动流程, 用vue是从这里开始的:
import { createApp } from 'vue'import App from './app'const app = createApp(App)app.mount('#app')
这部分代码很长,很多,所以目下只学了个流程,代码就贴个简单的架子。
从createApp开始,代码在runtime-core包的index.ts中:
export const createApp = ((...args) => {// A >> 创造app 实例对象const app = ensureRenderer().createApp(...args)if (__DEV__) {injectNativeTagCheck(app)}// 重写挂载的mountconst { mount } = appapp.mount = (containerOrSelector: Element | ShadowRoot | string): any => {// ...重写挂载的mount,可以独立到不用的平台实现}return app}) as CreateAppFunction<Element>
上述代码的A处,可以看到ensureRenderer().createApp(…args)这个方法返回了app实例,最终被return出去了。所以追进去看下ensureRenderer, 依旧在index.ts:
function ensureRenderer() {return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))}
发现这里其实返回的是createRenderer函数的返回值,createRenderer代码在runtime-core包的renderer.ts中,发现其最终调用同模块下的baseCreateRenderer函数,我们看下这个函数, 这是个超长的函数,我们需要看下框架:
function baseCreateRenderer(options: RendererOptions,createHydrationFns?: typeof createHydrationFunctions): any {// compile-time feature flags checkif (__ESM_BUNDLER__ && !__TEST__) {initFeatureFlags()}const {// ... 结构配置选项} = options// ******** 开始大规模函数定义const patch: PatchFn = () => {}const processText: ProcessTextOrCommentFn = () => {}const processCommentNode: ProcessTextOrCommentFn = () => {}const mountStaticNode = () => {}const patchStaticNode = () => {}const moveStaticNode = () => {}const removeStaticNode = () => {}const processElement = () => {}const mountElement = () => {}const setScopeId = () => {}const mountChildren: MountChildrenFn = () => {}const patchElement = () => {}const patchBlockChildren: PatchBlockChildrenFn = () => {}const patchProps = () => {}const processFragment = () => {}const processComponent = () => {}const mountComponent: MountComponentFn = () => {}const updateComponent = () => {}const setupRenderEffect: SetupRenderEffectFn = () => {}const updateComponentPreRender = () => {}const patchChildren: PatchChildrenFn = () => {}const patchUnkeyedChildren = () => {}const patchKeyedChildren = () => {}const move: MoveFn = () => {}const unmount: UnmountFn = () => {}const remove: RemoveFn = () => {}const removeFragment = () => {}const unmountComponent = () => {}const unmountChildren: UnmountChildrenFn = () => {}const getNextHostNode: NextFn = vnode => () => {}const render: RootRenderFunction = () => {}// ******** end 大规模函数定义const internals: RendererInternals = {p: patch,um: unmount,m: move,r: remove,mt: mountComponent,mc: mountChildren,pc: patchChildren,pbc: patchBlockChildren,n: getNextHostNode,o: options}let hydrate: ReturnType<typeof createHydrationFunctions>[0] | undefinedlet hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefinedif (createHydrationFns) {;[hydrate, hydrateNode] = createHydrationFns(internals as RendererInternals<Node,Element>)}// 返回对象return {render,hydrate,createApp: createAppAPI(render, hydrate)}}
上面的代码本来是一个几千行的代码,这里列出的只是结构,而且这些函数的签名都不对,为了简单,都删了参数列表。
我们只看关心的部分,之前的vue app实例其实就是createApp调用后的返回值,那么找下来就是baseCreateRenderer返回对象中的creatrApp,它是这么得到的reateAppAPI(render, hydrate),这个render就是上面这些大规模函数中的render:const render: RootRenderFunction = () => {}。
ok,先看下createAppAPI,此函数位于runtime-core包的apiCreateApp.ts:
export function createAppAPI<HostElement>(render: RootRenderFunction,hydrate?: RootHydrateFunction): CreateAppFunction<HostElement> {//接受两个参数,根组件的对象与props,默认为nullreturn function createApp(rootComponent, rootProps = null) {if (rootProps != null && !isObject(rootProps)) {__DEV__ && warn(`root props passed to app.mount() must be an object.`)rootProps = null}const context = createAppContext();//创建应用contentconst installedPlugins = new Set();//所有插件let isMounted = false//app实例const app: App = (context.app = {_uid: uid++,_component: rootComponent as ConcreteComponent,_props: rootProps,//props_container: null,//挂载容器_context: context,version,get config() {},set config(v) {},// 挂载中间件,通过useuse(plugin: Plugin, ...options: any[]) {},// 使用mixin的apimixin(mixin: ComponentOptions) {},// 在这个App实例上定义组件component(name: string, component?: Component): any {},// 定义指令directive(name: string, directive?: Directive) {},// 挂载内容,核心的组件渲染逻辑mount(rootContainer: HostElement, isHydrate?: boolean): any {},//卸载unmount() {},provide(key, value) {}})return app}}
看下结构,createAppAPI返回的是一个函数:createApp,createApp返回的就是vue实例了,看到vue实例的方法都在这里了,依旧只贴了个架子。
不过,在使用vue的时候,createApp得到app之后,会调用app.mount(‘#app’);这里的mount就在这里了,所以看下mount函数:
// rootContainer : #appmount(rootContainer: HostElement, isHydrate?: boolean): any {if (!isMounted) {// 1. 没有挂载,创建根节点的VNodeconst vnode = createVNode(rootComponent as ConcreteComponent,rootProps);// store app context on the root VNode.// this will be set on the root instance on initial mount.vnode.appContext = context// HMR root reloadif (__DEV__) {context.reload = () => {render(cloneVNode(vnode), rootContainer)}}if (isHydrate && hydrate) {hydrate(vnode as VNode<Node, Element>, rootContainer as any)} else {//NOTE:实例化触发render,使用渲染器渲染VNode,传入VNode与container容器render(vnode, rootContainer)}isMounted = trueapp._container = rootContainer// for devtools and telemetry; (rootContainer as any).__vue_app__ = appif (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {devtoolsInitApp(app, version)}return vnode.component!.proxy} else if (__DEV__) {warn(`App has already been mounted.\n` +`If you want to remount the same app, move your app creation logic ` +`into a factory function and create fresh app instances for each ` +`mount - e.g. \`const createMyApp = () => createApp(App)\``)}},
上面方法中rootComponent就是const app = createApp(App)的时候传进去的App组件。可以看到它先创建了这个App(根组件的)Vnode,然后后面经过一些环境判断,最终的逻辑都是调用render
// vnode => App// rootContainer => #apprender(vnode, rootContainer)
这个render,就又回到了renderer.ts的baseCreateRenderer中了,这回单看render函数:
const render: RootRenderFunction = (vnode, container) => {//卸载流程if (vnode == null) {if (container._vnode) {unmount(container._vnode, null, null, true)}// 1. 创建或者更新组件patch(container._vnode || null, vnode, container)}flushPostFlushCbs();//缓存VNode节点,表示已经渲染过container._vnode = vnode}
我们看到核心的逻辑来到了patch函数中,那就继续看patch:
const patch: PatchFn = (n1,n2,container,anchor = null,parentComponent = null,parentSuspense = null,isSVG = false,optimized = false) => {if (n1 && !isSameVNodeType(n1, n2)) {anchor = getNextHostNode(n1)unmount(n1, parentComponent, parentSuspense, true)// n1设置为null,保证后面走整个节点的mount逻辑n1 = null}//节点属于没有优化的类型if (n2.patchFlag === PatchFlags.BAIL) {optimized = falsen2.dynamicChildren = null}const { type, ref, shapeFlag } = n2switch (type) {case Text: // 处理文本 略...case Comment: // 处理注释 略...case Static:// 处理静态节点 略...case Fragment:// 处理Fragment元素</> 略...default://elemment 处理DOM元素if (shapeFlag & ShapeFlags.ELEMENT) {// 元素处理逻辑processElement( /* 参数略 */ )} else if (shapeFlag & ShapeFlags.COMPONENT) {// ==> 组件 组件处理逻辑processComponent( /* 参数略 */ )} else if (shapeFlag & ShapeFlags.TELEPORT) {// 处理TELEPORT 略} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {// 处理SUSPENSE 略} else if (__DEV__) {warn('Invalid VNode type:', type, `(${typeof type})`)}}if (ref != null && parentComponent) {setRef(ref, n1 && n1.ref, parentSuspense, n2)}}
代码被删过还是挺少的,发现patch方法对不同类型的节点处理方法都进行了分发,不过现在只关系n2.shapeFlag === ShapeFlags.COMPONENT 的情况,就是处理组件,处理组件的逻辑很显然要去看processComponent函数的代码了:
const processComponent = (n1: VNode | null,n2: VNode,container: RendererElement,anchor: RendererNode | null,parentComponent: ComponentInternalInstance | null,parentSuspense: SuspenseBoundary | null,isSVG: boolean,optimized: boolean) => {if (n1 == null) {if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {// keep-alive不管} else {// 组件初始化挂载mountComponent(n2,container,anchor,parentComponent,parentSuspense,isSVG,optimized)}} else {updateComponent(n1, n2, optimized)}}
我们看挂在阶段的话,n1应该就是null,所以继续追踪下去就是mountComponent,顾名思义,updateComponent是更新节点用的,现在是挂载阶段,mountComponent is Here:
// 挂载组件的处理逻辑const mountComponent: MountComponentFn = (initialVNode, // n2container,anchor,parentComponent,parentSuspense,isSVG,optimized) => {// 1. 创建组件实例const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(initialVNode,parentComponent,parentSuspense))// 非核心流程 略...if (__DEV__ && instance.type.__hmrId) { }if (__DEV__) { }if (isKeepAlive(initialVNode)) { }if (__DEV__) { }// 2. 设置组件实例setupComponent(instance) //render 数据 组件实例上面 setup// 非核心流程 略...if (__DEV__) { }if (__FEATURE_SUSPENSE__ && instance.asyncDep) { }// 3. setupRenderEffectsetupRenderEffect(instance,initialVNode,container,anchor,parentSuspense,isSVG,optimized)if (__DEV__) { }}
这个函数中我们观察主流程:1. 创建组件实例;2. 设置组件实例 setupComponent(instance); 3. 调用setupRenderEffect;
1) 先跟进下createComponentInstance,这个方法在同包下的component.ts:
export function createComponentInstance(vnode: VNode,parent: ComponentInternalInstance | null,suspense: SuspenseBoundary | null) {const type = vnode.type as ConcreteComponent// inherit parent app context - or - if root, adopt from root vnodeconst appContext =(parent ? parent.appContext : vnode.appContext) || emptyAppContext// 创建实例const instance: ComponentInternalInstance = {uid: uid++,// 组件唯一idvnode,// 组件的VNodetype,// VNode 节点类型parent,// 父组件实例appContext,// app上下文root: null!, // to be immediately set 根组件实例next: null,// 新的组件VNodesubTree: null!, // will be set synchronously right after creation 子组件的VNodeupdate: null!, // will be set synchronously right after creation //带有副作用的更新函数render: null,// 渲染函数proxy: null,// 渲染上下文代理exposed: null,withProxy: null,// 带有with区块的渲染上下文代理effects: null,// 响应式的闲逛对象provides: parent ? parent.provides : Object.create(appContext.provides),//依赖注入accessCache: null!,// 渲染代理的属性访问缓存renderCache: [],// 渲染缓存// local resovled assetscomponents: null, //注册的组件directives: null,//注册的指令// resolved props and emits optionspropsOptions: normalizePropsOptions(type, appContext), // 标准化的propsemitsOptions: normalizeEmitsOptions(type, appContext),// 标准化的emits// emitemit: null as any, // to be set immediately //事件派发方法emitted: null,// statectx: EMPTY_OBJ,// 渲染上下文data: EMPTY_OBJ,// data数据props: EMPTY_OBJ,// props数据attrs: EMPTY_OBJ,// 普通属性slots: EMPTY_OBJ,// 插槽refs: EMPTY_OBJ,// 组件refsetupState: EMPTY_OBJ,// steup返回的响应式结果setupContext: null,// setup函数上下文// suspense relatedsuspense,suspenseId: suspense ? suspense.pendingId : 0,asyncDep: null,asyncResolved: false,// lifecycle hooks 生命周期// not using enums here because it results in computed propertiesisMounted: false,// 是否挂载isUnmounted: false,// 是否卸载isDeactivated: false,// 是否激活bc: null,// before createc: null,// createdbm: null,// before mountedm: null,// mountedbu: null,// before updateu: null,// updatedum: null,// unmountedbum: null,// before unmountda: null,// deactiveda: null,// activedrtg: null,// render triggeredrtc: null,// render trackedec: null// error}if (__DEV__) { }instance.root = parent ? parent.root : instanceinstance.emit = emit.bind(null, instance)if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { }return instance}
这个方法最终return组件的instance,这么多属性,详细含义见注释。
2)setupComponent也和上面的createComponentInstance位于同一个文件下面,这个函数是在设置初始属性另外用户在setup中写的逻辑也在这里执行:
export function setupComponent(instance: ComponentInternalInstance,isSSR = false) {isInSSRComponentSetup = isSSRconst { props, children, shapeFlag } = instance.vnodeconst isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT//设置属性initProps(instance, props, isStateful, isSSR)initSlots(instance, children)// 执行setup(){} 设置有状态的实例相关数据const setupResult = isStateful? setupStatefulComponent(instance, isSSR) // 设置setup和render: undefinedisInSSRComponentSetup = falsereturn setupResult}
这里的setupStatefulComponent 非常值得一看究竟:
function setupStatefulComponent(instance: ComponentInternalInstance,isSSR: boolean) {const Component = instance.type as ComponentOptionsif (__DEV__) { }// 1. 创建渲染代理的属性访问缓存instance.accessCache = Object.create(null)// 2. create public instance / render proxy 创建上下文代理// 访问实例上的props、options、data的时候,都可以通过组件实例上的ctx上访问到对应的结果,这就是一个代理的过程instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)if (__DEV__) { }// 3. call setup() 调用setupconst { setup } = Componentif (setup) {// 省略 setup,其实setup中的逻辑后面也会// 走到finishComponentSetup} else {// 完成组件实例的设置finishComponentSetup(instance, isSSR)}}
可以看到,这段代码的逻辑是首先,设置上下文代理,而后如果又setup的逻辑需要执行就执行setup,有没有setup其实最后都能走到了finishComponentSetup函数中,这个finishComponentSetup就很重要了,因为我们模版的编译逻辑就在里面:
截取一段代码:
if (Component.render) {instance.render = Component.render as InternalRenderFunction}} else if (!instance.render) {// could be set from setup() 设置对应的renderif (compile && Component.template && !Component.render) {if (__DEV__) { }// 缓存render到component中,在线编译模板的情况Component.render = compile(Component.template, {isCustomElement: instance.appContext.config.isCustomElement,delimiters: Component.delimiters})if (__DEV__) { }}//挂载render到实例上instance.render = (Component.render || NOOP) as InternalRenderFunction// ....}
可以看到instance.render 最终挂载了编译的结果(编译的结果是一个render函数这在上文中提及),更深入的逻辑就不进入了。
3)setupRenderEffect: instance已经构造好了,下面调用的就是这个函数, 这个函数回到了renderer.ts的baseCreateRenderer中来,这可能将是初始化中最复杂的一个函数:
const setupRenderEffect: SetupRenderEffectFn = (instance,initialVNode,container,anchor,parentSuspense,isSVG,optimized) => {// instance的update被赋值了effectinstance.update = effect(function componentEffect() {if (!instance.isMounted) { // 没挂载let vnodeHook: VNodeHook | null | undefinedconst { el, props } = initialVNodeconst { bm, m, parent } = instance // beforemount// beforeMount hook 生命周期的调用 before mounted、mountedif (bm) { invokeArrayFns(bm) }// onVnodeBeforeMountif ((vnodeHook = props && props.onVnodeBeforeMount)) {invokeVNodeHook(vnodeHook, parent, initialVNode)}if (__DEV__) { }// ⭐️⭐️⭐️⭐️⭐️// 这个方法很重要const subTree = (instance.subTree = renderComponentRoot(instance))if (__DEV__) { }if (el && hydrateNode) {if (__DEV__) { }hydrateNode(/* 略参数 */)if (__DEV__) { }} else {if (__DEV__) { }// 把对应的子树渲染到container中,子树可能是element、text、component等patch(null,subTree,container,anchor,instance,parentSuspense,isSVG)if (__DEV__) { }// 保留渲染生成子树的根节点 DOM节点initialVNode.el = subTree.el}// mounted hook 挂载的生命周期调用if (m) { queuePostRenderEffect(m, parentSuspense) }// onVnodeMounted// ... 省略了部分处理 issues的逻辑...} else { // 组件已经被挂载,那就是更新了/*** 省略了更新流程 大致是:* 1 更新组件VNode的节点信息* 2 渲染成为新的子树VNode* 3 根据新旧子树VNode执行patch逻辑*/}}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)}
setupRenderEffect这个函数,以及其内部的调用函数renderComponentRoot将会将整个流程串联起来,非常重要~
这个函数的结构是这样,只有一句话instance.update = effect(componentEffect);
上文中我们学习了effect是vue3中响应式数据的重要触发方法,effect内部的函数fn会先执行,然后因为响应式数据的track和trigger机制,在track阶段收集的、trigger阶段查找到的依赖关系本质上都是一个个effect函数。
那么,现在讨论的是挂载阶段,就挂载阶段来看,再仔细看下effect中的componentEffect,这个函数最重要的一点就是生成对应的subTree,调用了renderComponentRoot,这个函数在componentRenderUtils.ts中。
这个函数就从instance中拿到了render方法,并执行:
result = normalizeVNode(render.length > 1? render(props,__DEV__? {get attrs() {markAttrsAccessed()return attrs},slots,emit}: { attrs, slots, emit }): render(props, null as any /* we know it doesn't need it */))
在render中使用了响应式数据,所以依赖关系的收集这些步骤也会如期进行,我们的整体流程就串联起来了。
关于挂载和更新整体总结下,这里整体的核心流程是:
- 判断是挂载组件,还是更新组件,挂载调用mountComponent,更新调用updateComponent;
- 对于首次挂载的mountedComponent内部;
a. 创建组件实例,根据组件VNode;
b. 设置组件实例,调用setup方法,以及处理options等等;
c. 触发并且运行带有副作用的渲染函数,内部会结合使用Effect与render的调用; - 渲染逻辑
a. 生成对应的subTree,因为组件会是其他子组件的情况,这个过程是调用render的过程,然后生成对应的subTree;
b. 把对应的subTree渲染到container中,通过执行对应的patch,然后再次进入到这个函数,如果patch是element,就开始触发渲染任务;
不过结合响应式数据整体串联起来更重要。
