惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Privacy International News Feed
Martin Fowler
Martin Fowler
D
Docker
Y
Y Combinator Blog
云风的 BLOG
云风的 BLOG
U
Unit 42
T
Tailwind CSS Blog
J
Java Code Geeks
G
Google Developers Blog
MongoDB | Blog
MongoDB | Blog
阮一峰的网络日志
阮一峰的网络日志
WordPress大学
WordPress大学
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
美团技术团队
F
Fortinet All Blogs
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
The GitHub Blog
The GitHub Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Recorded Future
Recorded Future
N
Netflix TechBlog - Medium
Google DeepMind News
Google DeepMind News
Hacker News: Ask HN
Hacker News: Ask HN
L
LINUX DO - 最新话题
Microsoft Security Blog
Microsoft Security Blog
N
News and Events Feed by Topic
I
Intezer
TaoSecurity Blog
TaoSecurity Blog
NISL@THU
NISL@THU
小众软件
小众软件
博客园 - 聂微东
博客园 - Franky
有赞技术团队
有赞技术团队
P
Palo Alto Networks Blog
爱范儿
爱范儿
H
Hacker News: Front Page
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cisco Blogs
P
Proofpoint News Feed
I
InfoQ
Google DeepMind News
Google DeepMind News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Vercel News
Vercel News
H
Heimdal Security Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
量子位

小蘿蔔丁

About 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。 小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。
小蘿蔔丁的自留地: 喜欢码字,数码控,美剧迷,热爱开源技术,前端搬砖,专注科技互联网。
2019-09-21 · via 小蘿蔔丁

💡 本篇博文可能学到的知识点

  1. 更好的理解 Vue 响应式工作原理
  2. 学习 Vue 的设计模式
  3. 学习 Proxy API
  4. 使用 Proxy 实现观察者模式

现代前端开发必不可少会用到的 Vue、React 等框架,这些框架的共同之处在于都提供了响应式(Reactive)和组件化(Composable)的视图组件,组件化开发重新定义了前端开发技术栈。结合前端构建工具以及基于框架出现的各种经过精心设计的 UI 组件库,让前端也进入到了一个工程化的时代。构建页面变得从未有过的简洁高效。

如果你是一名经验丰富(nian ling da)的程序员,或多或少都会接触到没有这些框架之前的状态,那时候我们还手持 jQuery 利器,操纵着一手好 dom,当你初次接触到响应式框架的时候或许会被它的好用所惊艳到。我们只需要改变数据,dom 就更新了。本篇博文主要是来讨论被惊艳到的响应式框架是如何实现的。我们首先来看看 Vue 是如何实现响应式系统的?

Vue 是如何实现响应式系统的?

看一个简单的小例子

假设我们购物车中有一个商品,单价 price,数量 quantity,总价为 total

有这样一个简单的功能,点击按钮让 price 发生变化,那么 total 为计算属性,也随之发生变化。

<!-- App.vue -->
<div id="app">
  <div>Price: ¥{{ price }}</div>
  <div>Quantity: {{ quantity }}</div>
  <div>Total: ¥{{ total }}</div>
  <div>
    <button @click="price = 10">
      change price
    </button>
  </div>
</div>
// main.jsnew Vue({  el: '#app',  data: {    price: 6.0,    quantity: 2  },  computed: {    total() {      return this.price * this.quantity    }  }})

考虑一下 Vue 是如何实现这样的功能的,其实我们是更改了数据,而依赖于它的计算属性也发生了变化。

Vue 是如何实现追踪数据的变化的

在官方文档,深入响应式原理里有讲到

当你把一个普通的 JavaScript 对象传入 Vue 实例做为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。

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

reactivity

一些弊端

受现代 JavaScript 的限制

对于已创建的实例,Vue **无法检测到对象属性的添加和删除。**Vue 允许使用 Vue.set(object, propertyName, value) 的方法进行动态添加响应式属性,或者使用别名写成 this.$set(object, propertyName, value)

对于数组,Vue 无法检测以下数组的变动

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

第一条的解决方式同样是使用 this.$set(vm.items, indexOfItem, newValue)

第二条的解决方式是用 vm.items.splice(indexOfItem, 1, newValue)

对于以上的弊端,其实官方已经给出了解决方法,就是使用 Proxy 代替 Object.defineProperty API,这个改动会伴随着万众期待的 Vue 3.0 的发布而应用。大家都知道 Vue 2.x 也就是现在的版本,响应式是通过 Object.defineProperty API 来实现的,那么既然知道了解决方法,我们不妨提前学习一下 Proxy 是如何做到响应式的。

使用 Proxy 实现响应式系统

Proxy 简介

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

基本用法

let p = new Proxy(target, hanlder)

参数:

target 用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler 一个对象,其属性是当执行一个操作时定义代理的行为的函数。

MDN 上的基础示例

let handler = {  get: function (target, name) {    return name in target ? target[name] : 37  }}let p = new Proxy({}, handler)p.a = 1p.b = undefinedconsole.log(p.a, p.b) // 1, undefinedconsole.log('c' in p, p.c) // false, 37

上述例子使用了 get ,当对象中不存在属性名时,缺省返回数为 37。我们知道了对于操作对象,可以在使用 handler 处理一些事务。关于 handler 则有十三种。

实现一个响应式系统

