Vue3 源码学习 Day1

141 阅读4分钟

Vue 源码学习记录

Day 1

今日目标:

  • 环境搭建
  • 实例创建过程
  • 实例挂在过程

环境搭建

  1. 克隆项目
git clone git@github.com:vuejs/core.git
  1. 安装依赖
npm i pnpm -g
cd ./core
pnpm install
  1. 改变脚本
// package.json scripts中
"dev": "node scripts/dev.js --sourcemap"

sourcemap是为了解决开发代码与实际运行代码不一致时帮助我们debug到原始开发代码的技术。

  1. 运行
pnpm run dev

实例创建过程

在使用vue3过程中,在入口文件中,我们一般会见到如下代码:

createApp(App).mount("#app");

那么在createApp中,到底发生了什么?

根据浏览器中打断点和单步调试的功能,我们轻易找到了 createApp 是 runtime-dom/src/index.ts 中的方法。

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  ...
}

接着,我们看 ensureRenderer 这个方法,这里的 renderer 是一个单例的变量,一开始为undefined,因此调用 createRenderer 来创建一个 renderer,那么这里的 app 实际上是 renderer.createApp(...args)

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

那接着来看 createRenderer,经过断点调试,发现这个方法在 runtime-core/src/renderer.ts 中:

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

而这个 createRenderer 又调用了 baseCreateRenderer,那么我们来看 baseCreateRenderer,由于这个函数的代码量实在过于庞大,我们从简看它的 return。

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  ...
  return {
    render,         // 把接收到的vnode转换成dom,追加到宿主元素
    hydrate,        // 服务端渲染,暂且不看
    createApp: createAppAPI(render, hydrate)    // 重点
  }
}

到这里,我们发现 renderer.createApp(...args) 实际上是 renderer.createAppAPI(render, hydrate)(...args),那我们接着来看createAppAPI。它是在 runtime-core/src/apiCreateApp.ts 中。它 return 的是一个名为 createApp(这个createApp不同于上面那个createApp) 的 function。它的作用返回一个 App 的实例,这个实例上有use,mixin,mount等等的方法。

function createAppAPI(render,hydrate) {
  return function createApp(rootComponent, rootProps = null) {
    ...
    const context = createAppContext()      // 创建一个context
​
    let isMounted = false
​
    const app: App = (context.app = {
      ...
      mount(rootContainer,isHydrate,isSVG) {
          ...
        } else if (__DEV__) {
          ...
        }
      },
      ...
    })
​
    ...
​
    return app
  }
}

当上面的这个createApp执行完,此时的调用栈应该回到最外层的createApp中。我们接着看最外层的createApp。

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
​
  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }
​
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    ... // 封装一下根节点的mount
  }
​
  return app
})

通过阅读上面的代码,其实createApp干的事情有以下几点:

  • 调用createApp这个函数 -> 调用 ensureRenderer() -> 调用 baseCreateRenderer()
  • 通过第一步的结果生成一个renderer(渲染器)
  • renderer是一个对象
{
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
}
  • 调用renderer上面的 createApp(不是第一步中的createApp)方法,这个方法返回一个App类型的实例
  • 封装根节点的mount
  • 通过最外层的createApp将这个App实例返回

实例挂载过程

搞清楚了createApp干的事情之后,自然就要搞清楚 mount 干的事情。实际上mount调用的就是上面App实例上的mount。而这个mount就是上面createApp中的 app.mount

  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)   
    // 标准化容器, containerOrSelector 是 "#app"
    // 这里传进一个选择器,normalizeContainer返回一个dom
    if (!container) return
​
    const component = app._component
    // 这里 component实际上是 createApp中传进来的对象,有setup 和 directive
    
    
    if (!isFunction(component) && !component.render && !component.template) {
      // __UNSAFE__
      // Reason: potential execution of JS expressions in in-DOM template.
      // The user must make sure the in-DOM template is trusted. If it's
      // rendered by the server, the template should not contain any user data.
      // 这里如果没有template,将innerHTML赋值给template
      component.template = container.innerHTML
      // 2.x compat check
      if (__COMPAT__ && __DEV__) {
        for (let i = 0; i < container.attributes.length; i++) {
          const attr = container.attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null
            )
            break
          }
        }
      }
    }
​
    // clear content before mounting
    container.innerHTML = ''
    
    // 重点是这一行,这里调用了mount函数
    // 而这个mount函数是之前createApp返回的app实例里的那个mount函数
    // container是#app的dom
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

在来看mount的实现:

mount(
  rootContainer: HostElement,       // dom
  isHydrate?: boolean,              // false
  isSVG?: boolean                   // false
): any {
  
  // 这个isMounted初始化是false
  if (!isMounted) {
    const vnode = createVNode(                  // 创建vnode
      rootComponent as ConcreteComponent,       
      // rootComponent:组件对象,有template、setup、directive
      rootProps
    )
    // store app context on the root VNode.
    // this will be set on the root instance on initial mount.
    // 这个context是通过createAppApi中的CreateAppContext调用来生成的一个初始化对象
    vnode.appContext = context
​
    
    if (isHydrate && hydrate) {
      // 服务端渲染,不看
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      // 其实重点是render函数,这个render函数是通过renderer渲染器的一个属性(方法)
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    app._container = rootContainer
    // for devtools and telemetry
    ;(rootContainer as any).__vue_app__ = app
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      app._instance = vnode.component
      devtoolsInitApp(app, version)
    }
    
    // 返回一个proxy
    return getExposeProxy(vnode.component!) || vnode.component!.proxy
  } else if (__DEV__) {
    ...
  }
},

重点我们来看render函数,在runtime-core/src/renderer.ts中:

const render: RootRenderFunction = (vnode, container, isSVG) => {
  // vnode是虚拟节点
  // container是#app
​
  if (vnode == null) {
    // 卸载组件
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    // 重点是这个patch函数,这个是虚拟dom相关的知识点,先不看,
    // 暂且可以理解为打补丁,将vnode更新到contaienr上去
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  
  // 异步更新相关
  // 大致看了下,就是生成一个单例activePostFlushCbs,然后对这个Cbs队列进行更新
  flushPostFlushCbs()
  container._vnode = vnode
}

所以mount干的事情有以下几点:

  • normalizeContainer,标准化容器
  • 调用mount
  • 创建虚拟节点vnode
  • vnode作为参数传给render方法
  • render调用patch方法,更新页面
  • 更新异步任务队列
  • 将vnode挂载到container上去
  • 设置isMounted为true
  • 返回一个proxy