【源码学习】你知道data,props,methods初始化的顺序么? (附思维导图)

我心飞翔 分类:vue

声明

🔊 本文是开始学习 Vue 源码的第三篇笔记,当前的版本是 2.6.14 。如果对你有一点点帮助,请点赞鼓励一下,如果有错误或者遗漏,请在评论区指出,非常感谢各位大佬。

🔊 代码基本上是逐行注释,由于本人的能力有限,很多基础知识也进行了注释和讲解。由于源码过长,文章不会贴出完整代码,所以基本上都是贴出部分伪代码然后进行分析,建议在阅读时对照源码,效果更佳。

🔊 从本篇文章开始,可能会出现暂时看不懂的地方,是因为还没有学习前置知识,不必惊慌,只需知道存在这样一个知识点,接着向下看,看完了前置知识,回过头来再看这里就一目了然了。

本文代码所在路径\vue-dev\src\core\instance\state.js

前言

先回顾一下上文,我们知道了 Vue 的初始化过程,在 Vue.prototype._init 中我们分成四个部分进行分析,其中第三部分做了一系列的初始化,本文继续学习其中的一个初始化过程,响应式原理的核心部分 initState 。也就是 datapropsmethodswatchcomputed 的初始化过程。

【源码学习】你知道data,props,methods初始化的顺序么? (附思维导图)

initState

代码注释

/**
 * @description: 初始化数据 响应式原理的入口
 * @param {*} vm 实例Vm
 */
export function initState (vm: Component) {
  // 为当前组件创建了一个watchers属性,为数组类型  vm._watchers保存着当前vue组件实例的所有监听者(watcher)
  vm._watchers = []
  // 从实例上获取配置项
  const opts = vm.$options
  //如果vm.$options上面定义了props 初始化props 对props配置做响应式处理  
  //代理props配置上的key到vue实例,支持this.propKey的方式访问
  if (opts.props) initProps(vm, opts.props)
  //如果vm.$options上面定义了methods 初始化methods ,props的优先级 高于methods的优先级
  //代理methods配置上的key到vue实例,支持this.methodsKey的方式访问
  if (opts.methods) initMethods(vm, opts.methods)
  //如果vm.$options上面定义了data ,初始化data, 代理data中的属性到vue实例,支持通过 this.dataKey 的方式访问定义的属性
  if (opts.data) {
    initData(vm)
  } else {
    //这里是data为空时observe 函数观测一个空对象:{}
    observe(vm._data = {}, true /* asRootData */)
  }
  //如果vm.$options上面定义了computed 初始化computed
  //computed 是通过watcher来实现的,对每个computedKey实例化一个watcher,默认懒执行.
  //将computedKey代理到vue实例上,支持通过this.computedKey的方式来访问computed.key
  if (opts.computed) initComputed(vm, opts.computed)
  //如果vm.$options上面定义了watch 初始化watch
  if (opts.watch && opts.watch !== nativeWatch) { 
    // 判断组件有watch属性 并且没有nativeWatch( 兼容火狐)
    initWatch(vm, opts.watch)
  }
}

代码解读

⭐ 为当前组件创建了一个 watchers 属性,为数组类型 vm._watchers 保存着当前 vue 组件实例的所有监听者(watcher)

⭐ 从代码中可以看出,初始化的顺序是 props -> methods -> data -> computed -> watch

initProps 如果 vm.$options 上面定义了 props 初始化 propsprops 配置做响应式处理,代理 props 配置上的 keyvue 实例,支持 this.propKey 的方式访问。

initMethods 如果 vm.$options 上面定义了 methods 初始化 methods , props 的优先级 高于 methods 的优先级,代理 methods 配置上的 keyvue 实例 , 支持 this.methodsKey 的方式访问。

initData 如果 vm.$options 上面定义了 data ,初始化 data, 代理 data 中的属性到 vue 实例,支持通过 this.dataKey 的方式访问定义的属性。data 为空时 observe 函数观测一个空对象。

initComputed 如果 vm.$options 上面定义了 computed 初始化 computedcomputed 是通过watcher 来实现的,对每个 computedKey 实例化一个 watcher,默认懒执行。将 computedKey 代理到 vue 实例上,支持通过 this.computedKey 的方式来访问 computed.key

initWatch 判断组件有 watch 属性,并且没有 nativeWatch( 兼容火狐)。如果 vm.$options 上面定义了 watch 初始化 watch

proxy

代码注释

// 代理对象
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

/**
 * 代理 通过sharedPropertyDefinition对象 给key添加一层getter和setter  将key代理到 vue 实例上
 * 当我们访问this.key的时候,实际上就会访问 vm._data.key / vm._props.key
 * @param {*} target  实例vm
 * @param {*} sourceKey  _data / _props
 * @param {*} key data / props 中的属性
 */

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
  }
  // 拦截对 this.key的访问
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

