RObserver—给 React 打个 watch 补丁

RObserver是为了解决React Component 不能方便的监控数据变化的痛点。

我们先来看一下 React Component 的生命周期


image

React生命周期图

如果我们需要页面中某个数据(监控对象)发生变化时,产生副作用让另一个数据(作用对象)也发生变化。这个监控对象,可能是父组件传来的值,也可能是redux中的状态,也可能是当前组件的state。

为了实现上述需求。我们需要在 componentWillUpdate 或者 componentDidUpdate 生命周期中,将监控对象改变前后的数据进行对比,再对作用对象做出相应的操作。但是如果监控对象很多,处理起来就会很麻烦。如下代码演示:

componentWillUpdate(nextProps, nextState) {    const { count } = this.state;    const { targetA, targetB, targetC } = this.props;    const { targetA: nTargetA, targetB: nTargetB, targetC: nTargetC } = nextProps;    if (targetA !== nTargetA) {      this.setState({ count: count + 1 });    }    if (      targetB !== nTargetB &&      !(        [targetB, nTargetB].every(t => Array.isArray(t)) &&        targetB.toString() === nTargetB.toString()      )    ) {      this.setState({ count: count + 1 });    }    if (      targetC !== nTargetC &&      !(        [targetB, nTargetB].every(t => Object.prototype.toString.apply(t) === '[object Object]') &&        JSON.stringify(targetB) === JSON.stringify(nTargetB)      )    ) {      this.setState({ count: count + 1 });    }  }}

RObserver工具是为了简化该过程,引入工具后只需要改成下面这种就可以了:

constructor(props) {    super(props);    this.state.rob = new RObserver();  }  componentDidMount() {    this.state.rob.subscribe(this, {      'props.targetA': this.observerChange,      'props.targetB': {        deep: true,        handler: this.observerChange,      },      'props.targetC': {        deep: true,        handler: this.observerChange,      },      'props.targetD': {        compareFun: (pre, curr) => {          return pre.name === curr.name;        },        handler: this.observerChange,      },    });  }  observerChange = () => {    const { count } = this.state;    this.setState({ count: count + 1 });  };  componentDidUpdate() {    this.state.rob.watch(this);  }

RObserver 工具执行逻辑如图:

image

源码如下:

// 监控处理函数type HandlerCb = (pre: any, curr: any) => void;// 监控方法处理方式type Observer = {  target: string; // 监控对象  compareFun: (pre: any, curr: any) => boolean; // 对比函数,用户可以自定义是否触发监控回调函数  watcherList: HandlerCb[]; // 监控回调,当上一次记录的值与当前值不同时触发  preValue?: any; // 记录上一次的值  deep?: boolean; // 是否深度监控,如果开启深度监控,将会使用deepCompare来深度对比两个对象,};type HandlerConfig = {  handler: HandlerCb;  deep: boolean;  debounce?: number; // delay有值时开启防抖  compareFun: (pre: any, curr: any) => boolean;};// 所有监控对象type FunMap = {  [target: string]: HandlerCb | HandlerConfig;};interface RObserver {  // 监控对象映射  obMap: {    [target: string]: Observer[];  };  // 注册监控对象  subscribe: (context: Object, funMap: FunMap) => void;  //   开启监控  watch: (context: Object) => void;  //   注销对象监控  destroy: (target: string, fun?: Function) => void;}class RObserver {  constructor() {    this.obMap = {};    this.subscribe = function(context: Object, funMap: FunMap) {      Object.keys(funMap).forEach(key => {        let watcher: Function;        let config: any = {};        if (typeof funMap[key] === 'function') {          watcher = funMap[key] as HandlerCb;        } else if (typeof funMap[key] === 'object') {          // 函数防抖          const { handler = () => {}, debounce: delay = 0, compareFun, deep } = funMap[            key          ] as HandlerConfig;          if (/^[1-9]\d*/.test(delay + '')) watcher = debounce(handler, delay);          else watcher = handler;          config = { compareFun, deep };        }        if (!this.obMap[key]) {          const preValue = getValue(key, context);          this.obMap[key] = {            target: key,            preValue: config.deep ? deepCloneObject(preValue) : preValue,            watcherList: [watcher],            ...config,          };        } else if (this.obMap[key].watcherList.indexOf(watcher) === -1) {          this.obMap[key].watcherList.push(watcher);        }      });    };    this.watch = function(context) {      Object.values(this.obMap).forEach(        ({ target, preValue, watcherList, deep, compareFun }: Observer) => {          const value = getValue(target, context);          const currValue = deep ? deepCloneObject(value) : value;          if (compareFun) {            if (!compareFun(preValue, currValue)) {              watcherList.forEach(watcher => watcher(preValue, currValue));            }          } else if (deep) {            if (!deepCompare(preValue, currValue)) {              watcherList.forEach(watcher => watcher(preValue, currValue));            }          } else {            if (preValue !== currValue) {              watcherList.forEach(watcher => watcher(preValue, currValue));            }          }          this.obMap[target].preValue = currValue;        }      );    };    this.destroy = function(target: string, fun?: Function) {      if (!target) throw new Error('必须指定注销目标');      if (!this.obMap[target] || !this.obMap[target].watcherList) throw new Error('注销目标未注册');      if (fun) {        this.obMap[target].watcherList = this.obMap[target].watcherList.filter(f => f !== fun);      } else {        delete this.obMap[target];      }    };  }}// 深度对比两个对象export function deepCompare(...args: any[]): boolean {  let i, l, leftChain, rightChain;  function compare2Objects(x: any, y: any) {    let p;    // remember that NaN === NaN returns false    // and isNaN(undefined) returns true    if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {      return true;    }    // Compare primitives and functions.    // Check if both arguments link to the same object.    // Especially useful on the step where we compare prototypes    if (x === y) {      return true;    }    // Works in case when functions are created in constructor.    // Comparing dates is a common scenario. Another built-ins?    // We can even handle functions passed across iframes    if (      (typeof x === 'function' && typeof y === 'function') ||      (x instanceof Date && y instanceof Date) ||      (x instanceof RegExp && y instanceof RegExp) ||      (x instanceof String && y instanceof String) ||      (x instanceof Number && y instanceof Number)    ) {      return x.toString() === y.toString();    }    // At last checking prototypes as good as we can    if (!(x instanceof Object && y instanceof Object)) {      return false;    }    if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {      return false;    }    if (x.constructor !== y.constructor) {      return false;    }    if (x.prototype !== y.prototype) {      return false;    }    // Check for infinitive linking loops    if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {      return false;    }    // Quick checking of one object being a subset of another.    // todo: cache the structure of arguments[0] for performance    for (p in y) {      if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {        return false;      } else if (typeof y[p] !== typeof x[p]) {        return false;      }    }    for (p in x) {      if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {        return false;      } else if (typeof y[p] !== typeof x[p]) {        return false;      }      switch (typeof x[p]) {        case 'object':        case 'function':          leftChain.push(x);          rightChain.push(y);          if (!compare2Objects(x[p], y[p])) {            return false;          }          leftChain.pop();          rightChain.pop();          break;        default:          if (x[p] !== y[p]) {            return false;          }          break;      }    }    return true;  }  // throw "Need two or more arguments to compare";  if (args.length < 1) {    return true;  }  leftChain = []; // this can be cached  rightChain = [];  if (!compare2Objects(args[0], args[i])) {    return false;  }  return true;}function getValue(key: string, obj: any) {  // 匹配 a.b[c.d].e.f 这样的字符 切割成['a','b','c.d','e','f']数组  const keyList = key.split(/\.|\[(.*)\]/g).filter(k => !!k);  return keyList.reduce((p, c) => (p || {})[c], obj);}function debounce(func: Function, wait: number = 200): Function {  let timeout;  function f(...args) {    const context = this;    if (timeout) clearTimeout(timeout);    timeout = setTimeout(() => {      func.apply(context, args);    }, wait);  }  return f;}const deepCloneObject = obj => {  let clone;  if (Object.prototype.toString.apply(obj) === '[object Object]') {    if (obj.constructor.name === 'Object') {      clone = {};      Object.keys(obj).forEach(key => {        clone[key] = deepCloneObject(obj[key]);      });    } else {      clone = obj;    }  } else if (Object.prototype.toString.apply(obj) === '[object Array]') {    clone = obj.map(e => deepCloneObject(e));  } else {    clone = obj;  }  return clone;};export default RObserver;
 另外,RObserver 还提供了 destroy 函数,来解除对监控对象的监控。

创新点

RObserver 除了可以在当前组件内进行监控,还可以让用户通过单例模式实现跨组件的状态监控。

示例如下:

// ob.jsconst rob = new RObserver();export default rob;// a.jsimport rob from 'xx/ob.js';class A extends React.PureComponent {  componentDidMount() {    rob.subscribe(      { targetA: 1 },      {        targetA: handler,      }    );  }}// b.jsimport rob from 'xx/ob.js';class B extends React.PureComponent {  componentDidUpdate() {    rob.watch({ targetA: 2 });  }}
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容