阅读 63

Vue3 响应式原理探索Part 4 - ref 的原理和实现

“这是我参与更文挑战的第9天,活动详情查看: 更文挑战

前文摘要

通过之前的学习,我们已经通过 Proxy + Reflect 搭建出了一个基础的响应式实现。

本文,我们将学习 ref 的实现。

为什么需要 Ref

假设出现这样的场景:

let param = reactive({ width: 5, height: 2  });
let size = 0;
let newWidth = 0;

effect(() => {
    size = param.width * 2 * param.height;
});

effect(() => {
    newWidth = param.width * 2;
});
复制代码

我们很容易想到将 size 的计算公式改一下:

effect(() => {
    size = newWidth * param.height;
});
复制代码

但这样做是无效的,因为 newWidth 不是响应式的,sizeeffect 并不会重新计算。

如果我们能将 newWidth 改成响应式的,并且还没有将它包裹在另外一个响应式对象里面就最好不过来。

如果你熟悉 Vue3Composition API,你可能会考虑到这个时候可以使用 ref 去创建一个响应式的引用。

根据 Vue 的文档,一个响应式的引入传入一个内部值,然后返回一个可变的响应式引用 ref 对象。 ref 对象有一个单个的属性 .value 指向传入的内部值。

let param = reactive({ width: 5, height: 2  });
let size = 0;
let newWidth = ref(0);

effect(() => {
    newWidth.value = param.width * 2;
});

effect(() => {
    size = newWidth.value * param.height;
});

复制代码

上面的代码还不能工作,因为我们还没有实现 ref 。

如何实现 Ref

基于 Reactive 封装

简单了事,我们可以基于 Reactive 实现。

function ref(initialValue) {
  return reactive({ value: initialValue })
}
复制代码

它是可以工作的,完整的代码如下:

const targetMap = new WeakMap();
let activeEffect = null // The active effect running

function track(target, key) {
    if (!activeEffect) return;
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
        depsMap.set(key, (dep = new Set()));
    }
    dep.add(activeEffect);
}
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach(element => {
            element();
        });
    }
}

function reactive(target) {
    const handler = {
        get(target, key, receiver) {
            let result = Reflect.get(target, key, receiver);
            track(target, key);
            return result;
        },
        set(target, key, value, receiver) {
            let oldValue = target[key];
            let result = Reflect.set(target, key, value, receiver);
            if (result && oldValue != value) {
                trigger(target, key);
                // console.log('trigger', key);
            }
            return result;
        }
    }
    return new Proxy(target, handler);
}


function effect(eff) {
    activeEffect = eff  // Set this as the activeEffect
    activeEffect()      // Run it
    activeEffect = null // Unset it
}

function ref(initialValue) {
    return reactive({ value: initialValue })
}

let param = reactive({ width: 5, height: 2  });
let size = 0;
let newWidth = ref(0);

effect(() => {
    newWidth.value = param.width * 2;
});

effect(() => {
    size = newWidth.value * param.height;
});

console.log(`newWidth is ${newWidth.value}, size is ${size}`); // newWidth is 10, size is 20

param.width = 10;

console.log(`newWidth is ${newWidth.value}, size is ${size}`); // newWidth is 20, size is 40
复制代码

然而它并不是 Vue3 的实现方式,我们看一下另外一种实现。

基于 Object 的 getter 和 setter 实现

首先我们需要熟悉对象存取器(Object Accessors),他们有时也被称呼为 JavaScript 计算属性(不要将他们和 Vue 计算属性混淆)。他们是原生的 JavaScript, 而不是 Vue 的一个功能。

getter 和 setter 基础学习

let user = {
  firstName: 'Gregg',
  lastName: 'Pollack',

  get fullName() {
    return `${this.firstName} ${this.lastName}`
  },

  set fullName(value) {
    [this.firstName, this.lastName] = value.split(' ')
  },
}

console.log(`Name is ${user.fullName}`) // => Name is Gregg Pollack
user.fullName = 'Adam Jahr'
console.log(`Name is ${user.fullName}`) // => Name is Adam Jahr

复制代码

参考文档:

具体实现

在对象存取器里面,我们可以加入 triggertrack 函数。

function ref(raw) {
    const r = {
        get value() {
            track(r, 'value')
            return raw
        },
        set value(newVal) {
            raw = newVal
            trigger(r, 'value')
        },
    }
    return r
}
复制代码

完整代码如下:

const targetMap = new WeakMap();
let activeEffect = null // The active effect running

function track(target, key) {
    if (!activeEffect) return;
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
        depsMap.set(key, (dep = new Set()));
    }
    dep.add(activeEffect);
}
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach(element => {
            element();
        });
    }
}

function reactive(target) {
    const handler = {
        get(target, key, receiver) {
            let result = Reflect.get(target, key, receiver);
            track(target, key);
            return result;
        },
        set(target, key, value, receiver) {
            let oldValue = target[key];
            let result = Reflect.set(target, key, value, receiver);
            if (result && oldValue != value) {
                trigger(target, key);
            }
            return result;
        }
    }
    return new Proxy(target, handler);
}


function effect(eff) {
    activeEffect = eff  // Set this as the activeEffect
    activeEffect()      // Run it
    activeEffect = null // Unset it
}

function ref(raw) {
    const r = {
        get value() {
            track(r, 'value')
            console.log(raw);
            return raw
        },
        set value(newVal) {
            raw = newVal
            trigger(r, 'value')
        },
    }
    return r
}

let param = reactive({ width: 5, height: 2  });
let size = 0;
let newWidth = ref(0);

effect(() => {
    newWidth.value = param.width * 2;
});

effect(() => {
    size = newWidth.value * param.height;
});

// console.log(`newWidth is ${newWidth.value}, size is ${size}`); // newWidth is 10, size is 20

param.width = 6;

// console.log(`newWidth is ${newWidth.value}, size is ${size}`); // newWidth is 12, size is 24
复制代码

可以看到,我们基于 Objectgettersetter 实现了一个 ref

小结

本人首先介绍了 Ref 的应用场景及相应的两种实现。

  • 基于 Reactive 封装
  • 基于 Object 的 getter 和 setter 实现

希望对你有所帮助。

文章分类
前端
文章标签