Vue2源码系列-响应式原理

373 阅读8分钟

这是我参与8月更文挑战的第3天,活动详情查看: 8月更文挑战

目录

一、实现原理
二、基础知识
  设计模式
  Object.defineProperty
三、架构
四、视图->模型
五、模型->视图
六、额外惊喜
  nextTick
  $set
  $del
  重写数组方法

我们这里以 Vue 的 2.6.14 版本为例讲解;篇幅有限,仅节选关键源码展示

一、实现原理

先给总结,毕竟不是所有人都喜欢有趣(bushi)的源码。

官方图镇楼

vueReactivity.png

  • 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter
  • 每一个组件都有自己的 watcher 实例,通过上一篇的简单介绍,这里指的就是 render-watcher,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染
  • 在编译阶段给表单标签(input、select 等)绑定了 input 方法,用户编辑完成后触发事件修改对应的 data

二、基础知识

设计模式

网上有很多文章都在讨论 Vue 的数据双向绑定用的到底是 “观察者模式” 还是 “发布与订阅模式”,要回答这个问题先要了解下两种模式。

首先需要明确的是,这两种模式是有区别的,区别主要在于有没有中间人(Broker)这个角色。

在观察者模式里,changed() 方法所在的实例对象,就是被观察者(Subject,或者叫 Observable),它只需维护一套观察者(Observer)的集合,这些 Observer 实现相同的接口,Subject 只需要知道通知 Observer 时,需要调用哪个统一方法就好了,两者属于松耦合的关系


观察者模式

而在发布订阅模式里,发布者不会直接通知订阅者,它俩彼此互不相识。
发布者只需发消息给 Broker,而订阅者需要从 Broker 中读取(订阅)消息。


发布订阅模式

说到这里,你觉得 Vue 的数据绑定应该属于哪种模式?


严格来说,Vue 数据绑定的模式不具有普适性,难以说属于哪一种模式,非要说一种的话,应该更接近 发布订阅模式。

  • 发布者:setter

    • 数据更新,触发 set 函数,set 函数通知 Dep 执行 notify
  • 调度中心(Broker):Dep

    • 负责收集依赖和收到发布者的命令执行 notify 方法,调度中心通知 Wather 执行更新方法。
    • watcher 去调用自己的 update 方法,本质上执行的是 new Watcher 时候,拿到的回调函数。
  • 订阅者:getter

    • 订阅/收集 wather

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

语法:Object.defineProperty(obj, prop, descriptor)

  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称或 Symbol 。
  • descriptor:要定义或修改的属性描述符。

描述符(descriptor)有以下几种选择

描述默认值
configurable为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除false
enumerable为 true 时表示该属性可枚举false
value该属性对应的值undefined
writable表明该属性是否可写;为 true 时,value 可以被修改false
get属性的 getter 函数;访问该属性时,会调用此函数,返回值即该属性的值;执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)undefined
set属性的 setter 函数;属性值被修改时,会调用此函数;该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象undefined

set 无法检测到 对象属性的增删操作数组的修改

三、架构

众所周知,Vue 是一款 MVVM 框架,那什么是 MVVM 框架呢?


图片来源:维基百科

如上图所示,MVVM 由三个部分组成:视图(View)、视图模型(ViewModel)、模型(Model)。旨在利用 数据绑定,更好地促进视图层开发与模式其余部分的分离。

Vue 中的数据绑定是双向的,即:视图层对数据的修改也会修改模型层对应的数据,模型层数据的修改也会直接反映在视图上。
当然这一切都是由框架完成的。

四、视图->模型

这里我们用个例子讲解

<div id="app"></div>
<script>
  new Vue({
    el: "#app",
    template: '<input v-model="msg"/>',
    data: () => {
      return { msg: "hello world" };
    },
  });
</script>

在 Vue2 的版本中我们使用 v-model 实现数据的绑定,而 v-model 其实也只是语法糖,等价于 <input :value="msg" @input="msg = $event.target.value"/>

首先,在编译阶段 parse(src/compiler/parser) 方法将模板解析为 AST,结果如下

{
  "type": 1,
  "tag": "input",
  "attrsList": [
    {
      "name": "v-model",
      "value": "msg",
      "start": 7,
      "end": 20,
    },
  ],
  "attrsMap": {
    "v-model": "msg",
  },
  "rawAttrsMap": {
    "v-model": {
      "name": "v-model",
      "value": "msg",
      "start": 7,
      "end": 20,
    },
  },
  "children": [],
  "start": 0,
  "end": 22,
  "plain": false,
  "hasBindings": true,
  "directives": [
    {
      "name": "model",
      "rawName": "v-model",
      "value": "msg",
      "arg": null,
      "isDynamicArg": false,
      "start": 7,
      "end": 20,
    },
  ],
  "static": false,
  "staticRoot": false,
};

