您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

深入浅出 Vue 响应式原理!

时间:2019-07-16 09:12:32  来源:  作者:

作者 | 浪里行舟

责编 | 胡巍巍

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JAVA 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。本文将针对响应式原理做一个详细介绍,并且带你实现一个基础版的响应式系统。

什么是响应式

我们先来看个例子:

<divid="App">

<div>Price :¥{{ price }}</div>

<div>Total:¥{{ price * quantity }}</div>

<div>Taxes: ¥{{ totalPriceWithTax }}</div>

<button@click="changePrice">改变价格</button>

</div>

varapp = new Vue({

el: '#app',

data{

return{

price: 5.0,

quantity: 2

};

},

computed: {

totalPriceWithTax {

returnthis.price * this.quantity * 1.03;

}

},

methods: {

changePrice {

this.price = 10;

}

}

})

  1.  

上例中当price 发生变化的时候,Vue就知道自己需要做三件事情:

  • 更新页面上price的值
  • 计算表达式 price*quantity 的值,更新页面
  • 调用totalPriceWithTax 函数,更新页面

数据发生变化后,会重新对页面渲染,这就是Vue响应式,那么这一切是怎么做到的呢?

想完成这个过程,我们需要:

  • 侦测数据的变化
  • 收集视图依赖了哪些数据
  • 数据变化时,自动“通知”需要更新的视图部分,并进行更新

对应专业俗语分别是:

  • 数据劫持 / 数据代理
  • 依赖收集
  • 发布订阅模式

如何侦测数据的变化

首先有个问题,在Java中,如何侦测一个对象的变化?其实有两种办法可以侦测到变化:使用 Object.defineProperty和ES6的 Proxy,这就是进行数据劫持或数据代理。这部分代码主要参考珠峰架构课。

方法1.Object.defineProperty

Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

functionrender{

console.log('模拟视图渲染')

}

letdata = {

name: '浪里行舟',

location: { x: 100, y: 100}

}

observe(data)

functionobserve(obj) { // 我们来用它使对象变成可观察的

// 判断类型

if(!obj || typeofobj !== 'object') {

return

}

Object.keys(obj).forEach(key=>{

defineReactive(obj, key, obj[key])

})

functiondefineReactive(obj, key, value) {

// 递归子属性

observe(value)

Object.defineProperty(obj, key, {

enumerable: true, //可枚举(可以遍历)

configurable: true, //可配置(比如可以删除)

get: functionreactiveGetter{

console.log('get', value) // 监听

returnvalue

},

set: functionreactiveSetter(newVal) {

observe(newVal) //如果赋值是一个对象,也要递归子属性

if(newVal !== value) {

console.log('set', newVal) // 监听

render

value = newVal

}

}

})

}

}

data.location = {

x: 1000,

y: 1000

} //set {x: 1000,y: 1000} 模拟视图渲染

data.name // get 浪里行舟

上面这段代码的主要作用在于:observe这个函数传入一个 obj(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理,以此来达到实现侦测对象变化。值得注意的是, observe 会进行递归调用。那我们如何侦测Vue中 data 中的数据,其实也很简单:

classVue{

/* Vue构造类 */

constructor(options) {

this._data = options.data;

observer(this._data);

}

}

这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行追踪变化。不过这种方式有几个注意点需补充说明:

  • 无法检测到对象属性的添加或删除(如 data.location.a=1)。

Vue 通过 Object.defineProperty来将对象的key转换成 getter/setter的形式来追踪变化,但 getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。如果是删除属性,我们可以用 vm.$delete实现,那如果是新增属性,该怎么办呢?

1)可以使用 Vue.set(location,a,1) 方法向嵌套对象添加响应式属性;2)也可以给这个对象重新赋值,比如 data.location={...data.location,a:1}

  • Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写,具体代码如下:

functionrender{

console.log('模拟视图渲染')

}

letobj = [1, 2, 3]

letmethods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']

// 先获取到原来的原型上的方法

letarrayProto = Array.prototype

// 创建一个自己的原型 并且重写methods这些方法

letproto = Object.create(arrayProto)

methods.forEach(method=>{

proto[method] = function{

// AOP

arrayProto[method].call(this, ...arguments)

render

}

})

functionobserver(obj) {

// 把所有的属性定义成set/get的方式

if(Array.isArray(obj)) {

obj.__proto__ = proto

return

}

if(typeofobj == 'object') {

for(letkey inobj) {

defineReactive(obj, key, obj[key])

}

}

}

