Vue3相关

103 阅读3分钟

Q: Vue3相比较于Vue2升级的地方?

  • 代码库的文件夹结构变化:vue2的时候是把所有模块都放在/src下,例如

    • compile

    • core

    • platforms

    • shared

    • types

    而Vue3是基于monorepo的结构,在 packages下放了多个文件夹。

  • 类型语言的变化:Vue2采用的是flow,Vue3采用的是TypeScript

  • 源码在响应式方面的变化:Vue2基于defineProperty,Vue3基于Proxy API。

Vue3 setup处理逻辑

组件实例的设置流程 setupComponent

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

设置有状态的组件实例 setupStatefulComponent

runtime-core/src/component.ts

function setupStatefulComponent(
	instance: ComponentInternalInstance,
	isSSR: boolean
) {
	// 0. create render proxy property access cache
	instance.accessCache = Object.create(null)
	
    // 1. create public instance / render proxy
    // also mark it raw so it's never observed
    instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))

    // 2. call setup()
	
    const { setup } = Component
    if (setup) {

		// 如果setup函数带参数,创建一个setupContext
        const setupContext = (instance.setupContext =
	        setup.length > 1 ? createSetupContext(instance) : null)

		
		// 执行setup函数获取结果
	   const setupResult = callWithErrorHandling(
	      setup,
    	  instance,
	      ErrorCodes.SETUP_FUNCTION,
       	  [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
       )
		

		// 处理setup执行结果
	    handleSetupResult(instance, resolvedResult, isSSR)
	}
}

注意proxy 的get方法的取值优先级:

       switch (n) {
          case AccessTypes.SETUP:
            return setupState[key]
          case AccessTypes.DATA:
            return data[key]
          case AccessTypes.CONTEXT:
            return ctx[key]
          case AccessTypes.PROPS:
            return props![key]
          // default: just fallthrough
        }

finishComponentSetup

在handleSetupResult的最后,会执行这个函数完成组件实例的设置。

当组件没有定义的时候,也会执行finishComponentSetup去完成组件实例设置。

这个函数功能:

1、标准化模板或者渲染函数

Vue的runtime-only和runtime-compile两个版本的区别就在于,是否有注册compile方法。

在Vue3中,compile方法是通过外部注册的。

在finishComponentSetup函数中,我们先看instance.render是否存在,如果不存在,则开始标准化流程,这里主要是处理三种情况:

1)compile和组件template属性存在,render方法不存在。

这时候,会由runtime-compiled版本在JavaScript运行时进行模板编译,生成render函数。

2)compile和render方法不存在,template属性存在。

这时候,由于没有compile,用的是runtime-only版本,就会报一个警告来告诉用户,想要运行时编译就得使用runtime-compiled版本的Vue.js。

3)组件既没有写render函数,又没有写template模板。

这时候,需要报一个警告,告诉用户组件缺少了render函数或者template模板。

处理完以上情况后,就要把组件的render函数赋值给instance.render。组件渲染的时候,运行instance.render函数生成组件的子树Vnode。

2、兼容Options API

Vue3之所以可以兼容Options API语法,是因为Vue3里有一个applyOptions方法,实现了Vue2的Options API的功能。

大概是一下这些步骤:

  1. 处理全局mixin

  2. 处理extend

  3. 处理本地mixins

  4. props已经在外面处理过了

  5. 处理inject

  6. 处理methods方法

  7. 处理data

  8. 处理计算属性

  9. 处理watch

  10. 处理provide

  11. 处理组件

  12. 处理指令

  13. 处理生命周期option

Provide和Inject API的原理

provide其实是从当前组件实例开始,往上通过this.$parent.provide,并以此递归知道App根节点。

那么Inject的原理也就很好理解了,通过查过父组件的provide数据来获取key。

Vue3相比较于Vue2,提供了

import {provide, inject} from 'vue';

这样更加宽松的API结构,可以更好的获取数据。

转换生成AST

baseParse的实现

export function baseParse(
  content: string,
  options: ParserOptions = {}
): RootNode {
  // 创建解析上下文
  const context = createParserContext(content, options)
  const start = getCursor(context)
  // 解析子节点,并创建AST
  return createRoot(
    parseChildren(context, TextModes.DATA, []),
    getSelection(context, start)
  )
}

createParserContext

parseChildren

插槽:如何实现内容分发

**在父组件渲染阶段,子组件的插槽部分的DOM是不能渲染的。**需要通过某种方式保留下来,等到子组件渲染的时候再渲染。

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (openBlock(), _reateBlock(component_layout, null, {
		header: _withCtx(() => [
			_createVNode("h1", null, _toDisplayString(_ct.header), 1 /* TEXT */)
		]),
		default: _withctx(() => [
			createVNode("p", null, _toDisplayString(_ctx.main), 1 /* TEXT */)
		]),
		footer: _withCtx(() => [
			_createVNode ("p", null, _toDisplayString(_ctx footer), 1 /* TEXT */)
		]),
		_: 1
  }))
}

--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

export const initSlots = (
  instance: ComponentInternalInstance,
  children: VNodeNormalizedChildren
) => {
  if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
    const type = (children as RawSlots)._
    if (type) {
      // users can get the shallow readonly version of the slots object through `this.$slots`,
      // we should avoid the proxy object polluting the slots of the internal instance
      instance.slots = toRaw(children as InternalSlots)
      // make compiler marker non-enumerable
      def(children as InternalSlots, '_', type)
    } else {
      normalizeObjectSlots(
        children as RawSlots,
        (instance.slots = {}),
        instance
      )
    }
  } else {
    instance.slots = {}
    if (children) {
      normalizeVNodeSlots(instance, children)
    }
  }
  def(instance.slots, InternalObjectKey, 1)
}

initSlots的实现很简单,就是对了把插槽部分转换成函数,然后保留在instance.slots上。

在子组件中,插槽的渲染主要是通过renderSlot实现的。

插槽的实现,实际上就是一种延时渲染,把父组件中编写的插槽内容保存到一个对象上,并且把具体渲染DOM的代码用函数的方式封装,然后在子组件渲染的时候,根据插槽名在对象中找到对应的函数,然后执行这些函数做真正的渲染。