Vue2源码 - init.ts(纯干货,自带水)

前言

本篇通过注释源码的方式,不说没用的废话。带你一字一句的学习Vue2版本源码。有过Vue2项目经验,然后在搭配Vue的源码一起阅读最好。

源码模块中的重要方法

1.initMixin()

js 复制代码
function initMixin(Vue: typeof Component) {
  // 在vue实例中添加_init方法,每个 Vue 实例在被创建时都会调用的方法 !!!
  // Record<Keys, Type> 工具类型
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this 
    // 计数器
    vm._uid = uid++

    let startTag, endTag
    // 简单理解为测试性能的即可,先不要过度纠结
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // 定义这个属性的作用是为了在后面判断一个对象是不是Vue实例,起到一个标记的作用
    vm._isVue = true
    // 定义这个属性用来防止 Vue 实例被转换成响应式对象。 因为Vue是让别的对象响应式的,而不是把自己响应式。
    // 设置为true,就代表这是一个不需要被响应式的对象。
    vm.__v_skip = true
    // 创建一个副作用的实例
    vm._scope = new EffectScope(true /* detached */) 
    // vm._scope 是一个 EffectScope 实例,它用来管理和组织 Vue 实例中的副作用。_vm 属性是一个标记,
    // 表示这个 EffectScope 实例是属于一个 Vue 实例的。
    /**
     * 目的:
     * 这样做的目的是为了在后续的代码中,可以通过检查 EffectScope 实例的 _vm 属性,
     * 来判断这个 EffectScope 实例是否属于一个 Vue 实例。
     * 这在某些情况下,例如在处理嵌套组件或者组件的销毁过程中,会非常有用。 
     */
    vm._scope._vm = true
    /** 
     * 如果 options 存在,并且 options._isComponent 为 true,
     * 调用 initInternalComponent 初始化内部组件。(这个函数在下面有讲解)
     * 否则,调用 mergeOptions 和 resolveConstructorOptions 函数来合并和解析选项,
     * 然后将结果赋值给 $options 属性。
     * 这个 $options 属性包含了 Vue 实例的终极版本,
     * 是用户提供的选项和默认选项的合并结果!!!
     * 这么解释应该通俗吧。读到这的时候脑补一下你们写的代码。
    */
    if (options && options._isComponent) {
      initInternalComponent(vm, options as any)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }

    /**
     * 如果在开发模式下(__DEV__ 为 true),
     * 会调用 initProxy 函数来创建一个代理对象,并将这个代理对象赋值给 _renderProxy。
     * 这个代理对象用来在渲染函数中捕获对未定义属性的访问,以便在开发者访问未定义的属性时发出警告。
     * (这个警告由 Vue 在运行时发出的,它会在浏览器的控制台中显示。
     *  当你在开发模式下运行 Vue 应用,并且试图访问或设置一个不存在的属性时,
     *  Vue 会通过代理对象捕获这个操作,并在浏览器的控制台中发出一个警告,告诉你这个属性是未定义的。)
     * 否则 生产模式下,不需要这个代理对象,所以直接将 Vue 实例本身赋值给 _renderProxy。
     *  总结一下就是,都在生产环境中了,我还给你提示干啥?提示了,你是能改还是咋滴??
     */
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    
    // 这个也是吧实例赋值。既然赋值了,后面肯定会用到,不解释了怕刹不住
    vm._self = vm
    // 用来初始化 Vue 实例的生命周期相关的属性,例如 $parent、$children ...
    initLifecycle(vm)
    // 用来初始化 Vue 实例的事件系统,例如 设置父组件传递的事件监听器...
    initEvents(vm)
    // 用来初始化 Vue 实例的渲染函数,创建 createElement()
    initRender(vm)
    // 触发 beforeCreate 生命周期钩子。这就是我们所熟知的 此钩子会在 Vue 实例创建后,在数据观察和事件/观察者设置之前被调用。
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    // 用来解析并设置 inject 选项。inject 选项用来从祖先组件接收数据。
    initInjections(vm)
    // 初始化 Vue 实例的状态,例如 props、methods、data ....
    initState(vm)
    // 解析并设置 provide 选项。provide 选项用来向子孙组件提供数据。
    initProvide(vm)
    // 触发 created 生命周期钩子。这个钩子会在 Vue 实例创建完成后被调用,
    // 此时,实例已经完成了以下的配置:数据观察(data observer),属性和方法的运算,watch/event 事件回调。
    // 然而,挂载阶段还没开始,$el 属性目前不可见。 
    callHook(vm, 'created')

    
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
    // 如果 vm.$options.el 存在,那么会调用 vm.$mount(vm.$options.el) 来挂载 Vue 实例。
    // 当然了,如果不存在,就需要手动挂载了
    /** 
     *  举个例子,说一下什么叫自动挂载和手动挂载
     *  自动挂载: 创建 Vue 实例时,如果提供了 el 选项,Vue 会在内部自动调用 vm.$mount 方法来挂载实例
     *  new Vue({
          el: '#app',
          render: h => h(App)
        })
        手动挂载:建 Vue 实例时,如果没有提供 el 选项,那么 Vue 实例会处于未挂载状态,需要我们手动调用 vm.$mount 方法来挂载
        new Vue({
          render: h => h(App),
        }).$mount('#app')
        所以,我们使用脚手架做vue项目的时候,一般用手动挂载。
    */
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

2.initInternalComponent

js 复制代码
function initInternalComponent( vm: Component, options: InternalComponentOptions ) {
  // 先介绍一下这句代码的意思,省流:做个浅拷贝。
  // 1.给 vm.$options 赋值。赋值的时候会创建一个新的对象,
  //   这个新的对象的原型是 vm.constructor.options,然后将这个新的对象赋值给 vm.$options
  // 2.然后在把vm.$options 赋值给opts常量。ok,我在这个函数中操作opts就相当于操作vm.$options了。 
  // 知识点:由于 JavaScript 的变量查找是从内到外的,所以使用局部变量 opts 可以比直接使用vm.$options更快
  const opts = (vm.$options = Object.create((vm.constructor as any).options))
  
  const parentVnode = options._parentVnode
  opts.parent = options.parent // 父组件实例
  opts._parentVnode = parentVnode// 父vnode

  const vnodeComponentOptions = parentVnode.componentOptions!
  opts.propsData = vnodeComponentOptions.propsData // 组件传递的 props 数据
  opts._parentListeners = vnodeComponentOptions.listeners // 父组件过来的事件监听器
  opts._renderChildren = vnodeComponentOptions.children // 渲染的子节点
  opts._componentTag = vnodeComponentOptions.tag // 组件标签名
  // 如果 options 参数中包含 render 属性,那么将 render 和 staticRenderFns 属性赋值给新的 $options 对象
  if (options.render) {
    opts.render = options.render
    // options.staticRenderFns 是啥?
    // 如果模板中有一些静态的部分,例如一些不会改变的文本或者标签,
    // 那么这些静态的部分会被提取出来,生成一些静态的渲染函数,这些函数就被存放在 options.staticRenderFns 中
    // 后续的渲染只需要复用第一次渲染的结果,不需要再次生成。这可以提高渲染的性能。
    opts.staticRenderFns = options.staticRenderFns
  }
}

3.resolveConstructorOptions

js 复制代码
function resolveConstructorOptions(Ctor: typeof Component) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    // 判断是父组件的选项是否有变化。如果有变化,那么就需要更新子组件的选项,没变化啥也不干
    if (superOptions !== cachedSuperOptions) {
      // 更新子组件的superOptions属性,使其指向新的父组件选项
      Ctor.superOptions = superOptions
      // 获取子组件的选项中与父组件选项不同的部分
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // 如果存在不同的部分,那么就将这部分选项合并到子组件的extendOptions属性中
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      // 将父组件的选项和子组件的extendOptions合并,然后将结果赋值给子组件的options属性。
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      // 如果子组件有名字,那么就将子组件添加到选项的components属性中,方便在模板中使用
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  // 返回 options选项
  return options
}
相关推荐
卸任10 分钟前
Electron霸屏功能总结
前端·react.js·electron
fengci.10 分钟前
ctfshow黑盒测试前半部分
前端
忆琳12 分钟前
Vue3 优雅解决单引号注入问题:自定义指令 + 全局插件双方案
vue.js·element
喵个咪21 分钟前
Headless 架构优势:内容与展示解耦,一套 API 打通全端生态
前端·后端·cms
小江的记录本25 分钟前
【JEECG Boot】 JEECG Boot——数据字典管理 系统性知识体系全解析
java·前端·spring boot·后端·spring·spring cloud·mybatis
喵个咪28 分钟前
传统 CMS 太笨重?试试 Headless 架构的 GoWind,轻量又强大
前端·后端·cms
chenjingming66629 分钟前
jmeter导入浏览器上按F12抓的数据包
前端·chrome·jmeter
张元清29 分钟前
不用 Server Components 也能做 React 流式 SSR —— 实战指南
前端·javascript·面试
前端技术32 分钟前
ArkTS第三章:声明式UI开发实战
java·前端·人工智能·python·华为·鸿蒙