详细编译过程不是本文的重点,就不展开了。

之后调用 generate(src/compiler/codegen)处理 for、if、slot 等属性;我这个例子都没有,直接开始处理 指令(derective),这个例子中的指令也就是 v-model,同时这里用的节点是 input,直接调用 genDefaultModel 处理 model

genDefaultModel 这里也比较重要

  • 添加 value,值为 (msg)
  • 添加事件,根据是否 lazy、节点类型是不是 range 判断绑定的事件用 change__r 还是 input,这里最后绑定的是 input,值为 if($event.target.composing)return;msg=$event.target.value

处理完 model 后,AST 多了两个属性 propsevents

{
  "props": [{ "name": "value", "value": "(msg)" }],
  "events": {
    "input": {
      "value": "if($event.target.composing)return;msg=$event.target.value"
    }
  }
}

最后得到 render 函数,_c 方法用于创建节点

function anonymous() {
  with (this) {
    return _c("input", {
      directives: [
        { name: "model", rawName: "v-model", value: msg, expression: "msg" },
      ],
      domProps: { value: msg },
      on: {
        input: function ($event) {
          if ($event.target.composing) return;
          msg = $event.target.value;
        },
      },
    });
  }
}

综上,本质上是 Vue 在编译时,给 input 标签绑定了 input 方法,<select><textarea> 也是绑定的 input 方法;当用户编辑时触发,如果还处在编辑阶段(composing 为 true)则啥也不做,如果编辑完成了就将值赋给 msg

五、模型->视图

上一篇中我们说过,在初始化阶段会调用 initData 方法对 data 初始化

// state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // 省略的部分都是在校验 data 的格式与属性名是否合法
  ...
  // observe data
  observe(data, true /* asRootData */)
}

在获取到 data 后,调用 observe,这里就是梦开始的地方

Observer

observe

直接上源码

export function observe (value: any, asRootData: ?boolean): Observer | void {
  ...
  ob = new Observer(value)
  ...
  return ob
}

new 了一个 Observer,传入上一步的 data

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

  constructor (value: any) {
    // step 1
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // step 2
    ...
    this.walk(value)
  }

  walk (obj: Object) {
    // step 3
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  ...
}
  • step 1:初始化,创建 Dep 实例;将当前 Observer 实例映射到 __ob__ 属性,可读取、修改、删除但不可枚举
  • step 2:调用自身的 walk 方法,传入 value 也就是 data
  • step 3:遍历 data,调用 defineReactive

看到这里,有的童鞋可能就要问了:“为啥 step 1 这里要新建一个 Dep 实例,我查了完整的源码也没有用到啊?”

先声明一下,我没有故意省略,而是 Observer 里确实没有用到这个 this.dep,但是在 $set$del 的实现中有妙用,继续往下看吧(狗头)

defineReactive

重点讲一下 defineReactive

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // step 1
  const dep = new Dep();
  // step 2
  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }
  // step 3
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }
  // step 4
  let childOb = !shallow && observe(val)
  // step 5
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // step 6
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        // step 7
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      // step 8
      const value = getter ? getter.call(obj) : val;
      // step 9
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      ...
      // step 10
      if (getter && !setter) return;
      // step 11
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      ...
      // step 12
      dep.notify();
    },
  });
}
  • step 1:创建 Dep 实例,注意与 Observer 构造函数中创建的 Dep 区分开
  • step 2:获取 data 的默认对象描述,若 configurable 为 false 表示该属性不可修改,直接返回
  • step 3:
    • 缓存 data 默认的 getter 与 setter
    • 若 getter 不存在 或 存在 stter,且 defineReactive 只有两个入参(仅有 obj 与 key),则将 obj[key] 赋值给 val
  • step 4:shallow 表明是否只做浅监听,默认 undefined,(为 true 时,若监听的属性值为对象不再继续监听其属性)
    • 若 shallow 为 true,则不执行后面的 observe(val),childOb 为 false
    • 若 shallow 为 false/undefined,则执行后面的 observe(val),observe 方法返回一个 Observer 实例,childOb 等于 返回的 Observer 实例
  • step 5:使用 Object.defineProperty 设置属性为 可枚举、可修改,并加上 getter 与 setter
  • step 6:访问该属性时触发 getter
    • 判断当前属性是否存在 getter,存在则直接调用 getter 获取属性值,不存在则使用 step 3 得到的 val
    • Dep.target 存在,则调用 dep.depend 做依赖收集,这里的 Dep.target 指向的是 Watcher
  • step 7:若 childOb 不等于 false,即 childOb 指向 Observer 实例,则调用 dep.depend,对该属性值(能走到这说明这个属性值是个对象)做依赖收集
  • step 8:修改该属性值时触发 setter,同 step 6 一样先获取到该属性原来的值
  • step 9:
    • 判断新设置的属性值与原来的属性值是否一致,若一致则返回,不做处理
    • 这里还判断了新设置的值不等于自身且原来的值也不等于自身的情况,会出现这种情况的取值也就只有 NaN 和 Symbol 两种了
  • step 10:若存在 getter 没有 setter 则直接返回,不做处理
  • step 11:
    • 若存在 setter,调用 setter 给属性设置新属性值
    • 存在则将新属性值赋值给 val 参数
  • step 12:最后调用 dep.notify