functiondefineReactive(data, key, value) {

observer(value)

Object.defineProperty(data, key, {

get {

returnvalue

},

set(newValue) {

observer(newValue)

if(newValue !== value) {

render

value = newValue

}

}

})

}

observer(obj)

function$set(data, key, value) {

defineReactive(data, key, value)

}

obj.push(123, 55)

console.log(obj) //[1, 2, 3, 123, 55]

这种方法将数组的常用方法进行重写,进而覆盖掉原生的数组方法,重写之后的数组方法需要能够被拦截。但有些数组操作Vue时拦截不到的,当然也就没办法响应,比如:

obj.length-- // 不支持数组的长度变化

obj[0]=1// 修改数组中第一个元素,也无法侦测数组的变化

ES6提供了元编程的能力,所以有能力拦截,Vue3.0可能会用ES6中Proxy 作为实现数据代理的主要方式。

方法2.Proxy

Proxy 是 Java 2015 的一个新特性。Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性, Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外 Proxy支持代理数组的变化。

functionrender{

console.log('模拟视图的更新')

}

letobj = {

name: '前端工匠',

age: { age: 100},

arr: [1, 2, 3]

}

lethandler = {

get(target, key) {

// 如果取的值是对象就在对这个对象进行数据劫持

if(typeoftarget[key] == 'object'&& target[key] !== null) {

returnnewProxy(target[key], handler)

}

returnReflect.get(target, key)

},

set(target, key, value) {

if(key === 'length') returntrue

render

returnReflect.set(target, key, value)

}

}

letproxy = newProxy(obj, handler)

proxy.age.name = '浪里行舟'// 支持新增属性

console.log(proxy.age.name) // 模拟视图的更新 浪里行舟

proxy.arr[0] = '浪里行舟'//支持数组的内容发生变化

console.log(proxy.arr) // 模拟视图的更新 ['浪里行舟', 2, 3 ]

proxy.arr.length-- // 无效

以上代码不仅精简,而且还是实现一套代码对对象和数组的侦测都适用。不过 Proxy兼容性不太好!

为什么要收集依赖

我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如第一例子中,模板中使用了price 数据,当它发生变化时,要向使用了它的地方发送通知。那如果多个Vue实例中共用一个变量,如下面这个例子:

letglobalData = {

text: '浪里行舟'

};

lettest1 = newVue({

template:

`<div>

<span>{{text}}</span>

<div>`,

data: globalData

});

lettest2 = newVue({

template:

`<div>

<span>{{text}}</span>

<div>`,

data: globalData

});

如果我们执行下面这条语句:

globalData.text= '前端工匠';

此时我们需要通知 test1 以及 test2 这两个Vue实例进行视图的更新,我们只有通过收集依赖才能知道哪些地方依赖我的数据,以及数据更新时派发更新。那依赖收集是如何实现的?其中的核心思想就是“事件发布订阅模式”。接下来我们先介绍两个重要角色-- 订阅者 Dep和观察者 Watcher ,然后阐述收集依赖的如何实现的。

订阅者 Dep

1.为什么引入 Dep

收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。

于是我们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,说得具体点,它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。

2. Dep的简单实现

classDep{

constructor{

/* 用来存放Watcher对象的数组 */

this.subs = [];

}

/* 在subs中添加一个Watcher对象 */

addSub (sub) {

this.subs.push(sub);

}

/* 通知所有Watcher对象更新视图 */

notify {

this.subs.forEach((sub) =>{

sub.update;

})

}

}

以上代码主要做两件事情:

  • 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
  • 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。

所以当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify。调用也很简单:

let dp = newDep

dp.addSub(=>{

console.log('emit here')

})

dp.notify

  1.  

观察者 Watcher

1.为什么引入Watcher

Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释:

当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。

依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。形成如下所示的这样一个关系(图参考《剖析 Vue.js 内部运行机制》)。

2.Watcher的简单实现

classWatcher{

constructor(obj, key, cb) {

// 将 Dep.target 指向自己

// 然后触发属性的 getter 添加监听

// 最后将 Dep.target 置空

Dep.target = this

this.cb = cb

this.obj = obj

this.key = key

this.value = obj[key]

Dep.target = null

}

update {

// 获得新值

this.value = this.obj[this.key]

// 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图

this.cb(this.value)

}

}

以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行 update 函数。

收集依赖

所谓的依赖,其实就是Watcher。至于如何收集依赖,总结起来就一句话,在getter中收集依赖,在setter中触发依赖。先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了。

具体来说,当外界通过Watcher读取数据时,便会触发getter从而将Watcher添加到依赖中,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

最后我们对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码,实现了一个简易的数据响应式。

functionobserve(obj) {

// 判断类型

if(!obj || typeofobj !== 'object') {

return

}

Object.keys(obj).forEach(key=>{

defineReactive(obj, key, obj[key])

})

functiondefineReactive(obj, key, value) {

observe(value) // 递归子属性

letdp = newDep //新增

Object.defineProperty(obj, key, {

enumerable: true, //可枚举(可以遍历)

configurable: true, //可配置(比如可以删除)

get: functionreactiveGetter{

console.log('get', value) // 监听

// 将 Watcher 添加到订阅

if(Dep.target) {

dp.addSub(Dep.target) // 新增

}

returnvalue

},

set: functionreactiveSetter(newVal) {

observe(newVal) //如果赋值是一个对象,也要递归子属性

if(newVal !== value) {

console.log('set', newVal) // 监听

render

value = newVal

// 执行 watcher 的 update 方法

dp.notify //新增

}

}

})

}

}

classVue{

constructor(options) {

this._data = options.data;

observer(this._data);

/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */

newWatcher;

console.log('模拟视图渲染');

}

}

当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

总结

最后我们依照下图(参考《深入浅出vue.js》),再来回顾下整个过程:

  • 在 newVue 后, Vue 会调用 _init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter函数。
  • 当render function 执行的时候,因为会读取所需对象的值,所以会触发getter函数从而将Watcher添加到依赖中进行依赖收集。
  • 在修改对象的值的时候,会触发对应的 setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。
参考文章和书籍
  • 剖析 Vue.js 内部运行机制
  • 深入浅出Vue.js
  • Vue官方文档
  • 前端面试之道
  • 前端开发核心知识进阶
  • Java响应式的最通俗易懂的解释(译)

作者简介:浪里行舟,硕士研究生,专注于前端,运营有个人公众号前端工匠,致力于打造适合初中级工程师能够快速吸收的一系列优质文章。

声明:本文系作者投稿,转载请联系原作者。



