1. 调试环境搭建

  1. 下载代码
  2. 使用安装依赖 yarn
  3. 修改package.json,dev命令中添加--sourcemap

    1. "dev": "node scripts/dev.js --sourcemap"
  4. 执行编译yarn dev,生成结果的路径:packages\vue\dist\vue.global.js

  5. 可使用packages\vue\examples,也可以自己编写 html,引入上面生成vue.global.js文件,结合chrome 浏览器进行调试。 ```html

{{message}}

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/338495/1622369051276-4b6fa2e2-f89e-4ef8-b779-effee5434a74.png#clientId=u684abc8b-6312-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=374&id=u3f43b851&margin=%5Bobject%20Object%5D&name=image.png&originHeight=748&originWidth=1644&originalType=binary&ratio=1&rotation=0&showTitle=false&size=123542&status=done&style=none&taskId=u49979369-f993-4caa-a969-c6e08a34678&title=&width=822)
  2. <a name="sinzf"></a>
  3. # 2. 总览
  4. <a name="i9B01"></a>
  5. ## 2.1 目录
  6. 源码在package文件夹内,采用了[monorepo](https://en.wikipedia.org/wiki/Monorepo) 管理项目<br />![](https://cdn.nlark.com/yuque/0/2021/png/338495/1621577912716-b9e4c0f3-bd1a-4168-b536-bd65c36212f2.png#crop=0&crop=0&crop=1&crop=1&from=paste&height=351&id=o8IyU&margin=%5Bobject%20Object%5D&originHeight=351&originWidth=628&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=628)
  7. - compiler-core 核心编译逻辑
  8. - compiler-dom 针对浏览器平台编译逻辑
  9. - compiler-sfc 针对单文件组件编译逻辑
  10. - compiler-ssr 针对服务端渲染编译逻辑
  11. - runtime-core 运行时核心
  12. - runtime-dom 运行时针对浏览器的逻辑
  13. - runtime-test 浏览器外完成测试环境仿真
  14. - reactivity 响应式逻辑
  15. - template-explorer 模板浏览器
  16. - server-renderer 服务器端渲染
  17. - share 公用方法
  18. - vue 代码入口,整合编译器和运行时
  19. 包的依赖关系:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/338495/1622370251954-00d9d7f6-a7fc-4fc8-a3c4-bc5d80dab773.png#clientId=u684abc8b-6312-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u6db5f510&margin=%5Bobject%20Object%5D&name=image.png&originHeight=513&originWidth=989&originalType=binary&ratio=1&rotation=0&showTitle=false&size=25092&status=done&style=none&taskId=ub7b638c2-9c16-4a3c-8f07-0dcecb95bed&title=)
  20. <a name="WLdVk"></a>
  21. ## 2.2 Vue基本原理
  22. Vue主要包含三大件:
  23. - 响应式模块:主要是创建响应式的数据对象,观察这些数据的改变。
  24. - 编译模块:将 template 模板编译到 render 函数
  25. - 渲染模块:分为三个部分
  26. - render 阶段:调用 render 函数返回虚拟DOM
  27. - mount 阶段:将虚拟DOM挂载到页面元素上
  28. - patch 阶段:将新旧的虚拟阶段进行比较,更新网页变化的部分
  29. 简单描述下大体流程就是:
  30. 1. template 编译成 render 函数,然后使用响应式模块将数据进行初始化
  31. 2. 调用 render函数返回虚拟 dom,调用 mount 方法进行,创建web页面
  32. 3. 如果响应式数据发生变化,会再次调用渲染函数,创建新的虚拟 dom,执行 patch 操作,更新 web 页面
  33. 本文主要介绍渲染部分的内容
  34. <a name="NUmFX"></a>
  35. # 3. 源码分析
  36. <a name="Xqpgz"></a>
  37. ## 3.1 ceateApp方法
  38. ```javascript
  39. import { createApp } from 'vue'
  40. const app = createApp({})

调用ceateApp()返回一个应用实例,这其实是一个入口函数:

  1. // 位置:runtime-dom/index.ts
  2. const createApp = ((...args) => {
  3. // 创建 app 对象
  4. const app = ensureRenderer().createApp(...args)
  5. const { mount } = app
  6. // 重写 mount 方法
  7. app.mount = (containerOrSelector) => {
  8. // ...
  9. }
  10. return app
  11. })

