从0到1实现一个响应式函数

992 阅读7分钟

响应式是啥

可以自动响应数据变量的代码机制,我们就称之为是响应式的。


let m = 2
console.log(m)
console.log(m ** 2)

响应式是当m被赋予新值的时候上面的两个打印重新执行 也就是当变量值变化的时候,与之变量有关的函数重新执行一遍,发生在页面上的表现是,当值变化时,展示也发生变化。 js是面向对象语言,那么对象中的响应式如下:

obj = {
  name: 'lss',
  age: 18,
} // 现在obj是一个响应式对象
obj.name = 'luyi' // obj的值变化

// 页面上执行
let newValue = obj.name
console.log(obj.name) // 更新为新值

响应式函数的设计

最简单的响应式函数

根据上面的理解,可以设计出以下最简单版本的响应式函数

obj = {
  name: 'lss',
  age: 18,
}

// 用于收集响应式函数
const watchFn = function(fn) {
  fn()
};
obj.name = 'luyi';
watchFn(() => {
  console.log('obj.name>>>>>', obj.name);
});

打印结果如下:

obj.name>>>>> luyi

封装收集响应式函数的函数

但是有一个问题,当响应式函数一多,就需要写很多个watchFn函数,这样才可以一一响应,有什么办法呢?用数组来收集响应式函数,上面的可以改进,变成如下:

obj = {
  name: 'lss',
  age: 18,
};

// 用于收集响应式函数
let reactiveFns = [];
const watchFn = function(fn) {
  reactiveFns.push(fn);
};
watchFn(() => {
  console.log('obj.name>>>>>', obj.name);
});
watchFn(() => {
  console.log('obj.age>>>>>', obj.age);
});

obj.name = "luyi"
reactiveFns.forEach(fn => {
  fn()
});

打印结果如下:

obj.name>>>>> luyi
obj.age>>>>> 18

封装收集响应式函数的类

现在不用写很多个watchFn函数了,但是还有不便之处,就是需要我们手动让响应函数们执行,手动触发监听函数,还是不太好,进一步优化

// 用于收集响应式函数
class Depend {
  constructor() {
    this.reactiveFns = []
  }
  addDepend(fn) {
    if(fn) {
      this.reactiveFns.push(fn)
    }
  }
  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}

// 封装一个响应式的函数
const depend = new Depend()
function watchFn(fn) {
  depend.addDepend(fn)
}

obj = {
  name: 'lss',
  age: 18,
};

watchFn(() => {
  console.log('obj.name>>>>>', obj.name);
});
watchFn(() => {
  console.log('obj.age>>>>>', obj.age);
});

obj.name = 'luyi';
depend.notify();

输出结果和上面一样,你可能想这个不是和上面的响应方式一样吗?只是变成了类来管理响应函数和通知,不着急,慢慢看,类的作用还没体现出来

在类里面实现自动响应

  1. 存取属性符 + proxy
// 用于收集响应式函数
class Depend {
  constructor() {
    this.reactiveFns = []
  }
  addDepend(fn) {
    if(fn) {
      this.reactiveFns.push(fn)
    }
  }
  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}

// 封装一个响应式的函数
const depend = new Depend()
function watchFn(fn) {
  depend.addDepend(fn)
}

obj = {
  name: 'lss',
  age: 18,
};

const objProxy = new Proxy(obj, {
  get(target, key, receiver) {
    return Reflect.get(obj, key, receiver)
  },
  set(target, key, newValue, receiver ) {
    Reflect.set(obj, key, newValue, receiver)
    depend.notify() // 在此处实现自动响应,只要obj的属性有变化,那么就会执行响应函数
  }
})

watchFn(() => {
  console.log('obj.name>>>>>', obj.name);
});
watchFn(() => {
  console.log('obj.age>>>>>', obj.age);
});
objProxy.name = 'luyi'; // 使用代理对象对obj的属性或者方法进行操作

打印结果和上条一样 2. 存取属性符 + defineProperty方法

// 用于收集响应式函数
class Depend {
  constructor() {
    this.reactiveFns = []
  }
  addDepend(fn) {
    if(fn) {
      this.reactiveFns.push(fn)
    }
  }
  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}

// 封装一个响应式的函数
const depend = new Depend()
function watchFn(fn) {
  depend.addDepend(fn)
}

