vue3 reactive 思路引导

2,697 阅读3分钟

首先这是一个精简版,所以有很多缺陷。但这篇文章仅仅是用来理清为什么,而不是去实现一个完备的reactive 模块。 如果对reactive 比较熟悉,这篇文章大可不必浪费时间。

假设设计一个 晚餐价格计算的功能。 total 总价 = dinnerPrice 晚餐费用 + tip 小费

待改造代码

render函数

function paint(){
	document.getElementById('app').innerHTML = `
		<p>${dinnerPrice}</p>
		<p>${tip}</p>
		<p>${total}</p>
	`
}

render 函数需要的变量

render 函数中,需要三个变量为dinnerPrice,tip, total

let dinnerPrice = 100;
let tip = 10;
let total = 0;

更新函数

视图和数据定义完了后,还需要定义更新函数

function updateTotal(){
  total = dinnerPrice * tip
}

原始调用方式

这样一个基本雏形就有了,调用方式如下

  1. 修改dinner 或者 tip dinnerPrice = 200
  2. 手动调用updateTotal 更新数据 updateTotal()
  3. 手动调用paint 来更新视图paint()

reactive

修改dinnerPrice 或者 tips 自动更新total

引入proxy 对数据进行代理

如果想用proxy 进行代理数据,则数据必须是Object,因此数据需要调整为Object 结构

function paint(){
	document.getElementById('app').innerHTML = `
		<p>${data.dinnerPrice}</p>
		<p>${data.tip}</p>
		<p>${data.total}</p>
	`
}
const rawData = {
  dinnerPrice:100,
  tip:10,
  total:0
}

const data = proxy(rawData,{})

function updateTotal(){
  data.total = data.dinnerPrice * data.tip
}

set trap

我们想要实现:修改数据,视图自动更新

这就需要定义handler.set trap,来实现代理操作

const data = proxy(rawData,{
  get(){},
  set(obj,key,value){
    // 赋值操作
    obj[key] = value//为简化操作,这里不使用Reflict 
    // 如果key 是 dinnerPrice 或者 tip,那么说明我们需要执行updateTotal函数
    // 如果不进行判断,会爆栈,原因:设置dinnerPrice 或者tip会 触发total set 操作,从而无限set
    if(key === 'dinnerPrice' || key === 'tip'){
      updateTotal()
    }
  }
})

这样,就能够实现当设置dinnerPrice 或者 tip 自动更新total。

引入deps 概念

但是,这里的key是固定死的,可以进行进一步的优化。方法:将key 列表通过一个数据结构来进行维护,如果data的某个key进行了set操作,并且这个数据结构中有这个key , 那么就执行对应的更新函数

// 为新的数据结构起名为 deps (dependences)
const deps = {
  tip:[updateTotal],
  dinnerPrice:[updateTotal]
}

const data = proxy(rawData,{
  ...,
  set(obj,key,value){
    obj[key] = value;
    deps[key] && deps[key].forEach(fn=>fn())
  }
})

优化了代码结构后,代码结构清晰了不少,但是我们目标是修改数据,自动更新视图,这里的deps必须自动手动添加。

添加动态dep

想要动态添加dep,需要用到handler.get 方法。handler.get 会拦截当对象的读取操作。在这里可以对dep 进行动态添加。

const deps = {}
//如果key 存在,那么则将更新函数加入它的依赖中
const data = proxy(rawData,{
  get(obj,key){
    deps[key] = !!deps[key] ? [...deps[key],updateTotal] : []
  }
  return obj[key]
})

引入watcher 概念

上述的方法,实现了为deps 动态添加updateTotal 这个函数,但是问题很大,每次对象的读取操作执行都会重复添加依赖。我们只需要执行一次即可,并且能够自主添加不同的更新函数,所以,进行一下修改

let runningFn = null;

function watcher(target){
  runningFn = target
  target()
  runningFn = null
}

watcher(updateTotal)// == watcher(()=>data.total = data.dinnerPrice + data.tip)

使用watcher 进行依赖收集

我们使用watcher 对需要的更新操作进行watch ,执行watcher 就会触发对应的get操作

修改proxy

const data = proxy(rawData,{
  get(obj,key){
    //此处的依赖收集为简化板,存在缺陷,key可能存在相同的,导致错误。vue3 的依赖收集为两层 deps[obj][key]
    const collectDep = (key)=>deps[key] = !!deps[key] ? [...deps[key],updateTotal] : []
    //如果是wathcher 执行的操作,那么进行依赖收集
    !!ruuningFn && collectDep(key)
    return obj[key]
  },
    ...
})

完整代码

这样一个reactive 就基本写完了,下面进行封装

//封装proxy
const observe = dataObj => new Proxy(dataObj,{
  get(obj,key){
    //此处的依赖收集为简化板,存在缺陷,key可能存在相同的,导致错误。vue3 的依赖收集为两层 deps[obj][key]
    const collectDep = (key)=>deps[key] = !!deps[key] ? [...deps[key],updateTotal] : []
    //如果是wathcher 执行的操作,那么进行依赖收集
    !!ruuningFn && collectDep(key)
    return obj[key]
  },
  set(obj,key,value){
    obj[key] = value;
    deps[key] && deps[key].forEach(fn=>fn())
  }
})

let runningFn = null
const watcher = target=>{
  runningFn = target;
  target()
  runningFn = null;
}

//对render函数进行依赖收集
watcher(function render(){
	document.getElementById('app').innerHTML = `
    	<p>${data.dinnerPrice}</p>
    	<p>${data.tip}</p>
    	<p>${data.total}</p>
	`
})

// 对数据更新操作进行依赖收集
watcher(()=>data.total = data.dinnerPrice + data.tip)