vue3. Data response of X source code analysis

Posted by jcstanley on Thu, 06 Jan 2022 03:27:28 +0100

preface

If you miss autumn maple and winter snow, the cherry blossoms in spring will be in full bloom. Recently, I have been preparing for my own exam. After the exam, I can finally continue to study the source code and write articles. Hahaha. Those who have studied Vue know that data response is extremely important in Vue framework. Data response is the core content, whether writing code or interviewing. In the official website document of vue3, the author said that if you want to make the data more responsive, you can put the data in reactive. The official document mentioned here, and the author didn't understand it at first. Later, after reading the source code, I knew that in vue3, the response has become a separate module, and the module dealing with the response is reactive;

General flow of data response

In vue3 0, the core file we need to find is vue3 0 in the src under the runtime core in the packages of the source code; The line we are studying today goes along the render line;

 return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }

Find the render function under this file, as shown below; This function is used to render the incoming vnode to the specified container;

 const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }

Check the patch method, and else if (shapeflag & shapeflags. Component) will be used for initialization

 const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
  ) => {
    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          // Initialize this
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized,
            internals
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized,
            internals
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
    }
  }

Next, look at the processComponent method, and then go to the familiar mountComponent

 const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        // Initialize the mount process
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

Enter the mountComponent method, where the more important instance is to create a component instance and setupComponent is prepared for installing components; For option processing; Setuprenderexffec is used to establish the side effects of rendering functions and is used when relying on collection.

const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // Create component instance
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    if (__DEV__ && instance.type.__hmrId) {
      registerHMR(instance)
    }

    if (__DEV__) {
      pushWarningContext(initialVNode)
      startMeasure(instance, `mount`)
    }

    // inject renderer internals for keepAlive
    if (isKeepAlive(initialVNode)) {
      ;(instance.ctx as KeepAliveContext).renderer = internals
    }

    // resolve props and slots for setup context
    if (__DEV__) {
      startMeasure(instance, `init`)
    }
    // Installing components: options handling
    setupComponent(instance)
    if (__DEV__) {
      endMeasure(instance, `init`)
    }

    // setup() is async. This component relies on async logic to be resolved
    // before proceeding
    if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
      parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

      // Give it a placeholder if this is not hydration
      // TODO handle self-defined fallback
      if (!initialVNode.el) {
        const placeholder = (instance.subTree = createVNode(Comment))
        processCommentNode(null, placeholder, container!, anchor)
      }
      return
    }

    // Side effects of creating rendering functions: dependency collection
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

    if (__DEV__) {
      popWarningContext()
      endMeasure(instance, `mount`)
    }
  }

Go to the setupComponent function and watch the internal logic of the setupComponent function, where there is the initialization of the attribute slot; Here, you can see the setupStatefulComponent method, which is used to handle the response type.

export 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)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}

Enter the method setupStatefulComponent, where const component = instance Type as componentoptions is used for component configuration. Where instance Proxy = new proxy (instance.ctx, publicinstanceproxyhandlers) is used for proxy. data, $etc. are processed here.

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  // Component configuration
  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 = {}
  // 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
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    currentInstance = instance
    pauseTracking()
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    resetTracking()
    currentInstance = null

    if (isPromise(setupResult)) {
      if (isSSR) {
        // return the promise so server-renderer can wait on it
        return setupResult.then((resolvedResult: unknown) => {
          handleSetupResult(instance, resolvedResult, isSSR)
        })
      } else if (__FEATURE_SUSPENSE__) {
        // async setup returned Promise.
        // bail here and wait for re-entry.
        instance.asyncDep = setupResult
      } else if (__DEV__) {
        warn(
          `setup() returned a Promise, but the version of Vue you are using ` +
            `does not support it yet.`
        )
      }
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    // Processing options and other transactions
    finishComponentSetup(instance, isSSR)
  }
}

Since there is no setup in our case, we will execute finish component setup (instance, isssr) to handle things related to the optional API. Enter this function to view the code logic, and you will see the following code. This part of the code is used to handle things related to the optional API and to support vue2 Version of X.

  // support for 2.x options
  // Support options API
  if (__FEATURE_OPTIONS_API__) {
    currentInstance = instance
    applyOptions(instance, Component)
    currentInstance = null
  }

