IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Vue2.x 源码 - 初始化:initMixin(Vue)、initProxy(vm)、initInjections(vm)、initProvide(vm) -> 正文阅读

[JavaScript知识库]Vue2.x 源码 - 初始化:initMixin(Vue)、initProxy(vm)、initInjections(vm)、initProvide(vm)

上一篇:Vue2.x 源码学习准备

这篇主要看一下 initMixin 这个混入以及涉及到的 initProxy(vm)、initInjections(vm)、initProvide(vm) 方法;

准备工作

我们在使用 Vue 时是通过 new Vue()来进行初始化的,那么这个 Vue 从哪里来的呢?

1、在 main.js 引入的 Vue 是在入口文件 src/platforms/web/entry-runtimes.js 里面暴露出来的;
2、入口文件里面的 Vue 是从 src/platforms/web/runtime/index.js 里面引入的;
3、src/platforms/web/runtime/index.js 里面的 Vue 则是从 src/core/index.js 里面引入的;
4、src/core/index.js 里面的 Vue 又是从 src/core/instance/index.js 里面引入的;

这样就找到了 Vue的本体;那么 Vue 到底是什么呢?

src/core/instance/index.js 文件中

//vue本体,就是一个方法或者称之为一个类
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
//往vue的原型上混入一些自定义的原型方法
initMixin(Vue) // 定义 _init 方法
stateMixin(Vue) // 定义数据相关方法 $set、$delete、$watch
eventsMixin(Vue) // 定义事件相关方法 $on, $once, $off, $emit
lifecycleMixin(Vue) // 定义 _update、$forceUpdate(强制更新)以及生命周期方法 $destroy
renderMixin(Vue) // 定义方法 $nextTick、_render(将render函数转为vnode)
export default Vue

这里 warn 信息直接告诉我们 Vue 的本质:Vue是一个构造函数,应该用“new”关键字调用 ;然后在 Vue 构造函数里面执行 this._init(options) 方法,传入初始化参数;代码最后面执行几个方法往 Vue 原型上混入一些自定义的原型方法,下面会分别说明一下。

一、初始化混入 initMixin(Vue)

src/core/instance/init.js 文件里面

let uid = 0
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // 每个组件初始化时添加唯一标识
    vm._uid = uid++
    let startTag, endTag
    // window的performance属性的相关方法,通过传入的相关数值,记录性能
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }
    // 初始化后 vm 实例标记为true,其他场景会用到,例如 observer
    vm._isVue = true
    // 合并选项
    if (options && options._isComponent) { // 组件形式的实例
      // 为vm.$options添加一些属性
      initInternalComponent(vm, options)
    } else { // 非组件形式的实例
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    //代理初始化,不同环境不一样的初始化方法
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {// 如果不是开发环境,则vue实例的_renderProxy属性指向vue实例本身
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm) // 初始化组件实例相关属性 $parent、$children、$root、$refs 等
    initEvents(vm) // 初始化自定义事件
    initRender(vm) // 挂载可以将 render函数转为vnode 的方法
    callHook(vm, 'beforeCreate') //调用 beforeCreate 钩子函数
    initInjections(vm) // 初始化inject
    initState(vm) // 初始化 data/props
    initProvide(vm) // 初始化 provide
    callHook(vm, 'created') //调用 created 钩子函数
    // window的performance属性的相关方法,通过传入的相关数值,记录性能
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
	//配置项有 el,自动调用 $mount 方法挂载,挂载的?标就 是把模板渲染成最终的 DOM
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

这一部分主要做了如下操作:

1、缓存当前的上下文到 vm 变量中;
2、为 vm 添加唯一标识;
3、打标记用来测试性能;
4、用 _isVue 值标识当前实例;
5、合并选项,区分组件实例和非组件实例;
6、初始化代理;
7、下面就是一些组件、事件、render、inject、data/props、provide 的初始化;
8、再次打标记用来测试性能;
9、el 挂载。

总结:先合并 options => 初始化代理 => 初始化组件实例相关属性,确定组件(Vue实例)的父子关系 => 初始化事件,将父组件自定义事件传递给子组件 => 绑定将 render 函数转化为 vnode 的方法 => 调用 beforeCreate 生命周期钩子 => 初始化inject, 让子组件可以访问到对应的依赖 => 将组件定义的状态(props, methods, data, computed, watch)挂载到this下 => 初始化provide 为子组件提供依赖 => 调用 created生命周期钩子 => 执行 $mount 挂载 el。

1、initInternalComponent:合并组件实例的参数
// 优化内部组件实例化,因为动态选项合并非常慢,而且内部组件选项不需要特殊处理。
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  //这样做是因为它比动态枚举更快
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode
  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag
  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

Object.create这个函数,把组件构造函数的 options 挂载到 vm.$options__proto__上,指定vm.$options 的原型;通过传入的参数 options 为 vm.$options 添加一些属性,把组件依赖父组件的 props 、listeners 等属性挂载到 vm.$options ,方便子组件调用。

2、resolveConstructorOptions:合并父级构造器参数和实例本身参数

区分 Vue 构造器和 Vue.extend 拓展器,Ctor.superVue.extend 里面定义的属性。如果是构造器则直接返回参数,如果是拓展器则执行内部代码。

export function resolveConstructorOptions (Ctor: Class<Component>) {
  //构造函数上的option
  let options = Ctor.options
  //有super属性则说明Ctor是Vue.extend构建的子类,super指向父级构造器
  if (Ctor.super) {
    //递归获取父级上最新的 options(可能不止一个父级,所以要递归)
    const superOptions = resolveConstructorOptions(Ctor.super) 
    //extend时父级默认的options
    const cachedSuperOptions = Ctor.superOptions 
    //有可能会被 Vue.mixin 混入一些新的属性,所以这里要判断父类是否变了,变了则赋值更新
    if (superOptions !== cachedSuperOptions) {
      // options入参变了,修改默认参数
      Ctor.superOptions = superOptions
      //检查是否有任何后期修改/附加的选项(mixin)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // 更新基本扩展选项,一般在混入新的 options会用到
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}
//获取到执行extend之后出现变化的options项
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  let modified
  //自身的options
  const latest = Ctor.options
  //执行Vue.extend时封装的options
  const sealed = Ctor.sealedOptions
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = latest[key]
    }
  }
  return modified
}

执行 extend 时,会出现 Vue.mixin 来对父级和子级混入一些参数,这个时候需要判断拓展器执行前后父级和子级的参数是否发生了变化,变了则更新最新的参数;最后返回合并后的的构造函数的options。

3、mergeOptions:合并实例参数和入参

src/core/util/options.js 文件中,这个方法的目的是合并构造函数 options 和传入的 options 这两个对象。

export function mergeOptions (
  parent: Object, //构造器参数
  child: Object, //实例化时传入参数
  vm?: Component //实例本身
): Object {
  //检查组件名称是否合法,不合法则发出警告信息。
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child) 
  }
  //如果child是function类型的话,我们取其options属性作为child
  if (typeof child === 'function') {
    child = child.options
  }
  //分别是把options中的props,inject,directives属性转换成对象的形式
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  //只有合并选项具有_base属性
  //当传入的options里有mixin或者extends属性时,再次调用mergeOptions方法合并mixins和extends里的内容到实例的构造函数options上
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  //核心合并策略strats、defaultStrat
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

这里合并 options 的时候会针对 props、inject、directives 分别进行合并;当传入的 options 里有 mixin 或者 extends 属性时会再次调用 mergeOptions 方法合并 mixins 和 extends 里的内容到实例的构造函数 options 上。

然后就是这个方法的核心部分,分别循环 parent 和 child ,循环 child 时额外判断当前属性不在 parent 属性里,然后调用 mergeField 方法通过 strats 和 defaultStrat 合并策略来合并 options。

defaultStrat 的逻辑是,如果 child 上该属性值存在时,就取 child 上的该属性值,如果不存在,则取 parent 上的该属性值。
strats 又细分几种策略:

1、el、propsData :直接走的 defaultStrat 策略;
2、component、directive、filter:首先缓存 parent ;如果 child 有则合并,以 child为准;
3、watch:child 属性不存在直接返回 parent;child 属性存在则判断是不是对象;parent 属性不存在则直接返回 child 属性;都存在则合并;
4、props、methods、inject、computed:如果 child 属性存在则判断是否是对象,parent 上没有该属性则直接返回 child 上属性;如果 child 和 parent 都有则合并,以child 的值为准;
5、钩子函数:child 上不存在而 parent 上存在则返回 parent 上属性;child 和 parent 上都有则返回 concat 后的属性(同名child覆盖parent);child 有而 parent 上没有则返回 child 属性(这个属性必须是数组,如果不是则转成数组);
6、data 、provide:会区分合并的是不是 Vue 实例;是Vue实例,options 有 data 属性则调用 mergeData 合并 child 和 parent,没有则走 defaultStrat 策略;不是 Vue 实例,没有 child 则返回 parent,没有 parent 则返回 child,两个都有则调用 mergeData 合并 child 和 parent;

