Vue3源码【三】—— createApp执行流程源码分析

使用

在vue3当中是通过createApp将页面给挂在到index.html文件根元素下。下面是一个使用的例子,那么他是怎么运行的呢?又是怎么做的一个链式调用呢?下面就来一一分析

createApp(App)  // APP是一个vue组件
  .use(ElementPlus, { // 注册El-plus组件
    locale: zhCn,
    size: 'small',
    zIndex: 3000,
  })
  .use(router)       // 注册Vue-Router
  .use(ContextMenu)   // 一个右键菜单组件
  .use(createPinia()) // 注册pinia
  .mixin(drawMixin)  // 混入
  .provide('M', '1')  // 
  .directive('color', (el, binding) => {     // 添加自定义指令
    el.style.color = binding.value
  })
  .directive('load', loadingDirective)
  .use(i18nPlugin, {
    greetings: {
      hello: '你好!'
    }
  })
  .mount("#app");    // 挂载

createAppAPI

源码位于packages/runtime-core/src/apiCreateApp.ts

  • 这里进行了一些简化,可以看到在创建应用程序时,会创建一个App对象,然后返回一个createApp函数,这个函数接收两个参数,一个是根组件,一个是根组件的属性。
  • 在得到app实例之后,他里面有很多方法,比如use、mixin、component、directive等。他每一个方法都会返回app,这也就是上面可以使用链式调用的原因
  • 最后就是一个mount方法,这个方法接收一个参数,一个是挂载的元素。将根组件挂载到挂载元素上。这样就完成了整个vue3的初始化流程。
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction,
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    if (!isFunction(rootComponent)) {
      rootComponent = extend({}, rootComponent)
    }

    if (rootProps != null && !isObject(rootProps)) {
      // 传递给app.mount必须是一个对象
      rootProps = null
    }

    const context = createAppContext()
    const installedPlugins = new WeakSet()

    let isMounted = false

    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,

      use(plugin: Plugin, ...options: any[]) {
      },

      mixin(mixin: ComponentOptions) {
      },

      component(name: string, component?: Component): any {
      },

      directive(name: string, directive?: Directive) {
      },

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        namespace?: boolean | ElementNamespace,
      ): any {
      },

      unmount() {
      },

      provide(key, value) {
      },

      runWithContext(fn) {
      },
    })

    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }

    return app
  }
}

use 使用组件库

先看一下app的use方法的源码。这里就是看使用use方法传递过来的组件当中有没有install方法,如果有的话,就执行install方法,如果没有的话,就执行组件本身。

const app: App = (context.app = {
  use(plugin: Plugin, ...options: any[]) {
    if (installedPlugins.has(plugin)) {
      // 插件已被应用
    } else if (plugin && isFunction(plugin.install)) {
      // 判断组件当中的install是不是一个函数
      installedPlugins.add(plugin);
      plugin.install(app, ...options);
    } else if (isFunction(plugin)) {
      // 看组件是不是函数
      installedPlugins.add(plugin);
      plugin(app, ...options);
    }
    return app;
  }
});

顺便看一下el-plus的install方法。就是去执行makeInstaller.install方法,遍历el-plus当中的组件通过app.use©进行全局注册。
app是vue3当中创建app对象,然后options是use时传递的配置,会将所有的配置都通过provideGlobalConfig弄成全局的provide。后面我们再看provide的原理

var installer = makeInstaller([...Components, ...Plugins]);

const makeInstaller = (components = []) => {
  const install = (app, options) => {
    if (app[INSTALLED_KEY])
      return;
    app[INSTALLED_KEY] = true;
    components.forEach((c) => app.use(c));
    if (options)
      provideGlobalConfig(options, app, true);
  };
  return {
    version,
    install
  };
};

Provide & Inject

全局Provide

在给app添加provide方法时,其实做的就是将key和value添加到provides对象当中。这些都在根元素上。

const app: App = (context.app = {
  provide(key, value) {
    context.provides[key as string | symbol] = value;
    return app;
  }
});

Provide源码分析

先看看provide的源码。源码位于:packages/runtime-core/src/apiInject.ts

在使用provide的时候,会将key和value添加到provides对象当中。这个是到父级别的元素上。
在创建组件实例的时候(createComponentInstance),也会将provides对象添加到父组件实例上。
这也就是在兄弟组件之间去取值时能取到的原因,会从父组件实例的provides对象中取值。

export function provide<T, K = InjectionKey<T> | string | number>(
  key: K,
  value: K extends InjectionKey<infer V> ? V : T
) {
  if (!currentInstance) {
    // 组件实例不存在,provide只能在setup函数中调用
  } else {
    // 数据都会存到组件实例上 provides
    let provides = currentInstance.provides;
    // 默认情况下,实例继承其父对象的provides对象。但当它需要提供自己的价值观时,它会创建own提供对象使用parent提供对象作为原型。
    // 通过这种方式,在inject中,我们可以简单地从direct中查找注射parent并让原型链来完成工作。
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides;
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides);
    }
    provides[key as string] = value;
  }
}

