前言
我依稀记得当初刚接触Vue时,一直分不清ref,toRef,toRefs,后来也只是囫囵吞枣知道该怎么用,直到最近在读霍春阳老师的Vue设计与实现,并看了部分源码,才知其原理,写出来给曾经的盲点画上一个句号。
ref
ref用来将原始值变为响应式,需要使用value去访问和修改值。(也可以是对象,不过不建议,因为内部还是会调用reactive)
什么是响应式呢?当某个值变化,涉及到这个值的函数会重新执行。
非响应式
<template>
<div>{{str}}</div>
</template>
<script>
import { onMounted,} from 'vue'
export default {
setup(){
let str = 'vue' //数据默认是非响应式的
onMounted(()=>{
setTimeout(()=>{
str = 'React' //str的值变化,视图并未更新
console.log(str);
},2000)
}
)
return{
str
}
}
}
</script>
响应式
setup(){
let str = ref('vue')
onMounted(()=>{
setTimeout(()=>{
str.value = 'React'
console.log(str.value);
},2000)
}
)
return{
str
}
}
手写一个ref
在src目录下创建一个文件夹utils,utils中创建myref.js
//myref.js
import { reactive } from "vue"
function myref(val){
const wrapper = {
value:val
}
Object.defineProperty(wrapper,'__v_isRef',{
value:true
})
//在wrapper身上添加一个属性为__v_isRef,值为true
//用于区分是 ref('vue')还是reactive({value:'vue'})
//这里埋下了一个问题,就是<div>{{str}}</div>为什么不用写成<div>{{str.value}</div>?后面解答。
return reactive(wrapper) //返回的也是一个对象 {value:...}
}
export default myref
用上我们自己的'ref'。
import myref from './utils/myref'
export default {
setup(){
let str = myref('vue')
onMounted(()=>{
setTimeout(()=>{
str.value = 'React'
console.log(str.value);
},2000)
}
)
return{
str
}
}
}
</script>
看看效果
reactive能将一个对象变为响应式,(下次再教大家手写一个reactive,这东西有点复杂。)
此处的原理就是将用ref包裹的值作为一个对象中value属性的值,用reactive将该对象变为响应式(reactive中用ES6的proxy代理了这个对象)。
<div>{{str}}</div>与<div>{{str.value}}</div>
根据ref的代码可知return{str},str是一个名为str的对象,属性为{value:''React}.
可以理解为:
return{
str:{
value:'React'
}
}
那我们在模板中不该是<div>str.value</div>来访问吗?
Vue官方为了减少用户的心智负担,对setup的中的return值做了处理(此处以js为例,不深入模板的解析)
我们来看一下怎么处理的
function proxyRefs(target){
return new Proxy(target,{
//Proxy接收两个参数,第一个是要代理的对象,第二个对为对第一个对象的操作。
get(target,key,receiver){
//get接收三个参数,第一个为代理对象,第二个为属性,第三个可以理解为函数调用中this的指向
const value = Reflect.get(target,key,receiver)
return value.__v_isRef ? value.value:value
//如果是ref,则将str的访问改为str.value
},
set(target,key,newValue,receiver){
const value = target[key]
if(value.__v_isRef){//如果是ref,则将对str的修改改为对str.value的修改
value.value = newValue
return true
}
return Reflect.set(target,key,newValue,receiver)
}
})
}
let str = {
value:'vue',
__v_isRef:'true'
}
const myreturn = {
str
}
console.log(proxyRefs(myreturn).str);//此处通过代理解决了通过value访问的过程
'<div>{{str}}</div>'
vue解析器在解析<div>{{str}}</div>会调用parseInterpolation函数,该函数会有一个返回值。
return{
type:'Interpolation',
content:{
type:'Expression',
content:decodeHtml(content)
}
}
生成一个模板抽象语法树
const ast = {
type:'Root',
children:[
{
type:'Element',
tag:'div',
isSelfClosing:false,
props:[],
children:[
type:'Interpolation',
content:{
type:'Expression',
content:'str'
}
]
}
]
}
模板抽象语法树->js抽象语法树->渲染函数渲染(就不细挖了,看源码看的脑壳疼)...
toRef
响应丢失
举个栗子
<template>
<div>{{r}}</div>
</template>
<script>
import { onMounted, reactive} from 'vue'
export default {
setup(){
// let str = myref('vue')
let str = reactive({v:'Vue',r:'React'})
onMounted(()=>{
setTimeout(()=>{
str.r = 'Angular'
console.log(str.r);
},2000)
}
)
return{
...str
}
}
}
</script>
这里的str由于展开...运算符发生了响应丢失。
return{
...str
}
等价于
return{
v:'Vue',
r:'React'
}
返回的就是一个普通的对象,并不是响应式的
为了解决响应丢失的问题,我们引入了toRef
toRef(obj,property)接收两个参数,第一个为对象,第二个对象其中的一个属性
return{
...{r:toRef(str,'r'),}
}
//这种写法就像脱裤子放屁,但是它确实解决了...响应丢失的问题。
手写一个toRef
在utils中建立一个文件mytoRef.js
//mytoRef.js
function mytoRef(obj,key){
const wrapper = {
get value(){ //当我们访问值时,间接访问了具有响应式的对象。
return obj[key]
},
set value(val){//当我们修改值时,间接修改了具有响应式的对象。
obj[key] = val
}
}
Object.defineProperty(wrapper,'__v_isRef',{
value:true
})
return wrapper
}
export default mytoRef
import mytoRef from './utils/mytoRef'
export default {
setup(){
let str = reactive({v:'Vue',r:'React'})
onMounted(()=>{
setTimeout(()=>{
str.r = 'Angular'
console.log(str.r);
},2000)
}
)
return{
...{r:mytoRef(str,'r'),}
}
toRefs
为了使用拓展符...时更方便的解决响应式丢失问题,引入了toRefs。
toRefs(obj)接收一个参数,即用reactive包裹后返回的对象。
<template>
<div>{{r}}</div>
</template>
<script>
import { onMounted,reactive, toRefs} from 'vue'
export default {
setup(){
let str = reactive({v:'Vue',r:'React'})
onMounted(()=>{
setTimeout(()=>{
str.r = 'Angular'
console.log(str.r);
},2000)
}
)
return{
...toRefs(str)
}
}
}
</script>
手写一个toRefs
在utils中建立一个文件mytoRefs.js
//mytoRefs.js
import mytoRef from "./mytoRef"
function mytoRefs(obj){
const ret = {}
for(const key in obj){
ret[key] = mytoRef(obj,key)
}
return ret
}
export default mytoRefs
import mytoRefs from './utils/mytoRefs'
export default {
setup(){
let str = reactive({v:'Vue',r:'React'})
onMounted(()=>{
setTimeout(()=>{
str.r = 'Angular''
console.log(str.r);
},2000)
}
)
return{
...mytoRefs(str)
}
}
}
如有错误,欢迎指正。