preface
Speaking of the marathon, we all know that the marathon is the longest track and field event in the world (42.195 km). It is the most physically demanding event in all sports and also the most mentally challenging event. If you can run a full marathon, what can you not do?
Similarly, studying the source code of some of the best libraries can be a tedious process, like participating in a source-level marathon. Today, I will take you to run a marathon on the source parsing version of vue.js 3.0 rendering system. During the whole process, I will also provide you with a supply station (flow chart) for you to read.
thinking
Before starting today’s article, you can think about:
vue
How is the file converted toDOM
Nodes and render to the browser?- When data is updated, what is the whole update process?
🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️ 🤔 ️
Vuejs has two phases: compile time and run time.
Compile time
The. Vue file we wrote during normal development cannot be directly run in the browser, so in the webpack compilation stage, we need to compile the. Vue file through vue-loader to generate the corresponding JS code, and the template template corresponding to the Vue component will be converted into the render function by the compiler.
The runtime
Then, when the compiled code actually runs in the browser, the render function executes and returns the VNode, known as the virtual DOM, and finally renders the VNode as a real DOM node.
After understanding the idea of VUE component rendering, let’s start from vue.js 3.0 (hereafter referred to as VUe3) source code, in-depth understanding of the entire vue component rendering process is what?
To prepare
This article is mainly to analyze vue3 rendering system, in order to facilitate debugging, we directly through the introduction of vue.js file source debugging analysis.
vue3
Download the source code
# secure shell (SSH)
https://github.com/vuejs/vue-next
Or download the version I used to take notes
https://github.com/AsyncGuo/vue-next/tree/vue3_notes
Copy the code
- generate
vue.global.js
file
npm run dev
# bundles ... / vue - next/packages/vue/SRC/index. The ts - > packages/vue/dist/vue. Global. Js...
# created packages/vue/dist/vue. Global. Js in 2.8 s
Copy the code
- Start the development environment
npm run serve
Copy the code
- The test code
<! - to debug the code directory: / packages/vue/examples/test. The HTML -- -- >
<script src=". /.. /dist/vue.global.js"></script>
<div id="app">
<div>static node</div>
<div>{{title}}</div>
<button @click="add">click</button>
<Item :msg="title"/>
</div>
<script>
const Item = {
props: ['msg'].template: `<div>{{ msg }}</div>`
}
const app = Vue.createApp({
components: {
Item
},
setup() {
return {
title: Vue.ref(0)}},methods: {
add() {
this.title += 1
}
},
})
app.mount('#app')
</script>
Copy the code
Create an
From the above test code, we can see that vue3 and vue2 are mounted differently. Vue3 is created using the createApp entry function. Let’s look at the implementation of createApp:
// Import file: / uE-next /packages/runtime-dom/ SRC /index.ts
const createApp = ((. args) = > {
console.log('createApp into arguments:. args);// Create an application
constapp = ensureRenderer().createApp(... args);const { mount } = app;
/ / rewrite the mount
app.mount = (containerOrSelector) = > {
// ...
};
return app;
});
Copy the code
ensureRenderer
EnsureRenderer first create a Web-side renderer with one ensureRenderer. Let’s see the implementation:
// Methods to update attributes
const patchProp = () = > {
// ...
}
// The DOM manipulation method
const nodeOps = {
insert: (child, parent, anchor) = > {
parent.insertBefore(child, anchor || null)},remove: child= > {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
...
}
// Set the parameters required by the Web renderer
const rendererOptions = extend({ patchProp }, nodeOps);
let renderer;
// Delay renderer creation
function ensureRenderer() {
return (renderer || (renderer = createRenderer(rendererOptions)));
}
Copy the code
As you can see here, by creating the renderer lazily, we can greatly reduce the size of the package by tree-shaking the renderer-related code when we rely only on responsive packages.
createRenderer
EnsureRenderer indicates that the true entry is the createRenderer method:
// /vue-next/packages/runtime-core/src/renderer.ts
export function createRenderer(options) {
return baseCreateRenderer(options)
}
function baseCreateRenderer(options, createHydrationFns) {
// Common DOM manipulation methods
const {
insert: hostInsert,
remove: hostRemove,
...
} = options
/ / = = = = = = = = = = = = = = = = = = = = = = =
// The core process of rendering
// Encapsulate functions by caching closures
/ / = = = = = = = = = = = = = = = = = = = = = = =
const patch = () = > {} // Core diff process
const processElement = () = > {} / / processing element
const mountElement = () = > {} / / mount element
const mountChildren = () = > {} // Mount the child node
const processFragment = () = > {} // Process the fragment node
const processComponent = () = > {} // Process components
const mountComponent = () = > {} // Mount the component
const setupRenderEffect = () = > {} // Run the render function with side effects
const render = () = > {} // Render the mount process
// ...
/ / = = = = = = = = = = = = = = = = = = = = = = =
// 🐶2000+ line convergent function 🐶
/ / = = = = = = = = = = = = = = = = = = = = = = =
return {
render,
hydrate, // Server render related
createApp: createAppAPI(render, hydrate)
}
}
Copy the code
Let’s skip the implementation of these introversion functions and look at the implementation of createAppAPI:
createAppAPI
function createAppAPI(render, hydrate) {
// Actually create the app entry
return function createApp(rootComponent, rootProps = null) {
// Create vUE application context
const context = createAppContext();
// The vue plug-in is installed
const installedPlugins = new Set(a);let isMounted = false;
const app = (context.app = {
_uid: uid++,
_component: rootComponent, / / the root component
use(plugin, ... options) {
// ...
return app
},
mixin(mixin) {},
component(name, component) {},
directive(name, directive) {},
mount(rootContainer) {},
unmount() {},
provide(key, value){}});return app;
};
}
Copy the code
As you can see, the createApp function returned by createAppAPI is the actual entry point for creating the application. CreateApp creates the vUE app context, initializes the app, binds the app context to the app instance, and returns the app.
One important point to note here is that the use, mixin, Component, and directive methods on the app object all return the app instance and can be invoked in chain by the developer.
// always use 🐶🐶🐶
createApp(App).use(Router).use(Vuex).component('component',{}).mount("#app")
Copy the code
To this app application instance has been created ~, print to view the createdapp
Application: So to summarize creatingapp
Application example process:
- create
Web side
Corresponding renderer (Lazy creation, tree-shaking) - perform
baseCreateRenderer
Methods (The main flow of the subsequent mount phase is through the closure cache internalization function) - perform
createAppAPI
Methods (1. Create an application context. 2. Create app and return)
Mount the stage
Next, when we execute app.mount, we start to mount the components. The app.mount method we call is the rewritten mount method:
const createApp = ((. args) = > {
// ...
const { mount } = app; // Cache the original mount method
/ / rewrite the mount
app.mount = (containerOrSelector) = > {
// Get the container
const container = normalizeContainer(containerOrSelector);
if(! container)return;
const component = app._component;
// Determine if the root component passed in is not a function & the root component has no render function & no template, then set the contents of the container to the root component's template
if(! isFunction(component) && ! component.render && ! component.template) { component.template = container.innerHTML; }// Empty the contents of the container
container.innerHTML = ' ';
// Mount the cache
const proxy = mount(container, false, container);
return proxy;
};
return app;
});
Copy the code
Mount (createAppAPI); mount (createAppAPI); mount (createAppAPI);
function createAppAPI(render, hydrate) {
// Actually create the app entry
return function createApp(rootComponent, rootProps = null) {
// ...
const app = (context.app = {
// Mount the root component
mount(rootContainer, isHydrate, isSVG) {
if(! isMounted) {// Create a vNode for the root component
const vnode = createVNode(rootComponent, rootProps);
// The root vNode has application context
vnode.appContext = context;
// Render the virtual vNode as a real node and mount it
render(vnode, rootContainer, isSVG);
isMounted = true;
// Record the root component container of the application
app._container = rootContainer;
rootContainer.__vue_app__ = app;
app._instance = vnode.component;
returnvnode.component.proxy; }}});return app;
};
}
Copy the code
To summarize, what does the mount method mainly do?
- Create the corresponding of the root component
vnode
The root component vnode
The bindingApplication Context
- Apply colours to a drawing
vnode
Create a real node and mount it - Record the mount status
As you may have noticed, the mount method here is a standard cross-platform rendering process that abstracts a VNode and then implements platform-specific rendering via rootContainer, for example, a DOM object in a browser environment and other platform-specific values in other platforms. This is why we overrode the mount method when calling the creataApp method of the Runtime-DOM package to improve the rendering logic for different platforms.
Create a vnode
When it comes to VNode, most people tend to associate it with high performance and mistakenly assume that vNode performance is necessarily higher than manual DOM manipulation, which is not the case. The bottom layer of VNode also needs to operate DOM. On the contrary, if the patch process of VNode is too long, the page will also lag. Vnode is the abstraction of the native DOM, which will play a certain abstraction in the processing of cross-platform design. For example: server side rendering, small program side rendering, WEEX platform…
Next, let’s look at the process of creating a VNode:
function _createVNode(type, props, children, patchFlag, ...) :VNode {
// normalize class & style
// For example, class=[], class={}, and style=[] must be normalized
if (props) {
// ...
}
// Get the vNode type
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0;
return createBaseVNode()
}
Copy the code
function createBaseVNode(
type,
props = null,
children = null.) {
// The default structure of a vNode
const vnode = {
__v_isVNode: true.// Whether it is a Vnode
__v_skip: true.// Skip reactive datafication
type, // The first argument to create a vnode
props, / / DOM parameters
children,
component: null.// Component instance (instance), created by createComponentInstance
shapeFlag, // Type flag. In the patch phase, the corresponding rendering process is carried out by matching shapeFlag. };// Standard child node
if (needFullChildrenNormalization) {
normalizeChildren(vnode, children);
}
// Collect dynamic child nodes or child blocks to the parent block tree
if (isBlockTreeEnabled > 0 &&
!isBlockNode &&
currentBlock &&
(vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) && vnode.patchFlag ! = =32 /* HYDRATE_EVENTS */) {
currentBlock.push(vnode);
}
return vnode;
}
Copy the code
From the above code, we can summarize what is done in the vNode creation stage:
- Normalize class & style (e.g., class=[], class={}, style=[], etc.)
- tag
vnode
The type ofshapeFlag
, corresponding to the root componentvnode
Type (typeIs the root componentrootComponent
, the root component is in object format, soshapeFlag
The 4) - Normalized child node (children are empty when initialized)
- Collect dynamic child nodes or children
block
To the parentblock tree
(Here it isvue3
New concepts introduced:block tree
, space is limited, this article will not expand the statement.)
Here, we can print to see what the root component corresponds to at this pointvnode
Structure:
Rendering vnode
CreateVNode obtains the vNode of the root component and executes the render function, which is the render function cached by the closure of baseCreateRenderer:
// The actual render method called is the render method cached in the baseCreateRenderer method
function baseCreateRenderer() {
const render = (vnode, container) = > {
if (vnode == null) {
if (container._vnode) {
// Uninstall the component
unmount()
}
} else {
// Mount properly
patch(container._vnode || null, vnode, container)
}
}
}
Copy the code
- When the incoming
vnode
fornull
& there are old onesvnode
, doUninstall the component - Otherwise, mount it normally
- After mounting, execute component life cycles in batches
- Bind vNodes to the container so that subsequent update phases pass through the old and new
vnode
forpatch
⚠️ : Next, the entire rendering process will be performed in the inner convergent function of the core function baseCreateRenderer ~
patch
Next, let’s look at the implementation of patch function in the render process:
const patch = (
n1, / / the old vnode
n2, / / new vnode
container, // Mount the container.) = > {
// ...
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
// Process text
processText(n1, n2, container, anchor)
break
case Comment:
// Comment the node
processCommentNode(n1, n2, container, anchor)
break
case Static:
// Static node
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
}
break
case Fragment:
/ / fragments node
processFragment(n1, n2, container, ...)
break
default:
if (shapeFlag & 1 /* ELEMENT */) { // Handle DOM elementsprocessElement(n1, n2, container, ...) ; }else if (shapeFlag & 6 /* COMPONENT */) { // Process componentsprocessComponent(n1, n2, container, ...) ; }else if (shapeFlag & 64 /* TELEPORT */) { type.process(n1, n2, container, ...) ; }else if (shapeFlag & 128 /* SUSPENSE */) { type.process(n1, n2, container, ...) ; }}}Copy the code
By analyzing the patch function, we can find that the patch function will go through different processing logic by judging the difference between type and shapeFlag. Today, we mainly analyze the processing of component types and ordinary DOM elements.
processComponent
Initialize render with type object and shapeFlag as 4(bit operations 4 & 6).
const processComponent = (n1, n2, container, ...) = > {
if (n1 == null) {
if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
// Activate components (cached components)parentComponent.ctx.activate(n2, container, ...) ; }else {
// Mount the component
mountComponent(n2, container, ...);
}
}
else {
// Update the componentupdateComponent(n1, n2, optimized); }};Copy the code
If n1 is null, the component is mounted. Otherwise update the component.
mountComponent
Let’s move on to the implementation of the mountComponent function that mounts the component:
const mountComponent = (initialVNode, container, ...) = > {
// 1. Create a component instance
const instance = (
// The component instance is mounted to the Component property of the component vNode
initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense)
);
// 2. Set the component instance
setupComponent(instance);
// 3. Set up and run the render function with side effectssetupRenderEffect(instance, initialVNode, container,...) ; };Copy the code
After omitting the code irrelevant to the main flow, you can see that the mountComponent function does three things:
- Creating a component instance
function createComponentInstance(vnode, parent, suspense) {
const type = vnode.type;
// Bind the application context
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
// The default value for the component instance
const instance = {
uid: uid$1+ +,// Unique id of the component
vnode, // VNode of the current component
type, // VNode node type
parent, // Instance of the parent component
appContext, // Application context
root: null./ / root instance
next: null.// If the component mounted is null, it is set to instance.vnode. The next update will run updateComponentPreRender
subTree: null.// The component's render vNode, generated by the component's render function, is created and synchronized
update: null.// The component content is mounted or updated to the view's execution callback and synchronized after creation
scope: new EffectScope(true /* detached */),
render: null.// The component's render function, assigned in the setupStatefulComponent phase
proxy: null.// is a proxy CTX field, which is referred to when this is used internally
// local resovled assets
// resolved props and emits options
// emit
// props default value
// inheritAttrs
// state
// suspense related
// lifecycle hooks
};
{
instance.ctx = createDevRenderContext(instance);
}
instance.root = parent ? parent.root : instance;
instance.emit = emit.bind(null, instance);
return instance;
}
Copy the code
The createComponentInstance function initializes the instance of the component and returns it, printing out the instance content of the root component:
- Setting up component instances
function setupComponent(instance, isSSR = false) {
const { props, children } = instance.vnode;
// Determine whether it is a state component
const isStateful = isStatefulComponent(instance);
// Initialize component attributes, slots
initProps(instance, props, isStateful, isSSR);
initSlots(instance, children);
// Mount setup information when state component
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
return setupResult;
}
Copy the code
The logic of setupComponent is very simple. First, initialize the props and slots components and mount them to the component instance. Then, based on the component type vnode.shapeFlag===4, Determine whether to mount setup information (that is, vue3’s Composition API).
function setupStatefulComponent(instance, isSSR) {
const Component = instance.type;
// Create the attribute access cache for the render context
instance.accessCache = Object.create(null);
// Create the render context proxy
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
const { setup } = Component;
// Determine whether the component has setup
if (setup) {
// Determine if setup has parameters, and if so, create a setup context and mount the component instance
// For example: setup(props) => {}
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null);
// Execute the setup function
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [shallowReadonly(instance.props) , setupContext]);
handleSetupResult(instance, setupResult, isSSR);
}
else{ finishComponentSetup(instance, isSSR); }}Copy the code
Check whether the component has setup functions:
- If you set up
setup
Function, then executessetup
Function and determine the type of its return value. Set the component instance if the return value type is functionrender
The value ofsetupResult
Otherwise, as a component instancesetupState
The value of the
function handleSetupResult(instance, setupResult, isSSR) {
// Determine the setup return value type
if (isFunction(setupResult)) {
// If it returns a function, it is the render method of the component instance
instance.render = setupResult;
}
else if (isObject(setupResult)) {
// If the return value is an object, it is treated as the setupState of the component instance
instance.setupState = proxyRefs(setupResult);
}
else if(setupResult ! = =undefined) {
warn$1(`setup() should return an object. Received: ${setupResult === null ? 'null' : typeof setupResult}`);
}
finishComponentSetup(instance, isSSR);
}
Copy the code
- Of the component instance
render
Methods, analysisfinishComponentSetup
The function,render
Functions can be set in three ways: - if
setup
If the return value is a function type, theninstance.render = setupResult
- If the component exists
render
Methods,instance.render = component.render
- If the component exists
template
The template,The instance. The render = the compile (template)
Component instance render optimization, the instance. The render = the setup () | | component. The render | | the compile (template)
function finishComponentSetup(instance, ...) {
const Component = instance.type;
// Bind the render method to the component instance
if(! instance.render) {if(compile && ! Component.render) {const template = Component.template;
if (template) {
// Generate the render function by compiling the template with the compilerComponent.render = compile(template, ...) ; } } instance.render = (Component.render || NOOP); }// support for 2.x options. }Copy the code
Once the components are set up, we can look at them againinstance
What has happened to the content of:
Data, Proxy, Render, and setupState of the component instance are already bound to their initial values.
- Set up and run the render function with side effects
const setupRenderEffect = (instance, initialVNode, container, ...) = > {
// Create a reactive side effect function
const componentUpdateFn = () = > {
// First render
if(! instance.isMounted) {// Render component generates subtree vNodes
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container, ...) ; initialVNode.el = subTree.el; instance.isMounted =true;
}
else {
/ / update}};// create render effcet
const effect = new ReactiveEffect(
componentUpdateFn,
() = > queueJob(instance.update),
instance.scope // track it in component's effect scope
);
const update = (instance.update = effect.run.bind(effect));
update.id = instance.uid;
update();
};
Copy the code
We continue with the setupRenderEffect function, which first creates a render effect. (Responsive systems also have other side effects: Computed Effect, Watch Effect), bind the side-effect execution function to the update property of the component instance (the update process triggers the update function again), and execute the update function immediately to trigger the first update.
function renderComponentRoot(instance) {
const { proxy, withProxy, render, ... } = instance;
let result;
try {
const proxyToUse = withProxy || proxy;
// Execute the render method of the instance, return vNode, and then normalize vNode
/ /, when performing a render method will be called proxyToUse, namely will trigger PublicInstanceProxyHandlers getresult = normalizeVNode(render.call(proxyToUse, proxyToUse, ...) ); }return result;
}
Copy the code
At this point, the renderComponentRoot function executes the instance’s Render method, the function tied to the instance’s Render method in the setupComponent phase, and normalizes the VNodes returned by Render and returns them as subtree VNodes.
Also we can print to view a treevnode
Content:
At this point, some of you may be wondering why there are two VNode trees. What’s the difference between these two VNode trees?
-
initialVNode
InitialVNode is a component vNode that describes the entire component object. A component VNode defines properties associated with the component: data, props, lifecycle, and so on. Subtree VNodes are generated by rendering component VNodes.
-
sub tree
The subtree vNodes are generated by the render method of the component vNode, which is essentially a description of the component template, the DOM VNode that is actually rendered to the browser.
After the subTree is generated, mount the subTree node to the Container using the patch method. Next, we continue to analyze, you can see the screenshot of subTree above: subTree type value is Fragment, recall the implementation of patch method:
const patch = (
n1, / / the old vnode
n2, / / new vnode
container, // Mount the container.) = > {
const { type, ref, shapeFlag } = n2
switch (type) {
case Fragment:
/ / fragments node
processFragment(n1, n2, container, ...)
break
default:
// ...}}Copy the code
Fragment is one of the new features mentioned in VUe3. In VUe2, multi-node components are not supported, whereas vue3 is officially supported. If you think about it, it’s a single root component, but vue3 has a Fragment wrapped around the bottom layer. Let’s look at the implementation of processFragment:
const processFragment = (n1, n2, container, ...) = > {
// Create text nodes at the beginning and end of the fragment
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(' '));
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(' '));
if (n1 == null) {
hostInsert(fragmentStartAnchor, container, anchor);
hostInsert(fragmentEndAnchor, container, anchor);
// Mount an array of child nodesmountChildren(n2.children, container, ...) ; }else {
/ / update}};Copy the code
Next mount the child node array:
const mountChildren = (children, container, ...) = > {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i])
: normalizeVNode(children[i]));
patch(null, child, container, ...);
}
};
Copy the code
Traverse the child nodes, patch each child node, and perform recursive processing according to the type of child node. Next, we’ll focus on DOM elements of type ELEMENT, called processElement:
const processElement = (n1, n2, container, ...) = > {
if (n1 == null) {
// Mount the DOM element
mountElement(n2, container,...)
} else {
/ / update}}Copy the code
const mountElement = (vnode, container, ...) = > {
let el;
let vnodeHook;
const { type, props, shapeFlag, ... } = vnode;
{
// Create a DOM node and bind it to the EL of the current vnodeel = vnode.el = hostCreateElement(vnode.type, ...) ; }// Insert the parent node
hostInsert(el, container, anchor);
};
Copy the code
Create a DOM node and mount it to vnode.el. Then mount the DOM node to a container, continue the recursive processing of other VNodes, and finally mount the entire VNode to the browser view. The mountElement method mentioned hostCreateElement and hostInsert, which are the corresponding processing methods of the parameters passed in the initial creation of the renderer, thus completing the entire cross-platform initial rendering process.
Update process
After analyzing the entire process of vue3’s first rendering, how does vuE3 update the rendering after the data update? The next stage of analyzing the update process will involve the knowledge of vuE3’s responsive system (due to the limited space, we will not expand more responsive knowledge, you can continue to follow our official account, looking forward to more detailed analysis in the subsequent chapters).
Depend on the collection
Recall that the setupComponent phase creates the render context proxy for the first rendering instance, and the Render Component method for the vNode is executed in the Render subTree phase, using renderComponentRoot. At the same time, will trigger a rendering context agent PublicInstanceProxyHandlers get, so as to realize rely on collection.
function setupStatefulComponent(instance, isSSR) {...// Create the render context proxy
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
}
Copy the code
function renderComponentRoot(instance) {
const proxyToUse = withProxy || proxy;
/ /, when performing a render method will be called proxyToUse, namely will trigger PublicInstanceProxyHandlers get
result = normalizeVNode(
render.call(proxyToUse, proxyToUse, ...)
);
return result;
}
Copy the code
We can look at the contents of the render method of this component vNode:
Or print to view the render method content:
(function anonymous(
) {
const _Vue = Vue
const { createVNode: _createVNode, createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div".null."static node", -1 /* HOISTED */)
const _hoisted_2 = ["onClick"]
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
const _component_item = _resolveComponent("item")
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1,
_createElementVNode("div".null, _toDisplayString(title), 1 /* TEXT */),
_createElementVNode("button", { onClick: add }, "click".8 /* PROPS */, _hoisted_2),
_createVNode(_component_item, { msg: title }, null.8 /* PROPS */["msg"]]),64 /* STABLE_FRAGMENT */}}})))Copy the code
Look carefully render the first parameter to the _ctx, namely the incoming rendering context proxy agent, when access to the title field, will trigger a PublicInstanceProxyHandlers the get method, The PublicInstanceProxyHandlers logic and how?
// The handler implementation of the proxy render context
const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const { ctx, setupState, data, props, accessCache, type, appContext } = instance;
let normalizedProps;
// The key value does not start with $
if (key[0]! = ='$') {
Where does the current attribute need to be fetched from the cache first
// Performance optimization: Cache attributes should be fetched according to which type to avoid the overhead of triggering hasOwn every time
const n = accessCache[key];
if(n ! = =undefined) {
switch (n) {
case 0 /* SETUP */:
return setupState[key];
case 1 /* DATA */:
return data[key];
case 3 /* CONTEXT */:
return ctx[key];
case 2 /* PROPS */:
return props[key];
// default: just fallthrough}}SetupState => data => props => CTX => Failed to obtain the value
else if(setupState ! == EMPTY_OBJ && hasOwn(setupState, key)) { accessCache[key] =0 /* SETUP */;
return setupState[key];
}
else if(data ! == EMPTY_OBJ && hasOwn(data, key)) { accessCache[key] =1 /* DATA */;
return data[key];
}
else if (
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)) {
accessCache[key] = 2 /* PROPS */;
return props[key];
}
else if(ctx ! == EMPTY_OBJ && hasOwn(ctx, key)) { accessCache[key] =3 /* CONTEXT */;
return ctx[key];
}
else if (shouldCacheAccess) {
accessCache[key] = 4 /* OTHER */; }}},set() {},
has(){}};Copy the code
Let’s use the key as the title example to briefly introduce the logic of get:
-
First check whether the key value starts with $, obviously title goes no logic
-
See if the accessCache cache exists
Performance optimization: Which type should cache attributes be fetched according to, avoiding the overhead of triggering **hasOwn** every time
-
Finally, according to the order for: setupState = > data = > props = > CTX PublicInstanceProxyHandlers set and from the processing logic, the same to the order processing
-
If yes, set accessCache first and then obtain the value of title from setupState
When setupstate. title is accessed, there are two stages in the process that trigger the proxy get:
- First of all, the trigger
setupState
The correspondingproxy
theget
And then gettitle
To determine whether it isRef
? - If yes, continue obtaining
ref.value
That triggerref
Type of dependency collection process - If no, the value is a common data type and no dependent collection is performed
// The proxy prxoy for setupState is set when the component instance is set
SetupComponent =>setupStatefulComponent=>handleSetupResult
instance.setupState = proxyRefs(setupResult)
export function proxyRefs(objectWithRefs) {
return isReactive(objectWithRefs)
? objectWithRefs
: new Proxy(objectWithRefs, {
get: (target, key, receiver) = > {
return unref(Reflect.get(target, key, receiver))
},
set: (target, key, value, receiver) = >{}})}export function unref(ref) {
return isRef(ref) ? ref.value : ref
}
Copy the code
- access
ref.value
When the triggerref
Dependency collection. So let’s analyze it firstVue.ref()
What is the implementation logic of?
// Call vue.ref (0) to trigger the createRef process
// omit other extraneous code
function ref(value) {
return createRef(value, false)}function createRef(rawValue) {
return new RefImpl(rawValue, false)}// the implementation of ref
class RefImpl {
constructor(value) {
this._rawValue = toRaw(value)
this._value = toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
}
function trackRefValue(ref) {
if (isTracking()) {
if(! ref.dep) { ref.dep =new Set()}// Add side effects for dependency collection
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
Copy the code
When ref. Value is accessed, the value method of RefImpl instance will be triggered, and thus trackRefValue will be triggered to collect DEp.add (activeEffect). So who’s the activeEffect?
Recall the implementation of the setupRenderEffect phase:
const setupRenderEffect = (instance, initialVNode, container, ...) = > {
// Create a reactive side effect function
const componentUpdateFn = () = > {};
// create render effcet
const effect = new ReactiveEffect(
componentUpdateFn,
() = > queueJob(instance.update),
instance.scope
);
const update = (instance.update = effect.run.bind(effect));
update();
};
Create an implementation of the effect class
class ReactiveEffect {
run() {
try {
effectStack.push((activeEffect = this))
// ...
return this.fn()
} finally{}}}Copy the code
Dep. Add (activeEffect) sets the global activeEffect to the current render effect when the update function (run method of the render effect instance) is executed. This enables dependency collection.
We can print the contents of setupState to verify our analysis:
From the screenshot, we can see that the side effect of title collection is rendering effect. Careful students can find that fn method in the screenshot is componentUpdateFn function, execute fn() to continue to mount children.
Distributed update
After analyzing the dependency collection phase, how does VUe3 distribute updates?
When we click the button to perform this. The title + = 1, the same will trigger PublicInstanceProxyHandlers set methods, and the order of the set to trigger the same and get the same: setupState = > data = > other are not allowed to modify judgments (such as: Reserved fields at the beginning of props, $)
// The handler implementation of the proxy render context
const PublicInstanceProxyHandlers = {
set({ _: instance }, key, value) {
const { data, setupState, ctx } = instance;
// 1. Update the setupState property value
if(setupState ! == EMPTY_OBJ && hasOwn(setupState, key)) { setupState[key] = value; }// 2. Update the data attribute value
else if(data ! == EMPTY_OBJ && hasOwn(data, key)) { data[key] = value; }// ...
return true; }};Copy the code
Set setupState[key] to continue firing the set method of setupState:
const shallowUnwrapHandlers: ProxyHandler<any> = {
set: (target, key, value, receiver) = > {
const oldValue = target[key]
// oldValue is of type ref &value is not ref
if(isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true
} else {
// Otherwise, return directly
return Reflect.set(target, key, value, receiver)
}
}
}
Copy the code
When the value of oldvalue. value is set, the set method of ref is triggered to determine whether there is DEP in ref, and the side effect effect.run() is executed to distribute updates and complete the update process.
class RefImpl{
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
// Determine if the ref has a dependency and send an update
function triggerRefValue(ref) {
ref = toRaw(ref)
if (ref.dep) {
triggerEffects(ref.dep)
}
}
// Send updates
function triggerEffects(dep) {
for (const effect of isArray(dep) ? dep : [...dep]) {
if(effect ! == activeEffect || effect.allowRecurse) {// Perform side effects
effect.run()
}
}
}
Copy the code
conclusion
In conclusion, we have finished analysis vue3 the whole rendering process and update process, of course we are only from the main rendering process analysis, complete the complexity of the rendering process, such as tree optimization based on block implementation, patch phase diff optimization and responsive in the update process optimization and how details such as stage. Interested students can analyze their own learning or pay attention to the follow-up analysis of our public number.
The original intention of this article is to provide you with an outline of the entire vuE3 rendering process, with an overall impression, and then to analyze more detailed points, you will have more ideas and direction.
Finally, attach a complete rendering flow chart to share with you.
The resources
[1] Vue.js 3.0 source address
[2] More on vue.js 3.2 optimization of responsive parts