

在vue中,我们知道它的核心思想是数据驱动视图,表现层我们知道在页面上,当数据发生变化,那么视图层也会发生变化。这种数据变化驱动视图背后依靠的是什么?
正文开始...
// src/core/instance/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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
我们会发现其实在vue2源码中,本质上就是利用Object.defineProperty来劫持对象。
每劫持一组对象,每一个属性会实例化一个Dep对象,每个拦截的对象属性都会动态添加get和set将传入的data或者prop变成响应式,在Object.defineProperty的get中,当我们访问对象的某个属性时,就会先调用get方法,依赖收集调用dep.depend(),当我们设置该属性值时就会调用set方法调用dep.notify()``派发更新所有的数据,在调用notify时会调用实例Watch的run,从而执行watch的回调方法。
在vue2源码中劫持对象实现数据驱动视图,那么我们依葫芦画瓢,化繁为简,实现一个自己的数据劫持。
新建一个index.js
// index.js
var options = {
name: 'Maic',
age: 18,
from: 'china'
}
const renderHtml = (data, key) => {
const appDom = document.getElementById('app');
appDom.innerHTML = `<div>
<p>options:${JSON.stringify(options)}</p>
<p>${key}: ${JSON.stringify(data)}</p>
</div>`;
}
const defineReactive = (target, key) => {
let val = target[key];
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: function() {
return val;
},
set: function(nval) {
console.log(nval, '==nval')
val = nval;
renderHtml(nval, key);
}
})
}
Object.keys(options).forEach(key => {
defineReactive(options, key);
})
renderHtml(options, 'name');
再新建一个html引入index.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue2-reactive</title>
</head>
<body>
<div id="app"></div>
<script src="./index.js"></script>
</body>
</html>
直接打开index.html

当我们大开控制台时,我们直接修改options.age = 10此时会触发拦截器的set方法,从而进行更新页面数据操作。
在源码里里面处理是相当复杂的,我们可以看到访问数据时,会先调用get方法,在dep.depend()进行依赖收集,然后再设置对象的值时,会调用set方法,派发更新操作。更多关于vue2响应式原理可以参考这篇文章响应式原理[1]
vue3主要利用Proxy这个API来实现对象劫持的,关于Proxy可以看下阮一峰老师的es6教程proxy[2],全网讲解Proxy最好的教程了。
继续用个例子来感受下
var options = {
name: 'Maic',
age: 18,
from: 'china'
}
const renderHtml = (data, key) => {
const appDom = document.getElementById('app');
appDom.innerHTML = `<div>
<p>options:${JSON.stringify(options)}</p>
<p>${key}: ${JSON.stringify(data)}</p>
</div>`;
}
renderHtml(options, 'name');
var proxy = new Proxy(options, {
get: function (target, key, receiver) {
console.log(key, receiver)
return Reflect.get(target, key)
},
set: function(target, key, val, receiver) {
console.log(key, val, receiver);
renderHtml(val, key);
return Reflect.set(target, key, val);;
}
})
当我们在控制输入proxy.name = 111时,此时就会触发new Proxy()内部的set方法,而我们此时采用的是利用Reflect.set(target,key,val)成功的设置了,在get中,我们时用Relect.get(target, key)获取对应的属性值。
这点与vue2中劫持数据的方式比较大,具体可以看下vue3源码响应式reactive实现
// package/reactivity/src/reactive.ts
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
从源码中我们可以看出在vue3使用reative初始化响应式数据时,实际上它就是就是一个被proxy代理后的数据,并且使用WeakMap来存储响应式数据的。
相比较vue2的defineProperty,vue3的Proxy更加强大,因为代理对象对劫持的对象动态新增属性也一样有检测,而defineProperty就没有这种特性,它只能劫持已有的对象属性。
vue2中数据劫持是用Object.defineProperty,当访问对象属性时会触发get方法进行依赖收集,当设置对象属性时会触发set方法进行派发更新操作。vue3中数据劫持时用new Proxy()来做的,可以动态的监测对象的属性新增与删除操作,效率高,实用简单[1]响应式原理: https://ustbhuangyi.github.io/vue-analysis/v2/reactive/reactive-object.html#object-defineproperty
[2]proxy: https://es6.ruanyifeng.com/#docs/proxy
[3]code example: https://github.com/maicFir/lessonNote/tree/master/vue/01-响应式