vue双向绑定的原理
vue2的双向绑定是通过数据劫持结合发布者-订阅者模式实现的。其核心是通过Object.defineProperty()来实现数据的劫持,在数据变化是发送数据给订阅者,触发相应的监听回调。
vue3中使用了es6的Proxy,可以理解成,在目标对象之前假设了一层拦截,对所有对象的访问必须通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
Object.defineproperty()
Object.defineProperty()方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象,它的用法如下。
Obejct.defineProperty(object,property,descriptor)
参数:
1. - object:属性所在的对象
2. - property:对象中的某个属性
3. - descriptor:属性描述对象
vue实现双向绑定原理图大致如下:
1. 属性劫持
当我们访问对象的某个属性的时候实际上就会触发Object.defineProperty()的get,设置属性的时候会触发set,因此我们可以劫持对象上的所有属性:
var obj2 = {
name:'张三'
};
function DefineProperty(obj,key,val){
Object.defineProperty(obj,key,{
get(){
console.log('触发了get方法')
return val
},
set(newVal){
val = newVal;
}
})
}
function Observe(obj){
Object.keys(obj).forEach(key => {
DefineProperty(obj,key,obj[key])
})
}
Observe(obj2)
console.log(obj2.name)
//张三
这样我们就已经完成对对象属性的劫持
2.深度监听
以上只能监听属性的值是基本数据类型,需要添加递归监听对象,例:
function DefineProperty(obj,key,val){
const dep = new Dep()
Object.defineProperty(obj,key,{
get(){
console.log('触发了get方法')
if (Dep.target) {
dep.addDep(Dep.target)
}
return val
},
set(newVal){
val = newVal;
dep.notify()
}
})
}
function Observe(obj){
if(typeof obj !== 'object' || obj === null) return
Object.keys(obj).forEach(key => {
if(typeof obj[key] === 'object'){
Observe(obj[key])
}
DefineProperty(obj,key,obj[key])
})
}
然后在set函数里面会放一个通知函数dep.notify(),在访问属性的时候在get函数去执行添加订阅者操作,Dep的主要作用是收集依赖,并设置通知函数。
class Dep{
constructor(){
this.deps = []
}
addDep(dep){
this.deps.push(dep)
}
notify(){
this.deps.forEach(dep = dep.update())
}
}
添加Watcher
class Watcher{
constructor() {
// 将当前watcher实例指定到Dep静态属性target
Dep.target = this;
}
update() {
console.log("属性更新了");
}
}
defineReactive(data,key,value){
const dep = new Dep()
Object.defineProperty(data,key,{
get(){
Dep.target && dep.addDep(Dep.target)
return value
}
})
}
从代码上看,我们设计了一个订阅器 Dep 类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 Dep.target,这是一个全局唯一 的Watcher,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组。
3.监听数组的变化
我们都知道在vue中通过数组的下标改变数组中某一项的时候,vue无法监听数组的变化,在vue官网是这么写的
我们对
Object.defineProperty()进行测试:
var obj2 = [
{
id:1,
name:'张三'
},
{
id:2,
name:'李四'
},
{
id:3,
name:'王五'
},
];
function DefineProperty(obj,key,val){
if(typeof val === 'object'){
Observe(val)
}
Object.defineProperty(obj,key,{
get(){
console.log('触发了get方法'+key)
return val
},
set(newVal){
if(typeof newVal === 'object')Observe(newVal)
console.log('触发了set方法'+newVal)
val = newVal
}
})
}
function Observe(obj){
if(typeof obj !== 'object' || obj === null) return
Object.keys(obj).forEach(key => {
DefineProperty(obj,key,obj[key])
})
}
Observe(obj2)
obj2[0] = {id:4,name:'赵六'};
console.log(obj2)
可以看出
Object.defineProperty()是支持对数组的监听的。尤雨溪曾回答过这个问题
并且对数组的相关方法进行了重写
onst arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
Object.defineProperty()局限的地方:
1.一次只能对一个属性进行监听,需要遍历来对所有属性监听。
2. 在遇到一个对象的属性还是一个对象的情况下,需要递归监听。
3. 对于对象的新增属性,需要手动监听
4. 对于数组通过push、unshift方法增加的元素,也无法监听
Proxy
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。语法:
var proxy = new Proxy(target,handler)
参数含义:
target:要拦截的目标对象
handler:用来定制拦截行为
请看下面的例子:
let person = {
name:'张三',
age:14
}
let handler = {
get(obj,key){
return obj[key]
},
set(obj,key,val){
obj[key] = val
// return true
}
}
let proxy = new Proxy(person,handler)
proxy.name = '李四'
proxy.sex = '男'
console.log(proxy.name)
console.log(proxy.age)
console.log(proxy.sex)
get接收三个参数
obj:要监听的对象
key:属性名
proxy:创建的Proxy实例(可选)
set接受四个参数
obj:要监听的对象
key:属性名
val:属性值
proxy:创建的Proxy实例(可选)
输出“李四”,14,男,说明set get拦截成功,同时说明Proxy可以同时拦截多个属性,新添加的属性同样可以拦截。
下面是 Proxy 支持的拦截操作一览,一共 13 种:
- get(target, propKey, receiver) :拦截对象属性的读取,比如
proxy.foo和proxy['foo']。 - set(target, propKey, value, receiver) :拦截对象属性的设置,比如
proxy.foo = v或proxy['foo'] = v,返回一个布尔值。 - has(target, propKey) :拦截
propKey in proxy的操作,返回一个布尔值。 - deleteProperty(target, propKey) :拦截
delete proxy[propKey]的操作,返回一个布尔值。 - ownKeys(target) :拦截
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey) :拦截
Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 - defineProperty(target, propKey, propDesc) :拦截
Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。 - preventExtensions(target) :拦截
Object.preventExtensions(proxy),返回一个布尔值。 - getPrototypeOf(target) :拦截
Object.getPrototypeOf(proxy),返回一个对象。 - isExtensible(target) :拦截
Object.isExtensible(proxy),返回一个布尔值。 - setPrototypeOf(target, proto) :拦截
Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 - construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)。
Proxy解决了Object.defineProperty()无法监听对象和数组的缺陷,使用Proxy监听的事件更加方便和灵活。
关于Proxy更加详细的用法推荐研究阮一峰老师的《ECMAScript 6 入门》一书。