Vue源码学习记录-01

62 阅读4分钟

下载源码
直接运行 npm run dev 可能产生报错 Pasted image 20250312093109.png 去 dev.js 把 git 提交相关部分注释 Pasted image 20250312093500.png 再次运行 npm run dev Pasted image 20250312093547.png 可以看到 Vue 的源码打包到了这个位置
创建一个测试页面,注意:这里引入的是刚才源码打包好的 vue.global.js


<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .container {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      font-size: 30px;
    }
  </style>
</head>

<body>
  <div id="app" class="container">
    <div>我是div</div>
    <span>我是span</span>
    <p>我是p</p>
    <h1>{{ count }}</h1>
    <button @click="decrement">减一</button>
    <button @click="increment">加一</button>
  </div>
  <script src="../../vue/dist/vue.global.js"></script>
  <script>
    const app = Vue.createApp({
        data() {
          return {
            count: 0
          }
        },
        methods: {
          increment() {
            this.count += 1;
          },
          decrement() {
            this.count -= 1;
          }
        }
      })
    app.mount("#app");
  </script>
</body>

</html>

其中 createApp 函数可以在这里找到 Pasted image 20250312103450.png 可以看到,这里创建了 app 对象,调用了 ensureRenderer() 函数的 createApp 函数 其中给 app 对象添加了 mount 方法,对,就是 app.mount("#app") 这个 mount,将 Vue 组件挂载到 id 为 app 的HTML元素上 ensureRenderer 又调用了 createRenderer Pasted image 20250312105504.png 之后调用了 baseCreateRenderer Pasted image 20250312105613.png 返回了这个对象 Pasted image 20250312105752.png 其中 createApp 是调用了 createAppAPI 方法 Pasted image 20250312110039.png createAppAPI 返回了 createApp 函数 ,这里面定义了 app 对象 Pasted image 20250312110140.png 这里面有各种方法,包括 mixin、mount 等方法,最后返回 app 对象,所以 app 对象可以调用 mount 方法 Pasted image 20250312110330.png

mount 方法

app.mount 方法最终调用的是 createApp 中的 mount 方法 Pasted image 20250313094919.png 而 createApp 中的 mount 方法的根容器参数是一个元素类型,而最外层调用的 app.mount("#app")类型并不一样,这就是参数归一化,其外层调用了normalizeContainer Pasted image 20250313095627.png Pasted image 20250313100156.png 通过判断是不是 string,如果是就调用 querySelector 进行元素的获取,如果不是可直接返回 container
接下来是模板归一化 Pasted image 20250313100509.png 对 component 进行判断,如果同时满足不是函数、不是 render、不是模板,则把容器i的 innerHTML 给组件的模板,之后再清空容器的 innerHTML。
为什么要清空容器的 innerHTML?
因为如果不清空,会变成这个样子
Pasted image 20250313101212.png 为什么会是这样?
是因为,component 是有 template 属性,而参数模板归一化,已经把静态的 HTML 交给模板了 Pasted image 20250313101637.png Pasted image 20250313101803.png Vue 组件里的处理好的模板会插入到容器中,如果静态内容不清空就会是之前的效果,静态内容与 Vue 组件管理的动态内容共存。 Pasted image 20250313102005.png 而最后 mount 方法把元素渲染出来,就是通过 render 方法 Pasted image 20250313102949.png render 方法需要三个参数,虚拟节点,根容器和 svg,最后调用 patch 函数 Pasted image 20250313103157.png 简单概括 patch 函数的功能:
1、传递容器和虚拟节点两个参数,把虚拟节点挂载到容器中
2、传递旧的和新的虚拟节点两个参数,会把新的虚拟节点替换掉旧的虚拟节点
Vue 中的 patch 方法如下,进行类型的判断,Switch 处理各种类型
Pasted image 20250314103855.png 处理元素 patch 函数的 switch 会走 processElement Pasted image 20250314104843.png processElement 又会调用 mountElement Pasted image 20250314105016.png mountElement 会接收虚拟节点 Pasted image 20250314105358.png 之后会调用 hostCreateElement,之后判断有没有子节点,如果有调用 mountChildren ,如果没有设置元素文本 Pasted image 20250314110146.png 之后调用 hostInsert 把元素插入到容器中。 Pasted image 20250314110514.png 其中 mountChildren 遍历子节点,调用 patch 函数,就又会执行之前这些逻辑 Pasted image 20250314110707.png

更新子树

effect 函数:会自动收集依赖,依赖改变,自动调用内部的回调函数。
当 data 改变,effect 就会重新执行这个 componentEffect 函数 Pasted image 20250314101515.png Pasted image 20250314101635.png 之后进行判断当前是否已经挂载,如果没有挂载,创建子树,渲染根组件 Pasted image 20250314102159.png 之后继续调用 patch 函数,把 subtree 传入 Pasted image 20250314102949.png

多个根节点

在调用挂载组件方法中的 setupComponent 设置组件方法后会,调用 setupRenderEffect 方法 Pasted image 20250319101951.png 其结构如下 Pasted image 20250319102314.png 可以打印有根节点和无根节点的子树结构查看

<div id="app" class="container">
    <div>
      <div>我是div</div>
      <span>我是span</span>
      <p>我是p</p>
      <h1>{{ count }}</h1>
      <button @click="decrement">减一</button>
      <button @click="increment">加一</button>
    </div>
  </div>

Pasted image 20250319103611.png

<div id="app" class="container">
    
      <div>我是div</div>
      <span>我是span</span>
      <p>我是p</p>
      <h1>{{ count }}</h1>
      <button @click="decrement">减一</button>
      <button @click="increment">加一</button>
    
  </div>

Pasted image 20250319103702.png 对比之下可以看到
没有根节点时,是外面生成了根节点,<Fragment> 标签进行包裹。 为什么是 Fragment ?
因为普通元素会渲染为实际的 DOM 节点,而 Fragment 不会渲染任何实际节点,通过样式就可以对比出来,手写的根节点没有设置样式就是第一张图的样子。 Pasted image 20250319105256.png Pasted image 20250319105327.png

虚拟节点是如何创建的

挂载组件调用这个 mountComponent 方法,内部创建了实例,然后调用 setupComponent(instance) 进行初始化 Pasted image 20250319154038.png 之后调用 setupRenderEffect 方法 Pasted image 20250319154348.png setupRenderEffect 方法内部的第一次挂载会进行创建之前提到的 subTree Pasted image 20250319154637.png 而 subTree 的创建是通过 renderComponentRoot 方法 Pasted image 20250319155918.png 内部创建了 result, result 调用了 render.call,最后返回了 result 给 instance.subTree Pasted image 20250319160205.png 为了找到 render 方法的来源需要往回查看 instance 的来源,是 createComponentInstance Pasted image 20250319160354.png 可以看到这里有 render ,但是为 null,所以需要继续寻找 Pasted image 20250320092638.png 之后是初始化组件 Pasted image 20250320094838.png 找到 setupStatefulComponent,顺着方法继续找 Pasted image 20250320094929.png 找到了初始化完成方法,里面找到了 render 方法 Pasted image 20250320095131.png compile 把 template 通过 AST 抽象语法树 转为 JavaScript 代码,创建虚拟节点