可见createApp内部就创建了一个 app 对象,且提供了mount方法。
进一步深入到内部:

ensureRenderer()用来创建一个一个渲染器对象:

  1. // 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法
  2. const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
  3. // 延时创建渲染器,当用户只依赖响应式包的时候,可以通过 tree-shaking 移除核心渲染逻辑相关的代码
  4. let renderer
  5. function ensureRenderer() {
  6. return renderer || (renderer = createRenderer(rendererOptions))
  7. }
  8. function createRenderer(options) {
  9. return baseCreateRenderer(options)
  10. }
  11. function baseCreateRenderer(options) {
  12. function render(vnode, container) {
  13. // 组件渲染的核心逻辑
  14. }
  15. return {
  16. render,
  17. hydrate,
  18. createApp: createAppAPI(render, hydrate)
  19. }
  20. }

hydrate为 SSR 相关参数先忽略,继续查看createAppAPI方法:

  1. function createAppAPI(render) {
  2. // createApp createApp 方法接受的两个参数:根组件的对象和 props
  3. return function createApp(rootComponent, rootProps = null) {
  4. const app = {
  5. ...
  6. // 创建默认APP配置
  7. const context = createAppContext()
  8. const installedPlugins = new Set()
  9. let isMounted = false
  10. _component: rootComponent,
  11. _props: rootProps,
  12. _container: null,
  13. _context: context,
  14. ...
  15. use() {...},
  16. mixin() {...},
  17. component() {...},
  18. directive() {...},
  19. // 重点看mount函数
  20. mount(rootContainer) {
  21. if (!isMounted) {
  22. // 创建根组件的 vnode
  23. const vnode = createVNode(rootComponent, rootProps)
  24. // store app context on the root VNode.
  25. vnode.appContext = context
  26. // 利用渲染器渲染 vnode
  27. render(vnode, rootContainer)
  28. isMounted = true
  29. app._container = rootContainer
  30. return vnode.component!.proxy
  31. }
  32. }
  33. unmount() {...},
  34. provide() {...}
  35. }
  36. //返回app实例
  37. return app
  38. }

3.2 mount 方法

我们在使用createApp({ ... }).mount('#app')mount方法,不是直接执行上面的app实例里的mount方法,其是属于runtime-core包,是一个标准的可跨平台的组件流程。

mount方法在rumtime-dom包里的createApp()方法里进行了重写,这是针对web平台,其他平台也可以对app.mount 进行重写以实现不同平台的渲染逻辑。

  1. app.mount = (containerOrSelector) => {
  2. // 标准化容器
  3. const container = normalizeContainer(containerOrSelector)
  4. if (!container) return
  5. const component = app._component
  6. // 如组件对象没有定义 render 函数和 template 模板,那就取DOM里面原本内容当作模版
  7. if (!isFunction(component) && !component.render && !component.template) {
  8. component.template = container.innerHTML
  9. }
  10. // 挂载前清空容器内容
  11. container.innerHTML = ''
  12. // 真正的挂载,执行app.mount方法
  13. return mount(container)
  14. }
  • normalizeContainer方法:可以传字符串选择器或者 DOM 对象字符串选择器,最终也会 DOM 对象,作为最终挂载的容器。

3.3 创建和渲染VNode

3.3.1 创建

VNode的概念应该都不陌生,就是Virtual DOM,其实就是JavaScript对象描述DOM:

  1. <div>
  2. <MyComponent />
  3. </div>
  1. const elementVNode = {
  2. tag: 'div',
  3. data: null,
  4. children: {
  5. tag: MyComponent,
  6. data: null
  7. }
  8. }

回到app.mount方法,其中主要做了两件事,创建 VNode和渲染VNode

先来看创建VNode:

  1. const vnode = createVNode(rootComponent, rootProps)

createVNode的函数如下:

  1. function createVNode(type, props = null
  2. ,children = null) {
  3. // 处理 props 相关逻辑,标准化 class 和 style
  4. if (props) {
  5. ...
  6. }
  7. // 对 vnode 类型信息编码
  8. const shapeFlag = isString(type)
  9. ? 1 /* ELEMENT */
  10. : isSuspense(type)
  11. ? 128 /* SUSPENSE */
  12. : isTeleport(type)
  13. ? 64 /* TELEPORT */
  14. : isObject(type)
  15. ? 4 /* STATEFUL_COMPONENT */
  16. : isFunction(type)
  17. ? 2 /* FUNCTIONAL_COMPONENT */
  18. : 0
  19. const vnode = {
  20. type,
  21. props,
  22. shapeFlag,
  23. // 一些其他属性
  24. }
  25. // 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
  26. normalizeChildren(vnode, children)
  27. return vnode
  28. }

上述代码的流程为:

  1. 对 props 做标准化处理
  2. 对 VNode 的类型信息编码
  3. 创建 VNode 对象,标准化子节点 children 。
  4. 返回VNode

3.3.2 渲染

再回到app.mount方法 ,创建完VNode,接下来是渲染VNode:

  1. // 创建根组件的 vnode
  2. const vnode = createVNode(rootComponent, rootProps)
  3. // 利用渲染器渲染 vnode
  4. render(vnode, rootContainer)

查看render方法:

  1. // 位置:runtime-core/renderer.ts
  2. const render = (vnode, container) => {
  3. if (vnode == null) {
  4. // 销毁组件
  5. if (container._vnode) {
  6. unmount(container._vnode, null, null, true)
  7. }
  8. } else {
  9. // 创建或者更新组件
  10. patch(container._vnode || null, vnode, container)
  11. }
  12. // 缓存 vnode 节点,表示已经渲染
  13. container._vnode = vnode
  14. }

主要是查看pach方法:

  1. const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
  2. // 如果存在新旧节点, 且新旧节点类型不同,则销毁旧节点
  3. if (n1 && !isSameVNodeType(n1, n2)) {
  4. ...
  5. }
  6. const { type, shapeFlag } = n2
  7. // 先通过type来判断选择处理方法
  8. switch (type) {
  9. case Text:
  10. // 处理文本节点
  11. break
  12. case Comment:
  13. // 处理注释节点
  14. break
  15. case Static:
  16. // 处理静态节点
  17. break
  18. case Fragment: //Vue3新增
  19. // 处理 Fragment 元素
  20. break
  21. default:
  22. // 通过 shapeFlag编码判断
  23. if (shapeFlag & 1 /* ELEMENT */) {
  24. // 处理普通 DOM 元素
  25. processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  26. }
  27. else if (shapeFlag & 6 /* COMPONENT */) {
  28. // 处理组件
  29. processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  30. }
  31. else if (shapeFlag & 64 /* TELEPORT */) {
  32. // 处理 TELEPORT,Vue3新增
  33. }
  34. else if (shapeFlag & 128 /* SUSPENSE */) {
  35. // 处理 SUSPENSE,Vue3新增
  36. }
  37. }
  38. }

patch接受的前 3 个参数:

  • n1 表示老的 VNode,为 null 时即为第一次挂载
  • n2 表示新的 VNode节点
  • container 为被挂载的DOM容器

处理组件

我们先查看处理组件的processComponent

  1. const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  2. if (n1 == null) {
  3. // 挂载组件
  4. mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  5. }
  6. else {
  7. // 更新组件
  8. updateComponent(n1, n2, parentComponent, optimized)
  9. }
  10. }

如果 n1 为 null,则执行挂载组件的逻辑,否则执行更新组件的逻辑。

先看mountComponent

  1. const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  2. // 创建组件实例
  3. const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
  4. // 设置组件实例
  5. setupComponent(instance)
  6. // 设置并运行带副作用的渲染函数
  7. setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
  8. }

这个函数主要是创建和设置组件实例,设置并运行带副作用的渲染函数setupRenderEffect

  1. const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
  2. // create reactive effect for rendering
  3. instance.update = effect(function componentEffect() {
  4. if (!instance.isMounted) {
  5. // beforeMount hook
  6. if (bm) { invokeArrayFns(bm) }
  7. // 渲染组件生成subTree的vnode
  8. const subTree = (instance.subTree = renderComponentRoot(instance))
  9. // 把子树 subTree挂载到 container 中
  10. patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
  11. // 保留渲染生成的子树根 DOM 节点
  12. initialVNode.el = subTree.el
  13. // mounted hook
  14. if (m) {queuePostRenderEffect(m, parentSuspense)}
  15. instance.isMounted = true
  16. }
  17. else {
  18. // 更新组件
  19. ...
  20. patch(prevTree, nextTree,
  21. hostParentNode(prevTree.el),
  22. getNextHostNode(prevTree),
  23. instance,
  24. parentSuspense,
  25. isSVG)
  26. ...
  27. }
  28. }, prodEffectOptions)
  29. }

