本文已参与[新人创作礼]活动,一起开展掘金创作之路。
我们想要实现响应式,需要这三步:
-
拦截对数据的访问与更改 (数据劫持)
-
知道哪里用到了数据 (依赖收集)
-
当数据发生改变时,怎么自动通知用到数据的地方,让它们自动更新 (发布订阅模式)
数据:指响应式数据
第一步 : 怎么拦截对数据的访问与更改?
Q : 为什么我们需要拦截对数据的访问与更改?
主要是有两个理由:
- 我们怎么知道哪里用到了数据?
- 当数据改变时,我们怎么通知用到这些数据的地方自动更新? (没错,就是第二步和第三步)
一般来说,我们定义的数据只会被用到它的地方访问 ,比如:页面,或者需要用这些数据经过某些计算得到的数据。
假如我们可以拦截对数据的访问,那么一旦有人访问了这些数据,就自动调用我们事先定义好的函数(getter)来处理一些事情(比如依赖收集) ,
同理,如果我们可以拦截对数据的更改,那么一旦有人修改了这些数据,就自动调用我们事先定义好的函数(setter)来通知用到这些数据的地方进行更新
Q : 我们具体怎么拦截对数据的访问与更改?
在JS中有两种办法可以拦截对数据的访问与更改
-
方法一 :
Object.defineProperty -
方法二 :
Proxy-
data,reactive,ref函数定义的对象内部是通过Proxy来拦截对数据的访问与更改 -
ref函数定义的基本数据是通过Object.defineProperty来拦截对数据的访问与更改 -
实际上就是利用
Object.defineProperty和Proxy来对数据(基本类型)加上getter,setter
-
这里不计较
Proxy与defineProperty的区别,前者更优
1. defineProperty函数
- 该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性。
- 语法:
Object.defineProperty(obj, prop, descriptor) obj:要添加或修改属性的对象prop:要添加或修改属性的名称descriptor:一个配置对象
descriptor对象的属性
-
value:prop属性的值,可以是任何有效的 JavaScript 值 默认为 undefined -
writable:控制prop是否可以被修改, 默认为false,即不能修改 -
configurable:控制prop是否可以遍历, 默认为false,即不能遍历 -
enumerable:控制prop是否可以被删除, 默认为false,即不能被删除 -
get:值为一个函数,默认为undefined -
getter什么时候被调用?:当prop属性被访问时调用并返回prop属性的值
getter是指get属性+其值(函数)
set:值为一个函数,默认为undefined,该函数接受一个参数,并将该参数的新值分配给该属性setter什么时候被调用?:当prop属性被修改时调用,把prop属性被修改之后的值传递给set函数的参数
setter是set属性+其值(函数)
<script>
let person = {
name:"小明",
age:18
}
let sex = "男"
Object.defineProperty(person, "sex", {
value:"男",
writable:true,
enumerable:true,
configurable:true,
get:function(){
return sex
},
set:function(value){
number = value
}
})
</script>
2. Proxy函数
- 用于创建一个对象的代理对象,然后通过操作这个代理对象来操作目标对象
- 语法:
const p = new Proxy(target, handler) target:要拦截的目标对象,handler:是一个对象,用来定制拦截行为
handlerd对象的属性
target:目标对象proKey:属性名value:属性值receiver:proxy 实例本身get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
一共用13中拦截行为:es6.ruanyifeng.com/#docs/proxy
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
3. 方法一拦截对数据的访问与更改(模拟)
- 假设我们要对
ref函数定义的数据进行数据劫持
const sex = ref(0)
- Vue利用方法一对
ref函数定义的所有属性(这里指基本类型的属性)加上getter,setter
这里的person假设代表存放所有响应式数据的对象
Object.defineProperty(person, "sex", {
value:"man",
writable:true,
enumerable:true,
configurable:true,
get:function(){
return sex
},
set:function(value){
number = value
}
})
4. 方法二拦截对数据的访问与更改
- 假设我们要对
reactive函数定义的响应式数据做数据劫持
ren = {
name: '哈哈',
age: 18,
person: {
sex:"hello"
}
}
- 利用方法二对
reactive函数返回的对象中的所有基本类型的属性加上getter,setter
-
如果属性是对象或者数组,那么递归调用方法二来对这个对象的所有基本类型属性加上
getter,setter -
data函数返回的对象实际上是通过reactive函数来弄的
reactive(ren)
reactive函数
function reactive(target) {
// 通过proxy将对象变为响应式
const observed = new Proxy(target, baseHandler);
// 返回proxy代理后的对象
return observed;
}
const baseHandler = {
get(target, key) {
// Reflect.get
const res = Reflect.get(target, key);
// @todo 依赖收集
// 尝试获取值obj.age,触发getter
track(target, key);
return typeof res === "object" ? reactive(res) : res;
},
set(target, key, val) {
const info = { oldValue: target[key], newValue: val };
// Reflect.set
// target[key] = val;
const res = Reflect.set(target, key, val);
// @todo 响应式去通知变化 触发执行,effect函数是响应式对象修改触发的
trigger(target, key, info);
},
};
第二步:依赖收集
依赖收集的核心思想就是:观察者模式
在观察者模式中
-
- 观察目标:用到的响应式数据
- 观察者:视图、计算属性、侦听器这些用到响应式数据的东西
依赖收集的时机
- 依赖是指观察者Watcher
- 只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个watcher收集到Dep中
依赖收集的原理
Vue源码中负责依赖收集的类有三个:
Dep: 扮演观察目标的角色Watcher: 扮演观察者的角色Observer: 扮演辅助的角色
三者的关系
Dep实际上就是对Watcher的管理
Dep是一个发布者,可以被多个观察者订阅,Dep中有一个subs存放依赖收集的观察者,以便在数据变更的时候通知所有的watcherDep和Observer的关系就是Observer监听整个data,遍历data的每个属性给每个属性加上getter和setter, 当getter被调用的时候往Dep类里塞依赖(dep.depend),当setter被调用的时候通知所有watcher进行update(dep.notify)Dep与Watcher的关系就是watcher中实例化了dep并向dep.subs中添加了订阅者,dep通过notify遍历了dep.subs通知每个watcher更新
data: 包括所有声明的响应式数据
Observer
defineReactive在gētter调用时进行依赖收集,这里就是Vue收集依赖的入口
将Observer类的实例挂在__ob__属性上,提供后期数据观察时使用,实例化Dep类实例,并且将对象/数组作为value属性保存下来
如果value是个对象,就执行walk()过程,遍历对象把每一项数据都变为可观测数据(调用defineReactive方法处理)
如果value是个数组,就执行observeArray()过程,递归地对数组元素调用observe()。
class Observer {
constructor(v){
// 每一个Observer实例身上都有一个Dep实例
this.dep = new Dep()
// 如果数据层次过多,需要递归去解析对象中的属性,依次增加set和get方法
def(v,'__ob__',this) //给数据挂上__ob__属性,表明已观测
if(Array.isArray(v)) {
// 把重写的数组方法重新挂在数组原型上
v.__proto__ = arrayMethods
// 如果数组里放的是对象,再进行监测
this.observerArray(v)
}else{
// 非数组就直接调用defineReactive将数据定义成响应式对象
this.walk(v)
}
}
observerArray(value) {
for(let i=0; i<value.length;i++) {
observe(value[i])
}
}
walk(data) {
let keys = Object.keys(data); //获取对象key
keys.forEach(key => {
defineReactive(data,key,data[key]) // 定义响应式对象
})
}
}
function defineReactive(data,key,value){
const dep = new Dep() //实例化dep,用于收集依赖,通知订阅者更新
observe(value) // 递归实现深度监测,注意性能
Object.defineProperty(data,key,{
configurable:true,
enumerable:true,
get(){
//获取值
// 如果现在处于依赖的手机阶段
if(Dep.target) {
dep.depend()
}
// 依赖收集
return value
},
set(newV) {
//设置值
if(newV === value) return
observe(newV) //继续劫持newV,用户有可能设置的新值还是一个对象
value = newV
console.log('值变化了:',value)
// 发布订阅模式,通知
dep.notify()
// cb() //订阅者收到消息回调
}
})
}
Dep
用来存放Watcher对象,每一个数据都有一个Dep类实例。
在一个项目中会有多个观察者,但由于JavaScript是单线程的,所以在同一时刻,只能有一个观察者在执行。此刻正在执行的那个观察者所对应的Watcher实例就会赋值给Dep.target这个变量,从而只要访问Dep.target就能知道当前的观察者是谁
var uid = 0
export default class Dep {
constructor() {
this.id = uid++
this.subs = [] // subscribes订阅者,存储订阅者,这里放的是Watcher的实例
}
//收集观察者
addSub(watcher) {
this.subs.push(watcher)
}
// 添加依赖
depend() {
// 自己指定的全局位置,全局唯一
//自己指定的全局位置,全局唯一,实例化Watcher时会赋值Dep.target = Watcher实例
if(Dep.target) {
this.addSub(Dep.target)
}
}
//通知观察者去更新
notify() {
console.log('通知观察者更新~')
const subs = this.subs.slice() // 复制一份
subs.forEach(w=>w.update())
}
}
Watcher
关心在数据变更之后能得获得通知,通过回调函数进行更新
由上面的Dep可知,Watcher需要实现以下两个功能:
dep.depend()的时候往subs里面添加自己dep.notify()的时候调用watcher.update(),进行更新
note:watcher有三种:render watcher(模板渲染版观察者)、 computed watcher(计算属性版观察者)、user watcher(监视属性版观察者)
var uid = 0
import {parsePath} from "../util/index"
import Dep from "./dep"
export default class Watcher{
constructor(vm,expr,cb,options){
this.vm = vm // 组件实例
this.expr = expr // 需要观察的表达式
this.cb = cb // 当被观察的表达式发生变化时的回调函数
this.id = uid++ // 观察者实例对象的唯一标识
this.options = options // 观察者选项
this.getter = parsePath(expr)
this.value = this.get()
}
get(){
// 依赖收集,把全局的Dep.target设置为Watcher本身
Dep.target = this
const obj = this.vm
let val
// 只要能找就一直找
try{
val = this.getter(obj)
} finally{
// 依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。
Dep.target = null
}
return val
}
// 当依赖发生变化时,触发更新
update() {
this.run()
}
run() {
this.getAndInvoke(this.cb)
}
getAndInvoke(cb) {
let val = this.get()
if(val !== this.value || typeof val == 'object') {
const oldVal = this.value
this.value = val
cb.call(this.target,val, oldVal)
}
}
}
要注意的是,watcher中有个sync属性,绝大多数情况下,watcher并不是同步更新的,而是采用异步更新的方式,也就是调用queueWatcher(this)推送到观察者队列当中,待nextTick的时候进行调用
总结
依赖收集
- 对
computed属性初始化时,触发computed watcher依赖收集 - 对
watch属性初始化时,触发user watcher依赖收集 render()时,触发render watcher依赖收集re-render时,render()再次执行,会移除所有subs中的watcer的订阅,重新赋值。
observe->walk->defineReactive->get->dep.depend()->
watcher.addDep(new Dep()) ->
watcher.newDeps.push(dep) ->
dep.addSub(new Watcher()) ->
dep.subs.push(watcher)
\
第三步:派发更新
- 在组件中对响应式数据进行了修改,触发了对应数据的
setter - 然后调用
dep.notify() - 最后遍历
subs数组(Watcher实例),调用每一个watcher的update方法
核心就是订阅者模式,Watcher 订阅 Dep,当数据发生变化时,Dep实例会遍历subs数组通知每个订阅的Watcher
代码逻辑
set ->
dep.notify() ->
subs[i].update() ->
watcher.run() || queueWatcher(this) ->
watcher.get() || watcher.cb ->
watcher.getter() ->
vm._update() ->
vm.__patch__()