vue2源码分析-vue入口文件global-api分析

文章背景

  • vue项目开发过程中,首先会有一个初始化的流程,以及我们会使用到很多全局的api,如 this.$set this.$delete this.$nextTick,以及初始化方法extend,initUse, initMixin , initExtend, initAssetRegisters 等等那它们是怎么实现,让我们一起来探究下吧

源码目录

global-api代码初始化

  • 代码路径 src\vue-2.6.14\src\core\global-api\index.js

代码分析

vue挂载的全局属性

  • vue.util 公共方法
    • warn 代码警告处理
    • extend 将源数据中的值拷贝到目标数据中
    • mergeOptions 合并选项
    • defineReactive 定义响应式数据
  • set set 方法,修改对象的某个属性,以更新视图
  • del del 方法,删除数组的某个索引下的数据 或者 对象的某个属性
  • nextTick nextTick方法
  • observable 调用observer类的observe方法,将对象变为响应式对象,并且返回该响应式对象
  • options vue的一些选项

初始化方法

  • extend
    • extend函数此处用于扩展,将源数据中的值拷贝到目标数据中,数据类型兼容到了 对象和数组
  • initUse
    • 初始化插件
  • initMixin
    • 初始化混入 逻辑有点多,后期详细补充
  • initExtend
    • 构造一个vue的子类
  • initAssetRegisters(Vue)
    • 注册或者获取全局组件、指令、过滤器
js 复制代码
/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'
export function initGlobalAPI(Vue: GlobalAPI) {
  // note 初始化全局api sjf-step2
  debugger
  const configDef = {}
  configDef.get = () => config
  // 初始化vue的全局config配置
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      // set在修改对象的字段时触发,此处表示在设置 Vue的config属性时触发
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
    //在开发环境中,不允许替换整个Vue.config 对象,只允许设置单独的字段
  }
  Object.defineProperty(Vue, 'config', configDef)
  // 
  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {//将全局方法,挂载到Vue的util方法上
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set// set 方法,修改对象的某个属性,以更新视图
  Vue.delete = del // del 方法,删除数组的某个索引下的数据 或者 对象的某个属性
  Vue.nextTick = nextTick // nextTick方法
  // set delete nextTick 除了可以使用 this.$set,this.$delete,this.$nextTick调用,亦可以在Vue实例上直接调用

  // 2.6 explicit observable API
  Vue.observable = (obj) => {
    observe(obj)// 调用observer类的observe方法,将对象变为响应式对象
    return obj//并且返回该响应式对象
  }

  Vue.options = Object.create(null)
  //调用 Object.create(null) 创建一个没有原型链的空对象
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
  // ASSET_TYPES = ['component','directive','filter'] 
  // 遍历ASSET_TYPES数组,给Vue.options添加三个属性,分别是components,directives,filters

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.

  Vue.options._base = Vue
  // 将vue赋值给Vue.options._base,用于在weex的多实例场景下,标识"基础"构造函数,以扩展所有纯对象组件
  extend(Vue.options.components, builtInComponents)
  // 将builtInComponents对象的属性,复制到Vue.options.components上
  // extend函数此处用于扩展
  initUse(Vue)//初始化插件
  initMixin(Vue)//初始化混入 逻辑有点多,后期详细补充
  initExtend(Vue)//构造一个vue的子类 
  initAssetRegisters(Vue) // 注册或者获取全局组件、指令、过滤器
}

extend 扩展数据

  • 代码路径 src\vue-2.6.14\src\shared\util.js
  • extend函数此处用于扩展,将源数据中的值拷贝到目标数据中,数据类型兼容到了 对象和数组
  • 入参说明
    • to 目标值,_from 源数据
  • 核心代码说明
    • const key in _from
    • _from 可以是一个数组 ,也可以是一个对象
    • 如果是数组,则key是数组的索引
    • 如果是对象,则key是对象的键值
js 复制代码
export function extend (to: Object, _from: ?Object): Object {
  // sjf-note-best-code
  // 将源数据中的值拷贝到目标数据中,数据类型兼容到了 对象和数组
  for (const key in _from) {
    // const key in _from 
    // _from 可以是一个数组 ,也可以是一个对象
    // 如果是数组,则key是数组的索引
    // 如果是对象,则key是对象的键值
    to[key] = _from[key]
  }
  //当我们还在苦逼地用 Array.isArray 判断数据类型是否是数组来区分代码写两套逻辑时, vue源代码已经实现了,一个代码同时兼容了数组和对象的拷贝,简直不要太优秀啊
  return to
}
  • 优秀代码设计思想
    • 当我们还在苦逼地用 Array.isArray 判断数据类型是否是数组来区分代码写两套逻辑时, vue源代码已经实现了,一个代码同时兼容了数组和对象的拷贝,简直不要太优秀啊

initUse 初始化插件

  • 代码路径 src\vue-2.6.14\src\core\global-api\use.js
  • 入参说明
  • plugin 参数 有可能是函数也有能是对象
    • 当是对象时需要访问install属性并且使用apply修改this为plugin后调用
    • 当是函数时则直接调用
  • 核心代码说明
  • installedPlugins
    • 定义 installedPlugins 参数,
    • 读取vue实例上的_installedPlugins属性,如果读取不到则赋值为空数组
    • 缓存插件,将插件添加到已安装插件的数组中
  • 防止插件的重复加载
    • 如果插件已经安装,则将插件直接返回
js 复制代码
import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // _installedPlugins 函数内部的变量 用来存储已经注册的插件
    // _installedPlugins 不存在时,初始化为一个空数组
    // vue源码中并没有声明 _installedPlugins 并且此处是非箭头函数
    if (installedPlugins.indexOf(plugin) > -1) {
      // 防止插件的重复加载
      return this
    }
    const args = toArray(arguments, 1)
    // toArray 方法将参数转化为数组
    // arguments 是一个类数组对象,有length属性,但是没有数组的方法
    // 通过toArray方法将arguments转化为数组,并且去掉第一个参数
    args.unshift(this)//再将this添加到args数组的头部
    if (typeof plugin.install === 'function') {
      // 如果插件有install方法,则调用install方法
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      // 如果插件是一个函数,则直接调用
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)//缓存插件,将插件添加到已安装插件的数组中
    return this
  }
}

initMixin 初始化混入

  • 初始化混入 逻辑有点多,后期单独出一篇文章详细补充

initExtend 初始化vue组件对象

  • 代码路径 src\vue-2.6.14\src\core\global-api\extend.js 函数说明
  • 基于构造函数Vue,创建了一个Vue组件对象,并初始化vue选项式api 核心代码说明
  • 获取父类的cid,也就是组件的id,即组件的标识
  • cachedCtors,定义一个缓存对象
    • 如果缓存对象中已经有了当前类的id,则表明在此操作前已经创建过了
    • 通过id访问到缓存对象中的子类,并直接返回
    • 这样做可以提高性能,避免重复的子类创建
  • 获取到组件的name属性
    • 如果当前传入的组件选项中有name属性则直接读取,否则访问Super(父类的)name属性
    • name是组件的标识,一般确实永不到,但是组件递归的时候必须要写,否则无法调用子组件
  • 调用validateComponentName校验name属性是否合法
  • sub类的处理
    • 通过函数的方式创建一个sub类,并将父类的原型链继承到子类中(原型链继承方式),同时还需要修正constructor的指向
    • 将父类的options字段和传入组件对象选项进行合并,并保存在子类sub的options字段中
    • 将父类保存到子类的 super 字段中,确保子类能拿到父类
  • 初始化 props
    • props 是当前组件的props属性
    • const in props 遍历对象的属性
    • 将 组件传入的props属性代理到组件实例的_props属性上
    • 初始化 computed
    • computed 是当前组件的computed属性
    • const in props 遍历对象的属性
    • defineComputed 逻辑暂时没看,后期单独出一期文章补充
  • extend,mixin,use等api添加到子类中
  • 新增 superOptions,extendOptions,sealedOptions等选项
  • 将子类返回
js 复制代码
/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { defineComputed, proxy } from '../instance/state'
import { extend, mergeOptions, validateComponentName } from '../util/index'

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  // sjf-note-best-code
  Vue.cid = 0//初始化cid为0 可以理解为组件的标识
  let cid = 1 

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    // 基于构造函数Vue,创建了一个Vue组件对对象,并初始化vue选项式api
    extendOptions = extendOptions || {}
    // extendOptions是一个对象,如果没有传入,则初始化为空对象
    // extendOptions 是组件传入的选项
    const Super = this//将this(vue的实例)赋值给Super
    const SuperId = Super.cid

    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    //定义一个缓存对象,用生成的id作为属性键,如果cachedCtors[SuperId]存在则表明该组件的构造函数已经生成,直接返回即可
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    // extendOptions 当前组件的选项 的 
    // Super 是 Vue的实例 options 是Vue的选项
    // 如果传入了extendOptions.name 则使用extendOptions.name 否则使用Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      //判断是否是开发环境
      validateComponentName(name)
      //校验组件名称是否合法, 如果不合法会抛出异常
    }
    const Sub = function VueComponent (options) {

      this._init(options)
      //此处的this是vue实例 ,调用了_init方法,初始化组件实例
    }
    Sub.prototype = Object.create(Super.prototype)
    // Super.prototype 是Vue的原型对象
    // Object.create(Super.prototype) 创建了一个新的对象,该对象的原型是Super.prototype
    // Object.create() 方法创建一个新对象,不会有原型链上的属性和方法
    
    Sub.prototype.constructor = Sub// consturctor指向自身 sjf-todo
    // 通过函数的方式创建一个sub类,并将父类的原型链继承到子类中(原型链继承方式),同时还需要修正constructor的指向
    Sub.cid = cid++//组件的cid自增1

    Sub.options = mergeOptions(
      //将父类的options字段和传入组件对象选项进行合并,并保存在子类sub的options字段中
      Super.options,//Vue的选项
      extendOptions//当前组件的选项
    )
    Sub['super'] = Super//将Super(根组件的实例)赋值给当前组件的super属性