// 组件实例
export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null,
) {
  const type = vnode.type as ConcreteComponent
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext

  const instance: ComponentInternalInstance = {
    // 还有别的属性 这里先都不看
    provides: parent ? parent.provides : Object.create(appContext.provides),
  }

  return instance
}

inject源码分析

inject的作用是注入数据,该数据来自于它的祖先组件 provide方法提供的数据

如果在一个组件中使用 inject(key, ‘a’)方法,那么它会先从其父组件的 provides对象本身去查找这个 key,如果找到了就返回对应的数据,如果没有找到,则通过
provides的原型去查找这个 key,此时的 provides的原型指向的就是它的父级 provides对象。实际上,inject查找数据的方法其实就是利用了js中原型链查找方式。

export function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false
) {
  // 回退到 currentRenderingInstance。以便可以在中调用
  const instance = currentInstance || currentRenderingInstance;

  if (instance || currentApp) {
    // 如果实例位于根目录,则回退到appContext的provides
    const provides = instance
      ? instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides
      : currentApp!._context.provides;

    if (provides && (key as string | symbol) in provides) {
      return provides[key as string];
    } else if (arguments.length > 1) {
      return treatDefaultAsFactory && isFunction(defaultValue)
        ? defaultValue.call(instance && instance.proxy)
        : defaultValue;
    }
  }
}

mount 挂载

  • 先检查是否已经挂载,再创建一个虚拟节点vnode,并且设置context上下文
  • 根据namespace设置命名空间
  • 根据isHydrate参数的值决定是使用hydrate函数还是render函数来将虚拟节点渲染到根容器中
  • 最后返回vnode的组件代理
const app: App = (context.app = {
  mount(
    rootContainer: HostElement,
    isHydrate?: boolean,
    namespace?: boolean | ElementNamespace,
  ): any {
    if (!isMounted) {
      if (__DEV__ && (rootContainer as any).__vue_app__) {
        // 已经挂载了一个app,需要先执行unmount卸载之后再挂载
      }
      const vnode = createVNode(rootComponent, rootProps)

      vnode.appContext = context

      if (namespace === true) {
        namespace = 'svg'
      } else if (namespace === false) {
        namespace = undefined
      }

      // HMR root reload
      if (__DEV__) {
        context.reload = () => {
          render(
            cloneVNode(vnode),
            rootContainer,
            namespace as ElementNamespace,
          )
        }
      }

      if (isHydrate && hydrate) {
        hydrate(vnode as VNode<Node, Element>, rootContainer as any)
      } else {
        render(vnode, rootContainer, namespace)
      }
      isMounted = true
      app._container = rootContainer
      ;(rootContainer as any).__vue_app__ = app

      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        app._instance = vnode.component
        devtoolsInitApp(app, version)
      }

      return getExposeProxy(vnode.component!) || vnode.component!.proxy
    }
  },
})

unmount卸载

该函数用于卸载一个Vue应用。先检查应用是否已挂载,如果是,则通过render函数将应用的组件渲染为null,从而从DOM中移除应用同时删除应用容器上的Vue应用引用。

const app: App = (context.app = {
  unmount() {
    if (isMounted) {
      render(null, app._container);
      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        app._instance = null;
        devtoolsUnmountApp(app);
      }
      delete app._container.__vue_app__;
    }
  }
});

render函数

源码位于packages/runtime-core/src/renderer.ts 2357line
如果第一个参数是null,则执行销毁组件的逻辑,否则执行patch函数来创建或者更新组件的逻辑(diff比较更新DOM)

  let isFlushing = false;
const render: RootRenderFunction = (vnode, container, namespace) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true);
    }
  } else {
    patch(
      container._vnode || null,
      vnode,
      container,
      null,
      null,
      null,
      namespace
    );
  }
  if (!isFlushing) {
    isFlushing = true;
    flushPreFlushCbs();
    flushPostFlushCbs();
    isFlushing = false;
  }
  container._vnode = vnode;
};

directive指令

指令本质就是一个js对象,对象上挂着一些钩子函数。全局注册的指令都挂载到context当中。

const app: App = (context.app = {
  directive(name: string, directive?: Directive) {
    if (!directive) {
      return context.directives[name] as any;
    }
    context.directives[name] = directive;
    return app;
  }
});

mixin混入

判断是否支持Options API,以及当前这个mixin还没有被混入

const app: App = (context.app = {
  mixin(mixin: ComponentOptions) {
    if (__FEATURE_OPTIONS_API__) {
      if (!context.mixins.includes(mixin)) {
        context.mixins.push(mixin);
      }
    }
    return app;
  }
});

component组件

注册组件,如果组件已经存在,则返回组件,否则则注册当前组件返回app

const app: App = (context.app = {
  component(name: string, component?: Component): any {
    if (!component) {
      return context.components[name];
    }
    context.components[name] = component;
    return app;
  }
});
  • 19
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Modify_QmQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值