带你从0开始了解vue3核心(运行时)

前置知识

运行时

  • h函数生成vnode。
  • render函数渲染vnode,生成真实的dom。

dom树

runtime-core 和 runtime-dom分开编写的原因: 不同的宿主环境使用不同的api。runtime-core只涉及运行时核心代码。暴露出统一的接口,让不同宿主环境定制化。

因为vue中在做一些标记计算时,大量使用到位运算,所以我们需要了解一些基础的 位运算

  • 左移 a << b: 表示a乘以2的b次方
  • 右移 a >> b: 表示a除以2的b次方,小于1的都为0。
js 复制代码
1 << 2
4
1 >> 2
0
1 >> 4
0
2 >> 4
0
js 复制代码
15 & 9 = 1111 & 1001 = 1001 = 9
  • 按位或 a | b: 表示a、b二进制对应位置只要有一位为1,就为1。然后再转为10进制。
js 复制代码
15 | 9 = 1111 | 1001 = 1111 = 15
  • 按位异或 a ^ b: 表示a、b二进制对应位置二进制都不一样。才为1。然后再转为10进制。
js 复制代码
15 | 9 = 1111 | 1001 = 0110 = 6
  • 按位非 ~a: 表示将a二进制对应位置反位。(0改成1,1改成0),然后转为10进制。 注意位运算符"非"将所有的 32 位取反,而值的最高位 (最左边的一位) 为 1 则表示负数 因为32位最后一位表示符号。 计算秘诀就是值 + 1取反(正数变负数,负数变正数)。
js 复制代码
// 我以为的
~15 = ~1111  = 0000 = 0
// -(原值 + 1)
~15 = -16

话说看完这些我已经有点晕了。记住>> 、<<、| 、 &即可。

h

h函数参数处理,调用createVNode函数

类似于适配器模式,主要是处理参数的差异,然后调用createVNode函数创建VNode。注意h函数不是只能传递三个参数,他会将三个参数往后的都作为children。

调用createVnode时,我们需要做了一些优化。

动态节点做标记 patchFlag

  • 标记区分不同的类型。
  • diff算法时,可以区分静态节点(不做处理),以及不同类型的动态节点。 vue2、3模板编译时进行优化对比

静态节点提升作用域 hoistStatic

  • 将静态节点的定义提升到父级作用域缓存起来。
  • 多个相邻的静态节点会被合并起来,作为一个静态的html模板

这种优化是一个典型的空间换时间的优化。

事件缓存 cacheHandler

标记节点类型shapeFlag(父节点和子节点)

createVNode,createBaseVNode基本逻辑,最终输出标准vnode

  • 处理type传入的类型,赋值,然后通过children的类型和type的类型按位或算出shapeFlag。
    • element + string children = 9
    • element + array chidlren = 17

第一次赋值区分字符串,组件,Text, Fragment, Comment, Suspense, Teleport等类型。其中Text, Comment, Fragment类型都是Vue内置的类型,都是Symbol类型。使用时需要导入。 第二次赋值区分children的类型值。

  • 标准化props(normalizeClass, normalizeStyle),处理class, style增强写法。
  • 标准化children(normalizeChidlren),然后赋值给VNode对象。

通过断点调试发现,vnode的生成是先执行children的h函数,再执行外部的h函数。

h函数的执行还是比较简单的,主要就是上面所讲到的问题,搞清楚就没啥问题了。

h函数代码实现

render

  • 判断是挂载还是更新。
  • 生成dom元素。
  • 生成dom元素内容。
  • 处理props,将其挂载到dom对应的属性上。(这里都标准化了在h函数阶段)。
  • 将dom元素插入到指定的根元素中。
  • 将vnode对象放在当前根元素之中。

看了一下源码,非常的复杂,暴露出一个createApp函数,回调了很多个函数。在源码实现的时候需要注意一下事项

  • 在组件挂载后,我们一定要将当前vnode赋值给挂载的根节点_vnode属性,他是后续作为oldVNode传递的。
js 复制代码
const render = (vnode: VNode, container) => {
    if (vnode == null) {
      // TODO: 卸载
    } else {
      // 打补丁patch / mount
      patch(container._vnode, vnode, container)
    }
    // 将当前vnode赋值给container._vnode作为旧节点
    container._vnode = vnode
  }
  • 在当前组件挂载后,我们需要将创建出来的dom元素赋值给vnode的el这样作为下次更新时,节点挂载的根对象。
js 复制代码
const mountElement = (vnode, container, anchor) => {
    const { type, props, shapeFlag } = vnode

    // 创建 element
    const el = (vnode.el = hostCreateElement(type))

    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // 设置 文本子节点
      hostSetElementText(el, vnode.children as string)
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 设置 Array 子节点
      mountChildren(vnode.children, el, anchor)
    }

    // 处理 props
    if (props) {
      // 遍历 props 对象
      for (const key in props) {
        hostPatchProp(el, key, null, props[key])
      }
    }

    // 插入 el 到指定的位置
    hostInsert(el, container, anchor)
  }
  • 我们在dom更新时,需要取出挂载时赋值给oldVNode.el的根对象,并赋值给newVNode.el,让其设置children和props时使用。
