vue3 初始化的时候做了什么?

2.jpg

入口函数 createApp

vue2的入口函数是一个构造函数 Vue并且要实例化才能使用,到了vue3 入口函数变成一个单独的函数createApp我们可以打一个断点进入到入口函数内部去看看

(文件在:vue-next3.2/packages/runtime-dom/src/index.ts) image.png 发现其实内部调用的是另一个createApp,是由ensureRenderer函数调用返回的结果进行点调用,所以可以推断ensureRenderer返回的渲染器是一个对象,且一定会有一个createApp方法,

跳转到ensureRenderer内部去看看 image.png ensureRendere函数作用是确保全局renderer存在,存在直接返回,不存在调用createRenderer进行创建一个新的renderer

createRenderer内部比较庞大(2018行),这里就不上代码了,主要是分成三部分,1.拿到平台的操作方法(renderer支持自定义) 2.定义了一系列的操作方法,有用于diif的:patchpatchPropspatchElement,更新挂载DOM的:mountChildrenmountComponentupdateComponent,但是最终要的还是patchrender方法,最后的renderer看一下里面有啥 image.png

render:渲染函数

createApp:是由createAppAPI 执行之后产生的函数

hydrate:用于服务端渲染

应用实例的产生

image.png

77701e003f5b498aabe8b2e11b4267d5_tplv-k3u1fbpfcp-watermark.png

vue2中实例一开始就包含了很多$开头的方法,但是在vue3中,这些方法并不是直接挂在实例上,而是挂载到了组件实例的渲染上下文中,vue3的实例都是一个代理对象,可以说,vue3非常依赖于Proxy,所有的数据代理都由Proxy进行代理,

image.png

至于为什么会在外面也看到这些方法,其实因为vue3在创建根组件实例的时候,对所有的以$开头的方法和属性也都进行了代理,

image.png

再往后看发现实例上有一些方法,这些方法在vue2中是静态方法的,也就是需要Vue.xxx调用的,但是,vue3没有静态方法,全部由静态方法变为实例方法,变成app.xxx,这样可以非常好的利用摇树优化 不会出现 dead code 使用就打包 不使用就不打包并且vue3 中的 filter 以及被移除了,还有一些属性,如:_component用户写配置项,_container组件挂载容器,_context:应用程序上下文

image.png

初始化

image.png

createAppAPI创建的createApp执行完毕之后,会返回一个app实例,每一个根实例身上会有一个mount用于挂载,但是第一次执行执行的mount并不是实例上的mount,而是在入口函数createApp中,先把原本的函数存起来,然后扩展的mount,并且这个mount内部调用实例上的mount

image.png

这个扩展mount接受一个选择器参数,什么选择器都行,获取容器的方式querySelector() 推荐使用id选择器(一般都是#app),接下来就是获取模板,用户可能会直接卸载容器中,后面的就是对vue2的兼容验证了,之后就是清除模板中的内容,后面挂载需要一个空的容器,

执行实例上的mount,接受三个参数,rootContainer:容器 isHybrate:服务端渲染 isSVG 是否渲染的是SVG,函数内主要分为两步,根据组件生成VNode、将VNode渲染在容器中

创建VNode

d2a65eb6d6a545238475696672b2e481_tplv-k3u1fbpfcp-watermark.png

vue的VNode是由createVNode进行创建,但其实是调用_createNode去创建,在这个函数中:主要的任务就是把用户传递进来的一些属性进行处理,如classstyle,最主要的是对props处理和对组件本身进行第一次标记,shapeFlag,然后再传递给createBaseVNode,让它再进行根据属性进行创建,在createBaseVNode中先初始化一个公共的VNode,然后再通过外部传入的属性,进行修改,并且,VNode的一些公共属性就是在这里进行初始化的,这些标记都十分重要,是为之后的编译过程埋下了伏笔。

  • appContext:应用程序的上下文
  • shapeFlag:标明该VNode是啥,如:原生节点、组件(函数式组件或者状态组件)、文本子节点等
  • patchFlag:记录了VNode那些是动态的,
  • dynamicProps:需要监视哪些熟悉的变化
  • dynamicChildren:需要监视那些子节点
  • el:记录当前VNode的实际真实节点

....

之后就是对子节点进行处理了,以及对块树(black tree)的跟踪,这也是vue diff加快的原因之一,

  if (needFullChildrenNormalization) {
  // 标准化子节点
    normalizeChildren(vnode, children)
    // normalize suspense children
    if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
      ;(type as typeof SuspenseImpl).normalize(vnode)
    }
  } else if (children) {
    // compiled element vnode - if children is passed, only possible types are
    // string or Array.
    // 说明了子元素 只能是文本子节点或者是数组子节点
    vnode.shapeFlag |= isString(children)
      ? ShapeFlags.TEXT_CHILDREN
      : ShapeFlags.ARRAY_CHILDREN
  }

// track vnode for block tree
  if (
    isBlockTreeEnabled > 0 &&
    // avoid a block node from tracking itself
    !isBlockNode &&
    // has current parent block
    currentBlock &&
    // presence of a patch flag indicates this node needs patching on updates.
    // component nodes also should always be patched, because even if the
    // component doesn't need to update, it needs to persist the instance on to
    // the next vnode so that it can be properly unmounted later.
    (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    currentBlock.push(vnode)
  }
复制代码

最后把创建好的VNode返回(最后的处理是对 vue2的兼容,这里就不去赘述),相对于vue2的VNode,去掉了tag等一系列,反倒多了一个type属性,里面记录着VNode的模板以及一些配置,但是两个VNode产生的时间节点不一样,vue3是直接创建的,而vue2是先进行编译后才会产生VNode,最终交给patch去渲染的的VNode都是由生成的render函数去生成的,

挂载解析

image.png

mount函数中也调用的render,但是这个render函数是renderer.ts中的,其作用就是将VNode渲染到容器中,核心就是打补丁,然后将最新的vnode存储,用于下一次对比,在代码中,有一个处理 container._vnode || null 这是为了兼容第一次初始化渲染,没有旧的vnode

path函数接受很多参数,但是最重要的参数只有三个,n1n2container,分别是旧的虚拟DOM、新的虚拟DOM、挂载容器,在patch函数内部,会对传入的虚拟的DOM先进行处理,后进行template的解析,生成render函数,

核心逻辑

image.png

处理的节点类型有:文本、注释、静态节点、Fargment(一系列没有根节点,并排排列的节点)、elementcomponentteleportsuspense,在第一次进入patch函数时,type是根组件的配置对象,所以会先执行解析组件逻辑(if(shapeFlag & ShapeFlags.COMPONENT)),也就是会先执行processComponent

image.png

在第一次n1肯定是null,会走if的逻辑,第二个if是内置组件keepAlive缓存的组件,初始化显然不是,也就是走else,执行mountComponent,

渲染函数的产生

挂载组件一共分为以下几步

  1. 创建组件实例

image.png image.png image.png

instance是组件实例,这里不再是一些$xxx的方法,而是一些属性,其中一些属性看过vue2源码的人会比较眼熟,可以一眼就看出来,如bc = beforeCreatebm = beforeMountbu = beforeUpdatebum = beforeUnmount,其中最终的要的就是ctx属性,里面就是$xxx方法,还有一些其他重要属性,如typevnodeslotsprops

  1. 安装组件

image.png

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)
  // 初始化属性和插槽
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)

  const setupResult = isStateful
  // 安装有状态的组件
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}
复制代码

在这个函数中,主要工作就是进行,初始化属性和插槽,以及安装有状态的组件,在这个时候,setup还没执行,props就已经初始化了,说明props的数据优先于组件本身的数据,之后就是调用setupStatefulComponent

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  // Component 是当前组件的配置
  const Component = instance.type as ComponentOptions

  if (__DEV__) {
    if (Component.name) {
      validateComponentName(Component.name, instance.appContext.config)
    }
    if (Component.components) {
      const names = Object.keys(Component.components)
      for (let i = 0; i < names.length; i++) {
        validateComponentName(names[i], instance.appContext.config)
      }
    }
    if (Component.directives) {
      const names = Object.keys(Component.directives)
      for (let i = 0; i < names.length; i++) {
        validateDirectiveName(names[i])
      }
    }
    if (Component.compilerOptions && isRuntimeOnly()) {
      warn(
        `"compilerOptions" is only supported when using a build of Vue that ` +
          `includes the runtime compiler. Since you are using a runtime-only ` +
          `build, the options should be passed via your build tool config instead.`
      )
    }
  }
  // 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))
  if (__DEV__) {
    exposePropsOnRenderContext(instance)
  }
  // 2. call setup()
  // 
  const { setup } = Component
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    setCurrentInstance(instance)
    pauseTracking()
    // 存在并执行setup 并往里面传递一些参数 传递的是 props 和 ctx
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    resetTracking()
    unsetCurrentInstance()

    if (isPromise(setupResult)) {
      setupResult.then(unsetCurrentInstance, unsetCurrentInstance)

      if (isSSR) {
        // return the promise so server-renderer can wait on it
        return setupResult
          .then((resolvedResult: unknown) => {
            handleSetupResult(instance, resolvedResult, isSSR)
          })
          .catch(e => {
            handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
          })
      } else if (__FEATURE_SUSPENSE__) {
        // async setup returned Promise.
        // bail here and wait for re-entry.
        instance.asyncDep = setupResult
      } else if (__DEV__) {
        warn(
          `setup() returned a Promise, but the version of Vue you are using ` +
            `does not support it yet.`
        )
      }
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    // 处理选项等事务
    finishComponentSetup(instance, isSSR)
  }
}
复制代码

