1.响应式定义
把JS对象封装成响应式对象。当发生数据获取或修改等变化,触发对应的更新方法实现自动更新。
安装
npm init test
npm i vue
测试
const {effect,reactive} = requrie('@vue/reactivity')
const obj = reactive({val:1})
const showCnt = 0
effect(() => {
showCnt = 100 + obj.val
})
setInterval(() => {
obj.val++
}, 1000);
2. reactive
实现思路
整理思路:
- 通过浏览器Proxy代理对象的get和set方法
- 当调用effect方法时候,遇到对象访问属性,触发get方法,
- 由于在触发get之前,已经把effect临时存储了全局的activeEffect
- 这时会在track方法里面收集到全局的activeEffect 到effectSet
- 当对对象做设置属性时候,触发set方法,出发trigger,调用对应的effectSet里面的activeEffect方法
数据结构
- 使用大Map:targetMap记录所有响应式对象
- targetMap里面所有key对应具体的响应式reactive对象
- targetMap的key对应值是depsMap,记录每个对象属性
- 每个属性对应effectSet 关联多个effectFn方法
测试驱动开发
基于vitest 测试用例
使用 npm create vue@latest搭建vitest,参考Vue测试驱动开发(TDD)的应用
在 src/reactivity/tests/
新增reactive.spec.js
import { describe, it, expect } from 'vitest'
import { effect } from '../effect'
import { reactive } from '../reactive'
describe('响应式测试', () => {
it('reactive测试', () => {
const ret = reactive({ num: 0 })
let val
effect(() => {
val = ret.num
})
expect(val).toBe(0)
ret.num++
expect(val).toBe(1)
ret.num = 12
expect(val).toBe(12)
//嵌套测试
const ret2 = reactive({ obj2: {num2:0} })
let val2
effect(() => {
val2 = ret2.obj2.num2
})
expect(val2).toBe(0)
ret2.obj2.num2++
expect(val2).toBe(1)
})
})
/src/components/reactive.js
import { track,trigger } from './effect'
export function reactive(obj) {
if (typeof obj!=='object') {
return obj
}
return new Proxy(obj,handles)
}
const createGetFn = () => {
return function get(target,key,receiver){
const res = Reflect.get(target,key,receiver)
track(target,key)
console.log("get",target[key])
if (typeof res ==='object') {
return reactive(res)
}
return res
}
}
const createSetFn = () => {
return function set(target,key,value,receiver){
const res = Reflect.set(target,key,value,receiver)
trigger(target,key)
console.log("set",value)
return res
}
}
const get = createGetFn()
const set = createSetFn()
const handles = {get,set} //注意这里必需是 get set 不能自己重命名
/src/components/effect.js
由于需要动态获取effect方法,所以需要把track和trigger 定义放在里面。导出给reactive使用
let activeEffect
export function effect(fn) {
const effectFn = () => {
activeEffect = effectFn
fn()
activeEffect = null
}
effectFn()
return effectFn
}
const targetMap = new WeakMap() //设置外层大map关联所有对象
//收集依赖
export function track(target,key) {
let depsMap = targetMap.get(target) // 获取具体对象map
if(!depsMap) {
depsMap = new Map()
targetMap.set(target,depsMap)
}
//设置具体对象对应的属性关联effect方法map
let effectSet = depsMap.get(key)
if(!effectSet) {
effectSet = new Set()
}
if(!effectSet.has(activeEffect) && activeEffect) {
effectSet.add(activeEffect)
}
depsMap.set(key,effectSet)
}
//触发依赖
export function trigger(target,key) {
const depsMap = targetMap.get(target)
if(!depsMap) {
return
}
const effectSet = depsMap.get(key)
if(!effectSet) {
return
}
effectSet && effectSet.forEach(fn => {
if(typeof fn === 'function'){
fn()
}
})
}
测试
npm run test:unit
vscode+ node 调试
使用ndb
npm install -g ndb
ndb npm run test
3.ref
实现思路
- 直接通过对象的get 和 set方法 访问隐藏的value值。
- 如果发现是对象则嵌套调用reactive返回。
- 每次在get set的对象里加入__isRef = true,再下次传入检验是否已经存在该属性,减少重复
代码实现
新增测试用例 ref.spec.js
import { describe, it, expect } from 'vitest'
import { effect } from '../effect'
import { ref } from '../ref'
describe('响应式测试', () => {
it('ref测试', () => {
const ret3 = ref(0)
let val3
effect(() => {
val3 = ret3.value
})
expect(val3).toBe(0)
ret3.value++
expect(val3).toBe(1)
let obj = {num4:1}
const ret4 = ref(obj)
let val4
effect(() => {
val4 = ret4.value.num4
})
expect(val4).toBe(1)
ret4.value.num4++
expect(val4).toBe(2)
const ret5 = ref(ret4)
expect(ret5).toBe(ret4)
})
})
ref.js
import { track, trigger } from './effect'
import { reactive } from './reactive'
export function ref(obj) {
console.log("obj.__isRef",obj.__isRef)
if(obj.__isRef) { //缓存优化 下次重复传入会忽略
console.log("命中缓存,直接返回")
return obj
}
return new RefImpl(obj)
}
class RefImpl {
constructor(obj) {
this.__isRef = true //缓存优化
this._value = chkToRective(obj)
}
get value() {
track(this, 'value')
return this._value
}
set value(newVal) {
if (this._value !== newVal) {
this._value = chkToRective(newVal)
trigger(this, 'value')
}
}
}
const chkToRective = (val) => {
if (typeof val === 'object') {
return reactive(val)
}
return val
}
测试用例
import { describe, it, expect } from 'vitest'
import { effect } from '../effect'
import { reactive } from '../reactive'
import { ref } from '../ref'
import { computed } from '../computed'
describe('响应式测试', () => {
it('ref测试', () => {
const ret3 = ref(0)
let val3
effect(() => {
val3 = ret3.value
})
expect(val3).toBe(0)
ret3.value++
expect(val3).toBe(1)
let obj = {num4:1}
const ret4 = ref(obj)
let val4
effect(() => {
val4 = ret4.value.num4
})
expect(val4).toBe(1)
ret4.value.num4++
expect(val4).toBe(2)
const ret5 = ref(ret4)
expect(ret5).toBe(ret4)
})
})
4.computed
实现思路
- 改造effect.js 的effect方法,不是立即执行,而且通过fn.scheduler, 用于自定义触发逻辑。
- 改造effect.js 的trigger方法,遍历触发方法的时候判断是否含有scheduler,有则执行scheduler,没有才执行原来的fn。
effect.js 修改点
let activeEffect
export function effect(fn, options = {}) {
// effect嵌套,通过队列管理
const effectFn = () => {
try {
activeEffect = effectFn
//fn执行的时候,内部读取响应式数据的时候,就能在get配置里读取到activeEffect
return fn()
} finally {
activeEffect = null
}
}
if (!options.lazy) {
effectFn()
}
effectFn.scheduler = options.scheduler // 延迟执行
return effectFn
}
//触发依赖
export function trigger(target,key) {
const depsMap = targetMap.get(target)
if(!depsMap) {
return
}
const effectSet = depsMap.get(key)
if(!effectSet) {
return
}
effectSet && effectSet.forEach(fn => {
if(typeof fn === 'function'){
if (fn.scheduler) {
fn.scheduler()
} else {
fn()
}
}
})
}
- computed方法支持get和set方法
- 新建一个ComputedRefImpl类,跟ref类似,有get set 方法操作内置的_value
- ComputedRefImpl 新增上面传入的get和set方法
- ComputedRefImpl 新增dirty属性,用于get方法重复获取值时候,缓存处理
- ComputedRefImpl 新增effect的内置方法,通过fn.scheduler的方法,执行如下
constructor(getter,setter) {
this._getter = getter
this._setter = setter
this._value = undefined
this._dirty = true
this.effect = effect(getter, { // 这里二次包裹computed内编写的代码。让里面触发的变量变成响应式, lazy为false 延迟执行
lazy: true,
scheduler: () => {
this._value = getter()
},
})
}
代码实现
测试用例 computed.spec.js
import { describe, it, expect } from 'vitest'
import { effect } from '../effect'
import { reactive } from '../reactive'
import { ref } from '../ref'
import { computed } from '../computed'
describe('computed测试',()=>{
it('computed base',()=>{
const ret = reactive({ count: 1 })
const num = ref(2)
const sum = computed(() => num.value + ret.count)
expect(sum.value).toBe(3)
ret.count++
expect(sum.value).toBe(4)
num.value = 10
expect(sum.value).toBe(12)
})
it('computed修改',()=>{
const author = ref('xxxx')
const course = ref('yyyy')
const title = computed({
get(){
return author.value+":"+course.value
},
set(val){
[author.value,course.value] = val.split(':')
}
})
expect(title.value).toBe('xxxx:yyyy')
author.value="aaa"
course.value="bbb"
expect(title.value).toBe('aaa:bbb')
// //计算属性赋值
title.value = 'cc:dd'
expect(author.value).toBe('cc')
expect(course.value).toBe('dd')
})
})
先优化effect代码,让其支持延迟执行effect的方法
effect.js
let activeEffect
export function effect(fn, options = {}) {
// effect嵌套,通过队列管理
const effectFn = () => {
try {
activeEffect = effectFn
//fn执行的时候,内部读取响应式数据的时候,就能在get配置里读取到activeEffect
return fn()
} finally {
activeEffect = null
}
}
if (!options.lazy) {
effectFn()
}
effectFn.scheduler = options.scheduler // 延迟执行
return effectFn
}
const targetMap = new WeakMap() //设置外层大map关联所有对象
//收集依赖
export function track(target,key) {
let depsMap = targetMap.get(target) // 获取具体对象map
if(!depsMap) {
depsMap = new Map()
targetMap.set(target,depsMap)
}
//设置具体对象对应的属性关联effect方法map
let effectSet = depsMap.get(key)
if(!effectSet) {
effectSet = new Set()
}
if(!effectSet.has(activeEffect) && activeEffect) {
effectSet.add(activeEffect)
}
depsMap.set(key,effectSet)
}
//触发依赖
export function trigger(target,key) {
const depsMap = targetMap.get(target)
if(!depsMap) {
return
}
const effectSet = depsMap.get(key)
if(!effectSet) {
return
}
effectSet && effectSet.forEach(fn => {
if(typeof fn === 'function'){
if (fn.scheduler) {
fn.scheduler()
} else {
fn()
}
}
})
}
computed.js
import { effect} from './effect.js'
export function computed(fnOrOptions) {
let getter,setter;
if( typeof fnOrOptions === 'function') {
getter = fnOrOptions
setter = () => {
console.log('computed value 只读')
}
}else{
getter = fnOrOptions.get
setter = fnOrOptions.set
}
return new ComputedRefImpl(getter,setter)
}
class ComputedRefImpl {
constructor(getter,setter) {
this._getter = getter
this._setter = setter
this._value = undefined
this._dirty = true
this.effect = effect(getter, { // 这里二次包裹computed内编写的代码。让里面触发的变量变成响应式, lazy为false 延迟执行
lazy: true,
scheduler: () => {
this._value = getter()
},
})
}
get value() {
if(this._dirty) { // 这里重复访问 computed的xx.value 时候缓存处理
this._dirty = false
this._value = this.effect()
}
return this._value
}
set value(newValue) {
this._setter(newValue)
this._dirty = true
}
}