重新回到我们的小例子,我们有一些变量,单价 price,数量 quantity,总价为 total。我们来看下 JavaScript 是如何工作的

let price = 6let quantity = 2let total = 0total = price * quantityconsole.log(total) // 12price = 10console.log(total) // 12

这好像不是我们的期望,我们期望的是,改变了 price,total 的值也会更新。如何做到这一点,并不难。

let price = 6let quantity = 2let total = 0total = price * quantityconsole.log(total) // 12price = 10console.log(total) // 12const updateTotal = () => {  total = price * quantity}updateTotal()console.log(total) // 20

我们定义了一个方法 updateTotal,让这个方法执行了我们需要更新 total 的业务逻辑,再重新执行这个方法那么 total 的值就改变了。

我们可以考虑下我们想到达到什么样的目的,其实很简单,就是当改变 price 或者 quantity 的时候 total 的值会跟着改变。学过设计模式的话,我们很容易想到这个场景符合观察者模式。

使用观察者模式

观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

observer_pattern

现在我们使用观察者模式定义了观察变量 price 与 quantity,如果它们的值发生变化,那么依赖于它的计算属性 total 将会得到一个 notify,这个 notify 即是我们的目标,去执行 updateTotal。

创建依赖类

把观察者模式抽象为一个依赖类

// 代表依赖类class Dep {  constructor() {    this.subscribers = [] // 把所有目标收集到订阅里  }  addSub(sub) {    // 当有可观察目标时,添加到订阅里    if (sub && !this.subscribers.includes(sub)) {      // 只添加未添加过的订阅      this.subscribers.push(sub)    }  }  notify() {    // 当被观察的属性发生变动时通知所有依赖于它的对象    this.subscribers.forEach((fn) => fn()) // 重新执行所有订阅过的目标方法  }}

那么如何使变量 price 和 quantify 变得可观察,在 Vue 2.x 中使用的是 Object.defineProperty ,本文会使用 Proxy 来实现。

// 使变量变为一个可观察的对象的属性const dataObj = {  price: 6,  quantity: 2}let total = 0let target = nullclass Dep { // 代表依赖类    ...}const dep = new Dep()data = new Proxy(dataObj, {  get (obj, key) {    dep.addSub(target)  // 将目标添加到订阅中    return obj[key]  },  set (obj, key, newVal) {    obj[key] = newVal // 将新的值赋值给旧的值,引起值的变化    dep.notify()  // 被观察的属性值发生变化,即通知所有依赖于它的对象  }})total = data.price * data.quantityconsole.log(total)  // 12data.price = 10console.log(total)  // 12target = () => {  total = data.price * data.quantity}target()target = nullconsole.log(total)  // 20

上面代码稍微有些凌乱,我们重构一下,观察者模式结合 Proxy 最终目的就是输出被观察的对象。我们可以抽象为一个 observer

使用 Proxy 实现观察者模式

// 将依赖类与 Proxy 封装为 observer,输入一个普通对象,输出为被观察的对象const observer = (dataObj) => {  const dep = new Dep()  return new Proxy(dataObj, {    get(obj, key) {      dep.addSub(target) // 将目标添加到订阅中      return obj[key]    },    set(obj, key, newVal) {      obj[key] = newVal // 将新的值赋值给旧的值,引起值的变化      dep.notify() // 被观察的属性值发生变化,即通知所有依赖于它的对象    }  })}const data = observer(dataObj)

我们注意到,其实每次我们还要重新执行我们的目标 target ,让 total 值发生变化。这块儿逻辑我们可以抽象为一个 watcher ,让它帮我们做一些重复做的业务逻辑。

创建 watcher

const watcher = (fn) => {  target = fn  target()  target = null}watcher(() => {  total = data.price * data.quantity})

我们最终代码优化为:

// 使变量变为一个可观察的对象的属性const dataObj = {  price: 6,  quantity: 2}let total = 0let target = null// 代表依赖类class Dep {  ...}// 使用 Proxy 实现了观察者模式const observer = dataObj => {  ...}const data = observer(dataObj)// 高阶函数,重复执行订阅方法const watcher = fn => {  ...}watcher(() => {  total = data.price * data.quantity})console.log(total)  // 12data.price = 30console.log(total) // 60

我们最终实现了开始的想法,total 会根据 price 值的改变而改变。实现了简单的响应式系统。

为了缩小篇幅,上面的方法同时也有讲过,即折叠(简化)起来,我们再回到 Vue 是如何追踪数据的依赖的那张图。再看看我们是如何实现的。

vue-reactive-proxy

总结一下

通过小例子我们学习到的内容

  • 我们学习到了通过创建一个 Dep 依赖类,来收集依赖关系,在订阅者属性被改变时,所有依赖于它的对象得以得到一个通知。
  • 结合 Dep 依赖类,我们使用 Proxy 实现了观察者模式的 observer 方法
  • 我们创建了一个 watcher 观察者方便管理我们要重新执行的业务逻辑,即我们添加到订阅里需要执行的方法

结尾

本文讲到了 Vue2.x 中响应式系统的一些弊端,在即将到来的 Vue 3.0 Updates 中都将得到解决,在去年这时候尤大在 Plans for the Next Iteration of Vue.js 这篇博文中也有提到过。让我们期待 Vue 3.0 的到来吧。