functionflushJobs(seen?: CountMap) { isFlushPending = false isFlushing = true if (__DEV__) { seen = seen || newMap() }
// Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child so its render effect will have smaller // priority number) // 2. If a component is unmounted during a parent component's update, // its update can be skipped. // 组件更新的顺序是从父到子 因为父组件总是在子组件之前创建 所以它的渲染效果将具有更小的优先级 // 如果一个组件在父组件更新期间被卸载 则可以跳过它的更新 queue.sort(comparator) // ... // 先执行queue中的job 然后执行pendingPostFlushCbs中的job // 这里可以实现watch中的 postFlush try { for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { const job = queue[flushIndex] if (job && job.active !== false) { if (__DEV__ && check(job)) { continue } callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) } } } finally { // job执行完毕后清空队列 flushIndex = 0 queue.length = 0
// in SSR there is no need to setup an actual effect, and it should be noop // unless it's eager or sync flush let ssrCleanup: (() =>void)[] | undefined if (__SSR__ && isInSSRComponentSetup) { // we will also not call the invalidate callback (+ runner is not set up) // ssr处理 }
// oldValue声明 多个source监听则初始化为数组 let oldValue: any = isMultiSource ? newArray((source as []).length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE // 调度器调用时执行 const job: SchedulerJob = () => { if (!effect.active || !effect.dirty) { return } if (cb) { // watch(source, cb) // 获取newValue const newValue = effect.run() if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i])) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // cleanup before running cb again if (cleanup) { cleanup() } // 调用cb 参数为newValue、oldValue、onCleanup callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, // pass undefined as the old value when it's changed for the first time oldValue === INITIAL_WATCHER_VALUE ? undefined : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, onCleanup, ]) // 更新oldValue oldValue = newValue } } else { // watchEffect effect.run() } }
// important: mark the job as a watcher callback so that scheduler knows // it is allowed to self-trigger (#1727) job.allowRecurse = !!cb
// 创建任务队列的调度回调scheduler let scheduler: EffectScheduler if (flush === 'sync') { // 同步更新 即每次响应式数据改变都会回调一次cb scheduler = job as any // the scheduler function gets called directly } elseif (flush === 'post') { // job放入pendingPostFlushCbs队列中 // pendingPostFlushCbs队列会在queue队列执行完毕后执行 函数更新effect通常会放在queue队列中 // 所以pendingPostFlushCbs队列执行时组件已经更新完毕 scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { // default: 'pre' job.pre = true if (instance) job.id = instance.uid // 默认更新 // 原理在这:https://rile14929.github.io/zh-CN/vue3%E7%BB%84%E4%BB%B6%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E5%92%8CNextTick%E7%9A%84%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6.html scheduler = () => queueJob(job) }
// 创建effect effect.run的时候建立effect与getter内响应式数据的关系 const effect = new ReactiveEffect(getter, NOOP, scheduler)
get(target: Target, key: string | symbol, receiver: object) { const isReadonly = this._isReadonly, shallow = this._shallow if (key === ReactiveFlags.IS_REACTIVE) { // 代理 __v_isReactive return !isReadonly } elseif (key === ReactiveFlags.IS_READONLY) { // 代理 __v_isReadonly return isReadonly } elseif (key === ReactiveFlags.IS_SHALLOW) { // 代理 __v_isShallow return shallow } elseif (key === ReactiveFlags.RAW) { // 函数中判断响应式对象是否存在 __v_raw 属性,如果存在就返回这个响应式对象本身。 if ( receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) || // receiver is not the reactive proxy, but has the same prototype // this means the reciever is a user proxy of the reactive proxy Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver) ) { return target } // early return undefined return }
// 是否是数组 const targetIsArray = isArray(target)
if (!isReadonly) { // arrayInstrumentations 包含对数组一些方法修改的函数 if (targetIsArray && hasOwn(arrayInstrumentations, key)) { returnReflect.get(arrayInstrumentations, key, receiver) } if (key === 'hasOwnProperty') { return hasOwnProperty } }
const res = Reflect.get(target, key, receiver) // 内置 Symbol key 不需要依赖收集 if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res }
// 依赖收集 if (!isReadonly) { track(target, TrackOpTypes.GET, key) }
if (shallow) { return res }
if (isRef(res)) { // ref unwrapping - skip unwrap for Array + integer key. return targetIsArray && isIntegerKey(key) ? res : res.value }
// 如果 res 是个对象或者数组类型,则递归执行 reactive 函数把 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) }
functioncreateArrayInstrumentations() { const instrumentations: Record<string, Function> = {} // instrument identity-sensitive Array methods to account for possible reactive // values ;(['includes', 'indexOf', 'lastIndexOf'] asconst).forEach(key => { instrumentations[key] = function (this: unknown[], ...args: unknown[]) { // toRaw 可以把响应式对象转成原始数据,this就是Reflect的receiver const arr = toRaw(this) as any 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 = arr[key](...args) if (res === -1 || res === false) { // if that didn't work, run it again using raw values. return arr[key](...args.map(toRaw)) } else { return res } } }) // instrument length-altering mutation methods to avoid length being tracked // which leads to infinite loops in some cases (#2137) ;(['push', 'pop', 'shift', 'unshift', 'splice'] asconst).forEach(key => { instrumentations[key] = function (this: unknown[], ...args: unknown[]) { pauseTracking() pauseScheduling() const res = (toRaw(this) as any)[key].apply(this, args) resetScheduling() resetTracking() return res } }) return instrumentations }
也就是说,当 target 是一个数组的时候,我们去访问 target.includes、target.indexOf、 target.lastIndexOf 就会执行 arrayInstrumentations 代理的函数,除了调用数组本身的方法求值外,还对数组每个元素做了依赖收集。因为一旦数组的元素被修改,数组的这几个 API 的返回结果都可能发生变化,所以我们需要跟踪数组每个元素的变化。
// 创建要执行的deps数组 let deps: (Dep | undefined)[] = [] if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target // 清空数组或者map的时候触发所有key对应的的effect函数 deps = [...depsMap.values()] } elseif (key === 'length' && isArray(target)) { const newLength = Number(newValue) depsMap.forEach((dep, key) => { if (key === 'length' || (!isSymbol(key) && key >= newLength)) { deps.push(dep) } }) } else { // schedule runs for SET | ADD | DELETE // set add delete操作 将key对应的effect函数添加到deps数组中 if (key !== void0) { deps.push(depsMap.get(key)) }
// also run for iteration key on ADD | DELETE | Map.SET // 根据不同的操作类型push对应的dep switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } elseif (isIntegerKey(key)) { // new index added to array -> length changes deps.push(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { deps.push(depsMap.get(ITERATE_KEY)) } break } }
pauseScheduling() for (const dep of deps) { if (dep) { triggerEffects( dep, DirtyLevels.Dirty, __DEV__ ? { target, type, key, newValue, oldValue, oldTarget, } : void0, ) } } resetScheduling() }
mount( rootContainer: HostElement, isHydrate?: boolean, namespace?: boolean | ElementNamespace, ) { if (!isMounted) { // #5571 if (__DEV__ && (rootContainer as any).__vue_app__) { warn( `There is already an app instance mounted on the host container.\n` + ` If you want to mount another app on the same host container,` + ` you need to unmount the previous app by calling \`app.unmount()\` first.`, ) } const vnode = createVNode(rootComponent, 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 = () => { // casting to ElementNamespace because TS doesn't guarantee type narrowing // over function boundaries render( cloneVNode(vnode), rootContainer, namespace as ElementNamespace, ) } }
if (isHydrate && hydrate) { hydrate(vnode as VNode<Node, Element>, rootContainer as any) } else { render(vnode, rootContainer, namespace) } isMounted = true app._container = rootContainer // for devtools and telemetry ;(rootContainer as any).__vue_app__ = app
return getExposeProxy(vnode.component!) || vnode.component!.proxy } elseif (__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)\``, ) } }
// resolve props and slots for setup context if (!(__COMPAT__ && compatMountInstance)) { if (__DEV__) { startMeasure(instance, `init`) } // 2.设置组件实例 // 初始化组件, 主要是对组件的props/slots进行初始化处理 // 执行setup 生成render函数(所以setup是在所有选项式API钩子之前调用 包括beforeCreate) setupComponent(instance) if (__DEV__) { endMeasure(instance, `init`) } }
// setup() is async. This component relies on async logic to be resolved // before proceeding if (__FEATURE_SUSPENSE__ && instance.asyncDep) { // ... } else { // 3.调用设置和运行有副作用的渲染函数
if (isPromise(setupResult)) { setupResult.then(unsetCurrentInstance, unsetCurrentInstance) if (isSSR) { // return the promise so server-renderer can wait on it return setupResult .then((resolvedResult: unknown) => { handleSetupResult(instance, resolvedResult, isSSR) }) .catch(e => { handleError(e, instance, ErrorCodes.SETUP_FUNCTION) }) } elseif (__FEATURE_SUSPENSE__) { // async setup returned Promise. // bail here and wait for re-entry. instance.asyncDep = setupResult if (__DEV__ && !instance.suspense) { const name = Component.name ?? 'Anonymous' warn( `Component <${name}>: setup function returned a promise, but no ` + `<Suspense> boundary was found in the parent component tree. ` + `A component with async setup() must be nested in a <Suspense> ` + `in order to be rendered.`, ) } } elseif (__DEV__) { warn( `setup() returned a Promise, but the version of Vue you are using ` + `does not support it yet.`, ) } } else { handleSetupResult(instance, setupResult, isSSR) } } else { finishComponentSetup(instance, isSSR) } }
// patch新旧节点更新组件 patch( prevTree, nextTree, // parent may have changed if it's in a teleport hostParentNode(prevTree.el!)!, // anchor may have changed if it's in a fragment getNextHostNode(prevTree), instance, parentSuspense, isSVG ) // ... } }
pauseTracking() // props update may have triggered pre-flush watchers. // flush them before the render update. flushPreFlushCbs(instance) resetTracking() }
下面讨论一下渲染的时机。规范定义在一次循环中,Update the rendering 会在 Microtasks: Perform a microtask checkpoint 后运行。
渲染时机
以下的例子中,用 chrome 的 Developer tools 的 Timeline 查看各部分运行的时间点。当我们点击这个 div 的时候,截取了部分时间线。
黄色部分是脚本运行,紫色部分是更新 render 树、计算布局,绿色部分是绘制。
绿色和紫色部分可以认为是 Update the rendering。
例子 1
1 2 3 4 5 6 7 8 9 10
<divid="con">this is con</div> <script> var t = 0 var con = document.getElementById('con') con.onclick = function(){ setTimeout(functionsetTimeout1(){ con.textContent = t }, 0) } </script>
<divid="con">this is con</div> <script> var con = document.getElementById('con') var i = 0 var raf = function(){ requestAnimationFrame(function(){ con.textContent = i Promise.resolve().then(function () { i++ if (i < 3) raf() }) }) } con.onclick = function(){ raf() } </script>
map of animation frame callbacks 为空,也就是帧动画回调为空,可以通过 requestAnimationFrame 来请求帧动画。
如果上述的判断决定本轮不需要渲染,那么下面的几步也不会继续运行:
This step enables the user agent to prevent the steps below from running for other reasons, for example, to ensure certain tasks are executed immediately after each other, with only microtask checkpoints interleaved (and without, e.g., animation frame callbacks interleaved). Concretely, a user agent might wish to coalesce timer callbacks together, with no intermediate rendering updates. 有时候浏览器希望两次「定时器任务」是合并的,他们之间只会穿插着 microTask 的执行,而不会穿插屏幕渲染相关的流程
An event loop has one or more task queues. For example, a user agent could have one task queue for mouse and key events (to which the user interaction task source is associated), and another to which all other task sources are associated. Then, using the freedom granted in the initial step of the event loop processing model, it could give keyboard and mouse events preference over other tasks three-quarters of the time, keeping the interface responsive but not starving other task queues. Note that in this setup, the processing model still enforces that the user agent would never process events from any one task source out of order.
根活动指的是浏览器触发的一系列流程。例如,当你点击页面内容,浏览器触发一个 Event 作为根活动,该 Event 可能回调一个事件处理事件。 在 Main 面板中的火焰图中,根活动展示在上部,在 Call Tree 和 Event Log 面板中,根活动展示在顶层。
Call Tree 标签页
Call Tree 标签页中展示记录中被选中部分的活动信息。
图中 Activity 列中显示的 Timer fired、 Paint、Recalculate Style 和 Layout 代表根活动。层级嵌套表示代表回调栈。如图中 Function Call 调用 u,再调用 getImageUrl,继续调用 getLinkUrl 等等。 Self Time 表示对应活动消耗的时间,Total Time 表示对应活动以及子活动共同消耗的时间。 点击 Self Time,Total Time 或者 Activity 表头区域,可按对应列排序。 利用 Filter 输入框区域,输入活动名过滤事件。 Grouping 分组菜单默认为 No Grouping,利用该功能可以根据不同的分类将活动进行分组。 点击右侧 Show Heaviest Stack,在右侧展示当前选中活动中占用时间最多的子活动信息。
Bottom-Up 标签页
利用 Bottom-Up 标签查看占用最多时间的活动。 Self Time 表示对应活动消耗的时间。 Total Time 表示对应活动以及子活动共同消耗的时间。
Event Log 标签页
Event Log 标签页按顺序展示记录中发生的活动。 Start Time 列表示该项活动的开始时间,该时间相对于记录开始时间计算。例如图中选中项开始时间为 1566.2 ms,代表该活动在记录开始之后 1566.2 ms 后开始。 Self Time 表示对应活动消耗的时间。 Total Time 表示对应活动以及子活动共同消耗的时间。 点击 Start Time 、Self Time、Total Time 表头区域,可按对应列排序。 利用 Filter 输入框区域,输入活动名过滤事件。 利用 Duration 下拉菜单过滤>=1ms 或者>=15ms 的活动。该菜单默认选中 All 选项,展示所有活动。 利用 Loading、Experience、Scripting、Rendering、Painting 选项进行分类过滤。
分析每秒传输帧数(FPS)
查看 FPS 图表了解整个记录中 FPS 的概况。
Frames 模块查看每一帧时间消耗。
利用 FPS meter 工具(MoreTools—>Rendering)在页面运行时实时查看 FPS 信息。