Vue2.0源码分析
如果觉得写得不错,请到GitHub
我一个Star
上一篇:Vue2.0源码分析:Rollup构建,目录设计和整体流程
下一篇:Vue2.0源码分析:响应式原理(下)
介绍
在上一章节,我们分析过initState()
方法的整体流程,知道它会处理props
、methods
和data
等等相关的内容:
export function initState (vm: Component) {
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(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
那么我们的深入响应式原理介绍会以initState()
方法开始,逐步分析Vue
中响应式的原理,下面这张图可以很好的展示响应式的原理。
前置核心概念
Object.defineProperty介绍
也许你已经从很多地方了解到,Vue.js
利用了Object.defineProperty(obj, key, descriptor)
方法来实现响应式,其中Object.defineProperty()
方法的参数介绍如下:
obj
:要定义其属性的对象。key
:要定义或修改属性的名称。descriptor
:要定义或修改属性的描述符。
其中descriptor
有很多可选的键值, 然而对Vue
响应式来说最重要的是get
和set
方法,它们分别会在获取属性值触发getter
和设置属性值的时候触发setter
。在介绍原理之前,我们来使用Object.defineProperty()
来实现一个简单的响应式例子:
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log('get msg')
return val
},
set: function reactiveSetter (newVal) {
console.log('set msg')
val = newVal
}
})
}
const vm = {
msg: 'hello, Vue.js'
}
let msg = ''
defineReactive(vm, 'msg', vm.msg)
msg = vm.msg // get msg
vm.msg = 'Hello, Msg' // set msg
msg = vm.msg // get msg
为了在别的地方方便的使用Object.defineProperty()
方法,因此我们把其封装成一个defineReactive
函数。
proxy代理
在我们的开发过程中,我们经常会直接使用this.xxx
的形式直接访问props
或者data
中的值,这是因为Vue
为props
和data
默认做了proxy
代理。关于什么是proxy
代理,请先看一个简单的例子:
this._data = {
name: 'AAA',
age: 23
}
// 代理前
console.log(this._data.name) // AAA
proxy(vm, '_data', key)
// 代理后
console.log(this.name) // AAA
接下来我们详细介绍proxy()
方法是如何实现的,在instance/state.js
文件中定义了proxy
方法,它的代码也很简单:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
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
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
我们可以从上面的代码中发现,proxy
方法主要是做了属性的get
和set
方法劫持。
const name = this.name
this.name = 'BBB'
// 等价于
const name = this._data.name
this._data.name = 'BBB'
$options属性
在之前的介绍中,我们知道当我们初始化Vue
实例的时候传递的options
会根据不同的情况进行配置合并,关于具体的options
合并策略我们会在之后的章节详细介绍,现阶段我们只需要知道$options
可以拿到合并后的所有属性,例如props
、methods
以及data
等等。
假设我们定义了如下实例:
const vm = new Vue({
el: '#app',
props: {
msg: ''
},
data () {
return {
firstName: 'AAA',
lastName: 'BBB',
age: 23
}
},
methods: {
sayHello () {
console.log('Hello, Vue.js')
}
},
computed: {
fullName () {
return this.firstName + this.lastName
}
}
})
那么我们在之后可以通过下面的方式来取这些属性。
const opts = this.$options
const props = opts.props
const methods = opts.methods
const data = opts.data
const computed = opts.computed
const watch = opts.watch
// ...等等
props处理
介绍完以上前置核心概念后,我们第一个要学习的就是Vue.js
是如何处理与props
相关的逻辑的。我们把与props
相关的逻辑主要分成三个部分,分别是props
规范化、props
初始化和props
更新。
props规范化
在了解规范化之前,我们先来列举一下在日常的开发过程中,我们主要有如下几种撰写组件props
的方式:
- 数组形式:
props
可以写成一个数组,但数组中的key
元素必须为string
类型。
export default {
props: ['name', 'age']
}
- 键值不为对象:此种方式常见于只需要定义
key
类型的props
。
export default {
props: {
name: String
}
}
- 规范格式:此种方式是
Vue.js
接受props
最好的格式,对于一个有很高要求的组件来说,它通过会撰写很严格的props
规则,这在各个开源UI框架中是最常见的。
export default {
props: {
name: {
type: String,
default: ''
},
age: {
type: Number,
default: 0,
validator (value) {
return value >= 0 && value <= 100
}
}
}
}
而props
规范化所做的事情,就是把各种不是规范格式的形式,规范化为规范格式,方便Vue.js
在后续的过程中处理props
。那么接下来,我们就来分析Vue.js
是如何对props
规范化的。
props
规范化的过程发生在this._init()
方法中的mergeOptions
合并配置中:
import { mergeOptions } from '../util/index'
export function _init (Vue) {
const vm = this
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
其中mergeOptions()
方法是定义在src/core/util/options.js
文件中,它在其中有一段这样的方法调用:
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 省略代码
normalizeProps(child, vm)
return options
}
我们可以发现,规范化props
的代码,主要集中在normalizeProps()
方法中,那么接下来我们详细分析normalizeProps()
方法:
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
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.')
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
为了更好的理解normalizeProps()
方法,我们来撰写几个案例来详细说明:
- 数组形式:当
props
是数组时,会首先倒序遍历这个数组,然后使用typeof
来判断数组元素的类型。如果不是string
类型,则在开发环境下报错,如果是string
类型,则先把key
转化为驼峰形式,然后把这个key
赋值到临时的res
对象中,此时的键值固定为{ type: null }
// 规范化前
export default {
props: ['age', 'nick-name']
}
// 规范化后
export default {
props: {
age: {
type: null
},
nickName: {
type: null
}
}
}
- 对象形式:当为对象时会使用
for-in
遍历对象,紧接着和数组形式一样使用camelize
来把key
转成驼峰形式,然后使用isPlainObject()
方法来判断是否为普通对象。如果不是,则转成{ type: Type }
对象形式,其中Type
为定义key
时的Type
,如果是,则直接使用这个对象。
// 规范化前
export default {
props: {
name: String,
age: Number
}
}
// 规范化后
export default {
props: {
name: {
type: String
},
age: {
type: Number
}
}
}
- 既不是数组形式也不是对象形式:报错
// 报错:Invalid value for option "props": expected an Array or an Object,but got String
export default {
props: 'name, age'
}
props初始化
在了解了props
规范化后,我们紧接着来了解一下props
初始化的过程。props
初始化过程同样是发生在this._init()
方法中,它在initState
的时候被处理:
export function initState (vm) {
// 省略代码
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
}
然后我们来详细看一下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 (!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 {
defineReactive(props, key, value)
}
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
在仔细阅读initProps()
方法后,我们可以对initProps()
方法进行总结,它主要做三件事情:props校验和求值、props响应式和props代理。
props响应式
我们先来看看最简单的props
响应式,这部分的过程主要使用了我们在之前介绍过的defineReactive
方法:
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
)
}
})
唯一值得注意的地方就是:在开发环境下,props
的响应式劫持了setter
方法,这样做的是为了保证props
为单项数据流:既我们不能在子组件中直接修改父组件传递的props
值。
props代理
经过props
响应式后,我们会在实例上得到this._props
对象,为了方便我们更好的获取props
的值,我们需要对props
做一层proxy
代理。关于proxy
的实现,我们已经在之前的章节中介绍过了。
this._props = {
name: '',
age: 0
}
// 代理前
console.log(this._props.name)
proxy(vm, `_props`, key)
// 代理后
console.log(this.name)
props校验求值
最后我们来看稍微复杂一点的props
校验求值,这部分的功能发生在validateProp
,它的代码如下:
export function validateProp (
key: string,
propOptions: Object,
propsData: Object,
vm?: Component
): any {
const prop = propOptions[key]
const absent = !hasOwn(propsData, key)
let value = propsData[key]
// boolean casting
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) {
// only cast empty string / same name to boolean if
// boolean has higher priority
const stringIndex = getTypeIndex(String, prop.type)
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
if (
process.env.NODE_ENV !== 'production' &&
// skip validation for weex recycle-list child component props
!(__WEEX__ && isObject(value) && ('@binding' in value))
) {
assertProp(prop, key, value, vm, absent)
}
return value
}
代码分析:我们可以从以上代码中发现,validateProp
虽然说的是带有校验的功能,但它并不会抛出错误进而阻止validateProp()
方法返回value
,而是根据校验的过程中的不同情况尽可能的提示出很清晰的提示。实质上validateProp()
方法最主要的还是返回value
,同时也根据不同的props
写法处理不同的情况。我们可以将validateProp()
方法进行总结,它主要做如下几件事情:
- 处理
Boolean
类型的props
。 - 处理
default
默认数据。 props
断言。
那么我们接下来将分别对这几件事情进行详细的描述。
处理Boolean类型
我们先来看几个props
传递Boolean
的例子:
// Component A
export default {
props: {
fixed: Boolean
}
}
// Component B
export default {
props: {
fixed: [Boolean, String]
}
}
// Component C
export default {
props: {
fixed: []
}
}
然后回到源码中处理Boolean
类型getTypeIndex
的地方,这个函数的代码如下:
function getTypeIndex (type, expectedTypes): number {
if (!Array.isArray(expectedTypes)) {
return isSameType(expectedTypes, type) ? 0 : -1
}
for (let i = 0, len = expectedTypes.length; i < len; i++) {
if (isSameType(expectedTypes[i], type)) {
return i
}
}
return -1
}
这个函数的实现逻辑比较清晰:
- 以
Component A
组件为例,它的props
不是一个数组但却是Boolean
类型,因此返回索引0
。 - 以
Component B
组件为例,因为它的props
都是一个数组,所以要遍历这个数组,然后返回Boolean
类型在数组中的索引i
。 - 以
Component C
组件为例,虽然它是一个数组,但数组中没有任何元素,因此返回索引-1
。
在拿到booleanIndex
后,我们需要走下面这段代码逻辑:
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) {
// only cast empty string / same name to boolean if
// boolean has higher priority
const stringIndex = getTypeIndex(String, prop.type)
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
代码分析:
- 在
if
条件判断中absent
代表虽然我们在子组件中定义了props
,但是父组件并没有传递任何值,然后&
条件又判断了子组件props
有没有提供default
默认值选项,如果没有,那么它的值只能为false
。
// 父组件未传递fixed
export default {
name: 'ParentComponent'
template: `<child-component />`
}
// 子组件fixed值取false
export default {
name: 'ChildComponent',
props: {
fixed: Boolean
}
}
- 在
else if
条件判断中,我们判断了两种特殊的props
传递方式:
// Parent Component A
export default {
name: 'ParentComponentA',
template: `<child-component fixed />`
}
// Parent Component B
export default {
name: 'ParentComponentB',
template: `<child-component fixed="fixed" />`
}
对于第一个种情况stringIndex
为-1
,booleanIndex
为0
,因此value
的值为true
。对于第二种情况,则需要根据props
的定义具体区分:
// Child Component A
export default {
name: 'ChildComponentA'
props: {
fixed: [Boolean, String]
}
}
// Child Component B
export default {
name: 'ChildComponentB',
props: [String, Boolean]
}
- 对于
ChildComponentA
来说,由于stringIndex
值为1
,booleanIndex
值为0
,booleanIndex < stringIndex
因此我们可以认为Boolean
具有更高的优先级,此时value
的值为true
。 - 对于
ChildComponentB
来说,由于stringIndex
值为0
,booleanIndex
值为1
,stringIndex < booleanIndex
因此我们可以认为String
具有更高的优先级,此时value
的值不处理。
处理default默认数据
处理完Boolean
类型后,我们来处理默认值,既我们提到过的虽然子组件定义了props
,但父组件没有传递的情况。
// 父组件未传递fixed
export default {
name: 'ParentComponent'
template: `<child-component />`
}
// 子组件提供了default选项
export default {
name: 'ChildComponent',
props: {
fixed: {
type: Boolean,
default: false
}
}
}
对于以上案例会走如下代码的逻辑:
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
}
function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
// no default, return undefined
if (!hasOwn(prop, 'default')) {
return undefined
}
const def = prop.default
// warn against non-factory defaults for Object & Array
if (process.env.NODE_ENV !== 'production' && isObject(def)) {
warn(
'Invalid default value for prop "' + key + '": ' +
'Props with type Object/Array must use a factory function ' +
'to return the default value.',
vm
)
}
// the raw prop value was also undefined from previous render,
// return previous default value to avoid unnecessary watcher trigger
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
// call factory function for non-Function types
// a value is Function if its prototype is function even across different execution context
return typeof def === 'function' && getType(prop.type) !== 'Function'
? def.call(vm)
: def
}
代码分析:
- 首先判断了子组件有没有提供
default
默认值选项,没有则直接返回undefined
。 - 随后判断了
default
如果是引用类型,则提示必须把default
写成一个函数,既:
default: {}
default: []
// 必须写成
default () {
return {}
}
default () {
return []
}
- 最后再根据
default
的类型来取值,如果是函数类型则调用这个函数,如果不是函数类型则直接使用。 - 其中下面一段代码在这里我们并不会说明和分析它的具体作用,而是会在
props
更新章节来介绍。
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
props断言
最后我们来分析一下props
断言。
function assertProp (
prop: PropOptions,
name: string,
value: any,
vm: ?Component,
absent: boolean
) {
if (prop.required && absent) {
warn(
'Missing required prop: "' + name + '"',
vm
)
return
}
if (value == null && !prop.required) {
return
}
let type = prop.type
let valid = !type || type === true
const expectedTypes = []
if (type) {
if (!Array.isArray(type)) {
type = [type]
}
for (let i = 0; i < type.length && !valid; i++) {
const assertedType = assertType(value, type[i])
expectedTypes.push(assertedType.expectedType || '')
valid = assertedType.valid
}
}
if (!valid) {
warn(
getInvalidTypeMessage(name, value, expectedTypes),
vm
)
return
}
const validator = prop.validator
if (validator) {
if (!validator(value)) {
warn(
'Invalid prop: custom validator check failed for prop "' + name + '".',
vm
)
}
}
}
在assertProp
中我们有三种情况需要去断言:
required
:如果子组件props
提供了required
选项,代表这个props
必须在父组件中传递值,如果不传递则抛出错误信息Missing required prop: fixed
。- 对于定义了多个
type
的类型数组,则我们会遍历这个类型数组,只要当前props
的类型和类型数组中某一个元素匹配则终止遍历。,否则抛出错误提示信息。
// Parent Component
export default {
name: 'ParentComponent',
template: `<child-component :age="true" />`
}
// Chil Component
export default {
name: 'ChilComponent',
props: {
age: [Number, String]
}
}
// 报错:Invalid prop: type check failed for prop age,Expected Number, String,got with value true
- 用户自己提供的
validator
校验器我们也需要进行断言:
// Parent Component
export default {
name: 'ParentComponent',
template: `<child-component :age="101" />`
}
// Chil Component
export default {
name: 'ChilComponent',
props: {
age: {
type: Number,
validator (value) {
return value >=0 && value <=100
}
}
}
}
// 报错:Invalid prop: custom validator check failed for prop age
props更新
我们都知道子组件的props
值来源于父组件,当父组件值更新时,子组件的值也会发生改变,同时触发子组件的重新渲染。我们先跳过父组件的具体编译逻辑,直接看父组件的值更新,改变子组件props
值的步骤:
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
// 省略代码
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
}
代码分析:
- 以上
vm
实例为子组件,propsData
为父组件中传递的props
的值,而_propKeys
是之前props
初始化过程中缓存起来的所有的props
的key。 - 在父组件值更新后,会通过遍历
propsKey
来重新对子组件props
进行校验求值,最后赋值。
以上代码就是子组件props
更新的过程,在props
更新后会进行子组件的重新渲染,这个重新渲染的过程分两种情况:
- 普通
props
值被修改:当props
值被修改后,其中有段代码props[key] = validateProp(key, propOptions, propsData, vm)
根据响应式原理,会触发属性的setter
,进而子组件可以重新渲染。 - 对象
props
内部属性变化:当这种情况发生时,并没有触发子组件prop
的更新,但是在子组件渲染的时候读取到了props
,因此会收集到这个props
的render watcher
,当对象props
内部属性变化的时候,根据响应式原理依然会触发setter
,进而子组件可以重新进行渲染。
toggleObserving作用
toggleObserving
是定义在src/core/observer/index.js
文件中的一个函数,其代码很简单:
export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
shouldObserve = value
}
它的作用就是修改当前模块的shouldObserve
变量,用来控制在observe
的过程中是否需要把当前值变成一个observer
对象。
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
接下来我们来分析,在处理props
的过程中,什么时候toggleObserving(true)
,什么时候toggleObserving(false)
以及为什么需要这样处理?
function initProps (vm: Component, propsOptions: Object) {
if (!isRoot) {
toggleObserving(false)
}
// 省略defineReactive的过程
toggleObserving(true)
}
props
初始化的时候:
我们可以看到在最开始判断了当为非根实例(子组件)的时候,进行了toggleObserving(false)
的操作,这样做的目的是因为:当非根实例的时候,组件的props
来自于父组件。当props
为对象或者数组时,根据响应式原理,我们会递归遍历子属性然后进行observe(val)
,而正是因为props
来源于父组件,这个过程其实已经在父组件执行过了,如果不做任何限制,那么会在子组件中又重复一次这样的过程,因此这里需要toggleObserving(false)
,用来避免递归props
子属性的情况,这属于响应式优化的一种手段。在代码最后,又调用了toggleObserving(true)
,把shouldObserve
的值还原。
props
校验的时候:
我们先来看props
提供了default
默认值,且默认值返回了对象或者数组。
export default {
props: {
point: {
type: Object,
default () {
return {
x: 0,
y: 0
}
}
},
list: {
type: Array,
default () {
return []
}
}
}
}
对于以上point
和list
取默认值的情况,这个时候的props
值与父组件没有关系,那么这个时候我们需要toggleObserving(true)
,在observe
后再把shouldObserve
变量设置为原来的值。
export function validateProp () {
// 省略代码
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
}
在props
更新的时候:
当父组件更新的时候,会调用updateChildComponent()
方法,用来更新子组件的props
值,这个时候其实和props
初始化的逻辑一样,我们同样不需要对指向父组件的对象或数组props
进行递归子属性observe
的过程,因此这里需要执行toggleObserving(false)
。
export function updateChildComponent () {
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
vm.$options.propsData = propsData
}
}
整体流程图
在分析完以上所有与props
相关的逻辑后,我们可以总结如下流程图。
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
的整体流程图。
computed处理
处理computed
相关的逻辑,发生在initState
中,接下来我们详细分析与computed
相关的逻辑。
export function initState (vm: Component) {
// 省略代码
const opts = vm.$options
if (opts.computed) initComputed(vm, opts.computed)
}
我们知道computed
计算属性是依赖于其它响应式变量的,因此我们分析computed
的时候会分为两个步骤:computed初始化和computed更新。
computed初始化
在initState()
方法中如果我们传递了computed
,那么会调用initComputed()
方法。initComputed()
方法定义在src/core/instance/state.js
文件中,其代码如下:
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
代码分析:
- 在
initComputed()
方法中,首先定义了一个_computedWatchers
的变量,这个变量的作用是缓存当前实例所有计算属性的watcher
。 - 接下来遍历所有的
computed
,然后对每一个computed
进行类型判断,如果是function
类型,那么直接使用,如果是对象则代表是get/set
形式,则直接取get
。如果最后获取到的computed
的getter
为null
,则在开发环境下提示错误。
// 两种类型的计算属性
export default {
props: ['index'],
data () {
return {
firstName: 'first',
lastName: 'last'
}
},
computed: {
fullName () {
return this.firstName + this.lastName
},
active: {
get: function () {
return this.index
},
set: function (newVal) {
this.$emit('update:index', newVal)
}
}
}
}
以上面代码为例,两种类型的computed
获取到的getter
分别如下:
// function类型
const getter = function () {
this.firstName + this.lastName
}
// get/set类型
const getter = function () {
return this.index
}
- 然后在非
SSR
服务端渲染的情况下,会在_computedWatchers
上新建一个Watcher
的实例。以上面代码为例,_computedWatchers
在遍历完毕后,可以用如下代码表示:
// 当前vm实例
{
_computedWatchers: {
fullName: new Watcher(),
active: new Watcher()
}
}
- 最后、首先判断了当前遍历的
computed
是否已经在vm
实例上,如果不在则调用defineComputed()
方法,如果在还需要判断当前遍历的computed
是否和props
、data
命名冲突,如果冲突则提示错误。 注意:对应子组件而言,这个时候当前遍历的computed
已经在vm
实例上了,所以并不会调用defineComputed()
方法,我们从上面代码注释也能看的出来。对于子组件而言,真正initComputed
的过程是发生在Vue.extend
方法中:
Vue.extend = function (extendOptions) {
// 省略代码
const Super = this
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 初始化子组件的computed
if (Sub.options.computed) {
initComputed(Sub)
}
}
// extend.js中的initComputed定义
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}
然后initComputed
调用的defineComputed()
方法,就和我们现在的defineComputed()
方法是同一个方法,它和此时的initComputed()
方法定义在同一个位置(src/core/instance/state.js
):
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
defineComputed()
方法的逻辑很简单,根据不同的类型的computed
,对sharedPropertyDefinition
的get
和set
进行赋值。sharedPropertyDefinition
在之前的proxy
中,我们已经介绍过, 它就是Object.defineProperty()
方法descriptor
参数的一个共享配置。
在非SSR
服务端渲染的情况,sharedPropertyDefinition.get
的值是调用了createComputedGetter()
方法,而在SSR
服务端渲染的情况下是调用了createGetterInvoker()
方法。我们在分析Vue
源码的过程中,因为侧重于Web
浏览器端的表现,因此我们接下来会分析createComputedGetter()
方法的实现。createComputedGetter()
方法和defineComputed()
方法定义在同一个位置,代码如下:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
我们可以看到createComputedGetter()
方法返回了一个函数,这个函数会在获取computed
的时候被调用,例如组件渲染的时候:
<template>
<div>{{fullName}}</div>
</template>
根据以上代码我们再来看computedGetter()
方法:组件渲染的时候会获取fullName
计算属性,然后调用computedGetter()
方法,在这个方法执行的时候,首先判断watcher.dirty
属性,这个属性在new Watcher()
的时候与我们传入的const computedWatcherOptions = { lazy: true }
有关。在Watcher
类的构造函数中,有这样一段代码:
class Watcher {
// 省略代码
constructor (vm, expOrFn, cb, options, isRenderWatcher) {
if (options) {
this.lazy = !!options.lazy
} else {
this.lazy = false
}
this.dirty = this.lazy
}
}
因为我们传入的lazy
值为true
,因此watcher.dirty
条件判断为真,进行watcher.evaluate()
计算。随后判断了Dep.target
为真,则进行依赖收集watcher.depend()
,关于依赖收集我们会在之后的章节详细介绍。我们只要知道,当在组件渲染的时候触发的computed
依赖收集,收集的是render watcher
。最后,我们看一下watcher.evaluate()
方法的实现:
class Watcher {
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
}
evaluate()
方法它的实现非常简单,就是触发computed
的getter
进行求值,然后把dirty
设置为false
。
computed更新
在介绍完了computed
的初始化后,我们在来看computed
的更新过程,以下面例子为例:
export default {
template: `
<div>{{fullName}}</div>
<button @click="change">change</button>
`
data () {
return {
total: 0,
firstName: 'first',
lastName: 'last'
}
},
computed: {
fullName () {
if (this.total > 0) {
return this.firstName + this.lastName
} else {
return 'pleace click'
}
}
},
methods: {
change () {
this.total++
}
}
}
因为total
、firstName
和lastName
全部为响应式变量,所以fullName
这个计算属性初始化的时候,此时total
值为0
,fullName
计算属性有两个Watcher
,其中一个是计算属性watcher
,另外一个是渲染watcher
。当点击按钮触发事件后,会触发total
属性的setter
方法,进而调用一个叫做notify
的方法。
set: function reactiveSetter (newVal) {
// 省略
dep.notify()
}
其中notify()
是定义在Dep
类中的一个方法:
export default class Dep {
constructor () {
this.subs = []
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
代码分析:
subs
就是我们收集起来的watcher
,它是一个数组,对应上面案例的话它是一个长度为2的数组并且其中一个为render watcher
。- 在
notify()
方法调用时,会遍历subs
数组,然后依次调用当前watcher
的update
方法。其中update
方法是定义在Watcher
类中的一个实例方法,代码如下:
class Watcher {
// 省略其它
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
当第一次遍历时,此时的watcher
为计算属性watcher
,我们已经在前面介绍过计算属性watcher
它的this.lazy
值为true
,因此会进行this.dirty = true
。
当第二次遍历时,此时的watcher
为渲染watcher
,对于渲染watcher
而言,它的lazy
值为false
,this.sync
为false
,因此会调用queueWatcher()
方法。我们目前不需要知道queueWatcher
是怎么实现的,只需要知道queueWatcher()
方法在调用时,会触发updateComponent()
方法:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
我们可以看到updateComponent()
方法调用了vm._update
方法,而这个方法的作用就是重新进行组件渲染,在组件渲染的过程中,会再次读取fullName
的值,也就是说会调用下面这段代码:
fullName () {
if (this.total > 0) {
return this.firstName + this.lastName
} else {
return 'pleace click'
}
}
因为此时的total
值为1
,所以会返回this.firstName + this.lastName
的值,而firstName
和lastName
又是定义在data
中的响应式变量,会依次触发firstName
和lastName
的getter
,然后进行依赖收集。在组件渲染完毕后,fullName
的依赖数组subs
此时会有四个watcher
,分别是三个计算属性watcher
和一个渲染watcher
。无论这三个计算属性watcher
哪一个值更新了,都会再出重复以上的流程,这就是computed
更新的过程。
在分析完computed
的相关流程后,我们可以得到如下流程图
watch处理
在介绍完处理computed
相关的逻辑后,我们接下来看watch
是如何处理的。
watch初始化
export function initState (vm: Component) {
// 省略代码
const opts = vm.$options
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
我们可以看到,处理watch
的逻辑发生在initWatch()
方法中,在这个方法调用之前,首先对watch
做了判断,其中nativeWatch
是定义在src/core/util/env.js
中的一个常量:
// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch
然后,让我们来看一下initWatch
的实现,它定义在src/core/instance/state.js
文件中:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
我们可以看到,initWatch()
方法的实现非常简单,首先对watch
做了判断,如果是数组则遍历这个数组调用createWatcher()
方法,如果不是则直接调用createWatcher()
。按照watch
的使用规则,我们有如下几种形式的写法:
export default {
data () {
return {
age: 23,
name: 'AAA',
nested: {
a: {
b: 'b'
}
}
}
},
watch: {
name (newVal, oldVal) {
console.log(newVal, oldVal)
},
nested: {
handler (newVal, oldVal) {
console.log(newVal, oldVal),
},
deep: true
}
}
}
接着,我们需要来看一下createWatcher()
函数的具体实现:
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
代码分析:
createWatcher()
方法的主要作用就是进行watch
参数规范化,然后将规范化后的参数传递给vm.$watch()
。- 在
createWatcher()
中首先判断了handler
参数是否为普通对象,如果是普通对象则代表是如下形式定义的watch
:
{
watch: {
nested: {
handler (newVal, oldVal) {
console.log(newVal, oldVal),
},
deep: true
}
}
}
此时,应该把handler
赋值给可选的options
参数,然后handler
赋值为真正的回调函数。
- 接着,对
handler
进行了类型判断,如果是string
类型则把此时vm[handler]
赋值给它。根据这段代码的逻辑,意味着我们可以选择把watch
回调函数定义在methods中
:
export default {
data () {
return {
name: 'AAA'
}
},
watch: {
name: 'nameWatchCallback'
},
methods: {
nameWatchCallback (newVal, oldVal) {
console.log(newVal, oldVal)
}
}
}
- 最后,把规范化后的参数传递给
vm.$watch()
。关于$watch()
何时挂载到Vue.prototype
上,我们已经在之前介绍过了,它发生在stateMixin
中。
在分析完createWatcher()
方法实现逻辑后,我们接着来看$watch()
方法的具体实现逻辑:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
我们可以发现,$watch
方法主要做两件事情:创建Watcher实例和返回unwatchFn函数,接下来我们分别对这两部分的逻辑进行详细的解释。
创建Watcher实例
我们先来看一下Watcher
构造函数的代码:
// 精简代码
class Watcher {
constructor (vm, expOrFn, cb, options, isRenderWatcher) {
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
}
}
我们从构造函数中可以看到,当实例化一个watch
的时候,会根据传递的options
来处理deep
、user
、lazy
、sync
以及before
属性。watcher
根据不同的用法,有几种不同的分类:
render watcher
:渲染watcher
,例如当在template
模板中使用{{}}
语法读取一个变量的时候,此时这个变量收集的依赖就是render watcher
,当这个变量值更新的时候会触发render watcher
进行组件的重新渲染。是否为渲染warcher
,使用构造函数参数isRenderWatcher
为true
进行区分。computed watcher
:计算属性watcher
,当我们在定义计算属性的时候,计算属性收集的依赖就是另外一个或者多个变量,当其中一个变量的值发生变量,就会触发计算属性重新进行求值。是否为计算属性watcher
,使用options.lazy
为true
进行区分。user watcher
:用户自定义watcher
,多发生在this.$watch
或者组件watch
选择配置中,此时收集的依赖就是变量自身,当变量的值发生变化的时候,就会调用watch
提供的回调函数。是否为用户自定义watcher
,使用options.user
为true
进行区分。
返回unwatchFn函数
我们在构造函数中可以发现,它定义了一个_watchers
变量,然后在每次实例化的时候,把自身添加到这个数组中,这样做的目的是为了方便清除依赖。在之前的介绍中,我们知道$watch
返回了一个unwatchFn
函数,它用来取消监听。接下来,我们看一下teardown()
方法的具体实现。
// Watcher类精简代码
class Watcher {
constructor () {
this.active = true
this.deps = []
}
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
// Dep类精简代码
class Dep {
constructor () {
this.subs = []
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
}
teardown()
方法的实现很简单,就是从deps
数组中移除当前的watcher
,其中deps
存储的是Dep
实例。