实现一个极简单的Mobx

先写个测试

最近写React的时候,使用 mobxmobx-state-tree 作为状态管理。很喜欢 mobx 的使用方式。所以试着弄懂它背后的原理。

对于Mobx来说,最重要的是两个功能: observableautorun 以及 autorun对应的依赖收集功能。

下面的示例代码只是为了解释原理,有很多功能缺失和不完善的地方

先写一个简单的单元测试,用来表示observableautorun 的功能

const { observable, autorun } = require('../index');

it('should pass', (done) => {
    let changed = false;
    let shouldFailed = false;
    const testObject = observable({ a: 10, b: 'test' });
    autorun(() => {
      console.log('autorun...', testObject.a);
      expect(testObject.a).toBe(changed ? 11 : 10);
      if (shouldFailed) { expect(shouldFailed).toBe(false) }
      done()
    });
    changed = true;
    testObject.a = 11;
    shouldFailed = true;
    testObject.b = '123';
})
  • 把一个对象变成了可观察。这个对象有两个字段ab
  • autorun中,检测到函数只依赖.a
  • .a被赋值的时候,执行autorun里的函数。
  • .b被赋值,不会执行autorun中的函数,因为.b并没有被autorun依赖。

依赖收集

  1. 需要知道autorun(handler)中,函数handler都用到了observable对象的哪些变量。
  2. 通过observable包装一个对象得到,使得对象中的字段被使用时(读取、赋值等)能够被触发一个函数。
  3. 当运行 handler 的时候,所有使用observable对象的地方,都会被收集为依赖。
  4. observable对象的某个字段被赋值时,能够触发与之关联的所有autorun函数。

实现

我们先实现observable方法。完成上述提到的第二条。需要用到Proxy 功能。Proxy 是用于给set get 的时候,加一个钩子。关于Proxy的具体使用,可以看一下MDN的文档。这里直接上代码+注释。

const proxyHandler = {
// 当尝试对target读取某个name字段时,会调用此方法
  get(target, name) {
    console.log('someone try to get...');
    return target[name];
  },
// 当尝试把target的name字段赋值为value时,会调用此方法
  set(target, name, value) {
    console.log('some try to set ...');
    target[name] = value;
    return true;
  }
};

function observable(origin) {
  return new Proxy(origin, proxyHandler);
}

接着实现上述提到的第三条。就是在运行handler的时候,开始收集依赖。Mobx的做法是,给每个observable对象,都加一个$mobx字段。这里我们也采用这样的方法。

class Admin {
  constructor() {
  // 我为了简单,这里就采用 field -> handlers 的映射
    this.dependsMap = new Map();
  }

  // 保存依赖...
  save(field, handler) {
    const depends = this.dependsMap.get(field) || new Set();
    depends.add(handler);
    this.dependsMap.set(field, depends);
  }

  // 当某个字段被修改时,调用此函数,触发依赖此字段的所有autorun handler
  trigger(field) {
    if (this.dependsMap.has(field)) {
      const handlers = this.dependsMap.get(field);
      for (const func of handlers) {
      // 注意这里,在trigger里面,也需要收集handler的依赖
        collecting = { func };
        func();
        collecting = null;
      }
    }
  }

  isDepend(field) {
    return this.dependsMap.has(field);
  }
}

function observable (object) {
  Object.defineProperty(object, '$fake', {
    enumerable: false, // 为了让 $fake 字段不能够被遍历出来
    writeable: true,
    configurable: true,
    value: new Admin()
  });
  return new Proxy(object, proxyHandler);
}

最后,我们直接实现上述提到的第一步和第四步吧。

let collecting = null;
// autorun方法非常简单,就是在调用func之前,记录一下当前正在收集依赖即可。
function autorun (func) {
  collecting = { func };
  func();
  collecting = null;
}

再完善一下proxyHandler

  • set时,
    • 如果依赖收集开启中,则把set存入依赖收集中。注意这里有一个没处理的情况:我在handler1里修改了某个字段,应该让其它依赖此字段的handler都执行
    • 如果依赖收集没有开启,则触发此字段的handler方法
  • get 时,
    • 如果开启依赖收集,则收集依赖后,返回结果
    • 如果没开启,则直接返回此字段的值。

const objectProxyHandler = {
  get(target, name) {
    if (name === '$fake') { return target[name] }
    if (collecting) {
      target.$fake.save(name, collecting.func);
    }
    return target[name];
  },
  set(target, name, value) {
    target[name] = value;
    if (collecting) {
      target.$fake.save(name, collecting.func);
    } else {
      if (target.$fake.isDepend(name)) {
        target.$fake.trigger(name);
      }
    }
    return true;
  },
};

这样... 就实现了最简单的 observableautorun 功能。

完整代码如下:

class Admin {
  constructor() {
    this.dependsMap = new Map();
  }

  save(field, handler) {
    const depends = this.dependsMap.get(field) || new Set();
    depends.add(handler);
    this.dependsMap.set(field, depends);
  }

  trigger(field) {
    if (this.dependsMap.has(field)) {
      const handlers = this.dependsMap.get(field);
      for (const func of handlers) {
        collecting = { func };
        func();
        collecting = null;
      }
    }
  }

  isDepend(field) {
    return this.dependsMap.has(field);
  }
}

let collecting = null;

const objectProxyHandler = {
  get(target, name) {
    if (name === '$fake') {
      return target[name];
    }
    if (collecting) {
      target.$fake.save(name, collecting.func);
    }
    return target[name];
  },
  set(target, name, value) {
    target[name] = value;
    if (collecting) {
      target.$fake.save(name, collecting.func);
    } else {
      if (target.$fake.isDepend(name)) {
        target.$fake.trigger(name);
      }
    }
    return true;
  },
};

function observable (object) {
  Object.defineProperty(object, '$fake', {
    enumerable: false,
    writeable: true,
    configurable: true,
    value: new Admin()
  });
  return new Proxy(object, objectProxyHandler);
}

function autorun (func) {
  collecting = { func };
  func();
  collecting = null;
}

module.exports = {
  observable,
  autorun
};