tip:childOb 看不懂没事,$set$del 里有它(狗头)

Dep

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor() {
    // step 1
    this.id = uid++;
    this.subs = [];
  }
  // step 2
  addSub(sub: Watcher) {
    this.subs.push(sub);
  }
  ...
  // step 3
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
  // step 4
  notify() {
    ...
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}
  • step 1:Dep 构造函数
    • uid 默认值为 0, 赋值给 id 后加 1
    • subs 初始化为空数组, 存储 Watcher 实例
  • step 2:将 Watcher 实例添加到 subs 中
  • step 3:依赖收集, Dep.target 存在则调用 Watcher 的 addDep 方法, 将当前 Dep 实例与 Watcher 关联
    • 在创建非懒执行的 watcher 时(lazy = false),Dep.target 等于当前创建的 Watcher 实例
  • step 4:遍历 subs, 逐个调用 Watcher 实例的 update 方法

Watcher

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  // 篇幅有限,仅展示了部分属性
  ...
  // step 1
  addDep(dep: Dep) {
    ...
    dep.addSub(this);
  }
  ...
  // step 2
  update() {
    if (this.lazy) {
      // step 3
      this.dirty = true;
    } else if (this.sync) {
      // step 4
      this.run();
    } else {
      // step 5
      queueWatcher(this);
    }
  }
  ...
}
  • step 1:把当前的 watcher 推入 dep 实例的 watcher 队列(subs)里去
  • step 2:本质就是执行创建 Watcher 实例时传入的回调函数(cb)
  • step 3:若 Watcher 的配置 lazy 为 true,则将 dirty 置为 true,只有当前 Watcher 实例所监听的属性被引用时,执行 evaluate 后才会再置为 false,等待下一次引用
  • step 4:sync 表明是否立即执行,若是则立即执行回调函数(cb)
  • step 5:如果 lazy 为 false,sync 也为 false,则调用 queueWatcher

这个 queueWatcher 维护了一个 Watcher 实例队列,在下一次事件循环时,先通过 Watcher 实例的 id,对它们进行排序,之后再逐个调用 watcher.run 方法。怎么让这些操作在下次事件循环时再执行呢?这里就使用了 nextTick

通常说的 依赖收集 是指 watcher 去收集自己所依赖的数据属性。不过从源码实现上来看,实际上是把 watcher 对象推入了 Dep 实例的 watcher 队列(subs)里,更像是 Dep 在 “收集” watcher

render-watcher

在上一篇我们介绍过 Vue 的执行过程,render-watcher 在挂载阶段被创建,即在 created 之后, mounted 之前,判断是否配置了 el,若配置了则开始进行挂载

// initMixin
if (vm.$options.el) {
  vm.$mount(vm.$options.el);
}

在挂载过程中会新建一个 Watcher 实例,这个实例即 render-watcher

// lifecycle.js / mountComponent
new Watcher(
  vm,
  updateComponent,
  noop,
  {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, "beforeUpdate");
      }
    },
  },
  true /* isRenderWatcher */
);

该实例会挂到当前组件实例的 _watcher 属性上,负责监听数据的变化,完成视图的更新。监听数据的变化完成视图更新的核心就是 render-watcher

user-watcher

user-watcher 顾名思义就是用户创建或者说配置的 watch,它的创建发生在初始化阶段的 initWatch,负责处理用户配置的 watch,在 beforeCreate 之后,created 之前。

export function stateMixin (Vue: Class<Component>) {
  ...
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    ...
    const watcher = new Watcher(vm, expOrFn, cb, options)
    ...
  }
}

render-watcher 类似,不同的是 user-watcher 的回调函数由用户定义,而 render-watcher 的回调函数为更新视图的函数

computed-watcher

computed-watchercomputed 属性使用的 Watcher,它的创建同样发生在初始化阶段,在 initWatch 之前,执行 initComputed;同样是在 beforeCreate 之后

// state.js
const computedWatcherOptions = { lazy: true };
function initComputed(vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null);
  ...
  if (!isSSR) {
    // create internal watcher for the computed property.
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    );
  }
  ...
}

创建好 watcher 后,使用 Object.defineProperty 拦截 computed 定义的属性的 getter,当属性被读取时触发。

computed-watcher 创建时 lazy 为 true,不会将当前 watcher 实例赋给 Dep.target,而是当在页面上读取 computed 的属性时,Dep.target 等于 computed-watcher,执行 getter 后,清空依赖(deps),Dep.target 重新等于上一个 watcher 实例,computed-watcher 重新收集依赖

六、额外惊喜

nextTick

作用

在前面的介绍中,我们知道数据发生变更后会触发 watcherupdate 方法,对于未设置 sync 属性的变更会调用 queueWatcher 方法,其最终使用的是 nextTick 方法

nextTick 方法的作用便是:将所有更新操作加入到 任务队列,在同一事件循环中执行

官方文档也介绍了这一特性:

Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。

(对任务队列不熟悉的童鞋,可以先看一下我之前的文章 从浏览器原理谈 EventLoop 与微任务宏任务

源码

再来看一下源码

export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve;
  // step 1
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, "nextTick");
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  // step 2
  if (!pending) {
    pending = true;
    timerFunc();
  }
  // step 3
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}
  • step 1:将回调函数 cb 推入 callbacks 数组
  • step 2:pending 用来标识当前是否正在执行回调函数
    • 若 pending 为 false,则将 pending 置为 true,调用 timeFunc
  • step 3:若没有传入回调函数,且 Promise 可用,则返回 promise

介绍 timeFunc 方法前,先讲一下 flushCallbacks,作用就是逐个执行 callbacks 中的函数

function flushCallbacks() {
  pending = false;
  // 浅拷贝 callbacks
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    // 逐一执行 callback 数组中的函数
    copies[i]();
  }
}

最后介绍 timerFunc,主要作用就是根据不同平台选择合适的方案将 callbacks 加入到异步任务队列中

let timerFunc;
if (typeof Promise !== "undefined" && isNative(Promise)) {
  // step 1
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
    if (isIOS) setTimeout(noop);
  };
  isUsingMicroTask = true;
} else if (
  !isIE &&
  typeof MutationObserver !== "undefined" &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
  // step 2
  let counter = 1;
  // step 2-1
  const observer = new MutationObserver(flushCallbacks);
  // step 2-2
  const textNode = document.createTextNode(String(counter));
  // step 2-3
  observer.observe(textNode, {
    characterData: true,
  });
  // step 2-4
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
  // step 3
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // step 4
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}
  • step 1:判断 Promise 是否可用,优先使用 Promise,为了处理 IOS 下微任务队列的异常状态,额外设置了个空定时器
  • step 2:若不支持 Promsie,则进一步判断是否支持 MutationObserver API
    • step 2-1:实例化一个观察者对象
    • step 2-2:创建一个文本节点
    • step 2-3:监听文本节点变化
    • step 2-4:手动更改文本节点 data,MutationObserver 监听到变化立即触发 callbacks 执行
  • step 3:使用 setImmediate
  • step 4:兜底使用 setTimeout

按照优先级划分如下:

  1. Promise(微任务)
  2. MutationObserver(微任务)
  3. setImmediate(宏任务)
  4. setTimeout(宏任务)

应用

nextTick 的应用主要在于:当你需要等待视图更新完成后再执行某些操作时使用

常见场景如:当你使用代码去生成一个新节点时,又需要立即获取到这个刚渲染出来的节点

在上一小节中,我们知道使用 nextTick 不仅可以采用传回调函数的方式,还可以把它当作 thenable 对象使用

因此,以下两种方式都是可以的

Vue.$nextTick(() => {
  console.log("from callback");
});
Vue.$nextTick.then(() => {
  console.log("from then");
});

$set

作用

由于 Object.DefineProperty 的局限性,无法检测到 对象属性的增加操作

如:const a = {}; a.b = 'test'

因此,Vue 提供了 $set 方法,手动变更对象的属性,触发视图更新。

源码

export function set(target: Array<any> | Object, key: any, val: any): any {
  ...
  // step 1
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }
  // step 2
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  // step 3
  const ob = (target: any).__ob__;
  // step 4
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== "production" &&
      warn(
        "Avoid adding reactive properties to a Vue instance or its root $data " +
          "at runtime - declare it upfront in the data option."
      );
    return val;
  }
  // step 5
  if (!ob) {
    target[key] = val;
    return val;
  }
  // step 6
  defineReactive(ob.value, key, val);
  // step 7
  ob.dep.notify();
  return val;
}
  • step 1:判断 target 是否为数组,key 是否为数组下标(是否为正整数)
    • 若是,则重新设置数组长度,并调用 splice 将 key 对应的元素替换为 val,返回 val
  • step 2:判断 key 属性是否在 target 中已存在,且 key 不为 Object 原型上的属性
    • 若是,则直接修改 target 的 key 属性值为 val,返回 val
  • step 3:获取 target 的 __ob__ 属性
    • 在初始化阶段,根据 data 创建 Observer 时,将 当前 Observer 实例映射到 __ob__ 属性,同时创建一个 Dep 赋给 dep 属性
  • step 4:
    • 能走到这里说明 key 不是 target 中原有的属性,是新增的属性
    • 判断 target 是不是 Vue 实例本身;若是,则直接返回 val,若在非生产模式下弹出警告,不允许往 Vue 实例上增加响应式属性
  • step 5:若 ob 不存在,说明 key 为非响应式属性,直接修改 target 的 key 属性值,返回 val
  • step 6:通过 defineReactive 将 target 的 key 属性值改为 val
  • step 7:调用 ob 自身 depnotify 方法触发回调函数,这里的回调就是更新视图。

看到这里有的童鞋又要疑惑了,这里的 dep 是何时完成的依赖收集呢?

在对 data 的初始化中 defineReactive 对 data 的各个属性增加 getter、setter 拦截器,在 getter 拦截器中有如下代码段

...
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
  ...
  get: function reactiveGetter () {
    ...
    if (Dep.target) {
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
        ...
      }
    }
    return value
  },
  ...
}
...

这里的 childOb 便是 data 中的属性对应的 __ob__ 属性,若 childOb 存在,则调用 dep.depend 做依赖收集,这里的 dep 即 Observer 构造函数中创建的 Dep

$delete

作用

$delete 的出现同样是因为 Object.DefineProperty 无法检测到 对象属性的删除操作

如:const a = { b:'test' }; delete a.b;

源码

export function del (target: Array<any> | Object, key: any) {
  ...
  // step 1
  const ob = (target: any).__ob__
  ...
  // step 2
  if (!hasOwn(target, key)) {
    return
  }
  // step 3
  delete target[key]
  // step 4
  if (!ob) {
    return
  }
  // step 5
  ob.dep.notify()
}
  • step 1:获取 target 的 __ob__ 属性
  • step 2:判断 key 属性是否在 target 中已存在,若不存在则直接返回
  • step 3:使用 delete 关键字,完成属性删除操作
  • step 4:若 ob 不存在,说明 key 为非响应式属性,直接返回
  • step 5:调用 ob 自身 depnotify 方法触发回调函数,这里的回调就是更新视图

重写数组方法

作用

Object.DefineProperty 的局限性,无法直接检测到数组变化。Vue 对 data 方法返回的对象中的元素进行响应式处理时,如果元素是数组时,仅仅对数组本身进行响应式化,而不对数组内部元素进行响应式化。因此,官方规定了 7 个数组方法,push()pop()shift()unshift()splice()sort()reverse()

源码

// step 1
const arrayProto = Array.prototype
// step 2
export const arrayMethods = Object.create(arrayProto)

// step 3
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

// step 4
methodsToPatch.forEach(function (method) {
  // step 5
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    // step 6
    const result = original.apply(this, args)
    // step 7
    const ob = this.__ob__
    // step 8
    let inserted
    switch (method) {
      case 'push':
        case 'unshift':
          inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // step 9
    ob.dep.notify()
    return result
  })
})
  • step 1:缓存数组原型
  • step 2:创建一个新对象,使用 arrayProto 作为 arrayMethods 的__proto__
  • step 3:需要进行功能拓展的数组方法
  • step 4:遍历数组方法
  • step 5:缓存原生数组方法
  • step 6:执行并缓存原生数组方法的执行结果
  • step 7:获取 ob 属性
  • step 8:若执行的是 pushunshiftsplice 方法,涉及到追加元素的方法,则使用 ob 的 observeArray 方法,将新元素设置为响应式
  • step 9:调用 ob 自身 depnotify 方法触发视图更新

文章同时发在个人公众号,欢迎关注 MelonField

参考