mini-vue学习历程(Hello-World篇)

115 阅读8分钟

从毕业入职到现在,当牛马已经快一年了,突然想来看看vue3源码(别骂我,下定决心好好学习了)

从网上得知mini-vue相当于对vue3源码的凝练,精华都在这里面了,如果直接去看源码,可能会因为文件代码量太大而放弃,vue3的源码多的一部分原因也是因为很多代码都是针对的环境和错误场景,于是乎,github启动!找了10.4kstars的项目来看看:github.com/cuixiaorui/…

克隆下来后第一步,先看文件目录

- packages
    - complier-core
    - reactivity
    - runtime-core
    - runtime-dom
    - runtime-test
    - shared
    - vue
- .gitgnore // git上传忽略文件
- LICENSE // 产品许可文件
- package.json // 项目依赖项
- pnpm-lock.yaml
- README_EN.md
- README.md
- rollup.config.js // 打包配置
- tsconfig.json // 定义编译器如何编译TypeScript代码
- vitest.config.ts // 合并项目的 Vite 配置和 Vitest 特定的测试配置

不知道从哪下手了,看看readme 先看看流程图

image.png

那么第一步:进入packages/vue/example,helloworld启动

image.png

右边打印了很多,看上去这就是我们要学习的流程了,跟着右边文件一个个打开来读一读(学习过程中打印的内容用斜体加粗表示)。

runtime-core/src/createApp.ts 大致看看就知道是平时vue最开始我们写的createApp API,在该方法里构建一个app对象,基于根组件创建 vnode 然后去runtime-core/src/vnode.ts文件里看看虚拟节点的创建过程:

createVNode:接受三个参数(type, props, children)
    - type: 创建的dom元素类型,有可能是 string 也有可能是对象,如果是对象的话,那么就是用户设置的 options
    - props:需要传入组件的数据
    - children: 子组件

创建vnode对象

const vnode = {
    el: null, // 将el设为null标志当前节点尚未挂载到真实dom上
    component: null, // 强调这不是一个组件
    key: props?.key,
    type,
    props: props || {},
    children,
    shapeFlag: getShapeFlag(type), // 使用位掩码,快速判断 vnode 类型
}

调用 render,基于 vnode 进行开箱
即将 vnode 转换成真实的 DOM 节点

调用 patch
runtime-core/src/renderer.ts 在render函数中会调用patch函数,patch(null, vnode, container),第一个null表示旧节点不存在

// patch函数接收的参数如下
patch(
    n1, // 旧节点
    n2, // 新节点
    container = null,
    anchor = null,
    parentComponent = null
)

因为第一个进入函数的是“div”,是component组件,所以会先打印“处理 component”,然后进入processComponent方法,因为n1没有值所以进入mountComponent函数:

  1. 创建组件实例(createComponentInstance方法,文件在/runtime-core/src/component里)
    a. 定义一个instance对象
 const instance = {
    type: vnode.type,
    vnode,
    next: null, // 需要更新的 vnode,用于更新 component 类型的组件
    props: {},
    parent,
    provides: parent ? parent.provides : {}, //  获取 parent 的 provides 作为当前组件的初始化值 这样就可以继承 parent.provides 的属性了
    proxy: null, // 组件代理对象(用于模板访问)
    isMounted: false, // 组件挂载状态
    attrs: {}, // 存放 attrs 的数据
    slots: {}, // 存放插槽的数据
    ctx: {}, // 渲染上下文(模板中的this)
    setupState: {}, // 存储 setup 的返回值
    emit: () => {},
  };

b.赋值上下文

instance.ctx = {
    _: instance,
  };

c.赋值绑定emit

instance.emit = emit.bind(null, instance) as any;

emit函数在/runtime-core/src/componentEmits文件里

1.先获取props
2.处理事件名转换成onXXX函数,以便emit匹配
  1. 打印“创建组件实例: App
  2. 进入setupComponent函数
  • 处理props:进入initProps函数,初始化props,将props直接赋值给 instance.props,打印“initProps
  • 进入initSlots方法,打印“初始化 slots”,将传入的slot处理成函数返回
  • component有两种类型——options(stateful有状态)和function,若为options创建的,则进入setupStatefulComponent函数
    • 打印“创建 proxy
    • 创建instance.ctx对象的代理
      • get:当访问的不是public api时,先检测访问的 key 是否存在于 setupState 中, 是的话直接返回,反之判断key 是不是在 props 中,是的话直接返回;若访问的是public api,即$XXX, 返回对应实例里的对应key的值
      • set:访问的 key 是否存在于 setupState 中,如果存在直接赋值)
    • 初始化 Component = instance.type;
    • 执行setup函数(如果存在):
      • 设置当前 currentInstance 的值;
      • 进入createSetupContext函数;
      • 打印“初始化 setup context”;
      • 初始化setup执行时的上下文;
      • 初始化setupResult(将props设置为只读),进入createReactiveObject函数,因为初始化没有缓存所以新建一个Proxy,使当前proxy设置为只读,然后存在shallowReadonlyMap里,返回undifined
    • 设置setCurrentInstance为null
    • 处理setupResult,进入handleSetupResult函数(当前实例对象,setupResult):1. 由于setupResult为undifined,进入finishComponentSetup函数;2. 给当前实例对象instance设置render函数
  1. 进入setupRenderEffect函数:
  • 进入effect函数"\reactivity\src\effect.ts"
  • 创建ReactiveEffect对象(触发依赖函数)
  • 打印“创建 ReactiveEffect 对象
  • 把 effect 推到微任务的时候在执行
  • 执行ReactiveEffect的run函数:
    • 打印“run”;
    • shouldTrack设置为true;
    • 给全局的 activeEffect 赋值;
    • 打印“执行用户传入的 fn”;
    • 执行传入的fn —— componentUpdateFn
      • 打印“App:调用 render,获取 subTree
      • 赋值实例对象的proxy给proxyToUse
      • render.call生成当前组件的虚拟 DOM 子树(subTree)
      • 调用normalizeVNode函数,确保子树的每个节点都是规范的 VNode 对象
      • 将生成的子树保存到 instance.subTree,供后续更新时进行 diff 比对
      • 打印“subTree”,和对应的vnode树
      • 打印“App:触发 beforeMount hook
      • 打印“App:触发 onVnodeBeforeMount hook
      • 调用patch函数
        • 传入的subTree的type为div,且shapeFlag为17,打印“处理 element”,进入processElement函数
          • 传入的n1为null,进入mountElement函数
          • 调用hostCreateElement创建真实div DOM,打印“CreateElement div
          • 进入mountChildren函数,依次创建真实DOM
            • 处理p标签,依次打印: “mountChildren:p对应的内容
            • 进入patch函数打印:“处理 element
            • CreateElement p
            • 处理文本:主页
            • SetElementText el(p) text(主页)
            • 进入hostPatchProp处理传入的props,为真实 DOM 元素(el)设置、更新或移除指定的属性(key)
            • 打印“vnodeHook -> onVnodeBeforeMount
            • DirectiveHook -> beforeMount
            • transition -> beforeEnter
            • 进入hostInsert函数:
              • 打印“Insert
              • p元素插入div元素
              • 打印“vnodeHook -> onVnodeMounted
              • DirectiveHook -> mounted
              • 打印“transition -> enter
            • 处理HelloWorld组件,打印“***mountChildren: HelloWorld对应的内容”
            • shapeFlag为52,打印“处理component”,进入processComponent函数,n1为null,调用mountComponent函数:
              • 进入createComponentInstance创建instance实例,赋值emit
              • 打印 “创建组件实例:HelloWorld
              • 进入setupComponent方法
                • 处理props和slots,依次打印“initProps”和“初始化slots”,为slots_children,所以进入normalizeObjectSlots函数,但children为[],所以直接返回
                • 进入setupStatefulComponent函数
                  • 打印“创建proxy
                  • 给HelloWorld组件的proxy对象赋值,让用户可以直接在 render 函数内直接使用 this 来触发 proxy
                  • 调用setup
                  • 进入setCurrentInstance函数对setCurrentInstance全局变量赋值,使其在 setup 中获取组件实例 instance
                  • 进入createSetupContext函数,打印“初始化 setup context”,并将结果赋值给setupContext
                  • 处理把 props 设置为只读的,给setupResult赋值
                  • 清空currentInstance值
                  • 处理setupResult,进入handleSetupResult函数,但由于serupResult值为undifined,跳到finishComponentSetup函数中去,设置render函数
            • 调用setupRenderEffect函数,给instance的update赋值,进入effect方法
              • 打印“创建 ReactiveEffect 对象
              • 把用户传过来的值合并到 _effect 对象上去
              • 进入_effect对象的run函数上去
              • 打印“run
              • 执行 fn(componentUpdateFn) 收集依赖
                • 设置shouldTrack为true
                • 给全局的activeEffect赋值,利用全局属性来获取当前的effect
                • 打印“执行用户传入的 fn
                • 进入componentUpdateFn函数:
                  • HelloWorld组件还没被渲染,所以打印“HelloWorld:调用 render,获取 subTree
                  • 定义proxyToUse为HelloWorld实例对象的proxy
                  • 定义subTree为实例对象的subTree,并对实例对象的subTree进行规范化
                  • 执行render函数,同时call绑定对象,使其可在render函数中通过this来使用proxy
                  • 由于用到了count值,所以进入ref.ts文件,触发了get方法,进入trackRefValue函数,调用isTracking返回为true判断当前是需要收集依赖的,所以进入trackEffects函数:
                    • 传入trackEffect的dep为空,即这个依赖还未被收集,于是dep传入当前activeEffect对象
                    • 将dep存在activeEffect的deps对象中去
                  • 返回当前ref对象的值——0
                  • 进入h,创建HelloWorld对象
                  • 打印“HelloWorld:触发 beforeMount hook
                  • 打印“HelloWorld:触发 onVnodeBeforeMount hook
                  • 进入patch:
                    • 打印“处理 element”,再进入processElement函数渲染组件
                    • 再进入createElement,打印“CreateElement div
                    • 并进入mountElement创建元素,打印“处理文本:hello world: count: 0”,
                    • 再进入setElementText函数,打印“SetElementText <div></div> hello world: count: 0”,将元素的textContext设为text
                    • 再处理props,进入patchProp函数,打印“PatchProp 设置属性:tId 值:helloWorld”、“key: tId 之前的值是:null”,不是以onXXX开头,所以直接调用setAttribute给对应元素设置属性
                    • 打印“vnodeHook -> onVnodeBeforeMount”, “DirectiveHook -> beforeMount”,“transition -> beforeEnter
                    • 将HelloWorld组件插入到div中去,打印“Insert
                    • 打印“vnodeHook -> onVnodeMounted”,“DirectiveHook -> mounted”,“transition -> enter
                  • 打印“HelloWorld:触发 mounted hook
                • 重置shouldTrack为false,activeEffect为undifined
                • 返回undifined
              • 定义runner函数,把 _effect.run 这个方法返回,让用户可以自行选择调用的时机(调用 fn)
          • 处理props,最外层div有个props 为{tid:1},于是赋值nextVal为{tid:1},进入patchProp函数
            • 打印“PatchProp 设置属性:tId 值:1”,“key: tId 之前的值是:null
            • 由于不为onXXX方法,所以直接调用setAttribute给div绑定属性tid为1
          • 打印“vnodeHook -> onVnodeBeforeMount”,“DirectiveHook -> beforeMount”,“transition -> beforeEnter
          • 进入insert方法
            • 打印“Insert
            • 将div插入根节点去(此时页面有显示了)
          • 打印“vnodeHook -> onVnodeMounted”,“DirectiveHook -> mounted”,“transition -> enter
      • 打印“App:触发 mounted hook
    • 将结果返回,同时重置shouldTrack、activeEffect
    • 把 _effect.run 这个方法返回,用户可以自行选择调用时机

结束!!!