Writing is not easy, without the permission of the author forbid to reprint in any form! If you think the article is good, welcome to follow, like and share! Continue to share technical blog posts, follow the wechat public account 👉🏻 front-end LeBron
Effect and Reactive
Effect, as the core of Vue’s responsive principle, appears in Computed, Watch and Reactive
It works with functions such as Reactive(Proxy), track, and trigger to collect dependencies and trigger dependency updates
- Effect
- Side effect dependent function
- Track
- Depend on the collection
- Trigger
- Depend on the trigger
Effect
Effect can be understood as a side effect function that is collected as a dependency and triggered after a reactive data update.
Vue’s responsive apis, such as Computed and Watch, are implemented by Effect
- Let’s start with the entry function
- Entry functions are mainly logical processing, and the core logic is located in create Active Effect
function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
// If it is already effect, reset it
if (isEffect(fn)) {
fn = fn.raw
}
/ / create the effect
const effect = createReactiveEffect(fn, options)
// If the execution is not lazy, execute it first
if(! options.lazy) { effect() }return effect
}
Copy the code
- createReactiveEffect
const effectStack: ReactiveEffect[] = []
function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
const effect = function reactiveEffect() :unknown {
// Effect stop is called if the function is not activated
if(! effect.active) {// if there is no scheduler, return directly, otherwise execute fn
return options.scheduler ? undefined : fn()
}
// Check whether EffectStack has effect
if(! effectStack.includes(effect)) {/ / remove effect
cleanup(effect)
try {
/* * Start recollecting dependencies * press into the stack * Set Effect to activeEffect * */
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
/* * Eject effect after completion * reset dependencies * reset activeEffect * */
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
effect.id = uid++ // Add id, effect unique identifiereffect.allowRecurse = !! options.allowRecurse effect._isEffect =true // whether is effect
effect.active = true // Whether to activate
effect.raw = fn // Mount the original object
effect.deps = [] // The current effect DEP array
effect.options = options // Options passed in
return effect
}
// Every time effect runs, dependencies are recollected. Deps is an effect dependency array that needs to be emptied
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0}}Copy the code
Track
The Track function is often found in the getter of Reactive and is used for dependency collection
Source code details see notes
function track(target: object.type: TrackOpTypes, key: unknown) {
// activeEffect null indicates no dependency
if(! shouldTrack || activeEffect ===undefined) {
return
}
// targetMap dependency management Map, used to collect dependencies
// Check if there is target in targetMap
let depsMap = targetMap.get(target)
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// DeP is used to collect dependent functions. When the listening key changes, the dependency function in the DEP is updated
let dep = depsMap.get(key)
if(! dep) { depsMap.set(key, (dep =new Set()))}if(! dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep)// The development environment will trigger onTrack for debugging only
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
Copy the code
Trigger
Trigger is often found in setter functions in Reactive that Trigger dependency updates
Source code details see notes
function trigger(
target: object.type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
// Get dependency Map, if not, no need to trigger
const depsMap = targetMap.get(target)
if(! depsMap) {// never been tracked
return
}
// Use Set to save effects that need to be triggered to avoid duplication
const effects = new Set<ReactiveEffect>()
// Define dependency add-function
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }// Add the dependency from depsMap to effects
// Don't look at the branches just to understand the principles
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) = > {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if(key ! = =void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length'))}break
case TriggerOpTypes.DELETE:
if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break}}// Encapsulate effects to execute functions
const run = (effect: ReactiveEffect) = > {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
// call if there is a scheduler
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
// Triggers all dependent functions in Effects
effects.forEach(run)
}
Copy the code
Reactive
Knowing that Track is used for dependency collection and Trigger is used for dependency triggering, when are they called? Check out the source code of Reactive (see the comments for details).
Note: the source code structure is more complex (encapsulation), to facilitate the understanding of the principle, the following is a simplified source code.
- In summary
- Do dependency collection in the getter
- Trigger dependency updates when you setter
function reactive(target:object){
return new Proxy(target,{
get(target: Target, key: string | symbol, receiver: object){
const res = Reflect.get(target, key, receiver)
track(target, TrackOpTypes.GET, key)
return res
}
set(target: object, key: string | symbol, value: unknown, receiver: object){
let oldValue = (target as any)[key]
const result = Reflect.set(target, key, value, receiver)
// trigger(target, TriggerOpTypes.ADD, key, value)
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
return result
}
})
}
Copy the code
Computed
Computed is a common and useful attribute in Vue that changes its value synchronously after a dependency changes and uses cached values if the dependency does not change.
- Vue2
- The implementation of Computed in Vue2 implements dependency collection of responsive data and dependency update triggered by indirect chain through nested watcher.
- Effect appears in Vue3, reimplementing the Computed property
- Effects can be understood as side effects functions that are collected as dependencies and fired after reactive data updates.
Show me the Code
- If you read through this computed function, you’ll see that you’re just doing a brief assignment of getters and setters
- Computed supports two ways of writing
- function
- Getter and setter
- Computed supports two ways of writing
function computed<T> (
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () = > {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return newComputedRefImpl( getter, setter, isFunction(getterOrOptions) || ! getterOrOptions.set )as any
}
Copy the code
- The core logic is all in ComputedRefImpl, so let’s move on
- Data is marked as old by the dirty variable
- Assign dirty to true after reactive data updates
- On the next get, it recalculates when dirty is true and assigns dirty to false
class ComputedRefImpl<T> {
private_value! : Tprivate _dirty = true
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true;
public readonly [ReactiveFlags.IS_READONLY]: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
this.effect = effect(getter, {
lazy: true.// Assign dirty to true after reactive data updates
// The next time you run the getter to determine that dirty is true, that is, to recalculate the computed value
scheduler: () = > {
if (!this._dirty) {
this._dirty = true
// Dispatches all side effects that refer to the currently calculated property
trigger(toRaw(this), TriggerOpTypes.SET, 'value')}}})this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
// When responsive data is updated, dirty is true
// After recalculating the data, assign dirty to false
if (self._dirty) {
self._value = this.effect()
self._dirty = false
}
// Rely on collection
track(self, TrackOpTypes.GET, 'value')
// Returns the calculated value
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
Copy the code
Watch
Watch is mainly used to monitor a variable and perform corresponding processing
Vue3 not only reconstructs Watch, but also adds a WatchEffect API
- Watch
CallBack is used to listen on a variable and retrieve both old and new values via callBack
watch(state, (state, prevState) = >{})
Copy the code
- WatchEffect
Every update is executed, automatically collecting used dependencies
Unable to obtain new and old values, you can manually stop listening
The callback passed in by onInvalidate(fn) is executed when watchEffect is restarted or stopped
const stop = watchEffect((onInvalidate) = >{
// ...
onInvalidate(() = >{
// ...})})// Stop listening manually
stop()
Copy the code
Differences between Watch and watchEffect
- Watch executes lazily, and watchEffect executes every time code is loaded
- Watch can specify listening variables, and watchEffect automatically relies on collection
- Watch gets old and new values, but watchEffect does not
- The watchEffect has onInvalidate, while the Watch does not
- Watch can only listen on objects such as REF and Reactive, and watchEffect can only listen on specific attributes
Source Code
Show me the Code
- Here you can see that the core logic of Watch and watchEffet is encapsulated in doWatch
// watch
export function watch<T = any.Immediate extends Readonly<boolean> = false> (
source: T | WatchSource<T>,
cb: any, options? : WatchOptions<Immediate>) :WatchStopHandle {
if(__DEV__ && ! isFunction(cb)) { warn(`\`watch(fn, options?) \` signature has been moved to a separate API. ` +
`Use \`watchEffect(fn, options?) \` instead. \`watch\` now only ` +
`supports \`watch(source, cb, options?) signature.`)}return doWatch(source as any, cb, options)
}
export function watchEffect(effect: WatchEffect, options? : WatchOptionsBase) :WatchStopHandle {
return doWatch(effect, null, options)
}
Copy the code
- doWatch
The following is the truncated version of the source code, to understand the core principle
See notes for details
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
instance = currentInstance
) :WatchStopHandle {
let getter: () = > any
let forceTrigger = false
let isMultiSource = false
// Do getter assignments for different cases
if (isRef(source)) {
// ref is obtained by.value
getter = () = > (source asRef).value forceTrigger = !! (sourceas Ref)._shallow
} else if (isReactive(source)) {
// reactive directly
getter = () = > source
deep = true
} else if (isArray(source)) {
// If it is an array, do traversal
isMultiSource = true
forceTrigger = source.some(isReactive)
getter = () = >
source.map(s= > {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return traverse(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER, [
instance && (instance.proxy as any)
])
} else {
__DEV__ && warnInvalidSource(s)
}
})
} else if (isFunction(source)) {
// In the case of a function
// Watch if cb exists and watchEffect if cb does not exist
if (cb) {
// getter with cb
getter = () = >
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER, [
instance && (instance.proxy as any)
])
} else {
// no cb -> simple effect
getter = () = > {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onInvalidate]
)
}
}
} else {
// Abnormal condition
getter = NOOP
// Throw an exception
__DEV__ && warnInvalidSource(source)
}
// Deep listen logic processing
if (cb && deep) {
const baseGetter = getter
getter = () = > traverse(baseGetter())
}
let cleanup: () = > void
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) = > {
cleanup = runner.options.onStop = () = > {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// record oldValue and get newValue through runner
// Callback is encapsulated as job
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
const job: SchedulerJob = () = > {
if(! runner.active) {return
}
if (cb) {
// watch(source, cb)
const newValue = runner()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) = >
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onInvalidate
])
oldValue = newValue
}
} else {
// watchEffect
runner()
}
}
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)job.allowRecurse = !! cb// Handle the trigger time of the job by reading the configuration
// Again encapsulates the execution of the job into the Scheduler
let scheduler: ReactiveEffectOptions['scheduler']
if (flush === 'sync') { // Synchronize execution
scheduler = job
} else if (flush === 'post') { // Execute after update
scheduler = () = > queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
// Execute before update
scheduler = () = > {
if(! instance || instance.isMounted) { queuePreFlushCb(job) }else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
job()
}
}
}
// Handle dependency collection with effect side effects, calling scheduler (which encapsulates callback execution) after dependency update
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
})
// Collect dependencies
recordInstanceBoundEffect(runner, instance)
// Read the configuration and initialize the watch
// Whether there is cb
if (cb) {
// Whether to execute immediately
if (immediate) {
job()
} else {
oldValue = runner()
}
} else if (flush === 'post') {
// Whether to execute after update
queuePostRenderEffect(runner, instance && instance.suspense)
} else {
runner()
}
// Returns the manual stop function
return () = > {
stop(runner)
if(instance) { remove(instance.effects! , runner) } } }Copy the code
Mixin
Mixin means mix and is a common logic encapsulation tool.
The principle is simple: merge.
- Merge is divided into object merge and life cycle merge
- Object, mergeOption
- Overwrite the merge of type Object.assign
- Life cycle, mergeHook
- The merge puts the two life cycles into a queue and calls them one by one
- Object, mergeOption
- mergeOptions
function mergeOptions(
to: any.from: any, instance? : ComponentInternalInstance |null,
strats = instance && instance.appContext.config.optionMergeStrategies
) {
if (__COMPAT__ && isFunction(from)) {
from = from.options
}
const { mixins, extends: extendsOptions } = from
extendsOptions && mergeOptions(to, extendsOptions, instance, strats)
mixins &&
mixins.forEach((m: ComponentOptionsMixin) = >
mergeOptions(to, m, instance, strats)
)
// Iterate over the mixin object
for (const key in from) {
// If so, override
if (strats && hasOwn(strats, key)) {
to[key] = strats[key](to[key], from[key], instance && instance.proxy, key)
} else {
// If it does not exist, it will be assigned directly
to[key] = from[key]
}
}
return to
}
Copy the code
- mergeHook
Simply put it into a Set and call it as it’s called
function mergeHook(
to: Function[] | Function | undefined.from: Function | Function[]
) {
return Array.from(new Set([...toArray(to), ...toArray(from)))}Copy the code
Vuex4
Vuex is a state management library commonly used in Vue. After the release of Vue3, this state management library also issued Vuex4 adapted to Vue3
Faster than vex3. X principle
-
Why does every component pass
This.$store accesses store data?
- In beforeCreate, store is injected through mixin
-
Why is all the data in Vuex responsive
- When we create a store, we call
new Vue
, creates an instance of Vue, which borrows Vue’s responsivity.
- When we create a store, we call
-
How does mapXxxx get the data and methods in store
- MapXxxx is just a syntax sugar, and the underlying implementation is also taken from $Store and returned to computed/Methods.
In general, you can think of Vue3. X as a mixin injected by each component?
Explore the principle of Vuex4
Removing redundant code is essential
createStore
- Start with createStore
- It can be found that state in VEX4 is created through reactive apis, and New Vue instance in Vex3
- The implementation of dispatch and COMMIT basically encapsulates a layer of execution, so don’t worry too much about it
export function createStore (options) {
return new Store(options)
}
class Store{
constructor (options = {}) {// omit some code...
this._modules = new ModuleCollection(options)
const state = this._modules.root.state
resetStoreState(this, state)
// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
// omit some code...}}function resetStoreState (store, state, hot) {
// omit some code...
store._state = reactive({
data: state
})
// omit some code...
}
Copy the code
install
- Vuex is used in Vue as a plug-in, and install is called at createApp
export function createAppAPI<HostElement> (render: RootRenderFunction, hydrate? : RootHydrateFunction) :CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
// omit some code....
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null._context: context,
version,
// omit some code....
use(plugin: Plugin, ... options:any[]) {
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)}else if(plugin && isFunction(plugin.install)) { installedPlugins.add(plugin) plugin.install(app, ... options) }else if(isFunction(plugin)) { installedPlugins.add(plugin) plugin(app, ... options) }else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`)}return app
},
// omit some code....}}Copy the code
- Store of install
- It is obtained by Inject
- Implement this.$store fetch
Let’s move on to the provide implementation
install (app, injectKey) {
// The implementation is obtained by inject
app.provide(injectKey || storeKey, this)
// Implement this.$store fetch
app.config.globalProperties.$store = this
}
Copy the code
App. Dojo.provide implementation
provide(key, value) {
// If it already exists, a warning is displayed
if (__DEV__ && (key as string | symbol) in context.provides) {
warn(
`App already provides property with key "The ${String(key)}". ` +
`It will be overwritten with the new value.`)}// Place store in context's provide
context.provides[key as string] = value
return app
}
// context Context is a context object
const context = createAppContext()
export function createAppContext() :AppContext {
return {
app: null as any.config: {
isNativeTag: NO,
performance: false.globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined.warnHandler: undefined.compilerOptions: {}},mixins: [].components: {},
directives: {},
provides: Object.create(null)}}Copy the code
Vue.useStore
- Use Vuex in the Vue3 Composition API
import { useStore } from 'vuex'
export default{
setup(){
conststore = useStore(); }}Copy the code
- The realization of the useStore
function useStore (key = null) {
returninject(key ! = =null ? key : storeKey)
}
Copy the code
Vue.inject
- Retrieve the store from the key saved while providing
- If there is a parent instance, the parent instance provides. If there is no parent instance, the root instance provides
function inject(
key: InjectionKey<any> | string, defaultValue? : unknown, treatDefaultAsFactory =false
) {
const instance = currentInstance || currentRenderingInstance
if (instance) {
If there is a parent instance, the parent instance provides. If there is no parent instance, the root instance provides
const provides =
instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
// Retrieve the store from the key saved while providing
if (provides && (key as string | symbol) in provides) {
return provides[key as string]
// omit some code......}}Copy the code
Vue.provide
- Vue’s provide API is also relatively simple, equivalent to assigning values directly through key/value
- The current instance provides and the parent instance provides are connected by a stereotype chain
function provide<T> (key: InjectionKey<T> | string | number, value: T) {
if(! currentInstance) {if (__DEV__) {
warn(`provide() can only be used inside setup().`)}}else {
let provides = currentInstance.provides
const parentProvides =
currentInstance.parent && currentInstance.parent.provides
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides)
}
// TS doesn't allow symbol as index type
provides[key as string] = value
}
}
Copy the code
injection
- Why does every component instance have a Store object?
- Provides is injected when the component instance is created
function createComponentInstance(vnode, parent, suspense) {
const type = vnode.type;
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
const instance = {
parent,
appContext,
// ...
provides: parent ? parent.provides : Object.create(appContext.provides),
// ...
}
// ...
return instance;
}
Copy the code
Provide, Inject, getCurrentInstance and other apis can be introduced from vue for library development/higher-order usage, which will not be described here.
Diff algorithm optimization
Before understanding the Diff algorithm optimization of Vue3, we can first understand the Diff algorithm of Vue2
This section focuses on clarifying the algorithm and will not do line-by-line source analysis
- The main optimization points in Vue3 are
- Double-end comparison -> longest increasing subsequence when updateChildren
- Full Diff -> Static tag + partial Diff
- Static ascension
updateChildren
- Vue2
- Head to head comparison
- Tail-to-tail comparison
- Head to tail comparison
- Tail-to-head comparison
- Vue3
- Head to head comparison
- Tail-to-tail comparison
- Move/remove/add based on the longest increasing subsequence
An 🌰
- oldChild [a,b,c,d,e,f,g]
- newChild [a,b,f,c,d,e,h,g]
- A head-to-head comparison is performed first, breaking out of the loop when different nodes are compared
- [a, b]
- A tail-to-tail comparison is then performed, breaking out of the loop when different nodes are compared
- [g]
- The remaining [f, c, d, e, h]
- Generate arrays with newIndexToOldIndexMap [5, 2, 3, 4, -1]
- The corresponding node of the longest increasing subsequence [2, 3, 4] is [C, D, e].
- The remaining nodes are moved/added/deleted based on [C, D, E]
The longest increasing subsequence reduces Dom element movement and minimizes Dom operations to reduce overhead.
The longest increasing subsequence algorithm can look at longest increasing subsequence
Static markup
Full Diff is performed for VDOM in Vue2, while static tags are added for incomplete Diff in Vue3
Vnodes are statically marked as in the following enumeration
- patchFlag
export enum PatchFlags{
TEXT = 1 , // Dynamic text node
CLASS = 1 << 1./ / 2 dynamic class
STYLE = 1 << 2./ / 4 dynamic style
PROPS = 1 << 3.//8 dynamic property, but does not contain class name and style
FULL_PROPS = 1 << 4.//16 has the dynamic key property. When the key changes, a complete diff comparison is required
HYDRATE_EVENTS = 1 << 5.//32 Nodes with listening events
STABLE_FRAGMENT = 1 << 6.//64 A fragment that does not change the order of child nodes
KEYED_FRAGMENT = 1 << 7.//128 Fragment with key attribute or partial element node with key
UNKEYEN_FRAGMENT = 1 << 8.//256 Fragment without key on the child node
NEED_PATCH = 1 << 9.//512 a node will only compare non-props
DYNAMIC_SLOTS = 1 << 10.Dynamic slot / / 1024
HOISTED = -1.// Static node
// Indicates to exit optimization mode during diff
BAIL = -2
}
Copy the code
An 🌰
- The template looks like this
<div>
<p>Hello World</p>
<p>{{msg}}</p>
</div>
Copy the code
- Generate VDOM source code
The MSG variable is tagged
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div".null, [
_createVNode("p".null."Hello World"),
_createVNode("p".null, _toDisplayString(_ctx.msg), 1 /* TEXT */)))}// Check the console for the AST
Copy the code
conclusion
- Vnodes are tagged to classify nodes that need to be dynamically updated and those that do not
- Static nodes only need to be created once, rendering is directly reused, and do not participate in the diff algorithm process.
Static ascension
-
Vue2 is recreated each time whether or not an element participates in an update
-
Vue3 elements that do not participate in updates are created only once and then reused repeatedly with each rendering
-
Instead of repeating the static content every time we render, we’ll just take the constants we created from the beginning.
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
/* * Static promotion before */
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div".null, [
_createVNode("p".null."Xmo"),
_createVNode("p".null."Xmo"),
_createVNode("p".null."Xmo"),
_createVNode("p".null, _toDisplayString(_ctx.msg), 1 /* TEXT */)))}/* * Static promotion after */
const _hoisted_1 = /*#__PURE__*/_createVNode("p".null."Xmo", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("p".null."Xmo", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("p".null."Xmo", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div".null, [
_hoisted_1,
_hoisted_2,
_hoisted_3,
_createVNode("p".null, _toDisplayString(_ctx.msg), 1 /* TEXT */)))}// Check the console for the AST
Copy the code
CacheHandlers Event listener cache
-
OnClick is considered dynamic binding by default, so it is tracked every time it changes
-
But because it is the same function, there is no trace of the change, and can be directly cached and reused.
/ / template
<div>
<button @click="onClick">btn</button>
</div>
// Before using cache
// The familiar static flag 8 /* PROPS */ is displayed,
// It marks the Props (properties) of the tag as a dynamic property.
// If we have an attribute that doesn't change and we don't want it to be marked as dynamic, then we need a cacheHandler.
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div".null, [
_createVNode("button", { onClick: _ctx.onClick }, "btn".8 /* PROPS */["onClick"]]))}// Check the console for the AST
// After using cache
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div".null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (. args) = >(_ctx.onClick(... args))) },"btn")))}// Check the console for the AST
Copy the code
The onClick method is cached.
When used, if the method can be found in the cache, it will be used directly.
If not, inject the method into the cache.
Anyway, it’s caching the method.
-
Nuggets: Front-end LeBron
-
Zhihu: Front-end LeBron
-
Continue to share technical blog posts, follow the wechat public account 👉🏻 front-end LeBron