简单的实现Vue3中的ref,reactive
简单的实现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的实现
从上述的用例来看,我们不难发现,为了达到这个功能我们需要实现
- 收集函数依赖,及
effect
中的函数 - 在添加了响应式的值变化时候,应该自动调用其所依赖的函数
分析完之后,我们来实现一下
依赖收集
具体内容原理比较简单,直接见代码
let activeEffect = null // 表示正在收集的函数,方便后续收集(不理解可以先跳过,等看完整体实现再来看这)
function watchEffect(effect){
activeEffect = effect
effect() // 收集的时候得执行一次
activeEffect = null
}
响应变化
初一看,这里要做的事情又很多,比如 如何监听变化,如何让他自动调用所依赖函数,这个数具体有哪些相关函数等等,似乎完全没有思路,但是仔细一想,这么多功能那肯定是需要我们去封装一个类依次来收集实现
- 初始化所获得值(_value)
- 收集保存相关函数(depend)
- 调用相关函数(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
),其肯定依赖于被监听的数
基于以上的分析,可以分别写一个set
和get
方法,来满足
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
的类型,来存储映射一下这层关系,如图
因此,构造一个函数,来获取相对于的
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.x
和Vue3.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