{"id":13672686,"url":"https://github.com/FuuuOverclocking/learn-vue-observer","last_synced_at":"2025-04-27T22:32:41.613Z","repository":{"id":123406376,"uuid":"141999655","full_name":"FuuuOverclocking/learn-vue-observer","owner":"FuuuOverclocking","description":"介绍 Vue 响应式原理的实现过程","archived":false,"fork":false,"pushed_at":"2018-07-23T14:41:49.000Z","size":39,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-11-11T10:42:42.521Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/FuuuOverclocking.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-07-23T10:32:53.000Z","updated_at":"2023-09-09T11:53:01.000Z","dependencies_parsed_at":"2024-01-17T04:18:28.007Z","dependency_job_id":null,"html_url":"https://github.com/FuuuOverclocking/learn-vue-observer","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FuuuOverclocking%2Flearn-vue-observer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FuuuOverclocking%2Flearn-vue-observer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FuuuOverclocking%2Flearn-vue-observer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FuuuOverclocking%2Flearn-vue-observer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/FuuuOverclocking","download_url":"https://codeload.github.com/FuuuOverclocking/learn-vue-observer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251219600,"owners_count":21554444,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T09:01:44.439Z","updated_at":"2025-04-27T22:32:36.604Z","avatar_url":"https://github.com/FuuuOverclocking.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"笔者日前学习了 Vue 的 Observer 部分，简单地谷歌了一下，因为没有找到解释地十分彻底的中文资源，记下自己对其的理解并分享。\n\n转载需注明出处 https://segmentfault.com/a/1190000015709022 ，有帮助请点赞。\n\n本文引用的 Vue 版本为 v2.5.17-beta.0 。\n不过 Vue 的 Observer 部分自2017年以来至今没什么大变化，v2.5.16 到 v2.5.17-beta.0 对 Observer 有个小小的 bugfix。\n\n## 内容\n\n本文介绍 Vue 响应式原理的实现过程，并试图以之为参照改造出一个便于移植的库。这里笔者把 Vue 的 observer 部分提出来独立地讲，读者不需要对 Vue 其他部分十分熟悉。\n\nVue 的响应式模型十分完善，实现地足够巧妙，私以为有学习的必要。本文准备从写一个简单的模型出发，一步步填充功能，演化成 Vue 源码的形态，所以文章看起来似乎巨长，但代码多有重复；我认为这样写，读者看起来会比较轻松，所以请不必长文恐惧。卢瑟福说，“只有你能将一个理论讲得连女仆都懂了，你才算真正懂了”。虽然读者可能不是女仆(?)，我也会写得尽量明白的。\n\n本文对 Observer 介绍地很完全，对象和数组的不同处理，[deep watching][7]，以及异步队列都会讲解。当然，也不会完全整成源码那么麻烦，一些只和 Vue 有关的代码删除了，此外[计算属性(computed property)][6]的部分只说明原理，省略了实现。\n\n但一般的 JS 技巧，[ECMAScript 6][1]，闭包的知识，[Object.defineProperty][2] 的知识还是需要具备的。\n\nVue 源码是用 Flow 写的，本文改成 TypeScript 了(同为类型注解，毕竟后者更流行)，未学习过的同学只要把文中不像 JS 的部分去掉，当 JS 就行了。\n\nJS 中数组是对象的一种，因为 Observer 部分对数组与普通对象的对待区别很大，所以下文说到对象，都是指 constructor 为 Object 的普通对象。\n\n## 准备\n\n可以先`git clone git@github.com:vuejs/vue.git`一份源码备看。observer 的部分在源码的 src/core/observer 目录下。\n\n本文代码已经放在 https://github.com/xyzingh/learn-vue-observer ，运行 `npm i \u0026\u0026 npm run test` 可以测试。\n\n新建文件夹 learn-vue-observer，创建几个文件。\n\nutil.ts\n```js\n/* 一些常用函数的简写 */\n\nexport function def(obj: any, key: string, value: any, enumerable: boolean = false) {\n  Object.defineProperty(obj, key, {\n    value,\n    enumerable,\n    writable: true,\n    configurable: true,\n  });\n}\n\nexport function isObject(obj: any) {\n  return obj !== null \u0026\u0026 typeof obj === \"object\";\n}\n\nexport function hasOwn(obj: any, key: string): boolean {\n  return Object.prototype.hasOwnProperty.call(obj, key);\n}\n\nexport function isPlainObject(obj: any): boolean {\n  return Object.prototype.toString.call(obj) === \"[object Object]\";\n}\n\nexport function isNative(ctor: any): boolean {\n  return typeof ctor === \"function\" \u0026\u0026 /native code/.test(ctor.toString());\n}\n\nexport function remove(arr: any[], item: any): any[] | void {\n  if (arr.length) {\n    const index = arr.indexOf(item);\n    if (index \u003e -1) {\n      return arr.splice(index, 1);\n    }\n  }\n}\n```\n\n## 版本 0.1\n\n假设我们要把下面这个对象转变成响应式的。\n\n```js\nlet obj = {\n  a: {\n    aa: {\n      aaa: 123,\n      bbb: 456,\n    },\n    bb: \"obj.a.bb\",\n  },\n  b: \"obj.b\",\n};\n```\n\n怎样算作是响应式的呢？如果将 obj 的任意键的值改变，都能执行一个相应的函数进行相关操作（比如更新DOM），那么就算得上响应式了。为此，我们势必为 obj 的每个键创建代理，使对 obj 的直接操作变成透过代理操作。代理的方式有许多，[Object.observe][3]，[Proxy][4]，getter/setter。但 Object.observe 已经被废弃，Proxy 巨硬家从 Edge 才开始支持，IE 全灭，所以可行的只有 getter/setter （IE9 开始支持）。然而 getter/setter 依然有很大的局限性，即只能转化已有属性，因此需要为用户提供特别的函数来设置新属性，这个函数我们最后再提。\n\nobj 的值都转成 getter/setter 了，真实值存在哪呢？Vue 的做法是藏在闭包里。\n\n下面我们定义3个函数/类，尝试递归地设置 obj 的 getter/setter。\n\nindex.ts\n```js\nimport { def, hasOwn, isObject, isPlainObject } from \"./util\";\n\n/**\n * 尝试对 value 创建 Observer 实例，\n * value 如果不是对象或数组，什么都不做。\n * @param value 需要尝试监视的目标\n */\nexport function observe(value: any) {\n  if (!isObject(value)) {\n    return;\n  }\n\n  let ob: Observer | void;\n  if (typeof value.__ob__ !== \"undefined\") {\n    ob = value.__ob__;\n  } else {\n    ob = new Observer(value);\n  }\n  return ob;\n}\n\nexport class Observer {\n  constructor(value: any) {\n    def(value, \"__ob__\", this);\n    this.walk(value);\n  }\n  public walk(value: any) {\n    for (const key of Object.keys(value)) {\n      defineReactive(value, key);\n    }\n  }\n}\n\nfunction defineReactive(obj: any, key: string, val?: any) {\n  // 闭包中的 val 藏着 obj[key] 的真实值\n  if (arguments.length === 2) {\n    val = obj[key];\n  }\n\n  let childOb = observe(val); // val 如果不是对象的话，是返回 undefined 的。\n  Object.defineProperty(obj, key, {\n    enumerable: true,\n    configurable: true,\n    get() {\n      ////////////////\n      console.log(\"you get \" + val);\n      ////////////////\n      return val;\n    },\n    set(newVal) {\n      if (newVal === val) {\n        return;\n      }\n      ////////////////\n      console.log(\"you set \" + newVal);\n      ////////////////\n      val = newVal;\n      childOb = observe(newVal);\n    }\n  });\n}\n```\n\n我们可以试一下\n\n```js\nobserve(obj);\nconsole.log(obj.a.aa.aaa = 234);\n```\n\n输出应为\n\n```\nyou get [object Object]\nyou get [object Object]\nyou set 234\n234\n```\n\n但是，有个问题，我们不应假设 obj 的每个键就是简单的值，万一本来就是 getter/setter 呢？\n\n```js\nlet obj2 = {};\nObject.defineProperty(obj2, \"a\", {\n    configurable: true,\n    enumerable: true,\n    get() {\n        return obj2._a;\n    },\n    set(val) {\n        obj2._a = val;\n    },\n});\nObject.defineProperty(obj2, \"_a\", {\n    enumerable: false,\n    value: 123,\n    writable: true,\n});\n```\n\n因此，需要修改 defineReactive ，继续用闭包保存 getter/setter 。\n\n```js\nfunction defineReactive(obj: any, key: string, val?: any) {\n  const property = Object.getOwnPropertyDescriptor(obj, key);\n  if (property \u0026\u0026 property.configurable === false) {\n    return;\n  }\n\n  const getter = property!.get; // property! 的叹号是 TypeScript 语法，忽略即可\n  const setter = property!.set;\n\n  // 为什么写成 (!getter || setter) ？后面会讨论。\n  if ((!getter || setter) \u0026\u0026 arguments.length === 2) {\n    val = obj[key];\n  }\n\n  let childOb = observe(val);\n  Object.defineProperty(obj, key, {\n    enumerable: true,\n    configurable: true,\n    get() {\n      const value = getter ? getter.call(obj) : val;\n      ////////////////\n      console.log(\"you get \" + value);\n      ////////////////\n      return value;\n    },\n    set(newVal) {\n      const value = getter ? getter.call(obj) : val;\n      if (newVal === value) {\n        return;\n      }\n      ////////////////\n      console.log(\"you set \" + newVal);\n      ////////////////\n      if (setter) {\n        setter.call(obj, newVal);\n      } else {\n        val = newVal;\n      }\n      childOb = observe(newVal);\n    },\n  });\n}\n```\n\n这样就可以成功地把 obj2 转变成响应式的。\n\n笔者在理解 ```if ((!getter || setter) \u0026\u0026 arguments.length === 2)``` 时遇到过障碍，这其实是讲：\n\n  1. 如果 arguments 长为3，参数 val 存在，就认为是显式地设置了这个键的值，原来的值就不考虑了\n  2. 如果 getter setter 都存在，就认为这对 getter/setter 是在代理某个真实值，所以需要 val = obj[key]，然后 let childOb = observe(val) 对这个真实值继续进行递归设置\n  3. 否则 如果 getter 存在，setter 不存在，认为 getter 大概只是返回某个生成的值，不执行 val = obj[key]，也就导致下面 let childOb = observe(undefined)\n  4. getter 不存在，setter 存在，这类奇葩事情不在考虑范围内（例如 document.cookie）\n\n这是 v2.5.17-beta.0 的一个 bugfix ，有关的讨论原文来自↓\n[issue/7280](https://github.com/vuejs/vue/issues/7280)\n[issue/7302](https://github.com/vuejs/vue/issues/7302)\n[pull/7981](https://github.com/vuejs/vue/pull/7981)\n[issue/8494](https://github.com/vuejs/vue/issues/8494)\n\n## 版本 0.2: 加上对数组的支持\n\n并不是说之前的版本不支持数组，而是一般开发者使用数组与使用对象的方法有区别。数组在 JS 中常被当作栈，队列，集合等数据结构的实现方式，会储存批量的数据以待遍历。编译器对对象与数组的优化也有所不同。所以对数组的处理需要特化出来以提高性能。\n\n首先，不能再对数组每个键设置 getter/setter 了，而是修改覆盖数组的 push, pop, ... 等方法。[用户要修改数组只能使用这些方法，否则不会是响应式的][5]（除了 Vue.set, Vue.delete）。\n\n因此，准备一个数组方法的替代品。哪些方法应当替代掉？那些不会干涉原数组的方法不需要修改；删除数组元素的方法需要替代；增加或替换数组元素的方法需要替换，还要尝试把新的值变成响应式的。\n\narray.ts\n```js\nimport { def } from './util';\n\nconst arrayProto = Array.prototype as any;\n// 建立以 Array.prototype 为原型的 arrayMethods 并导出\nexport const arrayMethods = Object.create(arrayProto);\n\n// 会干涉原数组的方法\nconst methodsToPatch = [\n  'push',\n  'pop',\n  'shift',\n  'unshift',\n  'splice',\n  'sort',\n  'reverse',\n];\n\nmethodsToPatch.forEach((method: string) =\u003e {\n  // 原方法的缓存\n  const original = arrayProto[method];\n\n  // 在 arrayMethods 上定义替代方法\n  def(arrayMethods, method, function (this: any, ...args: any[]) {\n    const result = original.apply(this, args);\n    const ob = this.__ob__;\n\n    // 新增的元素\n    let inserted: any[] | void;\n\n    switch (method) {\n      // 会增加或替换元素的方法\n      case 'push':\n      case 'unshift':\n        inserted = args;\n        break;\n      case 'splice':\n        inserted = args.slice(2);\n        break;\n    }\n    if (inserted){\n      ob.observeArray(inserted); // Observer 上新增的方法\n    }\n    ///////////////////////////////\n    console.log(\"array is modified.\");\n    ///////////////////////////////\n    return result;\n  });\n});\n```\n\n然后修改 Observer，区别对待数组。\n\n```js\nexport class Observer {\n  constructor(value: any) {\n    def(value, \"__ob__\", this);\n    if (Array.isArray(value)) {\n      // 替换原型（Object.setPrototype 这个方法执行地比较慢，而且支持情况堪忧）\n      Object.setPrototypeOf(value, arrayMethods);\n      this.observeArray(value);\n    } else {\n      this.walk(value);\n    }\n  }\n  public walk(value: any) {\n    for (const key of Object.keys(value)) {\n      defineReactive(value, key);\n    }\n  }\n  public observeArray(items: any[]) {\n    // 设置 l = items.length 防止遍历过程中 items 长度变化\n    for (let i = 0, l = items.length; i \u003c l; i++) {\n      // 直接观察数组元素，省略在键上设置 getter/setter 的步骤\n      observe(items[i]);\n    }\n  }\n}\n```\n\n## 版本 1.0: 增加 Dep, Watcher\n\n### 从 API 出发思考写法\n\nvm.$watch( expressionOrFunction, callback [, options] ) 是 Vue 最基础的观察自身 data 的方式。我们参考这个函数，提出适用本文的一个函数:\n\n**watch( target, expression, callback )**\n\n观察 target 这个对象的表达式 exp 的值，一旦发生变化时执行 callback （同步地）。callback 的第一个参数为新的值，第二个参数为旧的值，this 为 target。\n\n例如 `watch(obj, \"a.aa.bbb\", val =\u003e console.log(val))` ，当 obj.a.aa.bbb 改变时，控制台会打印新的值。注意 obj 应该已经经过 observe(obj) 转化过了。\n\n之前版本我们在 getter/setter 处留下了\n\n/////////////////\nconsole.log(XXX)\n/////////////////\n\n只要把这些替换成相应代码，就能实现 watch 方法了。\n\n现在来定义一下哪些情况应执行 callback 。\n\n假设 obj.a.aa.bbb = 456 ，我们对这个键进行了 watch :\n1. obj.a.aa.bbb = 456 值没变，不需要\n2. obj.a.aa.bbb = 999 应执行 callback\n3. obj.a.aa = { bbb: 456 } 值没变，不执行 callback\n4. obj.a.aa = { bbb: 999 } 应执行 callback\n5. obj = {a:{aa:{bbb:999}}} 对象都被替换成新的了，想执行 callback 也不可能\n\n假设我们还对 obj.a.aa 进行了 watch :\n1. obj.a.aa.bbb = 999 虽然 obj.a.aa 发生了变异(mutation)，但 obj.a.aa 还是它自己，不执行 callback\n2. obj.a.aa = { bbb: 456 } 应执行 callback\n\n简而言之，如果 target 沿着 expression 解析到的值与之前的不全等，就认为需要执行 callback 。对于基础类型来说，就是值的不全等。对于普通对象，就是引用不相同。但数组比较特殊，对数组元素进行了操作，就应执行 callback 。\n\n怎么组织代码呢？Evan You (Vue 作者) 的方法比较巧妙。\n\n### Observer, Dep, Watcher\n\n创建两个新的类，Dep, Watcher 。Dep 是 Dependency 的简称，每个 Observer 的实例，成员中都有一个 Dep 的实例。\n\n这个 Dep 的实质是个数组，放置着监听这个 Observer 的 Watcher ，当这个 Observer 对应的值变化时，就通知 Dep 中的所有 Watcher 执行 callback 。\n\n```js\nexport class Observer {\n  constructor(value: any) {\n    this.dep = new Dep(); // 新增\n    def(value, \"__ob__\", this);\n    if (Array.isArray(value)) {\n// .........................\n```\n\nWatcher 是调用 watch 函数产生的，它保存着 callback 并且维护了一个数组，数组存放了所有 存有这个 Watcher 的 Dep 。这样当这个 Watcher 需要被删除时，可以遍历数组，从各个 Dep 中删去自身，也就是 unwatch 的过程。\n\nWatcher 何时被放入 Dep 中的先不谈。先说说 Dep 都在什么地方。\n\n以上说得并不全对，应该说，原始的 Dep 是创建在 defineReactive 的闭包中，Observer 的 dep 成员只是这个原始的 Dep 的备份，始终一起被维护，保持一致。另外，Observer 只会建立在对象或数组的 \\_\\_ob\\_\\_ 上，如果键的值不是对象或数组，只会有闭包中的 Dep 保存这个键的 Watcher 。\n\n```js\nfunction defineReactive(obj: any, key: string, val?: any) {\n  const dep = new Dep(); // 新增\n  const property = Object.getOwnPropertyDescriptor(obj, key);\n// ...........................\n```\n\n举例来说，\n\n```js\nlet obj = {\n  // obj.__ob__.dep: 保存 obj 的 dep\n\n  a: { // 闭包中有 obj.a 的 dep\n    // obj.a.__ob__.dep: 保存 obj.a 的 dep\n\n    aa: { // 闭包中有 obj.a.aa 的 dep\n      // obj.a.aa.__ob__.dep: 保存 obj.a.aa 的 dep\n\n      aaa: 123, // 闭包中有 obj.a.aa.aaa 的 dep\n      bbb: 456, // 闭包中有 obj.a.aa.bbb 的 dep\n    },\n    bb: \"obj.a.bb\", // 闭包中有 obj.a.bb 的 dep\n  },\n  b: \"obj.b\", // 闭包中有 obj.b 的 dep\n};\nobserve(obj);\n```\n\n数组特殊对待，不对数组的成员进行 defineReactive ，\n\n```js\nlet obj = {\n  arr: [ // 闭包中 obj.arr 的 dep\n    // obj.arr.__ob__.dep\n\n    2, // 没有 dep ，没有闭包\n    3,\n    5,\n    7,\n    11,\n    { // 没有闭包\n      // obj.arr[6].__ob__.dep 存在\n    },\n    [ // 没有闭包\n      // obj.arr[7].__ob__.dep 存在\n    ],\n  ],\n};\nobserve(obj);\n```\n\n复习一下，dep 实质是个数组，放着监听这个键的 Watcher 。\n\n当这个键的值被修改时，就应该通知相应 dep 的所有 Watcher ，我们在 Dep 上设置 notify 方法，用来实现这个功能。\n\n为此，修改 setter 的部分。\n\n```js\nfunction defineReactive(obj: any, key: string, val?: any) {\n  const dep = new Dep();\n  // .................................\n  Object.defineProperty(obj, key, {\n    enumerable: true,\n    configurable: true,\n    get() {\n      // .............................\n    },\n    set(newVal) {\n      // .............................\n      }\n      childOb = observe(newVal);\n      dep.notify();\n    },\n  });\n}\n```\n\n数组的部分，\n\narray.ts\n```js\n  // ................\n  def(arrayMethods, method, function (this: any, ...args: any[]) {\n    const result = original.apply(this, args);\n    const ob = this.__ob__;\n    // .....................\n    if (inserted){\n      ob.observeArray(inserted);\n    }\n    ob.dep.notify();\n    return result;\n  });\n```\n\n如此一来，每当修改值时，相应的 Watcher 都会被通知了。\n\n现在的问题是，何时怎么把 Watcher 放入 dep 中。下面我们先来尝试实现 Dep 。\n\ndep.ts\n```js\nimport { remove } from \"./util\";\nimport { Watcher } from \"./watcher\";\n\nlet uid = 0;\n\nexport default class Dep {\n  public id: number;\n  public subs: Watcher[];\n\n  constructor() {\n    this.id = uid++;\n    this.subs = [];\n  }\n\n  public addSub(sub: Watcher) {\n    this.subs.push(sub);\n  }\n\n  public removeSub(sub: Watcher) {\n    remove(this.subs, sub);\n  }\n\n  public notify() {\n    // 先复制一份，应对通知 Watcher 过程中，this.subs 可能变化的情况\n    const subs = this.subs.slice();\n    for (let i = 0, l = subs.length; i \u003c l; i++) {\n      // Watcher 上定义了 update 方法，用来被通知\n      subs[i].update();\n    }\n  }\n}\n```\n\n### （重点）用 Touch 的方法，收集依赖\n\n假设用 `watch(obj, \"a.aa.bbb\", val =\u003e console.log(val))` ，创建了一个 Watcher ，这个 Watcher 应被放进哪些 Dep 中呢？\n\n因为 `obj.a`, `obj.a.aa` 改变时，`obj.a.aa.bbb` 的值可能改变，所以答案是 `obj.a`, `obj.a.aa`, `obj.a.aa.bbb` 的闭包中的 Dep, 前两者是对象，所以在 \\_\\_ob\\_\\_.dep 中再放一份。\n\n因为在对表达式 `obj.a.aa.bbb` 求值时，会依次执行 `obj.a`, `(obj.a).aa`, `((obj.a).aa).bbb` 的 getter ，这也正好对应了应被放入 Watcher 的键，所以很自然的一个想法是，\n\n**规定一个全局变量，平常是 null ，当在决定某个 Watcher 该放入哪些 Dep 的时候（即 依赖收集 阶段），让这个全局变量指向这个 Watcher 。然后 touch 被监视的那个键，换言之，对那个键求值。途中会调用一连串的 getter ，往那些 getter 所对应的 Dep 里放入这个 Watcher 就对了。之后再将全局变量改回 null 。**\n\n这个做法的妙处，还在于它可以同时适用 [deep watching][7] 和 [计算属性(computed property)][6]。deep watching 后面会再说，对于计算属性，这使得用户直接写函数就行，无需显式说明这个计算属性所依赖的其他属性，十分优雅，因为在运算这个函数时，用到其他属性就会触发 getter ，可能的依赖都会被收集起来。\n\n我们来尝试实现，\n\n```js\nexport default class Dep {\n  // Dep.target 即前文所谓的全局变量\n  public static target: Watcher | null = null;\n\n  public id: number;\n  public subs: Watcher[];\n\n  public depend() {\n    if (Dep.target) {\n      this.addSub(Dep.target);\n    }\n  }\n// ...................................................\n```\n\n```js\nfunction defineReactive(obj: any, key: string, val?: any) {\n  const dep = new Dep();\n  // ...................................................\n  let childOb = observe(val);\n  Object.defineProperty(obj, key, {\n    enumerable: true,\n    configurable: true,\n    get() {\n      const value = getter ? getter.call(obj) : val;\n\n      // 如果处在依赖收集阶段\n      if (Dep.target) {\n        dep.depend();\n        if (childOb) {\n          childOb.dep.depend();\n        }\n      }\n\n      return value;\n    },\n  // ....................................................\n}\n```\n\n现在也该把一直谈论的 Watcher 给实现了。根据前面说的，它应该有个 update 方法。\n\nwatcher.ts\n```js\nimport Dep from \"./dep\";\n\nlet uid = 0;\n\nexport class Watcher {\n  public id: number;\n  public value: any;\n  public target: any;\n  public getter: (target: any) =\u003e any;\n  public callback: (newVal: any, oldVal: any) =\u003e void;\n\n  constructor(\n    target: any,\n    expression: string,\n    callback: (newVal: any, oldVal: any) =\u003e void,\n  ) {\n    this.id = uid++;\n    this.target = target;\n    this.getter = parsePath(expression);\n    this.callback = callback;\n    this.value = this.get();\n  }\n\n  public get() {\n    // 进入依赖收集阶段\n    Dep.target = this;\n\n    let value: any;\n    const obj = this.target;\n    try {\n      // 调用了一连串 getter ，对应的键的 dep 中放入了这个 watcher\n      value = this.getter(obj);\n    } finally {\n      // 退出依赖收集阶段\n      Dep.target = null;\n    }\n    return value;\n  }\n\n  public update() {\n    this.run();\n  }\n  public run() {\n    this.getAndInvoke(this.callback);\n  }\n  public getAndInvoke(cb: (newVal: any, oldVal: any) =\u003e void) {\n    const value = this.get();\n    if (value !== this.value || isObject(value) /* 监视目标为对象或数组的话，仍应执行回调，因为值可能变异了 */) {\n      const oldVal = this.value;\n      this.value = value;\n      cb.call(this.target, value, oldVal);\n    }\n  }\n}\n\nconst bailRE = /[^\\w.$]/;\nfunction parsePath(path: string): any {\n  if (bailRE.test(path)) {\n    return;\n  }\n  const segments = path.split(\".\");\n  return (obj: any) =\u003e {\n    for (const segment of segments) {\n      if (!obj) { return; }\n      obj = obj[segment];\n    }\n    return obj;\n  };\n}\n```\n\n## 版本 1.1: 特化数组的依赖收集\n\n```js\nfunction defineReactive(obj: any, key: string, val?: any) {\n// .....................................\n      if (Dep.target) {\n        dep.depend();\n        if (childOb) {\n          childOb.dep.depend();\n          if (Array.isArray(value)) {\n            dependArray(value);\n          }\n        }\n      }\n// ......................................\n}\n\nfunction dependArray(value: any[]) {\n  for (let e, i = 0, l = value.length; i \u003c l; i++) {\n    e = value[i];\n\n    // 若为多维数组，继续递归监视\n    e \u0026\u0026 e.__ob__ \u0026\u0026 e.__ob__.dep.depend();\n    if (Array.isArray(e)) {\n      dependArray(e);\n    }\n  }\n}\n```\n\n讨论的原文来自 [issue/3883][8] ，举例而言，\n\n```js\nlet obj = {\n  matrix: [\n    [2, 3, 5, 7, 11],\n    [13, 17, 19, 23, 29],\n  ],\n};\nobserve(obj);\nwatch(obj, \"matrix\", val =\u003e console.log(val));\nobj.matrix[0].push(1);\n// 导致 matrix[0].__ob__.dep.notify() ，由于递归监视，这个 dep 里也有上面的 Watcher\n```\n\n## 版本 1.2: 完善 Watcher 的生命周期\n\n只有 watch 没有 unwatch 自然是不合理的。[前面提到](https://segmentfault.com/a/1190000015709022#articleHeader6)，Watcher 也维护了一个数组 deps，存放所有 放了这个 Watcher 的 Dep ，当这个 Watcher 析构时，可以从这些 Dep 中删去自身。\n\n我们给 Watcher 增加 active, deps, depIds, newDeps, newDepIds 属性，addDep, cleanupDeps, teardown 方法，其中 teardown 方法起的是析构的作用，active 标志 Watcher 是否可用，其他的都是围绕着维护 deps 。\n\n```js\nexport class Watcher {\n  // ..............................\n  public active = true;\n  public deps: Dep[] = [];\n  public depIds = new Set\u003cnumber\u003e();\n  public newDeps: Dep[] = [];\n  public newDepIds = new Set\u003cnumber\u003e();\n\n  public run() {\n    if (this.active) {\n      this.getAndInvoke(this.callback);\n    }\n  }\n\n  // newDeps 是新一轮收集的依赖，deps 是之前一轮收集的依赖\n  public addDep(dep: Dep) {\n    const id = dep.id;\n    if (!this.newDepIds.has(id)) {\n      this.newDepIds.add(id);\n      this.newDeps.push(dep);\n      if (!this.depIds.has(id)) {\n        dep.addSub(this);\n      }\n    }\n  }\n\n  public get() {\n    Dep.target = this;\n\n    let value: any;\n    const obj = this.target;\n    try {\n      value = this.getter(obj);\n    } finally {\n      Dep.target = null;\n      this.cleanupDeps();\n    }\n    return value;\n  }\n\n  // 清理依赖\n  // 之前收集的依赖 如果不出现在新一轮收集的依赖中，就清除掉\n  // 再交换 deps/newDeps, depIds/newDepIds\n  public cleanupDeps() {\n    let i = this.deps.length;\n    while (i--) {\n      const dep = this.deps[i];\n      if (!this.newDepIds.has(dep.id)) {\n        dep.removeSub(this);\n      }\n    }\n    const tmpIds = this.depIds;\n    this.depIds = this.newDepIds;\n    this.newDepIds = tmpIds;\n    this.newDepIds.clear();\n\n    const tmp = this.deps;\n    this.deps = this.newDeps;\n    this.newDeps = tmp;\n    this.newDeps.length = 0;\n  }\n\n  public teardown() {\n    if (this.active) {\n      let i = this.deps.length;\n      while (i--) {\n        this.deps[i].removeSub(this);\n      }\n      this.active = false;\n    }\n  }\n}\n```\n\n修改之前的 dep.ts\n\n```js\nexport default class Dep {\n  public depend() {\n    if (Dep.target) {\n      // this.addSub(Dep.target);\n      Dep.target.addDep(this);\n    }\n  }\n}\n```\n\n## 版本 2.0: deep watching\n\nDeep watching 的原理很简单，就是在用 touch 收集依赖的基础上，递归遍历并 touch 所有子元素，如此一来，所有子元素都被收集到依赖中。其中只有防止对象引用成环需要稍微注意一下，这个用一个集合记录遍历到的元素来解决。\n\n我们给 Watcher 构造函数增加一个 deep 选项。\n\n直接贴代码，\n\n```js\nexport class Watcher {\n  public deep: boolean;\n  constructor(\n    target: any,\n    expression: string,\n    callback: (newVal: any, oldVal: any) =\u003e void,\n    {\n      deep = false,\n    },\n  ) {\n    this.deep = deep;\n    // ................................\n  }\n  public get() {\n    Dep.target = this;\n\n    let value: any;\n    const obj = this.target;\n    try {\n      value = this.getter(obj);\n    } finally {\n      if (this.deep) {\n        // touch 所有子元素，收集到依赖中\n        traverse(value);\n      }\n      Dep.target = null;\n      this.cleanupDeps();\n    }\n    return value;\n  }\n\n  public getAndInvoke(cb: (newVal: any, oldVal: any) =\u003e void) {\n    const value = this.get();\n    if (value !== this.value ||\n      isObject(value) ||\n      this.deep /* deep watcher 始终执行 */\n    ) {\n      const oldVal = this.value;\n      this.value = value;\n      cb.call(this.target, value, oldVal);\n    }\n  }\n}\n```\n\ntraverse.ts\n```js\nimport { isObject } from \"./util\";\n\nconst seenObjects = new Set();\n\nexport function traverse(val: any) {\n  _traverse(val, seenObjects);\n  seenObjects.clear();\n}\n\nfunction _traverse(val: any, seen: Set\u003cany\u003e) {\n  let i;\n  let keys;\n  const isA = Array.isArray(val);\n  if ((!isA \u0026\u0026 !isObject(val)) || Object.isFrozen(val)) {\n    return;\n  }\n  if (val.__ob__) {\n    const depId = val.__ob__.dep.id;\n    if (seen.has(depId)) {\n      return;\n    }\n    seen.add(depId);\n  }\n  if (isA) {\n    i = val.length;\n    while (i--) { _traverse(val[i] /* touch */, seen); }\n  } else {\n    keys = Object.keys(val);\n    i = keys.length;\n    while (i--) { _traverse(val[keys[i]] /* touch */, seen); }\n  }\n}\n```\n\n## 版本 2.1: 异步 Watcher, 异步队列\n\n使用异步 Watcher 可以缓冲在同一次事件循环中发生的所有数据改变。如果在本次执行栈中同一个 Watcher 被多次触发，只会被推入到队列中一次。这样在缓冲时去除重复数据，能够避免不必要的计算，提高性能。\n\nVue 源码中的异步队列模型比下文中的复杂，因为 Vue 要保证\n\n1. 从父组件到子组件的更新顺序\n2. 用户定义的 watcher 在 负责渲染的 watcher 之前运行\n3. 若在父组件的 watcher 运行时摧毁了子组件，子组件的 watcher 应被跳过\n4. 被计算属性依赖的另一计算属性先运行\n\n如果这些是你的兴趣，请直接转战源码 src/core/observer/scheduler.js 。\n\n现在修改 watcher.ts ，\n\n```js\nexport class Watcher {\n  public deep: boolean;\n  public sync: boolean;\n  constructor(\n    target: any,\n    expression: string,\n    callback: (newVal: any, oldVal: any) =\u003e void,\n    {\n      deep = false,\n      sync = false, // 增加同步选项\n    },\n  ) {\n    this.deep = deep;\n    this.sync = sync;\n    // ............................\n  }\n  public update() {\n    if (this.sync) {\n      this.run();\n    } else {\n      queueWatcher(this); // 推入队列\n    }\n  }\n}\n```\n\n创建 scheduler.ts\n```js\n/// \u003creference path=\"next-tick.d.ts\" /\u003e\nimport { nextTick } from \"./next-tick\";\nimport { Watcher } from \"./watcher\";\n\nconst queue: Watcher[] = [];\nlet has: { [key: number]: true | null } = {};\nlet waiting = false;\nlet flushing = false;\nlet index = 0;\n\n/**\n * 重置 scheduler 的状态.\n */\nfunction resetSchedulerState() {\n  index = queue.length = 0;\n  has = {};\n  waiting = flushing = false;\n}\n\n/**\n * 刷新队列，并运行 watcher\n */\nfunction flushSchedulerQueue() {\n  flushing = true;\n  let watcher;\n  let id;\n\n  queue.sort((a, b) =\u003e a.id - b.id);\n\n  \n  for (index = 0; index \u003c queue.length /* 不缓存队列长度，因为新的 watcher 可能在执行队列时加进来 */; index++) {\n    watcher = queue[index];\n    id = watcher.id;\n    has[id] = null;\n    watcher.run();\n  }\n\n  resetSchedulerState();\n}\n\n/**\n * 将一个 watcher 推入队列\n * 相同 ID 的 watcher 会被跳过\n * 除非队列中之前的相同ID的 watcher 已被处理掉\n */\nexport function queueWatcher(watcher: Watcher) {\n  const id = watcher.id;\n  if (has[id] == null) {\n    has[id] = true;\n    if (!flushing) {\n      queue.push(watcher);\n    } else {\n      let i = queue.length - 1;\n\n      // 放到队列中相应 ID 的位置\n      while (i \u003e index \u0026\u0026 queue[i].id \u003e watcher.id) {\n        i--;\n      }\n      queue.splice(i + 1, 0, watcher);\n    }\n    if (!waiting) {\n      waiting = true;\n      \n      // 放入微任务队列\n      nextTick(flushSchedulerQueue);\n    }\n  }\n}\n```\n\n如果不清楚微任务队列是什么，可以阅读下 [理解浏览器和node.js中的Event loop事件循环][9] 。\n\n下面贴一下 Vue 的 nextTick 实现。\nnext-tick.d.ts\n```js\n// 自己给 next-tick 写了下接口\nexport declare function nextTick(cb: () =\u003e void, ctx?: any): Promise\u003cany\u003e | void;\n```\n\nnext-tick.js (注意这是 JS)\n```js\nimport { isNative } from \"./util\";\n\nconst inBrowser = typeof window !== \"undefined\";\nconst inWeex = typeof WXEnvironment == \"undefined\" \u0026\u0026 !!WXEnvironment.platform;\nconst weexPlatform = inWeex \u0026\u0026 WXEnvironment.platform.toLowerCase();\nconst UA = inBrowser \u0026\u0026 window.navigator.userAgent.toLowerCase();\nconst isIOS = (UA \u0026\u0026 /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === \"ios\");\n\nfunction noop() {}\nfunction handleError() {}\n\nconst callbacks = [];\nlet pending = false;\n\nfunction flushCallbacks() {\n  pending = false;\n  const copies = callbacks.slice(0);\n  callbacks.length = 0;\n  for (let i = 0; i \u003c copies.length; i++) {\n    copies[i]();\n  }\n}\n\n// Here we have async deferring wrappers using both microtasks and (macro) tasks.\n// In \u003c 2.4 we used microtasks everywhere, but there are some scenarios where\n// microtasks have too high a priority and fire in between supposedly\n// sequential events (e.g. #4521, #6690) or even between bubbling of the same\n// event (#6566). However, using (macro) tasks everywhere also has subtle problems\n// when state is changed right before repaint (e.g. #6813, out-in transitions).\n// Here we use microtask by default, but expose a way to force (macro) task when\n// needed (e.g. in event handlers attached by v-on).\nlet microTimerFunc;\nlet macroTimerFunc;\nlet useMacroTask = false;\n\n// Determine (macro) task defer implementation.\n// Technically setImmediate should be the ideal choice, but it's only available\n// in IE. The only polyfill that consistently queues the callback after all DOM\n// events triggered in the same loop is by using MessageChannel.\n/* istanbul ignore if */\nif (typeof setImmediate !== \"undefined\" \u0026\u0026 isNative(setImmediate)) {\n  macroTimerFunc = () =\u003e {\n    setImmediate(flushCallbacks);\n  };\n} else if (typeof MessageChannel !== \"undefined\" \u0026\u0026 (\n  isNative(MessageChannel) ||\n  // PhantomJS\n  MessageChannel.toString() === \"[object MessageChannelConstructor]\"\n)) {\n  const channel = new MessageChannel();\n  const port = channel.port2;\n  channel.port1.onmessage = flushCallbacks;\n  macroTimerFunc = () =\u003e {\n    port.postMessage(1);\n  };\n} else {\n  /* istanbul ignore next */\n  macroTimerFunc = () =\u003e {\n    setTimeout(flushCallbacks, 0);\n  };\n}\n\n// Determine microtask defer implementation.\n/* istanbul ignore next, $flow-disable-line */\nif (typeof Promise !== \"undefined\" \u0026\u0026 isNative(Promise)) {\n  const p = Promise.resolve();\n  microTimerFunc = () =\u003e {\n    p.then(flushCallbacks);\n    // in problematic UIWebViews, Promise.then doesn't completely break, but\n    // it can get stuck in a weird state where callbacks are pushed into the\n    // microtask queue but the queue isn't being flushed, until the browser\n    // needs to do some other work, e.g. handle a timer. Therefore we can\n    // \"force\" the microtask queue to be flushed by adding an empty timer.\n    if (isIOS) { setTimeout(noop); }\n  };\n} else {\n  // fallback to macro\n  microTimerFunc = macroTimerFunc;\n}\n\n/**\n * Wrap a function so that if any code inside triggers state change,\n * the changes are queued using a (macro) task instead of a microtask.\n */\nexport function withMacroTask(fn) {\n  return fn._withTask || (fn._withTask = function() {\n    useMacroTask = true;\n    const res = fn.apply(null, arguments);\n    useMacroTask = false;\n    return res;\n  });\n}\n\nexport function nextTick(cb, ctx) {\n  let _resolve;\n  callbacks.push(() =\u003e {\n    if (cb) {\n      try {\n        cb.call(ctx);\n      } catch (e) {\n        handleError(e, ctx, \"nextTick\");\n      }\n    } else if (_resolve) {\n      _resolve(ctx);\n    }\n  });\n  if (!pending) {\n    pending = true;\n    if (useMacroTask) {\n      macroTimerFunc();\n    } else {\n      microTimerFunc();\n    }\n  }\n  // $flow-disable-line\n  if (!cb \u0026\u0026 typeof Promise !== \"undefined\") {\n    return new Promise((resolve) =\u003e {\n      _resolve = resolve;\n    });\n  }\n}\n```\n\n## 总结\n\n本文代码已经放在 https://github.com/xyzingh/learn-vue-observer ，运行 `npm i \u0026\u0026 npm run test` 可以测试。\n\n  [1]: http://es6.ruanyifeng.com/\n  [2]: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty\n  [3]: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/observe\n  [4]: http://es6.ruanyifeng.com/#docs/proxy\n  [5]: https://cn.vuejs.org/v2/guide/list.html#%E6%95%B0%E7%BB%84%E6%9B%B4%E6%96%B0%E6%A3%80%E6%B5%8B\n  [6]: https://cn.vuejs.org/v2/guide/computed.html#%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7\n  [7]: https://cn.vuejs.org/v2/api/#vm-watch\n  [8]: https://github.com/vuejs/vue/issues/3883\n  [9]: https://juejin.im/post/5ab88836f265da237410f701","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FFuuuOverclocking%2Flearn-vue-observer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FFuuuOverclocking%2Flearn-vue-observer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FFuuuOverclocking%2Flearn-vue-observer/lists"}