// 将父类保存到子类的 super 字段中,确保子类能拿到父类
    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      //如果当前组件有props属性,则初始化props
      initProps(Sub)  
    }
    if (Sub.options.computed) {
      //如果当前组件有computed属性,则初始化computed
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use
// Vue的一些原生API 等扩展到子构造器上边
    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)
// Vue的一些原生API 等扩展到子构造器上边
    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

function initProps (Comp) {
  const props = Comp.options.props
  // props 是当前组件的props属性
  for (const key in props) {  
    // const in props 遍历对象的属性
    proxy(Comp.prototype, `_props`, key)
    // proxy 代理数据
    // 此处是将 组件传入的props属性代理到组件实例的_props属性上
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  // computed 是当前组件的computed属性
  for (const key in computed) {
    // const in computed 遍历对象的属性
    defineComputed(Comp.prototype, key, computed[key])
    // defineComputed 代理数据
    // 此处是将 组件传入的computed属性代理到组件实例的_props属性上
  }
}

initAssetRegisters 注册或者获取全局组件、指令、过滤器

  • src\vue-2.6.14\src\core\global-api\assets.js 函数说明
  • 注册或者获取全局 component 组件、directive指令、 filter 过滤器 函数入参说明
  • id 标识名称id
  • definition 定义类型是函数或者对象
    • 当type 是directive或者filter的时候,是函数
    • 当type是components的时候是,对象 核心代码说明
  • 判断是是否有传入 definition 参数
    • 无则表明不是注册全局组件、指令、过滤器,而直接读取,直接通过标识名称id读取返回即可
    • 有则标明是注册
  • 校验组件名是否合规
    • 非生产环境 并且type值是component组件名称,则需要校验组件名是否合规
    • 如果不合规则在 validateComponentName函数中抛出异常,下文有说明
  • 根据type类型分别做逻辑处理
  • component
  • type为 component,并且 isPlainObject 判断 definition类型是 object,则执行下边逻辑
    • 读取 name 属性,如果读不到definition的name属性,则使用标识名称id
    • 调用 vue 的extend扩展定义,并重新赋值
  • directive
    • type值为 directive ,并且 definition的类型为 function
    • 定义一个对象有bindupdate属性,赋值为definition
  • 以上逻辑处理好了directive和component选项下的definition,filter选项不需要特殊处理
  • 根据选项式api对象读取到指定id的component 组件、directive指令、 filter 过滤器并赋值为 definition
  • 将 definition返回给调用者
js 复制代码
/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  /* 
  'component',
  'directive',
  'filter'
  */
//   注册或者获取全局组件、指令、过滤器
  debugger
  ASSET_TYPES.forEach(type => {//定义资源注册方法
    Vue[type] = function (
      id: string,//标识名称id
      definition: Function | Object//定义函数或者对象 
      // 当type 是directive或者filter的时候,是函数
      // 当type是components的时候是,对象
    ): Function | Object | void {
      // 
      console.log("id_definition",type,id,definition)
      if (!definition) {
        // 没有传入definition 的时候,则之直接读取并返回
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          // 非生产环境 交易案组件名称,如果不合规,则抛出异常
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id//设置 definition.name属性 
          // 值为组件传入的name || id
          definition = this.options._base.extend(definition)
          // 调用 vue 的extend扩展定义,并重新赋值
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
          // 自定义指令的处理逻辑
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

源码中使用到的小代码片段补充

ASSET_TYPES

js 复制代码
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

proxy代理函数

  • 代码引用地址 src\vue-2.6.14\src\core\instance\state.js
  • 使用defineProperty代理数据
js 复制代码
const sharedPropertyDefinition = {//代理数据的配置
  enumerable: true,//是否可枚举
  configurable: true,//是否可修改删除
  get: noop,//get方法,用于读取数据 noop 定义一个临时的空的函数
  set: noop//set方法,用于设置数据 noop 定义一个临时的空的函数
}

export function proxy (target: Object, sourceKey: string, key: string) {
  //  代理数据
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key] //返回数据
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val//设置数据
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
  // 通过Object.defineProperty方法,将sharedPropertyDefinition的get和set方法绑定到target对象的key属性上
}

toArray 将目标数组中的数据,从指定位置开始拷贝

  • 代码引用地址 src\vue-2.6.14\src\shared\util.js
  • 将目标数组中的数据,从指定位置开始拷贝

代码实现原理

  1. 接收字段
    • list 源数据
    • start 从源数据的哪个位置开始拷贝 默认为 0
  1. 声明一个变量i值为 list.length - start
  2. while 循环
    • 然后将 i + start
    • 则 最后的 i + start 仍然等于 list.length
    • 当i的值为0时,循环结束
  1. 则实现了将原数组从指定位置拷贝到目标数组的操作 代码优秀设计思想分析,它凭什么优秀
  2. 第一点,一般拷贝数组,
  • 常规的操作就是 for 循环,遍历数组中的每一个数据,然后根据条件判断,将数组中满足条件的数据拿到或者将不满足条件的数据过滤掉,这样确实能够处理数据,但是却需要将每条数据都循环一遍 .
  • 优秀的写法 此处,使用了while循环,不需要循环每一条数据,在调件变量为false的时候就跳出了循环,提高了代码的执行效率
  1. 可拷贝从指定位置开始的数据
  2. 语法层面上使用了 new Array(i)
  • 创建了指定数组,并且指定了位数,同时生明其中的每个变量都为empty
  • 很多人都知道new Array 却不知道 可以传入个数参数

当我们还开为这中需求苦恼,并且写一堆js时,vue源码中已经写出来如此优秀的代码,值得参考

js 复制代码
export function toArray (list: any, start?: number): Array<any> {
  // sjf-note-best-code
  start = start || 0
  let i = list.length - start
  const ret: Array<any> = new Array(i)
  // new Array() 创建一个空数组
  // new Array(i) 创建i个为空的数组
  // 相当于 Array.fill(0,i,undefined)

  while (i--) {
    ret[i] = list[i + start]
    //先将 i = list.length - start 
    // 然后将 i + start 
    // 则 最后的 i + start 仍然等于 list.length
    // 只不过是i 的值是从 list.length - start 到 0 递减
    // 当i的值为0时,循环结束
  }
  // while循环的优点是,即即使数组的数据没有处理完成,并且不需要break或者return false 以及 throw new Error()来结束循环,就可以自动结束循环
  return ret
  // 将处理好的数组返回
}

isPlainObject

  • 代码引用地址 src\vue-2.6.14\src\shared\util.js
  • 判断 数据类型是否是 object
js 复制代码
export function isPlainObject (obj: any): boolean {
  return _toString.call(obj) === '[object Object]'
}

validateComponentName

  • 代码引用地址 src\vue-2.6.14\src\core\util\options.js
  • 校验组件名是否合法
js 复制代码
export function validateComponentName (name: string) {
  if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid custom element name in html5 specification.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    // 
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}

isBuiltInTag 判断组件名是不是Vue内置的

  • 引用地址 src\vue-2.6.14\src\shared\util.js
js 复制代码
export const isBuiltInTag = makeMap('slot,component', true)
// 判断组件名是不是内置的标签,不是原生html的标签而是vue内置的标签

isReservedTag

js 复制代码
- 判断是否是 原生html或者isSVG标签 
export const isReservedTag = (tag: string): ?boolean => {
  return isHTMLTag(tag) || isSVG(tag)
}
export const isHTMLTag = makeMap(
  'html,body,base,head,link,meta,style,title,' +
  'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
  'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
  'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
  's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
  'embed,object,param,source,canvas,script,noscript,del,ins,' +
  'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
  'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
  'output,progress,select,textarea,' +
  'details,dialog,menu,menuitem,summary,' +
  'content,element,shadow,template,blockquote,iframe,tfoot'
)
export const isSVG = makeMap(
  'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' +
  'foreignobject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +
  'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',
  true
)

致谢

  • 感谢vue源码给予了我学习借鉴的机会
  • 感谢日常项目中给我的锻炼
  • 这篇文章是vue初始化流程的代码,其实没什么难度,但是这是最开始的一步,无法跳过,后期会有核心代码的分析,如果感兴趣请关注

  • 感谢您百忙之中抽时间阅读我写的博客,谢谢你的肯定,也希望对您能有所帮助
  • 如果您有更好的见解请在评论区留言或者私聊我,期待与您的交流
相关推荐
酷酷的阿云6 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子1 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x2 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落2 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
麦麦大数据2 小时前
基于vue+neo4j 的中药方剂知识图谱可视化系统
vue.js·知识图谱·neo4j
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea