{"action":"created","issue":{"url":"https://api.github.com/repos/berwin/Blog/issues/17","repository_url":"https://api.github.com/repos/berwin/Blog","labels_url":"https://api.github.com/repos/berwin/Blog/issues/17/labels{/name}","comments_url":"https://api.github.com/repos/berwin/Blog/issues/17/comments","events_url":"https://api.github.com/repos/berwin/Blog/issues/17/events","html_url":"https://github.com/berwin/Blog/issues/17","id":277677735,"node_id":"MDU6SXNzdWUyNzc2Nzc3MzU=","number":17,"title":"深入浅出 - vue变化侦测原理","user":{"login":"berwin","id":3739368,"node_id":"MDQ6VXNlcjM3MzkzNjg=","avatar_url":"https://avatars0.githubusercontent.com/u/3739368?v=4","gravatar_id":"","url":"https://api.github.com/users/berwin","html_url":"https://github.com/berwin","followers_url":"https://api.github.com/users/berwin/followers","following_url":"https://api.github.com/users/berwin/following{/other_user}","gists_url":"https://api.github.com/users/berwin/gists{/gist_id}","starred_url":"https://api.github.com/users/berwin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/berwin/subscriptions","organizations_url":"https://api.github.com/users/berwin/orgs","repos_url":"https://api.github.com/users/berwin/repos","events_url":"https://api.github.com/users/berwin/events{/privacy}","received_events_url":"https://api.github.com/users/berwin/received_events","type":"User","site_admin":false},"labels":[{"id":354601797,"node_id":"MDU6TGFiZWwzNTQ2MDE3OTc=","url":"https://api.github.com/repos/berwin/Blog/labels/javascript","name":"javascript","color":"fbca04","default":false},{"id":506002292,"node_id":"MDU6TGFiZWw1MDYwMDIyOTI=","url":"https://api.github.com/repos/berwin/Blog/labels/vue.js","name":"vue.js","color":"42b983","default":false}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":35,"created_at":"2017-11-29T09:09:02Z","updated_at":"2018-06-11T07:34:37Z","closed_at":null,"author_association":"OWNER","body":"# 深入浅出 - vue变化侦测原理\r\n\r\n其实在一年前我已经写过一篇关于  [vue响应式原理的文章](https://github.com/berwin/Blog/issues/11)，但是最近我翻开看看发现讲的内容和我现在心里想的有些不太一样，所以我打算重新写一篇更通俗易懂的文章。\r\n\r\n我的目标是能让读者读完我写的文章能学到知识，有一部分文章标题都以深入浅出开头，目的是把一个复杂的东西排除掉干扰学习的因素后剩下的核心原理通过很简单的描述来让读者学习到知识。\r\n\r\n关于vue的内部原理其实有很多个重要的部分，变化侦测，模板编译，virtualDOM，整体运行流程等。\r\n\r\n今天主要把变化侦测这部分单独拿出来讲一讲。\r\n\r\n## 如何侦测变化？\r\n\r\n关于变化侦测首先要问一个问题，在 js 中，如何侦测一个对象的变化，其实这个问题还是比较简单的，学过js的都能知道，js中有两种方法可以侦测到变化，`Object.defineProperty` 和 ES6 的`proxy`。\r\n\r\n到目前为止vue还是用的 `Object.defineProperty`，所以我们拿 `Object.defineProperty`来举例子说明这个原理。\r\n\r\n这里我想说的是，不管以后vue是否会用 `proxy` 重写这部分，我讲的是原理，并不是api，所以不论以后vue会怎样改，这个原理是不会变的，哪怕vue用了其他完全不同的原理实现了变化侦测，但是本篇文章讲的原理一样可以实现变化侦测，原理这个东西是不会过时的。\r\n\r\n之前我写文章有一个毛病就是喜欢对着源码翻译，结果过了半年一年人家源码改了，我写的文章就一毛钱都不值了，而且对着源码翻译还有一个缺点是对读者的要求有点偏高，读者如果没看过源码或者看的和我不是一个版本，那根本就不知道我在说什么。\r\n\r\n好了不说废话了，继续讲刚才的内容。\r\n\r\n知道 `Object.defineProperty` 可以侦测到对象的变化，那么我们瞬间可以写出这样的代码：\r\n\r\n```javascript\r\nfunction defineReactive (data, key, val) {\r\n    Object.defineProperty(data, key, {\r\n        enumerable: true,\r\n        configurable: true,\r\n        get: function () {\r\n            return val\r\n        },\r\n        set: function (newVal) {\r\n            if(val === newVal){\r\n                return\r\n            }\r\n            val = newVal\r\n        }\r\n    })\r\n}\r\n```\r\n写一个函数封装一下 `Object.defineProperty`，毕竟 `Object.defineProperty ` 的用法这么复杂，封装一下我只需要传递一个 data，和 key，val 就行了。\r\n\r\n现在封装好了之后每当 `data` 的 `key` 读取数据 `get` 这个函数可以被触发，设置数据的时候 `set` 这个函数可以被触发，但是，，，，，，，，，，，，，，，，，，发现好像并没什么鸟用？\r\n\r\n## 怎么观察？\r\n现在我要问第二个问题，“怎么观察？”\r\n\r\n思考一下，我们之所以要观察一个数据，目的是为了当数据的属性发生变化时，可以通知那些使用了这个 `key` 的地方。\r\n\r\n举个🌰：\r\n\r\n```html\r\n<template>\r\n  <div>{{ key }}</div>\r\n  <p>{{ key }}</p>\r\n</template>\r\n```\r\n模板中有两处使用了 `key`，所以当数据发生变化时，要把这两处都通知到。\r\n\r\n所以上面的问题，我的回答是，先收集依赖，把这些使用到 `key` 的地方先收集起来，然后等属性发生变化时，把收集好的依赖循环触发一遍就好了~\r\n\r\n总结起来其实就一句话，**getter中，收集依赖，setter中，触发依赖**。\r\n\r\n## 依赖收集在哪？\r\n\r\n现在我们已经有了很明确的目标，就是要在getter中收集依赖，那么我们的依赖收集到哪里去呢？？\r\n\r\n思考一下，首先想到的是每个 `key` 都有一个数组，用来存储当前 `key` 的依赖，假设依赖是一个函数存在 `window.target` 上，先把 `defineReactive` 稍微改造一下：\r\n\r\n```javascript\r\nfunction defineReactive (data, key, val) {\r\n    let dep = [] // 新增\r\n    Object.defineProperty(data, key, {\r\n        enumerable: true,\r\n        configurable: true,\r\n        get: function () {\r\n            dep.push(window.target) // 新增\r\n            return val\r\n        },\r\n        set: function (newVal) {\r\n            if(val === newVal){\r\n                return\r\n            }\r\n            \r\n            // 新增\r\n            for (let i = 0; i < dep.length; i++) {\r\n            \t dep[i](newVal, val)\r\n            }\r\n            val = newVal\r\n        }\r\n    })\r\n}\r\n```\r\n\r\n在 `defineReactive` 中新增了数组 dep，用来存储被收集的依赖。\r\n\r\n然后在触发 set 触发时，循环dep把收集到的依赖触发。\r\n\r\n但是这样写有点耦合，我们把依赖收集这部分代码封装起来，写成下面的样子：\r\n\r\n```javascript\r\nexport default class Dep {\r\n  static target: ?Watcher;\r\n  id: number;\r\n  subs: Array<Watcher>;\r\n\r\n  constructor () {\r\n    this.id = uid++\r\n    this.subs = []\r\n  }\r\n\r\n  addSub (sub: Watcher) {\r\n    this.subs.push(sub)\r\n  }\r\n\r\n  removeSub (sub: Watcher) {\r\n    remove(this.subs, sub)\r\n  }\r\n\r\n  depend () {\r\n    if (Dep.target) {\r\n      this.addSub(Dep.target)\r\n    }\r\n  }\r\n\r\n  notify () {\r\n    // stabilize the subscriber list first\r\n    const subs = this.subs.slice()\r\n    for (let i = 0, l = subs.length; i < l; i++) {\r\n      subs[i].update()\r\n    }\r\n  }\r\n}\r\n```\r\n\r\n然后在改造一下 `defineReactive`：\r\n\r\n```javascript\r\nfunction defineReactive (data, key, val) {\r\n    let dep = new Dep()        // 修改\r\n    Object.defineProperty(data, key, {\r\n        enumerable: true,\r\n        configurable: true,\r\n        get: function () {\r\n            dep.depend() // 修改\r\n            return val\r\n        },\r\n        set: function (newVal) {\r\n            if(val === newVal){\r\n                return\r\n            }\r\n\r\n            dep.notify() // 新增\r\n            val = newVal\r\n        }\r\n    })\r\n}\r\n```\r\n\r\n这一次代码看起来清晰多了，顺便回答一下上面问的问题，依赖收集到哪？收集到Dep中，Dep是专门用来存储依赖的。\r\n\r\n## 收集谁？\r\n\r\n上面我们假装 `window.target` 是需要被收集的依赖，细心的同学可能已经看到，上面的代码 `window.target` 已经改成了 `Dep.target`，那 `Dep.target`是什么？我们究竟要收集谁呢？？\r\n\r\n![黑人问号脸](http://p0.qhimg.com/t01bf92bd63d32b73bc.jpg)\r\n\r\n收集谁，换句话说是当属性发生变化后，通知谁。\r\n\r\n我们要通知那个使用到数据的地方，而使用这个数据的地方有很多，而且类型还不一样，有可能是模板，有可能是用户写的一个 watch，所以这个时候我们需要抽象出一个能集中处理这些不同情况的类，然后我们在依赖收集的阶段只收集这个封装好的类的实例进来，通知也只通知它一个，然后它在负责通知其他地方，所以我们要抽象的这个东西需要先起一个好听的名字，嗯，就叫它watcher吧~\r\n\r\n所以现在可以回答上面的问题，收集谁？？收集 Watcher。\r\n\r\n## 什么是Watcher？\r\n\r\nwatcher 是一个中介的角色，数据发生变化通知给 watcher，然后watcher在通知给其他地方。\r\n\r\n关于watcher我们先看一个经典的使用方式：\r\n\r\n```javascript\r\n// keypath\r\nvm.$watch('a.b.c', function (newVal, oldVal) {\r\n  // do something\r\n})\r\n```\r\n\r\n这段代码表示当 `data.a.b.c` 这个属性发生变化时，触发第二个参数这个函数。\r\n\r\n思考一下怎么实现这个功能呢？\r\n\r\n好像只要把这个 watcher 实例添加到 `data.a.b.c` 这个属性的 Dep 中去就行了，然后 `data.a.b.c` 触发时，会通知到watcher，然后watcher在执行参数中的这个回调函数。\r\n\r\n好，思考完毕，开工，写出如下代码：\r\n\r\n```javascript\r\nclass Watch {\r\n    constructor (expOrFn, cb) {\r\n        // 执行 this.getter() 就可以拿到 data.a.b.c\r\n        this.getter = parsePath(expOrFn)\r\n        this.cb = cb\r\n        this.value = this.get()\r\n    }\r\n\r\n    get () {\r\n        Dep.target = this\r\n        value = this.getter.call(vm, vm)\r\n        Dep.target = undefined\r\n    }\r\n\r\n    update () {\r\n        const oldValue = this.value\r\n        this.value = this.get()\r\n        this.cb.call(this.vm, this.value, oldValue)\r\n    }\r\n}\r\n```\r\n\r\n这段代码可以把自己主动 `push` 到  `data.a.b.c` 的 Dep 中去。\r\n\r\n因为我在 `get` 这个方法中，先把 Dep.traget 设置成了 `this`，也就是当前watcher实例，然后在读一下 `data.a.b.c` 的值。\r\n\r\n因为读了 `data.a.b.c` 的值，所以肯定会触发 `getter`。\r\n\r\n触发了 `getter` 上面我们封装的 `defineReactive ` 函数中有一段逻辑就会从 `Dep.target` 里读一个依赖 `push` 到 `Dep` 中。\r\n\r\n所以就导致，我只要先在 Dep.target 赋一个 `this`，然后我在读一下值，去触发一下 `getter`，就可以把 `this` 主动 `push` 到 `keypath` 的依赖中，有没有很神奇~\r\n\r\n依赖注入到 `Dep` 中去之后，当这个 `data.a.b.c` 的值发生变化，就把所有的依赖循环触发 update 方法，也就是上面代码中 update 那个方法。\r\n\r\n`update` 方法会触发参数中的回调函数，将value 和 oldValue 传到参数中。\r\n\r\n所以其实不管是用户执行的 `vm.$watch('a.b.c', (value, oldValue) => {})` 还是模板中用到的data，都是通过 watcher 来通知自己是否需要发生变化的。\r\n\r\n## 递归侦测所有key\r\n现在其实已经可以实现变化侦测的功能了，但是我们之前写的代码只能侦测数据中的一个 key，所以我们要加工一下 `defineReactive` 这个函数：\r\n\r\n```javascript\r\n// 新增\r\nfunction walk (obj: Object) {\r\n  const keys = Object.keys(obj)\r\n  for (let i = 0; i < keys.length; i++) {\r\n    defineReactive(obj, keys[i], obj[keys[i]])\r\n  }\r\n}\r\n\r\nfunction defineReactive (data, key, val) {\r\n    walk(val) // 新增\r\n    let dep = new Dep()\r\n    Object.defineProperty(data, key, {\r\n        enumerable: true,\r\n        configurable: true,\r\n        get: function () {\r\n            dep.depend()\r\n            return val\r\n        },\r\n        set: function (newVal) {\r\n            if(val === newVal){\r\n                return\r\n            }\r\n\r\n            dep.notify()\r\n            val = newVal\r\n        }\r\n    })\r\n}\r\n```\r\n\r\n这样我们就可以通过执行 `walk(data)`，把 `data` 中的所有 `key` 都加工成可以被侦测的，因为是一个递归的过程，所以 `key` 中的 `value` 如果是一个对象，那这个对象的所有key也会被侦测。\r\n\r\n## Array怎么进行变化侦测？\r\n现在又发现了新的问题，`data` 中不是所有的 `value` 都是对象和基本类型，如果是一个数组怎么办？？数组是没有办法通过 `Object.defineProperty` 来侦测到行为的。\r\n\r\nvue 中对这个数组问题的解决方案非常的简单粗暴，我说说vue是如何实现的，大体上分三步：\r\n\r\n第一步：先把原生 `Array` 的原型方法继承下来。\r\n\r\n第二步：对继承后的对象使用 `Object.defineProperty` 做一些拦截操作。\r\n\r\n第三步：把加工后可以被拦截的原型，赋值到需要被拦截的 `Array` 类型的数据的原型上。\r\n\r\n**vue的实现**\r\n\r\n第一步：\r\n\r\n```javascript\r\nconst arrayProto = Array.prototype\r\nexport const arrayMethods = Object.create(arrayProto)\r\n```\r\n\r\n第二步：\r\n\r\n```javascript\r\n;[\r\n  'push',\r\n  'pop',\r\n  'shift',\r\n  'unshift',\r\n  'splice',\r\n  'sort',\r\n  'reverse'\r\n]\r\n.forEach(function (method) {\r\n  // cache original method\r\n  const original = arrayProto[method]\r\n\r\n  Object.defineProperty(arrayMethods, method, {\r\n    value: function mutator (...args) {\r\n      console.log(method) // 打印数组方法\r\n      return original.apply(this, args)\r\n    },\r\n    enumerable: false,\r\n    writable: true,\r\n    configurable: true\r\n  })\r\n})\r\n```\r\n\r\n现在可以看到，每当被侦测的 `array` 执行方法操作数组时，我都可以知道他执行的方法是什么，并且打印到 `console` 中。\r\n\r\n现在我要对这个数组方法类型进行判断，如果操作数组的方法是 push unshift splice （这种可以新增数组元素的方法），需要把新增的元素用上面封装的 `walk` 来进行变化检测。\r\n\r\n并且不论操作数组的是什么方法，我都要触发消息，通知依赖列表中的依赖数据发生了变化。\r\n\r\n那现在怎么访问依赖列表呢，可能我们需要把上面封装的 `walk` 加工一下：\r\n\r\n```javascript\r\n// 工具函数\r\nfunction def (obj: Object, key: string, val: any, enumerable?: boolean) {\r\n  Object.defineProperty(obj, key, {\r\n    value: val,\r\n    enumerable: !!enumerable,\r\n    writable: true,\r\n    configurable: true\r\n  })\r\n}\r\n\r\nexport class Observer {\r\n  value: any;\r\n  dep: Dep;\r\n  vmCount: number; // number of vms that has this object as root $data\r\n\r\n  constructor (value: any) {\r\n    this.value = value\r\n    this.dep = new Dep() // 新增\r\n    this.vmCount = 0\r\n    def(value, '__ob__', this) // 新增\r\n\r\n    // 新增\r\n    if (Array.isArray(value)) {\r\n      this.observeArray(value)\r\n    } else {\r\n      this.walk(value)\r\n    }\r\n  }\r\n\r\n  /**\r\n   * Walk through each property and convert them into\r\n   * getter/setters. This method should only be called when\r\n   * value type is Object.\r\n   */\r\n  walk (obj: Object) {\r\n    const keys = Object.keys(obj)\r\n    for (let i = 0; i < keys.length; i++) {\r\n      defineReactive(obj, keys[i], obj[keys[i]])\r\n    }\r\n  }\r\n\r\n  /**\r\n   * Observe a list of Array items.\r\n   */\r\n  observeArray (items: Array<any>) {\r\n    for (let i = 0, l = items.length; i < l; i++) {\r\n      new Observer(items[i])\r\n    }\r\n  }\r\n}\r\n```\r\n\r\n我们定义了一个 `Observer` 类，他的职责是将 `data` 转换成可以被侦测到变化的 `data`，并且新增了对类型的判断，如果是 `value` 的类型是 `Array` 循环 Array将每一个元素丢到 Observer 中。\r\n\r\n并且在 value 上做了一个标记 `__ob__`，这样我们就可以通过 `value` 的 `__ob__` 拿到Observer实例，然后使用 `__ob__` 上的 `dep.notify()` 就可以发送通知啦。\r\n\r\n然后我们在改进一下Array原型的拦截器：\r\n\r\n```javascript\r\n;[\r\n  'push',\r\n  'pop',\r\n  'shift',\r\n  'unshift',\r\n  'splice',\r\n  'sort',\r\n  'reverse'\r\n]\r\n.forEach(function (method) {\r\n  // cache original method\r\n  const original = arrayProto[method]\r\n  def(arrayMethods, method, function mutator (...args) {\r\n    const result = original.apply(this, args)\r\n    const ob = this.__ob__\r\n    let inserted\r\n    switch (method) {\r\n      case 'push':\r\n      case 'unshift':\r\n        inserted = args\r\n        break\r\n      case 'splice':\r\n        inserted = args.slice(2)\r\n        break\r\n    }\r\n    if (inserted) ob.observeArray(inserted)\r\n    // notify change\r\n    ob.dep.notify()\r\n    return result\r\n  })\r\n})\r\n```\r\n\r\n可以看到写了一个 `switch` 对 `method` 进行判断，如果是 `push`，`unshift`，`splice` 这种可以新增数组元素的方法就使用 `ob.observeArray(inserted)` 把新增的元素也丢到 `Observer` 中去转换成可以被侦测到变化的数据。\r\n\r\n在最后不论操作数组的方法是什么，都会调用 `ob.dep.notify()` 去通知 `watcher` 数据发生了改变。\r\n\r\n## arrayMethods 是怎么生效的？\r\n\r\n现在我们有一个 `arrayMenthods` 是被加工后的 `Array.prototype`，那么怎么让这个对象应用到`Array` 上面呢？\r\n\r\n思考一下，我们不能直接修改 `Array.prototype`因为这样会污染全局的Array，我们希望 `arrayMenthods` 只对 `data`中的`Array` 生效。\r\n\r\n所以我们只需要把 `arrayMenthods` 赋值给 `value` 的 `__proto__` 上就好了。\r\n\r\n我们改造一下 `Observer`：\r\n\r\n```javascript\r\nexport class Observer {\r\n  constructor (value: any) {\r\n    this.value = value\r\n    this.dep = new Dep()\r\n    this.vmCount = 0\r\n    def(value, '__ob__', this)\r\n\r\n    if (Array.isArray(value)) {\r\n      value.__proto__ = arrayMethods // 新增\r\n      this.observeArray(value)\r\n    } else {\r\n      this.walk(value)\r\n    }\r\n  }\r\n}\r\n```\r\n\r\n如果不能使用 `__proto__`，就直接循环 `arrayMethods ` 把它身上的这些方法直接装到 `value` 身上好了。\r\n\r\n**什么情况不能使用 `__proto__` 我也不知道，各位大佬谁知道能否给我留个言？跪谢~** \r\n\r\n所以我们的代码又要改造一下：\r\n\r\n```javascript\r\n// can we use __proto__?\r\nconst hasProto = '__proto__' in {} // 新增\r\nexport class Observer {\r\n  constructor (value: any) {\r\n    this.value = value\r\n    this.dep = new Dep()\r\n    this.vmCount = 0\r\n    def(value, '__ob__', this)\r\n\r\n    if (Array.isArray(value)) {\r\n      // 修改\r\n      const augment = hasProto\r\n        ? protoAugment\r\n        : copyAugment\r\n      augment(value, arrayMethods, arrayKeys)\r\n      this.observeArray(value)\r\n    } else {\r\n      this.walk(value)\r\n    }\r\n  }\r\n}\r\n\r\nfunction protoAugment (target, src: Object, keys: any) {\r\n  target.__proto__ = src\r\n}\r\n\r\nfunction copyAugment (target: Object, src: Object, keys: Array<string>) {\r\n  for (let i = 0, l = keys.length; i < l; i++) {\r\n    const key = keys[i]\r\n    def(target, key, src[key])\r\n  }\r\n}\r\n```\r\n\r\n## 关于Array的问题\r\n关于vue对Array的拦截实现上面刚说完，正因为这种实现方式，其实有些数组操作vue是拦截不到的，例如：\r\n\r\n```javascript\r\nthis.list[0] = 2\r\n```\r\n\r\n修改数组第一个元素的值，无法侦测到数组的变化，所以并不会触发 `re-render` 或 `watch` 等。\r\n\r\n在例如：\r\n\r\n```javascript\r\nthis.list.length = 0\r\n```\r\n\r\n清空数组操作，无法侦测到数组的变化，所以也不会触发 `re-render` 或 `watch` 等。\r\n\r\n因为vue的实现方式就决定了无法对上面举得两个例子做拦截，也就没有办法做到响应，ES6是有能力做到的，在ES6之前是无法做到模拟数组的原生行为的，现在 ES6 的 Proxy 可以模拟数组的原生行为，也可以通过 ES6 的继承来继承数组原生行为，从而进行拦截。\r\n\r\n## 总结\r\n\r\n![响应式结构图](https://raw.githubusercontent.com/vuejs/vuejs.org/master/src/images/data.png)\r\n\r\n最后掏出vue官网上的一张图，这张图其实非常清晰，就是一个变化侦测的原理图。\r\n\r\n`getter` 到 `watcher` 有一条线，上面写着收集依赖，意思是说 `getter` 里收集 `watcher`，也就是说当数据发生 `get`  动作时开始收集 `watcher`。\r\n\r\n`setter` 到 `watcher` 有一条线，写着 `Notify` 意思是说在 `setter` 中触发消息，也就是当数据发生 `set` 动作时，通知 `watcher`。\r\n\r\n`Watcher` 到 ComponentRenderFunction 有一条线，写着 `Trigger re-render` 意思很明显了。\r\n\r\n![image](https://user-images.githubusercontent.com/3739368/33662223-0834f8c2-dac6-11e7-8c1d-0610fb61b1f3.png)\r\n"},"comment":{"url":"https://api.github.com/repos/berwin/Blog/issues/comments/396149603","html_url":"https://github.com/berwin/Blog/issues/17#issuecomment-396149603","issue_url":"https://api.github.com/repos/berwin/Blog/issues/17","id":396149603,"node_id":"MDEyOklzc3VlQ29tbWVudDM5NjE0OTYwMw==","user":{"login":"berwin","id":3739368,"node_id":"MDQ6VXNlcjM3MzkzNjg=","avatar_url":"https://avatars0.githubusercontent.com/u/3739368?v=4","gravatar_id":"","url":"https://api.github.com/users/berwin","html_url":"https://github.com/berwin","followers_url":"https://api.github.com/users/berwin/followers","following_url":"https://api.github.com/users/berwin/following{/other_user}","gists_url":"https://api.github.com/users/berwin/gists{/gist_id}","starred_url":"https://api.github.com/users/berwin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/berwin/subscriptions","organizations_url":"https://api.github.com/users/berwin/orgs","repos_url":"https://api.github.com/users/berwin/repos","events_url":"https://api.github.com/users/berwin/events{/privacy}","received_events_url":"https://api.github.com/users/berwin/received_events","type":"User","site_admin":false},"created_at":"2018-06-11T07:34:37Z","updated_at":"2018-06-11T07:34:37Z","author_association":"OWNER","body":"@wcc233 哈哈哈，不冲突的、变化侦测和更新视图是两个系统。变化侦测只是侦测数据是否有变动。而具体如何更新DOM是另一个系统在操作~ 文档里写的意思是当变化侦测发现到数据有变化后将通知发出去，但更新系统不会立刻更新，而是在下一个tick再更新~\r\n\r\n为什么放到下一个tick更新，可以看下这个文章：https://github.com/berwin/Blog/issues/22"}}
{"actor":{"display_login":"berwin"}}