obj = {
  name: 'lss',
  age: 18,
};

// const objProxy = new Proxy(obj, {
//   get(target, key, receiver) {
//     return Reflect.get(obj, key, receiver)
//   },
//   set(target, key, newValue, receiver ) {
//     Reflect.set(obj, key, newValue, receiver)
//     depend.notify() // 在此处实现自动响应,只要obj的属性有变化,那么就会执行响应函数
//   }
// })
// 以上替换成以下
function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get() {
        return value;
      },
      set(newVal) {
        value = newVal;
        depend.notify()
      }
    })
  })
  return obj;
}

const objProxy = reactive(obj)

watchFn(() => {
  console.log('obj.name>>>>>', obj.name);
});
watchFn(() => {
  console.log('obj.age>>>>>', obj.age);
});

objProxy.name = 'luyi'; // 使用对象的存取属性符和defineProperty方法,来收集依赖

打印结果和上面一样 现在利用类的存取描述符以及Proxy或者defineProperty实现了自动响应 但是还是不完善,现在我们只改变了obj的name属性,为什么与age属性有关的也打印了呢?说明我们没有正确收集响应函数,响应函数没有正确依赖,所以还需要完善; 怎么完善呢? 按理应该每个属性,都应该对应着自己的许多个响应函数,也就是一个属性,应该对应着一个reactiveFns,而一个类只有一个reactiveFns,所以每个属性都对应着一个上面的类,怎么实现一个属性对应一个类?这个就要用到es6里面的map数据结构,往下看,如何使用:

正确的收集依赖

  1. proxy
// 用于收集响应式函数
class Depend {
  constructor() {
    this.reactiveFns = []
  }
  addDepend(fn) {
    if(fn) {
      this.reactiveFns.push(fn)
    }
  }
  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}

// 这个函数很重要,必须理解存取符,才能理解这一步
let activeReactiveFn = null;
function watchFn(fn) {
  activeReactiveFn = fn;
  fn(); // 在这个过程中收集响应式函数, 用到哪个属性,这个函数就是哪个属性的响应函数
  activeReactiveFn = null; // 收集完毕,清空activeReactiveFn
}
obj = {
  name: 'lss',
  age: 18,
};
debugger;
// 封装获取对应依赖的函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  let map = targetMap.get(target)
  if(!map) {
    map = new Map()
    targetMap.set(target, map)
  }
  let depend = map.get(key)
  if(!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend;
}

const objProxy = new Proxy(obj, {
  get(target, key, receiver) {
    const depend = getDepend(target, key);
    depend.addDepend(activeReactiveFn)
    return Reflect.get(obj, key, receiver)
  },
  set(target, key, newValue, receiver ) {
    Reflect.set(obj, key, newValue, receiver)
    const depend = getDepend(target, key);
    depend.notify() // 在此处实现自动响应,只要obj的属性有变化,那么就会执行响应函数
  }
})

// function reactive(obj) {
//   Object.keys(obj).forEach(key => {
//     let value = obj[key]
//     Object.defineProperty(obj, key, {
//       get() {
//         return value;
//       },
//       set(newVal) {
//         value = newVal;
//         depend.notify()
//       }
//     })
//   })
//   return obj;
// }

// const objProxy = reactive(obj)

watchFn(() => {
  console.log('obj.name>>>>>', objProxy.name); // 不直接对obj进行操作,使用代理函数操作
});
watchFn(() => {
  console.log('obj.age>>>>>', objProxy.age);
});

objProxy.name = 'luyi'; // 使用对象的存取属性符和defineProperty方法,来收集依赖
  1. defineProperty()
// 用于收集响应式函数
class Depend {
  constructor() {
    this.reactiveFns = []
  }
  addDepend(fn) {
    if(fn) {
      this.reactiveFns.push(fn)
    }
  }
  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}

// 这个函数很重要,必须理解存取符,才能理解这一步
let activeReactiveFn = null;
function watchFn(fn) {
  activeReactiveFn = fn;
  fn(); // 在这个过程中收集响应式函数, 用到哪个属性,这个函数就是哪个属性的响应函数
  activeReactiveFn = null; // 收集完毕,清空activeReactiveFn
}
obj = {
  name: 'lss',
  age: 18,
};
debugger;
// 封装获取对应依赖的函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  let map = targetMap.get(target)
  if(!map) {
    map = new Map()
    targetMap.set(target, map)
  }
  let depend = map.get(key)
  if(!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend;
}

