整体思路
在探索更新流程之前,我们先大致捋一下更新过程,从而对更新机制有一个整体的认识。
在初始化的过程中,经过了 setupRenderEffect 方法,在这个方法中就建立了更新机制;
当组件中的响应式数据发生变化时会执行更新函数;
在更新函数内部会调用 patch 方法。
接下来就进入源码中详细分析整个更新流程。
更新流程
有了整体思路之后,我们就知道了应该从建立更新机制的 setupRenderEffect 方法看起。
打开 packages\runtime-core\src\renderer.ts ,我们找到了该方法。
在该函数中,首先声明了 componentUpdateFn 。这个函数就是组件的更新函数,而在首次执行渲染函数时,就已经建立了依赖关系。该函数做了什么,在后面调用时再看,我们接着往下。
下面又声明了一个 effect 函数,该函数的作用是为数据变化和组件渲染之间建立响应式的联系,使得数据发生变化时调用对应的更新函数更新视图。
effect 是通过 ReactiveEffect 构造的一个实例,传入了3个参数。第一个参数就是上面的更新函数,是更新时真正执行的方法;第二个参数是一个定时器任务,通过 queueJob 对 update 进行处理,将来在更新时就会按照这种方式执行;第三个参数是作用域,我们这里并不关心。
我们看到在 queueJob 中处理的 update 实际上就是 effect.run() 。
接下来我们看一下 queueJob 中做了什么。
打开 packages\runtime-core\src\scheduler.ts 文件,可以找到 queueJob 方法。该方法就是将任务放入队列,然后执行批量更新,通过 Promise.then() 执行 flushJobs 。也就是说,会在未来微任务队列清空时执行。
flushJobs 会通过循环依次执行任务队列中的更新函数,也就是上面传入的 effect.run() 。
打开 packages\reactivity\src\effect.ts 文件,我们看到 run 方法返回了当前实例的成员函数 fn()。
在 run 方法的上面我们看到当前的实例就是通过 ReactiveEffect 构造的,而 fn 就是之前传入的第一个参数,也就是更新函数 componentUpdateFn 。
我们直接看更新流程,所以直接进入 else 分支。
可以看到,在更新过程中,先获取了最新的 vnode 和缓存的 oldVnode ,再调用 patch 方法执行 diff 算法,完成更新过程。
单步调试
看完源码,我们在通过单步调试走一遍更新流程。
我们在 packages\vue\examples 目录下新建一个 html 文件,引入打包后的 vue.js ,写一个简单的例子。
在浏览器中打开文件,在 setInterval 处打上断点,刷新页面,F11进入断点。首先进入了一个只在开发模式中产生作用的 createDevRenderContext 方法,我们并不关心,接着往下进入了 PublicInstanceProxyHandlers 方法中。这个就是当一个值发生变化时,对其 get 和 set 进行拦截,从而进行响应式处理的方法。
由于我们在代码中做的是一次赋值操作,所以会先获取一次 counter 的值,第一次会进入 get 中。接着是赋值,就会进入 set 中。我们将断点打在下面的 set 方法中,进入断点,往下走到407行,这里进行了赋值操作。
接着F11后,进入了 createSetter 方法,里面返回了一个 set 方法,在一系列操作后,会执行 trigger 方法。
在 trigger 方法的最后,会执行触发更新的方法 triggerEffects。
由于当前的值存在 scheduler ,所以这里执行了 scheduler 方法,而没有直接执行更新函数。
进入 scheduler ,我们发现走到了之前在源码中看到的 effect 方法,而 scheduler 就是其中的第二个参数。
再往下就是执行 queueJob ,将更新函数加入异步队列,然后执行 queueFlush ,启动批量更新任务。
根据之前源码部分的分析,我们知道最终执行的更新函数就是 componentUpdateFn ,所以我们将断点打在 componentUpdateFn 的更新流程部分。
在获取新的 vnode 和 缓存的 oldVnode 之后,就执行了 patch 方法,patch 方法走完后,页面上就渲染出了更新后的内容。
总结
整个更新流程可以分为两部分。一个是更新机制的建立,一个是更新过程。
更新机制的建立是在初始化的时候进行的:
结合之前的初始化流程,我们知道在 app.mount 中执行了 setupRenderEffect 方法,在该方法中通过 ReactiveEffect 构造的实例 effect 方法建立了数据变化和组件渲染之间的联系。
更新过程是在响应式数据发生改变时触发的:
在 counter 的值发生变化时,会触发拦截机制,进入 set 方法中;
在 set 中,会将新值赋值给 counter ,然后执行 trigger 方法;
trigger 方法中和更新相关的操作是执行 triggerEffects 方法,该方法会将和 counter 相关的所有 effect 都遍历一遍,并依次执行其中的更新函数;
在调用更新函数之前,会通过调用 effect.scheduler() 的方式来进行更新;
在 scheduler 中会执行 queueJob(update) ,将更新函数加入任务队列;
然后通过 queueFlush 启动批量更新任务;
在 queueFlush 中,会通过 Promise.then() 执行 flushJobs ,也就是在未来微任务队列清空时执行;
flushJobs 中执行的 effect.run() ,就是 effect.fn() ,也就是 ReactiveEffect 中的第一个参数 componentUpdateFn;
在 componentUpdateFn 中,通过 patch 方法执行 diff 算法,完成更新过程。
流程图: