单步调试
村长在课上介绍了一些学习源码的方法,我觉得其中的单步调试法比较适合刚开始学习源码的同学。
在搭建源码调试环境时,我们在todomvc中的createApp方法上打了一个断点,现在就从这里开始,一边调试一边看初始化的流程是如何进行的,调用了哪些方法。
应用程序实例创建过程
在调试之前,我们先给自己提两个问题。应用程序实例是如何创建的?实例长什么样?带着问题去源码中找答案,如果将这两个问题解决了,那么我们对实例的创建过程就基本了解了。
当我们刷新浏览器后,程序进入createApp方法处的断点,按F11或点击下图中的箭头进入断点,我们进入了 packages\runtime-dom\src\index.ts 文件中的createApp方法。
可以看到实例是通过 ensureRenderer().createApp(…args) 创建的,下面对实例中的 mount 方法进行了重写,在执行 mount 方法之前又做了一些其他的事情,然后就将实例返回了。我们想要弄清楚之前的两个问题,显然要接着进入 ensureRenderer 这个方法中,看它做了什么。
接着F11后,进入了 ensureRenderer 方法,这里只有一行代码,作用就是返回一个 renderer ,即渲染器。如果没有,则通过 createRenderer 方法创建一个 renderer 返回。第一次进入时 renderer 为空,则程序会进入 createRenderer 方法中,我们在该方法处打上断点,接着往下调试。
createRenderer 方法在 packages\runtime-core\src\renderer.ts 文件中,可以看到 createRenderer 方法是通过 baseCreateRenderer 方法返回渲染器的。这个方法是Vue3中最大的一个函数,共有2000多行,函数中做了什么,我们暂时先不关心,直接拉到函数末尾,看这个函数返回的什么。
在2355行,我们找到了该函数的返回结果,即上面的 renderer 。其中就有一个 createApp 方法,这个也就是上面执行 ensureRenderer 返回的createApp 。而这个函数也是通过另一个工厂函数 createAppAPI 返回的。我们再次打上断点,继续往下进行。
进入 packages\runtime-core\src\apiCreateApp.ts 文件中的 createAppAPI 方法中,这里终于找到了我们想要的实例 app 。
到这里,我们终于可以回答上面的两个问题了。
应用程序实例是通过 renderer 中的 createApp 方法创建的。
应用程序实例就是一个对象,里面包含了我们熟悉的 use、mixin、component、mount 等方法。
挂载过程
在创建完实例后,紧接着就调用了实例中的 mount 方法( .mount(‘#app’) ),执行挂载。调试挂载过程之前,我们依然提出一个问题:挂载都做了什么?带着这个问题继续调试下去。
在实例中的 mount 方法中,可以看到挂载过程做了两件事。第一件事就是创建根节点的 vnode ,第二件事就是将 vnode 传入 render 方法中,并执行。这个 render 方法是调用 createAppAPI 方法时传入的。
render 方法最终执行的是 patch 方法,这个方法的作用就是将传入的 vnode 转换为 dom ,并追加到宿主元素 #app 中。
首次patch过程
我们接着在 patch 方法处打上断点,继续调试首次 patch 过程。
进入 patch 方法中,我们将程序运行到 switch 语句处,可以看到当前的 type 是根组件,它是一个对象,会被当做组件处理。那么就会进入下面的 processComponent 方法。将断点打在该方法处。
继续调试,进入该方法。
首次执行会进入 mountComponent 方法挂载当前组件。
进入 mountComponent 方法后,我们将程序执行到上图中红框之后。这个 instance 就是组件实例。但它和Vue2中的不太一样,Vue2中的组件实例就是我们常用的 this 。但是Vue3中的组件实例中都是一些不认识的方法。
当我们再打开其中的 ctx 属性后,我们看到了一些熟悉的方法。这里才是组件的上下文。
再往下运行,发现执行了一个 setupComponent 方法,这个方法是对当前组件进行初始化操作。
如对 props 和 slots 的处理,当然最重要的是执行了 setupStatefulComponent 方法。
进入 setupStatefulComponent 方法后,会拿到组件中Vue3新增的Composition API中的 setup 方法。如果有,就执行。
继续往下可以看到,不管有没有执行 setup 方法,最终都会执行 finishComponentSetup 方法。
finishComponentSetup 方法主要是对模板的处理。首次进入时组件中用户没有写 render(渲染函数),则会获取当前组件的模板,通过编译函数 compile 方法生成 render ,方便之后的渲染和更新。
在完成了组件的初始化之后,接着执行了 setupRenderEffect 方法,该方法在之后的响应式流程中再详细了解。
这里我们主要关注其中1368行执行后产生的 subTree 。这个 subTree 就是当前组件的子树,就是 todomvc 页面中的 section ,和上面的根组件类似,也是一个虚拟dom。
接着往下,再次进入了 patch 方法。
这次 type 变成了 section ,进入 processElement 方法中。
首次进入 mountElement 方法。
在 mountElement 方法中真正创建了dom元素。但是并没有直接渲染在页面上,继续对该元素进行处理。当发现该元素还有子节点时,又会进入 mountChildren 方法中。
而在 mountChildren 方法中,会循环处理子元素,分别执行 patch 方法,向下递归,最终完成整棵子树的创建,再渲染到页面上。
总结
上面我们已经将初始化流程调试了一遍,接下来我们可以通过初始化流程的函数调用栈来总结复盘一下。
从下往上看,第一个是一个自调用函数,这个就是挂载,也就是 .mount(‘#app’);
然后执行了重写的方法 app.mount ;
在 app.mount 中执行了原生的 mount 方法;
mount 中执行了将 vnode 方法转化为 dom 的 render 方法;
render 方法最终执行的是 patch 方法;
在 patch 方法中,会通过类型判断当前是组件还是元素等;
由于当前是根实例,所以进入了处理组件的 processComponent ;
在 mountComponent 中对组件实例进行了初始化操作;
并且执行了 setupRenderEffect 方法,该方法为组件的更新做了一系列的处理;
然后执行了一次更新函数, instance.update 和 run 就是 setupRenderEffect 中的 componentUpdateFn 方法;
又一次进入 patch 方法;
这次进入时变成了 section ,所以进入处理元素的 processElement ;
然后创建元素,进入 mountElement ;
如果有子元素,会进入 mountChildren 循环递归处理子元素,最终完成整棵子树的创建。
流程图: