Vue3源码梳理:运行时的设计方案概况

发布时间:2023年12月18日

关于运行时和demo简单示例

  • 运行时,简单理解,就是把vnode渲染到页面中

    <div id='app'></div>
    <script>
      const { render, h } = Vue
    
      const vnode = h('div', {
        class: 'test',
      }, 'hello render')
    
      const container = document.querySelector('#app')
      render(vnode, container)
    </script>
    
  • 整个runtime包含两个环节

    • 1.利用h函数生成vnode
    • 2.利用render函数把vnode渲染到指定位置
  • 所以,我们的目标是

    • 理解vnode作用,为何要创建vnode
    • 创建vnode参数是干嘛的,为何要传递这些参数
  • 在理解这些之前, 我们需要了解

    • HTML DOM 节点树 与 虚拟 DOM 树
    • 这两者的区别

HTML DOM 节点树 与 虚拟 DOM 树

1 )两个概念

  • html dom 节点树
  • 虚拟 dom 树
<div>
  <h1> hello h1</h1>
  <!-- 哈哈 -->
  hello div
</div>
  • 浏览器会把它们通过一个dom树来表示
  • dom树的解释:https://zh.javascript.info/dom-nodes
  • 上述dom树的示例包含
    • 标签点击、注释节点、文本节点

2 )关于虚拟DOM

  • 官方关于虚拟dom的解释:https://cn.vuejs.org/guide/extras/rendering-mechanism.html#virtual-dom
    • 虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念
    • 意为将目标所需的 UI 通过数据结构“虚拟”地表示出来
    • 保存在内存中,然后将真实的 DOM 与之保持同步
    • 这个概念是由 React 率先开拓,随后在许多不同的框架中都有不同的实现,当然也包括 Vue。
  • 虚拟dom是一种理念,期望通过js对象描述一个div节点
  • 所以说,与其说虚拟 DOM 是一种具体的技术,不如说是一种模式,所以并没有一个标准的实现

3 ) 区别示例

拿文本节点来说

html dom 节点树表示

<div>text</div>

虚拟dom表示

const vnode = {
  type: 'div',
  children: 'text'
}

总结

  • 在运行时 runtime ,渲染器 renderer 会遍历整个虚拟dom树,并据此结构构建真实的dom树
  • 这个过程我们可以把它叫做挂载 mount
  • 在这个 vnode 对象发生变化时候,我们会对比 旧的 VNode 和 新的 VNode 之间的区别
  • 找出它们之间的区别,并应用这其中的变化到真实的dom上,这个过程叫做更新 patch

关于挂载和更新

简化版的demo

<div id='app'></div>
<script>
  // <div>hello render</div>
  const vnode = {
    type: 'div',
    children: 'hello render'
  }

  const vnode2 = {
    type: 'div',
    children: 'hello patch'
  }
  
  function render(oldVNode, newVNode, container) {
    // 第一次属于挂载,old不存在
    if(!oldVNode) {
      mount(newVNode, container)
    } else {
      patch(oldVNode, newVNode, container)
    }
  }

  // 挂载方法
  function mount(vnode, container) {
    const ele = document.createElement(vnode.type) // 1. 创建当前节点
    ele.innerText = vnode.children // 2.插入具体节点
    container.appendChild(ele) // 3. 将创建的节点存放到容器中
  } 

  // 卸载操作
  function unmount(container) {
    container.innerHTML = ''
  }
  
  // 更新操作
  function patch(oldVNode, newVNode, container) {
    // 1. 卸载
    unmount(container)
    // 2. 重新渲染
    const ele = document.createElement(newVNode.type)
    ele.innerText = newVNode.children
    container.appendChild(ele)
  }
  
  // 初始化时去挂载
  render(null, vnode, document.querySelector('#app'))

  // 延迟两秒进行更新
  setTimeout(() => {
    render(vnode, vnode2, document.querySelector('#app'))
  }, 2000)
</script>
  • 以上是挂载、更新的逻辑,是一个精简版的更新操作
  • vue本质上也是这类操作(删除旧节点,挂载新节点),但是性能更优,实现更复杂

h函数 和 render函数

在vue中的vnode对象实际上属性很多,我们精简一下

