[原理剖析]Vue中的ref,toRef,toRefs

573 阅读4分钟

前言

我依稀记得当初刚接触Vue时,一直分不清ref,toRef,toRefs,后来也只是囫囵吞枣知道该怎么用,直到最近在读霍春阳老师的Vue设计与实现,并看了部分源码,才知其原理,写出来给曾经的盲点画上一个句号。

image.png

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>

image.png

响应式

setup(){
  let str = ref('vue')
  onMounted(()=>{
    setTimeout(()=>{
      str.value = 'React'
      console.log(str.value);
    },2000)
  }
  )
  return{
    str
  }
}

image.png

手写一个ref

在src目录下创建一个文件夹utils,utils中创建myref.js

image.png

//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>

看看效果 image.png 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>

image.png 这里的str由于展开...运算符发生了响应丢失。

return{
...str
}
等价于
return{
v:'Vue',
r:'React'
}
返回的就是一个普通的对象,并不是响应式的

为了解决响应丢失的问题,我们引入了toRef
toRef(obj,property)接收两个参数,第一个为对象,第二个对象其中的一个属性

  return{
 ...{r:toRef(str,'r'),}
  }
 //这种写法就像脱裤子放屁,但是它确实解决了...响应丢失的问题。

image.png

手写一个toRef

在utils中建立一个文件mytoRef.js

image.png

//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'),}
  }

image.png

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>

image.png

手写一个toRefs

在utils中建立一个文件mytoRefs.js

image.png

//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)
  }
}
}

image.png 如有错误,欢迎指正。