Writing in the front
Vue3 has been around for a while now and has been particularly optimized compared to Vue2. But where is the specific good, in addition to developers with cool, the framework of the underlying optimization we need to study the source code to have personal experience.
This paper mainly compares Vue3 and Vue2 at the source level, thinking and summarizing what optimization the new Vue3 has done, and what is good about these optimization.
Ps: Click on the portal to organize the source code of Vue2
Note: some of the titles in the article are underlined blue method names. These methods have corresponding hyperlinks. Click to jump to the corresponding file location in the source code
1. Initialization
Compared with Vue2, Vue3 is a reconstruction and uses a multi-module architecture, which is divided into three modules: Compiler, Reactivity and Runtime
- compiler-core
- compiler-dom
- runtime-core
- runtime-dom
- reactivity
In the future, custom rendering only needs to be extended based on compiler and Runtime core modules
This led to the concept of the Renderer, which stands for renderer, being the entry point to the application, coming from the Runtime module
Under the Runtime module, we can extend the rendering rules of any platform. Currently, we will study the entry of the Web platform is the Runtime-DOM.
The method of initialization also changed, creating the page application using the createApp method on the Vue instance, so we started with createApp
1.1. Initialization of Vue
1.1.1. createApp
-
role
- To obtainThe renderer(
renderer
),The rendererthecreateApp
createApp object - extension
mount
methods
- To obtainThe renderer(
-
The core source
const createApp = ((. args) = > { constapp = ensureRenderer().createApp(... args);const { mount } = app; app.mount = (containerOrSelector: Element | ShadowRoot | string) :any= > { const container = normalizeContainer(containerOrSelector); if(! container)return; // Clear the DOM contents before mounting container.innerHTML = ' '; // Call the original mount method to mount it const proxy = mount(container); return proxy; }; return app; }) as CreateAppFunction<Element>; function ensureRenderer() { // Return the singleton renderer return renderer || (renderer = createRenderer<Node, Element>(rendererOptions)); } Copy the code
The createApp is currently one that is ensureRenderer and is intended to be a Web platform-based renderer. The “current” ensureRenderer is one of the createApp ensureRenderer and will be specified later.
RendererOptions is a web platform-specific way of manipulating DOM and properties.
1.1.2. createRenderer
-
role
- Through the parameter
options
createplatformtheClient renderer
- Through the parameter
-
The core source
function createRenderer<HostNode = RendererNode.HostElement = RendererElement> ( options: RendererOptions<HostNode, HostElement> ) { return baseCreateRenderer<HostNode, HostElement>(options); } Copy the code
BaseCreateRenderer is called again here to create the client renderer, and you can see below the current file that there is another method called createHydrationRenderer, which also calls baseCreateRenderer, which creates the server renderer
1.1.3. baseCreateRenderer
-
role
- Returns the real platform renderer based on platform operation parameters
-
The core source
function baseCreateRenderer(options: RendererOptions, createHydrationFns? :typeof createHydrationFunctions ) :any { Insert, remove, patchProp, etc const{... } = options// Next comes the many component rendering and diff methods const patch = (n1, n2, container, ...) = >{... }const processElement = (n1, n2, container) = >{... }const mountElement = (vnode, container, ...) = >{... }const mountChildren = (children, container, ...) = > {...} ... const render: RootRenderFunction = (vnode, container) = > { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null.null.true); }}else { // Initialization and updates go here, similar to vue2's __patch__ patch(container._vnode || null, vnode, container); } container._vnode = vnode; }; return { render, hydrate, createApp: createAppAPI(render, hydrate), }; } Copy the code
The renderer includes the render, Hydrate, and createApp methods,
This step is very important, and it is likely that future cross-platform development based on Vue3 will follow this pattern.
The options parameter is used to deconstruct the platform-based manipulation of dom and attributes, which is used to create real render and update functions. One of the things to watch out for is Patch, because patch is not only responsible for rendering and updating, but also for future initialization components through this portal to ⭐.
The Render method is similar to vm._update of vue2, responsible for initialization and update
Since baseCreateRenderer is a method that is over 1800 lines long, you can only focus on the renderer that is eventually returned when you initialize it,
The last createApp is created by the factory function createAppAPI
1.1.4. createAppAPI
-
role
- Through the parameter
render
andhydrate
createplatformthecreateApp
Method,createApp
Used to create the realApp (Vue) instance
- Through the parameter
-
The core source
function createAppAPI<HostElement> (render: RootRenderFunction, hydrate? : RootHydrateFunction) :CreateAppFunction<HostElement> { return function createApp(rootComponent, rootProps = null) { const app = { use(plugin: Plugin, ... options:any[]){ plugin.install(app, ... options);return app; }, mixin(mixin: ComponentOptions) { context.mixins.push(mixin); return app; }, component(name: string, component? : Component):any { context.components[name] = component; return app; }, directive(name: string, directive? : Directive) { context.directives[name] = directive; returnapp; }, mount(rootContainer: HostElement, isHydrate? :boolean) :any{... },unmount() { if (isMounted) { render(null, app._container); }},provide(key, value) { context.provides[key as string] = value; returnapp; }};return app; }; } Copy the code
Remember, I reminded you that there are two createApp methods
- in
runtime-core
Module: create realApp (Vue) instance - in
runtime-dom
In the module: Passruntime-core
Module to createRenderer for web platform, the use ofThe renderergetThe instance
CreateApp internally defines a number of methods on instances: use, mixin, Component, Directive, mount, unmount, provide. Those familiar with Vue2 may notice that static methods are now instance methods, and almost every method returns an app object, so it can be called chained, like this
const app = createApp({ setup() { const state = reactive({ count: 0});return { state }; }, }) .use(store) .directive(transfer) .mixin(cacheStore) .mount('#app'); Copy the code
When this step is complete, the createApp method with the renderer is also available, and the whole renderer is returned to the Runtime-dom module. Then create the app instance through the renderer, extend the mount method of the instance, and then enter the rendering stage of the instance.
Mount is the entrance of the rendering stage core source code as follows:
mount(rootContainer: HostElement, isHydrate? :boolean) :any { if(! isMounted) {// Initialize the virtual DOM tree const vnode = createVNode(rootComponent as ConcreteComponent, rootProps); if (isHydrate && hydrate) { // Server render hydrate(vnode as VNode<Node, Element>, rootContainer as any); } else { // Client render render(vnode, rootContainer); } return vnode.component!.proxy; } } Copy the code
The unextended mount method of an app instance is equivalent to Vue2’s updateComponent and does two things:
-
Get the virtual DOM,
-
Convert the virtual DOM to the real DOM using the Render method
-
The core source
const render: RootRenderFunction = (vnode, container) = > { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null.null.true); }}else { patch(container._vnode || null, vnode, container); } flushPostFlushCbs(); container._vnode = vnode; }; Copy the code
The render method is particularly similar to vm._update of Vue2, which is the entry point for initial rendering and component update. Patch is called for both. Since there is no old virtual DOM for the first rendering, n1 is null
-
- in
1.1.5. patch
-
role
- Initialize and update the component according to the type of the virtual DOM, and finally convert the virtual DOM into the real DOM. (PS: Component includes browser host component and custom component, same below)
-
The core source
const patch: PatchFn = ( n1,// The old virtual DOM n2,// New virtual DOM container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false ) = > { // Type of the new node const { type, ref, shapeFlag } = n2 switch (type) { case Text: ... break case Comment: ... break case Static: ... break case Fragment: ... break default: if (shapeFlag & ShapeFlags.ELEMENT) { ... } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } } } Copy the code
The reason for the component initialization and update is that Vue3’s patch is different from Vue2’s __patch__, which is only responsible for rendering, so we can say it is the rendering of the component. However, the function triggered by Vue3’s patch in the rendering stage includes not only the rendering of the component, but also the initialization stage of the component
Since the new virtual DOM (n2) passed in at initialization is the argument to the developer’s call to createApp, it is judged to be an object type and will be treated as a custom component, so the processComponent method is executed next
The core source code is as follows:
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) { ... } else { // Initialize rendermountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized); }}else { // Component updateupdateComponent(n1, n2, optimized); }};Copy the code
Since the old virtual DOM passed in for the first rendering is null, the mountComponent method is executed
1.1.6. mountComponent
-
role
- Initialize theCustom Components
- Creating a component instance
- Installing components (that is, component initialization)
- Install side effects: Finish rendering and define update functions
- Initialize theCustom Components
-
The core source
const mountComponent: MountComponentFn = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = > { // 1. Create a component instance const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense )); // inject renderer internals for keepAlive if (isKeepAlive(initialVNode)) { (instance.ctx as KeepAliveContext).renderer = internals; } // 2. Install components (that is, component initialization) setupComponent(instance); // setup() is async. This component relies on async logic to be resolved // before proceeding if (__FEATURE_SUSPENSE__ && instance.asyncDep) { parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect); // Give it a placeholder if this is not hydration // TODO handle self-defined fallback if(! initialVNode.el) {const placeholder = (instance.subTree = createVNode(Comment)); processCommentNode(null, placeholder, container! , anchor); }return; } // 3. Install side effect: finish rendering and define update function setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized); }; Copy the code
Component initialization includes:
createComponentInstance
: createComponent instancesetupComponent
: Installs components (component initialization). (similar to theVue2Initializing the executionvm._init
Methods)setupRenderEffect
: Installs the effects of the render function, completes the component rendering, and defines the update function for the component. (effectReplaced theVue2theWatcher)
1.2. Component initialization
1.2.1. setupComponent
-
role
- Installing components (component initialization)
mergeOptions
,- Define instance properties, events, processing slots,
- through
setupStatefulComponent
Methods to completeData response
- Installing components (component initialization)
-
The core source
function setupComponent(instance: ComponentInternalInstance, isSSR = false) { isInSSRComponentSetup = isSSR; const { props, children, shapeFlag } = instance.vnode; const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT; // Initialize props initProps(instance, props, isStateful, isSSR); // Initialize the slot initSlots(instance, children); // Data responsive const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined; isInSSRComponentSetup = false; return setupResult; } Copy the code
SetupStatefulComponent is responsible for data responsiveness
(1)setupStatefulComponent
-
role
- Complete data responsiveness
-
The core source
function setupStatefulComponent(instance: ComponentInternalInstance, isSSR: boolean) { // createApp configuration object const Component = instance.type as ComponentOptions; // 0. create render proxy property access cache instance.accessCache = Object.create(null); // 1.render function context instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers); // 2. Handle the setup function const { setup } = Component; if (setup) { const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null); currentInstance = instance; pauseTracking(); const setupResult = callWithErrorHandling(setup, instance, ErrorCodes.SETUP_FUNCTION, [ __DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext, ]); resetTracking(); currentInstance = null; if (isPromise(setupResult)) { if (isSSR) { ... } else if (__FEATURE_SUSPENSE__) { // async setup returned Promise. // bail here and wait for re-entry.instance.asyncDep = setupResult; }}else { FinishComponentSetup is also executedhandleSetupResult(instance, setupResult, isSSR); }}else{ finishComponentSetup(instance, isSSR); }}Copy the code
Without setup, handleSetupResult is executed, and finishComponentSetup is still called
(2)finishComponentSetup
-
role
- Make sure that
instance
There areRender function - Compatible with Vue2 Options API data response function
- Make sure that
-
The core source
function finishComponentSetup(instance: ComponentInternalInstance, isSSR: boolean) { const Component = instance.type as ComponentOptions; // template / render function normalization if (__NODE_JS__ && isSSR) { ... } else if(! instance.render) {// could be set from setup() if(compile && Component.template && ! Component.render) { Component.render = compile(Component.template, {isCustomElement: instance.appContext.config.isCustomElement, delimiters: Component.delimiters, }); } instance.render = (Component.render || NOOP) as InternalRenderFunction; // for runtime-compiled render functions using `with` blocks, the render // proxy used needs a different `has` handler which is more performant and // also only allows a whitelist of globals to fallthrough. if (instance.render._rc) { instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers); }}// applyOptions is compatible with Vue2 options API if (__FEATURE_OPTIONS_API__) { currentInstance = instance; pauseTracking(); applyOptions(instance, Component); resetTracking(); currentInstance = null; }}Copy the code
1.2.2. setupRenderEffect
-
role
- Side effects of installing render functions
- Complete component rendering
-
The core source
const setupRenderEffect: SetupRenderEffectFn = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) = > { // create reactive effect for rendering instance.update = effect( function componentEffect() { if(! instance.isMounted) {let vnodeHook: VNodeHook | null | undefined; const { el, props } = initialVNode; const { bm, m, parent } = instance; // 1. First get the virtual DOM of the current component const subTree = (instance.subTree = renderComponentRoot(instance)); if (el && hydrateNode) { ... } else { // Initialize: perform patch recursivelypatch(...) }}else { // updateComponent patch(...) } }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions ); }; Copy the code
My understanding of side effects: If reactive data is defined, the side effects function associated with it will be re-executed if the data changes
Instance. isMounted is used to initialize rendering or update components
So we find that Effect replaces the Watcher of Vue2
1.3. Process sorting
-
The entry point to Vue3 is the createApp method, which can obtain a renderer object with one ensureRender, call the renderer.createApp method to return the app object, and extend the $mount method
-
EnsureRender guarantees that the renderer is a singleton and can be created with the call baseCreateRenderer to createRenderer
-
BaseCreateRenderer is the method that actually creates the Renderer, which includes Render, Hydrate, and createApp, where the createApp method is created by calling createAppAPI
-
CreateAppAPI createAppAPI is a factory function that returns a real createApp method that creates an instance method of **Vue (app) ** within createApp and returns it
-
If the developer calls the mount method, the mount method continues, from Render to Patch, and finally to processComponent, where the data-responsive, real-world DOM is mounted, and the initialization phase is over
1.4. Thinking and summarizing
-
A renderer is an object that has three parts
- render
- hydrate
- createApp
-
Why are global methods tuned to instances?
- Avoid contamination between instances
- tree-shaking
- semantic
-
Vue2 changes are compared during initialization
- Added the concept of renderers where all methods are provided by renderers
- Vue2 creates applications by creating objects. Vue3 eliminates the concept of objects and instead returns instances using methods that can be chain-called
- The root component is a custom component
- A custom component creates a component instance, initializes it, and installs render/update functions
2. Data response and effect
In Vue2, the data response has the following small defects:
- Additional ** API (vue.set/vue.delete) ** is required for dynamically added or deleted keys
- Array responsiveness requires a separate set of logic
- Initialization is deeply recursive and relatively inefficient
- Unable to listen for new data structures
Map
,Set
Vue3 solves this problem by refactoring the responsive principle, doubling the speed and reducing the memory footprint by half
The reconstruction content is roughly as follows:
-
Use proxy instead of Object.defineProperty
-
Data lazy observation
-
Optimize the original publish/subscribe model by removing Observer, Watcher and Dep and replacing them with reactive, Effect and targetMap
track
: Used to trace dependenciestrigger
: Used to trigger dependenciestargetMap
: equivalent to publishing subscription center toTree structureRelationships between managed objects, keys, and dependencies
2.1. Explore the process from source code
The core methods for defining Vue3 responsive data are Reactive and REF.
Because Vue3 is still compatible with Vue2, the original options API can continue to be used. After debugging, it is found that reactive is implemented in resolveData. In addition, WHEN I debug ref, I find that Reactive is also used, so it can be considered that Reactive is the data responsive entrance of Vue3.
Before looking at Reactive, let’s look at several types of enumerations for reactive data
2.1.1. ReactiveFlag
export const enum ReactiveFlags {
SKIP = '__v_skip' / * * /.// Does not need to be proxied
IS_REACTIVE = '__v_isReactive'.// The flag of a responsive object, similar to Vue2's __ob__
IS_READONLY = '__v_isReadonly'.// The flag of the read-only object, which cannot be modified
RAW = '__v_raw' / * * /.// Primitive type
}
Copy the code
Of particular concern are IS_REACTIVE and RAW
2.1.2. reactive
- role
- Create reactive objects
- The core source
function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target; } return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers); } Copy the code
In addition toread-onlyObject, and everything else is allowed to executeResponsive processing
2.1.3. createReactiveObject
-
role
- Create reactive objects without repeating them
-
The core source
function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { // 1. If it is a read-only property or proxy, return it directly if(target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target; } // 2. If the object is already responsive, it is returned directly from the cache const proxyMap = isReadonly ? readonlyMap : reactiveMap; const existingProxy = proxyMap.get(target); if (existingProxy) { return existingProxy; } // 3. Create a responsive object const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ); // 4. Store in cache proxyMap.set(target, proxy); return proxy; } Copy the code
Weakmap and Weakset do not operate when the type is not Object, Array, Map, Set.
Handler is used based on Set, Map, and normal objects
You use baseHandlers, or mutableHandlers, if you are a normal object (containing an array)
2.1.4. mutableHandlers
- role
- Define responsive interception methods
getter
Trigger dependencies collect and define child elements of the responsivitysetter
Trigger dependent update
- Define responsive interception methods
- The core source
const get = function get(target: Target, key: string | symbol, receiver: object) {
// It is already used for edge cases such as reactive objects, read-only, etc./ / 1. Array
const targetIsArray = isArray(target);
if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver);
}
/ / 2. The object
const res = Reflect.get(target, key, receiver);
// 3. Rely on tracing
track(target, TrackOpTypes.GET, key);
// 4. If it is an object, continue to observe
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 reactive(res);
}
/ / 5. Return
return res;
};
const set = function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
) :boolean {
const oldValue = (target as any)[key];
// Edge case judgment.const hadKey =
isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
// Do not trigger dependency updates if the target is something in the prototype chain
if (target === toRaw(receiver)) {
// Rely on updates
if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value); }else if(hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue); }}return result;
};
function deleteProperty(target: object, key: string | symbol) :boolean {
const hadKey = hasOwn(target, key);
const oldValue = (target as any)[key];
const result = Reflect.deleteProperty(target, key);
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
}
return result;
}
// The catcher for the in operator.
function has(target: object, key: string | symbol) :boolean {
const result = Reflect.has(target, key);
if(! isSymbol(key) || ! builtInSymbols.has(key)) { track(target, TrackOpTypes.HAS, key); }return result;
}
/ / Object. The method and Object getOwnPropertyNames. GetOwnPropertySymbols trap method.
function ownKeys(target: object) : (string | number | symbol) []{
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY);
return Reflect.ownKeys(target);
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys,
};
Copy the code
getter
- The trigger
track
implementationDepend on the collection. - Keep looking down
- The trigger
setter
- The trigger
trigger
To be responsible for theTrigger rely on.
- The trigger
There are three methods to intercept getters in a proxy: GET, HAS, and ownKeys. There are two methods to intercept setters: set and deleteProperty. Unlike Vue2, Vue3’s data listening is executed lazy-only after the getter method is called, effectively reducing the first execution time.
2.1.5. targetMap
-
define
const targetMap = new WeakMap<any, KeyToDepMap>(); Copy the code
-
role
Publish and subscribe center, is a Map structure, with a tree structure to manage each object, the key of the object, the relationship between the corresponding effect of the key is roughly like this
type targetMap = { [key: Object]: { [key: string] :Set<ReactiveEffect>; }; }; Copy the code
2.1.6. activeEffect
-
define
let activeEffect: ReactiveEffect | undefined; Copy the code
-
role
Is a global variable used to temporarily hold the executing side effect function, essentially a side effect function. A bit like dep.target for Vue2
2.1.7. track
-
role
- Collect rely on
-
The core source
export function track(target: object.type: TrackOpTypes, key: unknown) { // There are two methods in the source to pause the collection and continue the collection, here is no pause flag // The global variable activeEffect is a side effect function set in instance.update or manually by the user if(! shouldTrack || activeEffect ===undefined) { return; } // Fetch all the keys of the object from the publish subscribe center let depsMap = targetMap.get(target); if(! depsMap) { targetMap.set(target, (depsMap =new Map())); } // Remove all dependencies on the key object let dep = depsMap.get(key); if(! dep) { depsMap.set(key, (dep =new Set())); } if (!dep.has(activeEffect)) { dep.add(activeEffect); activeEffect.deps.push(dep); } } Copy the code
Collecting dependencies is also a two-way operation,
TargetMap collects the side effects function, which also needs to reference all the side effects of the current key dependency for future reuse of the dependency. Why is recollecting dependencies explained in more detail below
2.1.8. trigger
-
role
- Trigger rely on
-
The core source
export function trigger( target: object.type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown) { const depsMap = targetMap.get(target); if(! depsMap) {// never been tracked return; } const effects = new Set<ReactiveEffect>(); // Add a side effect function const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > { if (effectsToAdd) { effectsToAdd.forEach(effect= > { if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect); }}); }};// Select depsMap based on type and key, handle edge cases, and finally ADD, DELETE, and SET to ADD depsMap content to effects.// First render and asynchronous update const run = (effect: ReactiveEffect) = > { if (effect.options.scheduler) { effect.options.scheduler(effect); } else{ effect(); }};// Iterate over the side effect function and execute effects.forEach(run); } Copy the code
There was a lot of code here, but taking out the core logic makes it immediately cleaner.
The component update function is created with effect passing in a second parameter that contains scheduler, which will be used in the run method here
The core is to execute the ADD method based on the key and the type of trigger dependency (ADD, DELETE, or SET), and put the side effects of the dependency into effects for batch execution
2.1.9. Side effects
My understanding of side effects is that if reactive data is defined, the side effect function associated with it is re-executed whenever the data changes
During the debug process, we found that watchEffect and Watch were also finally called by the doWatch method, so we can think of Effect as the entry point for creating the side effect function
(1) effectStack
-
define
const effectStack: ReactiveEffect[] = []; Copy the code
-
role
This is a stack (actually an array) structure that stores multiple side effects functions, ActiveEffects, for handling effects nested scenarios (more on this later).
(2)effect
-
role
- createSide effect functionIs triggered during execution
getter
completeDepend on the collection
- createSide effect functionIs triggered during execution
-
The core source
export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> { if (isEffect(fn)) { fn = fn.raw; } const effect = createReactiveEffect(fn, options); if(! options.lazy) { effect(); }return effect; } function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> { const effect = function reactiveEffect() :unknown { if(! effect.active) {return options.scheduler ? undefined : fn(); } if(! effectStack.includes(effect)) { cleanup(effect);try { enableTracking(); effectStack.push(effect); activeEffect = effect; return fn(); } finally { effectStack.pop(); resetTracking(); activeEffect = effectStack[effectStack.length - 1]; }}}as ReactiveEffect; // Add many attributes to effect.return effect; } Copy the code
The real side effect function is created in the createReactiveEffect method. The side effect function itself is first added to the top of the effectStack, then assigned to activeEffect, followed by fn, which triggers the getter method for reactive data for dependency collection. Add activeEffect to targetMap; When the dependency is triggered when the key changes, the corresponding side effect function is extracted from targetMap and executed, which is a side effect function as a dependency collection and triggering process
You might also be wondering, activeEffects are used to temporarily store the current side effect function, I get it, but why store it in the effectStack?
A later look at the Vue3 community revealed that Effect was designed to be nested, and that the stack structure here was designed to handle nested scenarios.
The stack is characterized by first in, last out, that is, last in side functions are executed first, and then out of the stack, ensuring that the order of execution is from the outside in
React Hook is not the same as React Hook, it may be the reason WHY I use react hook too much. I can’t accept the nesting for the moment. However, as a React developer, I don’t have much say in the design
(3) Side effects why do you need to re-collect dependencies
If you’re reading the source code for the reactive principle and dependency collection, you might wonder why every time you fire the getter, you go into track, and there’s a dependency collection process.
In fact, this is an edge case where some variables in the side effect function may be read in the condition, so there is a dynamically dependent behavior.
I don’t know much about it, right? It’s a little hard to describe, so let’s just do a little bit of code,
watchEffect(() = > {
if(state.role === ROOT) { notice(state.user); }});Copy the code
State. user is read in the condition. When the condition is met, the current side effect function is a dependency of state.user. When the condition is not met, state.user needs to clear the dependency. Would it be clearer to describe it this way
2.2. Process Overview
2.2.1. Data responsiveness
- Created during initializationResponsive object, to establish
getter
,setter
Interception,getter
Responsible for collecting dependencies,setter
Responsible for triggering dependencies - Render timecallThe component levelthe
effect
Method to the componentRender functionAssigned toThe global variableactiveEffect
And perform,Render functionTrigger the correspondingkey
thegetter
Function, doneDepend on the collection - When the user triggers again
key
thesetterMethods,targetMap
Extract the correspondingDepend on the functionAnd then executetrigger
methodsTrigger rely onTo complete the update
2.2.2. effect
So far, we know that ** triggers effect** in the following ways: instance.update, watch, watchEffect, computed
- When performing the
effect
Is called firstcreateReactiveEffect
Create a realSide effect function - If it is
computed
Is waiting for responsive datagetter
The triggerSide effect functionExecute, or otherwise execute during creation, and eventually firekey
thegetter
Function, doneDepend on the collection - Considering theNesting problemsThat will beSide effect functionIn the
effectStack
In the management, each timeperformthenOut of the stackTo ensureSide effect functionOrder of executionFrom outside to inside - Also consider the edge case of dynamic dependencies, so you need to re-collect the dependencies
2.3. Thinking and summarizing
-
Vue3 has so many advantages of data responsiveness, any disadvantages?
The new data-responsive solution is efficient and can intercept 13 apis, but the disadvantage is that it is not compatible with older versions of proxy, especially IE, which is still used in 1202. Ha ha ha joke ~
-
Why Reflect?
Reflect and Proxy complement each other, as long as there are methods on the Proxy object that Reflect also owns. Using Reflect is actually a security measure to make sure you’re working on the original object
-
Why cross-reference?
Similar to Vue2, the DEP and Watcher references each other and are removed when the key is deleted.
Vue3 also takes this into account by removing the key without removing the side effect function from the key-dependent function in targetMap
-
Effect nesting problem
The reason why function components of React cannot use hooks nested is that the design concept of React is different from that of Vue. The Function components of React execute each render as a function from top to bottom and manage the state of each hook through linked lists. This leads to hook chaos if hooks are used in conditions or nesting. However, vue only updates components by triggering dependencies, and there is no rerender, so nesting is reasonable, depending on how developers get used to the idea of switching.
-
About recollecting dependencies
React has the side effect of letting developers decide which values to rely on for their function execution. Vue3 does this for us, so developers don’t have to worry about it.
3. Update data asynchronously
Remember that setupRenderEffect was executed during the component but initialization phase, assigning the update function to instance.update via effect,
instance.update = effect(
function componentEffect() {
if(! instance.isMounted) {// Initialize: perform patch recursively
patch(...)
} else {
// updateComponent
patch(...)
}
},
__DEV__ ? createDevEffectOptions(instance) : prodEffectOptions
);
Copy the code
The second argument must have a key attribute {scheduler: queueJob}.
Recalling the effect function, the execution of the side effect function is called through the run method
The core source code is as follows
const run = (effect: ReactiveEffect) = > {
if (effect.options.scheduler) {
effect.options.scheduler(effect);
} else{ effect(); }};Copy the code
You can see that when scheduler is present, asynchronous updates are performed through the scheduler, namely through the queueJob method of instance.update
3.1 explore the principle from the source code
3.1.1. queueJob
- role
- Do not repeat for
queue
Add tasks - call
queueFlush
- Do not repeat for
- The core source
export function queueJob(job: SchedulerJob) { if((! queue.length || ! queue.includes(job, isFlushing && job.allowRecurse ? flushIndex +1 : flushIndex)) && job !== currentPreFlushParentJob ) { queue.push(job); queueFlush(); } } Copy the code
3.1.2. queueFlush
-
role
- Execute asynchronous tasks without repeating them
-
The core source
function queueFlush() { if(! isFlushing && ! isFlushPending) { isFlushPending =true; currentFlushPromise = resolvedPromise.then(flushJobs); }}Copy the code
Those familiar with Vue2’s source code may find Vue3’s asynchronous tasks much simpler,
The truly asynchronous tasks become completely promises, which are browser-based microtask queues to implement asynchronous tasks
const resolvedPromise: Promise<any> = Promise.resolve(); Copy the code
3.1.3. nextTick
- role
- Execute custom methods after DOM updates are complete
- The core source
export function nextTick(this: ComponentPublicInstance | void, fn? : () = >void) :Promise<void> { const p = currentFlushPromise || resolvedPromise; return fn ? p.then(this ? fn.bind(this) : fn) : p; } Copy the code
currentFlushPromise
isPromise objectThrough thethen
The method will keep going toMicrotask queueAdd methods
3.2. Process sorting
- When the component is initialized
setupRenderEffect
Method forinstance.update
Assignment update function - When the trigger
setter
The function will executetrigger
To removeEffect the functionthroughqueueJob
perform queueJob
Add tasks toqueue
Then run the commandqueueFlush
methodsqueueFlush
Is the realAsynchronous tasks, tasks are added to the microtask queue without being repeated- After the current synchronization task is complete, the browser refreshes the microtask queue to complete asynchronous update
3.3. Thinking and summarizing
- Vue3’s asynchronous tasks are much cleaner than Vue2’s and no longer compatible with older browsers
- The true asynchronous task is the then method of the Promise object
4. patch
Before studying patch, we first need to understand the compiler optimization of Vue3, because it directly changes the structure of VNode and lays a foundation for the extremely high performance of Vue3 patching algorithm.
4.1. Optimizations brought by the compiler
-
4.1.1. Static node promotion
Divide nodes into dynamic nodes and static nodes. Static nodes have the same scope as the render function, like this.
const _hoisted_1 = /*#__PURE__*/ _createTextVNode('A text node'); const _hoisted_2 = /*#__PURE__*/ _createVNode('div'.null.'good job! ', -1 /* HOISTED */); return function render(_ctx, _cache) { with (_ctx) { return _openBlock(), _createBlock('div'.null, [_hoisted_1, _hoisted_2]); }};Copy the code
Where _hoisted_1 and _HOisted_2 are promoted static nodes that are only created during the first rendering,
These static nodes will be bypassed in subsequent updates.
-
4.1.2. Patch marking and dynamic property logging
When we use a dynamic property in template, it’s recorded, like this,
<child-comp :title="title" :foo="foo" msg="hello"/> Copy the code
Will be recorded by the render function
export function render(_ctx, _cache, $props, $setup, $data, $options) { const _component_child_comp = _resolveComponent('child-comp'); return ( _openBlock(), _createBlock( _component_child_comp, { title: _ctx.title, foo: $setup.foo, msg: 'hello',},null.8 /* PROPS */['title'.'foo'])); }Copy the code
It can be seen that the last two parameters, 8, are a type in PatchFlags, which is essentially a binary number. Combination conditions can be made by bit-by-bit operation. Here, 8 represents the props of the current component that has dynamic changes. The second parameter indicates which props are dynamically changing.
In subsequent diff props only DiffTitle and foo.
-
4.1.3. block
If there are dynamic changes under the current node, it is stored as a block.
So in Vue3, the render function you should usually see is of this form
export function render(_ctx, _cache) { return _openBlock(), _createBlock('div'.null, [_hoisted_1, _hoisted_2]); } Copy the code
_openBlock opens the block, _createBlock creates the block, and all dynamically changing children are stored in dynamicChildren.
In the future, diff children will only be diff dynamicChildren.
-
4.1.4. Caching event handlers
If an inline function is used, it will be cached in _cache and used directly from _cache in the next update. The function will not be created repeatedly to avoid unnecessary rendering due to different references
Like this,
<child-comp @click="toggle(index)"/> Copy the code
Will be compiled into
export function render(_ctx, _cache, $props, $setup, $data, $options) { const _component_child_comp = _resolveComponent('child-comp'); return ( _openBlock(), _createBlock(_component_child_comp, { onClick: _cache[1] || (_cache[1] = $event => _ctx.toggle(_ctx.index)), }) ); } Copy the code
For inline functions, if the component is re-rendered, different references to the two functions may lead to repeated updates. This is common in React, which we optimized with useCallback. But Vue3 does that for us.
Now, what does patching do
4.2. patch
-
role
- Entry to component rendering and updates
- By calling the
processXXX
Execute render/update functions of the corresponding type
-
The core source
const patch: PatchFn = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false ) = > { // If patchFlag is included, optimization is enabled if (n2.patchFlag === PatchFlags.BAIL) { optimized = false n2.dynamicChildren = null } const { type, ref, shapeFlag } = n2 // Determine which patching algorithm to use based on the VNode type switch (type) { case Text: processText(n1, n2, container, anchor) break case Comment: processCommentNode(n1, n2, container, anchor) break case Static: if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } else if (__DEV__) { patchStaticNode(n1, n2, container, isSVG) } break case Fragment: processFragment(...) break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement(...) } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent(...) } else if(shapeFlag & ShapeFlags.TELEPORT) { ; (typeas typeof TeleportImpl).process(...) } else if(__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ; (typeas typeof SuspenseImpl).process(...) } else if (__DEV__) { warn('Invalid VNode type:', type, ` (The ${typeof type}) `)}}// set ref if(ref ! =null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2) } } Copy the code
Except that Text, Comment, Static, and Fragment are processed by Type, shapeFlag determines which patching algorithm to use in other cases.
These algorithms are basically similar, and processElement is selected for analysis in this paper
4.3. processElement
-
role
- Component first render: call
mountElement
- Component update: call
patchElement
- Component first render: call
-
The core source
const processElement = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) = > { isSVG = isSVG || (n2.type as string) === 'svg' if (n1 == null) { mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else { patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) } } Copy the code
N1 represents the old virtual DOM and n2 represents the new virtual DOM. At present, we analyze patching algorithm, so we need to look at patchElement method.
4.4. patchElement
-
role
- right
Element
The type ofVNodeforpatch
- right
-
The core source
const patchElement = ( n1: VNode, n2: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) = > { const el = (n2.el = n1.el!) let { patchFlag, dynamicChildren, dirs } = n2 // #1426 take the old vnode's patch flag into account since user may clone a // compiler-generated vnode, which de-opts to FULL_PROPS patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS const oldProps = n1.props || EMPTY_OBJ const newProps = n2.props || EMPTY_OBJ let vnodeHook: VNodeHook | undefined | null // 1. diff props if (patchFlag > 0) { if (patchFlag & PatchFlags.FULL_PROPS) { // Dynamic key, full diff required.// Finally call patchProps } else { // class is the case of dynamic attributes.// Finally call hostPatchProp // style is the case with dynamic attributes.// Finally call hostPatchProp // Handle dynamic properties in dynamicProps.// Loop to hostPatchProp } / / dynamic text.// Finally call hostSetElementText } else if(! optimized && dynamicChildren ==null) { // Full diff, i.e., no optimization.// Finally call patchProps } // 2. diff children if (dynamicChildren) { // Dynamic child nodepatchBlockChildren( n1.dynamicChildren! , dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG ) }else if(! optimized) {// Full diff, i.e., no optimization patchChildren( n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG ) } } Copy the code
It’s a little bit too much code, because it also includes the case without optimization, which is basically the same as Vue2,
With patchFlag, targeted update can be realized through hostPatchProp.
With the help of dynamicChildren, on-demand diff child nodes can be realized through patchBlockChildren, and full diff can be achieved without patchChildren.
HostPatchProp is very simple and just updated according to the parameters passed in. We focus on patchBlockChildren and patchChildren
4.5. patchBlockChildren
-
role
- To deal with
block
The level ofchildren
- To deal with
-
The core source
const patchBlockChildren: PatchBlockChildrenFn = (oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG) = > { for (let i = 0; i < newChildren.length; i++) { const oldVNode = oldChildren[i] const newVNode = newChildren[i] // Determine the container (parent element) for the patch. const container = // - In the case of a Fragment, we need to provide the actual parent // of the Fragment itself so it can move its children. oldVNode.type === Fragment || // - In the case of different nodes, there is going to be a replacement // which also requires the correct parent container! isSameVNodeType(oldVNode, newVNode) ||// - In the case of a component, it could contain anything.oldVNode.shapeFlag & ShapeFlags.COMPONENT || oldVNode.shapeFlag & ShapeFlags.TELEPORT ? hostParentNode(oldVNode.el!) ! :// In other cases, the parent container is not actually used so we // just pass the block element here to avoid a DOM parentNode call. fallbackContainer patch( oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, true)}}Copy the code
Traversal newChildren, namely dynamicChildren, diff the old and new vnodes at the same level through patch, and so on, continuously reduce the level of children, and call patch.
When invoking patch at a certain level, there will be no optimization option, and the new and old children will be processed eventually
4.6. patchChildren
-
role
- Patching algorithm for selecting child nodes
-
The core source
const patchChildren: PatchChildrenFn = ( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false ) = > { const c1 = n1 && n1.children const prevShapeFlag = n1 ? n1.shapeFlag : 0 const c2 = n2.children const { patchFlag, shapeFlag } = n2 // fast path if (patchFlag > 0) { if (patchFlag & PatchFlags.KEYED_FRAGMENT) { // Children with key patchKeyedChildren(...) return } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) { // Children without key patchUnkeyedChildren(...) return}}// children has 3 possibilities: text, array or no children. // Children has three possibilities: text, array, or no children. }Copy the code
Patchflags. KEYED_FRAGMENT and patchflags. UNKEYED_FRAGMENT are the basis of whether children contain keys or not. Select patchKeyedChildren or patchUnkeyedChildren based on whether the key is included.
Among them, patchKeyedChildren is a treatment method for children with key.
4.7. patchKeyedChildren
-
role
- diff 带
key
The child nodes of the
- diff 带
-
The core source
const patchKeyedChildren = ( c1: VNode[], c2: VNodeArrayChildren, container: RendererElement, parentAnchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) = > { let i = 0 const l2 = c2.length let e1 = c1.length - 1 // prev ending index let e2 = l2 - 1 // next ending index / / 1. The device // (a b) c // (a b) d e while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (isSameVNodeType(n1, n2)) { patch(...) } else { break } i++ } / / (2) to the tail // a (b c) // d e (b c) while (i <= e1 && i <= e2) { const n1 = c1[e1] const n2 = (c2[e2] = optimized ? cloneIfMounted(c2[e2] as VNode) : normalizeVNode(c2[e2])) if (isSameVNodeType(n1, n2)) { patch(...) } else { break } e1-- e2-- } // 3. If the new node has surplus, the new node is added, and the old node is deleted // (a b) // (a b) c // i = 2, e1 = 1, e2 = 2 // (a b) // c (a b) // i = 0, e1 = -1, e2 = 0 if (i > e1) { if (i <= e2) { const nextPos = e2 + 1 const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor while (i <= e2) { patch(...) i++ } } } // 4. If the old node is available, delete it // (a b) c // (a b) // i = 2, e1 = 2, e2 = 1 // a (b c) // (b c) // i = 0, e1 = 0, e2 = -1 else if (i > e2) { while (i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true) i++ } } // 5. Diff if there are different nodes in the middle // [i ... e1 + 1]: a b [c d e] f g // [i ... e2 + 1]: a b [e d c h] f g // i = 2, e1 = 4, e2 = 5 else{... }}Copy the code
The process is divided into 5 steps in the source code, but can be optimized extraction, divided into three steps
- The first step is to find all the same beginnings at once, a process called pinching
- Then find all the same endings at once, a process known as tail removal
- Break off both endsAfter processing, the process is calledWrap up
- 3.1. One of the old and new nodes is empty
- 3.1. If the new array has a surplus, add it.
- 3.2. Delete the old array if there is any surplus
- 3.2. Both old and new nodes have surplus
- Diff two old and new nodes and two remaining children
- 3.1. One of the old and new nodes is empty
An example might be more graphic
-
Add child nodes
const oldChildren = [a, b, c, d]; const newChildren = [a, e, f, b, c, d]; / / 1. The device const oldChildren = [b, c, d]; const newChildren = [e, f, b, c, d]; / / (2) to the tail const oldChildren = []; const newChildren = [e, f]; // 3. Add a batch of nodes when there is an empty node [e, f].Copy the code
-
Changes the state of the partial molecular node
const oldChildren = [a, g, h, b, c, d]; const newChildren = [a, e, f, b, c, d]; / / 1. The device const oldChildren = [g, h, b, c, d]; const newChildren = [e, f, b, c, d]; / / (2) to the tail const oldChildren = [g, h]; const newChildren = [e, f]; // 3. Diff the two children [g, h] and [e, f] const oldChildren = [g, h]; const newChildren = [e, f]; Copy the code
5. Think, summarize and supplement
-
Vue3 makes extensive use of the factory model
Methods such as createAppAPI and createRenderer return a real function via factory mode. These methods are generally included in the source core package. The purpose here is to make it better cross-platform. Can better cross-platform development, to avoid the development of Weex to directly modify the source code of this situation.
-
The concept and application of renderer
The renderer is an object that is a core concept in the Vue3 source code and contains three methods: Render, Hydrate, and createApp.
If we want to do custom rendering we can do it by creating custom renderers.
-
Compiler dependence
-
What does the compiler do?
Parse, transform, and generate convert template to render function
-
At what stage does compilation take place?
- In a Webpack environment, the compilation is done using vue-Loader in a development environment. Only vue Runtime files are retained in the production environment
- If it’s with youcompilertheruntimeVersion, the component initializes execution
mount
Method is finally calledsetupComponent
compile
-
Optimizations made during compilation
- Block Tree
- PatchFlags
- Static PROPS
- Cache Event handler
- prestringing
- How to implement targeted update
-
-
An operation
Bitwise operations are performed at the base of the number (the 32 digits that represent the number). Since bitwise operations are low-level operations, they tend to be the fastest.
Combination conditions can be quickly handled, such as scenarios with multiple roles and overlapping permissions in a project that can be solved using bitwise operations.
-
diff
Vue3’s real diff algorithm doesn’t change much and can be divided into three steps
- Pinched head,
- Go to the tail,
- Wrap up
- Batch add/delete
- diff
What really makes Vue3 diff fly is that the compiler makes a lot of optimizations.
-
About data responsiveness
Not only the object.defineProperty to proxy change, but also the lazy observation of child data.
-
Composition – API and hook
Much like React hooks, Vue3 hooks don’t have the mental burden of being different between old and new values, and side functions can be nested, but there is some mental burden of whether or not to deconstruct them.
Composition-api can reuse logic better than Vue2’s Options API.