重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
Vue.js实现响应式原理的核心是利用ES5的 Object.defineProperty
,而 IE8 以下是没有这个东西的,所以这也就是为什么Vue.js不能兼容IE8及以下的原因。
Object.defineProperty
Object.defineProperty
会在一个对象上定义一个属性,或者修改一个现有属性,并返回这个对象,它的用法如下:
Object.defineProperty(obj, prop, descriptor)
Obj
参数是要定义属性的对象,prop
是定义或修改的属性名称,descriptor
是将被定义或修改的描述符。
使用这种方式来操作对象的时候,最关键的就是 get
和 set
, get
是给一个属性提供的 getter
方法,在访问对象的属性的时候使用,set
是给一个属性提供的 setter
方法,在修改对象的属性的时候使用(这块可以看重学JavaScript【对象的结构、创建和继承关系】)。
一旦对象有了 getter
和 setter
,就可以简单的把该对象理解为 响应式对象,在Vue.js里被定义成响应式对象的对象,有 initState
,initProps
和 initData
。
initState
在Vue初始化的时候有一个 _init
方法,里面有一个 initState
:
Vue.prototype._init = function (options?: Object) {
// ...
iniitState(vm)
// ...
}
这个方法的作用是初始化了 props
,data
,methods
,computed
,watcher
等,它的定义在 src/core/instance/state.js
里:
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)
}
}
这里重点关注一下 props
和 data
。
initProps
initProps
的定义也在 src/core/instance/state.js
里:
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
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)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
props
的初始化过程,主要就是遍历定义的 props
配置,在遍历期间调用了一个 defineReactive
函数,这个函数就是把传入的 props
对象上的 key
变成一个响应式的,然后通过 vm._props.xxx
就可以访问到定义 props
中对应的属性,该方法在下面有分析。在下面还使用了一个 proxy
,这个 proxy
之前也分析过,这里就可以通过 proxy
把 vm._props.xxx
的访问代理到 vm.xxx
上,下面还会再分析一下它。
initData
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 */)
}
data
的初始化也是做两件事,首先对定义 data
函数返回的对象进行一次遍历,通过 proxy
把每一个值 vm._data.xxx
都代理到 vm.xxx
上;另一个是调用 observe
方法观测整个 data
的变化,把 data
也变成响应式,可以通过 vm._data.xxx
访问到定义 data
返回函数中对应的属性。
不管是 props
还是 data
,它们的初始化都是把它们变成一个响应式对象,在这个过程中会走几个函数,下面来具体分析一下。
proxy
在new Vue发生了什么事情文章里有分析过它的作用,这里再提一下: proxy
定义了 get
和 set
,通过 Object.defineProperty
在参数 target(就是vm) 上定义了 _data
属性,从而把我们常写的 this.xx
代理到 this._data.xx
上(也就是代理到实例上,可以理解为 vm._data.xx
),这样就可以在 data
或者 methods
里拿到并且使用 xx
。
上面是对 data
的,对于 props
而言也一样,对 vm._props.xxx
的读写就变成了 vm.xxx
的读写,而对于 vm._props.xxx
我们可以访问到定义在 props
中的属性,所以我们就可以通过 vm.xxx
访问到定义在 props
中的 xxx
属性了。
observe
observe
的功能就是用来监测数据变化的,它的定义在 src/core/observer/index.js
中:
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
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
}
如果 value
不是一个对象,并且是一个VNode,就直接返回。接着判断 valu
e有没有 __ob__
属性,并且它是一个 Observer
的实例的话,就返回这个 __ob__
。
下一个判断有一个 shouldObserve
布尔值,它有一个改变值的方法:
export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
shouldObserve = value
}
这个方法在上面的 initProps
上调用了一次:
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
注释上说:根的props应该需要观测,所以它的逻辑里,如果不是root就设置为false,那也就走不到 ob = new Observer(value)
这个逻辑了,这样就决定了:非根props是不会执行 new Observer
的,也就不会变成 Observer
的实例,所以这个 shouldObserve
就是控制要不要变成 Observer
实例的。
整体来看的话, observe
的作用就是给非VNode的对象数据添加一个 Observer
,如果已经添加过就直接返回,否则满足一些条件的话,就实例化一个 Observer
对象实例。
Observer
Observer
的定义是这样的:
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
它可以理解为定义了一个观察者的类,在每次 new 它的时候,都会有一个 value
值,会实例化一个 dep
,会有一个计数的 vmCount
等等,然后会调用 def
函数,这个 def
的定义是这样的:
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
也就是封装了一下 Object.defineProperty
。
这里调用 def
的目的是,给 value
添加一个 __ob__
属性,并且这个属性指向了当前实例,目的是第一次定义了它之后,在接下来后面调用 observe
的话,进行到 hasOwn(value, '__ob__')
判断的时候,可以直接返回当前实例。
接着判断 value
是数组的话就执行 observeArray
方法(递归数组元素,观察每一个元素),否则就是对象,就执行 walk
方法(遍历每一个键,从而观察它的值)。
这里再分析一下在 Observer
的 constructor
里,为什么它要调用 def
把 __ob__
指向 this,而不是直接 value.__ob__ = this
?
因为如果 value
是一个对象,就会走 walk
,如果用直接赋值的方式(就是 value.__ob__ = this
)的话,那 walk
就会遍历这个 __ob__
,然后执行 defineReactive
,而我们不希望它走这一步(因为没必要,我们也不会手动去修改这个 __ob__
),所以使用了 def
方法,然后传的最后一个参数 enumerable
没传,也就是false,也就是不可枚举,这样就不会遍历 __ob__
属性了。
defineReactive
最后再来分析一下 defineReactive
是如何把参数 obj
变成响应式的,它的定义在 src/core/observer/index.js
中:
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
通过 Object.getOwnPropertyDescriptor
拿到属性的定义,如果该属性的 configurable
是false,就什么都不做。然后尝试拿到该属性的原生 get
和 set
,如果没有 get
,有 set
,并且传入了2个参数(其实就是通过walk调用的话),就直接拿默认值。接着如果对象的值是一个对象的话,就递归调用 observe
,然后把该对象重写 get
和 set
,get
主要做的就是依赖收集, set
主要做的就是派发更新。这两个概念在后两篇会详细说一下。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!