• 【源码学习】Vue中的props、methods、data、computed、watch初始化的顺序


    声明
    本文是开始学习 Vue 源码的第三篇笔记,当前的版本是 2.6.14 。

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

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

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

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

    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 初始化 props 对 props 配置做响应式处理,代理 props 配置上的 key 到 vue 实例,支持 this.propKey 的方式访问。

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

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

    ⭐ initComputed 如果 vm.$options 上面定义了 computed 初始化 computed。computed 是通过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 添加一层 getter 和 setter 将 key 代理到 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 方法我们知道,props ,methods 是先于 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,实例化一个 Watcher 。watchers 保存了 vm._computedWatchers 的引用,所以这里同样会将该 watcher 保存到 vm._computedWatchers。每一个 computed 的 key,都会生成一个 watcher 实例,并且保存到 vm._computedWatchers 这个对象上。检测 computed 的命名是否与 data,props 冲突,在非生产环境将会打印警告信息。不冲突时,调用 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 方法。

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

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

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

    参考

    Vue.js 技术揭秘

    精通 Vue 技术栈的源码原理

  • 相关阅读:
    Linux命令发送Http的get或post请求(curl和wget两种方法)
    大数据面试题以及答案整理(一)
    大数据面试题及答案-汇总版
    Linux shell之打印输出
    Java开发中常见的危险信号(上)
    sencha touch笔记(5)——DataView组件(1)
    sencha touch(7)——list组件
    sencha touch笔记(6)——路由控制(1)
    [置顶] Android源码分析-点击事件派发机制
    UVa 10330 Power Transmission / 最大流
  • 原文地址:https://www.cnblogs.com/caihongmin/p/16664307.html
Copyright © 2020-2023  润新知