这里会先代理上下文,(Component是组件的配置)再从配置中去除setup,带错误处理的执行,会传递一些参数,传递是propsctx如果返回的是Promise,就代表是服务端渲染,返回以便服务器渲染器可以等待它,如果是异步服务端渲染,先存储起来,等待返回,不是,就是正常的,就进行后续的setup返回的结果处理

image.png

有两种情况 返回的是一个对象或者是返回的是一个函数,如果是对象(数据,并且假设可以从渲染模板中得到渲染函数),直接进行代理,并设置在组件实例上,而函数,是普通的render或者服务端渲染的ssrRender也是存储在组件实例上,返回的对象的代理方式类似ref,最后会调用finishComponentSetup去处理option API

最先转换v2的是渲染函数,在v3中,v2的渲染函数已经规范为函数式组件,具体兼容过程可以去看convertLegacyRenderFn函数 在renderer.ts, 在服务端渲染的情况下,确认配置中的render function 如果render function 不存在,全部变为NOOP函数,以便继承来自mixins/extend的函数,在客户端渲染且实例上没有render function(一般用户不会手写render function,可以在setup中设置)的情况下,进行模板编译生成渲染函数,

但是为了兼容v2的内联模板,不存在直接找整个模板,找不到模板不会进行模板编译,在编译模板之前,会进行编译的配置的收集(v2和v3都会被收集),拿到最终配置选项,执行complie(实际上执行的是baseComplie)进行编译,进行编译三部曲

编译三部曲

prefixIdentifiers 这个参数是为防止(将vue的mode设置为module的情况)在严格模式下使用with(this)(在module模式下,默认是严格模式,不允许使用with(this))

image.png

image.png

编译三部曲分为:1.将模板转换为AST语法树 2.修饰AST语法树 3.生成render函数

将模板转换为AST语法树

将模板转换为AST语法树 (vue3编译是深度搜索优先,会优先将一个节点中的所有的子节点编译完毕,才会进行下一个节点的编译) 调用的是baseParse,但是其核心在parseChildren,在parseChildren中(在vue2中,是采用大量的正则表 达式进行匹配,vue3采用的是函数式的方式),代码比较庞大,这里一部分一部分的看,在此之前先介绍几个变量,

ancestors:存储的是按顺序的父节点的数组,而parent希望拿到的就是离我当编译位置最近的父元素, 在后面方便校验是否编译完上一个节点中的子节点,HTML模板是双标签,分为开始和结束,编译最先拿到的是开始标 签,后面编译到结束标签也好告诉程序,这个节点编译完毕

ns:当前节点类型

nodes:存储编译完的节点,

delimitersimage.png

image.png 主要分为 插值、文本、标签, 最先判断是不是插值,通过使用parseInterpolation解析,

然后是标签以打头的<的内容,如果使用正则匹配为<p<h1等,使用parseElement进行解析

image.png image.png

如果上两种方式都没有找到,就代表是普通文本,使用parseText解析

image.png

最后进行v2空白处理,根据处理结果,进行返回, image.png

最后的AST语法树长这样

image.png

转换AST语法树

单纯的依靠ast语法树无法生成render,需要拿到一些方法和属性,如函数缓存、静态节点,以及指令转换、节点转换等工具函数,对由模板生成的 AST 进行转换

image.png image.png image.png image.png

生成render函数

得到了最终的ast语法树,就可以传入generate函数中进行生成render,函数中会根据ast语法树中的一些标记做优化,如静态节点提升,静态属性提升,函数缓存,内联模板等

