学习vue源码(19)四探生命周期之初始化props

1,289 阅读8分钟

前面文章已经 把 created钩子函数触发前,beforeCreate触发后 的initInjections和initProvide讲完了,现在开始讲 initState的props部分。

  // src/core/instance/init.js
  Vue.prototype._init = function (options?: Object) {
    ……
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
  }

如代码所示,这一部分,也是created钩子函数触发前,beforeCreate触发后的最后一部分了。

这一部分也叫做初始化状态

当我们使用Vue.js开发应用时,经常会使用一些状态,例如props、methods、data、computed和watch。在Vue.js内部,这些玩状态在使用之前需要进行初始化。

initState函数

export function initState(vm){
 vm._watchers = [];
 const opts = vm.$options;
 if(opts.props) initProps(vm,opts.props);
 if(opts.methods) initMethods(vm,opts.methods);
 if(opts.data){
  initData(vm);
 }else{
  <!-- 不存在,则直接使用observe函数观察空对象 -->
  <!-- observe函数的作用是将数据转换为响应式的 -->
  observe(vm._data = {},true/*asRootData*/);
 }
 if(opts.computed) initComputed(vm,opts.computed);
 if(opts.watch && opts.watch !== nativeWatch){
  initWatch(vm,opts.watch);
 }

1、首先在vm上新增一个属性_watchers,用来保存当前组件中所有的watcher实例。无论是使用vm.$watch注册的watcher还是使用watch选项添加的watcher实例,都会添加到vm._watchers中。

2、可以通过vm._watchers得到当前Vue.js实例中所注册的所有watcher实例,并将它们一次卸载。

3、用户在实例化Vue.js时使用了哪些状态,哪些状态就需要被初始化,没有用到的状态则不用初始化。例如,用户只使用了data,那么只需要初始化data即可。

4、初始化的顺序其实是精心安排的。先初始化props,后初始化data,这样就可以在data中使用props中的数据了。在watch中既可以观察props,也可以观察data,因为它是最后被初始化的。

5、初始化状态可以分为5个子项,分别是初始化props、初始化methods、初始化data、初始化computed和初始化watch。

初始化props

开始之前,我提出问题

1、父组件 怎么传值给 子组件的 props
2、子组件如何读取props

这一节,我们带着问题去开始我们的讲解

明白这三个问题,那么相信你也就理解了 props 的工作原理

场景设置

现在我们有一个这样的 根组件 A 和 他的 子组件 testb 根组件A 会 把 parentName 传给 子组件 testb 的 props 根组件A 也是 组件testb 的 父组件

<div class="a" >
    <testb :child-name="parentName" ></testb>
</div>
new Vue({    
    el:".a",        
    name:"A",    
    components:{        
        testb:{            
            props:{                
                childName:""
            },            
            template: '<p>父组件传入的 props 的值 {{childName}}</p>',
        }
    },
    data(){        
        return {            
            parentName:"我是父组件"
        }
    },
})

按照上面的例子,开始我们的问题解析

父组件怎么传值给子组件的 props

这部分内容是 props 的重中之重,必须理解

1 props 传值的设置

根据上面的场景设置,testb 是一个子组件,接收一个 props(child-name) 然后 根组件 A 把 自身的 parentName 绑定到 子组件的属性 child-name 上

2 props 父传子前

父组件的模板 会被解析成一个 模板渲染函数

(function() {    
    with(this){  
        return _c('div',{staticClass:"a"},[
            _c('testb',{attrs:{"child-name":parentName}})
        ],1)
    }
})

这段代码需要解释下

其实,这是属于代码生成器的部分,感兴趣的可以看学习vue源码(9)手写代码生成器

1、_c 是渲染组件的函数,_c('testb') 表示渲染 testb 这个子组件
2、因为 with 的作用是,绑定大括号内代码的 变量访问作用域
3、这是一个匿名自执行函数,会在后面执行

简化上面的函数,做个例子测试一下

3 .props 开始赋值

之后,模板函数会被执行,执行时会绑定 父组件为作用域

所以渲染函数内部所有的变量,都会从父组件对象 上去获取

绑定了父作用域之后, parentName 自然会从父组件获取,类似这样

{ attrs: { child-name: parentVm.parentName } }

函数执行了,内部的 _c('testb') 第一个执行,然后传入了 赋值后的 attrs

父组件赋值之后的 attrs 就是下面这样

{ attrs: { child-name: "我是父组件" } }

此时,父组件就正式 利用 props 把 parentName 传给了 子组件的props child-name

4 .子组件保存 props

_c('testb',{attrs:{"child-name":parentName}})

子组件拿到父组件赋值过后的 attr

而 attrs 包含 普通属性 和 props,所以需要 筛选出 props,然后保存起来

5 .子组件 设置响应式 props

props 会被 保存到 实例的 _props 中,并且 会逐一复制到 实例上,并且每一个属性会被设置为 响应式 的

你看到的,每一个 实例都会有 一个 _props 的同时,也会把属性直接放在 实例上。

对于props还有个问题:就是我们写的时候可是是数组形式,也是对象形式,这是为什么呢?

  1. 数组形式
props: ['name', 'value']
  1. 对象形式

对象形式内部也提供了三种写法:

props: {
    // 基础的类型检查
    name: String,
    // 多个可能的类型
    value: [String, Number],
    // 对象形式
    id: {
        type: Number,
        required: true
    }

}

其实 vue初始化时,会把props统一规格化成对象形式

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    ...
  } else if (isPlainObject(props)) {
    ...
  } else if (process.env.NODE_ENV !== 'production') {
    ...
  }
  options.props = res
}

normalizeProps函数就是vue实际处理props的地方,从函数名的翻译我们可以看出该函数的功能就是标准化props的值。该函数主要分成3部分:① 从options对象中获取props的值并且定义一个res空对象;②几个if ... else,分别根据props值的不同类型来处理res对象;③ 用处理后的res对象覆盖原来options对象的props属性的值。

接下来看看那几个if ... else的代码:

if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  }

这个代码实际就是处理props的值为数组的情况,例如:

props: ['name', 'value']

。使用while遍历该数组,如果数组内元素的类型不是字符串并且不是生产环境,那么就抛错:‘props的值类型为数组时,数组里面的元素的类型就必须是字符串’。如果是字符串的情况下,使用camelize函数处理一下val的值,并且赋值给name变量。这里的camelize函数的实际作用就是将'-'转换为驼峰。camelize函数具体的实现方式在后面分析。然后在res对象上面添加一个为name变量的属性,该属性的值为空对象 { type: null }。

props: ['name', 'value']

这种写法经过上面的处理后就会变成了下面这样:

props: {
    name: {
        type: null
    },
    value: {
        type: null
    }
}

接下来看看下面这个else if(isPlainObject(props)),这里的isPlainObject函数实际就是返回props的值是否为object,isPlainObject函数的具体实现我们也在后面分析。

else if (isPlainObject(props)) {
   for (const key in props) {
     val = props[key]
     name = camelize(key)
     res[name] = isPlainObject(val)
       ? val
       : { type: val }
   }
 }

使用for...in遍历props对象,和上面一样使用camelize函数将'-'转换为驼峰。这里有个三目运算:

res[name] = isPlainObject(val) ? val : { type: val } 判断了一下val如果是object,那么在res对象上面添加一个为name变量的属性,并且将该属性的值设置为val。这个其实就是处理下面这种props的写法:

props: {
   // 对象形式
   id: {
       type: Number,
       required: true
   }
}

如果val不是object,那么也在res对象上面添加一个为name变量的属性,并且将该属性的值设置为{ type: val }。这个其实就是处理下面这种props的写法:

props: {
    // 基础的类型检查
    name: String,
    // 多个可能的类型
    value: [String, Number],
}

经过处理后props会变成了下面这样:

props: {
    name: {
        type: String
    },
    value: {
        type: [String, Number]
    }
}

所以不管我们使用vue提供的props哪种写法,最终vue都会帮我们转换成下面这种类型:

props: {
    name: {
        ...,
        type: '类型'
    }
}

明白了这些之后,看开头我们提出的initProps函数,就容易理解的多了

initProps

如果觉得有点难理解,可以先跟着代码后面的解释,逐行明白每一行代码的作用。


function initProps (vm: Component, propsOptions: Object) {
  
  const propsData = vm.$options.propsData || {}

  const props = vm._props = {}

  const keys = vm.$options._propKeys = []

  const isRoot = !vm.$parent

  if (!isRoot) {
    toggleObserving(false)
  }
  
  for (const key in propsOptions) {
    // 
    keys.push(key)
  
    const value = validateProp(key, propsOptions, propsData, vm)

    // 非生产环境下进行检查和提示
    if (process.env.NODE_ENV !== 'production') {
      // 
      const hyphenatedKey = hyphenate(key)
      // 
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (vm.$parent && !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 {
      defineReactive(props, key, value)
    }

    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  // 开启观察状态标识
  toggleObserving(true)
}
  1. 接收vm,propsOptions两个参数,propsOptions是我们开发者写的组件里的props。
  2. 赋值propsData,propsData父组件传入的props
  3. 定义实例的_props私有属性,并赋值给props
  4. key用来 缓存prop键,以便将来props更新可以使用Array而不是动态对象键枚举进行迭代。
  5. isRoot表示是否是根实例,对于非根实例,关闭观察标识即toggleObserving(false)
  6. 遍历props配置对象,向缓存键值数组key中添加键名。
  7. 验证prop的值:validateProp执行对初始化定义的props的类型检查和默认赋值,如果有定义类型检查,布尔值没有默认值时会被赋予false,字符串默认undefined。意思就是如果你在子组件写的props的类型是布尔值的话例如:
props:{
  flag:{
    type:Boolean
  }
}

那么即使父组件没有传入这个flag,那flag这个变量能用,且值是false,如果有默认值,例如

props:{
  flag:{
    type:Boolean,
    default:true
  }
}

那么即使父组件没有传入这个flag,那flag这个变量能用,且值是默认值。

如果父组件传入,也就是说,下面这些情况,子组件中flag的值都是true

<child flag></child>

<child flag="flag"></child>

如果prop是其他类型,父组件没传入的话,那就看有没有设置默认值,有的话就用默认值。

validateProp完成的就是这个功能,可以说是对props的值进行加工生成value

  1. hyphenate 进行键名的转换,将驼峰式转换成连字符式的键名。例如user-name就会转化成userName

  2. isReservedAttribute用于判断prop是不是保留变量名,是的话报错提示

  3. prop和data一样 都要通过defineReactive建立数据监听,成为响应式。

  4. proxy则是代理了。

ok,props的初始化就讲到这里啦。

本文使用 mdnice 排版