通俗易懂的Vue响应式原理以及依赖收集
qiyuwang 2024-11-04 15:15 8 浏览 0 评论
作者:hello等风来
转发链接:https://juejin.im/post/5efd3282e51d4534c45511e3
前言
最近在看一些底层方面的知识。所以想做个系列尝试去聊聊这些比较复杂又很重要的知识点。学习就好比是座大山,只有自己去登山,才能看到不一样的风景,体会更加深刻。今天我们就来聊聊Vue中比较重要的响应式原理以及依赖收集。
响应式原理
Object.defineProperty() 和 Proxy 对象,都可以用来对数据的劫持操作。何为数据劫持呢?就是在我们访问或者修改某个对象的某个属性的时候,通过一段代码进行拦截,然后进行额外的操作,返回结果。vue中双向数据绑定就是一个典型的应用。
Vue2.x 是使用 Object.defindProperty(),来实现对对象的监听。
Vue3.x 版本之后就改用Proxy实现。
在MDN中是这样定义:
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor)
- obj:要定义属性的对象
- prop:要定义或修改的属性名称
- descriptor:要定义或修改的属性描述符(configurable: 可改变的;writable:可写的;enumerable:可枚举的;get\set:设置或获取对象的某个属性的值)
const data = {}
const name = 'zhangsan'
Object.defineProperty(data, 'name', {
writable: true,
configurable: true,
get: function () {
console.log('get')
return name
},
set: function (newVal) {
console.log('set')
name = newVal
}
})
复制代码
当把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。简单理解就是在data和用户之间做了一层代理中间层,在vue initData的时候,将_data上面的数据代理到vm上,通过observer类将所有的data变成可观察的,及对data定义的每一个属性进行getter\setter操作,这就是Vue实现响应式的基础。
Vue数据响应式变化主要涉及 Observer, Watcher , Dep 这三个主要的类。因此要弄清Vue响应式变化需要明白这个三个类之间是如何运作联系的;以及它们的原理,负责的逻辑操作。
响应式原理(Observer)
Observer类是将每个目标对象(即data)的键值转换成getter/setter形式,用于进行依赖收集以及调度更新。那么在vue这个类是如何实现的:
- 1、observer实例绑定在data的ob属性上面,防止重复绑定;
- 2、若data为数组,先实现对应的变异方法(Vue重写了数组的7种原生方法)再将数组的每个成员进行observe,使之成响应式数据;
- 3、否则执行walk()方法,遍历data所有的数据,进行getter/setter绑定。这里的核心方法就是 defineReative(obj, keys[i], obj[keys[i]])
// 监听对象属性Observer类
class Observer {
constructor(value) {
this.value = value
if (!value || (typeof value !== 'object')) {
return
} else {
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
复制代码
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return val
},
set: function reactiveSetter(newVal) {
// 注意:value一直在闭包中,此处设置完之后,再get时也是会得到最新的值
if (newVal === val) return
updateView()
}
})
}
function updateView() {
console.log('视图更新了')
}
const data = {
name: 'zhangsan',
age: 20
}
new Observer(data)
data.name = 'lisi' // 打印‘视图更新了’
复制代码
这就是简单的一个Observer类,这也是vue响应式的基本原理。但我们都知道 object.defineproperty的存在一些缺点:
1、对于复杂的对象需要深度监听,回归到底,一次性计算量大
2、无法监听新增属性/删除属性(Vue.set Vue.delete)
3、无法监听数组,需特殊处理,也就是上面说的变异方法
这也就是vue3改进的一方面,后文我们也会着重讲解vue3 proxy如何做响应式的。
扩展一、vue如何深度监听
上图中我们看到data中的一级目录name、age在值改变的时候,会触发视图更新,但在我们实际开发过程中,data可能会是比较复杂的对象,嵌套了好几层:
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京'
}
}
data.info.address = '上海' // 并没有执行。
复制代码
造成这种原因是,代码中defineReactive接收到的val是一个对象,为了避免这种复杂的对象vue采用递归的思想在defineReactive函数中再执行一次observer函数就行,递归将对象在遍历一次获取key/value值,new Observer(val)。同样在设置值的时候可能会把name也设置成一个对象,因此在data值更新的时候也需要进行判断深度监听
function defineReactive(obj, key, val) {
new Observer(val) // 深度监听
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return val
},
set: function reactiveSetter(newVal) {
// 注意:value一直在闭包中,此处设置完之后,再get时也是会得到最新的值
if (newVal === val) return
new Observer(val) // 深度监听
updateView()
}
})
}
复制代码
扩展二、vue数组的监听
object.defineproperty对数组是不起作用的,那么在vue中又是如何去监听数组的变化,其实Vue 将被侦听的数组的变更方法进行了包裹。接下来将用简单代码演示:
// 防止全局污染,重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向oldArrayProperty
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () { // 在定义数组的方法
updateView()
oldArrayProperty[methodName].call(this, ...arguments) // 实际执行数组的方法
}
})
// 在Observer函数中对数组进行处理
if (Array.isArray(value)) {
value.__proto__ = arrProto
}
复制代码
从代码中看到,在Observer函数有一层对数组进行拦截,将数组的__proto__指向了一个arrProto,arrProto是一个对象,这个对象指向数组的原型,因此arrProto拥有了数组原型上的方法,然后在这对象上重新自定义了数组的7种方法将其包裹,但又不会影响数组原型的方法,这就是变异,再将数组的每个成员进行observe,使之成响应式数据。
依赖收集(Watcher、Dep)
我们现在有这么一个Vue对象
new Vue({
template:
`<div>
<span>text1:</span> {{text1}}
<div>`,
data: {
text1: 'text1',
text2: 'text2'
}
})
复制代码
我们可以从以上代码看出,data中text2并没有被模板实际用到,为了提高代码执行效率,我们没有必要对其进行响应式处理,因此,依赖收集简单理解就是收集只在实际页面中用到的data数据,那么Vue是如何进行依赖收集的,这也就是下面要讲的Watcher、Dep类了。
被Observer的data在触发 getter 时,Dep 就会收集依赖,然后打上标记,这里就是标记为Dep.target
Watcher是一个观察者对象。依赖收集以后的watcher对象被保存在Dep的subs中,数据变动的时候Dep会通知watcher实例,然后由watcher实例回调cb进行视图更新。
Watcher可以接受多个订阅者的订阅,当有data变动时,就会通过 Dep 给 Watcher 发通知进行更新。
我们可以用一些简单的代码去实现这个过程。
class Observer {
constructor(value) {
this.value = value
if (!value || (typeof value !== 'object')) {
return
} else {
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
// 订阅者Dep,存放观察者对象
class Dep {
constructor() {
this.subs = []
}
/*添加一个观察者对象*/
addSub (sub) {
this.subs.push(sub)
}
/*依赖收集,当存在Dep.target的时候添加观察者对象*/
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有watcher对象更新视图
notify () {
this.subs.forEach((sub) => {
sub.update()
})
}
}
class Watcher {
constructor() {
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
Dep.target = this;
}
update () {
console.log('视图更新啦')
}
/*添加一个依赖关系到Deps集合中*/
addDep (dep) {
dep.addSub(this)
}
}
function defineReactive (obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
dep.depend() /*进行依赖收集*/
return val
},
set: function reactiveSetter (newVal) {
if (newVal === val) return
dep.notify()
}
})
}
class Vue {
constructor (options) {
this._data = options.data
new Observer(this._data) // 所有data变成可观察的
new Watcher() // 创建一个观察者实例
console.log('render~', this._data.test)
}
}
let o = new Vue({
data: {
test: 'hello vue.'
}
})
o._data.test = 'hello mvvm!'
Dep.target = null
复制代码
总结
- 1、在Vue中模版编译过程中的指令或者数据绑定都会实例化一个Watcher实例,实例化过程中会触发get()将自身指向Dep.target;
- 2、data在Observer时执行getter会触发dep.depend()进行依赖收集,
- 3、当data中被 Observer的某个对象值变化后,触发subs中观察它的watcher执行 update() 方法,最后实际上是调用watcher的回调函数cb,进而更新视图。
Vue3-Proxy实现响应式
Proxy可以理解成在目标对象前架设一个拦截层,外界对该对象的出发必须先通过这层拦截层,因此提供了一种机制可以对外界的访问进行过滤和改写。
function reactive(value = {}) {
if (!value || (typeof value !== 'object')) {
return
}
// 代理配置
const proxyConf = {
get(target, key,receiver) {
// 只处理非原型的属性
let ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key)
}
const result = Reflect.get(target, key, receiver)
// 深度监听
// 性能如何提升? 什么时候用什么时候递归
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据不处理
const oldVal = target[key]
if (val === oldVal) return true
const ownKey = Reflect.ownKeys(target)
if (ownKeys.include(key)) {
console.log('已有的key', key)
} else {
console.log('新增的key', key)
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
return result
}
}
// 生成代理对象
const observed = new Proxy(value, proxyConf)
return observed
}
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京'
},
num: [1, 2, 3]
}
const proxyData = reactive(data)
proxyData.name ='lisi' // set name lisi
复制代码
proxy深度监听的性能提升,在proxy中对于复杂的对象,只会geter()的时候对当前层的监听,比如说在info中
info: {
address: '北京',
a: {
b: {
c: {
d: 2
}
}
}
}
复制代码
修改proxyData.info.a并不会把后面b、c、d递归出来,避免了object.defineProperty一次性全部递归计算完成。由于proxy原生对数组就能监听,所以也是对object.defineProperty缺点的一个改进。并且从代码中可以看出,在增加/删除时proxy也一样可以监听到,这就是proxy的优势。
扩展一、Reflect
reflect对象的方法和proxy对象的方法一一对应,只要是proxy对象的方法,就能在reflect对象找到对应的方法。这就使得proxy对象可以方便的调用对应的reflect方法来完成默认的行为,作为修改行为的基础。
Reflect有其实是对Object对象的规范化吧,将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty)放到Reflect对象上。
Reflect.get(target, name, receiver): 查到并返回target对象上的name属性,没有该属性会返回undefined
Reflect.set(target, name, value, receiver): 设置target对象的name属性等于value
Reflect.has(object, name): 判断对象上是否有name属性
Reflect.ownKeys(target): 返回对象的所有属性
扩展二、使用proxy实现观察者模式
// 观察者模式指的是函数自动观察数据对象的模式,一旦数据有变化,数据就会自动执行
const queuedObservers = new Set()
const observe = fn => queuedObservers.add(fn)
const observable = obj => new Proxy(obj, {set})
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
queuedObservers.forEach(observe => observe())
return result
}
const person = observable({ // 观察对象
name: '张三',
age: 20
})
function print() { // 观察者
console.log(`${person.name}, ${person.age}`)
}
observe(print)
person.name = '李四'
推荐Vue学习资料文章:
《一文带你搞懂vue/react应用中实现ssr(服务端渲染)》
《教你Vue3 Compiler 优化细节,如何手写高性能渲染函数(上)》
《教你Vue3 Compiler 优化细节,如何手写高性能渲染函数(下)》
《Deno将停止使用TypeScript,并公布五项具体理由》
《为什么Vue3.0不再使用defineProperty实现数据监听?》
《如何写出优秀后台管理系统?11个经典模版拿去不谢「干货」》
《一个由 Vue 作者尤雨溪开发的 web 开发工具—vite》
《提高10倍打包速度工具Snowpack 2.0正式发布,再也不需要打包器》
《大厂Code Review总结Vue开发规范经验「值得学习」》
《带你了解 vue-next(Vue 3.0)之 炉火纯青「实践」》
《「干货」Vue+高德地图实现页面点击绘制多边形及多边形切割拆分》
《细品pdf.js实践解决含水印、电子签章问题「Vue篇」》
《Vue仿蘑菇街商城项目(vue+koa+mongodb)》
《基于 electron-vue 开发的音乐播放器「实践」》
《「实践」Vue项目中标配编辑器插件Vue-Quill-Editor》
《「干货」Deno TCP Echo Server 是怎么运行的?》
《「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台》
《「实践」深入对比 Vue 3.0 Composition API 和 React Hooks》
《前端网红框架的插件机制全梳理(axios、koa、redux、vuex)》
《深入学习Vue的data、computed、watch来实现最精简响应式系统》
《10个实例小练习,快速入门熟练 Vue3 核心新特性(一)》
《10个实例小练习,快速入门熟练 Vue3 核心新特性(二)》
《教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」》
《尤大大细品VuePress搭建技术网站与个人博客「实践」》
《是什么导致尤大大选择放弃Webpack?【vite 原理解析】》
《带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】》
《带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】》
《一篇文章教你并列比较React.js和Vue.js的语法【实践】》
《深入浅出通过vue-cli3构建一个SSR应用程序【实践】》
《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》
《【新消息】Vue 3.0 Beta 版本发布,你还学的动么?》
《Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5》
《深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】》
《手把手教你深入浅出vue-cli3升级vue-cli4的方法》
《Vue 3.0 Beta 和React 开发者分别杠上了》
《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》
《Vue3 尝鲜》
《2020 年,Vue 受欢迎程度是否会超过 React?》
《手把手教你Vue解析pdf(base64)转图片【实践】》
《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》
《深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现》
《手把手教你D3.js 实现数据可视化极速上手到Vue应用》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】》
作者:hello等风来
转发链接:https://juejin.im/post/5efd3282e51d4534c45511e3
相关推荐
- 在Word中分栏设置页码一页两个页码的技巧!
-
施老师:在正常情况下,Word文档中一页只会出现一个页码。但在某种情况下,比如说:用了分栏后,我们希望一页中出现两个页码,那应该如何实现呢?今天,就由宁双学好网施老师来为大家讲一下,利用域来实现一页两...
- 如何在关键时刻向上自荐(如何在关键时刻做出正确选择)
-
抓住机会,挺身而出有种时刻叫“关键时刻”,关键时刻,作为一个认为自己有能力的、训练有素的人,应该考虑挺身而出,甚至应该不考虑就挺身而出。...
- WPS Word:跨页的文档表格,快速调整为一页。#Excel
-
如何快速将跨页的文档表格调整为一页?需要根据两种情况分别处理。如果表格所有行的行高相同,调整为一页的方法有两种。第一种方法是将光标移动到表格内,然后将鼠标移动到表格右下角的方框处,按住鼠标左键向上拖动...
- word文档插入下一页分节符(word下一页分页符)
-
在word文档中,对文档页面进行分页是特别常见的操作,其中的下一页分节符也是用得比较多的,但是一些人不太清楚在哪里设置,也不知道它具体能实现的功能是什么。接下来看看如何在word文档中插入下一页分节符...
- word文档如何设置某一页纸张的方向
-
word文档页面方向有横向和纵向,纵向是默认的纸张方向,有时我们需要将页面设置为横向,或只设置其中某一页方向,应该怎么操作呢?一起来看看下面的详细介绍第一步:...
- word怎么单独设置一页为横向(word2019怎样设置单独一页为横向)
-
word里面其中一页可以改为横向的吗?经过实际操作发现是完全可以的。...
- Word如何设置分栏,如何一页内容同时显示一栏和两栏
-
我们使用Word文档,有时需要用到两栏的排版,甚至一页内容同时包含一栏和两栏的排版,这种格式怎么设置呢?具体步骤如下:首先是两栏排版的设置,直接点击Word文件上方工具栏【布局】,选择【分栏】下面的【...
- Word怎么分页?这三个方法可以帮到你
-
我们不仅可以利用Word编辑文档,还可以编辑文集呢。但是有时候会出现两个部分的文章长短不一,我们需要对文档进行分页处理。这样可以方便我们对文档进行其他操作。那么Word怎么分页呢?大家可以采用下面这...
- Word内容稍超一页,如何优化至单页打印?
-
如何将两页纸的内容,缩到一页打印呢?有时候一页纸多一点内容,我们完全可以缩一下,放到一页来打印。...
- [word] word 表格如何跨行显示表头、标题
-
word表格如何跨行显示表头、标题在Word中的表格如果过长的话,会跨行显示在另一页,如果想要在其它页面上也显示表头,更直观的查看数据。难道要一个个复制表头吗?当然不是,教你简单的方法操作设置Wo...
- Word表格跨页如何续上表?(word如何让表格跨页不断掉)
-
长文档的表格跨页时,你会发现页末空白太多了,这时要怎么调整?选中整张表格,右击【表格属性】,点击【行】选项,之后勾选【允许跨页断行】,点击确定即可解决空白问题。...
- Word怎么连续自动生成页码,操作步骤来了!
-
Word怎么连续自动生成页码,操作步骤来了!...
- word文档怎么把两页合并成一页内容?教你4种方法
-
word怎么把两页合并成一页?word怎么把两页合并成一页?用四种方法演示一下。·方法一:把这一个文档合并成一页,按ctrl加a全选文档,然后右键点击段落,弹出的界面行距改成固定值,磅值可以改小一点,...
- 如何将Word中的一页的纸张方向设置为横向?这里提供详细步骤
-
默认情况下,MicrosoftWord将页面定向为纵向视图。虽然这在大多数情况下都很好,但你可能拥有在横向视图中看起来更好的页面或页面组。以下是实现这一目标的两种方法。无论使用哪种方法,请注意,如果...
- Word横竖混排你会玩吗?(word横排竖排混合)
-
我们在用Word排版的时候,一般都是竖版格式,但偶尔会需要到一些特殊的版式要求,比如文档中插入的一个表格,横向的内容比较多,这时就需要用到横版,否则表格显示不全。这种横竖版混排的要求,在Word20...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- navicat无法连接mysql服务器 (65)
- 下横线怎么打 (71)
- flash插件怎么安装 (60)
- lol体验服怎么进 (66)
- ae插件怎么安装 (62)
- yum卸载 (75)
- .key文件 (63)
- cad一打开就致命错误是怎么回事 (61)
- rpm文件怎么安装 (66)
- linux取消挂载 (81)
- ie代理配置错误 (61)
- ajax error (67)
- centos7 重启网络 (67)
- centos6下载 (58)
- mysql 外网访问权限 (69)
- centos查看内核版本 (61)
- ps错误16 (66)
- nodejs读取json文件 (64)
- centos7 1810 (59)
- 加载com加载项时运行错误 (67)
- php打乱数组顺序 (68)
- cad安装失败怎么解决 (58)
- 因文件头错误而不能打开怎么解决 (68)
- js判断字符串为空 (62)
- centos查看端口 (64)