effect函数为数据响应式相关的函数,这个可以查看Vue3的reactive办理,所谓副作用函数可以理解为当组件使用的数据发生变化时,effect传入的渲染函数会被重新执行,来触发组件的更新。

更新的核心逻辑是传入新旧VNode执行patch函数,其就是找出新旧VNode的不同,并找到合适的方式更新DOM。其中用到了大名鼎鼎的DOM diff算法,这里就先不讲了。

处理DOM

处理普通 DOM元素的processElementprocessComponent的逻辑相同,n1为null时挂载,不为时更新。

  1. const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  2. isSVG = isSVG || n2.type === 'svg'
  3. if (n1 == null) {
  4. //挂载元素节点
  5. mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  6. }
  7. else {
  8. //更新元素节点
  9. patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
  10. }
  11. }

查看mountElement

  1. const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  2. // 创建 DOM 元素节点,其实就是 document.createElement
  3. el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
  4. if (props) {
  5. // 处理 props,比如 class、style、event 等属性
  6. ...
  7. }
  8. if (shapeFlag & 8 /* TEXT_CHILDREN */) {
  9. // 处理子节点是纯文本的情况,其实就是 el.textContent = text
  10. hostSetElementText(el, vnode.children)
  11. }
  12. else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
  13. // 处理子节点是数组的情况
  14. mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
  15. }
  16. // 把创建的 DOM 元素节点挂载到 container 上
  17. hostInsert(el, container, anchor)
  18. }

该函数主要做了:

  1. 创建 DOM 元素节点
  2. 处理 props
  3. 处理 children
  4. 挂载 DOM 元素到 container 上。

其中,如果子节点是数组,则执行 mountChildren方法:

  1. const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => {
  2. for (let i = start; i < children.length; i++) {
  3. // 预处理 child
  4. const child = (children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]))
  5. // 递归 patch 挂载 child
  6. patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  7. }
  8. }

patch是递归调用的, 通过这种深度优先遍历树的方式,我们就可以构造完整的 DOM 树,完成组件的渲染和更新。

新特性介绍

在patch方法中,先通过type和shareFlag来判断选择处理VNode,其中有几个新常量,这里简单做个说明:

  • Fragment

    1. <template>
    2. <div>Hello</div>
    3. <div>World</div>
    4. </template>

    Vue2是没法这样创建组件,必须被一个标签包裹。而Vue3是可以的,生成的VNode是只有flag,没有tag的

    1. const elementVNode = {
    2. flags: VNodeFlags.FRAGMENT,
    3. tag: null,
    4. ....
    5. }
  • Teleport

    https://vue3js.cn/docs/zh/guide/teleport.html#teleport

Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML

  1. <!-- to 属性就是目标位置 -->
  2. <teleport to="#teleport-target">
  3. <div>test</div>
  4. </teleport>

这个标签的作用是当你在进行一个异步加载时,可以先提供一些静态组件作为显示内容,然后当异步加载完毕时再显示。

  1. <Suspense>
  2. <template #default>
  3. <UserProfile />
  4. </template>
  5. <template #fallback>
  6. <div>Loading...</div>
  7. </template>
  8. </Suspense>

3.4 Vue3的编译优化

Vue 是首先通过编译是把templete编译成render函数,然后再通过渲染生成DOM。

Vue3做到了在编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的。每个区块只需要追踪自身包含的动态节点。

还对diff算法进行重写,充分利用编译时的信息,使得性能得到了极大的提升。

官方的在线调试模板工具:https://vue-next-template-explorer.netlify.app/

Vue3还做了很多方面的优化,这里就不多介绍了(太菜,还看不懂)。

4.总结

本文主要介绍了Vue3 组件初始化以及渲染的过程,主要探究Vue源码的一个大体流程,对于响应式模块、编译器没有进行介绍,以后有时间进行整理,关于Vue3的新特性和优化以及对应的原理还需要时间进行学习和消化。

BTW:为更好的理解Vue的一个大体原理,根据一些课程和文章,写了一个mini-vue3的demo
https://github.com/mewcoder/codebase/blob/main/mini-vue3/index.html

最后来张图,拥抱Vue3吧!
image.png