1.本文将从零开始手写一份vue-next
中的响应式原理,出于篇幅和理解的难易程度,咱们将只实现核心的api
并忽略一些边界的功能点javascript
本文将实现的api
包括前端
- track
- trigger
- effect
- reactive
- watch
- computed
2.最近不少人私信我问前端问题,博客登录的少没及时回复,为此我建了个前端扣扣裙 519293536 你们之后有问题直接群里找我。都会尽力帮你们,博客私信我不多看
项目搭建
咱们采用最近较火的vite
建立项目vue
本文演示的版本java
- node
v12.16.1
- npm
v6.14.5
- yarn
v1.22.4
咱们首先下载模板node
yarn create vite-app vue-next-reactivity
复制代码
模板下载好后进入目录react
cd vue-next-reactivity 复制代码
而后安装依赖git
yarn install
复制代码
而后咱们仅保留src
目录下的main.js
文件,清空其他文件并建立咱们要用到的reactivity
文件夹es6

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

手写代码
响应式原理的本质
在开始手写前,咱们思考一下什么是响应式原理呢?web
咱们从vue-next
的使用中来解释一下
vue-next
中用到的响应式大概分为三个
- template或render
在页面中使用到的变量改变后,页面自动
进行了刷新
- computed
当计算属性函数中用到的变量发生改变后,计算属性自动
进行了改变
- watch
当监听的值发生改变后,自动
触发了对应的回调函数
以上三点咱们就能够总结出响应式原理的本质
当一个值改变后会自动
触发对应的回调函数
这里的回调函数就是template
中的页面刷新函数,computed
中的从新计算属性值的函数以及原本就是一个回调函数的watch
回调
因此咱们要去实现响应式原理如今就拆分为了两个问题
- 监听值的改变
- 触发对应的回调函数
咱们解决了这两个问题,便写出了响应式原理
监听值的改变
javascript
中提供了两个api
能够作到监听值的改变
一个是vue2.x
中用到的Object.defineProperety
const obj = {}; let aValue = 1; Object.defineProperty(obj, 'a', { enumerable: true, configurable: true, get() { console.log('我被读取了'); return aValue; }, set(value) { console.log('我被设置了'); aValue = value; }, }); obj.a; // 我被读取了 obj.a = 2; // 我被设置了 复制代码
还有一个方法就是vue-next
中用到的proxy
,这也是本次手写中会用到的方法
这个方法解决了Object.defineProperety
的四个痛点
- 没法拦截在对象上属性的新增和删除
- 没法拦截在数组上调用
push
pop
shift
unshift
等对当前数组会产生影响的方法 - 拦截数组索引过大的性能开销
- 没法拦截
Set
Map
等集合类型
固然主要仍是前两个
关于第三点,vue2.x
中数组索引的改变也得经过this.$set
去设置,致使不少同窗误认为Object.defineProperety
也无法拦截数组索引,其实它是能够的,vue2.x
没作的缘由估计就是由于性价比不高
以上4点proxy
就能够完美解决,如今让咱们动手开始写一个proxy
拦截吧!
proxy拦截
咱们在以前建立好的reactivity
目录建立两个文件
utils.js
存放一些公用的方法
reactive.js
存放proxy
拦截的方法
咱们先在utils.js
中先添加将要用到的判断是否为原生对象的方法
reactivity/utils.js
// 获取原始类型 export function toPlain(value) { return Object.prototype.toString.call(value).slice(8, -1); } // 是不是原生对象 export function isPlainObject(value) { return toPlain(value) === 'Object'; } 复制代码
reactivity/reactive.js
import { isPlainObject } from './utils'; // 本列只有数组和对象才能被观测 function canObserve(value) { return Array.isArray(value) || isPlainObject(value); } // 拦截数据 export function reactive(value) { // 不能监听的数值直接返回 if (!canObserve(value)) { return; } const observe = new Proxy(value, { // 拦截读取 get(target, key, receiver) { console.log(`${key}被读取了`); return Reflect.get(target, key, receiver); }, // 拦截设置 set(target, key, newValue, receiver) { const res = Reflect.set(target, key, newValue, receiver); console.log(`${key}被设置了`); return res; }, }); // 返回被观察的proxy实例 return observe; } 复制代码
reactivity/index.js
导出方法
export * from './reactive'; 复制代码
main.js
import { reactive } from './reactivity'; const test = reactive({ a: 1, }); const testArr = reactive([1, 2, 3]); // 1 test.a; // a被读取了 test.a = 2; // a被设置了 // 2 test.b; // b被读取了 // 3 testArr[0]; // 0被读取了 // 4 testArr.pop(); // pop被读取了 length被读取了 2被读取了 length被设置了 复制代码
能够看到咱们添加了一个reactive
方法用于将对象和数组进行proxy
拦截,并返回了对应的proxy
实例
列子中的1
2
3
都很好理解,咱们来解释下第4个
咱们调用pop
方法首先会触发get拦截,打印pop被读取了
而后调用pop
方法后会读取数组的长度触发get拦截,打印length被读取了
pop
方法的返回值是当前删除的值,会读取数组索引为2的值触发get拦截,打印2被读取了
pop
后数组长度会被改变,会触发set
拦截,打印length被设置了
你们也能够试试其余改变数组的方法
能够概括为一句话
对数组的自己有长度影响的时候length
会被读取和从新设置,对应改变的值的索引也会被读取或从新设置(push
unshift
)
添加回调函数
咱们经过了proxy
实现了对值的拦截解决了咱们提出的第一个问题
但咱们并无在值的改变后触发回调函数,如今让咱们来补充回调函数
reactivity/reactive.js
import { isPlainObject } from './utils';
// 本列只有数组和对象才能被观测
function canObserve(value) {
return Array.isArray(value) || isPlainObject(value);
}
+ // 假设的回调函数 + function notice(key) { + console.log(`${key}被改变了并触发了回调函数`); + } // 拦截数据 export function reactive(value) { // 不能监听的数值直接返回 if (!canObserve(value)) { return; } const observe = new Proxy(value, { // 拦截读取 get(target, key, receiver) { - console.log(`${key}被读取了`); return Reflect.get(target, key, receiver); }, // 拦截设置 set(target, key, newValue, receiver) { const res = Reflect.set(target, key, newValue, receiver); - console.log(`${key}被设置了`); + // 触发假设的回调函数 + notice(key); return res; }, }); // 返回被观察的proxy实例 return observe; } 复制代码
我么以最直观的方法在值被改变的set
拦截中触发了咱们假设的回调
main.js
import { reactive } from './reactivity'; const test = reactive({ a: 1, b: 2, }); test.a = 2; // a被改变了并触发了回调函数 test.b = 3; // b被改变了并触发了回调函数 复制代码
能够看到当值改变的时候,输出了对应的日志
但这个列子确定是有问题的,问题还不止一处,让咱们一步一步来升级它
回调函数的收集
上面的列子中a
和b
都对应了一个回调函数notice
,可实际的场景中,a
和b
可能对应分别不一样的回调函数,若是咱们单单用一个简单的全局变量存储回调函数,很明显这是不合适的,若是有后者则会覆盖前者,那么怎么才能让回调函数和各个值之间对应呢?
很容易想到的就是js
中的key-value
的对象,属性a
和b
分别做为对象的key
值则能够区分各自的value
值

但用对象收集回调函数是有问题的
上列中咱们有一个test
对象,它的属性有a
和b
,当咱们存在另一个对象test1
它要是也有a
和b
属性,那不是重复了吗,这又会触发咱们以前说到的重复的问题

有同窗可能会说,那再包一层用test
和test1
做为属性名不就行了,这种方法也是不可行的,在同一个执行上下文中不会出现两个相同的变量名,但不一样执行上下文能够,这又致使了上面说到的重复的问题
处理这个问题要用到js
对象按引用传递的特色
// 1.js const obj = { a: 1, }; // 2.js const obj = { a: 1, }; 复制代码
咱们在两个文件夹定义了名字属性数据结构彻底同样的对象obj
,但咱们知道这两个obj
并非相等的,由于它们的内存指向不一样地址
因此若是咱们能直接把对象做为key
值,那么是否是就能够区分看似"相同"的对象了呢?
答案确定是能够的,不过咱们得换种数据结构,由于js
中对象的key
值是不能为一个对象的
这里咱们就要用到es6
新增的一种数据结构Map
和WeakMap
咱们经过举例来讲明这种数据结构的存储模式
假设如今咱们有两个数据结构“相同”的对象obj
,它们都有各自的属性a
和b
,各个属性的改变会触发不一样的回调函数
// 1.js const obj = { a: 1, b: 2 }; // 2.js const obj = { a: 1, b: 2 }; 复制代码
用Map
和WeakMap
来存储就以下图所示
咱们将存储回调函数的全局变量targetMap
定义为一个WeakMap
,它的key
值是各个对象,在本列中就是两个obj
,targetMap
的value
值是一个Map
,本列中两个obj
分别拥有两个属性a
和b
,Map
的key
就是属性a
和b
,Map
的value
就是属性a
和b
分别对应的Set
回调函数集合

可能你们会有疑问为何targetMap
用WeakMap
而各个对象的属性存储用的Map
,这是由于WeakMap
只能以对象做为key
,Map
是对象或字符串均可以,像上面的列子属性a
和b
只能用Map
存
咱们再以实际api
来加深对这种存储结构的理解
- computed
const c = computed(() => test.a) 复制代码
这里咱们须要将() => test.a
回调函数放在test.a
的集合中,如图所示

- watch
watch(() => test.a, val => { console.log(val) }) 复制代码
这里咱们须要将val => { console.log(val) }
回调函数放在test.a
的集合中,如图所示

- template
createApp({
setup() {
return () => h('div', test.a); }, }); 复制代码
这里咱们须要将dom
刷新的函数放在test.a
中,如图所示

上面咱们已经知道了存储回调函数的方式,如今咱们来思考如何将回调函数放到咱们定义好的存储结构中
仍是拿上面的列子举列
watch(() => test.a, val => { console.log(val) }) 复制代码
这个列子中,咱们须要将回调函数val => { console.log(val) })
放到test.a
的Set
集合中,因此咱们须要拿到对象test
和当前对象的属性a
,若是仅经过() => test.a
,咱们只能拿到test.a
的值,没法得知具体的对象和属性
但其实这里读取了test.a
的值,就变相的拿到了具体的对象和属性
你们还记得咱们在前面用proxy
拦截了test.a
的读取吗,get
拦截的第一个参数就是当前读取的对象,第二个参数就是当前读取的属性
因此回调函数的收集是在proxy
的get
拦截中处理
如今让咱们用代码实现刚刚理好的思路
首先咱们建立effect.js
文件,这个文件用于存放回调函数的收集方法和回调函数的触发方法
reactivity/effect.js
// 回调函数集合 const targetMap = new WeakMap(); // 收集回调函数 export function track(target, key) { } // 触发回调函数 export function trigger(target, key) { } 复制代码
而后改写proxy
中的拦截内容
reactivity/reactive.js
import { isPlainObject } from './utils';
+ import { track, trigger } from './effect'; // 本列只有数组和对象才能被观测 function canObserve(value) { return Array.isArray(value) || isPlainObject(value); } - // 假设的回调函数 - function notice(key) { - console.log(`${key}被改变了并触发了回调函数`); - } // 拦截数据 export function reactive(value) { // 不能监听的数值直接返回 if (!canObserve(value)) { return; } const observe = new Proxy(value, { // 拦截读取 get(target, key, receiver) { + // 收集回调函数 + track(target, key); return Reflect.get(target, key, receiver); }, // 拦截设置 set(target, key, newValue, receiver) { const res = Reflect.set(target, key, newValue, receiver); + // 触发回调函数 + trigger(target, key); - // 触发假设的回调函数 - notice(key); return res; }, }); // 返回被观察的proxy实例 return observe; } 复制代码
这里还没补充effect
中的内容是让你们能够清晰的看见收集和触发的位置
如今咱们来补充track
收集回调函数和trigger
触发回调函数
reactivity/effect.js
// 回调函数集合 const targetMap = new WeakMap(); // 收集回调函数 export function track(target, key) { // 经过对象获取每一个对象的map let depsMap = targetMap.get(target); if (!depsMap) { // 当对象被第一次收集时 咱们须要添加一个map集合 targetMap.set(target, (depsMap = new Map())); } // 获取对象下各个属性的回调函数集合 let dep = depsMap.get(key); if (!dep) { // 当对象属性第一次收集时 咱们须要添加一个set集合 depsMap.set(key, (dep = new Set())); } // 这里添加回调函数 dep.add(() => console.log('我是一个回调函数')); } // 触发回调函数 export function trigger(target, key) { // 获取对象的map const depsMap = targetMap.get(target); if (depsMap) { // 获取对应各个属性的回调函数集合 const deps = depsMap.get(key); if (deps) { // 触发回调函数 deps.forEach((v) => v()); } } } 复制代码
而后运行咱们的demo
main.js
import { reactive } from './reactivity'; const test = reactive({ a: 1, b: 2, }); test.b; // 读取收集回调函数 setTimeout(() => { test.a = 2; // 没有任何触发 由于没收集回调函数 test.b = 3; // 我是一个回调函数 }, 1000); 复制代码
咱们来看看此时的targetMap
结构

targetMap
中存在key
值{ a: 1,b: 2 }
,它的value
值也是一个Map
,这个Map
中存在key
值b
,这个Map
的value
即是回调函数的集合Set
,如今就只有一个咱们写死的() => console.log('我是一个回调函数')
用图形结构就是这样

你们可能以为要收集回调函数要读取一次test.b
是反人类的操做,这是由于咱们尚未讲到对应的api
,日常读取的操做不须要这么手动式的调用,api
会本身处理
watch
上面的列子存在一个很大的问题,就是咱们没有自定义回调函数,回调函数在代码中直接被写死了
如今咱们将经过watch
去实现自定义的回调函数
watch
在vue-next
中的api
还蛮多的,咱们将实现其中一部分类型,这足以让咱们理解响应式原理
咱们将实现的demo
以下
export function watch(fn, cb, options) {} const test = reactive({ a: 1, }); watch( () => test.a, (val) => { console.log(val); } ); 复制代码
watch
接受三个参数
第一个参数是一个函数,表达被监听的值
第二个参数是一个函数,表达监听值改变后要触发的回调,第一个参数是改变后的值,第二个参数是改变前的值
第三个参数是一个对象,只有一个deep
属性,deep
表深度观察
如今咱们须要作的就是把回调函数(val) => { console.log(val); }
放到test.a
的Set
集合中
因此在() => test.a
执行读取test.a
前,咱们须要将回调函数用一个变量存储
当读取test.a
触发track
函数的时候,能够在track
函数中获取到这个变量,并将它存储到对应属性的集合Set
中
reactivity/effect.js
// 回调函数集合
const targetMap = new WeakMap();
+ // 当前激活的回调函数 + export let activeEffect; + // 设置当前回调函数 + export function setActiveEffect(effect) { + activeEffect = effect; + } // 收集回调函数 export function track(target, key) { // 没有激活的回调函数 直接退出不收集 if (!activeEffect) { return; } // 经过对象获取每一个对象的map let depsMap = targetMap.get(target); if (!depsMap) { // 当对象被第一次收集时 咱们须要添加一个map集合 targetMap.set(target, (depsMap = new Map())); } // 获取对象下各个属性的回调函数集合 let dep = depsMap.get(key); if (!dep) { // 当对象属性第一次收集时 咱们须要添加一个set集合 depsMap.set(key, (dep = new Set())); } // 这里添加回调函数 - dep.add(() => console.log('我是一个回调函数')); + dep.add(activeEffect); } // 触发回调函数 export function trigger(target, key) { // 省略 } 复制代码
由于watch
方法和track
、trigger
方法不在同一个文件,因此咱们用export
导出变量activeEffect
,并提供了一个方法setActiveEffect
修改它
这也是一个不一样模块下使用公共变量的方法
如今让咱们建立watch.js
,并添加watch
方法
reactivity/watch.js
import { setActiveEffect } from './effect'; export function watch(fn, cb, options = {}) { let oldValue; // 在执行fn获取oldValue前先存储回调函数 setActiveEffect(() => { // 确保回调函数触发 获取到的是新值 let newValue = fn(); // 触发回调函数 cb(newValue, oldValue); // 新值赋值给旧值 oldValue = newValue; }); // 读取值并收集回调函数 oldValue = fn(); // 置空回调函数 setActiveEffect(''); } 复制代码
很简单的几行代码,在执行fn
读取值前把回调函数经过setActiveEffect
设置以便在读取的时候track
函数中能够拿到当前的回调函数activeEffect
,读取完后再制空回调函数,就完成了
一样咱们须要导出watch
方法
reactivity/index.js
export * from './reactive';
+ export * from './watch'; 复制代码
main.js
import { reactive, watch } from './reactivity'; const test1 = reactive({ a: 1, }); watch( () => test1.a, (val) => { console.log(val) // 2; } ); test1.a = 2; 复制代码
能够看到列子正常执行打印出了2
,咱们来看看targetMap
的结构
targetMap
存在一个key
值{a:1}
,它的value
值也是一个Map
,这个Map
中存在key
值a
,这个Map的value
即是回调函数(val) => { console.log(val); }

targetMap
的图形结构以下

computed
watch的其余api
补充咱们将放到后面,在感觉到响应式原理的思惟后,咱们趁热打铁再来实现computed
的功能
一样的computed
这个api
在vue-next
中也有多种写法,咱们将只实现函数返回值的写法
export function computed(fn) {} const test = reactive({ a: 1, }); const w = computed(() => test.a + 1); 复制代码
但若是咱们仅实现computed
传入函数的写法,其实在vue-next
中和响应式原理没多大关系
由于vue-next
中提供的api
读取值不是直接读取的w
而是w.value
咱们建立computed.js
,补充computed
函数
reactivity/computed.js
export function computed(fn) { return { get value() { return fn(); }, }; } 复制代码
能够看到就几行代码,每次读取value
从新运行一次fn
求值就好了
reactivity/index.js
咱们再导出它
export * from './reactive';
export * from './watch';
+ export * from './computed'; 复制代码
main.js
import { reactive, computed } from './reactivity'; const test = reactive({ a: 1, }); const w = computed(() => test.a + 1); console.log(w.value); // 2 test.a = 2; console.log(w.value); // 3 复制代码
能够看到列子完美运行
这里带来了两个问题
- 为何
api
的写法不是直接读取w
而是w.value
的形式
这个和为啥有ref
是一个道理,proxy
没法拦截基础类型,因此要加一层value
包装成对象
vue-next
中的computed
真的和响应式原理不要紧了吗
其实有关系,在仅实现computed
传入函数的写法中,响应式原理启优化做用
能够看到若是按咱们以前的写法,即使w.value
的值没有变化,咱们读取的时候也会去执行一次fn
,当数据量多起来的时候,对性能的影响就大了
那咱们怎么优化呢?
容易想到的就是执行一次fn
对比新老值,但这和以前其实就同样了,由于咱们仍然执行了一次fn
这里咱们就能够运用响应式原理,只要内部的影响值test.a
被修改了,咱们就从新执行fn
获取一次值,否则就读取以前的存储的值
reactivity/computed.js
import { setActiveEffect } from './effect'; export function computed(fn) { // 变量被改变后此值才会为true 第一次进来时候为true let dirty = true; // 返回值 let value; // 设置为true表达下次读取须要从新获取 function changeDirty() { dirty = true; } return { get value() { // 当标志为true表明变量须要更改 if (dirty) { dirty = false; // 将变量控制设置为 setActiveEffect(changeDirty); // 获取值 value = fn(); // 制空依赖 setActiveEffect(''); } return value; }, }; } 复制代码
咱们定义了一个变量dirty
用于表达这个值是否被修改过,修改过就为true
一样的,咱们再每次读取值以前,将回调函数() => { dirty = true }
赋值给中间变量activeEffect
,而后再执行fn
读取,此时回调被收集,当对应的属性更改的时候,dirty
也就更改了
咱们再运行上面的列子,程序仍然正常运行了
咱们来看看targetMap
的结构,targetMap
存在一个key
值{a:1}
,它的value
值也是一个Map
,这个Map
中存在key
值a
,这个Map的value
即是回调函数function changeDirty() { dirty = true; }

targetMap
的图形结构以下

提取effect
在watch
和computed
中咱们都经历过 设置回调函数=>读取值(存储回调函数)=>清空回调函数 这三个步骤
在vue-next
的源码中这个步骤被提取为了一个公用函数,为了符合vue-next
的设计咱们将这个步骤提取出来,取名effect
函数的第一个参数是一个函数,函数执行后,会触发函数中各个变量的读取,并收集对应的回调函数
函数的第二个参数是一个对象
有一个schedular
属性,表达特殊指定的回调函数,若是没有这个属性,回调函数就是第一个参数
有一个lazy
属性,为true
时表明第一个参数传入的函数不用当即执行,默认为false
,即当即指定第一个参数传入的函数
reactivity/effect.js
// 回调函数集合
const targetMap = new WeakMap();
// 当前激活的回调函数
export let activeEffect;
- // 设置当前回调函数 - export function setActiveEffect(effect) { - activeEffect = effect; - } + // 设置当前回调函数 + export function effect(fn, options = {}) { + const effectFn = () => { + // 设置当前激活的回调函数 + activeEffect = effectFn; + // 执行fn收集回调函数 + let val = fn(); + // 制空回调函数 + activeEffect = ''; + return val; + }; + // options配置 + effectFn.options = options; + // 默认第一次执行函数 + if (!options.lazy) { + effectFn(); + } + return effectFn; + } // 收集回调函数 export function track(target, key) { // 省略 } // 触发回调函数 export function trigger(target, key) { // 获取对象的map const depsMap = targetMap.get(target); if (depsMap) { // 获取对应各个属性的回调函数集合 const deps = depsMap.get(key); if (deps) { // 触发回调函数 - deps.forEach((v) => v()); + deps.forEach((v) => { + // 特殊指定回调函数存放在了schedular中 + if (v.options.schedular) { + v.options.schedular(); + } + // 当没有特地指定回调函数则直接触发 + else if (v) { + v(); + } + }); } } } 复制代码
reactivity/index.js
导出effect
export * from './reactive';
export * from './watch';
export * from './computed';
+ export * from './effect'; 复制代码
main.js
import { reactive, effect } from './reactivity'; const test = reactive({ a: 1, }); effect(() => { document.title = test.a; }); setTimeout(() => { test.a = 2; }, 1000); 复制代码
effect
第一次自执行,将() => { document.title = test.a; }
这个回调函数放入了test.a
中,当test.a
改变,触发对应回调函数
targetMap
如图所示

图形结构如图所示

一样咱们更改computed
和watch
中的写法,用effect
替代
reactivity/computed.js
import { effect } from './effect'; export function computed(fn) { // 变量被改变后此值才会为true 第一次进来时候为true let dirty = true; let value; const runner = effect(fn, { schedular: () => { dirty = true; }, // 第一次不用执行 lazy: true, }); // 返回值 return { get value() { // 当标志为true表明变量须要更改 if (dirty) { value = runner(); // 制空依赖 dirty = false; } return value; }, }; } 复制代码
reactivity/watch.js
import { effect } from './effect'; export function watch(fn, cb, options = {}) { let oldValue; const runner = effect(fn, { schedular: () => { // 当这个依赖执行的时候 获取到的是新值 let newValue = fn(); // 触发回调函数 cb(newValue, oldValue); // 新值赋值给旧值 oldValue = newValue; }, // 第一次不用执行 lazy: true, }); // 读取值并收集依赖 oldValue = runner(); } 复制代码
main.js
import { reactive, watch, computed } from './reactivity'; const test = reactive({ a: 1, }); const w = computed(() => test.a + 1); watch( () => test.a, (val) => { console.log(val); // 2 } ); console.log(w.value); // 2 test.a = 2; console.log(w.value); // 3 复制代码
能够看到代码正常执行,targetMap
如图所示,属性a
中存放了两个回调函数

targetMap
图形结构如图所示

补充watch的options
咱们来看看这个列子
import { watch, reactive } from './reactivity'; const test = reactive({ a: { b: 1, }, }); watch( () => test.a, (val) => { console.log(val); // 没有触发 } ); test.a.b = 2; 复制代码
咱们用watch
观察了test.a
,当咱们去改变test.a.b
的时候,观察的回调并无触发,用过vue
的同窗都会知道,这种状况应该用deep
属性就能够解决
那么deep
是如何实现的呢
咱们再来回忆一下回调函数收集的过程
test.a
被读取时,回调函数被收集进了test.a
中,但这里test.a.b
并无被读取,因此回调函数天然就没有被收集进test.a.b
因此咱们只用在回调函数收集的时候,深度遍历一下test
,去读取一下各个属性便可
这里还须要注意一点,咱们用reactive
拦截对象的时候,是不会拦截对象的第二层的
const test = { a: { b: 1, }, }; const observe = new Proxy(test, { get(target, key, receiver) { return Reflect.set(target, key, receiver); }, }); test.a // 触发拦截 test.a.b // 不会触发拦截 复制代码
因此咱们须要递归的将拦截值用proxy
代理
reactivity/reactive.js
const observe = new Proxy(value, {
// 拦截读取
get(target, key, receiver) {
// 收集回调函数
track(target, key);
+ const res = Reflect.get(target, key, receiver); + return canObserve(res) ? 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/watch.js
import { effect } from './effect';
+ import { isPlainObject } from './utils'; + // 深度遍历值 + function traverse(value) { + if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key]); + } + } + return value + } export function watch(fn, cb, options = {}) { + let oldValue; + let getters = fn; + // 当存在deep属性的时候 深度遍历值 + if (options.deep) { + getters = () => traverse(fn()); + } + const runner = effect(getters, { - const runner = effect(fn, { schedular: () => { // 当这个依赖执行的时候 获取到的是新值 let newValue = runner(); // 触发回调函数 cb(newValue, oldValue); // 新值赋值给旧值 oldValue = newValue; }, // 第一次不用执行 lazy: true, }); // 读取值并收集回调函数 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: { b: 1 } }
上添加了回到函数,也在{ b: 1 }
上添加了

targetMap
图形结构如图所示

能够看到加入deep
属性后即可深度观察数据了,上面的列子中咱们都是用的对象,其实深度观察对数组也是须要的,不过数组的处理有一点不一样咱们来看看不一样点
数组的处理
import { watch, reactive } from './reactivity'; const test = reactive([1, 2, 3]); watch( () => test, (val) => { console.log(val); // 没有触发 } ); test[0] = 2; 复制代码
上面的列子是不会触发的,由于咱们只读取了test
,targetMap
里面啥也没有

因此在数组的状况下,咱们也属于deep
深度观察范畴,深度遍历的时候,须要读取数组的每一项
reactivity/watch.js
// 深度遍历值
function traverse(value) {
// 处理对象
if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key]);
}
}
+ // 处理数组 + else if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i]); + } + } return value; } 复制代码
main.js
import { watch, reactive } from './reactivity'; const test = reactive([1, 2, 3]); watch( () => test, (val) => { console.log(val); // [2, 2, 3] }, { deep: true } ); test[0] = 2; 复制代码
在上面的列子中添加deep
为true
能够看见回调触发了
targetMap
如图所示

第一项Set
是一个Symbol(Symbol.toStringTag)
,咱们不用管它
咱们将数组的每一项都进行了回调函数的储存,且也在数组的length
属性上也进行了存储
咱们再来看一个列子
import { watch, reactive } from './reactivity'; const test = reactive([1, 2, 3]); watch( () => test, (val) => { console.log(val); // 没有触发 }, { deep: true, } ); test[3] = 4; 复制代码
上面的列子不会触发,细心的同窗可能记得,咱们targetMap
里面只收集了索引为0
1
2
的三个位置,新增的索引为3
的并无收集

咱们应该如何处理这种临界的状况呢?
你们还记得咱们最初讲到的在proxy
下数组pop
方法的解析吗,当时咱们概括为了一句话
对数组的自己有长度影响的时候length
会被读取和从新设置
如今咱们经过索引新增值其实也是改变了数组自己的长度,因此length
会被从新设置,如今就有方法了,咱们在新增索引上找不到回调函数的时候,咱们能够去读取数组length
上存储的回调函数
reactivity/reactive.js
const observe = new Proxy(value, {
// 拦截读取
get(target, key, receiver) {
// 收集回调函数
track(target, key);
const res = Reflect.get(target, key, receiver);
return canObserve(res) ? reactive(res) : res;
},
// 拦截设置
set(target, key, newValue, receiver) {
+ const hasOwn = target.hasOwnProperty(key); + const oldValue = Reflect.get(target, key, receiver); const res = Reflect.set(target, key, newValue, receiver); + if (hasOwn) { + // 设置以前的属性 + trigger(target, key, 'set'); + } else if (oldValue !== newValue) { + // 添加新的属性 + trigger(target, key, 'add'); + } - // 触发回调函数 - trigger(target, key); return res; }, }); 复制代码
咱们用hasOwnProperty
判断当前属性是否在对象上,对于数组的新增索引很明显是不在的,此时会走到trigger(target, key, 'add');
这个函数
reactivity/effect.js
// 触发回调函数
export function trigger(target, key, type) {
// 获取对象的map
const depsMap = targetMap.get(target);
if (depsMap) {
// 获取对应各个属性的回调函数集合
- const deps = depsMap.get(key); + let deps = depsMap.get(key); + // 当数组新增属性的时候 直接获取length上存储的回调函数 + if (type === 'add' && Array.isArray(target)) { + deps = depsMap.get('length'); + } if (deps) { // 触发回调函数 deps.forEach((v) => { // 特殊指定回调函数存放在了schedular中 if (v.options.schedular) { v.options.schedular(); } // 当没有特地指定回调函数则直接触发 else if (v) { v(); } }); } } } 复制代码
而后咱们处理type
为add
的状况,当type
是add
且对象为数组的时候,咱们便去读取length
上存储的回调函数
能够看到这么一改写,列子就能够正常运行了
总结
1.其实读完本文后,你会发现本文不是一篇vue
源码解剖,咱们全程没有贴出vue-next
中对应的源码,由于我以为从零开始的思路去思考如何实现会比从源码解读去思考为何这么实现会好点
2.固然本文也只实现了简易的响应式原理,若是你想查看完整的代码能够点击这里,虽然不少功能点也没实现,但大致思路都是一致的,若是你能读懂本问讲解的思路,你确定能看懂vue-next
中对应的源码
3.最近不少人私信我问前端问题,博客登录的少没及时回复,为此我建了个前端扣扣裙 519293536 你们之后有问题直接群里找我。都会尽力帮你们,博客私信我不多看
本文的文字及图片来源于网络加上本身的想法,仅供学习、交流使用,不具备任何商业用途,版权归原做者全部,若有问题请及时联系咱们以做处理