Skip to content

Vue2响应式原理

Vue2 的响应式原理

ps:在读本章之前默认你已经对 Object.defineProperty 有一定程度的理解,然后大家会在本篇中经常看到Dev,这是由于我们在开发时需要看一些警告来帮助我们写更好的代码,但是进入到生产环境我们并不希望用户的控制台会看到一堆警告,还有一些例如 mock、ssr 都是服务端渲染相关的,不多作讲解,还有一些 vue3 的东西也不多在这里多作讲解

想了很久决定出的第一篇文章就是响应式系统的一个讲解吧,响应式是我们面试的时候非常经常考的一个题目,而且响应式系统也是我们学习 Vue 非常重要的一个内容,是重要的组成部分。但其本质其实也就是数据驱动视图主动更新。

首先的话我就先介绍 Vue2 的响应式原理

Vue2 在具体上其实就是通过几个核心部件来实现响应式:

  • Obsever:监视者类,监视数据的变化,在数据变化时告诉通知者
  • Dep,通知者类,通知订阅者更新视图,因为一个数据可能被多处使用,所以一个通知者会存储多位订阅者
  • Watcher,订阅者类,用于存储数据变化后要执行的更新函数,调用更新函数可以使用新的数据更新视图
  • Schduler,调度器,调度器会维护一个执行队列,在队列中同一个 watcher 只会存在一次,队列中的 watcher 不是立即执行,它会在 nextTick 后触发

再具体剖析这几个核心部件之前,我想我们需要先理解一下在 vue2 中 new 一个 vue 实例会经历哪些过程。

初始化

initMixin

源码:src/core/instance/init.t s

在初始化一个 vue 的时候会调用 init 方法,这块的代码主要在 initMixin 里,我们简单看一下。

ts
export function initMixin(Vue: typeof Component) {
    // 绑定初始化方法
    Vue.prototype._init = function (options?: Record<string, any>) {
        ···  // 上面大概对一些属性初始化,合并option等
        initLifecycle(vm) // 初始化实例的属性 $refs等
        initEvents(vm) // 初始化事件:$on 等
        initRender(vm) // 初始化渲染
        callHook(vm, 'beforeCreate', undefined, false /* setContext */) // 调用钩子函数
        initInjections(vm) // inject
        initState(vm)   // 重头!!! 初始化组建数据
        initProvide(vm) // provide
        callHook(vm, 'created') // 调用钩子函数
        ···
    }
}

初始化这里调用了很多方法,每个方法都做着不同的事,而关于响应式主要就是组件内的数据 props、data。而这一块的代码主要就在 initState 里面,所以下面我们就简单看一下 initState

initState

源码:src/core/instance/state.ts

ts
export function initState(vm: Component) {
    const opts = vm.$options;
    if (opts.props) initProps(vm, opts.props); // 初始化props

    initSetup(vm); // 组合式开发的api vue2本不该有

    if (opts.methods) initMethods(vm, opts.methods); // 初始化methods
    if (opts.data) {
        initData(vm); // 初始化data
    } else {
        // 如果data为空,就默认赋值为空对象,并监听
        const ob = observe((vm._data = {}));
        ob && ob.vmCount++;
    }
    if (opts.computed) initComputed(vm, opts.computed); // 初始化 computed
    if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch); // 初始化 watch
    }
}

简单看一下,就是读取 Vue 里绑定的 options 里的属性,然后调用一堆初始化,不多作讲解,直奔主题去找响应式相关的 initProps 和 initData

initProps

源码:src/core/instance/state.ts