{
  // 是否是一个vnode对象
  "__v_isVNode": true,
  // 当前节点类型
  "type": "div",
  // 当前节点的属性
  "props": {"class": "test"}
  // 它的子节点
  "children": "hello render"
}
  • h函数本质上就是一个生成vnode的函数
  • https://cn.vuejs.org/api/render-function.html#h

官方示例

import { h } from 'vue'

// 除了 type 外,其他参数都是可选的
h('div')
h('div', { id: 'foo' })

// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { class: 'bar', innerHTML: 'hello' })

// class 与 style 可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })

// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')

// 没有 prop 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])

// children 数组可以同时包含 vnode 和字符串
h('div', ['hello', h('span', 'hello')])
  • 官方文档上提供了各种各样的使用方式
  • 注意,除了 type 外,其他参数都是可选的
  • h 函数最多可接收三个参数
    • type: string | Component: 既可以是字符串(原生标记),也可以是一个Vue组件的定义
    • props?: object | null: 要传递的 prop
    • children?: Children | Slot | Slots: 子节点

2 ) render 函数

  • https://cn.vuejs.org/api/options-rendering.html#render

    render(vnode, container)
    
  • vnode 虚拟dom树

  • container: 承载的容器,真实节点的渲染节点位置

  • 通过render函数,我们可以通过编程形式来把虚拟dom转换成真实dom挂载到指定的容器盒子上

核心设计原则


1 ) 概述

  • vue源码中包含两块
    • runtime-core
    • runtime-dom
  • vue为什么要这么划分
    • 为什么不像是reactivity,都组织到一起
  • vue挂载和更新的逻辑处理是什么

2 ) vue为何分开设计

  • runtime-core 是运行时核心代码

    • 只放核心逻辑,不会放置宿主环境下的相关操作
    • 当当前的vue需要在浏览器端运行时,它就可以把操作dom的一些逻辑作为参数传递到render里面
    • 比如在 baseCreateRenderer 方法中的options,里面可以解构出很多 API
    • 这些 API 都是宿主环境传过来的函数
    • 假如当前 Vue 需要在浏览器上渲染,就把自己的一些 API 传递过来,类似于接口对接的方式,满足不同宿主平台的调用
    • 以此来满足不同平台的不同挂载渲染场景
  • runtime-dom 是浏览器渲染的核心逻辑,多是一个浏览器相关基本操作

    • 这里的很多API都会被作为参数,传递到 runtime-core 包中使用
    • 实现了 渲染 和 宿主平台 两者的解耦
    • 这里宿主平台可以是浏览器,可以是类浏览器环境
    • 一般基于vue渲染都是spa,服务端渲染 ssr
    • 除了这些渲染情况,还有weex, uniapp等都会用到vue的渲染服务
  • 所以

    • 分包的原因是:
      • 针对不同的宿主环境使用不同的API
  • 挂载和更新的逻辑处理

    • baseCreateRenderer 最终返回了一个对象 {render, hydrate, createApp},这个对象里有 render 函数

    • render函数有三个参数: vnode, container, isSVG 这第三个参数不用管

      const render: RootRenderFunction = (vnode, container, isSVG) => {
        // 不存在vnode
        if (vnode == null) {
          if (container._vnode) {
            unmount(container._vnode, null, null, true)
          }
        } else {
          // 存在vnode更新
          patch(container._vnode || null, vnode, container, null, null, null, isSVG)
        }
        flushPostFlushCbs()
        container._vnode = vnode
      }
      
    • 进入patch函数

      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)
        }
      }
      
    • 里面有一个switch,根据当前vnode的type来划分vnode的类型进行各自的处理

    • 挂载的操作,本质上都是依赖patch函数来执行的,内部根据type来匹配各类挂载流程

    • 所以,整个挂载的操作,本质上是依赖于patch函数来执行的,内部基于type来执行不同类型节点的挂载

    • 整个render的大致逻辑如下

      • baseCreateRenderer 这个函数
        • 包含核心的render方法
        • render方法的渲染会在vnode存在的时候使用patch函数
        • patch函数会根据当前节点的vnode类型来选择不用的节点挂载
        • 而每一种类型的挂载节点都类似处理:
          • 旧节点不存在时,进行挂载;
          • 旧节点存在进行更新
文章来源:https://blog.csdn.net/Tyro_java/article/details/135043267
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。