简单聊聊,vue2响应式的基础原理
2025-02-08 07:38 阅读(68)

前言


我们都知道vue2与vue3实现响应式的方式是不一样的,vue2靠的是数据劫持Object.defineProperty (),而vue3靠的则是数据代理Proxy

接下来,让我们具体的聊聊vue2是通过那些核心功能实现响应式的吧

https://www.zuocode.com

Vue2的响应式

首先我们看一段十分基础的代码片段

let obj = {
  a: 1,
  b: 2,
}

console.log(obj.a);
console.log(obj.b);

不出意外地将会打印1和2

如果我们使用数据劫持这个obj.a会发生什么呢?

let obj = {
  a: 1,
  b: 2,
}

Object.defineProperty(obj, 'a', {
  get() {
  },
  set() {
  }
})

console.log(obj.a);
console.log(obj.b);

值得一提的是,对于Object.defineProperty()这个方法就称为数据劫持,这个方法会接受三个参数,分别为一个对象、对象中的属性、以及一个固定的对象并且拥有两个方法get与set

如代码段所示obj中的a属性被劫持了,当我们主动地访问obj.a时会发现并不能得到我们想要的数据,如下图所示。

接着来看下面的代码

let obj = {
  a: 1,
  b: 2,
}
let value = obj.a
Object.defineProperty(obj, 'a', {
  get() {
    return value
  },
  set(val) {
    value = val
    renderViews()
  }
})

function renderViews() {
  console.log('视图变化');
}
obj.a = 3
console.log(obj.a);
console.log(obj.b);

首先需要声明的是,我声明了一个函数renderViews(),由于本文只涉及基础的响应式,所以直接声明函数并打印视图变化一句话用来模拟真实的视图更新。

对于Object.defineProperty()中的两个方法get是用于读取值的,而set则是用于修改值的。值得注意的是,我在外部声明了一个变量value这是由于必须要返回obj.a自己的值,然而如果直接返回obj.a,则会再次陷入对自己的数据劫持从而进入死循环,所以直接在外部声明这个值才是最优解。并且每次obj.a的值变更时都会被set读取并得到一个参数,这个参数就是修改的具体值,直接更新value的值,并且触发掉视图更新的函数即可实现数据最基本的响应式变化。

然而这样很快又带来了新的问题,假使obj中的属性对应的值也是一个对象,那么这个对象中的属性能被劫持到么?

let obj = {
  a: 1,
  b: 2,
  c: {
    d: 3
  }
}
let value = obj.c
Object.defineProperty(obj, 'c', {
  get() {
    return value
  },
  set(val) {
    value = val
    renderViews()
  }
})

function renderViews() {
  console.log('视图变化');
}
obj.c.d = 100

console.log(obj.c);

很明显,虽然数据确实能被修改了,但视图更新函数并没有被触发,也就意味着obj.c.d没有被数据劫持。


那接下来就应该写一个函数使得能够劫持一个对象中所有的嵌套对象中的属性,自然就得使用递归的方式实现

function observer(target) {
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

function defineReactive(target, key, value) {
  if (typeof target === 'object' && target !== null) {
    observer(value)
  }
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(val) {
      value = val
      renderViews()
    }
  })
}
function renderViews() {
  console.log('视图变化');
}

let obj = {
  a: 1,
  b: 2,
  c: {
    d: 3
  }
}

observer(obj)

obj.c.d = 100

在这段代码片中,为了实现业务的分离降低耦合性,重新打造了两个函数,第一个函数observer用于遍历对象,并将所有的对象传递给下一个函数defineReactive这个函数的职责主要是将上个函数传递过来的数据全部进行劫持,并且对传递的值进行判断,如果还是一个对象则递归重新调用函数observer,从而实现了对一个对象当中的所有属性进行数据劫持。

与此同时,也催生了新的问题,我们都知道对于数组与对象,在原生js中都有相应的方法能够对数组与对象的值进行修改,而这种修改的行为能被数据劫持所监听到么?

function observer(target) {
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

function defineReactive(target, key, value) {
  if (typeof target === 'object' && target !== null) {
    observer(value)
  }
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(val) {
      value = val
      renderViews()
    }
  })
}
function renderViews() {
  console.log('视图变化');
}

let obj = {
  a: 1,
  b: 2,
  c: {
    d: 3
  },
  e: [1, 2, 3]
}

observer(obj)

obj.e.push(100)

很显然push方法没有发生视图方法的调用,我们是否能考虑直接重写所有的显示原型上的方法,从而达成这一目的呢?

let oldArrayproto = Array.prototype
let proto = Object.create(oldArrayproto)
Array.from(['push', 'shift']).forEach((method) => {
  //函数劫持,重写函数
  proto[method] = function () {
    oldArrayproto[method].call(this, ...arguments)
    renderViews()
  }
})

// 观察者
function observer(target) {
  if (Array.isArray(target)) {
    target.__proto__ = proto
    return
  }
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

function defineReactive(target, key, value) {
  if (typeof target === 'object' && target !== null) {
    observer(value)
  }
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(val) {
      value = val
      renderViews()
    }
  })
}
function renderViews() {
  console.log('视图变化');
}

let obj = {
  a: 1,
  b: 2,
  c: {
    d: 3
  },
  e: [1, 2, 3]
}

observer(obj)

obj.e.push(100)

以上代码中就是对所有的数组上的方法进行了重写,但由于只是演示效果我只是对push与shift进行了重写。首先通过数组Array上的显示原型能拿到所有挂在数组上的方法,将其再次赋给新的变量proto便于操作不会改变原有的所有的方法。最后将所有的数组方法全部手动写入一个数组当中,用forEach去迭代,迭代打的过程就是对该函数进行劫持的过程,并使用call显示绑定原来的方法使得其能够执行原本方法所需要的功能。

vue2的缺陷


以上就完成了vue2响应式的绝大部分的设计,但vue2仍然具有几个未解决的问题,所以后面随着js版本的更新换代,vue3便被推出了。vue2未解决的问题如下

let oldArrayproto = Array.prototype
let proto = Object.create(oldArrayproto)
Array.from(['push', 'shift']).forEach((method) => {
  //函数劫持,重写函数
  proto[method] = function () {
    oldArrayproto[method].call(this, ...arguments)
    renderViews()
  }
})

// 观察者
function observer(target) {
  if (Array.isArray(target)) {
    target.__proto__ = proto
    return
  }
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

function defineReactive(target, key, value) {
  if (typeof target === 'object' && target !== null) {
    observer(value)
  }
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(val) {
      value = val
      renderViews()
    }
  })
}
function renderViews() {
  console.log('视图变化');
}

let obj = {
  a: 1,
  b: 2,
  c: {
    d: 3
  },
  e: [1, 2, 3]
}

observer(obj)

obj.f = 5
obj.e.length = 10

当我们主动地直接往对象obj添加一个属性f时,这个新增的f是无法被数据劫持到的和触发视图变化


同样如果我们直接去修改obj.e的数组长度时童谣无法触发视图变化


还有一个容易被忽视的点则是无论数据有没有被在html当中用到,所有的数据均会被数据劫持到


作者:Kousi

链接:https://juejin.cn