vue双向绑定核心的变化

120 阅读5分钟

vue双向绑定的原理

vue2的双向绑定是通过数据劫持结合发布者-订阅者模式实现的。其核心是通过Object.defineProperty()来实现数据的劫持,在数据变化是发送数据给订阅者,触发相应的监听回调。

vue3中使用了es6的Proxy,可以理解成,在目标对象之前假设了一层拦截,对所有对象的访问必须通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

Object.defineproperty()

Object.defineProperty()方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象,它的用法如下。

Obejct.defineProperty(object,property,descriptor)

参数:
1. - object:属性所在的对象
2. - property:对象中的某个属性
3. - descriptor:属性描述对象

vue实现双向绑定原理图大致如下:

mvvm.png 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官网是这么写的 1652690902(1).png 我们对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)

1652691221(1).jpg 可以看出Object.defineProperty()是支持对数组的监听的。尤雨溪曾回答过这个问题

946301182-5b5a8e2ddb4e7_fix732.webp 并且对数组的相关方法进行了重写

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.fooproxy['foo']
  • set(target, propKey, value, receiver) :拦截对象属性的设置,比如proxy.foo = vproxy['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 入门》一书。