// const objProxy = new Proxy(obj, {
//   get(target, key, receiver) {
//     const depend = getDepend(target, key);
//     depend.addDepend(activeReactiveFn)
//     return Reflect.get(obj, key, receiver)
//   },
//   set(target, key, newValue, receiver ) {
//     Reflect.set(obj, key, newValue, receiver)
//     const depend = getDepend(target, key);
//     depend.notify() // 在此处实现自动响应,只要obj的属性有变化,那么就会执行响应函数
//   }
// })

function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get() {
        const depend = getDepend(obj, key);
        depend.addDepend(activeReactiveFn)
        return value;
      },
      set(newVal) {
        value = newVal;
        const depend = getDepend(obj, key);
        depend.notify()
      }
    })
  })
  return obj;
}

const objProxy = reactive(obj)

watchFn(() => {
  console.log('obj.name>>>>>', objProxy.name); // 不直接对obj进行操作,使用代理函数操作
});
watchFn(() => {
  console.log('obj.age>>>>>', objProxy.age);
});

objProxy.name = 'luyi'; // 使用对象的存取属性符和defineProperty方法,来收集依赖

打印结果如下:

obj.name>>>>> lss # 添加监听时的执行结果
obj.age>>>>> 18 # 添加监听时的执行结果
obj.name>>>>> luyi # objProxy.name = 'luyi'之后的响应执行结果

现在成功实现自动响应及自动收集对应响应 但是还有不完美的地方,比如:监听多个一模一样的函数,这个时候reactiveFns内部就会存放多个内存地址不同但是作用相同的函数,这个时候就可以用set数据结构来进行优化:

watchFn(() => {
  console.log('obj.name>>>>>', objProxy.name); // 不直接对obj进行操作,使用代理函数操作
});
watchFn(() => {
  console.log('obj.name>>>>>', objProxy.name);
});

优化完善, 只用更改reactiveFns的数据结构属性即可,将Array变成Set

优化之后的响应式结构:

// 用于收集响应式函数
let set = new Set([]);
class Depend {
  constructor() {
    this.reactiveFns = set
  }
  addDepend(fn) {
    if(fn) {
      this.reactiveFns.add(fn)
    }
  }
  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}

// 这个函数很重要,必须理解存取符,才能理解这一步
let activeReactiveFn = null;
function watchFn(fn) {
  activeReactiveFn = fn;
  fn(); // 在这个过程中收集响应式函数, 用到哪个属性,这个函数就是哪个属性的响应函数
  activeReactiveFn = null; // 收集完毕,清空activeReactiveFn
}
obj = {
  name: 'lss',
  age: 18,
};
debugger;
// 封装获取对应依赖的函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  let map = targetMap.get(target)
  if(!map) {
    map = new Map()
    targetMap.set(target, map)
  }
  let depend = map.get(key)
  if(!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend;
}

// const objProxy = new Proxy(obj, {
//   get(target, key, receiver) {
//     const depend = getDepend(target, key);
//     depend.addDepend(activeReactiveFn)
//     return Reflect.get(obj, key, receiver)
//   },
//   set(target, key, newValue, receiver ) {
//     Reflect.set(obj, key, newValue, receiver)
//     const depend = getDepend(target, key);
//     depend.notify() // 在此处实现自动响应,只要obj的属性有变化,那么就会执行响应函数
//   }
// })

function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get() {
        const depend = getDepend(obj, key);
        depend.addDepend(activeReactiveFn)
        return value;
      },
      set(newVal) {
        value = newVal;
        const depend = getDepend(obj, key);
        depend.notify()
      }
    })
  })
  return obj;
}

const objProxy = reactive(obj)

logName = () => {
  console.log('obj.name>>>>>', objProxy.name); 
}
watchFn(logName);
watchFn(logName);

objProxy.name = 'luyi'; // 使用对象的存取属性符和defineProperty方法,来收集依赖

打印结果如下:

obj.name>>>>> lss # 添加监听时的执行结果
obj.name>>>>> lss # 添加监听时的执行结果
obj.name>>>>> luyi # objProxy.name = 'luyi'之后的响应执行结果

总结

以上是两种响应式函数的封装方法,proxy的是vue3里面使用的,defineProperty是vue2里面使用的