[笔记] Vue3.0源码解析

2021-02-18  本文已影响0人  木子士心呦

Vue 3.0 源码解析

源码优化

性能优化

语法 API 优化

01 | 组件渲染:vnode 到真实 DOM 是如何转变的?

应用程序初始化
在 Vue.js 内部,一个组件想要真正的渲染生成 DOM,还需要经历“创建 vnode - 渲染 vnode - 生成 DOM” 这几个步骤。
一个组件可以通过“模板加对象描述”的方式创建,组件创建好以后是如何被调用并初始化的呢?
在 Vue.js 3.0 中还导入了一个 createApp,其实这是个入口函数,它是 Vue.js 对外暴露的一个函数。
createApp 主要做了两件事情:创建 app 对象和重写 app.mount 方法。

  1. 创建 app 对象
    使用 ensureRenderer().createApp() 来创建 app 对象
const app = ensureRenderer().createApp(...args)

ensureRenderer() 用来创建一个渲染器对象,用 ensureRenderer() 来延时创建渲染器,这样做的好处是当用户只依赖响应式包的时候,就不会创建渲染器,因此可以通过 tree-shaking 的方式移除核心渲染逻辑相关的代码。

  1. 重写 app.mount 方法
    为什么要重写这个方法,而不把相关逻辑放在 app 对象的 mount 方法内部来实现呢?
    这是因为 Vue.js 不仅仅是为 Web 平台服务,它的目标是支持跨平台渲染,而 createApp 函数内部的 app.mount 方法是一个标准的可跨平台的组件渲染流程:标准的跨平台渲染流程是先创建 vnode,再渲染 vnode。
mount(rootContainer) {
  // 创建根组件的 vnode
  const vnode = createVNode(rootComponent, rootProps)
  // 利用渲染器渲染 vnode
  render(vnode, rootContainer)
  app._container = rootContainer
  return vnode.component.proxy
}

核心渲染流程:创建 vnode 和渲染 vnode

  1. 创建 vnode
    vnode 有什么优势呢?为什么一定要设计 vnode 这样的数据结构呢?

Vue.js 内部是如何创建这些 vnode 的呢?
通过 createVNode 函数创建了根组件的 vnode :对 props 做标准化处理、对 vnode 的类型信息编码、创建 vnode 对象,标准化子节点 children 。

const vnode = createVNode(rootComponent, rootProps)
function createVNode(type, props = null
,children = null) {
  if (props) {
    // 处理 props 相关逻辑,标准化 class 和 style
  }

  // 对 vnode 类型信息编码
  const shapeFlag = isString(type)
    ? 1 /* ELEMENT */
    : isSuspense(type)
      ? 128 /* SUSPENSE */
      : isTeleport(type)
        ? 64 /* TELEPORT */
        : isObject(type)
          ? 4 /* STATEFUL_COMPONENT */
          : isFunction(type)
            ? 2 /* FUNCTIONAL_COMPONENT */
            : 0
  const vnode = {
    type,
    props,
    shapeFlag,
    // 一些其他属性
  }
  // 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
  normalizeChildren(vnode, children)
  return vnode
}
  1. 渲染 vnode

02 | 组件更新:完整的 DOM diff 流程是怎样的?(上)

03 | 组件更新:完整的 DOM diff 流程是怎样的?(下)

04 | Setup:组件渲染前的初始化过程是怎样的?

<template>
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    })
    function increment() {
      state.count++
    }
    return {
      state,
      increment
    }
  }
}

</script>

模板中引用到的变量 state 和 increment 包含在 setup 函数的返回对象中,那么它们是如何建立联系的呢?
在Vue.js 2.x 编写组件的时候,会在 props、data、methods、computed 等 options 中定义一些变量。在组件初始化阶段,Vue.js 内部会处理这些 options,即把定义的变量添加到了组件实例上。等模板编译成 render 函数的时候,内部通过 with(this){} 的语法去访问在组件实例中的变量。

模板中引用到的变量 state 和 increment 包含在 setup 函数的返回对象中,那么它们是如何建立联系的呢?
到了 Vue.js 3.0,既支持组件定义 setup 函数,而且在模板 render 的时候,又可以访问到 setup 函数返回的值,这是如何实现的?
Vue.js 2.x 使用 new Vue 来初始化一个组件的实例,到了 Vue.js 3.0,我们直接通过创建对象去创建组件的实例。这两种方式并无本质的区别,都是引用一个对象,在整个组件的生命周期中去维护组件的状态数据和上下文环境。

创建和设置组件实例
组件的渲染流程:创建 vnode 、渲染 vnode 和生成 DOM。
渲染 vnode 的过程主要就是在挂载组件,挂载组件的代码主要做了三件事情:创建组件实例、设置组件实例和设置并运行带副作用的渲染函数。

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

使用createComponentInstance 方法,创建组件实例

function createComponentInstance (vnode, parent, suspense) {
  // 继承父组件实例上的 appContext,如果是根组件,则直接从根 vnode 中取。
  const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
  const instance = {
    // 组件唯一 id
    uid: uid++,
    // 组件 vnode
    vnode,
    …
  }

  // 初始化渲染上下文
  instance.ctx = { _: instance }
  // 初始化根组件指针
  instance.root = parent ? parent.root : instance
  // 初始化派发事件方法
  instance.emit = emit.bind(null, instance)
  return instance
}

组件实例 instance 上定义了很多属性.
Vue.js 2.x 使用 new Vue 来初始化一个组件的实例,到了 Vue.js 3.0,我们直接通过创建对象去创建组件的实例。这两种方式并无本质的区别,都是引用一个对象,在整个组件的生命周期中去维护组件的状态数据和上下文环境。
组件实例的设置流程就是对setup 函数的处理。

function setupComponent (instance, isSSR = false) {
  const { props, children, shapeFlag } = instance.vnode
  // 判断是否是一个有状态的组件
  const isStateful = shapeFlag & 4
  // 初始化 props
  initProps(instance, props, isStateful, isSSR)
  // 初始化 插槽
  initSlots(instance, children)
  // 设置有状态的组件实例
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  return setupResult
}

setupStatefulComponent 函数,它主要做了三件事:创建渲染上下文代理、判断处理 setup 函数和完成组件实例设置。

05 | 响应式:响应式内部的实现原理是怎样的?(上)

06 | 响应式:响应式内部的实现原理是怎样的?(下)

07 | 计算属性:计算属性比普通函数好在哪里?

08 | 侦听器:侦听器的实现原理和使用场景是什么?(上)

09 | 侦听器:侦听器的实现原理和使用场景是什么?(下)

10 | 生命周期:各个生命周期的执行时机和应用场景是怎样的?

11 | 依赖注入:子孙组件如何共享数据?

12 | 模板解析:构造 AST 的完整流程是怎样的?(上)

13 | 模板解析:构造 AST 的完整流程是怎样的?(下)

14 | AST 转换:AST 节点内部做了哪些转换?(上)

15 | AST 转换:AST 节点内部做了哪些转换?(下)

16 | 生成代码:AST 如何生成可运行的代码?(上)

17 | 生成代码:AST 如何生成可运行的代码?(下)

18 | Props:Props 的初始化和更新流程是怎样的?

19 | 插槽:如何实现内容分发?

20 | 指令:指令完整的生命周期是怎样的?

21 | v-model:双向绑定到底是怎么实现的?

22 | Teleport 组件:如何脱离当前组件渲染子组件?

23 | KeepAlive 组件:如何让组件在内存中缓存和调度?

24 | Transition 组件:过渡动画的实现原理是怎样的?(上)

25 | Transition 组件:过渡动画的实现原理是怎样的?(下)

26 | Vue Router:如何实现一个前端路由?(上)

27 | Vue Router:如何实现一个前端路由?(下)

上一篇 下一篇

猜你喜欢

热点阅读