Vue3追本溯源(一)入口函数

1,255 阅读4分钟

现如今前端的三大框架Vue、React、nodeJS是从事前端开发的必备技能,2020年9月随着Vue3的发布,Vue也迎来了更多开发者的目光,学习源码也要提上日程。这篇专栏将详细解析Vue3源码的主要流程以及用法的底层实现。

创建一个Vue实例

const Vue = createApp(/* options */).mount('#app')

可以看到Vue3的入口函数是调用了createApp这个方法,它是Vue3对外暴露的一个函数,一起来看下它的内部实现

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  // ...
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {}
  
  return app
}) as CreateAppFunction<Element>

createApp函数首先生成一个app对象,再将app中的mount方法解构出来,重新定义appmount方法,后续我们使用mount挂载时再详细解析,最后返回app对象。接下来先看下app对象是如何生成的。

app对象

const app = ensureRenderer().createApp(...args)

首先我们调用ensureRenderer()方法

function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  // ...
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    forcePatchProp: hostForcePatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    cloneNode: hostCloneNode,
    insertStaticContent: hostInsertStaticContent
  } = options
  
  // ...
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

可以看到ensureRenderer方法最终执行了baseCreateRenderer方法,创建了options对象,该对象中定义的一些属性是DOM操作的方法,例如节点的增加、删除、移位等等,后续解析VNode对象时会调用这些方法生成真正的DOM。方法最终返回了一个对象,当我们调用ensureRenderer返回的createApp属性时,就是调用了createAppAPI方法的返回值

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // ...
    const context = createAppContext()
    const installedPlugins = new Set()

    let isMounted = false
    const app: App = (context.app = {
      // ...
      _component: rootComponent as ConcreteComponent,
      //...
    })
    return app
  }
}

可以看到createAppAPI方法返回了一个createApp函数,所以最终是调用了createApp方法,此方法创建了一个app对象,定义了一些属性方法,例如mountunmount等等,最终将这个对象返回。

mount挂载

app对象生成之后,回看之前我们创建一个Vue实例时,在调用createApp方法之后会执行mount方法,也就是app.mount方法。接下来我们看下新的app.mount方法具体做了什么

// normalizeContainer 方法
function normalizeContainer(
  container: Element | ShadowRoot | string
): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container)
    // ...
    return res
  }
  // ...
  return container as any
}

// 重新定义的app.mount方法
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
  const container = normalizeContainer(containerOrSelector)
  if (!container) return

  const component = app._component
  if (!isFunction(component) && !component.render && !component.template) {
    component.template = container.innerHTML
    // ...
  }

  // clear content before mounting
  container.innerHTML = ''
  const proxy = mount(container, false, container instanceof SVGElement)
  if (container instanceof Element) {
    container.removeAttribute('v-cloak')
    container.setAttribute('data-v-app', '')
  }
  return proxy
}

该方法首先调用normalizeContainer函数,通过document.querySelector('#app')找到idappDOM节点(后续称之为root节点)。然后获取app_component对象,其实就是调用createApp方法时传入的options参数对象(包含data、methods、computed属性等等)。

if (!isFunction(component) && !component.render && !component.template) {
  component.template = container.innerHTML
}

判断传入的options参数不是函数类型、没有render属性 和 template属性,则root节点的内部HTML字符串赋值给options参数的template属性

container.innerHTML = ''

并将内部HTML字符串置为空。那现在页面上只有root节点,其内部的子节点都清空了,后续会将template字符串进行模版编译,重新添加子节点。

// container即为root节点
const proxy = mount(container, false, container instanceof SVGElement)

container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')

最后执行mount方法(老的app.mount方法,是在createAppAPI方法的返回结果函数中定义的),root节点移除的v-cloak属性,新增data-v-app属性。

总结

Vue3创建实例的方法开始,找寻它的入口函数。利用函数柯里化,先执行ensureRenderer方法,定义一些操作DOM节点所需要的函数、将虚拟DOM对象解析为真实DOM的方法等等。紧接着执行createApp方法,该方法定义了一个app对象,创建了一些属性,例如_component就是保存传入的options参数,定义了一些方法,例如mount、unmount等等,到此app对象就形成了。后续执行.mount('#app')方法,其实就是执行定义在app上的mount方法进行挂载。这里需要注意的是,原mount方法解构出来之后,定义了新的app.mount方法,在新方法中主要是找寻root根节点,将根节点内部的HTML字符串保存在template属性上(为后续的模版编译做准备),然后清空root节点内部,最后执行原mount方法进行挂载操作