vue3源码剖析 01

学习目标

  • 调试环境准备
  • 初始化流程分析
  • 手写实现
  • 自定义渲染器实战

调试环境准备

  • 迁出Vue3源码:git clonehttps://github.com/vuejs/vue-next.git
  • 安装依赖:yarn --ignore-scripts
  • 生成sourcemap文件,package.json

    1. "dev": "node scripts/dev.js --sourcemap"
  • 编译:yarn dev

    生成结果: packages\vue\dist\vue.global.js packages\vue\dist\vue.global.js.map

  • 调试范例代码:yarn serve

vue3源码架构

image.png

初始化过程

小目标

  • 捋清楚vue3初始化过程
  • setup()是如何生效的

vue3初始化过程

createApp()是如何创建vue实例的;创建的vue实例执行mount()都做了些什么。

测试代码

  1. <div id="app">
  2. <h1>vue3初始化流程</h1>
  3. </div>
  4. <script src="../../dist/vue.global.js"></script>
  5. <script>
  6. const { createApp } = Vue
  7. createApp({}).mount('#app')
  8. </script>

断点调试createApp() ensureRenderer() => renderer => createApp()

createAppAPI() => createApp

app.mount() => render() => patch() => processComponent() => mountComponent() => setupComponent() => setupRenderEffect()

执行流程

createApp()
packages/runtime-dom/src/index.ts
创建vue实例、扩展mount方法

createRenderer()/baseCreateRenderer()
packages/runtime-core/src/renderer.ts
创建renderer对象,它对外暴露 3 个重要方法render, hydrate, createApp,其中render
hydrate的实际使用者是createApp()返回的vue实例对象。

createAppAPI(render, hydrate)
packages/runtime-core/src/apiCreateApp.ts
返回生产vue实例的createApp函数。

render的使用者是vue实例的mount方法 我们发现component()/directive()/use()/mixin()这些方法都变成了实例方法,它们也会返回实例本 身,链式调用成为可能。

  1. createApp({})
  2. .component('comp', { template: '<div>this is comp</div>' })
  3. .directive('focus', { mounted (el) { el.focus() } })
  4. .mount('#app')

filter方法被移除了

mount(rootContainer: HostElement, isHydrate?: boolean)
packages/runtime-core/src/apiCreateApp.ts
createApp(rootComponent)中传入的根组件转换为vnode,然后渲染到宿主元素rootContainer
中。

render(vnode, container)
将传入vnode渲染到容器container上。

patch(n1, n2, container)
将传入的虚拟节点n1n2进行对比,并转换为dom操作。初始化时n1并不存在,因此操作将是一次dom创建。

mount(rootContainer)
packages/runtime-core/src/apiCreateApp.ts
执行根组件挂载,创建其vnode,并将它render()出来

render()
packages/runtime-core/src/renderer.ts
执行补丁函数patch()将vnode转换为dom。

patch(n1, n2, container)
packages/runtime-core/src/renderer.ts
根据n2的类型执行相对应的处理函数。对于根组件,执行的是processComponent()

processComponent()
packages/runtime-core/src/renderer.ts
执行组件挂载或更新,由于首次执行时n1为空,因此执行组件挂载逻辑mountComponent()

mountComponent()
packages/runtime-core/src/renderer.ts
创建组件实例,执行setupComponent()设置其数据状态,其中就包括setup()选项的执行

setup()如何生效

在vue3中如果要使用composition-api,就需要写在setup()中,它是如何生效并和options-api和谐共处
的?

测试代码

  1. <div id="app">
  2. <h1>setup()如何生效</h1>
  3. <p>{{foo}}</p>
  4. </div>
  5. <script src="../../dist/vue.global.js"></script>
  6. <script>
  7. const { createApp, h, ref } = Vue
  8. createApp({
  9. setup () {
  10. const foo = ref('hello, vue3!')
  11. return { foo }
  12. }
  13. }).mount('#app')
  14. </script>

执行过程

根组件执行挂载mount()时,执行渲染函数render()获取组件vnode,然后执行补丁函数patch()将其转换
为真实dom,对于组件类型会调用processComponent(),这里会实例化组件并处理其setup选项。

image.png

setupComponent()
packages/runtime-core/src/component.ts
初始化props、slots和data

setupStatefulComponent(instance, isSSR)
packages/runtime-core/src/component.ts
代理组件实例上下文,调用setup()

setup()会接收两个参数,分别是props和setupContext,可用于获取属性、插槽内容和派发事件

  1. createApp({
  2. props: ['bar'], // 属性依然需要声明
  3. setup (props) {
  4. // 作为setupResult返回
  5. return { bar: props.bar }
  6. }
  7. // 传入rootProps
  8. }, { bar: 'bar' }).mount('#app')

handleSetupResult(instance, setupResult, isSSR)
packages/runtime-core/src/component.ts
处理setup返回结果,如果是函数则作为组件的渲染函数,如果是对象则对其做响应化处理。

自定义渲染器


可以自定义渲染器,将获取到的vnode转换为特定平台的特定操作。

范例:利用canvas画图

第一步:我们创建一个渲染器,需要给它提供节点和属性的操作

  1. const { createRenderer } = Vue
  2. // 创建一个渲染器,给它提供节点和属性操作
  3. const nodeOps = {}
  4. const renderer = createRenderer(nodeOps);

第二步:创建画布,我们通过扩展默认createApp做到这一点

  1. // 保存画布和其上下文
  2. let ctx;
  3. let canvas;
  4. // 扩展mount,首先创建一个画布元素
  5. function createCanvasApp (App) {
  6. const app = renderer.createApp(App);
  7. const mount = app.mount
  8. app.mount = function (selector) {
  9. canvas = document.createElement('canvas');
  10. canvas.width = window.innerWidth;
  11. canvas.height = window.innerHeight;
  12. document.querySelector(selector).appendChild(canvas);
  13. ctx = canvas.getContext('2d');
  14. mount(canvas);
  15. }
  16. return app
  17. }
  18. // 创建app实例
  19. createCanvasApp({}).mount('#app')

此时已经可以看到canvas,但是会报一个错误,是因为我们上面组件是空的,vue想要创建一个comment元素导致

第三步:添加模板

  1. <script type="text/x-template" id="chart">
  2. <bar-chart :data="chartData"></bar-chart>
  3. </script>
  4. <div id="app"></div>
  1. createCanvasApp({
  2. template: '#chart',
  3. data () {
  4. return {
  5. chartData: [
  6. { title: "⻘铜", count: 200, color: "brown" },
  7. { title: "砖石", count: 300, color: "skyblue" },
  8. { title: "星耀", count: 100, color: "purple" },
  9. { title: "王者", count: 50, color: "gold" }
  10. ]
  11. }
  12. },
  13. }).mount('#app')

第四步:节点操作实现

  1. // 保存canvas实例和上下文
  2. let ctx, canvas
  3. const nodeOps = {
  4. createElement: (tag, isSVG, is) => {
  5. // 创建元素时由于没有需要创建的dom元素,只需返回当前元素数据对象
  6. return { tag }
  7. },
  8. insert: (child, parent, anchor) => {
  9. // 我们重写了insert逻辑,因为在我们canvasApp中不存在实际dom插入操作
  10. // 这里面只需要将元素之间的父子关系保存一下即可
  11. child.parent = parent
  12. if (!parent.childs) {
  13. parent.childs = [child]
  14. } else {
  15. parent.childs.push(child)
  16. }
  17. // 只有canvas有nodeType,这里就是开始绘制内容到canvas
  18. if (parent.nodeType === 1) {
  19. draw(child)
  20. }
  21. },
  22. patchProp (el, key, prevValue, nextValue) {
  23. el[key] = nextValue;
  24. }
  25. }

第四步:绘图逻辑

  1. const draw = (el, noClear) => {
  2. if (!noClear) {
  3. ctx.clearRect(0, 0, canvas.width, canvas.height)
  4. }
  5. if (el.tag == 'bar-chart') {
  6. const { data } = el;
  7. const barWidth = canvas.width / 10,
  8. gap = 20,
  9. paddingLeft = (data.length * barWidth + (data.length - 1) * gap) / 2,
  10. paddingBottom = 10;
  11. // x轴
  12. // 柱状图
  13. data.forEach(({ title, count, color }, index) => {
  14. const x = paddingLeft + index * (barWidth + gap)
  15. const y = canvas.height - paddingBottom - count
  16. ctx.fillStyle = color
  17. ctx.fillRect(x, y, barWidth, count)
  18. // text
  19. });
  20. }
  21. // 递归绘制子节点
  22. el.childs && el.childs.forEach(child => draw(child, true));
  23. }

配置自定义组件白名单: app.config.isCustomElement = tag => tag === ‘bar-chart’

作业

按课上讲解手写vue3初始化流程
要求:学习中心提交代码截图和代码
通过标准:能够正常运转,完成既定功能