系列文章:
methods处理
在分析完props相关逻辑后,接下来分析与methods相关的逻辑,这部分相比于props要简单得多。
export function initState (vm: Component) {
// 省略代码
const opts = vm.$options
if (opts.methods) initMethods(vm, opts.methods)
}
在initState()方法中,调用了initMethods()并传入了当前实例vm和撰写的methods。接下来,看一下initMethods方法具体的实现:
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
在以上代码中可以看到,initMethods()方法实现中最重要的一段代码就是:
// 空函数
function noop () {}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
它首先判断了定义的methods是不是function类型,如果不是则赋值为一个noop空函数,如果是则把这个方法进行bind绑定,其中传入的vm为当前实例。这样做的目的是为了把methods方法中的this指向当前实例,这样就能在methods方法中通过this.xxx的形式很方便的访问到props、data以及computed等与实例相关的属性或方法。
在开发环境下,它还做了如下几种判断:
- 必须为
function类型。
// 抛出错误:Method sayHello has type null in the component definition.
// Did you reference the function correctly?
export default {
methods: {
sayHello: null
}
}
- 命名不能和
props冲突。
// 抛出错误:Method name has already been defined as a prop.
export default {
props: ['name']
methods: {
name () {
console.log('name')
}
}
}
- 命名不能和已有的实例方法冲突。
// 抛出错误:Method $set conflicts with an existing Vue instance method.
// Avoid defining component methods that start with _ or $.
export default {
methods: {
$set () {
console.log('$set')
}
}
}
在分析完以上initMethods流程后,能得到如下流程图:
data处理
Vue中关于data的处理,根实例和子组件有一点点区别,接下来着重分析子组件中关于data的处理过程。
export function initState (vm: Component) {
const opts = vm.$options
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
}
在以上代码中,首先判断了opts.data,如果值为true则代表是子组件(子组件如果没有显示定义data,则使用默认值),否则代表是根实例。对于根实例而言不需要执行initData的过程,只要对vm._data进行observe即可。
接下来,详细分析initData的过程,它是定义在src/core/instance/state.js文件中的一个方法:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(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
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
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(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
虽然initData()方法的代码有点长,但详细观察后可以发现,其主要做的就是四件事情:类型判断取值、命名冲突判断、proxy代理以及observe(data)。
然后,分别对以上几块进行详细解释:
- 类型判断取值:对于子组件而言,由于组件可以多次复用,因此函数必须通过工厂函数模式返回一个对象,这样在组件多次复用时就能避免引用类型的问题。
// Child Component
// 抛出错误:data functions should return an object
export default {
data: {
msg: 'Hello, Data'
}
}
对于data是一个函数的情况,调用getData方法来取值,getData方法定义如下:
export function getData (data: Function, vm: Component): any {
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
代码分析:pushTarget是一个与响应式依赖收集有关的,会在后续进行详细说明。getData的取值过程包裹在try/catch中,通过data.call(vm, vm)进行调用返回,如果函数调用出错,则使用handleError进行错误统一处理。
- 命名冲突判断:由于
props和methods有更高的优先级,因此data属性的命名不能和props、methods中的命名冲突,因为无论是props、methods还是data最后都会反映在实例上。另外一种命名冲突,是不能以$或者_开头,因为这样很容易和实例私有方法、属性或对外暴露以$开头的方法、属性冲突。
// 1.与methods命名冲突
// 抛出错误:Method name has already been defined as a data property.
export default {
data () {
return {
name: 'data name'
}
},
methods: {
name () {
console.log('methods name')
}
}
}
// 2.与props命名冲突
// 抛出错误:The data property name is already declared as a prop.
// Use prop default value instead.
export default {
props: ['name'],
data () {
return {
name: 'data name'
}
}
}
// 3.不能以$和_开头
export default {
data () {
return {
$data: '$data'
_isVue: true
}
}
}
- proxy代理:在之前已经介绍过
proxy代理的作用,也讲过proxy代理_props的例子,这里代理_data跟代理_props是同样的道理。
export default {
data () {
return {
msg: 'Hello, Msg'
}
}
}
// 代理前
console.log(this._data.msg)
proxy(vm, '_data', 'msg')
// 代理后
console.log(this.msg)
- observe(data):
observe的作用是把传入值所有的属性(包括嵌套属性)递归的进行响应式defineReactive,会在之后的章节中详细介绍observe的实现原理,在initData中只要知道observe(data)会把data函数返回对象的所有属性全部变成响应式的即可。
在分析完initData的实现后,可以得到initData的整体流程图。