Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue2 设计系统之响应式 #79

Open
fayeah opened this issue Nov 4, 2021 · 0 comments
Open

Vue2 设计系统之响应式 #79

fayeah opened this issue Nov 4, 2021 · 0 comments

Comments

@fayeah
Copy link

fayeah commented Nov 4, 2021

之前写过一篇《Vue3 设计系统之响应式》。我们知道 Vue3 并没有延续 Vue2 的设计,而是进行了重写,那么 Vue2 的设计是怎么样的,为什么要进行重写呢?知其然也要知其所以然。那这篇文章就是对 Vue2 响应式的一个梳理,只针对 data 层面进行了剖析,没有涉及 Compile 相关内容哈,各位老师请指教呀 ~

回顾 Vue 组件的基本写法:

Vue.component("comp", {
  ...,
  data: {
    message: "Hello, world"
  },
});

文档中的两句话点出了 Vue 的响应式原理:

  1. 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。

  2. 每个组件实例都对应一个 Watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 Watcher,从而使它关联的组件重新渲染。

可见 Vue 的响应式有几个关键词:Object.defineProperty依赖收集/依赖追踪(Dependencies)Watcher。那基于这些信息我们来分析一下 Vue2 的 Reactivity。

利用观察者模式

当一个对象被修改时,则会自动通知依赖它的对象。

这便是观察者模式的一种使用方式,Vue 这里也采用了相应的设计。Talk is cheap, show me the code. 来看一个代码片段感受一下:

let Subject = {
    _state: 0,
    _observers: [],
    add: function(observer) {
        this._observers.push(observer);
    },
    getState: function() {
        return this._state;
    },
    setState: function(value) {
        this._state = value;
        for (let i = 0; i < this._observers.length; i++)
        {
            this._observers[i].signal(this);
        }
    }
};

let Observer = {
  signal: function(subject) {
    let currentValue = subject.getState();
    console.log(currentValue);
  }
}

Subject.add(Observer);
Subject.setState(10);

Subject(主题)维护着一个依赖列表即_observers,当 Subject 有任何状态(_state)更新时,便会自动通知到这些订阅者(_observers),通知的方式一般是调用订阅者的一个方法,在这里就是 signal方法。从而实现简单的广播通信。

Vue 则使用了 Dep 类来维护订阅者列表,跟上面例子设计思路是类似的:

class Dep { // Dep === Dependencies
  constructor () {
    this.subscribers = []
  }
  depend() {
    if (target && !this.subscribers.includes(target)) {
      this.subscribers.push(target)
    }
  }
  notify() {
    this.subscribers.forEach(sub => sub())
  }
}

Dep 中的subscribers存放了依赖列表,也就是targettarget为当前依赖,什么是当前依赖,哪个时机来存储或执行呢,且继续往下看。

Object.defineProperty()

大家都知道 Vue 利用了 ES5 的 Object.defineProperty(),对该方法不熟悉的请戳:point_right:MDN。我们也知道 Vue 通过 遍历 data 的所有属性来为每个属性做响应式。每一个 data 的属性都有一个依赖列表(即 Dep 类),每当获取(getter)某一个属性时进行依赖收集的操作,每当某一个属性值发生变化(setter)时,便通知其对应的依赖列表。由此可以得出以下代码片段:

 Object.keys(data).forEach(key => {
  let internalValue = data[key]

  // 每个属性都有一个Dep实例
  const dep = new Dep()

  Object.defineProperty(data, key, {
    get() {
      dep.depend() // 收集依赖,当前依赖为 target,暂且可以理解为全局的变量 target
      return internalValue
    },
    set(newVal) {
      internalValue = newVal
      dep.notify() // 通知该 key 对应的所有依赖,触发他们执行
    }
  })
})

我们一直在说 target,Dep 类里面使用 target 进行依赖收集,Object.defineProperty 的 getter 方法里面通过调用 Dep 的 depend 方法来进行依赖收集,那么 target 在哪里呢,它是怎么维护的呢?不得不引出Watcher这个概念,实际上每一个 Vue 组件实例都对应一个 Watcher 实例,将接触过的数据记录为依赖:

function watcher(myFunc) {
  target = myFunc
  target()
  target = null  //target 必须重置哈,因为Dep里面depend方法有一个判断,如果不重置,新的target便不能被添加到依赖列表里面
}

可能还是没那么直接哈,那我们把 DepObject.defineProperty 以及 Wather 都串起来就比较好理解了:

// 声明一个data,跟vue里面的 $data 类似,以及一个全局target
let data = { price: 5, quantity: 2 }
    let target = null

// 先调用watcher记录当前target,当前target即为匿名函数:() => {data.total = data.price * data.quantity}
watcher(() => {
  data.total = data.price * data.quantity
})

// 测试响应式
data.total // 10(思考一下这行代码发生了什么?)
data.price = 20 //调用set方法 (这行又执行到了哪里?)
data.total // 40 

大家可以根据这部分代码一点一点带到三部分代码里面阅读一下,很惊讶于 Vue 的设计,值得读很多遍 ~

Vue2 响应式的不足及解决办法

前面我们理解了 Vue2 的响应式的实现,对其原理的设计可谓叹为观止。但是,Vue2 也不是没有自身的缺陷,否则也不会在 Vue3 彻底重写了不是。缺点一句话概括就是:

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。

关于对象

先看一段代码片段,使用 Object.defineProperty 的方式给对象做拦截:

let initialVal;
    data={};
Object.defineProperty(data, 'name', {
  set: function(value) {
    console.log('set The method is implemented ');
    initialVal = value;
  },
  get: function() {
    return initialVal;
  }
});

// 修改data的name属性
data.name = "Fei"  // set方法被执行,打印出:set The method is implemented

// 为data添加属性 workingHrs
data.workingHrs = 8  // set方法未被执行,没有打印出:set The method is implemented

// 删除name属性
delete data.name // set方法未被执行,没有打印出:set The method is implemented

可见,Object.defineProperty 不能对对象属性的添加和删除进行监听,因此 Vue 也不能利用该原生方法做响应式处理。于是Vue.set便产生了。只需要简单的一行,就可以对新添加的属性实现响应式,Vue.delete同理:

Vue.$set(data, 'workingHrs', 8);

关于数组

我们把上面代码的 name 先改成数组,看看数组的情况:

// 修改name属性为数组
data.name = []  // set方法被执行,打印出:set The method is implemented

// 使用索引位数组设置元素
data.name[0] = 0 // set方法未被执行,未打印出log

// 设置数组长度
data.name.length = 1 // set方法未被执行,未打印出log

// 使用数组原生方法push
data.name.push(1) // set方法未被执行,未打印出log

可以看到对于数组的几乎所有操作,都不能触发 set 方法。而 Vue 中使用 push 可以实现响应式,这是为啥呢?因为 Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

结语

Vue2 做了很多的努力来帮助实现数据的响应式,但却是很繁琐的一件事情,很庆幸 ES6 对数据的拦截有了新的更新 Proxy 和 Reflect,将大大简化 Vue 本身的设计。希望大家读完这篇文章能对vue2的响应式有更进一步的理解,也许能为某些困惑的bug提供一些思路。

2021就只剩下两个月了,不知道今年还能写几篇技术博文,谁知道呢?今年的愿望清单大家实现的怎么样了呀?时间过的飞快,唯学习是我快乐 ~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant