The outline
At present, the community has a lot of Vue3 source code analysis article, but the quality level is not uniform, not systematic and comprehensive, always a knowledge point a knowledge point interpretation, so I read, there will be a fault, unable to integrate the whole Vue3 knowledge system, so can only own
And I built a Github I will be in my source code some notes, as well as some of my understanding of the source code, all put in this Github, not only for their own to review the old to learn new, but also to facilitate later partners can quickly understand the whole vUE system
Github address attached to vue source code parsing v3.2.26
The project includes mind map (will be updated gradually later), source annotations, simple version of handwriting principle, and help to understand the article, we hope you manual star
Without further ado, we are going to do the source guide, the source guide, will be divided into the following aspects:
- engineering
- componentization
- The renderer
- Responsive system
- The compiler
- Why read vue source code
Let’s do it one by one
engineering
learn(monorepo)
Monorepo means that the code for many projects is contained in a single code base of a version control system. These projects, while they may be related, are usually logically independent and maintained by different teams. To understand what he is, let’s use a picture
As you can see in the figure above, the general idea is that a repository has many packages, which can be packaged or distributed separately, and maintained by different people, which is the way many open source libraries are today
roullp
Rollup is a JavaScript module wrapper that compiles small chunks of code into large, complex chunks of code, and is currently the preferred packaging for libraries, Bundler
Typescirpt
Typescirpt, of course, has a hot community
CICD
Each project is different, interested can refer to
Dependency package Management
PNPM – Fast, disk space saving software package manager
Unit testing
Jest is an enjoyable JavaScript testing framework that focuses on simplicity
Code check
ESLint assembles JavaScript and JSX checking tools
The directory structure
Here we mainly introduce a few main core libraries, the source itself does not need to see details, we only need to understand the main principles, and can absorb some excellent code design and ideas
Learn build project, all directories are scattered in packages, and packages in packages can also use more important packages individually
- Compiler-core (the core logic of the compiler)
- Compiler-dom (dom platform compiler)
- Vue-compiler-sfc (Parsing SFC components is similar to VUE-lorder in VUE2)
- Reactivity (reactive)
- Run-time core (runtime code, including renderer, VNode, scheduler)
- Runtime-dom (DOM platform dependent)
- Vue (type the directory of different packages at the end)
- Shared (initialized variables, utility functions, etc.)
componentization
Another core idea of Vue3 is componentization. Componentization is to divide a page into multiple components, and the CSS, JavaScript, templates, images and other resources that each component relies on are developed and maintained together. Components are resource independent, reusable within the system, and nested among components.
When we develop real projects with Vue3, we write a bunch of components and assemble them into pages like building blocks. The vue.js website also spends a lot of time explaining what components are, how to write them, and the properties and features they have.
Of course, the concept of componentization is not unique to VUE3, it has been around for a long time
Component nature
Back in the days of JQuery, the concept of a template engine was something you’ve heard of for years.
For example
import { template } from 'lodash'
const compiler = template('<h1><%= title %></h1>')
const html = compiler({ title: 'My Component' })
document.getElementById('app').innerHTML = html
Copy the code
The above code is a template engine for LoDash, and its essence is: Template + data =HTML. I’m sure many of the old front ends know that back in the days when the front and back end didn’t separate, templates were the fate of most Web sites. The template SRR solution of that era was essentially a template engine
However, the template engine has changed in the vUE and React era
Template + Data =Vdom He introduced the concept of Virtual DOM
In Vue 3, our template is abstracted into the render function, which is our template, for example:
<div id="demo">
<div @click="handle">Click on the switch</div>
<div @click="handle1">Click Toggle 1</div>
<div v-if="falg">{{num}}</div>
<div v-else>{{num1}}</div>
</div>
Copy the code
So he’s going to end up compiling something like this
const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
const _hoisted_1 = ["onClick"]
const _hoisted_2 = ["onClick"]
const _hoisted_3 = { key: 0 }
const _hoisted_4 = { key: 1 }
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment } = _Vue
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div", { onClick: handle }, "Click to switch".8 /* PROPS */, _hoisted_1),
_createElementVNode("div", { onClick: handle1 }, "Click toggle 1".8 /* PROPS */, _hoisted_2),
falg
? (_openBlock(), _createElementBlock("div", _hoisted_3, _toDisplayString(num), 1 /* TEXT */))
: (_openBlock(), _createElementBlock("div", _hoisted_4, _toDisplayString(num1), 1 /* TEXT */)),64 /* STABLE_FRAGMENT */))}}Copy the code
The result of the render function should be a VDOM
Virtual DOM
The Virtual DOM is a JS object, for example
{
tag: "div".props: {},
children: [
"Hello World",
{
tag: "ul".props: {},
children: [{
tag: "li".props: {
id: 1.class: "li-1"
},
children: ["The first".1]}]}Copy the code
It corresponds to the DOM of expression
<div>
Hello World
<ul>
<li id="1" class="li-1">1</li>
</ul>
</div>
Copy the code
Why did the component go from producing HTML directly to producing the Virtual DOM? The reason is that Virtual DOM brings layered design, it abstracts the rendering process, makes the framework can render to the Web (browser) outside of the platform, and can achieve SSR, etc., and is not Virtual DOM’s performance is good
React test results are faster than React test results.
Components in VUE
In our daily writing components are as follows:
<template>
<div>This is a component {{num}}</div>
</template>
<script>
export default {
name:home
setup(){
const num=ref(1)
return {num}
}
};
</script>
Copy the code
The result is as follows:
const home={
setup(){
const num=ref(1)
function handleClick() {
num.value++
}
return {num,handleClick}
},
render(){
with (_ctx) {
const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", { onClick: handleClick }, "This is a component." + _toDisplayString(num), 9 /* TEXT, PROPS */, _hoisted_1))
}
}
}
Copy the code
In fact, you discover that it is a configuration object, which contains the data, the methods to manipulate the data, and the compiled template template functions
And when we use it, we use it as a tag reference
<template>
<div>
<home></home>
</div>
</template>
Copy the code
The final compiled result
const elementVNode = {
tag: 'div'.data: null.children: {
tag: home,
data: null}}Copy the code
This way, our components can participate in the VDOM, and our entire page can be rendered as a VDOM tree containing components, which can be assembled into a page by building blocks, as shown below:
The renderer
The so-called renderer is simply a tool (a function, usually called render) that forms a Virtual DOM from building blocks and renders it into a real DOM under a specific platform. The workflow of the renderer is divided into two stages: Mount and patch, if the old VNode exists, the new VNode is compared with the old VNode in an attempt to complete the DOM update with minimal resource overhead. This process is called patch, or “patching”. If the old VNode does not exist, the new VNode is directly mounted to the new DOM, a process called mount.
Before we do that, let’s look at the Virtual DOM categories.
Vue also represents vNode types in binary
export const enum ShapeFlags {
ELEMENT = 1.// Common node
FUNCTIONAL_COMPONENT = 1 << 1.//2 // function components
STATEFUL_COMPONENT = 1 << 2.//4 // Common components
TEXT_CHILDREN = 1 << 3.//8 // Text child node
ARRAY_CHILDREN = 1 << 4.//16 // Array child node
SLOTS_CHILDREN = 1 << 5./ / 32
TELEPORT = 1 << 6.//64 // Portal
SUSPENSE = 1 << 7.//128 // Can be asynchronous in components
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8./ / 256
COMPONENT_KEPT_ALIVE = 1 << 9.//512// keepALIVE
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 6 represents functional components and normal components
}
Copy the code
With these type distinctions, we can perform different mount logic and patch logic through different types
switch (type) {
// Text node
case Text:
processText(n1, n2, container, anchor)
break
// Comment the node
case Comment:
processCommentNode(n1, n2, container, anchor)
break
// Static node, this should be used in SSR
// Static vNodes are common only in SSR
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
/ / fragments
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
// Remove the special case node rendering, which is normal vNode rendering
// If it is a node type
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
// If it is a component type
// The first time the mount is performed, it is initialized as a component type
// After the vue3 revision, the component vNode is created directly with the configuration of common objects
// This configuration requires a function to fetch, which is also dynamically loaded
// The incoming name is picked up at runtime by resolvecompinent
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
// If it is a portal
} else if (shapeFlag & ShapeFlags.TELEPORT) {
; (type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
Suspense // Suspense experimental content
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
; (type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
}
}
Copy the code
So to this, the main method of the whole renderer patch core logic is explained to this, we intend to understand the whole source system, and context, do not tangle with the specific implementation, we also need to find the specific implementation in the source code.
Component mounted
Mount components is the initialization of the vnode node component types include reactive initialization, rely on collection, life cycle, compilation and so on, so it is necessary for us to know about the whole process, the components mounted to study the components mounted process, let us more clearly understand how the data in a vue and render function binding.
We removed the core logic of the renderer to explain the process of initialization of the entire component
const nodeOps = {
insert: (child, parent) = > {
parent.insertBefore(child, null)},createElement: (tag) = > {
return document.createElement(tag)
},
setElementText: (el, text) = > {
el.textContent = text
},
}
// We only consider the patch event
function patchEvent(el, key, val) {
el.addEventListener(key.slice(2).toLowerCase(), function () {
val()
})
}
/ / processing props
function patchProp(el, key, val) {
patchEvent(el, key, val)
}
// check if it is ref
function isRef(r) {
return Boolean(r && r.__v_isRef === true)}// Return the correct value
function unref(ref) {
return isRef(ref) ? ref.value : ref
}
const toDisplayString = (val) = > {
return val == null
? ' '
: String(val)
}
const computed = Vue.computed
Vue.computed = function (fn) {
const val = computed(fn)
// 模拟拿到effect
recordEffectScope(val.effect)
return val
}
const ref = Vue.ref
Vue.ref = function (val) {
const newRef = ref(val)
// 模拟拿到effect
recordEffectScope(val.effect)
return newRef
}
The current function is used in computed, watch, watchEffect, and so on
function recordEffectScope(effect, scope) {
scope = scope || activeEffectScope
if (scope && scope.active) {
scope.effects.push(effect)
}
}
// Internal use of vUE
// Use EffectScope inside the page to unload dependencies when the component is destroyed
// The current instance
let currentInstance = null
// Set the current instance
const setCurrentInstance = (instance) = > {
currentInstance = instance
instance.scope.on()
}
function parentNode(node) {
return node.parentNode
}
// error tolerance
function callWithErrorHandling(fn, instancel, args) {
let res
try{ res = args ? fn(... args) : fn() }catch (err) {
console.error(err)
}
return res
}
// create a response
function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get: (target, key, receiver) = > {
return unref(Reflect.get(target, key, receiver))
},
set: (target, key, value, receiver) = > {
return Reflect.set(target, key, value, receiver)
}
})
}
// Assign the render function
function finishComponentSetup(instance) {
Component = instance.type
// There may be a compilation process, temporarily omitted
instance.render = Component.render
}
function handleSetupResult(instance, setupResult) {
instance.setupState = proxyRefs(setupResult)
// There is also compiled content below
finishComponentSetup(instance)
}
// Create a component instance
function createComponentInstance(vnode, parent) {
// Get the type
const type = vnode.type
const instance = {
vnode,
type,
parent,
// An EffectScope has been created to handle dependencies in batches
scope: new EffectScope(true /* detached */),
render: null.subTree: null.effect: null.update: null,}return instance
}
// Component-type processing, including mount and update
function processComponent(n1, n2, container) {
if (n1 == null) {
mountComponent(n2, container)
} else {
updateComponent()
}
}
const processText = (n1, n2, container, anchor) = > {
if (n1 == null) {
nodeOps.insert(
(n2.el = createText(n2.children)),
container
)
}
}
// The node is initialized
function mountElement(n2, container, parentComponent) {
const { type, props, children, shapeFlag } = n2
let el = nodeOps.createElement(type)
// Determine the content with text child node
if (n2.shapeFlag === 9) {
nodeOps.setElementText(el, children)
}
// if there is a case for props
if (props) {
for (key in props) {
// Note that only events are handled, not styles
patchProp(el, key, props[key])
}
}
nodeOps.insert(el, container)
}
// Node type processing
function processElement(n1, n2, container, parentComponent) {
if (n1 == null) {
// If not, the type of mountelement is used
mountElement(
n2,
container,
parentComponent
)
} else {
// Next is the component update, which follows the content of patch, including the diff, which is the most core content}}// Component destruction method
function unmountComponent() {}/ / create a vnode
function createVNode(type, props, children, shapeFlag = 1) {
if (typeof children === "string") {
shapeFlag = 9
}
const vnode = {
type,
props,
children,
shapeFlag,
component: null,}return vnode
}
// Patch contains the mount of various components, nodes, comments, etc. Here we analyze only the mount of components in order to analyze dependency tracing
function patch(n1, n2, container, parentComponent = null) {
// If it is equal, it is the same
if (n1 === n2) {
return
}
const { type, shapeFlag } = n2
switch (type) {
// We only deal with component types and common node types
default:
if (shapeFlag & 1) {
processElement(n1, n2, container, parentComponent)
} else {
processComponent(n1, n2, container)
}
}
}
function setupStatefulComponent(instance) {
const Component = instance.type
const { setup } = Component
if (setup) {
setCurrentInstance(instance)
// Get the setup result
const setupResult = callWithErrorHandling(
setup,
instance,
[instance.props]
)
// We only consider the return object case, not the other case
handleSetupResult(instance, setupResult)
}
}
// Execute the setup function
function setupComponent(instance) {
// Execute the core logic
const setupResult = setupStatefulComponent(instance)
return setupResult
}
/ / get the vnode
function renderComponentRoot(instance) {
// The source code uses a Proxy to Proxy all responsive content access and modification and stored in instance. Proxy, unified processing
// We only need setupState for the main process
const {
type: Component,
render,
setupState,
} = instance
debugger
result = render.call(
setupState,
setupState,
)
return result
}
// Rely on collection to generate effect
function setupRenderEffect(instance, n2, container) {
const componentUpdateFn = () = > {
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
// This is the patch diff inside the component
patch(
prevTree,
nextTree,
container,
instance
)
}
// Call the internal ReactiveEffect
const effect = (instance.effect = new Vue.ReactiveEffect(
componentUpdateFn,
null,
instance.scope // Track it in component's effect scope
))
const update = (instance.update = effect.run.bind(effect))
update()
}
// Component initialization method
function mountComponent(initialVNode, container, parentComponent = null) {
const instance =
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
))
/ / the setup
setupComponent(instance)
// Rely on collection
setupRenderEffect(instance, initialVNode, container)
}
// Component update
function updateComponent() {
// Update logic
}
// The initialization of the render component is basically executing the path
function createApp(rootComponent, rootProps = null) {
// We pass the value directly
const vnode = createVNode(rootComponent, rootProps, null.4)
const mount = (container) = > {
if (typeof container === 'string') {
container = document.querySelector(container)
}
patch(null, vnode, container)
}
return { mount }
}
/ / initialization
createApp({
setup() {
const num = Vue.ref(1)
console.log(num)
const double = Vue.computed(() = > {
console.log(1111)
debugger
return num.value * 2
})
function handleClick() {
num.value++
}
return {
num,
double,
handleClick
}
},
render(_ctx) {
with (_ctx) {
return createVNode("div", { onClick: handleClick }, toDisplayString(double))
}
}
}).mount('#app')
Copy the code
The above code you can paste to run, you can also view on Github
Just to explain the core logic, we createApp, we pass in the current component configuration, we initialize the component with patch, we initialize the component with setupComponent, The instance this is not passed in through call, so this is not available in the function.
We then execute setupRenderEffect to start the dependency collection, and execute the Render method in the current method to trigger a reactive GET in setup to trigger the dependency collection process, which we’ll discuss in more detail in the reactive section later
ReactiveEffect
Reactiveeffects are at the heart of reactive systems, and reactive systems are at the heart of VUe3, so we have to understand what reactiveeffects are all about
Reactive ReactiveEffect is the core of reactive, and is responsible for collecting and updating dependencies.
// Record the current active object
let activeEffect
// Mark whether to trace
let shouldTrack = false
class ReactiveEffect{
active = true // Whether it is in the active state
deps = [] // All responsive objects that depend on this effect
onStop = null // function
constructor(fn, scheduler) {
this.fn = fn // Callback functions, for example, computed(/* fn */() => {return testref.value ++})
// function type. If it is not null then effect. Scheduler is executed in TriggerRefValue, otherwise effect
this.scheduler = scheduler
}
run() {
// If this effect does not need to be collected by reactive objects
if(!this.active) {
return this.fn()
}
// Two utility functions are used here: pauseTracking and enableTracking to change the state of shouldTrack
shouldTrack = true
activeEffect = this
ActiveEffect dependencies can be collected in the method after the activeEffect is set
const result = this.fn()
shouldTrack = false
// Empty the currently active effects after executing the side effects function
activeEffect = undefined
return result
}
// Pause the trace
stop() {
if (this.active) {
// Find all responsive objects that depend on this effect
// Remove effect from these reactive objects
cleanupEffect(this)
// Execute the onStop callback
if (this.onStop) {
this.onStop();
}
this.active = false; }}}Copy the code
This.fn (), at the component level, is a render function that triggers updates by re-executing the current method
In the whole reactive system, he established the relationship between reactive data and ReactiveEffect through a DEP, in which ReactiveEffect is stored. A reactive data has a DEP steward that manages the reactiveeffects associated with it. When the reactive data changes, the DEP steward is triggered to batch the reactiveeffects in it for updating. Note that in componentification, A component has only one render ReactiveEffect.
When it comes to implementing Render, you have to think of Diff
Diff in the renderer
As mentioned above, the purpose of using the Virtual DOM is not performance, but cross-platform. Therefore, when the page has a large number of content updates, performance cannot be guaranteed, and an algorithm is needed to reduce the performance overhead of DOM operations
The core of the basic principle of diFF algorithm on the market is diFF same-level comparison, not cross-level comparison, which can greatly reduce the calculation of JS, and there are different schools on the core algorithm of same-level comparison
React
A series of diff algorithms – find the nodes to move from front to back- Double diff- Walk from both ends to the middle to find the node to move
- Longest recursive subsequence diff– Find the node to move by solving the longest recursive subsequence
Vue3 currently uses Inferno and the core algorithm is the longest recursive subsequence
Here we are not redundant, the whole process of comparison, a lot of big guys have also analyzed, we are just an introduction, do not do in-depth exploration, intended to master a vUE source system together with you
Responsive system
Responsiveness — This term is thrown around a lot in programming, but what does it mean? Responsiveness is a programming paradigm that allows us to adapt to change in a declarative way.
The principle of responsiveness in VUE3 is described in the Proxy documentation
- Trace when a value is read: the proxy
get
In the processing functiontrack
The function records the property and the current side effect. - Detect when a value changes: Invoked on proxy
set
Processing functions. - Rerun the code to read the original value:
trigger
The function finds which side effects depend on the property and executes them.
The proxied objects are not visible to the user, but internally, they enable Vue to do dependency tracking and change notification in case the value of the property is accessed or modified.
So what did he tell you? Reactiveeffect.run reactiveEffect.run ReactiveEffect.run ReactiveEffect.run reactiveEffect.run
We have also prepared a working code, you can also paste down, practical experience
// Save temporary dependency functions for wrapping
const effectStack = [];
// Dependency map objects can only accept objects
let targetMap = new WeakMap(a);// Determine if it is an object
const isObject = (val) = >val ! = =null && typeof val === 'object';
// function of ref
function ref(val) {
Value ->proxy = value->proxy = value->proxy
// We don't use value access in the object case
return isObject(val) ? reactive(val) : new refObj(val);
}
// Create a responsive object
class refObj {
constructor(val) {
this._value = val;
}
get value() {
// Trigger GET after the first execution to collect dependencies
track(this.'value');
return this._value;
}
set value(newVal) {
console.log(newVal);
this._value = newVal;
trigger(this.'value'); }};// We will not consider nested objects in the object for the sake of understanding the principle
// The object is reactive
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// Reflect is used to perform the default action on an object, more formally and functionally
// Both Proxy and Object methods Reflect
const res = Reflect.get(target, key, receiver);
track(target, key);
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
trigger(target, key);
return res;
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key);
trigger(target, key);
returnres; }}); }// At this point, the current ref object is already listening for data changes
const newRef = ref(0);
// But there is still no responsiveness, so how does it achieve responsiveness ---- rely on collection, trigger updates =
// for dependency collection
// In order to generalize the method in the source code, it also passes in many parameters to be compatible with different situations
// We want to understand the principle, just need to wrap fn
function effect(fn) {
// Wrap the current dependency function
const effect = function reactiveEffect() {
// Error handling is also included in the mock source code, in order to prevent you from writing errors, which is the genius of the framework
if(! effectStack.includes(effect)) {try {
// Put the current function on a temporary stack, in order to trigger get in the following execution, can be found in the dependency collection of the current variable dependencies to establish relationships
effectStack.push(fn);
// Execute the current function and start to rely on the collection
return fn();
} finally {
// Execute successfully unstackeffectStack.pop(); }}; }; effect(); }// Establish relationships in collected dependencies
function track(target, key) {
// Retrieve the last data content
const effect = effectStack[effectStack.length - 1];
// If the current variable has dependencies
if (effect) {
// Check whether target exists in the current map
let depsMap = targetMap.get(target);
// If not
if(! depsMap) {// New Map stores the current WeakMap
depsMap = new Map(a); targetMap.set(target, depsMap); }// Get the set of response functions corresponding to key
let deps = depsMap.get(key);
if(! deps) {// Establish the relationship between the current key and its dependencies, because a key can have multiple dependencies
// To prevent duplicate dependencies, use set
deps = new Set(a); depsMap.set(key, deps); }// Store the current dependency
if(! deps.has(effect)) { deps.add(effect); }}}// Used to trigger updates
function trigger(target, key) {
// Get all dependencies
const depsMap = targetMap.get(target);
// If there are dependencies, pull them out
if (depsMap) {
// Get the set of response functions
const deps = depsMap.get(key);
if (deps) {
// Execute all response functions
const run = (effect) = > {
// The source code has asynchronous scheduling tasks, which we omit here
effect();
};
deps.forEach(run);
}
}
}
effect(() = > {
console.log(11111);
// In my implementation of effect, set cannot be triggered because it is not compatible to demonstrate the principle, otherwise it will be an infinite loop
// The vue source code triggers the compatibility process in effect only once
newRef.value;
});
newRef.value++;
Copy the code
In the above code, there is no render effect because if you use render effect it’s too big and you get confused, so we use a user effect in the Composition API, which is similar to Vue3, The difference is that render effect is a ReactiveEffect and user effect is a function that we pass in, and they all end up in the DEPS butler
The compiler
The compiler plays a big role in vuE3’s performance improvement. Due to the traversability of the template, there are many optimizations that can be made during compilation. Before that, let’s briefly outline the basic flow of the compiler
For those of you who know the compiler workflow, a complete compiler workflow looks like this:
- First of all,
parse
The raw code string is parsed to generate the abstract syntax tree AST. - Second,
transform
Transform the abstract syntax tree into a structure closer to the target DSL. - In the end,
codegen
Generate the target “DSL” from the transformed abstract syntax tree (A programming language with limited expressiveness designed for a specific domain) executable code.
That’s the same thing with vUE
The Parse phase
The parse function takes place by converting a template to an AST. It uses regular expressions to match the string in the template. The result is a crude AST object, which is an abstract representation of the source code syntax. It represents the syntactic structure of a programming language as a tree, with each node in the tree representing a structure in the source code. In our VUE compilation, it’s just a JS object
For example:
<template>
<p>hello World!</p>
</template>
Copy the code
Into the ast
{
"type": 0."children": [{"type": 1."ns": 0."tag": "template"."tagType": 0."props": []."isSelfClosing": false."children": [{"type": 1."ns": 0."tag": "p"."tagType": 0."props": []."isSelfClosing": false."children": [{"type": 2."content": "hello World!"."loc": {
"start": {
"column": 6."line": 2."offset": 16
},
"end": {
"column": 18."line": 2."offset": 28
},
"source": "hello World!"}}]."loc": {
"start": {
"column": 3."line": 2."offset": 13
},
"end": {
"column": 22."line": 2."offset": 32
},
"source": "hello World!
"}}]."loc": {
"start": {
"column": 1."line": 1."offset": 0
},
"end": {
"column": 12."line": 3."offset": 44
},
"source": "<template>\n <p>hello World!</p>\n</template>"}}]."helpers": []."components": []."directives": []."hoists": []."imports": []."cached": 0."temps": 0."loc": {
"start": {
"column": 1."line": 1."offset": 0
},
"end": {
"column": 1."line": 4."offset": 45
},
"source": "<template>\n <p>hello World!</p>\n</template>\n"}}Copy the code
If you are interested, go to the AST Explorer and you can see the AST generated by parsing javascript code from different Parsers online.
The Transform phase
In the Transform phase, Vue performs some transformation operations on the AST, and further processes some hoistStatic static elevations of UE and The PatchFlags patch identification of the cacheHandlers cache function
Codegen phase
Generating the render function
Vue3 compiler optimization strategy
As we’ve said before, the Transform phase does a lot of optimization, so let’s go through the details and start with a compiled piece of code
const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
const _hoisted_1 = ["onClick"]
const _hoisted_2 = ["onClick"]
const _hoisted_3 = /*#__PURE__*/_createElementVNode("div".null."Static node", -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createCommentVNode("
\n comment \n
"),
_createElementVNode("div", { onClick: handle }, "Click to switch".8 /* PROPS */, _hoisted_1),
_createElementVNode("div", { onClick: handle1 }, "Click toggle 1".8 /* PROPS */, _hoisted_2),
_hoisted_3,
_createElementVNode("div".null, _toDisplayString(num1) + "Dynamic node".1 /* TEXT */)].64 /* STABLE_FRAGMENT */))}}Copy the code
1. Static nodes are promoted
Static nodes are promoted to prevent patches from being processed at diff and thus flagged at compile time, so that static content is excluded from render and prevented from being created again when render is executed
// Use closures to place the result within render to prevent the current static node from being created by repeating render
const _hoisted_3 = /*#__PURE__*/_createElementVNode("div".null."Static node", -1 /* HOISTED */)
Copy the code
2. Patch marking and dynamic attribute recording
_createElementVNode("div", {onClick: handle}, "toggle ", 8 /* PROPS */, _hoisted_1),Copy the code
There is an 8 in the above code, which is patchFlag, and all its types are shown as follows:
export const enum PatchFlags {
// Dynamic text node
TEXT = 1.// 2 Dynamic class
CLASS = 1 << 1.// 4 dynamic style
STYLE = 1 << 2./ / dynamic props
PROPS = 1 << 3./** * indicates an element with an item with a dynamic key point. When the key is changed, a complete * always requires diff to remove the old key. This flag is mutually exclusive with class, style and props. * /
// Has a dynamic key property. When the key changes, a full diff is required
FULL_PROPS = 1 << 4.// 32 Nodes with listening events
HYDRATE_EVENTS = 1 << 5.// 64 A fragment that does not change the order of child nodes
STABLE_FRAGMENT = 1 << 6.// 128 Fragment with key
KEYED_FRAGMENT = 1 << 7.// 256 Fragment without key
UNKEYED_FRAGMENT = 1 << 8.// 512 a child node will only compare non-props
NEED_PATCH = 1 << 9.// 1024 Dynamic slot
DYNAMIC_SLOTS = 1 << 10.// The following are special, i.e., skipped in the diff phase
// 2048 represents a fragment that was created only because the user placed comments at the root level of the template, a flag that is used only for development because the comments are stripped out in production
DEV_ROOT_FRAGMENT = 1 << 11.// Static node, whose contents never change, no diff required
HOISTED = -1.// The diff used to indicate a node should end
BAIL = -2
}
Copy the code
After having a patchFlag, special diff logic can be executed according to the type of patchFlag, which can prevent the performance waste caused by full diff
3. Cache event handlers
By default, onClick is treated as a dynamic binding, so it is tracked every time it changes, but because it is the same function, it is not tracked and can be cached and reused
/ / template
<div> <button @click = 'onClick'>Am I</button> </div>
/ / the compiled
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div".null, [
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (. args) = >(_ctx.onClick && _ctx.onClick(... args))) },"点我")))}// Check the console for the AST
Copy the code
In the above code, there is no type of patchFlag, that is, patchFlag is a default value of 0. In this case, no diff is performed
if (patchFlag > 0) {
// The presence of patchFlag indicates the element's rendering code
// Generated by the compiler, can take a fast path.
// In this path, the old node and the new node are guaranteed to have the same shape
// (that is, in exactly the same place in the source template)
if (patchFlag & PatchFlags.FULL_PROPS) {
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
// class
if (patchFlag & PatchFlags.CLASS) {
if(oldProps.class ! == newProps.class) { hostPatchProp(el,'class'.null, newProps.class, isSVG)
}
}
// style
// this flag is matched when the element has dynamic style bindings
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
if (patchFlag & PatchFlags.PROPS) {
// if the flag is present then dynamicProps must be non-null
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
// #1471 force patch value
if(next ! == prev || key ==='value') {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
// text
// This flag is matched when the element has only dynamic text children.
if (patchFlag & PatchFlags.TEXT) {
if(n1.children ! == n2.children) { hostSetElementText(el, n2.childrenas string)
}
}
} else if(! optimized && dynamicChildren ==null) {
// unoptimized, full diff
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
Copy the code
In the above code, different diff logic is implemented according to the type of patchFlag. Once the patchFlag diff is removed, the old VNode will be reused directly, and the events of the old Vnode will be cached, so we can directly use it, saving the cost of re-creating the packaging function. Many people may not understand what I say, paste the vUE code, you will understand:
export function patchEvent(el: Element & { _vei? : Record<string, Invoker |undefined> },
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | null,
instance: ComponentInternalInstance | null = null
) {
// vei = vue event invokers
const invokers = el._vei || (el._vei = {})
const existingInvoker = invokers[rawName]
if (nextValue && existingInvoker) {
// patch
existingInvoker.value = nextValue
} else {
const [name, options] = parseName(rawName)
if (nextValue) {
// Wrap the event function first, then bind the wrapper function to the DOM
// If caching is enabled, the current wrapper function will be cached, saving the overhead of diff
const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
addEventListener(el, name, invoker, options)
} else if (existingInvoker) {
// remove
removeEventListener(el, name, existingInvoker, options)
invokers[rawName] = undefined}}}Copy the code
4. Piece of block
Create openBlock(), open a block, createBlock creates a block, save the dynamic parts of the block in a property called dynamicChildren. In the future, when the module is updated, only the dynamicChildren will be updated. In this way, patch can only process dynamic child nodes, so as to improve performance. We also combed the code:
/** * The main idea is to skillfully use the stack structure to put dynamic nodes into dynamicChildren **/
let blockStack = []
let currentBlock = null
// Initializers put the block's child dynamic elements on the stack using the stack structure
function _openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null:)} [])function closeBlock() {
blockStack.pop()
currentBlock = blockStack[blockStack.length - 1] | |null
}
function setupBlock(vnode) {
vnode.dynamicChildren = currentBlock
closeBlock()
return vnode
}
function createVnode(type, porps, children, isBlockNode = false.) {
const vnode = {
type,
porps,
children,
isBlockNode,
dynamicChildren: null
}
if(! isBlockNode && currentBlock) { currentBlock.push(vnode) }return vnode
}
function _createElementBlock(type, porps, children) {
// Passing true directly indicates that the current vNode is the start of a current block
return setupBlock(createVnode(type, porps, children, true))}// We assume source is a number
function _renderList(source, renderItem,) {
ret = new Array(source)
for (let i = 0; i < source; i++) {
ret[i] = renderItem(i + 1)}return ret
}
/ / generated vnode
function render(ctx) {
return (_openBlock(), _createElementBlock('Fragment'.null, [
createVnode("div".null."11111".true/* CLASS */),
createVnode("div".null, ctx.currentBranch, /* TEXT */),
(_openBlock(true), _createElementBlock('Fragment'.null, _renderList(5.(i) = > {
return (_openBlock(), _createElementBlock("div".null, i, /* TEXT */}})))))))console.log(render({ currentBranch: An old hero and a new costraint }))
Copy the code
You can copy it and see how dynamicChildren are generated
Why read vue source code
- 1. It may be the most advanced engineering solution on the market
- 2. The best learning material for TS
- 3, standard and maintainable code quality, elegant code skills,
- 4. Targeted performance optimization and packaging during development
- 5, faster positioning of problems encountered in the work
1, 2, 5, we’re not going to talk too much about it, but we’re going to focus on 3 and 4 for two examples
Specification of maintainable code quality, elegant coding techniques
// Plugin mechanics tips
// a global variable
let compile
function registerRuntimeCompiler(_compile) {
// Assign to the current template compiler so that other functions within the current template can be called
// This registration method is required for use in the Runtime
compile = _compile
}
// When the code is exported, it only needs to be registered in non-Runtime versions using the registration method
//finishComponentSetup contains compilation logic inside
function finishComponentSetup(instance) {
Component = instance.type
// Just check whether there is compile and render
if(compile && ! Component.render) {// Execute the compile logic}}Copy the code
// Vue3
function makeMap ( str, expectsLowerCase ) {
var map = Object.create(null);
var list = str.split(', ');
for (var i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? function (val) { return map[val.toLowerCase()]; }
: function (val) { returnmap[val]; }}var isHTMLTag = makeMap(
'html,body,base,head,link,meta,style,title,' +
'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
'embed,object,param,source,canvas,script,noscript,del,ins,' +
'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
'output,progress,select,textarea,' +
'details,dialog,menu,menuitem,summary,' +
'content,element,shadow,template,blockquote,iframe,tfoot'
);
var isHTMLTag = isHTMLTag('div');
Copy the code
// Function extension technique
function createAppAPI(rootComponent, rootProps = null) {
const mount = (container) = >{}return { mount }
}
function createRenderer() {
return {
createApp: createAppAPI()
}
}
function createApp(. args) {
constapp = createRenderer().createApp(... args)const { mount } = app
app.mount = () = > {
console.log('Execute your own logic')
mount()
}
}
Copy the code
Development can be targeted performance optimization, as well as packaging
// Implement a popover encapsulation technique
CreateVNode Render generates the real DOM
const { createVNode, render, ref } = Vue
const message = {
setup() {
const num = ref(1)
return {
num
}
},
template: ` < div > < div > {{num}} < / div > < div > this is a popup window < / div > < / div > `
}
// Generate an instance
const vm = createVNode(message)
const container = document.createElement('div')
// Change from patch to DOM
render(vm, container)
document.body.appendChild(container.firstElementChild)
Copy the code
The last
Vue source system guidance, I uneducated, all the understanding of this, if there is a mistake, please big guys criticism.
Here, on a higher level, to instill a little honesty
Is that I recently found in the community, a lot of people are particularly impetuous, think that understand the source code is much less, do not understand the source code is dish chicken, here I want to say a little bit I see the source code a little bit of feeling:
-
1, read the source code does not necessarily explain how strong you are, because he is not you write, he is reading with reading can not make you make more money the same truth
-
2, look at the source code can let us know what code is good, there is no doubt that vUE source code baseline, is a lot of people may write a lifetime can not reach the height
-
Look at the purpose of the source code, is to let us prevent closed doors, toward a correct direction to work
-
4, VUE source code is just a part of our discipline, the whole front-end ecology is very complex, if it is not interested in the source code (principle or to understand, to deal with the interview), there is no need to worry, to learn what you are interested in hone your craft.