CaveDraw源码04:实现自己的简单响应式

响应式的使用

参考深入响应式系统,响应式的使用:

import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
  document.body.innerHTML = `计数:${count.value}`
})

// 更新 DOM
count.value++

直白理解响应式,就是更新一个变量,UI也会自动更新,这样就取代了Jquery的手动更新UI,开发程序的时候,先关注UI和变量的连结,然后只需关注数据结构和逻辑就行了。

响应式的实现逻辑

观察上面的代码,可以看到,先使用ref函数包裹一个变量,然后使用watchEffect函数绑定变量和函数的对应关系。底层逻辑肯定是ref函数返回一个代理对象,代理对象的set方法中,会调用watchEffect函数中绑定的函数,肯定需要一个数据结构来维护代理对象和函数的关系,深入响应式系统也是这么干的:

副作用订阅将被存储在一个全局的 WeakMap>> 数据结构中。如果在第一次追踪时没有找到对相应属性订阅的副作用集合,它将会在这里新建。这就是 getSubscribersForProperty() 函数所做的事。

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    }
  })
}

function track(target, key) {
  if (activeEffect) {
    const effects = getSubscribersForProperty(target, key)
    effects.add(activeEffect)
  }
}

这里令人好奇的是,并没有声明变量和watchEffect中的函数的对应关系,它们是怎么绑定的呢?耐心往下看。

代码实现

这里比较搞脑子的地方在于,调用simpleReactive去包装代理对象的时候,并没有绑定变量和函数的对应关系。直到调用effect函数中,触发对象的get方法,才真正绑定上。

// 保存代理对象上的 key 和对应的切面方法(effect中传入的函数)
const targetMap = new Map();

既然它们使用get方法绑定的,那岂不是每次都要绑定?是在track方法中判断activeEffect==null来避免。

function track(target, type, key) {
    if (activeEffect == null) return;
import Utils from './utils';
/**
 * 简单响应式
 * 目标:创建响应式数据,也就是代理对象。代理对象的set方法上调用回调函数。关键是,如何得到对象的key和回调函数的对应关系(effect中传入的函数)。
 * 1. reactive创建代理,但是还没有挂载回调函数到代理。
 * 2. effect函数读取代理的key,并把effect中传入的函数挂载在key的set方法上。
 * 3. 第二步的实现其实是,在代理函数中添加切面方法track,effect函数读取代理的key的时候,触发track方法。如此就获得了对象的key和回调函数(effect中传入的函数)的对应关系。
 *
    // reactive创建代理
    const ret = simpleReactive({
        shoe: {
            size:10
        },
        sex: {
            male: 1,
            female: 0
        }
    })
    let val, val2
    // effect 中通过读取ret.shoe这个key,触发代理对象的get方法。从而绑定这个函数到对应的key,用来在设置ret.shoe这个key的值的时候调用。
    effect(() => {
        val = ret.shoe;
        var b = ret.shoe.size;
        var c = ret.sex.male;
        console.log(val);
    })
    effect(() => {
        val2 = ret.shoe;
        var b = ret.shoe.size;
        var c = ret.sex.male;
        console.log(val2.size*4);
        console.log(ret.sex);
    })
    setTimeout(function(){
        ret.sex.male=1111;
    },1000);
 *
*/
// 保存代理对象上的 key 和对应的切面方法(effect中传入的函数)
const targetMap = new Map();
// 保存已经 reactive 过的对象
const reactiveMap = new Map();
// effect 函数的临时保存
let activeEffect = null;

function effect(fn) {
    try {
        activeEffect = fn;
        // effect 方法中,通过读取代理的 key,可以是多个 key,就不断触发代理的 get 方法,进而触发 track 方法,绑定回调函数
        return fn();
    } finally {
        // 避免重复绑定
        activeEffect = null;
    }
}

function track(target, type, key) {
    if (activeEffect == null) return;
    console.log(`触发 track -> target: ${target} type:${type} key:${key}`);
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
    }
    let deps = depsMap.get(key);
    if (!deps) {
        deps = new Set();
    }
    if (!deps.has(activeEffect) && activeEffect) {
        deps.add(activeEffect);
    }
    depsMap.set(key, deps);
}

function trigger(target, type, key) {
    console.log(`触发 trigger -> target: ${target} type:${type} key:${key}`);
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        return;
    }
    const deps = depsMap.get(key)
    if (!deps) {
        return;
    }
    deps.forEach((effectFn) => {
        effectFn();
    });
}

const handlers = {
    get(target, key, receiver) {
        track(target, 'get', key);
        const res = Reflect.get(target, key, receiver)
        if (Utils.isObject(res)) {
            // 返回值也是 object,递归调用 reactive
            return reactive(res);
        }
        return res;
    },
    set(target, key, value, receiver) {
        const result = Reflect.set(target, key, value, receiver)
        trigger(target, 'set', key)
        return result
    }
}

function reactive(target) {
    if (typeof target !== 'object') {
        console.warn(`reactive  ${target} 必须是一个对象`);
        return target;
    }
    const existingProxy = reactiveMap.get(target)
    if (existingProxy) {
        return existingProxy;
    }
    const proxy = new Proxy(target, handlers);
    reactiveMap.set(target, proxy);
    return proxy;
}

export {
    reactive as simpleReactive,
    effect
}

反馈:您觉得本站怎么样?(此评价不会公开,也不会对博主产生任何实际利益。)
  • 非常优秀
  • 可以
  • 一般
  • 垃圾
  • 超级恶心
Copyright © 2023,枫糖, 版权所有,禁止转载、演绎、商用。
离开前,建议您浏览一下 归档 页面,或许有更多相关的、有趣的内容!
如需博主帮助,请转到 小卖铺 页面,购买手工活服务!

1 个评论

  1. maplesugar 2022-12-16

添加评论

code

反馈:您觉得本站怎么样?(此评价不会公开,也不会对博主产生任何实际利益。)
目录