export function generate(
  ast: RootNode,
  options: CodegenOptions & {
    onContextCreated?: (context: CodegenContext) => void
  } = {}
): CodegenResult {
// 生成上下文
  const context = createCodegenContext(ast, options)
  if (options.onContextCreated) options.onContextCreated(context)
  const {
    mode,
    push,
    prefixIdentifiers,
    indent,
    deindent,
    newline,
    scopeId,
    ssr
  } = context

  const hasHelpers = ast.helpers.length > 0
  // 是否可以使用 with(this)
  const useWithBlock = !prefixIdentifiers && mode !== 'module'
  const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
  const isSetupInlined = !__BROWSER__ && !!options.inline

  // preambles
  // in setup() inline mode, the preamble is generated in a sub context
  // and returned separately.
  const preambleContext = isSetupInlined
    ? createCodegenContext(ast, options)
    : context
  if (!__BROWSER__ && mode === 'module') {
    genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
  } else {
    // 静态提升
    genFunctionPreamble(ast, preambleContext)
  }
  // enter render function
  const functionName = ssr ? `ssrRender` : `render`
  // 服务端渲染和客户端渲染
  const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
  if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
    // binding optimization args
    args.push('$props', '$setup', '$data', '$options')
  }
  const signature =
    !__BROWSER__ && options.isTS
      ? args.map(arg => `${arg}: any`).join(',')
      : args.join(', ')

  if (isSetupInlined) {
    push(`(${signature}) => {`)
  } else {
    push(`function ${functionName}(${signature}) {`)
  }
  indent()

  if (useWithBlock) {
    push(`with (_ctx) {`)
    indent()
    // function mode const declarations should be inside with block
    // also they should be renamed to avoid collision with user properties
    // 重命名创建函数 一些方法 
    if (hasHelpers) {
      push(
        `const { ${ast.helpers
          .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
          .join(', ')} } = _Vue`
      )
      push(`\n`)
      newline()
    }
  }

  // generate asset resolution statements 生成配置资产列表,方便以后使用
  if (ast.components.length) {
    genAssets(ast.components, 'component', context)
    if (ast.directives.length || ast.temps > 0) {
      newline()
    }
  }
  if (ast.directives.length) {
    genAssets(ast.directives, 'directive', context)
    if (ast.temps > 0) {
      newline()
    }
  }
  if (__COMPAT__ && ast.filters && ast.filters.length) {
    newline()
    genAssets(ast.filters, 'filter', context)
    newline()
  }

   // 拿出vue中的方法
  if (ast.temps > 0) {
    push(`let `)
    for (let i = 0; i < ast.temps; i++) {
      push(`${i > 0 ? `, ` : ``}_temp${i}`)
    }
  }
  
  if (ast.components.length || ast.directives.length || ast.temps) {
    push(`\n`)
    newline()
  }

  // generate the VNode tree expression
  if (!ssr) {
    push(`return `)
  }
  if (ast.codegenNode) {
    genNode(ast.codegenNode, context)
  } else {
    push(`null`)
  }

  if (useWithBlock) {
    deindent()
    push(`}`)
  }

  deindent()
  push(`}`)

  return {
    ast,
    code: context.code,
    preamble: isSetupInlined ? preambleContext.code : ``,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined
  }
}
复制代码

到最后生成的函数还是字符串,需要在后面进行转换成函数,至此,整个编译流程结束

挂载开始

安装组件

image.png

回到之前finishComponentSetup函数执行栈,开始对v2的配置项处理,调用的是applyOptions(instance)

data选项的处理 image.png

computed选项处理 image.png 后面还有一些选项处理,在选项处理完毕之后,就是处理生命周期函数,约束暴露,最后执行完毕,回到mountComponent执行栈,开始安装渲染依赖setupRenderEffect

setupRenderEffect内部定义了一个函数componentUpdateFn,服务于更新和挂载两个项目,在内部,会有两种调用patch方式,区别就在于,是否存在旧的VNode,

// 初始化渲染
patch(
    null,
    subTree,
    container,
    anchor,
    instance,
    parentSuspense,
    isSVG
)

// 更新渲染
patch(
  prevTree,
  nextTree,
  // parent may have changed if it's in a teleport
  hostParentNode(prevTree.el!)!,
  // anchor may have changed if it's in a fragment
  getNextHostNode(prevTree),
  instance,
  parentSuspense,
  isSVG
)
复制代码

而新的VNode是调用renderComponentRoot产生的,在renderComponentRoot内部,分为函数式组件和有状态的组件两种,但一定是得到了render,如果用户没有设置,vue会先进行编译后进行赋值,最后把render的执行结果返回,且如果是函数组件,会将attrslotsemit传递进去,

安装依赖

在执行挂载更新的前后,会分别执行v2的生命周期函数和v3的生命周期函数,而setupRenderEffect等同于v2中的mountComponent,在v2中响应式依赖使用的是渲染watcher,而v3中通过effectsetupRenderEffect内部执行一个effecteffect是将传入的fn和它内部调用的响应式数据产生一个依赖映射关系,在v3.2之后,这里就改成使用实例化一个ReactiveEffect对象来产生依赖映射关系,在最后在把生成的update函数默认执行,内部就会调用componentUpdateFn,进行挂载,vue3初始化流程结束

挂载流程如下: mount() => processComponent() => mountComponent() => setupComponent() 然后 setupComponent()分别调用setupStatefulComponent()安装有状态的组件 setupRenderEffect() 依赖收集

最后:欢迎大佬指导和评论

猜你喜欢

转载自juejin.im/post/7032639754155851807