preface

Following the interview article, vuE2 interview questions including source level answer, this talk about vuE3 common interview questions and answers.

The level is limited, there are wrong, I hope you pointed out, or there are other interview questions to understand, welcome to come out, I study synchronous in this article.

Personal finishing front-end advanced knowledge website, welcome to pay attention to: warehouse address: github.com/chen-junyi/… Website address: Chen-junyi.github. IO /article/, domestic visit junyi-chen-.giitee. IO /article/

1. What are the differences between VUe3 and VUe2

Major changes:

  • Proxy replaces Object.definprototety responsive system
  • Ts instead of flow type checking
  • The directory structure is reconstructed, and the code is divided into three independent modules, which is more convenient for long-term maintenance
  • Rewrite VDOM to optimize compilation performance
  • Support the tree shaking
  • Added composition API (Setup) to make code easier to maintain

Minor changes:

  • Asynchronous components need the defineAsyncComponent method to create them
  • V – the usage model
  • V-if has a higher priority than V-for
  • The Destroyed lifecycle option is renamed unmounted
  • The beforeDestroy lifecycle option is renamed to beforeUnmount
  • Render function default createElement removed to global import
  • Component events now need to be declared in the emits option

New features:

  • Modular API
  • Teleport
  • Frashable (Enabling multiple root joints for components)
  • CreateRenderer (cross-platform custom renderer)

The v3 migration guide on the official website is recommended

2. What aspects does VUE3 improve performance

The performance was improved by rewriting responsive system, optimizing compilation and optimizing source volume (loading on demand).

1. Responsive system upgrade

When vue2 is initialized, Object. DefineProperty intercepts the access and modification of each property of data, getter collects dependencies, and setter dispatches updates. We also need to recursively call defineProperty when the property value is an object. Take a look at the code:

function observe(target) { if (target && typeof target === "Object") { Object.keys(target).forEach((key) => { defineReactive(target, key, target[key]) }) } } function defineReactive(obj, key, val) { const dep = new Dep(); Object.defineproperty (obj, key, {get() {return val}, set(v) {val = v dep.notify(); }})}Copy the code

If the property is an array, we also need to override the array’s seven methods (which change the original array’s seven methods) to notify the change:

const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { const original = arrayProto[method] def(arrayMethods, method, function mutator (... args) { const result = original.apply(this, args) const ob = this.__ob__ ob.dep.notify() return result }) })Copy the code

There are several disadvantages of Object.defineProperty that can be seen from these sections of code:

  • During initialization, all keys of an object need to be traversed. In the case of multiple levels, performance may be affected
  • Dynamic new and deleted object attributes cannot be intercepted and can only be replaced by the SET /delete API
  • New data structures such as Maps and sets are not supported
  • Unable to monitor array subscript changes (listening is too costly in performance)

So vue3 uses a proxy to completely replace Object.defineProperty with a responsive system. Proxy is a relatively new browser feature that intercepts the whole object rather than the attributes of the object. It can intercept a variety of methods, including the access, assignment and deletion of attributes. It does not need to traverse all attributes during initialization. Object properties are recursively proxyed when they are accessed, so performance is significantly better than vue2.

Summarize the advantages of proxy:

  • You can listen for a variety of actions, including dynamically added and deleted properties, has, apply, and so on
  • You can listen for properties such as index and length of an array
  • Lazy execution, no need for initialization when recursive traversal
  • New browser standards, better performance, and the possibility of continuous optimization

Take a look at some of the ways to implement intercepting objects.

export function reactive(target: object) {
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  const proxy = new Proxy(
    target,
    baseHandlers
  )
  proxyMap.set(target, proxy) // Use weakMap to collect
  return proxy
}
Copy the code

2. Compilation optimization (virtual DOM optimization)

Compilation optimization is done primarily by rewriting the virtual DOM. Optimized points include static markup of compiled templates, static promotion, and event caching

  • Static flag (PatchFlag)

According to UBC live, the performance of update is 1.3 times better, and SSR is 2 or 3 times better. When comparing updated nodes, only nodes with static tags are compared. Moreover, the PatchFlag enumeration defines more than ten types to more accurately locate the types of nodes to be compared.

Look at this code

< div id = "app" > < p > the front-end fun < / p > < div > {{message}} < / div > < / div >Copy the code

Vue2 compiled rendering functions:

function render() { with(this) { return _c('div', { attrs: { "id": "App"}} [_c (' p ', [_v (" front-end fun ")]), _c (' div ', [_v (_s (the message))])])}}Copy the code

This render function will return vnode, later update vue2 will adjust the patch function compared to the old Vnode diff algorithm update (in my last article parsing), this time the comparison is the entire Vnode, including the static node

front end fun

