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
}
相关推荐
前端李易安1 小时前
Web常见的攻击方式及防御方法
前端
PythonFun2 小时前
Python技巧:如何避免数据输入类型错误
前端·python
hakesashou2 小时前
python交互式命令时如何清除
java·前端·python
天涯学馆2 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF2 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi2 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器
ConardLi2 小时前
安全赋值运算符,新的 JavaScript 提案让你告别 trycatch !
前端·javascript
凌云行者3 小时前
使用rust写一个Web服务器——单线程版本
服务器·前端·rust
华农第一蒟蒻3 小时前
Java中JWT(JSON Web Token)的运用
java·前端·spring boot·json·token
积水成江3 小时前
关于Generator,async 和 await的介绍
前端·javascript·vue.js