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 };
// 返回一个监听的新数据 proxy
const 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
}
// 已经是响应式数据,并且不是只读的 proxy
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 原始数据已经有代理数据,直接找到之前代理后的数据
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 判断数据是否可以被代理,比如VNode对象,component实例等
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 处理代理逻辑,内部有对应的get、set
const 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)
// 数组则借助此对象; arrayInstrumentations
if (!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
}
// 不是只读数据,开始收集依赖:track
if (!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 any
arrayInstrumentations[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 any
arrayInstrumentations[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修改原型链上的属性会触发两次setter
if (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的时候,没有调度,直接执行fn
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
// 2. 开启允许依赖收集
enableTracking()
// 3. effect压栈
effectStack.push(effect)
// 4. 设置激活的effect
activeEffect = effect
// 5. 执行fn逻辑,触发内部数据依赖的收集,开始收集activeEffect
return fn();
// targetMap
} finally {
// 6. 出栈
effectStack.pop()
// 恢复之前的状态
resetTracking()
// 指向栈最后一个effect
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
// 这里
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true // 表示一个effect函数
effect.active = true // 激活状态
effect.raw = fn // 原始的函数
effect.deps = [] // effect对应的依赖 ,一个effect里面,有多个响应式数据的使用,可能这些数据会触发其他依赖的修改
//其他配置项
effect.options = options
return 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),这个数据结构见定义:
```typescript
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
// targetMap
const 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 target
depsMap.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 | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (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 changes
add(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case 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)
}
// 重写挂载的mount
const { mount } = app
app.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 check
if (__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] | undefined
let hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefined
if (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,默认为null
return 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();//创建应用content
const 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) {
},
// 挂载中间件,通过use
use(plugin: Plugin, ...options: any[]) {
},
// 使用mixin的api
mixin(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 : #app
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
// 1. 没有挂载,创建根节点的VNode
const 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 reload
if (__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 = true
app._container = rootContainer
// for devtools and telemetry
; (rootContainer as any).__vue_app__ = app
if (__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 => #app
render(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 = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (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, // n2
container,
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. setupRenderEffect
setupRenderEffect(
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 vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
// 创建实例
const instance: ComponentInternalInstance = {
uid: uid++,// 组件唯一id
vnode,// 组件的VNode
type,// VNode 节点类型
parent,// 父组件实例
appContext,// app上下文
root: null!, // to be immediately set 根组件实例
next: null,// 新的组件VNode
subTree: null!, // will be set synchronously right after creation 子组件的VNode
update: 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 assets
components: null, //注册的组件
directives: null,//注册的指令
// resolved props and emits options
propsOptions: normalizePropsOptions(type, appContext), // 标准化的props
emitsOptions: normalizeEmitsOptions(type, appContext),// 标准化的emits
// emit
emit: null as any, // to be set immediately //事件派发方法
emitted: null,
// state
ctx: EMPTY_OBJ,// 渲染上下文
data: EMPTY_OBJ,// data数据
props: EMPTY_OBJ,// props数据
attrs: EMPTY_OBJ,// 普通属性
slots: EMPTY_OBJ,// 插槽
refs: EMPTY_OBJ,// 组件ref
setupState: EMPTY_OBJ,// steup返回的响应式结果
setupContext: null,// setup函数上下文
// suspense related
suspense,
suspenseId: suspense ? suspense.pendingId : 0,
asyncDep: null,
asyncResolved: false,
// lifecycle hooks 生命周期
// not using enums here because it results in computed properties
isMounted: false,// 是否挂载
isUnmounted: false,// 是否卸载
isDeactivated: false,// 是否激活
bc: null,// before create
c: null,// created
bm: null,// before mounted
m: null,// mounted
bu: null,// before update
u: null,// updated
um: null,// unmounted
bum: null,// before unmount
da: null,// deactived
a: null,// actived
rtg: null,// render triggered
rtc: null,// render tracked
ec: null// error
}
if (__DEV__) { }
instance.root = parent ? parent.root : instance
instance.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 = isSSR
const { props, children, shapeFlag } = instance.vnode
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
//设置属性
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
// 执行setup(){} 设置有状态的实例相关数据
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR) // 设置setup和render
: undefined
isInSSRComponentSetup = false
return setupResult
}
这里的setupStatefulComponent 非常值得一看究竟:
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
if (__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() 调用setup
const { setup } = Component
if (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() 设置对应的render
if (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被赋值了effect
instance.update = effect(function componentEffect() {
if (!instance.isMounted) { // 没挂载
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance // beforemount
// beforeMount hook 生命周期的调用 before mounted、mounted
if (bm) { invokeArrayFns(bm) }
// onVnodeBeforeMount
if ((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,就开始触发渲染任务;
不过结合响应式数据整体串联起来更重要。