ts
function initProps(vm: Component, propsOptions: Object) {
    // 读取父组件传入的props
    const propsData = vm.$options.propsData || {};
    // 创建最终的props
    const props = (vm._props = shallowReactive({}));
    // 采用数组来取代动态读取props的keys,通过数组存放 props 的 key,就算 props 值空了,key 也会在里面
    const keys: string[] = (vm.$options._propKeys = []);
    const isRoot = !vm.$parent;
    // 转化根实例的props
    if (!isRoot) {
        toggleObserving(false);
    }
    for (const key in propsOptions) {
        keys.push(key);
        // 校验 props 类型、default 属性等
        const value = validateProp(key, propsOptions, propsData, vm);
        // 在开发环境中
        if (__DEV__) {
            // hyphenate其实就将小驼峰转化连字符形式
            const hyphenatedKey = hyphenate(key);
            // 检测是否是保留属性
            if (
                isReservedAttribute(hyphenatedKey) ||
                config.isReservedAttr(hyphenatedKey)
            ) {
                // `hyphenatedKey 是保留属性,不能用作组件 prop`
                warn(
                    `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
                    vm
                );
            }
            // 把 props 设置成响应式的
            defineReactive(
                props,
                key,
                value,
                () => {
                    // 如果用户修改 props 发出警告
                    if (!isRoot && !isUpdatingChildComponent) {
                        // `避免直接改变 prop`
                        warn(
                            `Avoid mutating a prop directly since the value will be ` +
                                `overwritten whenever the parent component re-renders. ` +
                                `Instead, use a data or computed property based on the prop's ` +
                                `value. Prop being mutated: "${key}"`,
                            vm
                        );
                    }
                },
                true /* shallow */
            );
        } else {
            // 生产环境中直接设置props为响应式
            defineReactive(props, key, value, undefined, true /* shallow */);
        }
        // 把不在默认 vm 上的属性,代理到实例上
        // 可以让 vm._props.xx 通过 vm.xx 访问
        if (!(key in vm)) {
            proxy(vm, `_props`, key);
        }
    }
    toggleObserving(true);
}

分析一下上面的代码可以发现主要就是做以下这些事:

  • 遍历父组件传进来的 props 列表
  • 校验每个属性的命名、类型、default 属性等,都没有问题就调用 defineReactive 设置成响应式
  • 然后用 proxy() 把属性代理到当前实例上,如把 vm._props.xx 变成 vm.xx,就可以访问

initData

源码:src/core/instance/state.ts

ts
function initData(vm: Component) {
    // 读取data 如果data是函数就调用getData函数获取,否则直接获取
    let data: any = vm.$options.data;
    data = vm._data = isFunction(data) ? getData(data, vm) : data || {};

    // 看data是否是一个对象
    if (!isPlainObject(data)) {
        data = {};
        __DEV__ &&
            warn(
                "data functions should return an object:\n" +
                    "https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
                vm
            );
    }
    // proxy data on instance
    // 获取当前实例的 data 属性名集合
    const keys = Object.keys(data);
    // 获取当前实例的 props
    const props = vm.$options.props;
    // 获取当前实例的 methods 对象
    const methods = vm.$options.methods;
    let i = keys.length;
    // 遍历key
    while (i--) {
        const key = keys[i];
        // 开发环境下判断 methods 里的方法是否存在于 props 中
        if (__DEV__) {
            if (methods && hasOwn(methods, key)) {
                warn(
                    `Method "${key}" has already been defined as a data property.`,
                    vm
                );
            }
        }
        // 开发环境下判断 data 里的属性是否存在于 props 中
        if (props && hasOwn(props, key)) {
            __DEV__ &&
                warn(
                    `The data property "${key}" is already declared as a prop. ` +
                        `Use prop default value instead.`,
                    vm
                );
        } else if (!isReserved(key)) {
            // 都不重名的情况下,代理到 vm 上
            // 可以让 vm._data.xx 通过 vm.xx 访问
            proxy(vm, `_data`, key);
        }
    }
    // 监听 data
    const ob = observe(data);
    ob && ob.vmCount++;
}

这里主要做的是:

  • 初始化一个 data,并拿到 keys 集合
  • 遍历 keys 集合,来判断有没有和 props 里的属性名或者 methods 里的方法名重名的
  • 没有问题就通过 proxy() 把 data 里的每一个属性都代理到当前实例上,就可以通过 this.xx 访问了
  • 最后再调用 observe 监听整个 data

把这些都讲完之后,我们就可以发现,最主要的监听就是在 observe 和 defineReactive 这两个方法上

🌟 Observer

observe

源码:src/core/observer/index.ts

首先要介绍的方法是 observe,其主要作用就是给数据加上监视器,可以理解为为 Observer 即将开启前做的一些合规检测

先看一下源码:

ts
export function observe(
    value: any,
    shallow?: boolean, // 浅响应
    ssrMockReactivity?: boolean // 服务端渲染 ssr
): Observer | void {
    // 判断值是否已经绑定(缓存)完了,直接返回缓存的对象
    if (value && hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
        return value.__ob__;
    }
    if (
        shouldObserve &&
        (ssrMockReactivity || !isServerRendering()) &&
        (isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value.__v_skip /* ReactiveFlags.SKIP */ &&
        !isRef(value) &&
        !(value instanceof VNode)
    ) {
        // 创建监听者
        return new Observer(value, shallow, ssrMockReactivity);
    }
}

可以看到源码本质上就是做了两个检测,第一个 if 语句就是判断值是否已经绑定(缓存)完了,直接返回缓存的对象;第二个 if 语句就是做了一些判断,先是用系统的参数,然后是判断值是否是数组或者对象,是否可添加属性,是否是 VNODE 类型等。当这些判断都通过了之后,就给没有添加 Observer 的数据添加一个 Observer,也就是监听者。

Observer

源码:src/core/observer/index.ts

这是一个类,作用是把一个正常的数据成可观测的数据,先看一下源码:

ts
export class Observer {
    dep: Dep; // 通知者类
    vmCount: number; // 根上的vm数量

    // 构造函数
    constructor(
        public value: any,
        public shallow = false,
        public mock = false
    ) {
        // this.value = value 已不用
        // 根据是否是ssr创建不同数组
        this.dep = mock ? mockDep : new Dep();
        this.vmCount = 0;
        // 给 value 添加 __ob__ 属性,值为value 的 Observe 实例
        // 表示已经变成响应式了,目的是对象遍历时就直接跳过,避免重复操作
        def(value, "__ob__", this);
        // 类型判断
        if (isArray(value)) {
            if (!mock) {
                // 判断数组是否有__proto__
                if (hasProto) {
                    // 如果有就重写数组的方法
                    (value as any).__proto__ = arrayMethods;
                } else {
                    // 没有就通过 def,也就是Object.defineProperty 去定义属性值
                    for (let i = 0, l = arrayKeys.length; i `< l; i++) {
                        const key = arrayKeys[i];
                        def(value, key, arrayMethods[key]);
                    }
                }
            }
            if (!shallow) {
                // 监听数组里所有的元素
                this.observeArray(value);
            }
        } else {
            // 如果是对象类型,遍历对象所有属性,转为响应式对象,也是动态添加 getter 和 setter,实现双向绑定
            const keys = Object.keys(value);
            for (let i = 0; i `< keys.length; i++) {
                const key = keys[i];
                defineReactive(
                    value,
                    key,
                    NO_INITIAL_VALUE,
                    undefined,
                    shallow,
                    mock
                );
            }
        }
    }

    // 监听数组里所有的元素
    observeArray(value: any[]) {
        for (let i = 0, l = value.length; i `< l; i++) {
            observe(value[i], false, this.mock);
        }
    }
}

这里主要做的是:

  • 给当前 value 打上已经是响应式属性的标记,避免重复操作
  • 然后判断数据类型
  • 如果是对象,就遍历对象,调用 defineReactive()创建响应式对象
  • 如果是数组,就遍历数组,调用 observe()对每一个元素进行监听

defineReactive()

源码:src/core/observer/index.ts
ts
export function defineReactive(
    obj: object,
    key: string,
    val?: any,
    customSetter?: Function | null,
    shallow?: boolean,
    mock?: boolean,
    observeEvenIfShallow = false
) {
    // 创建 dep 实例
    const dep = new Dep();
    // 拿到对象的属性描述符 就是enumerable之类的
    const property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
        return;
    }

    // 获取自定义的 getter 和 setter
    const getter = property && property.get;
    const setter = property && property.set;
    if (
        (!getter || setter) &&
        (val === NO_INITIAL_VALUE || arguments.length === 2)
    ) {
        val = obj[key];
    }

    // 如果 val 是对象并且不是浅响应的话就递归监听
    // 递归调用 observe 就可以保证不管对象结构嵌套有多深,都能变成响应式对象
    let childOb = shallow ? val && val.__ob__ : observe(val, false, mock);
    // 截持对象属性的 getter 和 setter
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        // 拦截 getter,当取值时会触发该函数
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val;
            // 进行依赖收集
            // 初始化渲染 watcher 时访问到需要双向绑定的对象,从而触发 get 函数
            if (Dep.target) {
                if (__DEV__) {
                    dep.depend({
                        target: obj,
                        type: TrackOpTypes.GET,
                        key,
                    });
                } else {
                    dep.depend();
                }
                if (childOb) {
                    childOb.dep.depend();
                    if (isArray(value)) {
                        dependArray(value);
                    }
                }
            }
            return isRef(value) && !shallow ? value.value : value;
        },
        // 拦截 setter,当值改变时会触发该函数
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val;
            // 判断是否发生变化
            if (!hasChanged(value, newVal)) {
                return;
            }
            if (__DEV__ && customSetter) {
                customSetter();
            }
            if (setter) {
                // 有setter就调用
                setter.call(obj, newVal);
            } else if (getter) {
                // 没有 setter 的访问器属性
                return;
            } else if (!shallow && isRef(value) && !isRef(newVal)) {
                // vue3的ref
                value.value = newVal;
                return;
            } else {
                val = newVal;
            }
            // 非浅响应情况下,新值是对象的话递归监听
            childOb = shallow
                ? newVal && newVal.__ob__
                : observe(newVal, false, mock);
            // 通知更新
            if (__DEV__) {
                dep.notify({
                    type: TriggerOpTypes.SET,
                    target: obj,
                    key,
                    newValue: newVal,
                    oldValue: value,
                });
            } else {
                dep.notify();
            }
        },
    });

    return dep;
}

这里主要做的是:

  • 先初始化一个 dep 实例
  • 在非浅响应情况下,如果是对象就调用 observe,递归监听,以保证不管结构嵌套多深,都能变成响应式对象
  • 然后调用 Object.defineProperty() 劫持对象属性的 getter 和 getter
  • 如果获取时,触发 getter 会调用 dep.depend() 把观察者 push 到依赖的数组 subs 里去,也就是依赖收集
  • 如果更新时,触发 setter 会做以下操作
    • 新值没有变化直接跳出
    • 有 setter 就调用,没有且有 getter 就直接退出
    • 如果新值是对象就调用 observe() 递归监听
    • 最后调用 dep.notify() 派发更新

从这里可以看出来核心是 dep 了,无论是依赖收集还是通知更新等,都是依赖 dep

🌼 Dep

Dep

源码:src/core/observer/dep.ts

从上面看完之后我们就可以发现 Dep 是整个依赖收集和通知更新的核心,接下来先看一下源码,由于源码已经在这几年修改了很多,但是核心还是不变,所以我们依旧把源码中的 DepTarget 当上文说到的 Watcher。

ts
// DepTarget 在这里面其实就是 Watcher 的接口,不过其规定的接口函数没有 Watcher 实现那么多。
export interface DepTarget extends DebuggerOptions {
    id: number;
    addDep(dep: Dep): void;
    update(): void;
}

export default class Dep {
    static target?: DepTarget | null;
    id: number;
    subs: Array<DepTarget | null>; // 观察者
    // pending subs cleanup
    _pending = false;

    constructor() {
        this.id = uid++;
        this.subs = [];
    }

    // 添加观察者
    addSub(sub: DepTarget) {
        this.subs.push(sub);
    }
    // 移除观察者
    removeSub(sub: DepTarget) {
        // 原本函数是:remove(this.subs, sub)
        // 但是拥有大量 sub 的 deps 在 Chromium 中清理速度非常慢 为了解决这个问题,我们暂时取消设置 sub ,并在下一次调度程序刷新时清除它们。
        this.subs[this.subs.indexOf(sub)] = null;
        if (!this._pending) {
            this._pending = true;
            pendingCleanupDeps.push(this);
        }
    }

    depend(info?: DebuggerEventExtraInfo) {
        if (Dep.target) {
            // 调用 Watcher 的 addDep 函数
            Dep.target.addDep(this);
            // 开发环境下的 debug 调试会对 watcher 做跟踪
            if (__DEV__ && info && Dep.target.onTrack) {
                Dep.target.onTrack({
                    effect: Dep.target,
                    ...info,
                });
            }
        }
    }

    notify(info?: DebuggerEventExtraInfo) {
        // 序列化 sub
        const subs = this.subs.filter((s) => s) as DepTarget[];
        if (__DEV__ && !config.async) {
            // 如果不在异步条件下运行,则不会在调度程序中对订阅进行排序,所以需要现在就对它们进行排序以确保它们以正确的顺序触发
            subs.sort((a, b) => a.id - b.id);
        }
        for (let i = 0, l = subs.length; i < l; i++) {
            // 遍历所有 watcher 实例数组
            const sub = subs[i];
            if (__DEV__ && info) {
                // 开发环境下的 debug 调试会对 watcher 触发事件
                sub.onTrigger &&
                    sub.onTrigger({
                        effect: subs[i],
                        ...info,
                    });
            }
            // 调用 sub 内置的更新函数
            sub.update();
        }
    }
}

// 同一时间全局只有一个观察者使用
Dep.target = null;
// 赋值观察者
const targetStack: Array<DepTarget | null | undefined> = [];

export function pushTarget(target?: DepTarget | null) {
    targetStack.push(target);
    Dep.target = target;
}

export function popTarget() {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
}

根据代码可以看出这是一个类,它实际上就是对 Watcher 的一种管理:

  • 这里首先初始化一个 subs 数组,用来存放依赖,也就是观察者,谁依赖这个数据,谁就在这个数组里,然后定义几个方法来对依赖添加、删除、通知更新等
  • 另外它有一个静态属性 target,这是一个全局的 Watcher,也表示同一时间只能存在一个全局的 Watcher,然后还有一个 target 的任务队列。

可以发现这里面的派发更新主要是调用其本身只带的 update 函数,大家可能会有疑惑,这里都没有看到 Watcher 相关的代码,接下来我们在看一个函数。

mountComponent

ts
export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
    // 绑定实例
  vm.$el = el
  // 查看是否有render渲染函数
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (__DEV__) {
        // 看警告应该就知道是干什么了吧哈哈
      if (
        (vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el ||
        el
      ) {
        warn(
          '您正在使用 Vue 的仅限运行时的构建,其中模板“编译器不可用。要么将模板预编译为渲染函数,要么使用编译器包含的构建'+
          vm
        )
      } else {
        warn(
          '无法挂载组件:未定义模板或渲染函数',
          vm
        )
      }
    }
  }
  // 调用钩子函数
  callHook(vm, 'beforeMount')

  let updateComponent
  // 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染,第一个 if 语句中的其他代码基本就是方便调试等
  if (__DEV__ && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

    // 配置 Watcher 的一些属性
  const watcherOptions: WatcherOptions = {
    // 当触发更新的时候,会在更新之前调用
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        // 调用生命周期钩子函数
        callHook(vm, 'beforeUpdate')
      }
    }
  }

  // 方便调试
  if (__DEV__) {
    watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
    watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
  }

  // 重点在这:为当前组件实例设置观察者,监控 updateComponent 函数得到的数据,下面有介绍
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
  hydrating = false

    // 这个应该算是 vue3 的东西
    // 用于刷新的刷新缓冲区:pre 观察程序在 setup() 中的队列
  const preWatchers = vm._preWatchers
  if (preWatchers) {
    for (let i = 0; i `< preWatchers.length; i++) {
      preWatchers[i].run()
    }
  }

  /
  // 没有老的 vnode,说明是首次渲染
  if (vm.$vnode == null) {
    vm._isMounted = true
    // 调用生命周期钩子函数
    callHook(vm, 'mounted')
  }
  return vm
}

现在对于这段源码只需要看到,这个函数是挂载函数,然后在挂载的时候会 new 一个 Watcher,这个作用呢就相当于依赖收集,也就是收集需要响应式的地方,下面配合 Watchr 会细致地说这个过程。

🔥 Watcher

` 源码:src/core/observer/watcher.ts

Watcher 也是一个类,也叫观察者(订阅者),是上文源码中的 DepTarget,不过有点出入的是 DepTarget 规定的需要实现的接口比 Watcher 少,Watcher 可以做的事更多。

先看 Watcher 的源码捋一下吧

ts
// 最后一个 implements DepTarget 明确的告诉我们 Watcher 其实就是 DepTarget 的实现。
export default class Watcher implements DepTarget {
  ··· // 声明配置项的 Type

  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
  // 副作用函数,vue3 时候会讲,这边其实不是很重要
  recordEffectScope(
      this,
      // if the active effect scope is manually created (not a component scope),
      // prioritize it
      activeEffectScope && !activeEffectScope._vm
        ? activeEffectScope
        : vm
        ? vm._scope
        : undefined
    )

    if ((this.vm = vm) && isRenderWatcher) {
      vm._watcher = this
    }

    // 初始化options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
      if (__DEV__) {
        this.onTrack = options.onTrack
        this.onTrigger = options.onTrigger
      }
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.post = false
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []

    // new 两个 dep 数组,原因下面会提到
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = __DEV__ ? expOrFn.toString() : ''

    // 解析 getter
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        __DEV__ &&
          warn(
            `监视路径:“${expOrFn}”失败, 观察程序只接受简单的点分隔路径。如果要完全控制,请改用函数。`
            vm
          )
      }
    }
    // 如果不是懒加载,就会执行 this.get 函数
    this.value = this.lazy ? undefined : this.get()
  }

  get() {
    // 该函数用于缓存 Watcher
    // 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用回调函数,也就是upcateComponent,对需要双向绑定的对象求值,从而触发依赖收集
      value = this.getter.call(vm, vm)
    } catch (e: any) {
      ··· // 错误处理
    } finally {
      // "深度监听
      if (this.deep) {
        traverse(value)
      }
      // 恢复Watcher,Dep 中有讲到
      popTarget()
      // 清理不需要了的依赖
      this.cleanupDeps()
    }
    return value
  }

  // 依赖收集时调用,请注意 newDepIds 和 depIds 是不一样的
  addDep(dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // 把当前 Watcher push 进数组
        dep.addSub(this)
      }
    }
  }

    // 清理不需要的依赖, 主要用于 移除订阅
    /*
    * 这里做的主要是:
    * 先遍历上一次添加的实例数组 deps,移除 dep.subs 数组中的 Watcher 的订阅
    * 然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换
    * 再把 newDepIds 和 newDeps 清空
    */
  cleanupDeps() {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp: any = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  // 派发更新时调用,可以结合 Observe 类 setter 和 Dep 的 notify 看

  update() {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 组件数据更新会走这里,这是 schduler 部分的,待会会讲
      queueWatcher(this)
    }
  }

  // 执行 watcher 的回调
  run() {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(
            this.cb,
            this.vm,
            [value, oldValue],
            this.vm,
            info
          )
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  // lazy Watcher 时才会用到
  evaluate() {
    this.value = this.get()
    this.dirty = false
  }

  // 调用 Watcher 收集的所有 Deps 的 depend 函数
  depend() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  // 从所有依赖项的 Dep 列表中删除自己。
  teardown() {
    if (this.vm && !this.vm._isBeingDestroyed) {
      remove(this.vm._scope.effects, this)
    }
    if (this.active) {
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
      if (this.onStop) {
        this.onStop()
      }
    }
  }
}

我们此时就可以联合上面的 mountComponent 函数代码讲一下依赖收集,先放出上面 mountComponent 代码中 new 一个 Watcher 的参数

ts
new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
);

结合两个和之前的 dep 等,我们大概就可以大概解释依赖收集的过程:

  • 挂载之前会实例化一个渲染 watcher ,进入 watcher 构造函数里就会执行 this.get() 方法
    • 看 watcher 构造函数代码的最后一行
  • 然后就会执行 pushTarget(this),就是把 Dep.target 赋值为当前渲染 watcher 并压入栈(为了恢复用)
  • 然后执行 this.getter.call(vm, vm),也就是上面的 updateComponent() 函数,里面就执行了 vm._update(vm._render(), hydrating)
  • 接着执行 vm._render() 就会生成渲染 vnode,这个过程中会访问 vm 上的数据,就触发了数据对象的 getter
  • 每一个对象值的 getter 都有一个 dep,在触发 getter 的时候就会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)
    • 这个首先要看 Obeserver 类的劫持 get 中的 dep.depend 然后再看 Dep 类中的 depend 方法,就捋清楚这个过程了
  • addDep 这里会做一些判断,以确保同一数据不会被多次添加,接着把符合条件的数据 push 到 subs 里,到这就已经完成了依赖的收集,不过到这里还没执行完,如果是对象还会递归对象触发所有子项的 getter,还要恢复 Dep.target 状态,以及清理不需要的依赖
    • 对应函数 traverse、popTarget、cleanupDeps

补充说明一下为什么需要两个 dep 数组:因为 Vue 是数据驱动的,每次数据变化都会重新 render,也就是说 vm.render() 方法就又会重新执行,再次触发 getter,所以用两个数组表示,新添加的 Dep 实例数组 newDeps 和上一次添加的实例数组 deps

讲完这些之后,我们发现代码中还有一个没讲,就是 queueWatcher(),他是做什么呢,其实就是我们上面讲的最后一个核心部件 Schduler

🌴 Scheduler

queueWatcher()

源码:src/core/observer/scheduler.ts

这是一个队列,也是 Vue 在做派发更新时的一个优化点。就是说在每次数据改变的时候不会都触发 watcher 回调,而是把这些 watcher 都添加到一个队列里,然后在 nextTick 后才执行

解决的问题大概就是这样一个情况:一个交给 watcher 的函数,它里面用到了属性 a、b、c、d,那么 a、b、c、d 属性都会记录依赖,于是下面的代码将会触发 4 次更新

ts
state.a = "new data";
state.b = "new data";
state.c = "new data";
state.d = "new data";

这样肯定是不合理的

接下来看源码吧

ts
export function queueWatcher(watcher: Watcher) {
    // 获得 watcher 的 id
    const id = watcher.id;
    // 判断当前 id 的 watcher 有没有被 push 过
    if (has[id] != null) {
        return;
    }

    // 判断是不是全局唯一的 watcher 并且不是递归,noRecurse 其的一个属性
    if (watcher === Dep.target && watcher.noRecurse) {
        return;
    }

    has[id] = true;
    if (!flushing) {
        // 如果没有进入刷新队列的状态,就将它放到刷新队列
        queue.push(watcher);
    } else {
        // 如果已经进入,从后往前找,找到第一个待插入的 id 比当前队列中的 id 大的位置,将 watcher 插入到队列中
        let i = queue.length - 1;
        while (i > index && queue[i].id > watcher.id) {
            i--;
        }
        queue.splice(i + 1, 0, watcher);
    }
    // 判断是不是在等待更新状态
    if (!waiting) {
        waiting = true;

        if (__DEV__ && !config.async) {
            flushSchedulerQueue();
            return;
        }
        // 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用
        nextTick(flushSchedulerQueue);
    }
}

看完上面代码可以发现主要做的是:

  • 先用 has 对象查找 id,保证同一个 watcher 只会 push 一次
  • 如过没进入刷新状态,就将 watcher 直接放入任务队列,如果在执行 watcher 期间又有新的 watcher 插入进来就会到这里,然后从后往前找,找到第一个待插入的 id 比当前队列中的 id 大的位置,插入到队列中。
  • 最后通过 waiting 保证 nextTick 只会调用一次

flushSchedulerQueue()

源码:src/core/observer/scheduler.ts

ts
function flushSchedulerQueue() {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // 根据 id 排序,有如下条件
  // 1.组件更新需要按从父到子的顺序,因为创建过程中也是先父后子
  // 2.组件内我们自己写的 watcher 优先于渲染 watcher
  // 3.如果某组件在父组件的 watcher 运行期间销毁了,就跳过这个 watcher
  queue.sort(sortCompareFn)

  // 不要缓存队列长度,因为遍历过程中可能队列的长度发生变化
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // 执行 beforeUpdate 生命周期钩子函数
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 执行组件内我们自己写的 watch 的回调函数并渲染组件
    watcher.run()
    // 检查并停止循环更新,比如在 watcher 的过程中又重新给对象赋值了,就会进入无限循环
    if (__DEV__ && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(`无限循环`),
          watcher.vm
        )
        break
      }
    }
  }

  // 重置状态之前,先保留一份队列备份
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // 调用组件激活的钩子  activated
  callActivatedHooks(activatedQueue)
  // 调用组件更新的钩子  updated
  callUpdatedHooks(updatedQueue)
  // 移除订阅
  cleanupDeps()

  // devtool hook
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

这里大概做的就是:

  • 先排序队列,排序条件有三点,看注释
  • 然后遍历队列,执行对应 watcher.before() 和 wacher.run()。需要注意的是,遍历的时候每次都会对队列长度进行求值,因为在 run 之后,很可能又会有新的 watcher 添加进来,这时就会再次执行到上面的 queueWatcher
  • 在这些之后就会调用组件的钩子

callUpdatedHooks

源码:src/core/observer/scheduler.ts

上面调用 callUpdatedHooks() 的时候就会进入这里, 执行 updated 了

ts
function callUpdatedHooks(queue: Watcher[]) {
    let i = queue.length;
    while (i--) {
        const watcher = queue[i];
        const vm = watcher.vm;
        if (
            vm &&
            vm._watcher === watcher &&
            vm._isMounted &&
            !vm._isDestroyed
        ) {
            callHook(vm, "updated");
        }
    }
}

至此 Vue2 的响应式原理流程的源码基本就分析完毕了,接下来就介绍一下上面流程中的不足之处

缺陷分析

使用 Object.defineProperty 实现响应式对象,还是有一些问题的

  • 比如给对象中添加新属性时,是无法触发 setter 的
  • 比如不能检测到数组元素的变化

而这些问题,Vue2 里也有相应的解决文案

Vue.set()

源码:src/core/observer/index.ts

给对象添加新的响应式属性时,可以使用一个全局的 API,就是 Vue.set() 方法 set 方法接收三个参数:

  • target:数组或普通对象
  • key:表示数组下标或对象的 key 名
  • val:表示要替换的新值
ts
export function set(
    target: any[] | Record<string, any>,
    key: any,
    val: any
): any {
    if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
        warn(`不能给undefined赋值`);
    }
    if (isReadonly(target)) {
        __DEV__ && warn(`不能给只读的对象赋值`);
        return;
    }
    const ob = (target as any).__ob__;
    // 如果是数组 而且 是合法的下标
    if (isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key);
        // 直接使用 splice 就替换,注意这里的 splice 不是原生的,所以才可以监测到,具体看下面
        target.splice(key, 1, val);
        // ssr
        if (ob && !ob.shallow && ob.mock) {
            observe(val, false, true);
        }
        return val;
    }
    if (key in target && !(key in Object.prototype)) {
        // 如果 key 存在于 target 里,就直接赋值,也是可以监测到的
        target[key] = val;
        return val;
    }
    // 获取 target.__ob__
    if ((target as any)._isVue || (ob && ob.vmCount)) {
        __DEV__ &&
            warn(
                "避免在运行时向 Vue 实例或其根$data添加响应式属性 - 在 data 选项中预先声明它。"
            );
        return val;
    }
    // 在 Observer 里介绍过,如果没有这个属性,就说明不是一个响应式对象
    if (!ob) {
        target[key] = val;
        return val;
    }
    // 然后把新添加的属性变成响应式
    defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock);
    // 手动派发更新
    if (__DEV__) {
        ob.dep.notify({
            type: TriggerOpTypes.ADD,
            target: target,
            key,
            newValue: val,
            oldValue: undefined,
        });
    } else {
        ob.dep.notify();
    }
    return val;
}

这里主要做的是:

  • 先判断如果是数组,并且下标合法,就直接使用重写过的 splice 替换
  • 如果是对象,并且 key 存在于 target 里,就替换值
  • 如果没有 __ob__,说明不是一个响应式对象,直接赋值返回
  • 最后再把新属性变成响应式,并派发更新

重写数组方法

源码:src/core/observer/array.js

ts
import { TriggerOpTypes } from "../../v3";
import { def } from "../util/index";
// 获取数组的原型
const arrayProto = Array.prototype;
// 创建继承了数组原型的对象
export const arrayMethods = Object.create(arrayProto);
// 会改变原数组的方法列表
const methodsToPatch = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "sort",
    "reverse",
];

// 重写数组事件
methodsToPatch.forEach(function (method) {
    // 保存原本的事件
    const original = arrayProto[method];
    // 创建响应式对象
    def(arrayMethods, method, function mutator(...args) {
        const result = original.apply(this, args);
        const ob = this.__ob__;
        let inserted;
        switch (method) {
            case "push":
            case "unshift":
                inserted = args;
                break;
            case "splice":
                inserted = args.slice(2);
                break;
        }
        if (inserted) ob.observeArray(inserted);
        // 派发更新
        if (__DEV__) {
            ob.dep.notify({
                type: TriggerOpTypes.ARRAY_MUTATION,
                target: this,
                key: method,
            });
        } else {
            ob.dep.notify();
        }
        // 做完我们需要的处理后,再执行原本的事件
        return result;
    });
});

这里做的主要是:

  • 保存会改变数组的方法列表
  • 当执行列表里有的方法的时候,比如 push,先把原本的 push 保存起来,再做响应式处理,再执行这个方法

总结

大家如果将这篇文章看完之后,应该就能大致理解这个响应式的原理和流程了,我现在以面试回答的口吻来梳理一下吧

Vue2 的响应式原理依赖于 Obeject.defineProperty 实现,有几个核心部件分别为:Observer、Dep、Watcher、Scheduler,Observer 负责把一个数据变成响应式数据,Dep 负责将其收集记录依赖并且方便通知更新, Watcher 存储更新后要执行的更新函数,Scheduler 负责将 Watcher 放到一个任务队列中,可以避免修改一个对象的不同属性时导致多次更新,其中 Observer 最核心的拦截函数 defineReactive 就是依赖于 Object.defineProperty 实现。

Vue2 的响应式流程:先是 init 时将 Prop 和 Data 调用 defineReactive 和 observe 来将其变成响应式数据,然后在挂载的时候会 new 一个 watcher 类,然后在渲染时触发了数据对象的 getter,然后调用 Dep 类和 Watcher 类关于依赖收集的相关函数,之后在每次数据更新的时候都会触发数据对象的 setter,就会调用 Dep 类和 Watcher 类关于更新的相关函数,但这个函数不会马上更新,而是会放入 Scheduler 部件待 nextTick 后再调用。

参考文献

备案号:闽ICP备2024028309号-1