Enter the applyOptions method; Turn down and you will see these lines of notes, which clearly explain vue2 The priority of each option in X, including props, inject, methods, data, etc.

  // options initialization order (to be consistent with Vue 2):
  // - props (already done outside of this function)
  // - inject
  // - methods
  // - data (deferred since it relies on `this` access)
  // - computed
  // - watch (deferred since it relies on `this` access)

If you continue to look down, you will see these lines of code. We don't use the mixed form, so this line of code, including the code related to the data corresponding formula, is in the resolveData method.
if (!asMixin) {

  if (!asMixin) {
    if (deferredData.length) {
      deferredData.forEach(dataFn => resolveData(instance, dataFn, publicThis))
    }
    if (dataOptions) {
      // Data response
      resolveData(instance, dataOptions, publicThis)
    }

Enter resolveData and you can see const data = datafn Call (publicthis, publicthis), this line of code is used to get the data object. instance. The line data = reactive (data) is used for responsive processing of data. The core is reactive, which is used for responsive processing. Either the optional api or the setup, the final method is the reactive method, which is used for responsive processing.

function resolveData(
  instance: ComponentInternalInstance,
  dataFn: DataFn,
  publicThis: ComponentPublicInstance
) {
  if (__DEV__ && !isFunction(dataFn)) {
    warn(
      `The data option must be a function. ` +
        `Plain object usage is no longer supported.`
    )
  }
  // Get data object
  const data = dataFn.call(publicThis, publicThis)
  if (__DEV__ && isPromise(data)) {
    warn(
      `data() returned a Promise - note data() cannot be async; If you ` +
        `intend to perform data fetching before component renders, use ` +
        `async setup() + <Suspense>.`
    )
  }
  if (!isObject(data)) {
    __DEV__ && warn(`data() should return an object.`)
  } else if (instance.data === EMPTY_OBJ) {
    // Responsive processing of data
    instance.data = reactive(data)
  } else {
    // existing data: this is a mixin or extends.
    extend(instance.data, data)
  }
}

Enter reactive and observe the code logic; The createReactiveObject is used to process data. Among them, target is the final thing to be transformed.

  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )

mutableHandlers contains some get, set, deleteProperty and other methods. mutableCollectionHandlers creates operations such as dependency collection.

vue2.x data response and 3 X-response comparison

Here, let's review vue2 How x handles responsive. It uses defineReactive to intercept each key to detect data changes. This set of processing method is problematic. When the data is nested one layer at a time, it will recurse layer by layer, thus consuming a lot of memory. From this point of view, this set of treatment is not friendly. Vue3 also uses defineReactive to intercept each key. The difference is that in vue3 The defineReactive in X uses proxy as a layer of proxy, which is equivalent to adding a layer of level. Vue2. X needs to recurse all keys of the object, which is slow. Array responses require additional implementation. Moreover, adding or deleting attributes cannot be monitored, and special APIs need to be used. Now, a new proxy directly solves all the problems. At the same time, the previous set of methods did not know the data structures such as Map,Set and Class.

General flow chart

Then let's sort out the sequence of the response process

Implement dependency collection

In the process of implementing responsive, dependency collection is closely related to it. In setuprenderexfect function, effect function is used for dependency collection. Enter the inside of the setuprenderexfect function. There is this function in the above code. We won't repeat it one by one here. Let's continue to look at it. Enter the function and you will see the following code. Effect can establish a dependency relationship: between the callback function of the incoming effect and the responsive data; Effect is equivalent to dep in vue2, and there is no watcher in vue3.

  instance.update = effect(function componentEffect() {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, parent } = instance

If you continue to look down, you will see the following code. subTree is the current component vnode, and the renderComponentRoot method is used to implement the root of the rendering component.

        const subTree = (instance.subTree = renderComponentRoot(instance))

Here, vue3 The responsive part of 0 is coming to an end

Code warehouse

Handwritten vue3 0 simple version of the implementation of data response, has been uploaded to the personal warehouse, interested can see. If you like, you can pay attention, hahaha. Pay attention to me, you have one more friend on the road of programming. https://gitee.com/zhang-shichuang/xiangyingshi/tree/master/

ending

vue's data response is often asked during the interview. The principle is to look at the source code. There will inevitably be boring times when reading the source code, but persistence is victory. Later, we will share the compilation process of vue and the source code knowledge related to react.

Topics: Front-end