带你从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,有一起小伙伴的@我哦。

相关推荐
优联前端20 分钟前
uni-app-通过vue-cli命令行快速上手
开发语言·前端·vue.js·uni-app·优联前端
极客先躯37 分钟前
高级java每日一道面试题-2024年9月15日-架构篇[分布式篇]-如何在分布式系统中实现事务?
java·数据库·分布式·面试·架构·事务·分布式篇
点燃银河尽头的篝火(●'◡'●)1 小时前
【BurpSuite】Cross-site scripting (XSS 学徒部分:1-9)
前端·web安全·网络安全·xss
Jiaberrr2 小时前
手把手教你:微信小程序实现语音留言功能
前端·微信小程序·小程序·语音·录音
熊猫在哪2 小时前
安装nuxt3
前端·nuxt.js
安冬的码畜日常2 小时前
【CSS in Depth 2 精译_036】5.6 Grid 网格布局中与对齐相关的属性 + 5.7本章小结
前端·css·css3·html5·网格布局·grid·css网格
啧不应该啊3 小时前
vue配置axios
前端·javascript·vue.js
__fuys__3 小时前
【HTML样式】加载动画专题 每周更新
前端·javascript·html
Want5953 小时前
HTML粉色烟花秀
前端·css·html
让开,我要吃人了3 小时前
HarmonyOS鸿蒙开发实战(5.0)自定义全局弹窗实践
前端·华为·移动开发·harmonyos·鸿蒙·鸿蒙系统·鸿蒙开发