/template>
要在浏览器中打断点,需要在浏览器的source面板中打开index.vue
文件,然后才能给代码打上断点。
那么第二个问题来了,如何在source面板中找到我们这里的index.vue
文件呢?
很简单,像是在vscode中一样使用command+p
(windows中应该是control+p)就可以唤起一个输入框。在输入框里面输入index.vue
,然后点击回车就可以在source面板中打开index.vue
文件。如下图:![](http://mmbiz.qpic.cn/mmbiz_png/8hhrUONQpFurs298SGqgic03Mb5j39UNmGOtBqs9t4VubDOjoDCv7VbfoxFS1TVhviarNQ4zjocviccPkx7IpAocQ/640?wx_fmt=png&from=appmsg)
然后我们就可以在浏览器中给const count = ref(0);
处打上断点了。
RefImpl
类
刷新页面此时断点将会停留在const count = ref(0);
代码处,让断点走进ref
函数中。在我们这个场景中简化后的ref
函数代码如下:
function ref(value) {
return createRef(value, false);
}
可以看到在ref
函数中实际是直接调用了createRef
函数。
接着将断点走进createRef
函数,在我们这个场景中简化后的createRef
函数代码如下:
function createRef(rawValue, shallow) {
return new RefImpl(rawValue, shallow);
}
从上面的代码可以看到实际是调用RefImpl
类new了一个对象,传入的第一个参数是rawValue
,也就是ref绑定的变量值,这个值可以是原始类型,也可以是对象、数组等。
接着将断点走进RefImpl
类中,在我们这个场景中简化后的RefImpl
类代码如下:
class RefImpl {
private _value: T
private _rawValue: T
constructor(value) {
this._rawValue = toRaw(value);
this._value = toReactive(value);
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
newVal = toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = toReactive(newVal);
triggerRefValue(this, 4, newVal);
}
}
}
从上面的代码可以看到RefImpl
类由三部分组成:constructor
构造函数、value
属性的getter
方法、value
属性的setter
方法。
RefImpl
类的constructor
构造函数
constructor
构造函数中的代码很简单,如下:
constructor(value) {
this._rawValue = toRaw(value);
this._value = toReactive(value);
}
在构造函数中首先会将toRaw(value)
的值赋值给_rawValue
属性中,这个toRaw
函数是vue暴露出来的一个API,他的作用是根据一个 Vue 创建的代理返回其原始对象。因为ref
函数不光能够接受普通的对象和原始类型,而且还能接受一个ref对象,所以这里需要使用toRaw(value)
拿到原始值存到_rawValue
属性中。
接着在构造函数中会执行toReactive(value)
函数,将其执行结果赋值给_value
属性。toReactive
函数看名字你应该也猜出来了,如果接收的value是原始类型,那么就直接返回value。如果接收的value不是原始类型(比如对象),那么就返回一个value转换后的响应式对象。这个toReactive
函数我们在下面会讲。
_rawValue
属性和_value
属性都是RefImpl
类的私有属性,用于在RefImpl
类中使用的,而暴露出去的也只有value
属性。
经过constructor
构造函数的处理后,分别给两个私有属性赋值了:
_rawValue
中存的是ref绑定的值的原始值。
如果ref绑定的是原始类型,比如数字0,那么_value
属性中存的就是数字0。
如果ref绑定的是一个对象,那么_value
属性中存的就是绑定的对象转换后的响应式对象。
RefImpl
类的value
属性的getter
方法
我们接着来看value
属性的getter
方法,代码如下:
get value() {
trackRefValue(this);
return this._value;
}
当我们对ref的value属性进行读操作时就会走到getter
方法中。
我们知道template经过编译后会变成render函数,执行render函数会生成虚拟DOM,然后由虚拟DOM生成真实DOM。
在执行render函数期间会对count
变量进行读操作,所以此时会触发count
变量的value
属性对应的getter
方法。
在getter
方法中会调用trackRefValue
函数进行依赖收集,由于此时是在执行render函数期间,所以收集的依赖就是render函数。
最后在getter
方法中会return返回_value
私有属性。
RefImpl
类的value
属性的setter
方法
我们接着来看value
属性的setter
方法,代码如下:
set value(newVal) {
newVal = toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = toReactive(newVal);
triggerRefValue(this, 4, newVal);
}
}
当我们对ref的value的属性进行写操作时就会走到setter
方法中,比如点击count++
按钮,就会对count
的值进行+1
,触发写操作走到setter
方法中。
给setter
方法打个断点,点击count++
按钮,此时断点将会走到setter
方法中。初始化count
的值为0,此时点击按钮后新的count
值为1,所以在setter
方法中接收的newVal
的值为1。如下图:![](http://mmbiz.qpic.cn/mmbiz_png/8hhrUONQpFurs298SGqgic03Mb5j39UNmThpme9ibBycSSzmuwr2NV4odMs0LEtXJicofUzPxZhU8AU03SeQmBgYA/640?wx_fmt=png&from=appmsg)
从上图中可以看到新的值newVal
的值为1,旧的值this._rawValue
的值为0。然后使用if (hasChanged(newVal, this._rawValue))
判断新的值和旧的值是否相等,hasChanged
的代码也很简单,如下:
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);
Object.is
方法大家平时可能用的比较少,作用也是判断两个值是否相等。和==
的区别为Object.is
不会进行强制转换,其他的区别大家可以参看mdn上的文档。
使用hasChanged
函数判断到新的值和旧的值不相等时就会走到if语句里面,首先会执行this._rawValue = newVal
将私有属性_rawValue
的值更新为最新值。接着就是执行this._value = toReactive(newVal)
将私有属性_value
的值更新为最新值。
最后就是执行triggerRefValue
函数触发收集的依赖,前面我们讲过了在执行render函数期间由于对count
变量进行读操作。触发了getter
方法,在getter
方法中将render函数作为依赖进行收集了。
所以此时执行triggerRefValue
函数时会将收集的依赖全部取出来执行一遍,由于render函数也是被收集的依赖,所以render函数会重新执行。重新执行render函数时从count
变量中取出的值就是新值1,接着就是生成虚拟DOM,然后将虚拟DOM挂载到真实DOM上,最终在页面上count
变量绑定的值已经更新为1了。
看到这里你是不是以为关于ref实现响应式已经完啦?
我们来看demo中的第二个例子,user
对象,回顾一下在template和script中关于user
对象的代码如下:
<div>
<p>user.count的值为:{{ user.count }}p>
<button @click="user.count++">user.count++button>
div>
</template>
function reactive(target) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
);
}
从上面的代码可以看到在reactive
函数中是直接返回了createReactiveObject
函数的调用,第三个参数是mutableHandlers
。从名字你可能猜到了,他是一个Proxy对象的处理器对象,后面会讲。
接着将断点走进createReactiveObject
函数,在我们这个场景中简化后的代码如下:
function createReactiveObject(
target,
isReadonly2,
baseHandlers,
collectionHandlers,
proxyMap
) {
const proxy = new Proxy(target, baseHandlers);
return proxy;
}
在上面的代码中我们终于看到了大名鼎鼎的Proxy
了,这里new了一个Proxy
对象。new的时候传入的第一个参数是target
,这个target
就是我们一路传进来的ref绑定的对象。第二个参数为baseHandlers
,是一个Proxy对象的处理器对象。这个baseHandlers
是调用createReactiveObject
时传入的第三个参数,也就是我们前面讲过的mutableHandlers
对象。
在这里最终将Proxy代理的对象进行返回,我们这个demo中ref绑定的是一个名为user
的对象,经过前面讲过函数的层层return后,user.value
的值就是这里return返回的proxy
对象。
当我们对user.value
响应式对象的属性进行读操作时,就会触发这里Proxy的get拦截。
当我们对user.value
响应式对象的属性进行写操作时,就会触发这里Proxy的set拦截。
get
和set
拦截的代码就在mutableHandlers
对象中。
Proxy
的set
和get
拦截
在源码中使用搜一下mutableHandlers
对象,看到他的代码是这样的,如下:
const mutableHandlers = new MutableReactiveHandler();
从上面的代码可以看到mutableHandlers
对象是使用MutableReactiveHandler
类new出来的一个对象。
我们接着来看MutableReactiveHandler
类,在我们这个场景中简化后的代码如下:
class MutableReactiveHandler extends
BaseReactiveHandler {
set(target, key, value, receiver) {
let oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (target === toRaw(receiver)) {
if (hasChanged(value, oldValue)) {
trigger(target, "set", key, value, oldValue);
}
}
return result;
}
}
在上面的代码中我们看到了set
拦截了,但是没有看到get
拦截。
MutableReactiveHandler
类是继承了BaseReactiveHandler
类,我们来看看BaseReactiveHandler
类,在我们这个场景中简化后的BaseReactiveHandler
类代码如下:
class BaseReactiveHandler {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, "get", key);
return res;
}
}
在BaseReactiveHandler
类中我们找到了get
拦截,当我们对Proxy代理返回的对象的属性进行读操作时就会走到get
拦截中。
前面讲过了经过层层return后user.value
的值就是这里的proxy
响应式对象,而我们在template中使用user.count
将其渲染到p标签上,在template中读取user.count
,实际就是在读取user.value.count
的值。
同样的template经过编译后会变成render函数,执行render函数会生成虚拟DOM,然后将虚拟DOM转换为真实DOM渲染到浏览器上。在执行render函数期间会对user.value.count
进行读操作,所以会触发BaseReactiveHandler
这里的get
拦截。
在get
拦截中会执行track(target, "get", key)
函数,执行后会将当前render函数作为依赖进行收集。到这里依赖收集的部分讲完啦,剩下的就是依赖触发的部分。
我们接着来看MutableReactiveHandler
,他是继承了BaseReactiveHandler
。在BaseReactiveHandler
中有个get
拦截,而在MutableReactiveHandler
中有个set
拦截。
当我们点击user.count++
按钮时,会对user.value.count
进行写操作。由于对count
属性进行了写操作,所以就会走到set
拦截中,set
拦截代码如下:
class MutableReactiveHandler extends BaseReactiveHandler {
set(target, key, value, receiver) {
let oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (target === toRaw(receiver)) {
if (hasChanged(value, oldValue)) {
trigger(target, "set", key, value, oldValue);
}
}
return result;
}
}
我们先来看看set
拦截接收的4个参数,第一个参数为target
,也就是我们proxy代理前的原始对象。第二个参数为key
,进行写操作的属性,在我们这里key
的值就是字符串count
。第三个参数是新的属性值。