, so there will be a certain performance loss.

Vue3 compiled rendering functions:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue" export function render(_ctx, _cache) { return (_openBlock(), _createBlock("div", { id: "App"}, [_createVNode("p", null, "have a good time "), _createVNode("div", NULL, _toDisplayString(_ctx.message), 1 /* TEXT */) ])) }Copy the code

Only the _createVNode function with a fourth argument is a non-static node, which requires subsequent diff. The fourth argument is that the node contains the type to be diff, such as the text node, only {{}} template variable binding, the text can be used to define enumeration elements in the source code.

TEXT = 1,// dynamic TEXT node CLASS = 1 << 1,// 2, dynamic CLASS node STYLE = 1 << 2, // 4, STABLE_FRAGMENT = 1 << 6, FULL_PROPS = 1 << 4, HYDRATE_EVENTS = 1 << 5, STABLE_FRAGMENT = 1 << 6, UNKEYED_FRAGMENT = 1 << 8, UNKEYED_FRAGMENT = 1 << 8, // 256 child nodes have no key NEED_PATCH = 1 << 9, // 512 DYNAMIC_SLOTS = 1 << 10, // dynamic slots HOISTED = -1, // Static promoted flags will not be diff, The following static upgrade will say BAIL = -2 //Copy the code

// bit operation, there is a sign right shift operator, do not understand can see my nuggets first article juejin.cn/post/688518…

  • Static ascension

Static promotion means taking some variables out of a function so that the function is not redeclared when it is executed again. Vue3 makes this optimization at compile time. Let’s look at the differences between vue2 and VUe3

vue2:

function render() { with(this) { return _c('div', { attrs: { "id": "App"}} [_c (' p ', [_v (" front-end fun ")]), _c (' div ', [_v (_s (the message))])])}}Copy the code

vue3:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue" const _hoisted_1 = { id: "App"} const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "front-end ", -1 /* HOISTED */) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", _hoisted_1, [ _hoisted_2, _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */) ])) }Copy the code

Vue3 takes the invariant node declaration out of the loop and goes directly to the _hoited variable when rendering, whereas VUe2 requires _c every time it renders to generate a new node. There is also a dot, the fourth parameter -1 of _hoisted_2’s _createVNode, indicating that this node never needs diff.

  • Event caching

By default, events are considered dynamic variables, so they are tracked every time the view is updated. Normally, however, our @click event is the same event before and after rendering the view, so we don’t need to track its changes, so Vue 3.0 has made an optimization for this called event listener caching

< div id = "app" > < p = "handleClick" > @ click front-end fun < / p > < / div >Copy the code

Vue3 after compilation:

import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue" const _hoisted_1 = { id: "app" } export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", _hoisted_1, [ _createVNode("p", { onClick: _cache [1] | | (_cache [1] = (... the args) = > (_ctx. HandleClick && _ctx. HandleClick args (...)))}, "front-end fun") ")}Copy the code

As you can see, onClick has a _cache that determines the cache assignment and thus becomes a static node

3. Optimization of source volume

Vue3 supports tree shaking by refactoring both global and internal apis. Every function, such as REF, Reavtived, computed, etc., is packaged only when it is used, unused modules are removed and the overall size of the package becomes smaller

3. Introduce the Composition API

Composition API is one of the most important features of VUe3. It is for better logic reuse and code organization, and it solves the problem that options API is not easy to split and reuse in large projects.

The Composition API is declared in the setup function, which is executed before the component is created, meaning that the component instance has not yet been created, so there is no this in the Setup option.

Setup takes props and context. Props is passed by the parent component and is inherently reactive. Context is a common object that contains attributes attrs, slots, and emit. Setup returns values that can be accessed in templates and other options, as well as rendering functions.

Vue2 processes data into reactive data. In VUe3, reactive data is defined by reactive and REF functions. One advantage of this is that the data bound to the template does not necessarily need to be reactive. Vue3 requires reactive data by user decision, whereas vuE2 can only use variables in the template by declaring them in data, resulting in a performance waste.

Since setup is executed before the component is created, accessing the component instance or lifecycle is done by introducing vUE functions such as getCurrentInstance, onMounted, etc. This is the way of functional programming, and it is easier to split the code logic. No more mixins to mix options.

With this feature, you can extract some of the reused code as a function, as long as you call it directly where it is used. It is very flexible.

import { toRefs, reactive, onUnmounted, onMounted } from 'vue';
function useMouse(){
    const state = reactive({x:0,y:0});
    const update = e=>{
        state.x = e.pageX;
        state.y = e.pageY;
    }
    onMounted(()=>{
        window.addEventListener('mousemove',update);
    })
    onUnmounted(()=>{
        window.removeEventListener('mousemove',update);
    })

    return toRefs(state);
}
Copy the code

Component use:

import useMousePosition from './mouse'
export default {
    setup() {
        const { x, y } = useMousePosition()
        return { x, y }
    }
}
Copy the code

SetupComponent setupStatefulComponent setupStatefulComponent setupStatefulComponent

function setupComponent( instance: ComponentInternalInstance, isSSR = false ) { isInSSRComponentSetup = isSSR const { props, children, shapeFlag } = instance.vnode const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT initProps(instance, props, isStateful, isSSR) initSlots(instance, children) const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : } function setupStatefulComponent(undefined isInSSRComponentSetup = false) instance: ComponentInternalInstance, isSSR: boolean ) { const Component = instance.type as ComponentOptions if (__DEV__) { if (Component.name) { validateComponentName(Component.name, instance.appContext.config) } if (Component.components) { const names = Object.keys(Component.components) for (let i = 0; i < names.length; i++) { validateComponentName(names[i], instance.appContext.config) } } if (Component.directives) { const names = Object.keys(Component.directives) for (let i = 0; i < names.length; i++) { validateDirectiveName(names[i]) } } } // 0. create render proxy property access cache instance.accessCache = Object.create(null) // 1. create public instance / render proxy // also mark it raw so it's never observed instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers) if (__DEV__) { exposePropsOnRenderContext(instance) } // 2. call setup() const { setup } = Component // Setup handler if (setup) {const setupContext = (instance.setupContext = setup.length > 1? createSetupContext(instance) : null) currentInstance = instance pauseTracking() const setupResult = callWithErrorHandling( setup, instance, ErrorCodes.SETUP_FUNCTION, [__DEV__ ? shallowReadonly(instance.props) : instance.props, SetupContext]) resetTracking() currentInstance = NULL else {finishComponentSetup(instance, isSSR)}}Copy the code

This function executes the setup option and does error handling.

Function callWithErrorHandling (fn: function, / / the fn is setup options for instance: ComponentInternalInstance | null, type: ErrorTypes, args? : unknown[] ) { let res try { res = args ? fn(... args) : fn() } catch (err) { handleError(err, instance, type) } return res }Copy the code

HandleSetupResult to determine whether the return value of setup is valid, finishComponentSetup complete setup processing, see finishComponentSetup function:

function finishComponentSetup( instance: ComponentInternalInstance, isSSR: boolean ) { const Component = instance.type as ComponentOptions // template / render function normalization if (__NODE_JS__ && isSSR) { if (Component.render) { instance.render = Component.render as InternalRenderFunction } } else if (! instance.render) { // could be set from setup() if (compile && Component.template && ! Component.render) { if (__DEV__) { startMeasure(instance, `compile`) } Component.render = compile(Component.template, { isCustomElement: instance.appContext.config.isCustomElement, delimiters: Component.delimiters }) if (__DEV__) { endMeasure(instance, `compile`) } } instance.render = (Component.render || NOOP) as InternalRenderFunction if (instance.render._rc) { instance.withProxy = new Proxy( instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers ) } } // support for 2.x options if (__FEATURE_OPTIONS_API__) { currentInstance = instance applyOptions(instance, Component) currentInstance = null } ... }Copy the code

This function binds the Render function to the current instance, and then calls the applyOptions function to handle and call the lifecycle hooks for options like data, computed, and watch outside setup. So you can conclude that the data options and other life cycles are not accessible in Setup.

4. Responsive implementation of VUe3

As mentioned earlier, vue3’s responsiveness is implemented by proxy in the source code’s/Packages /reactivity directory.

The process of the whole responsive system is as follows:

Reactive (get, set, deleteProperty, has, ownKeys, etc.)

2. Effect is used to declare a function cb that depends on reactive data (such as the render function) and execute the cb function, which triggers the getter for reactive data

3. Collect track dependencies in the getter of responsive data: store the mapping between responsive data and update function CB and store it in targetMap

4. When the responsive data is changed, trigger will be triggered to find the associated CB according to targetMap and execute it

Through the source code to see the implementation of these key functions:

reactive

/packages/reactivity/reactive:

function reactive(target: Object) {// If you try to observe a read-only proxy, If (target && (target as target)[reactiveFlags.is_readOnly]) {return target} return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap ) } function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any>) {// If (! isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)} ')} return target if (target [reactiveflags.raw] &&! (isReadonly &&target [reactiveflags.is_reactive]) {return target} Const existingProxy = proxymap. get(target) if (existingProxy) {return existingProxy} // Only whitelist-type objects that can be proxyed can be proxyed. Const targetType = getTargetType(target) if (targetType === targetType.invalid) {return target} Const proxy = new proxy (target, targetType === targetType.collection? Proxymap.set (target, proxy) return proxy}Copy the code

Reactive calls createReactiveObject to generate a responsive object that does different things to target and to proxy handlers that use baseHandlers that are passed in, mutableHandlers by default. This method imports from reactivity/baseHandlers:

mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {...// Perform special read values on arrays
    const targetIsArray = isArray(target)

    if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)
    
    // track relies on collection
    if(! isReadonly) { track(target, TrackOpTypes.GET, key) } ...// If the value read is an object, call reactive recursively to make it a reactive object
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) :boolean {
    let oldValue = (target as any)[key]
 
    ...
    
    // Determine whether to add or delete attributes
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if(! hadKey) {// trigger the update function
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
Copy the code

The mutableHandlers do things like GET, set, deleteProperty, so just get and set. Track dependency collection will be carried out during GET. If the property value of GET is an object, recursive response processing will be carried out, and set will trigger update.

track

function track(target: object.type: TrackOpTypes, key: unknown) {
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  // Get the dependency table for target
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Get the set of response functions corresponding to key
  let dep = depsMap.get(key)
  if(! dep) {// Dynamically create dependencies
    depsMap.set(key, (dep = new Set()))}ActiveEffect temporary variables, getters trigger dependent callback functions, which may be side effects generated by render or effect
  if(! dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep)if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}
Copy the code

When track relies on collection, it first judges whether targetMap has the object to be accessed. TargetMap is a weakMap structure with the format of {target: {key: [fn1,fn2]}}, target is the key of weakMap, value is a map type, key is the accessed target attribute, and value is the corresponding callback function set of this attribute. At the end there is an activeEffect judgment, which depends on the collected side effect function, which may be generated temporarily by FFect or in the render function.

trigger

function trigger(
  target: object.type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
  // Get the attribute mapping set for the target that triggered the update
  const depsMap = targetMap.get(target)
  if(! depsMap) {// never been tracked
    return
  }

  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }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
    // Do different callback processing depending on the type of operation triggered
    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}}const run = (effect: ReactiveEffect) = > {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  // Execute all set of callback functions
  effects.forEach(run)
}
Copy the code

Trigger triggers an update that finds the set of attribute dependencies corresponding to the target based on the targetsMap, then finds the set of callback functions based on the key, and then executes all callback functions based on the operation type.

effect

// effect stack, which holds all effect side effects
const effectStack: ReactiveEffect[] = []
function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if(! options.lazy) { effect() }return effect
}
function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
  const effect = function reactiveEffect() :unknown {
    if(! effect.active) {return options.scheduler ? undefined : fn()
    }
    // Whether effectStack has the currently executing side effect function
    if(! effectStack.includes(effect)) { cleanup(effect)try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]}}}asReactiveEffect effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect =true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}
Copy the code

EffectStack is an array of stack structures. The effect function is added to the effectStack, and the activeEffect function is temporarily assigned to the currently executing effect function. The effect function is added to the key callback function of the responsive data when used for track. Effect is executed and then activeEffect is assigned back to the end of the original effectStack.

5. What is the difference between vue3 hook and React hook

There is no doubt that Vue3 hook borrows from react Hook idea. The writing method of custom hook in VUe3 looks similar to React, but the actual use is slightly different, and the internal implementation principle is completely different.

First, there are two limitations of React Hook:

  1. Use hooks only at the top level.Do not call hooks in loops, conditions, or nested functions
  2. Only the React function calls the Hook.Never call a Hook in a normal JavaScript function

This is also covered on the React website.

Hook can only be used at the top level. This is because React hooks rely on the call order to confirm the corresponding Hook state. Hook will be called again every time it is re-rendered, so it is necessary to ensure that the call order of Hook will not change.

React uses vue differently than React.

  1. Setup is executed once, while React re-executes the hook every time it renders
  2. When a Hook needs to update a value, Vue assigns the value directly. React calls the Hook assignment function
  3. Call order is not required and can be placed in conditional statements

Different implementation principles:

Hooks in Vue are responsive objects that will be collected when read in render.

A hook in React is essentially a function that needs to be called again every time it is rerendered. It is stored in the linked list of {value1, setValue1} -> {value2, setValue2} in the order in which it is called. Therefore, it is necessary to strictly limit the execution order of hooks and forbid conditional calls.

6. The DOM diff of vue3 is different from that of React

Vdom is optimized for static nodes, static boosts, and event caching. React doesn’t do this.

React uses the vdom tree as a linked list to make use of the browser’s idle time to do diff. In other words, the concept of time slicing is that if the browser has animation or user interaction tasks over 16ms, the main process control is returned to the browser and diff continues after the idle time. The browser API implementation is requestIdleCallback.