Tags:Vue   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
尚硅谷前端研究院第 1 章:Vue 核心 Vue 简介官网英文官网: https://vuejs.org/中文官网: https://cn.vuejs.org/介绍与描述 动态构建用户界面的渐进式 JavaScript 框...【详细内容】
2021-08-26  Tags: Vue  点击:(115)  评论:(0)  加入收藏
学习成为一个更好的Vue开发者并不总是关于那些需要花时间和精力才能掌握的大概念。掌握一些技巧和窍门,可以让我们的编程生活变得更容易--没有大量重复的工作。在用 Vue 开发...【详细内容】
2021-07-26  Tags: Vue  点击:(105)  评论:(0)  加入收藏
基于优雅漂亮的 ant design 开发的管理后台,为数不多的好看 admin。关于 Antd Pro VueAntd Pro Vue 是一个企业级中后台前端/设计解决方案。在本站建站之初就推荐过 Ant Des...【详细内容】
2021-07-16  Tags: Vue  点击:(412)  评论:(0)  加入收藏
后面的模板是我们做后台管理系统经常所需要的东西。虽然,我们总可以花很多时间从头开始设计自己的模板,但有现在的模板让我们套,节省我们更多时间用来摸鱼,何乐而不为呢。这些现...【详细内容】
2021-04-27  Tags: Vue  点击:(473)  评论:(0)  加入收藏
为什么要从 Vue 转到 React,这篇文章为什么我们放弃了 Vue?不过对于大多数人来说,用 Vue 还是 React 不是自己说了算,多学一门技术不是坏处,而且 React 被大厂大量使用,要进入大厂...【详细内容】
2021-04-13  Tags: Vue  点击:(265)  评论:(0)  加入收藏
最近业务开发遇到了组织结构的展示,多级不固定,改了三板第一版用了echarts,第二版自己用递归组件写了发现很局限不灵活,内容确定的还好点不固定的就很乱了,期间也看了几个相关的...【详细内容】
2020-11-23  Tags: Vue  点击:(193)  评论:(0)  加入收藏
相信vue很多人都已经很熟悉了,利用脚手架很容易搭建一个vue项目 但项目多了以后每次部署测试环境就相当麻烦,还容易出错 所以趁这两天不忙,研究一下jenkins,也总算是入门了 jen...【详细内容】
2020-10-17  Tags: Vue  点击:(92)  评论:(0)  加入收藏
今天给小伙伴们分享一个高质量Avue大屏可视化模板AvueData。 avue-data 基于 vue+element-ui 二次封装的大屏可视化平台。提供2000多个模板库,屏幕自适应,支持自定义地图选择...【详细内容】
2020-08-31  Tags: Vue  点击:(4455)  评论:(0)  加入收藏
作者:小生方勤转发链接:https://mp.weixin.qq.com/s/bl5nHiRz7rGc5TbVbk-4rQ前言由于是工具,很可能你看到的时候有些工具包已经升级了,会有一些报错;这个你就需要自己探索了。工...【详细内容】
2020-08-26  Tags: Vue  点击:(65)  评论:(0)  加入收藏
作者: mcuking转发连接:https://mp.weixin.qq.com/s/y_gPdEZ0lRdquxqRd_7kPQ前言项目地址:preload-routeshttps://github.com/micro-frontends-vue/preload-routesasync-route...【详细内容】
2020-08-13  Tags: Vue  点击:(66)  评论:(0)  加入收藏
▌简易百科推荐
本文分为三个等级自顶向下地分析了glibc中内存分配与回收的过程。本文不过度关注细节,因此只是分别从arena层次、bin层次、chunk层次进行图解,而不涉及有关指针的具体操作。前...【详细内容】
2021-12-28  linux技术栈    Tags:glibc   点击:(3)  评论:(0)  加入收藏
摘 要 (OF作品展示)OF之前介绍了用python实现数据可视化、数据分析及一些小项目,但基本都是后端的知识。想要做一个好看的可视化大屏,我们还要学一些前端的知识(vue),网上有很多比...【详细内容】
2021-12-27  项目与数据管理    Tags:Vue   点击:(2)  评论:(0)  加入收藏
程序是如何被执行的&emsp;&emsp;程序是如何被执行的?许多开发者可能也没法回答这个问题,大多数人更注重的是如何编写程序,却不会太注意编写好的程序是如何被运行,这并不是一个好...【详细内容】
2021-12-23  IT学习日记    Tags:程序   点击:(9)  评论:(0)  加入收藏
阅读收获✔️1. 了解单点登录实现原理✔️2. 掌握快速使用xxl-sso接入单点登录功能一、早期的多系统登录解决方案 单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器...【详细内容】
2021-12-23  程序yuan    Tags:单点登录(   点击:(8)  评论:(0)  加入收藏
下载Eclipse RCP IDE如果你电脑上还没有安装Eclipse,那么请到这里下载对应版本的软件进行安装。具体的安装步骤就不在这赘述了。创建第一个标准Eclipse RCP应用(总共分为六步)1...【详细内容】
2021-12-22  阿福ChrisYuan    Tags:RCP应用   点击:(7)  评论:(0)  加入收藏
今天想简单聊一聊 Token 的 Value Capture,就是币的价值问题。首先说明啊,这个话题包含的内容非常之光,Token 的经济学设计也可以包含诸多问题,所以几乎不可能把这个问题说的清...【详细内容】
2021-12-21  唐少华TSH    Tags:Token   点击:(10)  评论:(0)  加入收藏
实现效果:假如有10条数据,分组展示,默认在当前页面展示4个,点击换一批,从第5个开始继续展示,到最后一组,再重新返回到第一组 data() { return { qList: [], //处理后...【详细内容】
2021-12-17  Mason程    Tags:VUE   点击:(14)  评论:(0)  加入收藏
什么是性能调优?(what) 为什么需要性能调优?(why) 什么时候需要性能调优?(when) 什么地方需要性能调优?(where) 什么时候来进行性能调优?(who) 怎么样进行性能调优?(How) 硬件配...【详细内容】
2021-12-16  软件测试小p    Tags:性能调优   点击:(20)  评论:(0)  加入收藏
Tasker 是一款适用于 Android 设备的高级自动化应用,它可以通过脚本让重复性的操作自动运行,提高效率。 不知道从哪里听说的抖音 app 会导致 OLED 屏幕烧屏。于是就现学现卖,自...【详细内容】
2021-12-15  ITBang    Tags:抖音防烧屏   点击:(25)  评论:(0)  加入收藏
11 月 23 日,Rust Moderation Team(审核团队)在 GitHub 上发布了辞职公告,即刻生效。根据公告,审核团队集体辞职是为了抗议 Rust 核心团队(Core team)在执行社区行为准则和标准上...【详细内容】
2021-12-15  InfoQ    Tags:Rust   点击:(25)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条