都用TypeScript,还不来分析Vue3源码(第一期)

createApp

位置🍎:packages/compiler-core/src/apiCreateApp.ts

动机🍎:我们知道vue暴露了createAppAPI【位置packages/compiler-core/src/renderer.ts Line:2256】,源码得知createApp等于createAppAPI(render, hydrate),从而我们先从apiCreateApp.ts出发

import { createApp } from 'vue'
import App from './App.vue'

/* 不推荐这么写 */
/* 因为所有的Api其实都是通过createApp(App)暴露出去的(mount等) */
createApp(App).mount('#app')
/* 后面我们经常需要在全局做操作所以最好的写法 */
const app=createApp(App);
app.mount('#app');

首先先让我们来看一下Vue3新版createApp里面有什么?

ensureRenderer

位置🍎:packages/compiler-dom/src/index.ts

动机:vue3运行的核心render方法

  • 经过断点发现createApp接受的参数通过三点运算符进行了展开处理【说明createApp这边应该可以接受多个参数】

    /* args多参数 */
    const createApp = ((...args) => {
        const app = ensureRenderer().createApp(...args);
       //do something....
    }
    

    ensureRenderer函数执行了返回了createRenderer函数

    /* extend==Object.assign */
    /*  rendererOptions对象合并 */
    const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
    
    let renderer;
    function ensureRenderer() {
        return renderer || (renderer = createRenderer(rendererOptions));
    }
    
  • rendererOptions包含了所有的节点操作packages/compiler-dom/src/nodeOps.ts

    • nodeOps(基础的节点操作)==【insertremove,createElementcreateTextcreateCommentsetTextsetElementTextparentNodenextSiblingquerySelectorsetScopeIdcloneNodeinsertStaticContent

    • patchProp比较新旧的属性

      export const patchProp: DOMRendererOptions['patchProp'] = (
       .....
      ) => {
        switch (key) {
          // special
          case 'class':
            patchClass(el, nextValue, isSVG)
            break
          case 'style':
            patchStyle(el, prevValue, nextValue)
            break
          default:
           //do something.....patchDOMProp
            break
        }
      }
      

      我们知道解析元素的属性,特殊的class的方法patchClass,特殊的style的方法patchStyle,以及最后的DOM原生的属性patchDOMProp方法

      我们从patchProp的类型DOMRendererOptions得知----->>>>RendererOptions的类型就是extend({ patchProp, forcePatchProp }, nodeOps)的合并。

      #学到语法可以通过DOMRendererOptions['patchProp']获取到某一串中的patchProp类型

    • forcePatchProp类型boolean,应该是是否强制比对props

  • createRenderer又把rendererOptions参数传递给baseCreateRenderer函数

    我们发现baseCreateRenderer【位置:packages/compiler-core/src/renderer.ts】重载overload了俩次,实现implementation了一次

    baseCreateRenderer最后implementation出了vnode的diff算法生成正式DOM的方法,这个方法调用了rendererOptions的所有关于节点的操作通过diff算法,里面还包含了一些热更新的方法。这里暂不细讲

createAppAPI

位置🍎:packages/compiler-core/src/apiCreateApp.ts

ensureRenderer()执行完了,然后就执行createApp

function createAppAPI(render, hydrate) {
    return function createApp(rootComponent, rootProps = null) {
        //do something....
    }
}

createAppAPI返回函数可以接受俩个参数:

  • rootComponent第一个参数毋庸置疑可以传递App根组件,rootComponent就是传入的参数APP==={name: "App", setup: ƒ}
  • rootProps第二个参数全局属性会传递_props上面
const app=createApp(App, {
  msg: '我是全局属性哦',
})

我们打印app的话,可以在其实例上找到{_props:{msg:'我是全局属性哦'}},这里跟源码切合实际。

createAppContext

首先初始化

//new 一下
const context = createAppContext()
//安装的插件
const installedPlugins = new Set()
//是否被加载到正式DOM上
let isMounted = false

context的初始值:

{
    app: null as any,
    config: {
      isNativeTag: NO,//代表false
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      isCustomElement: NO,//代表false
      errorHandler: undefined,
      warnHandler: undefined
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null)
  }
use
/* isFunction是否是typeof val === 'function' */
use(plugin: Plugin, ...options: any[]) {
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          installedPlugins.add(plugin)
          plugin(app, ...options)
        } else if (__DEV__) {
          warn(
            `A plugin must either be a function or an object with an "install" ` +
              `function.`
          )
        }
        return app
  }
  • Set()数据结构has到已经存在当前插件就会WarnDEV==判断是否在开发模式】

    Plugin has already been applied to target app.

  • 判断插件是否使用install方法,

    • 存在:把Vue实例以及options传递给install方法,提供给开发者使用,所以install方法能获取到俩个参数哦
    • 不存在:Vue3可以直接写function插件,导致不存在install方法,所以function直接能够获取到俩个参数,同install
  • 如果以上都不成立且当前环境开发模式就直接warn,生产模式不进行warn报错

