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
深入浅出Vue响应式原理 #70
Comments
有没有相对完整的闭环代码,这部分的new Watcher是会报错的。 |
收集视图依赖了哪些数据 我觉得改成 数据被哪些视图依赖 会更好些 因为本身data对象的下会对每个属性做监听 get的时候将watcher push到Dep中 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
前言
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。----官方文档
本文将针对响应式原理做一个详细介绍,并且带你实现一个基础版的响应式系统。本文的代码请猛戳Github博客
什么是响应式
我们先来看个例子:
上例中当price 发生变化的时候,Vue就知道自己需要做三件事情:
数据发生变化后,会重新对页面渲染,这就是Vue响应式,那么这一切是怎么做到的呢?
想完成这个过程,我们需要:
对应专业俗语分别是:
如何侦测数据的变化
首先有个问题,在Javascript中,如何侦测一个对象的变化?
其实有两种办法可以侦测到变化:使用
Object.defineProperty
和ES6的Proxy
,这就是进行数据劫持或数据代理。这部分代码主要参考珠峰架构课。方法1.Object.defineProperty实现
Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
上面这段代码的主要作用在于:
observe
这个函数传入一个obj
(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过defineReactive
处理,以此来达到实现侦测对象变化。值得注意的是,observe
会进行递归调用。那我们如何侦测Vue中
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
不能监听数组的变化,需要进行数组方法的重写,具体代码如下:这种方法将数组的常用方法进行重写,进而覆盖掉原生的数组方法,重写之后的数组方法需要能够被拦截。但有些数组操作Vue时拦截不到的,当然也就没办法响应,比如:
ES6提供了元编程的能力,所以有能力拦截,Vue3.0可能会用ES6中Proxy 作为实现数据代理的主要方式。
方法2.Proxy实现
Proxy
是 JavaScript 2015 的一个新特性。Proxy
的代理是针对整个对象的,而不是对象的某个属性,因此不同于Object.defineProperty
的必须遍历对象每个属性,Proxy
只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外**Proxy
支持代理数组的变化。**以上代码不仅精简,而且还是实现一套代码对对象和数组的侦测都适用。不过
Proxy
兼容性不太好!为什么要收集依赖
我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如第一例子中,模板中使用了price 数据,当它发生变化时,要向使用了它的地方发送通知。那如果多个Vue实例中共用一个变量,如下面这个例子:
如果我们执行下面这条语句:
此时我们需要通知 test1 以及 test2 这两个Vue实例进行视图的更新,我们只有通过收集依赖才能知道哪些地方依赖我的数据,以及数据更新时派发更新。那依赖收集是如何实现的?其中的核心思想就是“事件发布订阅模式”。接下来我们先介绍两个重要角色-- 订阅者 Dep和观察者 Watcher ,然后阐述收集依赖的如何实现的。
订阅者 Dep
1.为什么引入 Dep
收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。
于是我们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,说得具体点,它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
2. Dep的简单实现
以上代码主要做两件事情:
所以当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify。调用也很简单:
观察者 Watcher
1.为什么引入Watcher
Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释:
当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。形成如下所示的这样一个关系(图参考《剖析 Vue.js 内部运行机制》)。
2.Watcher的简单实现
以上就是 Watcher 的简单实现,在执行构造函数的时候将
Dep.target
指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行update
函数。收集依赖
所谓的依赖,其实就是Watcher。至于如何收集依赖,总结起来就一句话,**在getter中收集依赖,在setter中触发依赖。**先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了。
具体来说,当外界通过Watcher读取数据时,便会触发getter从而将Watcher添加到依赖中,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
最后我们对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码,实现了一个简易的数据响应式。
当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。
总结
最后我们依照下图(参考《深入浅出vue.js》),再来回顾下整个过程:
new Vue()
后, Vue 会调用_init
函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter
函数,而在当被赋值的时候会执行setter
函数。getter
函数从而将Watcher添加到依赖中进行依赖收集。setter
,setter
通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用update
来更新视图。参考文章和书籍
The text was updated successfully, but these errors were encountered: