что было сказано раньше
Во Vue2 я лично чувствую, что работа с данными больше похожа на «черный ящик».
А Vue3 более явно раскрывает реактивную систему, что дает нам больше гибкости в работе с данными.
Поэтому нам нужно лучше понять и освоить несколько адаптивных API Vue3, чтобы свободно использовать их в реальном бою.
Сначала поймите, что такое отзывчивость?
- Официальный сайт Vue3 привел пример
var val1 = 2
var val2 = 3
var sum = val1 + val2
Мы хотим, чтобы сумма менялась соответственно при изменении значения val1 или val2.
- народный
Я завишу от тебя, ты изменился. Вы просто дайте мне знать, чтобы я мог сделать некоторые "операции".
- Из исходного кода Vue3
Давайте запомним три ключевых английских слова, а их порядок такой же, как и при заполнении ответного слова.
effect > track > trigger > effect
Краткое объяснение: в процессе рендеринга компонента, предполагая, что в настоящее время используется «эффект» (побочный эффект), эффект будет касаться значения, которого он касается (то есть будет запущен метод получения значения) во время процесса, чтобы отслеживать значение. При изменении значения срабатывает триггер, и выполняется эффект для завершения ответа!
- объяснить с кодом
Во Vue есть три вида эффектов, скажем,Эффект рендеринга просмотра, эффект вычисляемого свойства, эффект слушателя
<template>
<div>count:{{count}}</div>
<div>computedCount:{{computedCount}}</div>
<button @click="handleAdd">add</button>
</template>
// ...
setup() {
const count = ref(1);
const computedCount = computed(() => {
return count.value + 1;
});
watch(count, (val, oldVal) => {
console.log('val :>> ', val);
});
const handleAdd = () => {
count.value++;
};
return {
count,
computedCount,
handleAdd
};
}
// ...
В приведенном выше коде отслеживание значений зависимостей будет храниться в такой коллекции, как показано на рисунке:
Примечание. Методы reactiveEffect в самом внутреннем массиве коллекций выше — это эффект прослушивателя, эффект рендеринга представления и эффект вычисляемого свойства.
Когда действие handleAdd будет выполнено, оно будет запущеноcount.valueМетод set триггера в ответ вызывает три эффекта, связанных с набором, затем обновляет представление, обновляет значение calculatedCount и вызывает метод обратного вызова прослушивателя наблюдения для вывода.
Неважно, если вы не очень хорошо это понимаете, головная дыня может сначала иметь общую структуру~
После краткого ознакомления с тем, что такое отзывчивость, давайте перейдем к теме этой статьи и узнаем больше об адаптивном API Vue3.
20 адаптивных API, встроенных в Vue3
1. reactive
Первый взгляд на прокси
Прежде чем понять реактивность, давайте сначала разберемся с волной ключевых классов, которые реализуют реактивный API > прокси ES6, у которого также есть хороший друг, Reflect. Здесь мы сначала рассмотрим простой пример:
const targetObj = {
id: 1,
name: 'front-refined',
childObj: {
hobby: 'coding'
}
};
const proxyObj = new Proxy(targetObj, {
get(target, key, receiver) {
console.log(`get key:${key}`);
return Reflect.get(...arguments);
},
set(target, key, value, receiver) {
console.log(`set key:${key},value:${value}`);
return Reflect.set(...arguments);
}
});
Давайте проанализируем две вещи:
- Распечатать объект за прокси в браузере
[[Handler]]: Процессор, перехваченный в данный моментget,set [[Target]]: целевой объект прокси[[IsRevoked]]: отзывается ли прокси
первый контакт[[IsRevoked]]Когда мне было немного любопытно узнать о его роли. Также любопытные друзья посмотрите на этот код:
// 用 Proxy 的静态方法 revocable 代理一个对象
const targetObj = { id: 1, name: 'front-refined' };
const { proxy, revoke } = Proxy.revocable(targetObj, {});
revoke();
console.log('proxy-after :>> ', proxy);
proxy.id = 2;
Вывод такой, как показано:
Ошибка: заданное действие не может быть выполнено с идентификатором, так как прокси отозван
- Напечатайте приведенный выше код в консоли, чтобы увидеть, что получится на выходе?
proxyObj.name
// get key:name
proxyObj.name="hello~"
// set key:name,value:hello~
proxyObj.childObj.hobby
// get key:childObj
proxyObj.childObj.hobby="play"
// get key:childObj
мы можем видеть, что для хоббиget/setВывод идет только к childObj . Если это так, это не остановит хоббиget/setТеперь, как отслеживать и запускать обновления? Перейдем к вопросам.
реактивный исходный код (прокси для глубоких объектов)
Мы можем видеть, что независимо от того, получено или установлено хобби, сначала будет получено значение childObj.// get key:childObj, то мы можем выполнить некоторые операции в методе доступа get. Вот пример исходного кода, связанного с реактивным (я знаю, что исходный код сложный, поэтому я упростил его и добавил комментарии. Этот код можно скопировать и запустить напрямую~):
// 工具方法:判断是否是一个对象(注:typeof 数组 也等于 'object'
const isObject = val => val !== null && typeof val === 'object';
// 工具方法:值是否改变,改变才触发更新
const hasChanged = (value, oldValue) =>
value !== oldValue && (value === value || oldValue === oldValue);
// 工具方法:判断当前的 key 是否是已经存在的
const hasOwn = (val, key) => hasOwnProperty.call(val, key);
// 闭包:生成一个 get 方法
function createGetter() {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
console.log(`getting key:${key}`);
// track(target, 'get' /* GET */, key);
// 深层代理对象的关键!!!判断这个属性是否是一个对象,是的话继续代理动作,使对象内部的值可追踪
if (isObject(res)) {
return reactive(res);
}
return res;
};
}
// 闭包:生成一个 set 方法
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
// 判断当前 key 是否已经存在,不存在的话表示为新增的 key ,后续 Vue “标记”新的值使它其成为响应式
if (!hadKey) {
console.log(`add key:${key},value:${value}`);
// trigger(target, 'add' /* ADD */, key, value);
} else if (hasChanged(value, oldValue)) {
console.log(`set key:${key},value:${value}`);
// trigger(target, 'set' /* SET */, key, value, oldValue);
}
return result;
};
}
const get = createGetter();
const set = createSetter();
// 基础的处理器对象
const mutableHandlers = {
get,
set
// deleteProperty
};
// 暴露出去的方法,reactive
function reactive(target) {
return createReactiveObject(target, mutableHandlers);
}
// 创建一个响应式对象
function createReactiveObject(target, baseHandlers) {
const proxy = new Proxy(target, baseHandlers);
return proxy;
}
const proxyObj = reactive({
id: 1,
name: 'front-refined',
childObj: {
hobby: 'coding'
}
});
proxyObj.childObj.hobby
// get key:childObj
// get key:hobby
proxyObj.childObj.hobby="play"
// get key:childObj
// set key:hobby,value:play
Видно, что после «крещения» Vue мы можем перехватить хоббиget/set.
ненужныйVue.set()охватывать
В Vue3 нам больше не нужно использовать метод Vue.set для динамического добавления реактивного свойства, потому что за ним стоит другой механизм реализации:
В Vue2 использование Object.defineProperty может заранее перехватывать только определенные свойства, а степень детализации невелика.
В Vue3 используется Proxy, который перехватывает весь объект.
Простое объяснение кода, например:
// Object.defineProperty
const obj1 = {};
Object.defineProperty(obj1, 'a', {
get() {
console.log('get1');
},
set() {
console.log('set1');
}
});
obj1.b = 2;
Вышеприведенный код не производит никакого вывода!
// Proxy
const obj2 = {};
const proxyObj2 = new Proxy(obj2, {
get() {
console.log('get2');
},
set() {
console.log('set2');
}
});
proxyObj2.b = 2;
// set2
Активируется метод доступа set.
2. shallowReactive
первый раз такое вижуshallowФормулировка, я думаю, классического мелкого сравнения в React, понятие «мелкий» соответствует, давайте посмотрим:
const shallowReactiveObj = shallowReactive({
id: 1,
name: 'front-refiend',
childObj: { hobby: 'coding' }
});
// 改变 id 是响应式的
shallowReactiveObj.id = 2;
// 改变嵌套对象的属性是非响应式的,但是本身的值是有被改变的
shallowReactiveObj.childObj.hobby = 'play';
Давайте посмотрим, как это управляется в исходном коде, давайте добавим что-то к реактивному упрощенному исходному коду выше (здесь просто используйте комментарий // +++, чтобы указать новый блок кода):
// ...
// +++ 新增了 shallow 入参
// 闭包:生成一个 get 方法
function createGetter(shallow = false) {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
console.log(`get key:${key}`);
// track(target, 'get' /* GET */, key);
// +++
// shallow=true,就直接 return 结果,所以不会深层追踪
if (shallow) {
return res;
}
// 深层代理对象的关键!!!判断这个属性是否是一个对象,是的话继续代理动作,使对象内部的值可追踪
if (isObject(res)) {
return reactive(res);
}
return res;
};
}
// +++
const shallowGet = createGetter(true);
// +++
// 浅处理器对象,合并覆盖基础的处理器对象
const shallowReactiveHandlers = Object.assign({}, mutableHandlers, {
get: shallowGet
});
// +++
// 暴露出去的方法,shallowReactive
function shallowReactive(target) {
return createReactiveObject(target, shallowReactiveHandlers);
}
// ...
3. readonly
Официальный сайт: прокси только для чтения, который получает объект (реактивный или обычный) или ссылку и возвращает исходный прокси. Прокси-серверы только для чтения являются глубокими: любые вложенные свойства, к которым осуществляется доступ, также доступны только для чтения.
Пример:
const proxyObj = reactive({
childObj: {
hobby: 'coding'
}
});
const readonlyObj = readonly(proxyObj);
// 如果被拷贝对象 proxyObj 做了修改,打印 readonlyObj.childObj.hobby 也会看到有变更
proxyObj.childObj.hobby = 'play';
console.log('readonlyObj.childObj.hobby :>> ', readonlyObj.childObj.hobby);
// readonlyObj.childObj.hobby :>> play
// 只读对象被改变,警告
readonlyObj.childObj.hobby = 'play';
// ⚠️ Set operation on key "hobby" failed: target is readonly.
В этом примере readonlyObj разделяет все с proxyObj, за исключением того, что это нельзя изменить. Все его свойства такжеОтзывчивыйДа, давайте еще раз посмотрим на исходный код, мы еще кое-что добавим в реактивный упрощенный исходный код выше:
// +++ 新增了 isReadonly 参数
// 闭包:生成一个 get 方法
function createGetter(shallow = false, isReadonly = false) {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
console.log(`get key:${key}`);
// +++
// 当前是只读的情况,自己不会被改变,所以就没必要进行追踪变化
if (!isReadonly) {
// track(target, "get" /* GET */, key);
}
// shallow=true,就直接 return 结果,所以不会深层追踪
if (shallow) {
return res;
}
// 深层代理对象的关键!!!判断这个属性是否是一个对象,是的话继续代理动作,使对象内部的值可追踪
if (isObject(res)) {
// +++
// 如果是只读,也要同步进行深层代理
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
// +++
const readonlyGet = createGetter(false, true);
// +++
// 只读处理器对象
const readonlyHandlers = {
get: readonlyGet,
// 只读,不允许 set ,所以这里警告
set(target, key) {
{
console.warn(
`Set operation on key "${String(
key
)}" failed: target is readonly.`,
target
);
}
return true;
}
};
// +++
// 暴露出去的方法,readonly
function readonly(target) {
return createReactiveObject(target, readonlyHandlers);
}
Как и выше, добавлен новый параметр isReadonly, чтобы указать, следует ли выполнять глубокое проксирование.
Пример выше только для чтения похож на «прокси через прокси», то есть:proxy(proxy(原始对象)), как показано на рисунке:
Дочерние компоненты, к которым мы обычно прикасаемся, чаще всего получают реквизиты, передаваемые родительским компонентом. Он был создан с доступом только для чтения, поэтому он остается доступным только для чтения. Если вы хотите изменить его, вы можете отправить его родительскому компоненту только через emit, тем самым обеспечив традиционный односторонний поток данных Vue.
4. shallowReadonly
Как следует из названия, это прокси-объектshallow=true & readonly=true, что тогда будет?
Например:
const shallowReadonlyObj = shallowReadonly({
id: 1,
name: 'front-refiend',
childObj: { hobby: 'coding' }
});
shallowReadonlyObj.id = 2;
// ⚠️ Set operation on key "id" failed: target is readonly.
// 对象本身的属性不能被修改
shallowReadonlyObj.childObj.hobby = 'runnnig';
// 嵌套对象的属性可以被修改,但是是非响应式的!
Давайте посмотрим, как это управляется в исходном коде, давайте продолжим добавлять что-то в реактивный упрощенный исходный код выше:
// ...
// +++
// shallow=true & readonly=true
const shallowReadonlyGet = createGetter(true, true);
// +++
// 浅只读处理器对象,合并覆盖 readonlyHandlers 处理器对象
const shallowReadonlyHandlers = Object.assign({}, readonlyHandlers, {
get: shallowReadonlyGet
});
// +++
// 暴露出去的方法,shallowReadonly
function shallowReadonly(target) {
return createReactiveObject(target, shallowReadonlyHandlers);
}
// ...
5. ref
Лично я считаю, что метод ref еще больше улучшает наше понимание ссылочных типов в js. Проще говоря, он оборачивает простой тип в объект, чтобы его можно было отслеживать (реактивный).
ref возвращает объект, содержащий свойство .value.
пример:
const refNum = ref(1);
refNum.value++;
Давайте посмотрим на принцип реализации, лежащий в основе этого (исходный код с уменьшенной ссылкой):
// 工具方法:值是否改变,改变才触发更新
const hasChanged = (value, oldValue) =>
value !== oldValue && (value === value || oldValue === oldValue);
// 工具方法:判断是否是一个对象(注:typeof 数组 也等于 'object'
const isObject = val => val !== null && typeof val === 'object';
// 工具方法:判断传入的值是否是一个对象,是的话就用 reactive 来代理
const convert = val => (isObject(val) ? reactive(val) : val);
function toRaw(observed) {
return (observed && toRaw(observed['__v_raw' /* RAW */])) || observed;
}
// ref 实现类
class RefImpl {
constructor(_rawValue, _shallow = false) {
this._rawValue = _rawValue;
this._shallow = _shallow;
this.__v_isRef = true;
this._value = _shallow ? _rawValue : convert(_rawValue);
}
get value() {
// track(toRaw(this), 'get' /* GET */, 'value');
return this._value;
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal;
this._value = this._shallow ? newVal : convert(newVal);
// trigger(toRaw(this), 'set' /* SET */, 'value', newVal);
}
}
}
// 创建一个 ref
function createRef(rawValue, shallow = false) {
return new RefImpl(rawValue, shallow);
}
// 暴露出去的方法,ref
function ref(value) {
return createRef(value);
}
// 暴露出去的方法,shallowRef
function shallowRef(value) {
return createRef(value, true);
}
основной классRefImpl, мы видим, что в классе мы используем классическийget/setАксессоры для отслеживания и запуска.convertМетод позволяет нам узнать, что ref используется не только для переноса типа значения, но и для объекта/массива, а затем передает объект/массив вreactiveВыступать в роли прокси. Просто посмотрите на пример:
const refArr = ref([1, 2, 3]);
const refObj = ref({ id: 1, name: 'front-refined' });
// 操作它们
refArr.value.push(1);
refObj.value.id = 2;
6. unref
Разверните ссылку: если параметр равен ref , верните.value, иначе возвращается сам параметр.
Исходный код:
function isRef(r) {
return Boolean(r && r.__v_isRef === true);
}
function unref(ref) {
return isRef(ref) ? ref.value : ref;
}
Чтобы облегчить разработку, Vue обрабатывает ref, используемый в шаблоне, и будет автоматически расширен, то есть нет необходимости писать .value Давайте посмотрим на реализацию, стоящую за этим:
Это объясняется способом «симуляции», основная логика не изменилась ~
// 模拟:在 setup 内定义一个 ref
const num = ref(1);
// 模拟:在 setup 返回,提供 template 使用
function setup() {
return { num };
}
// 模拟:接收了 setup 返回的对象
const setupReturnObj = setup();
// 定义处理器对象,get 访问器里的 unref 是关键
const shallowUnwrapHandlers = {
get: (target, key, receiver) =>
unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
} else {
return Reflect.set(target, key, value, receiver);
}
}
};
// 模拟:返回组件实例上下文
const ctx = new Proxy(setupReturnObj, shallowUnwrapHandlers);
// 模拟:template 最终被编译成 render 函数
/*
<template>
<input v-model="num" />
<div>num:{{num}}</div>
</template>
*/
function render(ctx) {
with (ctx) {
// 模拟:在template中,进行赋值动作 "onUpdate:modelValue": $event => (num = $event)
// num = 666;
// 模拟:在template中,进行读取动作 {{num}}
console.log('num :>> ', num);
}
}
render(ctx);
// 模拟:在 setup 内部进行赋值动作
num.value += 1;
// 模拟: num 改变 trigger 视图渲染effect,更新视图
render(ctx);
7. shallowRef
Введение в ref уже включает реализацию метода smallRef:this._value = _shallow ? _rawValue : convert(_rawValue);
Если входящее поверхностное значение истинно, исходное переданное значение будет возвращено напрямую, то есть глубокий прокси-объект больше не будет использоваться.Давайте рассмотрим два сценария:
- переданный объект
const shallowRefObj = shallowRef({
id: 1,
name: 'front-refiend',
});
После того, как вышеуказанный объект обработан, мы можем просто понять его как:
const shallowRefObj = {
value: {
id: 1,
name: 'front-refiend'
}
};
Поскольку он мелкий (мелкий), он остановится на значении и больше не будет выполнять глубокое проксирование.
То есть свойства вложенных объектов не будут отслеживаться, но мы модифицируем свойство value самого мелкогоRefObj, чтобы оно стало реактивным, например:shallowRefObj.value = 'hello~';
- Входящий является простым типом
const shallowRefNum = shallowRef(1);
Когда входящее значение имеет простой тип, объедините эти две строки кода:const convert = val => (isObject(val) ? reactive(val) : val);,this._value = _shallow ? _rawValue : convert(_rawValue);
Мы можем знать, что smallRef и ref имеют одинаковый конечный эффект, когда входной параметр имеет простой тип.
8. triggerRef
Лично я думаю, что этот API более абстрактен для понимания, и друзья будут обдумывать его вместе~
TriggerRef используется с smallRef, например:
const shallowRefObj = shallowRef({
name: 'front-refined'
});
// 这里不会触发副作用,因为是这个 ref 是浅层的
shallowRefObj.value.name = 'hello~';
// 手动执行与 shallowRef 关联的任何副作用,这样子就能触发了。
triggerRef(shallowRefObj);
Взгляните на принцип реализации, стоящий за этим:
В начале мы говорили о понятии эффекта, предполагая, что в данный момент мы идемпросмотр эффекта рендеринга.
Шаблон связывает значение, например:
<template> {{shallowRefObj.name}} </template>
При выполнении «рендеринга» будет прочитано мелкоеRefObj.value.name.Поскольку текущая ссылка неглубокая, можно отслеживать только изменение значения, поэтому отслеживайте в методе получения значения, например:track(toRaw(this), "get" /* GET */, 'value');
Исходный код метода track упрощен:
// targetMap 是一个大集合
// activeEffect 表示当前正在走的 effect ,假设当前是 视图渲染effect
function track(target, type, key) {
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()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
}
}
распечатать целевую карту
То есть, если мелкоеRefObj.value изменится, это может привести к срабатываниюпросмотр эффекта рендерингачтобы обновить представление, или мы можем запустить его вручную.
Однако то, что мы сейчас меняем,shallowRefObj.value.name = 'hello~';, поэтому мы «обманываем» метод триггера. Ручной триггер, пока наши входные параметры верны, представление будет обновляться в ответ. Взгляните на исходный код triggerRef и триггера:
function triggerRef(ref) {
trigger(toRaw(ref), 'set' /* SET */, 'value', ref.value);
}
// trigger 响应式触发
function trigger(target, type, key, newValue, oldValue, oldTarget) {
const depsMap = targetMap.get(target);
if (!depsMap) {
// 没有被追踪,直接 return
return;
}
// 拿到了 视图渲染effect 就可以进行排队更新 effect 了
const run = depsMap.get(key);
/* 开始执行 effect,这里做了很多事... */
run();
}
Мы получили это с целью и ключомПросмотр эффектов рендеринга. На этом этапе вы можете выполнить обновление вручную~
9. customRef
пользовательский исх. Этот API позволяет нам более точно понимать отслеживание и запуск. Давайте посмотрим на пример:
<template>
<div>name:{{name}}</div>
<input v-model="name" />
</template>
// ...
setup() {
let value = 'front-refined';
// 参数是一个工厂函数
const name = customRef((track, trigger) => {
return {
get() {
// 收集依赖它的 effect
track();
return value;
},
set(newValue) {
value = newValue;
// 触发更新依赖它的所有 effect
trigger();
}
};
});
return {
name
};
}
Давайте посмотрим на реализацию исходного кода:
// 自定义ref 实现类
class CustomRefImpl {
constructor(factory) {
this.__v_isRef = true;
const { get, set } = factory(
() => track(this, 'get' /* GET */, 'value'),
() => trigger(this, 'set' /* SET */, 'value')
);
this._get = get;
this._set = set;
}
get value() {
return this._get();
}
set value(newVal) {
this._set(newVal);
}
}
function customRef(factory) {
return new CustomRefImpl(factory);
}
В сочетании с исходным кодом ref, о котором мы упоминали выше, мы видим, что customRef просто более явно раскрывает внутреннюю реализацию ref, предоставляя нам более гибкий контроль. Например, триггер может быть задержан, например:
// ...
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
// 触发更新依赖它的所有 effect
trigger();
}, 2000);
}
// ...
10. toRef
Может использоваться для нового создания ссылки на свойство реактивного объекта, поддерживая реактивное соединение с его исходным свойством. Например:
Предположим, мы передаем реактивные данные составной функции, и внутри составной функции их можно модифицировать реактивно:
// 1. 传递整个响应式对象
function useHello(state) {
state.name = 'hello~';
}
// 2. 传递一个具体的 ref
function useHello2(name) {
name.value = 'hello~';
}
export default {
setup() {
const state = reactive({
id: 1,
name: 'front-refiend'
});
// 1. 直接传递整个响应式对象
useHello(state);
// 2. 传递一个新创建的 ref
useHello2(toRef(state, 'name'));
}
};
Давайте посмотрим на реализацию исходного кода:
// ObjectRef 实现类
class ObjectRefImpl {
constructor(_object, _key) {
this._object = _object;
this._key = _key;
this.__v_isRef = true;
}
get value() {
return this._object[this._key];
}
set value(newVal) {
this._object[this._key] = newVal;
}
}
// 暴露出去的方法
function toRef(object, key) {
return new ObjectRefImpl(object, key);
}
Несмотря на тоnameСвойство не существует, toRef также вернет доступную ссылку, например: мы указали свойство, которого нет у объекта в приведенном выше примере:
useHello2(toRef(state, 'other'));
Это действие эквивалентно добавлению другого свойства к объекту, и оно будет реагировать.
11. toRefs
Нижний слой toRefs — это toRef.
Преобразует реактивный объект в обычный объект, где каждое свойство результирующего объекта является ссылкой на соответствующее свойство исходного объекта, поддерживая реактивную связь с его исходным свойством.
Появление toRefs на самом деле для удобства разработки. Перейдем сразу к нескольким сценариям его использования:
- Разрушение реквизита
export default {
props: {
id: Number,
name: String
},
setup(props, ctx) {
const { id, name } = toRefs(props);
watch(id, () => {
console.log('id change');
});
// 没有使用 toRefs 的话,需要通过这种方式监听
watch(
() => props.id,
() => {
console.log('id change');
}
);
}
};
Таким образом, мы можем гарантировать, что сможем отслеживать изменение id (уничтожение без использования toRefs невозможно), потому что после передачи метода toRefs id фактически является ref-объектом.
- Преобразование при возврате настройки
<template>
<div>id:{{id}}</div>
<div>name:{{name}}</div>
</template>
// ...
setup() {
const state = reactive({
id: 1,
name: 'front-refiend'
});
return {
...toRefs(state)
};
}
Таким образом, нам удобнее записать соответствующее значение прямо в шаблон, без необходимости{{state.id}},{{state.name}}
Давайте посмотрим на исходный код:
function toRefs(object) {
const ret = {};
for (const key in object) {
ret[key] = toRef(object, key);
}
return ret;
}
12. compouted
Как упоминалось в начале, вычисление — это «вычисляемый эффект свойства». Он полагается на отзывчивые базовые данные, чтобы запускать свои обновления при изменении данных. Основным моментом вычислений является кеш, который может кэшировать вычисления с относительно высокой производительностью. Он возвращает объект ссылки.
Давайте взглянем на упрощенный исходный код вычисляемого замкнутого цикла (в основном, чтобы понять идею, хотя он и упрощен, но код все еще немного потерян, вы обязательно что-то приобретете, если не прочитаете его достаточно. Вы можно запустить напрямую, скопировав~):
<body>
<fieldset>
<legend>包含get/set方法的 computed</legend>
<button onclick="handleChangeFirsttName()">changeFirsttName</button>
<button onclick="handleChangeLastName()">changeLastName</button>
<button onclick="handleSetFullName()">setFullName</button>
</fieldset>
<fieldset>
<legend>只读 computed</legend>
<button onclick="handleAddCount1()">handleAddCount1</button>
<button onclick="handleSetCount()">handleSetCount</button>
</fieldset>
<script>
// 大集合,存放依赖相关
const targetMap = new WeakMap();
// 当前正在走的 effect
let activeEffect;
// 精简:创建一个 effect
const createReactiveEffect = (fn, options) => {
const effect = function reactiveEffect() {
try {
activeEffect = effect;
return fn();
} finally {
// 当前的 effect 走完之后(相关的依赖收集完毕之后),就退出
activeEffect = undefined;
}
};
effect.options = options;
// 该副作用的依赖集合
effect.deps = [];
return effect;
};
//#region 精简:ref 方法
// 工具方法:值是否改变,改变才触发更新
const hasChanged = (value, oldValue) =>
value !== oldValue && (value === value || oldValue === oldValue);
// ref 实现类
class RefImpl {
constructor(_rawValue) {
this._rawValue = _rawValue;
this.__v_isRef = true;
this._value = _rawValue;
}
get value() {
track(this, 'get', 'value');
return this._value;
}
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = newVal;
trigger(this, 'set', 'value', newVal);
}
}
}
// 创建一个 ref
function createRef(rawValue) {
return new RefImpl(rawValue);
}
// 暴露出去的方法,ref
function ref(value) {
return createRef(value);
}
//#endregion
//#region 精简:track、trigger
const track = (target, type, key) => {
if (activeEffect === undefined) {
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()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
// 存储该副作用相关依赖集合
activeEffect.deps.push(dep);
}
};
const trigger = (target, type, key, newValue) => {
const depsMap = targetMap.get(target);
if (!depsMap) {
// 没有被追踪,直接 return
return;
}
const effects = depsMap.get(key);
const run = effect => {
if (effect.options.scheduler) {
// 调度执行
effect.options.scheduler();
}
};
effects.forEach(run);
};
//#endregion
//#region 精简:computed 方法
const isFunction = val => typeof val === 'function';
// 暴露出去的方法
function computed(getterOrOptions) {
let getter;
let setter;
if (isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = () => {
// 提示,当前的 computed 如果是只读的,也就是说没有在调用的时候传入 set 方法
console.warn('Write operation failed: computed value is readonly');
};
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter);
}
// computed 核心方法
class ComputedRefImpl {
constructor(getter, _setter) {
this._setter = _setter;
this._dirty = true;
this.effect = createReactiveEffect(getter, {
scheduler: () => {
// 依赖的数据改变了,标记为脏值,等 get value 时进行计算获取
if (!this._dirty) {
this._dirty = true;
}
}
});
}
get value() {
// 脏值需要计算 _dirty=true 代表需要计算
if (this._dirty) {
console.log('脏值,需要计算...');
this._value = this.effect();
// 标记脏值为 false,进行缓存值(下次获取时,不需要计算)
this._dirty = false;
}
return this._value;
}
set value(newValue) {
this._setter(newValue);
}
}
//#endregion
//#region 例子
// 1. 创建一个只读 computed
const count1 = ref(0);
const count = computed(() => {
return count1.value * 10;
});
const handleAddCount1 = () => {
count1.value++;
console.log('count.value :>> ', count.value);
};
const handleSetCount = () => {
count.value = 1000;
};
// 2. 创建一个包含 get/set 方法的 computed
// 获取的 computed 数据
const consoleFullName = () =>
console.log('fullName.value :>> ', fullName.value);
const firsttName = ref('san');
const lastName = ref('zhang');
const fullName = computed({
get: () => firsttName.value + '.' + lastName.value,
set: val => {
lastName.value += val;
}
});
// 改变依赖的值触发 computed 更新
const handleChangeFirsttName = () => {
firsttName.value = 'si';
consoleFullName();
};
// 改变依赖的值触发 computed 更新
const handleChangeLastName = () => {
lastName.value = 'li';
consoleFullName();
};
// 触发 fullName set,如果 computed 为只读就警告
const handleSetFullName = () => {
fullName.value = ' happy niu year~';
consoleFullName();
};
// 必须要有读取行为,才会进行依赖收集。当依赖改变时候,才会响应式更新!
consoleFullName();
//#endregion
</script>
</body>
Замкнутый процесс вычисления выглядит следующим образом:
Когда объект ref, созданный с помощью вычисленного, вызывается в первый раз get (прочитано значение вычисленного), будет выполнена коллекция зависимостей.При изменении зависимости будет запущено выполнение планирования.dirty = true, отмечает грязные значения, которые необходимо вычислить. В следующий раз, когда вы вызовете функцию get вычисления, вам потребуется пересчитать, чтобы получить новые значения, и так далее.
13. watch
Что касается watch , вот немного более длинный пример исходного кода (код довольно длинный, но он весь упрощен, и есть блоки комментариев. Друзья, наберитесь терпения, копия может запускаться напрямую~)
<body>
<button onclick="handleChangeCount()">点我触发watch</button>
<button onclick="handleChangeCount2()">点我触发watchEffect</button>
<script>
// 大集合,存放依赖相关
const targetMap = new WeakMap();
// 当前正在走的 effect
let activeEffect;
// 精简:创建一个 effect
const createReactiveEffect = (fn, options) => {
const effect = function reactiveEffect() {
try {
activeEffect = effect;
return fn();
} finally {
// 当前的 effect 走完之后(相关的依赖收集完毕之后),就退出
activeEffect = undefined;
}
};
effect.options = options;
// 该副作用的依赖集合
effect.deps = [];
return effect;
};
//#region 精简:ref 方法
// 工具方法:判断是否是一个 ref 对象
const isRef = r => {
return Boolean(r && r.__v_isRef === true);
};
// 工具方法:值是否改变,改变才触发更新
const hasChanged = (value, oldValue) =>
value !== oldValue && (value === value || oldValue === oldValue);
// 工具方法:判断是否是一个方法
const isFunction = val => typeof val === 'function';
// ref 实现类
class RefImpl {
constructor(_rawValue) {
this._rawValue = _rawValue;
this.__v_isRef = true;
this._value = _rawValue;
}
get value() {
track(this, 'get', 'value');
return this._value;
}
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = newVal;
trigger(this, 'set', 'value', newVal);
}
}
}
// 创建一个 ref
function createRef(rawValue) {
return new RefImpl(rawValue);
}
// 暴露出去的方法,ref
function ref(value) {
return createRef(value);
}
//#endregion
//#region 精简:track、trigger
const track = (target, type, key) => {
if (activeEffect === undefined) {
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()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
// 存储该副作用相关依赖集合
activeEffect.deps.push(dep);
}
};
const trigger = (target, type, key, newValue) => {
const depsMap = targetMap.get(target);
if (!depsMap) {
// 没有被追踪,直接 return
return;
}
const effects = depsMap.get(key);
const run = effect => {
if (effect.options.scheduler) {
// 调度执行
effect.options.scheduler();
}
};
effects.forEach(run);
};
//#endregion
//#region 停止监听相关
// 停止侦听,如果有 onStop 方法一并调用,onStop 也就是 onInvalidate 回调方法
function stop(effect) {
cleanup(effect);
if (effect.options.onStop) {
effect.options.onStop();
}
}
// 清空改 effect 收集的依赖相关,这样子改变了就不再继续触发了,也就是“停止侦听”
function cleanup(effect) {
const { deps } = effect;
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect);
}
deps.length = 0;
}
}
//#endregion
//#region 暴露出去的 watchEffect 方法
function watchEffect(effect, options) {
return doWatch(effect, null, options);
}
//#endregion
//#region 暴露出去的 watch 方法
function watch(source, cb, options) {
return doWatch(source, cb, options);
}
function doWatch(source, cb, { immediate, deep } = {}) {
let getter;
// 判断是否 ref 对象
if (isRef(source)) {
getter = () => source.value;
}
// 判断是一个 reactive 对象,默认递归追踪 deep=true
else if (/*isReactive(source)*/ 0) {
// 省略...
// getter = () => source;
// deep = true;
}
// 判断是一个数组,也就是 Vue3 新的特性,watch 可以以数组的方式侦听
else if (/*isArray(source)*/ 0) {
// 省略...
}
// 判断是否是一个方法,这样子的入参
else if (isFunction(source)) {
debugger;
// 这里是类似这样子的入参,() => proxyObj.id
if (cb) {
// 省略...
} else {
// cb 为 null,表示当前为 watchEffect
getter = () => {
if (cleanup) {
cleanup();
}
return source(onInvalidate);
};
}
}
// 判断是否 deep 就会递归追踪
if (/*cb && deep*/ 0) {
// const baseGetter = getter;
// getter = () => traverse(baseGetter());
}
// 清理 effect
let cleanup;
const onInvalidate = fn => {
cleanup = runner.options.onStop = () => {
fn();
};
};
let oldValue = undefined;
const job = () => {
if (cb) {
// 获取改变改变后的新值
const newValue = runner();
if (hasChanged(newValue, oldValue)) {
if (cleanup) {
cleanup();
}
// 触发回调
cb(newValue, oldValue, onInvalidate);
// 把新值赋值给旧值
oldValue = newValue;
}
} else {
// watchEffect
runner();
}
};
// 调度
let scheduler;
// default: 'pre'
scheduler = () => {
job();
};
// 创建一个 effect,调用 runner 其实就是在进行依赖收集
const runner = createReactiveEffect(getter, {
scheduler
});
// 初始化 run
if (cb) {
if (immediate) {
job();
} else {
oldValue = runner();
}
} else {
// watchEffect 默认立即执行
runner();
}
// 返回一个方法,调用即停止侦听
return () => {
stop(runner);
};
}
//#endregion
//#region 例子
// 1. watch 例子
const count = ref(0);
const myStop = watch(
count,
(val, oldVal, onInvalidate) => {
onInvalidate(() => {
console.log('watch-clear...');
});
console.log('watch-val :>> ', val);
console.log('watch-oldVal :>> ', oldVal);
},
{ immediate: true }
);
// 改变依赖的值触发 触发侦听器回调
const handleChangeCount = () => {
count.value++;
};
// 停止侦听
// myStop();
// 2. watchEffect 例子
const count2 = ref(0);
watchEffect(() => {
console.log('watchEffect-count2.value :>> ', count2.value);
});
// 改变依赖的值触发 触发侦听器回调
const handleChangeCount2 = () => {
count2.value++;
};
//#endregion
</script>
</body>
Приведенный выше код просто реализует пример прослушивания объектами ref объектов watch, так как же нам правильно использовать watch? Давайте посмотрим на два момента вместе с исходным кодом:
- На официальном сайте описано, как написать источник прослушивания, который может быть функцией-получателем, возвращающим значение, или может быть напрямую ссылкой, то есть:
const state = reactive({ id: 1 });
// 使用
() => state.id
// 或
const count = ref(0);
// 使用 count
count
// 看完源码,我们也可以这样子写~
() => count.value
Объединив исходный код, мы обнаружили, что также можно напрямую прослушивать реактивный объект, и по умолчанию он будет выполнять глубокий мониторинг (deep=true), который рекурсивно обходит объект. Но прослушивание массива вызовет обратный вызов только при замене массива. Если вам нужно инициировать обратный вызов при изменении массива, вы должны указатьdeepопции. когда не указаноdeep = true:
const arr = ref([1, 2, 3]);
// 只有这种方式才会生效
arr.value = [4, 5, 6];
// 其他的无法触发回调
arr.value[0] = 111;
arr.value.push(4);
Лично рекомендуется максимально избегать глубокого прослушивания, потому что это может повлиять на производительность.В большинстве сценариев мы можем использовать метод прослушивания геттера, например, необходимость прослушивания изменений в массиве() => arr.value.length. Если вы хотите отслеживать изменения нескольких значений объекта одновременно, Vue3 предоставляет операции с массивами:
watch(
[() => state.id, () => state.name],
([id, name], [oldId, oldName]) => {
/* ... */
}
);
- Возвращаемое значение watch также является методом прекращения прослушивания, который существенно отличается от onInvalidate.Когда мы вызываем прекращение прослушивания, нижний уровень должен удалить коллекцию зависимостей, в настоящее время собранную эффектом, чтобы данные подчиненной зависимости изменились Не продолжать срабатывание, то есть «перестать слушать». и
onInvalidate, я лично считаю, что он обеспечивает операцию перед обратным вызовом.Для конкретного примера вы можете обратиться к статье, которую я написал ранее
Vue3 丨 Изменения Vue3 из 5 измеренийПодробности смотрите в разделе watchEffect vs watch.
14. watchEffect
Поделившись базовым кодом с часами, мы уже отразили его в анализе часов, друзья могут искать, и я не буду здесь вдаваться в подробности~
После просмотра стольких и немного сложных исходных кодов давайте взглянем на некоторые адаптивные виджеты API в Vue3. Друзья должны были видеть некоторые атрибуты с префиксом `__v_` в исходном коде. На самом деле, эти атрибуты используются для вынесения некоторых суждений. Давайте посмотрим:
15. isReadonly
Проверяет, является ли объект прокси-сервером только для чтения, созданным только для чтения.
function isReadonly(value) {
return !!(value && value["__v_isReadonly" /* IS_READONLY */]);
}
// readonly
const originalObj = reactive({ id: 1 });
const copyObj = readonly(originalObj);
isReadonly(copyObj); // true
// 只读 computed
const firsttName = ref('san');
const lastName = ref('zhang');
const fullName = computed(
() => firsttName.value + ' ' + lastName.value
);
isReadonly(fullName); // true
На самом деле при создании аксессора get было записано замыкание, а затем получено через соответствующий ключ, например:
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
// ...
if (key === '__v_isReadonly') {
return isReadonly;
}
// ...
};
}
16. isReactive
Проверьте, является ли объект реактивным прокси, созданным реактивным.
function isReactive(value) {
if (isReadonly(value)) {
return isReactive(value["__v_raw" /* RAW */]);
}
return !!(value && value["__v_isReactive" /* IS_REACTIVE */]);
}
Метод createGetter оценивает связанные:
// ...
if (key === '__v_isReactive' /* IS_REACTIVE */) {
return !isReadonly;
} else if (key === '__v_isReadonly' /* IS_READONLY */) {
return isReadonly;
}
// ...
17. isProxy
Проверьте, является ли объект прокси, созданным реактивным или только для чтения.
function isProxy(value) {
return isReactive(value) || isReadonly(value);
}
18. toRaw
toRaw можно использовать для печати исходного объекта, и иногда это удобнее, когда мы отлаживаем и просматриваем консоль.
function toRaw(observed) {
return ((observed && toRaw(observed["__v_raw" /* RAW */])) || observed);
}
toRaw Для преобразования объектов ref обернутый объект по-прежнему сохраняется, например:
const obj = reactive({ id: 1, name: 'front-refiend' });
console.log(toRaw(obj));
// {id: 1, name: "front-refiend"}
const count = ref(0);
console.log(toRaw(count));
// {__v_isRef: true, _rawValue: 0, _shallow: false, _value: 0, value: 0}
Метод createGetter оценивает связанные:
// ...
if (
key === '__v_raw' /* RAW */ &&
receiver === reactiveMap.get(target)
) {
return target;
}
// ...
Мы можем использовать объект, когда createGetter{key:原始对象,value:proxy 代理对象}Эта форма сохраняется в reactiveMap, а затем значение извлекается по ключу.
19. markRaw
Помечает объект, чтобы он никогда не был преобразован в прокси. Вернуть сам объект.
const def = (obj, key, value) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
});
};
function markRaw(value) {
// 标记跳过对该对象的代理
def(value, "__v_skip" /* SKIP */, true);
return value;
}
Метод createReactiveObject связан:
function createReactiveObject(target) {
//...
// 判断对象中是否含有 __v_skip 属性是的话,直接返回对象本身
if (target['__v_skip']) {
return target;
}
const proxy = new Proxy(target);
// ...
return proxy;
}
20. isRef
Определите, является ли это объектом ссылки.__v_isRefИдентификатор — это то, что мы назначаем в классе реализации RefImpl при создании ссылки.this.__v_isRef = true;
function isRef(r) {
return Boolean(r && r.__v_isRef === true);
}
Суммировать
Некоторые из вышеперечисленных 20 API практически не используются в нашем реальном проекте. Поскольку есть некоторые API, которые используются во всей структуре Vue3. Для нашего бизнес-сценария текущая частота использования должна бытьreactive,ref,computed,watch,toRefs...
Понимание всех реактивных API даст нам больше уверенности в кодировании и избавит от путаницы. Это также помогает нам лучше понять нижний уровень структуры, например: как используется прокси? Как Vue3 отслеживает простой тип? Как код может сделать нашу систему лучше. Первоначальная цель этой статьи — проанализировать эти API.
Как вы поняли эти 20 реактивных API?
😁 Интерфейс в порядке, обратите внимание~
В 2021 году официальная учетная запись будет сосредоточена на «усовершенствованном переднем плане», давайте вместе изучать Vue3, использовать Vue3 и углубляться в Vue3.
Наконец, я желаю своим друзьям счастливого Нового года и весеннего праздника~