mixin
mixin(mixin: ComponentOptions) {
        if (__FEATURE_OPTIONS_API__) {
          if (!context.mixins.includes(mixin)) {
            context.mixins.push(mixin)
          } else if (__DEV__) {
            warn(
              'Mixin has already been applied to target app' +
                (mixin.name ? `: ${mixin.name}` : '')
            )
          }
        } else if (__DEV__) {
          warn('Mixins are only available in builds supporting Options API')
        }
        return app
   }

首先我们可以看到__FEATURE_OPTIONS_API__常量,字面意思就是特性选项API,判断了这个常量说明mixin成为了一个可关闭的特性,从warn我们也可以看出,未来有一天,也就是将不支持Mixins这个Options API

context.mixins.push(mixin)看出mixins在全局是一个数组,这说明他还可能遵循着Vue2的mixins规则

mixins: [mixin2, mixin1]会使得mixin1覆盖mixin2

component
component(name: string, component?: Component): any {
        if (__DEV__) {
          validateComponentName(name, context.config)
        }
        if (!component) {
          return context.components[name]
        }
        if (__DEV__ && context.components[name]) {
          warn(`Component "${name}" has already been registered in target app.`)
        }
        context.components[name] = component
        return app
      }
  • 先检查组件validateComponentName()的不能存在原生Tag,比如说divspan
  • component参数为空的时候等,也会进行全局注册,不会报错
  • context.components[name]匹配到相同name的组件的时候会warn警告
  • 最后直接全局注册
directive
directive(name: string, directive?: Directive) {
        if (__DEV__) {
          validateDirectiveName(name)
        }

        if (!directive) {
          return context.directives[name] as any
        }
        if (__DEV__ && context.directives[name]) {
          warn(`Directive "${name}" has already been registered in target app.`)
        }
        context.directives[name] = directive
        return app
   }
  • 首先Vue3会先检查自定义指令不能够重名内置指令

    这是源码定制的内置指令【位置🍎:packages/compiler-core/src/directives.ts

    bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text

  • directive参数为空的时候等,也会进行全局注册,不会报错

  • context.directives[name]匹配到相同name的组件的时候会warn警告

  • 最后直接全局注册

mount

Don't say so much【多提一句:变量isMounted会变成true】

unmount
unmount() {
        if (isMounted) {
          render(null, app._container)
          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            devtoolsUnmountApp(app)
          }
        } else if (__DEV__) {
          warn(`Cannot unmount an app that is not mounted.`)
      }
  }
  • 前面也说,当isMounted挂载ok了,我们这边才有卸载的能力
provide
provide(key, value) {
        if (__DEV__ && (key as string | symbol) in context.provides) {
          warn(
            `App already provides property with key "${String(key)}". ` +
              `It will be overwritten with the new value.`
          )
        }
        // TypeScript doesn't allow symbols as index type
        // https://github.com/Microsoft/TypeScript/issues/24587
        context.provides[key as string] = value

        return app
   }
  • 首先key可以是string类型或者说是symbol类型拿来判断,并且如果in类型保护,也就是说context.provides当中有重复,Vue3会进行吧warn警告

  • 不能用symbols类型当作key

    TypeScript doesn't allow symbols as index type github.com/Microsoft/T…

以上是createApp暴露出来的所有Api

use---->router

观察到install方法

  app.component('RouterLink', RouterLink);
  app.component('RouterView', RouterView);
  app.config.globalProperties.$router = router;
  Object.defineProperty(app.config.globalProperties, '$route', {
  get: () => unref(currentRoute),
  });

注册了全局组件RouterLinkRouterView

RouterLink
  • to:表示链路的目标路由,当单击时,to prop的值将在内部传递给router.push(),因此它可以是字符串,也可以是route location对象

    • 必传参数【required: true
    • 可接收String或者·Object类型
      • /home
      • { name: 'home', params: { userId: '123' }}`
  • activeClass:标识链接处于被激活状态时应用于渲染的<a>class

  • ariaCurrentValue:当链接完全处于活动状态时传递给属性aria-current的值

  • custom:<router-link>不应该包裹<a>标签元素,当我们使用v-slot去创建个自定义的RouterLink,属性custom是非常有用的

    <router-link to="/home" v-slot="{ route }">
      <span>{{ route.fullPath }}</span>
    </router-link>
    
    //渲染成
    <a href="/home"><span>/home</span></a>
    
  • exact-active-class:已经激活状态下的class

/* 这是RouterLink组件 */
defineComponent({
  name: 'RouterLink',
  props: {
    to: {
      type: [String, Object] as PropType<RouteLocationRaw>,
      required: true,
    },
    activeClass: String,
    // inactiveClass: String,
    exactActiveClass: String,
    custom: Boolean,
    ariaCurrentValue: {
      type: String as PropType<RouterLinkProps['ariaCurrentValue']>,
      default: 'page',
    },
  },

  setup(props, { slots, attrs }) {
    //do something....
  },
})
RouterView

当有一个名称时,它将在匹配路由记录的components选项中呈现具有相应名称的组件

defineComponent({
    name: 'RouterView',
    props: {
        name: {
            type: String,
            default: 'default',
        },
        route: Object,
    },
    setup(props, { attrs, slots }) {
       //do something....
    },
});

然后把挂载$router$route

app.mount

const { mount } = app
  app.mount = (containerOrSelector: Element | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (container) {
      return mount(container, true)
    }
  }

首先前面return app这边我们就可以mount解构出来,然后载真实DOM

  • normalizeContainer方法会document.querySelector方法去HTML找是否存在该元素

    function normalizeContainer(container: Element | string): Element | null {
      if (isString(container)) {
        const res = document.querySelector(container)
        if (__DEV__ && !res) {
          warn(`Failed to mount app: mount target selector returned null.`)
        }
        return res
      }
      return container
    }
    

    当不存在元素的时候(且在生产模式下),warn===Failed to mount app: mount target selector returned null

    存在的时候会返回HTMLElement元素

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

    还需要判断App组件是不是Function或者有没有template或者有没有render,当都不存在的时候,会把真实DOM元素的内容变成template

    最后还会清除container元素的内容外加v-cloak属性,新增data-v-app属性

    最后解析组件【这里不多叙述】,然后挂载页面

原文链接:juejin.im

上一篇:[原理02] 权限控制
下一篇:手写bind和原生bind对比

相关推荐

  • 🚩Vue源码——订阅者的响应

    前言 在上篇专栏中介绍了发布者是如何收集订阅者(Watcher),本专栏来详细介绍发布者发生变化后,如何通知订阅者,而订阅者是如何响应。 一、如何通知订阅者 在 Vue 中发布者一般是数据,当数据发生...

    25 天前
  • 🚩Vue源码——组件是如何注册的

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 在上一篇 🚩Vue源码——组件...

    2 个月前
  • 🚩Vue源码——组件如何渲染成最终的DOM

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 Vue有两个核心思想,一个是数据...

    2 个月前
  • 🚩Vue源码——如何监听数据变化

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 Vue 是用数据来驱动来生成视图...

    1 个月前
  • 🚩Vue源码——如何深度收集渲染订阅者

    前言 本专栏是由一个问题引起,如果你已经知道答案了,可以忽略本专栏。 &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &...

    13 天前
  • 🚩Vue源码——nextTick实现原理

    前言 在上一篇专栏讲到订阅者的响应是先把订阅者添加到一个队列,然后再 nextTick 函数中去遍历这个队列,对每个订阅者进行响应处理。大家所熟悉的 Vue API Vue.nextTick 全局方法...

    21 天前
  • 🔥手写大厂前端知识点源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    8 个月前
  • 🔥前端面试大厂手写源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    8 个月前
  • (源码分析)为什么 Vue 中 template 有且只能一个 root ?

    引言 今年,疫情并没有影响到各种面经的正常出现,可谓是络绎不绝(学不动...)。然后,在前段时间也看到一个这样的关于 Vue 的问题,为什么每个组件 template 中有且只能一个 root? 可能...

    7 个月前
  • 面试还问redux?那我从头手撸源码吧(中间件篇)

    昨天的文章手写了一版redux的核心源码,redux库除了数据的状态管理还有一块重要的内容那就是中间件,今天我还是尝试将此部分源码完成。 中间件 react中管理数据的流程是单向的,就是说,从派发动作...

    2 年前

官方社区

扫码加入 JavaScript 社区