总之,就是会以 child 属性为准。

这里就把所有业务逻辑和组件的一些特性全部都转化放到 vm.$options 里面了,后面用到的时候只需要从 vm.$options 里面取值就可以了。

二、initProxy:初始化代理

let initProxy
initProxy = function initProxy (vm) {
    // 判断当前环境 Proxy 是否可用
    if (hasProxy) {
      // 确定要使用哪个代理处理程序
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }
  const hasHandler = {
    has (target, key) {
      const has = key in target
      const isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
      if (!has && !isAllowed) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }
  const getHandler = {
    get (target, key) {
      if (typeof key === 'string' && !(key in target)) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return target[key]
    }
  }
  export { initProxy }

当前环境 Proxy 不可用则 Vue 实例的 _renderProxy 属性指向 Vue 实例本身;Proxy 可用,如果实例的 options 上面存在 render 然后 render 上有 _withStripped 属性则调用 getHandler 否则调用 hasHandler。

getHandler :针对读取代理对象的属性时进行操作,属性不是字符串或者不存在则报错,否则返回属性值;
hasHandler:开发过程中错误调用 vm 属性时,起提示作用;

注意:options.render._withStripped这个只有在严格模式下不支持with时,手动设置为true才启用,所以一般都是使用hasHandler

三、initInjections(vm)、initProvide(vm)

src/core/instance/inject.js文件里面

export function initInjections (vm: Component) {
  //获取 inject 选项对应的依赖
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    //关闭响应式绑定
    toggleObserving(false)
    //遍历每一项属性
    Object.keys(result).forEach(key => {
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        //定义对象上的响应性属性
        defineReactive(vm, key, result[key])
      }
    })
    //打开响应式绑定
    toggleObserving(true)
  }
}
//遍历inject的key,如果provide中有key与inject的from属性同名,则将这个数据给result,如果没有,检测是否有默认值,将默认值给result,最后返回
export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // 创建空对象
    const result = Object.create(null)
    //如果支持hasSymbol,支持就用Reflect,否则用Object
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)
    //遍历inject属性
    for (let i = 0; i < keys.length; i++) {
      // 获取每个属性的值
      const key = keys[i]
      if (key === '__ob__') continue
      //provide的ky是否等于inject的from
      const provideKey = inject[key].from
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      if (!source) {
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    return result
  }
}

先遍历每一项,然后在遍历每一项的父级是否提供该项的依赖,有就返回到 result ,没有就继续找;获取到 result 之后会调用 toggleObserving 来关闭响应式绑定属性,具体实现在 defineReactive 里面,通过传参 true 和 false 来决定是否将绑定的属性设置为响应式数据;这里刻意在绑定 inject 上属性时关闭响应式绑定。

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

从 vm 实例的 options 里面取出 provide ,如果 provide 是 function 则绑定 this 给 vm._provided 私有属性,不是 function 则直接赋值给 vm._provided 私有属性;这样子组件就可以访问父组件提供的依赖了。

这里要额外的说说 inject 和 provide 这两个 API,他们俩是搭配使用的,provide 在父组件提供依赖绑定到 vm 实例的 _provided 属性上,这样可以全局访问这些绑定的依赖,inject 则在子组件通过入参在自己的父级链上获取到对应的依赖。

这里有一个问题:在初始化 inject 的时候会去父级上去找 provide ,但是 provide 的初始化在 inject 后面,这样会不会有问题呢?

答案是没有问题;这两个 API 主要是处理父子组件之间的传值,在初始化的时候,首先会初始化父组件,然后才会初始化子组件,所以这个时候子组件是可以拿到父组件里面的 provide 的;(父beforeCreate -> 父created -> 父beforeMount ->子beforeCreate -> 子created ->子beforeMount -> 子mounted-> 父mounted)

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-10-26 12:07:25  更:2021-10-26 12:09:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/13 2:37:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码