1、首先我们要创建 effect.js 文件,在里面写上一个全局变量 effectSave 用来存储传入的函数,然后再写一个 effect 函数,里面用来接收传入的 fn 形参,再写一个闭包函数 _effect,在闭包函数内部自动执行传入的 fn,并且把闭包赋值给 effectSave 全局变量。
实现:effect.js。注:方法里有些没提到的是为computed准备的
// 生成全局变量 let effectSave; // 创建 effect 函数,接收 fn 函数。 const effect = (fn, options) => { const _effect = function () { effectSave = _effect; // 是为了 computed 的回调, 在 trigger 函数里,把 computed 里传入来的 options 里的方法调用更改脏值,已用来不用每次获取 computed 的参数都会去调用计算。 effectSave.options = options let res = fn(); return res; } _effect(); // 要自走一下,因为要把传入的 fn 函数保存在全局变量 effectSave 里; return _effect; }
2、在写一个 tarck 函数,用来精准的把 effectSave 全局变量根据 target 存储到相应的 WeakMap 里。
实现:effect.js
// 全局变量用来根据传入的 target key 存储 effectSave 方法 let weakMap = new WeakMap(); const tarck = (target, key) = { // 首先获取 let depsMap = weakMap.get(target); // 如果没获取到,就是说没存储过这个 target 下面就来存储一下新值 if(!depsMap) { depsMap = new Map(); weakMap.set(target, depsMap); } // 获取 key 对应的响应函数集, 没存过下面就存储一下新值 let deps = depsMap.get(key); if(!deps) { // 建立当前key 和依赖的关系,因为一个key 会有多个依赖 // 为了防止重复依赖,使用set deps = new Set() depsMap.set(key, deps) } // 把 effectSave 全局变量添加到 Set 里。 deps.add(effectSave) }
3、再写一个 trigger 函数,用来调用 前面储存的 effectSave 函数。
实现:effect.js
// 同样接收一个 target 和 key 用来去 weakMap 里寻找 effectSave 函数。 const trigger = (target, key) => { let depMap = weakMap.get(target); let deps = depMap.get(key) deps.forEach(effect = > { // computed 用 if(effect?.options?.scheduler) { effect?.options?.scheduler() } else { effect(); } }) }
4、现在 effect 文件已经创建好了,就开始写 reactive 文件,要使用 proxy 来实现数据劫持来完成某些操作。
实现:reactive.js
const reactive = (target) => { // 先判断一下类型 if(typeOf target != 'object') { console.error('reactive 只能接收 object 类型的数据') } return new Proxt({ // 每次视图渲染的时候都会触发这个 get get(target, key, r) { let res = Reflect.get(target, key, r) // 每次视图渲染的时候都会获取这个值,所以在这个时候把 target 存入对应的 target 的 weakMap 里; 需要调用 tarck 方法 tarck(target, key); // 用于多层的 object if(target != null && typeOf target == 'object') { reactive(target) } // 把值返回出去 return res }, // 每次修改视图数据的时候,都会触发这个 set set(target, key, value, r) { let res = Reflect.set(target, key, value, r); // set 需要返回布尔值这个 Reflect.set 的返回值就是布尔值 // 修改就触发, 那么视图也得更新, 就得调用之前存储的 effectSave 方法, 调用 trigger 方法去找到并调用 trigger(target, key); // 为什么不用传 value 是因为 Reflect.set 的时候已经把 target 的数据变为最新的数据了。 return res } }) }
5、reactive 文件已创建好,通过 html 文件验证
实现:index.html
<body> <div id="app"></div> </body> let user = reactive({ name: '王搏', age: 22 }) // effect 来挂载方法,传入一个函数,effect 方法那边形参接收到自走一下,把传入的函数赋值给 effectSave effect(() => { document.querySelect("#app").innerHtml = `${user.name}-${user.age}`; }) setTimeout(_ => { user.name = '王一搏' }, 1000)
实现逻辑:
①首先是调用 reactive 传入一个对象, 在函数内部创建一个 Proxy 把传入的对象实行 Proxy 数据劫持, 当使用 user 的时候会触发 Proxy 的 get 属性, 当修改 user 数据的时候会触发 Proxy 的 set 属性。
②调用 effect 传入一个函数,传入的函数内部是挂载 dom 渲染数据到视图, effect 函数内部接收的 fn 形参函数自走(渲染视图)把它写在一个闭包里并且把闭包赋值给 effectSave 存储一下, 以便根据 target 存储到 WeekMap 里。
③effect 传入的函数里使用了 user.name 的数据,所以会触发 Proxy 的 get 属性, get 属性内部调用了 tarck 方法并传入了 target 和 key, target 就是调用 reactive 时传入的一个对象, key 就是当前 user.name 要获取的 key(key=name)。tarck 里做了处理, 就是创建一个全局变量 new WeakMap 通过 target 去保存全局变量 effectSave(调用effect时传入的函数) 以便后面调用更新视图, 具体是先通过 weakMap 的 get 去获取 target 的数据, 获取不到就创建一个 new Map() 调用 WeakMap 的 set 传入 target 和 new Map 值存入 WeakMap, 然后接收刚才 set 的返回, 用 newMap 的 set 去存入 key 和 new Set() , 最后根据 newMap.get 接收到的值去把 effectSave(调用effect时传入的函数) 通过 .add() 添加进去。
④更新:当 user.name = '王一搏' 修改的时候,会触发 Proxy 的 set 属性, set 属性内部修改好最新的 target 数据之后, 会调用 trigger 函数, 传入 target 和 key 去找到对应的 effectSave(调用effect时传入的函数) 调用。就实现了视图的更新。
6、实现 computed, 创建 computed.js 文件
实现: computed.js
const computed = (target) => { let _value = effect(target, { scheduler: () => _dirty = true; }); let catchValue; // 存储值 let _dirty = true; // 为了避免每次获取都会去调用 _value 计算。 class ComputedRefImpl { // 每次获取这个值的时候都会执行 get 方法 get() { // 首次进来 _dirty 是 true, 如果下次数据有修改就需要把 _dirty 改为 true 所以 effect 里,还要接收一个函数用来回调把 _dirty 的值改为 true, 重新渲染视图的时候, 就能重新计算获取了。 if(_dirty) { catchValue = _value(); // 计算过后就把 _dirty 改为 false; _dirty = false; } return catchValue; } } return ComputedRefImpl }
7、computed 文件已创建成功, 通过 html 文件验证
实现:index.html
<body> <div id="app"></div> </body> let user = reactive({ name: '王搏', age: 22 }) let userInfo = computed(() => { return `用户信息:姓名:${user.name}。年龄:${user.age}` }) // effect 来挂载方法,传入一个函数,effect 方法那边形参接收到自走一下,把传入的函数赋值给 effectSave effect(() => { document.querySelect("#app").innerHtml = ` 用户信息:${userInfo} `; })
实现逻辑:
①调用 computed 传入的方法首先会自走一次,然后调用 effect 方法, 把调用 computed 里写的方法和在 computed 函数里定义的一个对象 scheduler 方法(options),传入赋值给 effectSave 里。这个 options 的作用是修改脏值,防止每次获取 computed 值的时候,都会重新计算。
②首次自走的时候会访问 user, 然后 user 的 proxy get 触发去把当前访问的 target 存入 weakMap 里。 存入 weakMap 之后,再把 weakMap 对应的 target 的值里添加一个 new Map 根据当前传入的 key 去创建 new Set 再把这个 key 和 new Set 加入到通过 target 找到的 weakMap 里。再把 computed 函数里 effect 传入的方法赋值的 effectSave 加入到这个 Set 里。
③每当 user 数据发生变化,就会调用 user 的 proxy set 方法,然后去 trigger 里通过当前修改的 target 找 weakMap 值, 找到后,根据当前修改的 key 去获取那个 Set 里存的方法(里面有 dom 挂载传入的方法,还有 computed 传入的方法)因为是 computed 创建在前所以先存的是 computed 里更改脏值 _dirty 的方法 scheduler 所以循环 Set 的时候就把脏值 _dirty 改为了 true。 然后才会调用挂载传入的 dom 函数, 当 dom 函数里的 userInfo 被调用了, 所以又会去调用 computed 方法,然后触发 computed 函数里的 ComputedRefImpl 类 get 属性,因为前面 scheduler 把 _dirty 脏值改为了 true, 所以获取的时候就会重新去调用 computed 传入的方法去更新最新的数据到页面。
8.ref 简述一下
ref 是由类创建的(扩展是 reactive),ref 需要 value 修改值的原因是类里面的 get value 和 set value 做了数据劫持,劫持到就会去更新视图。如果 ref 传的是 object 类的 constructor 会判断如果传的是 object 会去调用 reactive 来做响应式。
const isObject = (val) => val !== null && typeof val === 'object' const toReactive = (value) => return isObject(value) ? reactive(value) : value class RefImpl { // 源码是接受俩参数,第二个主要是判断 shallowRef constructor(value) { this._rawValue = value; this._value = toReactive(value); // 里面判断了是否为对象,如果是对象的话就去调用reactive不是就直接原样返回 } get value() { return this._value } set value(newValue) { if(this.__rawValue != this._value) { console.log('需要更新视图') } } }