js 复制代码
 function patchElement(oldVNode, newVNode, anchor) {
    // 将根节点赋值给新VNode 用于比对后挂载,并且处理props
    const el = (newVNode.el = oldVNode.el!)

    const oldProps = oldVNode.props || EMPTY_OBJ
    const newProps = newVNode.props || EMPTY_OBJ

    // patch children
    patchChildren(oldVNode, newVNode, el, anchor)
    // patch props
    patchProps(el, newVNode, oldProps, newProps)
  }

挂载element + text children

  • path
  • 根据type和shapeFlag来决定怎么处理挂载, 这里是通过processElement
  • 根据是否有oldVnode,来决定是挂载mountElement 还是更新patchElement
  • 内部去创建元素 hostCreateElement , 创建text hostSetElementText , 再去处理props hostPatchProp , 最后是将元素插入到container中 hostInsert
js 复制代码
 const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    if (n1 === n2) {
      return
    }

    // 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,
          slotScopeIds,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) { // 挂载原生节点
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) { // 挂载组件节点
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

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

更新element + text children

  • patchElement
  • 内部进行children比较。patchChildren ,判断vNode第三个参数是否是数组还是字符串,分情况比较。然后再进行props的比较, patchProps
js 复制代码
 // children has 3 possibilities: text, array or no children.
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // text children fast path
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
      }
      if (c2 !== c1) {
        hostSetElementText(container, c2 as string)
      }
    } else {
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // prev children was array
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          // two arrays, cannot assume anything, do full diff
          patchKeyedChildren(
            c1 as VNode[],
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else {
          // no new children, just unmount old
          unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
        }
      } else {
        // prev children was text OR null
        // new children is array OR null
        if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
          hostSetElementText(container, '')
        }
        // mount new if array
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        }
      }
    }
  }

不同节点的element更新时

通过isSameVNodeType比较其类型和key值是否相同。不同就卸载旧节点。然后就将旧的vNode赋值为null。由于旧节点被设置为空,所以就将新节点挂载。

patchProps 非事件props的挂载

  • class。 直接通过el.className设置就可。

  • style。通过style.setProperty。并且会处理css兼容。autoPreFix。

    js 复制代码
    const prefixes = ['Webkit', 'Moz', 'ms']
  • value。通过el.value设置即可。

  • 其他属性。通过el.setAttribute()设置。

之所以区别挂载,是因为dom对象的属性和html元素的attr是不一样的概念。

在设置完新vNode的props后,它将再次遍历旧Vnode,删除新Vnode不存在的属性。

patchProps 事件props的挂载

这个和之前的属性不一样,这个会通过vei对象进行事件缓存。将事件缓存在vei.value中。对于事件的更新直接修改value就行,而不是通过调用addEventListener,removeEventListener频繁的创建和移除。

js 复制代码
    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) {
          // add
          const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
          addEventListener(el, name, invoker, options)
        } else if (existingInvoker) {
          // remove
          removeEventListener(el, name, existingInvoker, options)
          invokers[rawName] = undefined
        }
      }
    }

无状态组件挂载(element + text children 组件)

  • 初始化组件,createComponentInstance。组件实例中vnode属性绑定当前组件的vnode,vnode对象中的component属性绑定组件实例。
  • 初始化组件一系列数据。setupComponent
    • 有状态组件,执行setupStatefulComponent,如果有setup将被调用。如果没有setup我们将执行finishComponentSetup(给组件实例render赋初值)
  • setupRenderEffect,为组件实例effect属性赋值ReactiveEffect对象。这个对象将是以后触发状态更新时更新组件渲染视图的关键。 其中内部生成组件的vnode对象进行挂载。

无状态组件更新 (element + text children 组件)

其实就是卸载原来组件,重新挂载组件。(这是简单的分析来说的,如果同一个type并且key相同就需要diff了)

有状态组件挂载(element + text children 组件)

其他逻辑和无状态组件挂载一样,在初始化组件实例(setupComponent)时,需要处理状态数据,将其转换成响应式数据。

有状态组件更新(element + text children 组件)

其实就是卸载原来组件,重新挂载组件。(这是简单的分析来说的,如果同一个type并且key相同就需要diff了)

生命周期

setupComponent中直接执行

  • beforeCreate是在options选项处理之前调用的。
  • created 是在一些数据处理options处理完毕后执行的。

通过注册,等待时机执行

  • beforeMount,mounted 是在组件更新时触发的。
  • beforeUpdate,updated 是在组件更新时触发的。

setup函数挂载与更新

本质上就是在setupComponent(初始化组件实例属性)中判断是否有setup,如果有将执行并将setup返回值作为组件实例render属性的值。

运行时源码简单实现

往期文章

专栏文章

最近也在学习nestjs,有一起小伙伴的@我哦。

相关推荐
再吃一根胡萝卜2 小时前
08 · 前端:Vue3 + SSE 流式与执行链路可视化
面试
再吃一根胡萝卜2 小时前
03 · 后端:FastAPI 与分层架构
面试
mldong3 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排4 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角6 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!6 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰6 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
Interview Aid1126 小时前
Walmart Global Tech SDE 三轮面经|基础、并发、压力面
面试·职场和发展
mayaairi7 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js