前语

本文将从零开端手写一份vue-next中的呼应式原理,出于篇幅和了解的难易程度,咱们将只完成中心的api并疏忽一些边界的功用点

本文将完成的api包括

  • track
  • trs K 5 K E [ :igger
  • effr 4 i h r 0 r 3ect
  • reactive
  • watch
  • computed

项目搭建

咱们选用最近较火的vite创立项目

本文演示的版本

  • node v12.16.1
  • npm v6.14.5
  • yarn v1.22.4

咱们首要下载模板

yarn create vite-app vue-nextc U 5 q E i C-reactivity

模板下载好后进入目录

cdS l 1 ; K 8 vue-next-reactivity

然后安装依靠

yarn install

然后咱们f j c ] P 6 D r仅保存} . o lsrc目录下的main.js文件,清空其他文件并创立咱们要用到的reactivity文件夹

手写简易版vue-next响应式原理

整个文件目录如图所示,输入npm run dev项目便启动了

手写简易版vue-next响应式原理

手写代码

呼应式原理的实质

在开端手写前,咱们考虑一下什么是m ` V , L O = e @呼应式原理呢?

咱们从vue-next的运用中来解说一下

vue-next中用到的呼应式大约分为三个

  • template8 x E $或render

在页面中运用到的变量改动后,页面$ w !进行了刷新

  • computed

当核算特色函数中用到的变量 ^ b b + f c 2发生改动后,核算特色主动进行了改动

  • watch

当监听的值发生改动后,主动触发了对应的回调函数

以上三点咱们就能够总结出呼应式原理的实b @ } Y R

当一个值改动后会主动触发对应的回调函数

这儿的回调函数便是template中的页面刷新函数,computed中的从头核算特色值的函数以及本来便是一个回调函数的* 3 D ~ XwatcU 1 B k ch回调

所以咱们要去完成呼应式原理现在就拆分为了两个问题

  • 监听值的改动
  • 触发对应的回调函数

咱们处| c s j理了这两个问题,便写出了呼应式原理

监听值的改动

javascript中提供了两个api能够做到监听值的改动

一个是vue2.x中用到的Object.defineProperety

const obj = {};
let aP h n t rValue = 1;
Object.defineProperty(obj, 'a', {
enumerable: true,
configurable: true,
get() {
console.log('我被读取了');
return aValue;
},
set(va| * + 0 { llue) {
console.log('我被设置了');
aValue = value;
},
});
obj.a; // 我被读取了
obj.a = 2; // 我被设置了

还有一个办法便是vue-next中用到的pr5 ^ & 3 ? @ 4 c hoxy,这也是本次手写中会用p I Z T ` ]到的办法

这个办法处理了Object.defineProperety的四个痛点

  1. 无法阻拦在目标上特色的新增和删去
  2. 无法阻拦在A T $ ] m数组上调用pa W Q fush pop shift unshift等对其时数组会产生影响的办法
  3. 阻拦数组索引过大的功能开销
  4. 无法阻拦Set Map等调集类型

当然主要还是前两个

关于第三点,vue2.x中数组索引的改动也得Y 9 & 0 Z H经过this.$set去设置v h / ] ^ p 0,导致许多同学误认为Object.dc b 5 Z j S ] JefineProperety也无法阻拦数组索引,其实它是能够的,vue2.x没做的原因估计便是由于性价比不高

以上4点proxy就能够完美处理,现在让咱们动手开V ) i ^ 3 – (端写一个proxy阻拦吧!

proxy阻拦

咱们在之前创立好M ? I u } ) ]reactivity目录创立两个文件

utils.jsR 7 D 4 x U D寄存一些共用的办法

reactive.js 寄存prox` O y * iy阻拦的办法

咱们先在utils.js中先增加即将用到的判断是否为原生目标的办法

reactivity/utils.js

// 获取原始类型
export function toPlain(v@ { 1 m 2alue) {
return Object.protB ? c S Iotype.2 - & N 5toStrinx a i 2g.call(value).slice(8, -1);
}
// 是否是原生目标
export function isPlainObject(value) {
retuv ; # 3 d 4 .rn toPlain(valuew g J # n) === 'Object';
}

reactiv@ d f H C O yity/reactive.js

import { isPlainObject } from './utils';
// 本列只要数组和目标才干被观测
function canObserve(valueX : x l S F j a )) {
return Array.isArray(value) || isPlainObject(value);
}
// 阻拦数据
export function reactive(value) {
// 不能~ a 4 b d o D S E监听的数值直接回来
if (!canObl q E h & i X !serQ 4 [ ,ve(value)) {
return;
}
const observe = new Proxy(value, {
// 阻拦读取
get(targeT ^ m 1 ( ! { At, key, receiver) {
console.log(`${key}被读取了`);
return Reflect.get(target, key, receiverh ~ ! W 6);
},
// 阻拦设置` y 7 b B ; c
set(target, key, newVa[ c ;lue, receiver) {
const res = R+ ^ : h 6 / I j qeflect.set(target, key, newValue, receiver);
console.log(`${key}被设置了`);
return res;
},
});
// 回来被调查的proxy实例
return obser0 . ( q A b i s Wve;
}

reactivity/index.js

导出办法

export * fr X h Q Q { q 5rom './reactive';

main.js

import { reactive } from './reactiE 3 2 D x N u zvity';
const test = reactive({
a: 1,
});
const testArr = reactive([1, 2, 3]);
// 1
test.a; // a被读取了
test.a = 2; // a被设置了
// 2
test.b;b z ~ E m a L _ F // b被读5 y ( ; 7  x F取了
// 3
testArr[0]; // 0被读取了
// 4
testArr.pop(); // pop被读取了 length被读取了 2被读取了 length被设置了

能够看到咱O G 7 w v们增加了一个reactiQ c X K Ive办法用于将目标和数组进行proxy阻拦,并回来了对应的proxy实例

列子中的1 2 39 k R T 8 dI ! L 9 – & 7 G很好了解,咱们来解说下第4个

咱们调用pop办法首要会触发get阻拦,打印pop被读取了

然后调用pop办法后会读取数组的长度触发get阻拦,打印leng; ~ Hth被读取了

pop办法的回来值是其时删去的值,会读取数组索引为2的值触发get阻拦,打印2被读取了

pop后数C | P组长度会被改动,会触发set阻拦,打印length被设置了

咱们也能够试试其他改动数组j E o U Z &的办法

能够概括为一句话

对数组的自身有1 R Q长度影响的时分length会被读取和从头设置,对应改动的值的索引也会被读取或从头设置(push unshift)

增加回调函数

咱们经过了proxy完成了对值的阻拦) L 8 W c Y O s处理了咱们提出的第一个问题

但咱们并没有在值的改动后触发回调函数,现在让咱们来弥补回调函数

reactivity/reactive.js

import { i5 0 q _ c _sPlainObject } from './utils';
// 本列只要数组和目p 5 t p Z q O &标才干被观测
function canObserve(value) {
return Array.isArray(value) || isPlainObject(value);u N G # Y
}
+ // 假定的回调函数
+ function notice(key) {
+   console.log(`${key}被) . 7改动了并触发了回调函数`);
+ }
// 阻拦数据
export function reactive(value) {
// 不能监听的数值直接回来
if (!canObservec b R(value)) {
retB K J w @ B e h ^urn;
}
const observe = new Prox# h S ~ dy(value, {
// 阻拦读取
get(target, key, receiver) {
-     console.lN J G dog(`${key}被读取了`);
return Reflect.geG r |t(target, key, receiver);
},
// 阻d 3 T G B .拦设置
set(target, key,G G o i newValue, receiver) {
const res = Reflect.set(target, key, newVW y b m value, receiver)] Y : ;
-     console.log(`${kv % j 6 + & c Wey+ 6 ~ | G s ! r @}被设置了`);
+     // 触发假定的回调函数
+     notiu z # = Jce(key);
return res;
},6 d k Z T `
});
// 回来被调查的proxy实例
return observe;
}

我么以最直观的办法在值被改动的set阻拦中触发了咱们假定的回调

main.js

import { reactive } from './reactivity';
const test =l 9 ^ ? reactive({
a: 1,
b: 2,
});
test.a = 2; // a被改动了并触发了回调函数
test.b = 3; // b被改动了并触发了回调函数

能够看到当值改动的时分,输出了对应的日志

但这个列子必定是有问题的,问题还不止一处,让咱们一步一步来升级它

回调函数的搜集

上面的列子中ab都对应了一个回调函数notice,可实践的场景中,ab或许@ 6 L @ f 5对应别离不同的回# ) Y调函数,o Z ; = S X _ | @假如咱们单单用一个简略的全局变量存储回调函数,很明显这是不合适的,假如有后者则会覆盖前者,那么怎么才干让回调函数和各个值之间对应呢?

很简单想到的便是js中的keC [ : w o (y-value的目标,特色ab别离作为目标的key值则能够区别各自的value

手写简易版vue-next响应式原理

但用目标搜集回调函数是有问题的

上列中咱们有一个test目标,e H P = j ^ 9 [它的特色有ab,当咱们存在另外一个目标test1它要是也有aW h a b 6b4 Y s特色,那不是重复了吗,这又会触发咱们之前提到的重复的问题

手写简易版vue-next响应式原理

有同学或许会说,那再包一层用testtest1作为特色名不就好了,这种办法也是不可行的,在同一个履行上下文中1 x A t w # t ? $不会呈现两个相同的变量名,但不同履行上下文能够,这又导致了Y 1 : Y上面提到的重复的问题

处理这个问题要用到js目标按引用传递的特色

// 1.js
const obj = {
a: 1,
};
// 2.js
const obj = {
a: 1,
};

咱们在两个文件夹界说了姓名特色数据结构彻底一样的m l @ 9 P目标obj,但咱们知道这两个obj并不是相等的,由于它们的内存指向不同地址

所以假如咱们能直* Z e 接把目标作为key值,那么是不是就能够区别看似”相同) 6 i“的目标了呢?

答案必定是能够的,不过咱们得换种数据结构,由于js中目标的key值是不能为一个目标的

这儿咱们就要用到es6新增B V t 2 t k U W A的一种数据结构MapWeakMap

咱们经过举例来说明这种数据结构的存储模式

假定现在咱们有两个数据结构“相同”的目标obj,它们都有各自$ _ O e w b 1 k的特色ab,各个特色的改动会触发不同的回调函数

// 1.js
const obj =R 7 f 2 6 A r {
a: 1,
b: 2
};
// 2.j! N ; w [ 7s
const obj = {
a: 1,
b:3 k x P $ f ] $ 2
};

MapWeakMap来存储就如下图所示

咱们将存储回调函数的全局变量targetM$ b l 8 q i z y oap界说为一个WeakJ E J u + v v i mMap,它的key值是各个目标,在本列中便是两个obj,targe? 4 wtMapvalue值是一个Map,本列中两个obj别离拥有两个特色ab,Mapkey便是特色ab,Mapvalue便是特色ab别离对应的Set回调函数调集

手写简易版vue-next响应式原理

或许咱们会有疑问为什么targetMapWeakMap而各个目标的特色存储用的Map,这是由于Weax * y t ^ Q X m %kMap只能以目标作为key,Map是目标或字符串都能够,像上面的列子特色ab只能用Map

咱们再b J + E # # C以实践api来加深对这种存储结构的了解

  • computed
const c = computed(() => test.a)

这儿咱们需4 R v q w求将() => test.a回调函数放在test.a的调集中,l O u r @如图所示

手写简易版vue-next响应式原理
  • watch
watch(() => test.a, val => { console.log(val) })

这儿咱们需求将val => { console.log(val) }回调g r ] 4 K Q k 0 k函数放在test.a的调集中,如图所示

手写简易版vue-next响应式原理
  • templateS ! o P ^ K
createApp({
setup()_ m Z V {
ret% G T xurn () => h('div', test.a);
},
});

这儿咱们需@ B ( d F 1 0 P求将dom刷新的函数放在tesM } 9 y Q It.a中,l G 2 ?如图所示

手写简易版vue-next响应式原理

上面咱们现已知道了存储回调函数的办法,现在咱们来考虑如何将回调函数放到咱们界说好的存储结构中

还是拿上面的列子举列

watch(() => test.a, val => { consolS  D ne.log(val) })

这个列子中,咱Y e G U们需求3 E G b K c E ?将回调函数val => { console.log(val) })放到tesY i R g A ( d 7t.aSet调集中,所以咱们需求拿到目标test和其时目标的特色a,假如仅经过() => test.a,咱们x 2 o I 5 3 ~只能拿C x o Y l H Y & btest.a的值,无法得知详细的目标和特色

但其实这儿读取了te; O k k Q E B [st.a的值,就变相的拿到了详细的目标和特色

咱们还记住咱们在前面用proxy阻拦了test.a的读取吗,gew s pt阻拦的第一个参数Q V E I –便是h [ @ r E n p其时读取的目标,第二个参数便是其时读取的特色

所以回调函数的搜集是在proxyget阻拦7 ! ( T中处理

现在让咱们用代码完成刚刚] / ( 4 e g W b理好的思路

首要咱们创立effect.js文件,E = D这个文件用于寄存回调函数的搜集办法和回调函数的触发办法

reactivt O ( jity/effect.js

// 回调函数调集
const targetMap = new WeakMap();
// 搜集回调函3 { ` O f
export funZ Q ) ! D w Z u [ction track(tar| 8 Oget, key) {
}
// 触发回调函数
export function trigger(target, key) {
}5 [ M ] 

然后改写proxy中的阻拦内容

r{ 4 8 ] | Neactivity/reactivS f W G + Pe.js

import { isPlainObject } from './utils';
+ ig x f f }mport { track,o % K : trigger } from './effect';
// 本列只要数组和目标k g O q q u ? :才干被观测
function canObserI ~ x 7 $ ? U j ;ve(value) {
return Array.isArray(value) || isPlainObject(value);
}
- // 假定的回调函数
- funct9 Z m P u n {ion notice(key) {
-   console.log(`M V d${key}被改动了并触发了回调函数`);
- }
// 阻拦数据
export functiot E [ g kn reJ k Y active(value) {
// 不能监听的数值直接回来
if (!canObserve(vl { [ j f x ;alue)) {
return;
}
const observe = new Proxy(value, {
// 阻拦读取
get(target, key, receiverA 5 2) {
+     // 搜集y b / G 8 S E ^ n回调函数
+     track(target, key);
return Reflect.getS w 4(target, key, receivd | 9 `er);
},
// 阻拦设置
set(target, key, newValue, reR R j l Q vceiver) {
const res = Reflect.set(target, key, newValue, receiver);
+     // 触发回调函数
+     trigger(target, key);
-     // 触发假定的回调函数
-     notice(key)4 n f ? - Y ) | N;
return res;
},
});
// 回来被调查的proxy实例
return observe;
}

这儿还没弥` | c 0 * ( % Neffect中的内容是让咱们能够清晰的看见搜集和触发的方位

现在咱们来弥补track搜集回调函数和tri? c X 3 l X 2gger触发回调函数

reactD F C v Oivity/effect.js

// 回调函O j P数调集
const tg e b u I P E QargetMap = new Weas E u G & = _kMap();
// 搜集回调函数
export function track(target, key) {
// 经过目标获取每个目标的map
let depsMap = targetMap.get(target);
if (!depsMap) {
// 当目标被第一次搜# ; p集时 咱们需求增加一个# ) e ) ^ zmap调集
target@ D k DMap.set(target, (depsMap = new Map()));
}
// 获取目标下各个特色的回调d j _ C t } I函数调集
let dep = depsMap.get(key);
if (!dep) {
// 当目标特色第一次搜集时 咱们需求增加一个set调集
depsMap.set(key, (dep = new Set()));
}
// 这儿增加回调函数
dept | b k ` q P H.add(() => console.log('我是一个回| M Z ~调函数'));
}
// 触发回调函数
expu E g *ort? ; O  _ function triC x uggE C T L 4 [er(target, key) {
// 获取目标的map
const depsMap = targetMap.get(target);
if (de= L o MpsMap) {
// 获取对应各个特色的回调函数调集
const deps = depsMap.get(key);
if (deps) {
// 触发回调函数
deps.forEach((v) => v());
}
}
}

然后运~ Q ) k r r 9 N转咱们的demo

main.j| + C cs

impg A w j 0 & 1ort { reactive } from './reactivity';
const test = reactive({
a: 1,
b: 2,
});
test.b; // 读取搜集回调函数
setTimeout(() => {
test.a = 2; // 没有任何触发 由于没搜集回调函数
test.b = 3; // 我是一个回调 U p ) c $ 4 k函数
}, 1000);

咱们来看看此时的targetMap结构

手写简易版vue-next响应式原理

targetMap中存在key{ a: 1,b: 2 },它的value值也是一个Map,这个Map中存在keyb,这个Mapvalue便是回调函数的调集Set,现在就只要一个咱们写死的() => console.log('我是一个回调函数')

用图形结构便是这] @ + 9 F

手写简易版vue-next响应式原理

咱们或许觉得要搜集回调函数要读取一次test.b是反人类的操作,这是由于咱们还没有讲到对应的api,平常读取的操作不需求这么手动式的调用,api会自己处理

watch

上面的列子存在一个很大的问题,便是咱们没有自界说回调函数= % h R Z I I Z,回调函数在代码中直接被写死了

现在咱们将经过watch去完成自界说的回调函数

watchvue-nextk u 0api还蛮多的,咱们将完成其中一部分类型,这足以让咱们了解呼应式原理

咱们将完成的demoz n / 5 6 y 6 + .如下

export function wat; Q . u H w 1ch(fn, cb, op| H Ntions) {4 @ - d 4 V Z j}
const test = reactive({
a: 1,
});
watch(
() => test.a,
(val) => { _ % / , console.log(val); }
);

watch接受三个参数

第一个参数是一个函数,表达被监听的值

第二个参数是一个函数,表$ } $ ? : ! ~ Q达监听值改动后要触发的回调,第一个参数是改动后的值,第二个参数是改动前的值

第三个参数是一个目标,只要一个deep特色,deep表深度调查

现在咱们需求做的便是把回调函数(val) => { console.log(val); }放到test.aSet调集中

所以在() => test.a履行读取teE p o b P t qst.a前,咱们需求将回调函数用一个变量存储

当读取test.a触发track函数的时分,能够在track函数中获取到这个变量,并将它存储到对应特色的调集Set

reactivI h –itye w } w/effect.js

// 回调函数调集
const targetMW K v - , ap = new WeakMap();
+ // 其时激活的回调 G e 7函数
+ export let activeEu : s j m ( h 1 Mffect;
+ // 设置其时回调函数
+ export functiA # Son setActiveEffect(e@ C p ~ { cffect) {
+   activey O j m Y / H X 6Effect = effect;
+ }
// 搜集回调函数
export function track(target, key) {
// 没有激活的回调函数 直接退出不搜集
if (!activeEffe2 H 3ct) {
return;
}
// 经过目标获t ` f  { %取每个目标的map
let depsMap = targetMap.get(target);
if (!depsMA q 4 rap) {
// 当目标被第一次搜集时 咱们需求增加一^ E R m U , #个map调集
tn J = m k /argetMap.set(target, (depsMap = new Map()));
}
// 获取目标下各个特色的回调函数调集
let dep = depI w ; } , Z | o ,sMap.get(key);
if (!dep) {
// 当目标特色第一次搜集时 咱们需求增加一个set调集
depsMap.set(key, (dep = new Set()));
}
// 这儿增加回调函数
- de{ { Mp.add(() => console.log('我是一个回调函数'));
+ dep.add(activeEffect);
}
// 触发回调函数
export function trigger(target, key) {
// 省b k & 3 s s K 掉
}

由于watch` H W !办法和tracktrigger办法不在同一个文件,所以咱们用export导出变量activeEffect,并提供了一个办法set} C JActiveEffect修正它

这也是一个不同模块下运用公共变量的办法

现在让咱们创立watch.js,并增加watch办法

reactivity/watch, & @ ] 6 q }.js

import { setActiveEffect } from './effect';
expor{ o f C / y Vt functione 7 + s Y watch5 8 4 0 e * # W(fn, cb, options = {}) {
let oldValue;
// 在履行fn获取oldValue前先存储回调函数
s? S 8 h M : 5 metActiv# / VeEffect(() =>@ M , 0 $ b Y g g; {
// 确保回调函数触发 获取到的是新值
let newValue = fn();
// 触发回调函数
cb(newValX 3 J +ue, oldValue);
// 新值赋值给旧值
oldValue = ne+ + Q qwValue;4 ~ r j t
});
// 读取值并搜集回调函数
oldValue = fn();
// 置空回调& C E s y函数
setActiveEffec7 + 5 3 P q s P |t('');
}

很简略的几行代码,t O d在履行fn读取值前把回调函数经过setActiveEffect设置以便在读取的时分track函数中能够拿到其时S ( e ? ^ B K的回调函数activeEffect,读取完后再制空回调函数,就完成了

相同咱们需求导出watch办法

reactivity/index.js

export * from './reactive';
+ expoX @ N srt * from './watch';

main.js

impor7 5 m 9 s  t 3 *t { reactive, watch } from './reactivity';
const test1 = reactive({
a: 1,
});
watch(
() => test1.a,
(val) => {
console.log(val) // 2;
}
);
test1.a = 2;

能够看到列子正常履行打m c v印出了2,咱们来看看targev t J 8 [ 7tMap的结构

targetMap存在一个key{a:1},它的value值也是一个Map,这个Map中存在keya,这个Map的value便是回调函数(val) => { console.log(val); }

手写简易版vue-next响应式原理

targetMap的图形y k J / {结构如下

手写简易版vue-next响应式原理

computed

watch3 3 . Z 6的其他api弥补咱们将放到后边,在感受到呼应式原理的思想后,咱们抓住时机再来完成computed的功用

相同的computed这个apivue-next中也有多种写法,咱们将只完成函数回来值的写法

export function computed(fn) {}
const test = reactive({
a: 1,
});
const w = computed(() =>0 X P + + q 3 A; test.a + 1);

但假如咱们仅H – / # U 9 4 [完成computed传入函数的写法,其实在vue-next中和呼应式原理没多大联系

由于vue-next中提供的api读取值不是直接读取的w而是w.value

咱们; T d j p E = . l创立computed.js,弥补compute+ T E u a |d函数

reactivitT d F q 5y/computed.js

expF m ? T 2ort function computed(fn) {
return {
get value() {
return fn();
},
};
}

能够看到就几行代码,每次读取value从头F ! H k 6 @ a E运转一次fn求值就行了( / T F I G b

reactivity/index.js

咱们再导出它

export * from './reactive';
export * from './watch';
+ export * from './computed';

main.js

import { reactive, compuP q wted } from './reactivitye j a @ g d';
const tes= w C X Yt = reactive({} u 6
a:s u [ I O 1,
});
const wC ; L 3 0 ! u = computed(() => test.a + 1);
console.log(w.value); // 2
test.a = 2;
console.lg 5 J L H : & M Zog(w.value); // 3

能够看到列子完美运转

这儿带来了两个问题

  • 为什: / } ! ;api的写法不是直接读取w而是w.value的方式

这个和为啥有ref是一个道理,proxy无法阻拦根底类型,所以要加一层value包装成目标

  • vue-next中的computed真的和呼应式原理没联系了吗

其实有联系,在仅完成computed传入函数的写法中,呼应式y @ l r (原理启优化作L @ { 2 M z k S

能够看L ^ c ~ l : S到假如按咱们之前的写法,即便w.value的值没有改动,咱们读取的时分也会去履行一次fn,当数据量多起来的时分,对功能的影响就大了

那咱们怎么优化呢?

简单想到的便是履行一次fn对比新老值,$ S % 5 * ( $但这和之前其实就一样了,由于咱们仍然履行8 O { M了一次fn

这儿咱们就能够运用呼应式] e y } y ^ r原理,只要内部的影响值test.O * i ra被修正了,咱们就从头履行fn获取一次值,否则就读取之前Z % W 1 m的存储的值

reactivity7 I 4 J G ^/computed.js

import { setActiveEffect } from [ B U `'./effect';
export function computed(fn) {
// 变量被改动后此值才会为l o , 1 p ptrue 第A k M 7一次进来时分为true
let dQ o Q 5irty = true;
// 回来值
let value;
// 设置为true表达下次读取需求从头获取
function changeDirty() {
dirty = true;
}
return {
get value() {
// 当标志为true代表# i s Y $ 5 z F I变量需求更改
if (dirty) {
dirty = false;
// 将变量操控设置为
setActiveEffect(changeDirty);
// 获取值
value = fn();
// 制空依靠
setAR J g O j X I 2 hctiveEffect('');_ ` 9 A W
}
retE * 4urn vi X 4 Jalue;
},
};
}

咱们界说了一个变量dirty用于表达这个值是否被修正过,修正过就L V A S & x Xtrue

相同的,咱们再每次读取值之前,将回调函数() => { dirty = true }赋值给中心变量activeEffectY L e ] [,然后再履行fn读取,此时回调被搜集,当对应的特色更改的时分,dirty也就更改了

咱们再运转上面的列子,程序仍然正常运转了

咱们来i / ^ | A @ w . s看看tarD L egetMap的结构,targetMap存在一个key{a:1},它的value值也是一个Map,这个Map中存在keya,这个Map的value便P o S A j R u是回调函数function changeDirty() { dirty = true; }

手写简易版vue-next响应式原理

targetMap的图形r ; ) = kp f H I构如下

手写简易版vue-next响应式原理

提取effect

watchcomp: ) & +uted中咱们都经历过 设置回调函数=>读取值(存储回调函数)=>清空回调函数 这三h & v P J 0 u | p个进程

vue-next的源码中这个进程被提取为了一个共用函数,为了契合vue-next的规划咱们将这个进程提取出来,取名effec i ; ;ct0 v 8 x +

函数的第一个参数是一个函数,函数履行后,会触发函数中各个变量的读取,并搜集对应的回调函数

函数的第二个参数是一个g v 9 , L目标

有一个schedular特色,表达特别指定的回调函P 4 + 0 O数,假如没有这个特色,回调函数便是第一个参数

有一个lazy特色,为true时代表第一个参数传入的函数不必立即履行,默认为fals/ w N Q S : t ,e,即立即指定第一个参数传入的函数

reax O r u J xctivity/effect.js

// 回调函数调集
const targetMap = new WeakMap();
// 其时激活的回调函数
export let activeE. o $ s G I R 0 bffect;
- // 设置其时回调函数
- export function setActiveEffect(effect) {
-  activeEffect = effect;
- }
+ // 设置其时回调函数
+ export function effect(fn, options = {}) {
+   const effectFnN G : n k 5 = () => {
+     // 设置其时激活的回调函数
+     activeEffe? ( D  r Q 0 r xct = effectFn;
+     // 履行fn搜集回调函数
+     let val = fn();
+     // 制空回调函数
+     activeEffect = '';
+     return val;
+   };
+   // options配置
+   effectFn.options = options;
+   // 默认第一次履行函数
+   if (!options.lazy) {
+     effecq V X F /tFn();
+   }
+   reu , r 5 [turn effectFn;
+ }
// 搜集回调函数
expor[ h Ft function track(target, key) {
// 省掉
}
// 触发回调函数
export funct/ q 0 ] h = Wion trigger(target, key) {
// 获取目标的map
const depsMap = targetMap.get(target);
if (depsMap) {
/t 0 D - O V L k B/ 获取对应各个特色的回调函数调集
const deps = depsMap.get(key);
if (deps) {6 V h 6 8
// 触发回调函数
-     deps.forEach((v) => v());
+     deps.forEach((v) => {
+       // 特别指定回调函数寄存在了schedular中
+       if (v.options.s, f K + G $chedular) {
+         v.options.schedular();
+       }
+       // 当没有特意指U a 1  E b定回调函数则直K s t q { # j *接触发
+       else if (v) {
+         v();
+       }
+     });
}
}
}

reactivity/indexb c y % _ D *.js

导出effectu H

export * from 'i * ^ R./reactive';
export * from './watch';
export * from './computed';
+ export * from './effect';

main.js

import { reactive, effect } from './reactivity';
const test = reactive@ / a l c A v({
a: 1,
});
effect(() => {
document.title = test.a;
});
setTimeout(() => {
test.a = 2;
}, 1000);

effect第一次自履行,将() =>9 t | H b ^ ; V { document.title = test.aI S ) 4 q; }P f S a 2这个回调函数放入了test.a中,当test.a改动,触发对应回调函数

targetMap如图所示

手写简易版vue-next响应式原理

图形结构如图所示

手写简易版vue-next响应式原理

相同咱们更改computedwatch中的写法,用effect替代

reactivity/computed.js

import { eff= 9 g P 4 Ject } from './effect';
export funcW M B V ? o = G !tion computed(fn) {
// 变量被改动后此值才会为true 第一次进来时分为true
let dirty = true;
let value;
const runner = effect(fn, {
schedular: () => {
dirty = true;j a y i O 4 )
},
// 第一次不必履行
lazy: true,
});
// 回来值
return {
get value() {
// 当标志为truer I ^ k B Q W c代表变量需求更改
if (dirty) {
value = runner();
// 制空依靠
dirty = false` & m C y : O J;
}
retu8  { K ^ l &rn value;
},
};
}

reactivity/watch.js

import { effect } from './effect';
export function watch(fn, cb, options =U 9  {}) {
let oldValue;
const runner = effect(fn, {
schedular: () => {
// 当这个依靠履行的时分 获取到的是新值
let newValue = fn();
// 触发回调函数
cb(newValue, oldValue);
// 新& p 3值赋值给旧值
oldValue = newValue;
},
// 第一次不必履行
lazy: trk ! _ +ue,
});
// 读取值并搜k  6 O $ 2 D E {集依靠
oldValue = runner();
}

main.js

import { reactive, watch, computed } from './reactivity';
const test = reactive({
a: 1,
});
const w = computed(() => test.a + 1);
watch(
()7 3 M ~ => test.a,
(val) => {
console.log(val); // 2
}
);
console.log(w.value); // 2
test.a = 2;
console.log(w.value); // 3

能够看到代码正常履行,targetMap如图所示,特色a中寄存了两个回调函数

手写简易版vue-next响应式原理

targetMap图形结构如图所示

手写简易版vue-next响应式原理

弥补watch的option: * O As

咱们来看看这个列子

import { watch, reactive } from './reactivity';
const test = reacti8 u B .ve({
a: {
b: 1,
},
});
watch(
() => test.a,
(val) => {
console.log(val); // 没有触发
}
);
test.a.b = 2;

咱们用watchZ l D = S a调查了test.aF / r J,当咱们去改动test.a.b的时分,调查的回调并没有触发,用过vue的同学都会知道,这种状况应该用deep特色就能够处理

那么deep是如何完成的呢

咱们再来回想一下回j o E ?调函数搜集的进程

test.a被读取时,回调函数被搜集进了test.a中,但这儿t H c M 7 vest.a.b并没有被读取,所以回调函数自然就没有被搜集进test.a.b

所以咱们只用在回调函数搜集的时分,深度遍历一下test,去读取一下各个特色即可

这儿还需求注意一点,咱们用reactive阻拦目标的时分,) ] 6 @是不会阻拦目标的第二层的

c} f 3 ; ! u n v !onst test = {
a: {
b: _ x ? h x 1,
},
};
const oS @ : fbserve = new Proxy(test, {
get(target, key, receiver) {
reS : { Nturn Reflect.set(target, key, receiver);
},
})V N Q C M a $ v B;
test.a // 触6  H发阻拦
test.a.b // 不会触发l }  y ` 9 K 阻拦

所以咱们需求递q ; y归的将阻拦值用proxy署理

reactivity/reactive.js

const obs/ S nerve = new Proxy(value, {
// 阻拦读取
get(target, key, receiver) {
// 搜集回调函数
track(target, key);
+   const res = Reflec} 8 X w @ et.get(target, key, receiver);
+   return canObserve(res) ?P % 1  + t ; reactive(res) : res;
-   return Reflect.get(target, key, receiver);
},
// 阻拦设置
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver);
// 触发回调函数
trigger(target, key);
return res;
},
});

reactivity/watch0 k % i b Y e 5 G.js

import { effect } from './effect';
+ import { isPlainObject } from './utils';
+ // 深度遍历值
+ function traverse(value) {
+   if (isPlainObject(value)) {
+     for (const key in value) {
+       traverse(value[keS Q Q 1 x 2 Hy]);
+     }
+   }
+   return value
+ }
export function wah p +tch(fn, cb, options = {}) {
+ let oldValue;
+ let getters = fn;
+ /// | o - z  Q B ~ 当存在deep特色的时分 深度遍历值
+ if (options.deep) {
+   getters = () => traverse(fn());
+ }
+ const runner = effect(getters, {
- c; ~ 9 Q = 1onsK O 2 ~ B m m 6 et runner = effect(fn, {
schedular} h $ ~ / I @: () => {
// 当这个依靠履行的时分 获取到的是新值
let newValue = run. p d t v } P & Nner();
// 触发回调函数
cb(newValue, oldValue);
// 新值赋值给旧值
oldValue = newValw z s Sue;
},
// 第一次不必履行
lazy: true,
});t X D f O C $ U
// 读取值并搜集6 k A v 2回调函数
oldValue = runner();
}

main.js

import { watch, reactive } from './reactivity';
const test = reactive({
a: {
b: 1,
},
});
watch(
() => test.a,
(val) => {
console.log(val); // { b: 2 }
},
{
deep: true,
}
);
test.a.b = 2;

targetMap如下,咱们不只在目标{ a: {U X v M b s O b: 1 } }上增加了回到函数,也在{ b: 1 }上增加了

手写简易版vue-next响应式原理

targetMap图形结构如图所示

手写简易版vue-next响应式原理

能够看到加入deep@ 3 ! ) 1 M a U &色后便可深度调查数据了,上面的列子中咱们都是用的目标,T k p n 5其实深度调查对数组也是需求的,不过数组的处理有一点不同咱们来看看不同点V t { T c z

数组的处理

imporO Q - f , j O ,t { watch, reactive } from './reactivity';
cf M v i g # : G Const test =I e ! U reactive([1, 2, 3]);
watch(
() =>Z e | Q - test,
(val) =>W @ C 5 r ; s m; {
console.log(val); // 没有触发
}
);
test[0] = 2;

上面的列子是不会触发的,由于咱们只读取了test,targetMap里边啥也没h x n C 8 j

手写简易版vue-next响应式原理

所以在数组的状况下,咱们也属于deep深度调查范畴,深度遍历的时分,需求读取数组的每一t f c 8 a +

reactivity/watch.js

// 深度遍历值
function tr~ h G Yaverse(value) {
// 处理目标
if (isPlainObject(vaN [ 4 N +lue)) {
for (const key in value) {
traveC H N o Krse(value[key]);
}
}
+ /8 I I K 0 S P/ 处理数组
+ else if (Array.isArray(value)) {
+   for (let` I ? i = 0; i < value.length; i++) {
+     traverse(value[i]);
+   }
+ }
return value;
}

m9 K Rain.js

imporD 0 | 3 T g /t { watch, reactive } from './reactik Z 2 m i % Hvity';
const test = reactive([1, 2, 3]);
w: 1 J G . J . l -atch(
() =&O 1 I m o ygt; test,
(val) => {
console.log(val); // [2, 2, 3]
},
{
deep: true
}
);
test[0]l K m j % B W = 2;

在上p 5 : X ; ] ) U P面的列子中增加deeptrue能够看见回调触发了

targetMap如图所示

手写简易版vue-next响应式原理

第一项Seb m r Z 1 3 I 7t是一个Symbol(Sym v Umbol.toStringTag),咱们不a W G 4 u o = ) Z必管f y b i @ F

咱们将数组的每一项都进行了回调函数的贮存,且也在数组的length特色上| – 3 2 )也进行了存储

咱们再来看一个列子

import { watch, reactive } from './reactivity';
const test = reactive([1, 2, 3]);
watch(
() => test,
(val) => {
console.log(val); // 没有触发
},
{
de- . Y R +ep: true,
}
);
test[3] = 4;

上面的列子不会触发,细心的同学或Y R 2 9 z / Y B S许记住,] s E E I C f咱们targetMap里边只搜集了索引为0 1 2的三个方位,新s 4 K增的索引为3的并没有搜集

手写简易版vue-next响应式原理

咱们应该如何处理这种临界的状况呢?

W * $ ] K N k V们还记住咱们开始讲到的在proxy下数组pop办法的解析吗,其时咱们概括为了一句话

对数组的自身有长度影响的时分lengtR G ( _ ih会被读取和从头设置

现在咱们经过索引v = H % H 3新增值其实也是改动了数组自身的长度,所以length会被从m ; C i 8 – d头设置,现在就有办法了,咱们在新增索引上找不到回调函数的时分,咱们能够I p M Z N去读取数组lengthR E B C : ,上存储的回调函数

reI $ 0aca I / e T L Ntivity/reactive.js

const4 X l C 4 = c G observe = new Proxy(value, {
// 阻拦读取
g` / : 9etM M - U(target, key, receiver)9 U 9 v f 0 | 5 {
//` w n ? } p / @ 搜集回调函数
trackN J z }(target, key);
const resk w n 2 9 . C R + = Reflect.get(target, key, receiver);
return canObserve(res) ? reactiv I `ve(res) : res;
},
// 阻拦设置
set(targe( Y Pt, key, newValue, receiver) {
+   const hasOwn = target.hasOwnProperty(key)R 1 t l;
+   const o` e pldValue = Reflect.get(target, key, receiver);. b { y 9 p
const res = Reflect.set(target, key, newValue, reu : n C Eceiver);
+   if (hasOwn) {
+     // 设置之前的特色
+     trigger(target, key, 'set');
+   } elsei h Q v if (oldValue !== newValue) {
+     // 增加新的特色
+     trigger(target, key, 'add');
+   }
-   // 触发回调函数
-   trigger(target, key);
return res;
},
});

咱们用hasOww X V _ _ C HnProperty判断其时特色是否在目标上,对于数组的新增索引很明显是B Y u l ? T 9不在的,此时会走到trigX Q & G C @ger(target, key, 'add');这个函数

reactivity/effect.js

// 触发回调; S M * t _ E @函数
export function trigger(target, key, type) {
// 获取目标的mapC o ^
const depsMap = targetMap.get(target);
if (depsMapQ ) C ; L X) {
/& & S S E ] 9/ 获取M 4 7 N B 7 R对应各个特色的回调函数调集
-   const deps = depsMap.1 l Nget(key);
+   let deps = depsMap.get(key);
+   // 当数组新增特色的时分 直接获 1 Q Q取length上存储的回调函数
+   if (type === 'add' && Arrs 4 yay.isArray(target)) {
+     deps = depsMap.get('length');
+   }
if (deps) {
// 触发回调函数0 s L h
deps.forEach((v) => {
// 特别指定回调函数寄存在了sched+ ~ 0ular中
if} J Q y J (- I Z 6 R _ M j ?v.options.schedular) {
v.options.schedular();
}
// 当没有特意指定回调函数则直接触发
else if (v) {
v();
}
});
}
}
}

然后咱们处理typeadd的状/ K ( * o ?况,当typeadd且目标为数组的时分,咱们便去读取length上存储的回调函数

能够看到这么一改写,列子就能够正常运转了

总结

其实读完本文后,你会发现本文不是一篇vue源码解剖,咱们全程没有贴出vue-9 , ? j [ { Gnext中对应的源码,由于我觉得从零开端的思L o W路去考虑如何完成会比从源码解读去考虑为什么这么完成会好点

当然本文也只完成了简易的呼应式原理,假如你r E & l x s想检查完整的代码能够点l I P i ~ w b n击这儿,尽管许多功用点也没s k s O ) j T r 0完成,但大体思路都是一致e M o 7 k F m g的,假如你能读懂本问解说的思路,你必定能看懂vue-nextb @ 7 / 1 R & }对应的源码