代码解读

⭐ 通过 sharedPropertyDefinition 对象 给 key 添加一层 gettersetterkey 代理到 vue 实例上,当我们访问 this.key 的时候,实际上就会访问 vm._data.key / vm._props.key

initProps

代码注释

/**
 * @description: 初始化props
 * @param {*} vm 实例vm
 * @param {*} propsOptions 配置对象上的props
 */
function initProps (vm: Component, propsOptions: Object) {
  // 存放父组件传入子组件的props
  const propsData = vm.$options.propsData || {}
  // 存放经过转换后的最终的props的对象, props 与 vm._props 保持同一个引用,初始值为 {}
  const props = vm._props = {}

  // 缓存 props 的每个 key,性能优化, 一个存放props的key的数组,就算props的值是空的,key也会存在里面 ,keys 与 vm.$options._propKeys 保持同一个引用,初始值为 {}
  const keys = vm.$options._propKeys = []

  // 判断是不是根元素
  const isRoot = !vm.$parent

  //当组件不是根组件时,使用 toggleObserving(false) 取消对 Object Array 类型 Prop 深度观测,为什么这么做呢,因为 Object Array 在父组件中已经被深度观测过了。
  if (!isRoot) {
    toggleObserving(false)
  }
  
  // 遍历props配置对象
  for (const key in propsOptions) {
    // 向缓存键值数组中添加键名
    keys.push(key)
    /**
     * 用validateProp校验是否为预期的类型值,然后返回相应 prop 值(或default值)
     * 如果有定义类型检查,布尔值没有默认值时会被赋予false,字符串默认undefined
     */
    const value = validateProp(key, propsOptions, propsData, vm)
    //非生产环境
    if (process.env.NODE_ENV !== 'production') {
      // 进行键名的转换,将驼峰式转换成连字符式的键名
      const hyphenatedKey = hyphenate(key)
      
      // 校验prop是否为内置的属性, 内置属性:key,ref,slot,slot-scope,is
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      // 对属性建立观察,并在直接使用props属性时给予警告
      defineReactive(props, key, value, () => {
        // 子组件直接修改属性时 弹出警告
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
       // 生产环境下直接对属性进行存取器包装,建立依赖观察, 为 props 的每个 key 设置数据响应式
      defineReactive(props, key, value)
    }

    // 当实例上没有同名属性时,对属性进行代理操作,将对键名的引用指向vm._props对象中
    if (!(key in vm)) {
      // 代理 key 到 vm 对象上
      proxy(vm, `_props`, key)
    }
  }
   // 开启观察状态标识, 重新打开观测开关,避免影响后续代码执行
  toggleObserving(true)
}

代码解读

⭐ 初始化变量 propsData 存放父组件传入子组件的 props。const props = vm._props = { }  存放经过转换后的最终的 props 的对象 , props 与 vm._props 保持同一个引用,初始值为 {}。 const keys = vm.options._propKeys = [] , keys 与 vm.options._propKeys 保持同一个引用,初始值为 [] 。isRoot 判断是不是根元素。

⭐ 当组件不是根组件时,使用 toggleObserving(false) 取消对 Object Array 类型 Prop 深度观测。

⭐ 遍历 props 配置对象。缓存 props 的每个 key ,用以性能优化 。

⭐ 校验是否为预期的类型值,然后返回相应 prop 值(或 default 值),如果有定义类型检查,布尔值没有默认值时会被赋予 false,字符串默认 undefined

defineReactive,对属性建立观察。

⭐ 当实例上没有同名属性时,对属性进行代理操作 , 将对键名的引用指向 vm._props 对象中。

⭐ 开启观察状态标识,重新打开观测开关,避免影响后续代码执行 toggleObserving(true)

⭐ 本文对 initProps 掌握到这里即可,后面会详细分析 defineReactive 方法。

initMethods

代码注释

/**
 * @description: 初始化methods
 * @param {*} vm 实例vm
 * @param {*} methods 实例配置项上面的methods vm.$options.methods
 */
function initMethods (vm: Component, methods: Object) {
  // 获取实例配置上的props
  const props = vm.$options.props
  // 做一些检查 然后赋值给Vue实例
  for (const key in methods) {
    // 判断环境 只在非生产环境下起作用
    if (process.env.NODE_ENV !== 'production') {
      // 判断key是否是function类型
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      // 检测 methods 中的属性名是否与 props 冲突,由 initState 方法我们知道,props 是先与 methods 初始化的。
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      // 检测 methods 是否使用了关键字保留字, 而且不允许以$ 或者 _ 开头。
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    /**
     * 将 methods 中的所有方法赋值到 vue 实例上 ,支持通过 this.methodsKey 的方式访问定义的方法
     * 如果 key 不是一个函数 则赋值为空函数
     * 如果 key 是函数 则执行bind()函数
     */
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

代码解读

⭐ 判断属性是否是 function 类型,检测 methods 中的属性名是否与 props 冲突,由 initState 方法我们知道,props 是先于 methods 初始化的。检测 methods 是否使用了关键字保留字,而且不允许以 $ 或者 _ 开头。

⭐ 将 methods 中的所有方法赋值到 vue 实例上 , 支持通过 this.methodsKey 的方式访问定义的方法。

initData

代码注释

/**
 * @description: 初始化data
 * @param {*} vm 实例vm
 */
function initData (vm: Component) {
  //从vm.$options.data里面拿到data,就是我们在开发时候定义的data  赋值给data 还有vm._data
  let data = vm.$options.data
  /**
   * 判断data是不是一个function 保证后续处理的data是一个对象
   * 如果是 执行getData方法
   * 如果不是 返回 data || {}
   */

  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  //如果不是个对象的话,开发环境下会报一个警告
  if (!isPlainObject(data)) {
    //把data重置为一个空对象
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  //拿到data对象的key 组成一个数组
  const keys = Object.keys(data)
  //拿到props
  const props = vm.$options.props
  //拿到methods
  const methods = vm.$options.methods

  /**
   * 循环判断data中的属性和props,methods中的属性是否冲突
   * 因为所有的data,props,methods最终都会挂载到vm实例上
   */

  let i = keys.length
  while (i--) {
    const key = keys[i]
    //非生产环境
    if (process.env.NODE_ENV !== 'production') {
      //与methods判重
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    //与props判重
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      //判重通过,最终交给proxy做代理 ,代理data中的属性到vue实例,支持通过 this.dataKey 的方式访问定义的属性
      proxy(vm, `_data`, key)
    }
  }
  // 对data进行响应式处理
  observe(data, true /* asRootData */)
}

//如果data是一个函数 那么会走这个方法

export function getData (data: Function, vm: Component): any {

  // 收集依赖
  pushTarget()
  try {
    // 调用call 返回的值就是这个对象
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    // 释放依赖
    popTarget()
  }
}

代码解读

data 为空,直接观测一个空对象 observe(vm._data = {} , true)

data 不为空,判断 data 是不是一个 function,保证后续处理的 data 是一个对象。

⭐ 循环判断 data 中的属性和 props , methods 中的属性是否冲突,由 initState 方法我们知道,propsmethods 是先于 methods 初始化的。

⭐ 对 data 进行响应式处理 observe(data , true)

⭐ 本文对 initData 掌握到这里即可,后面会详细分析 observe 方法。

initComputed

代码注释

//用于传入Watcher实例的一个对象 懒执行
const computedWatcherOptions = { lazy: true }

/**
 * @description: 初始化computed
 * @param {*} vm 实例vm
 * @param {*} computed 定义的computed配置
 */
function initComputed (vm: Component, computed: Object) {

  // 声明变量 watchers,与 vm._computedWatchers 保持同一个引用,并且初始化值为空对象。
  const watchers = vm._computedWatchers = Object.create(null)

  // 声明变量isSSR,判断是不是 ssr(服务端渲染)
  const isSSR = isServerRendering()

  // 遍历 computed 配置对象 
  for (const key in computed) {
    // 获取 key 当次遍历对应的值.
    const userDef = computed[key]
    /**
     * 使用过 computed 都知道,它有两种写法  函数写法以及对象写法
     * computed: {
        compA: function() { return this.a + 1 },
        compB: {
                 get: function() { return this.b + 1 },
               }
       }
     * 判断是不是函数,如果是函数 getter 就是函数本身,如果是对象,getter就用他的get属性
     */
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    // 非开发环境下getter如果为null,警告
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    // 如果不是SSR
    if (!isSSR) {
      /**
       * 针对当次循环的 computed,实例化一个 Watcher , 所以computed其实就是通过Watcher来实现的
       * watchers 保存了 vm._computedWatchers 的引用,所以这里同样会将该 watcher 保存到 vm._computedWatchers。
       * 每一个 computed 的 key,都会生成一个 watcher 实例,并且保存到 vm._computedWatchers 这个对象上。
       */

      watchers[key] = new Watcher(
        vm, //实例vm
        getter || noop, // getter
        noop, // 空函数
        computedWatcherOptions // 配置对象 懒执行(不可更改)
      )
    }

    //if 语句用来检测 computed 的命名是否与 data,props 冲突,在非生产环境将会打印警告信息。
    if (!(key in vm)) {
      //不冲突时,调用 defineComputed 方法。
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
      //与data中的属性冲突
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        //与props中的属性冲突
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        //与methods中的属性冲突
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}

/**
 * @description: 为 sharedPropertyDefinition 添加 get, set 属性,将该 computed 属性添加到 Vue 实例 vm 上,并使用 sharedPropertyDefinition 作为设置项。
 * @param {*} target vm实例
 * @param {*} key 当次循环的computedKey
 * @param {*} userDef   computed.key
 */
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  //
  const shouldCache = !isServerRendering()


  if (typeof userDef === 'function') {
    // 如果computed.key是function类型走这里

    //设置sharedPropertyDefinition配置对象的get方法
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    //设置sharedPropertyDefinition配置对象的set方法
    sharedPropertyDefinition.set = noop
  } else {
    //如果computed.key不是function类型走这里

    //设置sharedPropertyDefinition配置对象的get方法
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    //设置sharedPropertyDefinition配置对象的get方法
    sharedPropertyDefinition.set = userDef.set || noop
  }
  //如果是非生产环境 并且sharedPropertyDefinition的set方法是noop
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    //将sharedPropertyDefinition的set方法设置为警告
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  //将computed配置项中的key,代理到vue实例上,支持通过this.computedKey的方式去访问 computed中的属性
  Object.defineProperty(target, key, sharedPropertyDefinition)
}


/**
 * @description: 在这里我们暂时只需要知道sharedPropertyDefinition的 get属性 被设置为这个方法的返回值就行
 * @param {*} key computedKey
 * @return {*} computedGetter
 */
function createComputedGetter (key) {
  return function computedGetter () {
    //拿到watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        //执行watcher.evaluate方法
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

/**
 * @description: 在这里我们暂时只需要知道sharedPropertyDefinition的 get属性 被设置为这个方法的返回值就行
 * @param {*} fn userDef.get
 * @return {*} computedGetter
 */
function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

代码解读

⭐ 声明变量 watchers,与 vm._computedWatchers 保持同一个引用,并且初始化值为空对象。

⭐ 声明变量 isSSR , 判断是不是 ssr (服务端渲染)。

⭐ 遍历 computed 配置对象,声明 userDef 变量存放当次遍历 key 对应的值 。 声明 getter 变量, 判断 userDef 是不是函数 , 如果是函数 getter 就是函数本身 , 如果是对象 getter 就用他的 get 属性 。非生产环境下 getter 如果为 null , 发出警告。如果不是 SSR,针对当次循环的 computed,实例化一个 Watcherwatchers 保存了 vm._computedWatchers 的引用,所以这里同样会将该 watcher 保存到 vm._computedWatchers。每一个 computedkey,都会生成一个 watcher 实例,并且保存到 vm._computedWatchers 这个对象上。检测 computed 的命名是否与 dataprops 冲突,在非生产环境将会打印警告信息。不冲突时,调用 defineComputed 方法。

⭐ 本文对 initComputed 掌握到这里即可,后面会详细分析 defineComputed 方法。

initWatch

代码注释

/**
 * @description: 初始化watch
 * @param {*} vm 实例vm
 * @param {*} watch  watch配置项 / vm.$options.watch
 */
function initWatch (vm: Component, watch: Object) {
  
  //遍历watch配置项  从这可以看出 key 和 watcher 实例可能是 一对多 的关系
  for (const key in watch) {
    //获取当次遍历 key 对应的值
    const handler = watch[key]
    //如果是数组的话
    if (Array.isArray(handler)) {
      //循环数组 为数组的每一项调用createWatcher方法
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      // 如果不是数组 直接调用createWatcher方法
      createWatcher(vm, key, handler)
    }
  }
}


/**
 * @description: 兼容性处理,保证 handler 肯定是一个函数,调用 $watch 
 * @param {*} vm 实例vm
 * @param {*} expOrFn watchKey
 * @param {*} handler watch.key
 * @param {*} options 配置选项
 */
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  //如果是对象 从 handler 属性中获取函数
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  //如果是字符串 表示的是一个methods方法,直接通过 this.methodsKey的方式  拿到这个函数
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  //调用vm.$watch方法
  return vm.$watch(expOrFn, handler, options)
}

代码解读

⭐ 遍历 watch 配置项 ,获取当次遍历 key 对应的值,如果是数组的话,循环数组,为数组的每一项调用 createWatcher 方法,如果不是数组,直接调用 createWatcher 方法。

⭐ 从这可以看出 keywatcher 实例可能是 一对多 的关系。

⭐ 本文对 initWatch 掌握到这里即可,后面会详细分析 createWatcher 方法。

总结

最后我们用一张思维导图总结一下

【源码学习】你知道data,props,methods初始化的顺序么? (附思维导图)

参考

Vue.js 技术揭秘

精通 Vue 技术栈的源码原理

本文由 李永宁 教程结合自己的想法整理而来,在此特别感谢前辈。

回复

我来回复
  • 暂无回复内容