前语
本文将从零开端手写一份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
文件夹
整个文件目录如图所示,输入npm run dev
项目便启动了
手写代码
呼应式原理的实质
在开端手写前,咱们考虑一下什么是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
的四个痛点
- 无法阻拦在目标上特色的新增和删去
- 无法阻拦在A T $ ] m数组上调用
pa W Q fush
pop
shift
unshift
等对其时数组会产生影响的办法 - 阻拦数组索引过大的功能开销
- 无法阻拦
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 d
都I ! 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被改动了并触发了回调函数
能够看到当值改动的时分,输出了对应的日志
但这个列子必定是有问题的,问题还不止一处,让咱们一步一步来升级它
回调函数的搜集
上面的列子中a
和b
都对应了一个回调函数notice
,可实践的场景中,a
和b
或许@ 6 L @ f 5对应别离不同的回# ) Y调函数,o Z ; = S X _ | @假如咱们单单用一个简略的全局变量存储回调函数,很明显这是不合适的,假如有后者则会覆盖前者,那么怎么才干让回调函数和各个值之间对应呢?
很简单想到的便是js
中的keC [ : w o (y-value
的目标,特色a
和b
别离作为目标的key
值则能够区别各自的value
值
但用目标搜集回调函数是有问题的
上列中咱们有一个test
目标,e H P = j ^ 9 [它的特色有a
和b
,当咱们存在另外一个目标test1
它要是也有aW h a b 6
和b4 Y s
特色,那不是重复了吗,这又会触发咱们之前提到的重复的问题
有同学或许会说,那再包一层用test
和test1
作为特色名不就好了,这种办法也是不可行的,在同一个履行上下文中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的一种数据结构Map
和WeakMap
咱们经过举例来说明这种数据结构的存储模式
假定现在咱们有两个数据结构“相同”的目标obj
,它们都有各自$ _ O e w b 1 k的特色a
和b
,各个特色的改动会触发不同的回调函数
// 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
};
用Map
和WeakMap
来存储就如下图所示
咱们将存储回调函数的全局变量targetM$ b l 8 q i z y oap
界说为一个WeakJ E J u + v v i mMap
,它的key
值是各个目标,在本列中便是两个obj
,targe? 4 wtMap
的value
值是一个Map
,本列中两个obj
别离拥有两个特色a
和b
,Map
的key
便是特色a
和b
,Map
的value
便是特色a
和b
别离对应的Set
回调函数调集
或许咱们会有疑问为什么targetMap
用WeakMap
而各个目标的特色存储用的Map
,这是由于Weax * y t ^ Q X m %kMap
只能以目标作为key
,Map
是目标或字符串都能够,像上面的列子特色a
和b
只能用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 @如图所示
- watch
watch(() => test.a, val => { console.log(val) })
这儿咱们需求将val => { console.log(val) }
回调g r ] 4 K Q k 0 k函数放在test.a
的调集中,如图所示
- 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 ?如图所示
上面咱们现已知道了存储回调函数的办法,现在咱们来考虑如何将回调函数放到咱们界说好的存储结构中
还是拿上面的列子举列
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.a
的Set
调集中,所以咱们需求拿到目标test
和其时目标的特色a
,假如仅经过() => test.a
,咱们x 2 o I 5 3 ~只能拿C x o Y l H Y & b到test.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其时读取的目标,第二个参数便是其时读取的特色
所以回调函数的搜集是在proxy
的get
阻拦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 * ( % N补effect
中的内容是让咱们能够清晰的看见搜集和触发的方位
现在咱们来弥补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
结构
targetMap
中存在key
值{ a: 1,b: 2 }
,它的value
值也是一个Map
,这个Map
中存在key
值b
,这个Map
的value
便是回调函数的调集Set
,现在就只要一个咱们写死的() => console.log('我是一个回调函数')
用图形结构便是这] @ + 9 F样
咱们或许觉得要搜集回调函数要读取一次test.b
是反人类的操作,这是由于咱们还没有讲到对应的api
,平常读取的操作不需求这么手动式的调用,api
会自己处理
watch
上面的列子存在一个很大的问题,便是咱们没有自界说回调函数= % h R Z I I Z,回调函数在代码中直接被写死了
现在咱们将经过watch
去完成自界说的回调函数
watch
在vue-next
中k u 0的api
还蛮多的,咱们将完成其中一部分类型,这足以让咱们了解呼应式原理
咱们将完成的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.a
的Set
调集中
所以在() => 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 !
办法和track
、trigger
办法不在同一个文件,所以咱们用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
中存在key
值a
,这个Map的value
便是回调函数(val) => { console.log(val); }
targetMap
的图形y k J / {结构如下
computed
watch3 3 . Z 6的其他api
弥补咱们将放到后边,在感受到呼应式原理的思想后,咱们抓住时机再来完成computed
的功用
相同的computed
这个api
在vue-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 X为true
相同的,咱们再每次读取值之前,将回调函数() => { dirty = true }
赋值给中心变量activeEffectY L e ] [
,然后再履行fn
读取,此时回调被搜集,当对应的特色更改的时分,dirty
也就更改了
咱们再运转上面的列子,程序仍然正常运转了
咱们来i / ^ | A @ w . s看看tarD L egetMap
的结构,targetMap
存在一个key
值{a:1}
,它的value
值也是一个Map
,这个Map
中存在key
值a
,这个Map的value
便P o S A j R u是回调函数function changeDirty() { dirty = true; }
targetMap
的图形r ; ) = k结p f H I构如下
提取effect
在watch
和comp: ) & +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
如图所示
图形结构如图所示
相同咱们更改computed
和watch
中的写法,用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
中寄存了两个回调函数
targetMap
图形结构如图所示
弥补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 }
上增加了
targetMap
图形结构如图所示
能够看到加入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有
所以在数组的状况下,咱们也属于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面的列子中增加deep
为true
能够看见回调触发了
targetMap
如图所示
第一项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
的并没有搜集
咱们应该如何处理这种临界的状况呢?
咱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();
}
});
}
}
}
然后咱们处理type
为add
的状/ K ( * o ?况,当type
是add
且目标为数组的时分,咱们便去读取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-next
中b @ 7 / 1 R & }对应的源码