这篇文章介绍vue的响应式原理了,尽量不贴源码,就是讲讲自己的理解,主要受众是对响应式原理一知半解或者零基础的。希望看完之后都能对响应式原理有所收获。
概要
响应式原理 其实就是渲染函数会在响应式数据发生变化的时候重新执行。
这样做的好处就是当页面中的动态数据发生变化时,我们不需要手动拿到最新的数据然后进行赋值,因为框架的响应式原理已经自动帮我们做了。
Vue2的具体实现
首先会把data属性(通常是一个函数,返回值是一个对象),比如:
export default {
data() {
return {
showAlert: false,
alertText: null,
}
},
}
如果函数的返回值不是对象话会进行警告
let data: any = vm.$options.data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
if (!isPlainObject(data)) {
data = {}
__DEV__ &&
warn(
'data functions should return an object:\n' +
'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
总之拿到data属性的对象之后,会把这个对象赋值给 vm._data 对象,接着对所有的key进行代理,这就是为什么我们可以通过this直接访问到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 (__DEV__) {
if (methods && hasOwn(methods, key)) {
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
__DEV__ &&
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)
}
}
可以看到这里还会检查data返回对象的属性有没有在props和methods中定义过,如果已经定义过的话会进行警告。
然后通过proxy的方式proxy(vm, _data, key),,让用户可以直接通过this拿到属性,其实我们用this._data也可以拿到。
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)
}
最后会对data进行observe
// observe data
const ob = observe(data)
ob && ob.vmCount++
在observe方法里面,主要创建了一个Observer类
new Observer(value, shallow, ssrMockReactivity)
在observe方法里面,主要创建了一个Observer类
new Observer(value, shallow, ssrMockReactivity)
然后在这个类里面会拿到data的每一个key,对key进行defineReactive方法。
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
}
在defineReactive里面会创建一个Dep实例const dep = new Dep(),Dep是Vue2实现响应式原理的核心关键。主要作用是存储我们希望自动执行的函数,在Vue2这里就是一些Watcher(有三个Watcher,渲染Watcher,computed watcher 和user watcher,user watcher就是我们vue2配置的watcher属性。)
然后会对data,key的值进行再次observe,直到值不是对象为止。
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) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key]
}
let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
接下来就是Vue2响应式的核心了,也是vue2能对数据进行劫持的关键,就是通过Object.defineProperty对对象属性的读取操作进行劫持,大致流程就是渲染函数(或者其他,比如computed和watcher属性)读取数据的时候把自己添加到这个属性创建的dep实例上,当这个数据被修改的时候,比如点击事件,再把收集到的渲染函数(或者其他)取出来重新执行。
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 (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
dep.notify()
}
})
Dep.target就是我们要收集的副作用函数,在get中被收集,然后在set中就会通过dep.notify()方法重新触发。下面再贴一下dep.depend方法和dep.notify方法。
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
// stabilize the subscriber list first
const subs = this.subs.filter(s => s) as DepTarget[]
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
sub.update()
}
}
补充说明
渲染函数其实就是将vue的模板进过一系列转换,转成最终生成vdom(虚拟dom)的函数,然后再通过patch方法转成真实dom挂载到浏览器上。