likes
comments
collection
share

简单的实现Vue3中的ref,reactive

作者站长头像
站长
· 阅读数 16

简单的实现Vue3中的ref,reactive

最近在(积极)摆烂的学习Vue3源码内容,想着有时间的时候写一些博客记录一下,也作为一个分享,若有文本or概念的问题,还请个位大佬详细指出,本人不胜感激

开篇介绍

  • 前言

    众所周知响应式是Vue的一大特色,而其原理也是面试的一个常考点,我觉着想要手撸一个mini-Vue不得不先了解一下,如何实现这个功能

  • 简单使用

    首先先来简单的使用和体验一下

    准备

    首先,随便创建一个文件夹,然后进行一个代码的包管理,代码如下

    md use
    cd use
    npm init
    

    然后安装一下所需要用到的包

    npm i @vue/reactivity
    

    然后创建一个HTML文件和js文件,这里就不过多赘述了

    体验

    话不多说,直接看代码

    import { ref, effect } from './node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
    
    let a = ref(10)
    effect(() => {
      let b = a.value + 10
      console.log(b)
    })
    a.value = 20
    

    注意因为是浏览器中打开,后边的文件尾缀得加上

    然后打开浏览器可以看到

    b的值打印了两次,这就表明这在修改了a的值之后,自动响应了变化,从而执行函数

ref的实现

从上述的用例来看,我们不难发现,为了达到这个功能我们需要实现

  1. 收集函数依赖,及effect中的函数
  2. 在添加了响应式的值变化时候,应该自动调用其所依赖的函数

分析完之后,我们来实现一下

依赖收集

具体内容原理比较简单,直接见代码

let activeEffect = null // 表示正在收集的函数,方便后续收集(不理解可以先跳过,等看完整体实现再来看这)
function watchEffect(effect){
    activeEffect = effect
    effect()  // 收集的时候得执行一次
    activeEffect = null
}
响应变化

初一看,这里要做的事情又很多,比如 如何监听变化,如何让他自动调用所依赖函数,这个数具体有哪些相关函数等等,似乎完全没有思路,但是仔细一想,这么多功能那肯定是需要我们去封装一个类依次来收集实现

  1. 初始化所获得值(_value)
  2. 收集保存相关函数(depend)
  3. 调用相关函数(notify)

对于上述的所需要保存的函数,我们用一个Set类型来保存,原因是为了防止多次收集相同的函数,所造成的空间浪费

因此代码如下

class Dep{
    constructor(value){
    this._value = value
    this.subscribers = new Set()  // 初始化保存函数列表
  }
    // 收集依赖
    depend(){
        if(activeEffect){
            this.subscribers.add(activEffect) 
        }
    }
    // 调用依赖
    notity(){
        this.subscribers.forEach(effect => {
       		effect()
        })
    }
}

在做完这些之后,好像大致上是实现完成了,但是我们还需要监听一下value的获取与改变,从而好调用相关函数

首先对与更改value时,不难想到,在更改之后,按照逻辑我们需要调用所有与之相关的函数;然后对于收集来说,如何判断这个函数是否依赖我们这个被监听的数呢?不难想到,当需要获取数的值时(也就是get),其肯定依赖于被监听的数

基于以上的分析,可以分别写一个setget方法,来满足

class Dep{
    constructor(value){
    this._value = value
    this.subscribers = new Set()  // 初始化保存函数列表
  }
    // 收集依赖
    depend(){
        if(activeEffect){
            this.subscribers.add(activEffect) 
        }
    }
    // 调用依赖
    notity(){
        this.subscribers.forEach(effect => {
       		effect()
        })
    }
    
    get value(){
    this.depend()  // 收集依赖
    return this._value
  }

  set value(newValue){
    if(newValue){
      this._value = newValue
      this.notify()  // 调用函数
    }
  }
}

综上,简单的ref已经被实现了,现在来使用一下

// ref.js
class Dep{
  constructor(value){
    this._value = value
    this.subscribers = new Set()

  }

  get value(){
    this.depend()
    return this._value
  }


  set value(newValue){
    if(newValue){
      this._value = newValue
      this.notify()
    }
  }

  depend(){
    if(activeEffect){
      this.subscribers.add(activeEffect)
    }
  }

  notify(){
    this.subscribers.forEach(effect => {
      effect()
    })
  }
}

let activeEffect = null
function watchEffect(effect){
  activeEffect = effect
  effect()
  activeEffect = null
}

export { Dep, watchEffect }
// main.js
import {Dep, watchEffect} from './响应式/index.js'
let a = new Dep(10)
watchEffect(() => {
  let b = a.value + 10
  console.log(b)
})

a.value = 20

其结果就不截图了, 可以自己尝试一下

reactive的实现

有了上述ref的实现,我们不难想到同样的对reactive我们只需要在数据监听以及对于相关依赖 收集做一些处理即可

依赖处理

众所周知,reatcive给我们的是一个对象,一个对象中有很多数值,加入我们不加以区分,所有的函数都收集在一个地方,那么我们无法精准的调用的到相关的函数,例如

const info = reacive({counter: 100, name: 'cpa'})
const foo = reacive({height: 1.7})

watchEffect(function one(){
  console.log(info.counter * 2, info.name);
})

watchEffect(function two(){
  console.log(info.counter * info.counter);
})

watchEffect(function three(){
  console.log(foo.height)
})

info.counter++ 

如果无脑全部收集在一起,那么在info.counter++ 的时候,三个函数都会被调用,但是我们希望的是只调用前面两个函数(因为第三个不依赖于info.counter),而对于改变info.name时,我们希望只调用第一个,因此我们需要创建一个map的类型,来存储映射一下这层关系,如图

简单的实现Vue3中的ref,reactive 因此,构造一个函数,来获取相对于的dep

// Map 的key为string类型,而Weakmap的key 可以为对象类型
let targetMap = new WeakMap()
function getDep(target, key){
  let depsMap = targetMap.get(target)
  if(!depsMap){
    depsMap = new Map()
    targetMap.set(target , depsMap)
  }

  let dep = depsMap.get(key)
  if(!dep){
    dep = new Dep()
    depsMap.set(key, dep)
  }

  return dep
}
数据监听

而对于数据监听,也是一个比较可以聊的地方,其中Vue2.xVue3.x分别使用的是不同的类型的监听模式,Vue2.x使用的是 Object.defineProperty, 而Vue3.x采用数据代理Proxy的方式来劫持监听数据,具体的区别本文就不再过多阐述,只给出两种不同的劫持方法

Vue3

function reactive(raw){
  return  new Proxy(raw,{
    get(target, key){
      const dep = getDep(target,key)
      dep.depend()
      return target[key]
    },
    set(target, key, newValue){
      if(target[key] != newValue){
        const dep = getDep(target, key)
        target[key] = newValue
        dep.notify()
      }
    }
  })
}

Vue2

function reactive(raw){
  Object.keys(raw).forEach(key => {
    const dep = getDep(raw, key)
    let value = raw[key]
    Object.defineProperty(raw, key, {
      get(){
        dep.depend()
        return value
      },
      set(newValue){
        if(value !== newValue){
          value = newValue
          dep.notify()
        }
      }
    })
  })
  return raw
}

最后,总体代码如下,可以自己进一步实现一下

class Dep{
  constructor(){
    this.subscribers = new Set()
  }

  depend(){
    if(activeEffect){
      this.subscribers.add(activeEffect)
    }
  }

  notify(){
    this.subscribers.forEach(effect => {
      effect()
    })
  }
}

let activeEffect = null
function watchEffect(effect){
  activeEffect = effect
  effect()
  activeEffect = null
}

let targetMap = new WeakMap()
function getDep(target, key){
  let depsMap = targetMap.get(target)
  if(!depsMap){
    depsMap = new Map()
    targetMap.set(target , depsMap)
  }

  let dep = depsMap.get(key)
  if(!dep){
    dep = new Dep()
    depsMap.set(key, dep)
  }

  return dep
}

function reactive(raw){
  return new Proxy(raw, {
    get(target, key){
      let dep = getDep(target, key)
      dep.depend()
      return target[key]
    },
    set(target, key, newValue){
      if(target[key] != newValue){
        let dep = getDep(target, key)
        target[key] = newValue
        dep.notify()
      }
      return true
    }
  })
}

export {Dep, watchEffect, reactive}

写在最后

本文参考了codewhy 的教程以及崔学社的mini-vue课程,带上了自己的一些思考,第一次写这样的文章,可能有些东西表述不是很清楚or因为眼界问题没有写的很好,但是还是卖出了第一步。研究原理确实比当切图仔快乐多了(呜呜呜),希望自己可以坚持下去,早日摆脱切图仔,成为搬砖工(bushi)

转载自:https://juejin.cn/post/7206615325021945915
评论
请登录