Vue2实现响应式的核心是利用了ES5的Object.defineProperty,这也是Vue不能兼容IE8及如下浏览器的缘由javascript
会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象java
能够在MDN看看关于它的使用介绍react
在Vue中主要使用到的是descriptor中的get和set,get 是一个给属性提供的 getter 方法,当访问了该属性的时候会触发getter方法。set 是一个给属性提供的 setter 方法,当对该属性作修改的时候会触发setter方法数组
建议去下载一份Vue2代码,这样对代码结构以及思路会比较清晰。想了解的函数方法也能够及时查到。接下来提到的实现函数代码篇幅不会太多,仅做为引导做用浏览器
在Vue2中,只要是在data属性里声明的字段,都会变成响应式属性,在代码中修改值,会对应的更新到dom上(也涉及到数据驱动知识)或对该值做出响应dom
let vm = new Vue({
data() {
return {
one: 'one'
}
},
watch: {
one: function() {
console.log('one changed')
}
},
computed: {
two: function() {
return this.one + 1
}
}
})
vm.one = '1'
// console.log: one changed
vm.two: '11'
// 此后系统会对这个修改做出一系列的响应如更新dom,更新有关数据等等
复制代码
在Vue的初始化_init函数执行时,其中会执行initState(vm)方法函数
// 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、methods、data、computed和 wathcer等属性作了初始化操做。这里咱们重点分析props 和 data:oop
// src/core/instance/state.js
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') {
...
} else {
defineReactive(props, key, value)
}
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
复制代码
props的初始化主要过程是遍历定义的props配置。遍历的过程主要作两件事情:ui
// 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 = {}
...
}
// 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') {
// 判断在methods中是否声明过了
if (methods && hasOwn(methods, key)) {
...
}
}
// 判断在props中是否声明过了
if (props && hasOwn(props, key)) {
...
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// 关键代码!!
observe(data, true /* asRootData */)
}
复制代码
data 的初始化主要过程也是作两件事:this
能够看到,不管是props仍是data的初始化都是把它们变成响应式对象,其中主要函数有defineReactive,proxy,observe
简单理解是:Vue在建立实例的时候,会拿到options中的data和props等字段,而后对他们进行响应式改造
进行响应式声明的入口只有data、props等几个属性,而且字段须要是显示声明的。因此在其余地方的字段声明就不会有响应式的效果,好比在执行代码中新加的属性,数组的部分操做(这个比较特殊)
若是在平时开发中遇到数据怎么怎么改都不会在页面上动的,能够先检查一下是否触及到上面的问题
let vm = new Vue({
data () {
return {
one: 1,
two: 2,
arr: []
}
},
created: {
this.three = 3
}
})
vm.one = 'one' // 会有对应的响应
vm.two = 'two' // 会有对应的响应
vm.three = 'three' // 不会有对应的响应!
vm.arr[0] = 'new one' // 不会有对应的响应!
复制代码
至于为何,继续看下去便能知一二
proxy的做用是把对象上的属性代理到vm实例上
// src/core/instance/state.js
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)
}
复制代码
这也就是为何咱们定义了以下props、data,经过vm实例就能访问到它
let comP = {
props: {
msg: 'hello'
},
data() {
return {
str: 'hi'
}
}
methods: {
say() {
console.log(this.msg, this.str)
}
}
}
复制代码
proxy方法的实现很简单,经过Object.defineProperty把target[sourceKey][key]的读写变成了对target[key]的读写,对 props而言,vm._props.xxx的读写变成了vm.xxx的读写,因此咱们就能够经过vm.xxx访问到定义在props中的xxx 属性了。同理data也如此
observe是用来给值绑上监测数据的变化的功能
// src/core/observer/index.js
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
}
复制代码
observe 方法的做用就是给非VNode的对象类型数据添加一个Observer,若是已经添加过则直接返回,不然在知足必定条件下去实例化一个Observer对象实例
Observer是一个类,它的做用是给对象的属性添加getter和setter,用于依赖收集和派发更新
export class Observer {
value: any;
dep: Dep;
vmCount: number;
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 (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
复制代码
Observer的构造函数逻辑很简单,首先实例化Dep对象(后面讲),接着经过执行def函数把自身实例添加到数据对象value的 __ob__属性上,def的定义在 src/core/util/lang.js中
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
复制代码
def函数是一个很是简单的Object.defineProperty的封装,这就是为何我在开发中输出data 上对象类型的数据,会发现该对象多了一个__ob__的属性
回到Observer的构造函数,接下来会对value作判断,对于数组会调用observeArray方法,不然对纯对象调用walk方法(重要)。能够看到 observeArray是遍历数组再次调用observe方法,而walk方法是遍历对象的key调用defineReactive方法
defineReactive的功能是定义一个响应式对象,给对象动态添加getter和setter
// src/core/observer/index.js
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
}
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 关键代码!若是是value是对象,进入下一层响应式绑定
let childOb = !shallow && observe(val)
// 这里这里这里是关键!对key进行绑定,而不是对value
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
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
复制代码
defineReactive函数最开始初始化Dep对象的实例,接着拿到obj的属性描述符,而后会对子对象递归调用observe 方法(observe函数会判断传入值的类型而后作对应操做),这样就保证了不管obj的结构多复杂,它的全部子属性也能变成响应式的对象,这样咱们访问或修改 obj 中一个嵌套较深的属性,也能触发getter和setter。最后利用 Object.defineProperty去给obj的属性key添加getter和 setter
而关于getter和setter的具体实现,篇幅太多,下篇介绍
响应式的声明大概就是这样:拿到配置中的data、props等属性,对里面的字段都进行响应式改造:
是否是其实没什么捏