vue响应式数据、依赖收集更新、watch/computed原理

文章内容

  1. 响应式原理相关functionclass的讲解
  2. Object数据类型的响应式初始化和特殊更新模式
  3. Array数据类型的响应式初始化和特殊更新模式
  4. 渲染Watcher的依赖收集和派发更新分析:流程图
  5. computed类型的依赖收集和派发更新分析:流程图和源码分析
  6. watch类型的的依赖收集和派发更新分析:源码分析

响应式原理初始化

响应式数据设置代理

  • 访问props的item对应的key时,使用this.[key]会自动代理到vm._props.[key]
  • 访问data的item对应的key1时,使用this.[key1]会自动代理到vm._data.[key1]
scss 复制代码
function initProps(vm: Component, propsOptions: Object) {
    for (const key in propsOptions) {
        if (!(key in vm)) {
            proxy(vm, `_props`, key)
        }
    }
}
ini 复制代码
function initData(vm: Component) {
    let data = vm.$options.data
    data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {};
    const keys = Object.keys(data)
    const props = vm.$options.props
    const methods = vm.$options.methods
    let i = keys.length
    while (i--) {
        const key = keys[i]
        // 监测props是否已经有这个key了,有的话弹出警告
        proxy(vm, `_data`, key)
    }
}
vbnet 复制代码
export function proxy(target: Object, sourceKey: string, key: string) {
    sharedPropertyDefinition.get = function proxyGetter() {
        return this[sourceKey][key]
    }
    sharedPropertyDefinition.set = function proxySetter(val) {
        this[sourceKey][key] = val
    }
    Object.defineProperty(target, key, sharedPropertyDefinition)
}

Vue.props响应式数据设置

在合并配置mergeOptions()中,会调用normalizeProps()对props的数据进行整理,最终确保initPros调用时props已经是一个对象,因此不需要Observer判断是否是数组,直接对key进行defineReactive即可

vbnet 复制代码
function initProps(vm: Component, propsOptions: Object) {
    const propsData = vm.$options.propsData || {}
    const props = vm._props = {}
    const keys = vm.$options._propKeys = []

    for (const key in propsOptions) {
        keys.push(key)
        const value = validateProp(key, propsOptions, propsData, vm)
        defineReactive(props, key, value)
    }
}

Vue.data响应式数据设置

  • data建立一个Observer,主要功能是根据value类型判断,是数组则递归调用observe,为每一个item都创建一个Observer对象,如果是对象,则遍历key,为每一个key都创建响应式监听
javascript 复制代码
function initData(vm: Component) {
    let data = vm.$options.data
    data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
    // observe data
    observe(data, true /* asRootData */)
}

export function observe(value: any, asRootData: ?boolean): Observer | void {
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    // ... 判断数据value是否已经设置响应式过
    let ob = new Observer(value)
    return ob
}
typescript 复制代码
export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that have this object as root $data

    constructor(value: any) {
        this.value = value
        this.dep = new Dep()
        if (Array.isArray(value)) {
            this.observeArray(value)
        } else {
            this.walk(value)
        }
    }

    walk(obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }

    observeArray(items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i])
        }
    }
}

Object.defineProperty响应式基础方法

  • get:返回对应key的数据 + 依赖收集
  • set:设置对应key的数据+派发更新
vbnet 复制代码
export function defineReactive(obj: Object, key: string, val: any, ...args) {
    const dep = new Dep()
    let childOb = !shallow && observe(val) // 如果val也是object

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    // key对应的val是Object,当val里面的key发生改变时
                    // 即obj[key][key1]=xxx
                    // 也会通知目前obj[key]收集的Watcher的更新
                    childOb.dep.depend()
                    if (Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            childOb = !shallow && observe(newVal)
            dep.notify()
        }
    })
}

Dep响应式依赖管理类

  • 每一个key都有一个Dep管理类
  • Dep具备addSub,即关联Watcher(渲染Watcher或者其它)的能力
  • Dep具备depend(),被Watcher显式关联,可以被Watcher触发dep.notify()通知它关联Watcher更新的能力
scss 复制代码
Dep.target = null
const targetStack = []
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

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

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Watcher响应式依赖收集和派发更新执行类

  • get()方法进行pushTarget(this),触发对应的getter回调,开始收集,然后popTarget(this),停止收集,最后触发cleanupDeps()进行依赖的更新
  • update()将更新内容压入队列中,然后根据顺序调用Watcher.run(),也就是回调constructor()传进来的this.cb方法
kotlin 复制代码
export default class Watcher {
    constructor(...args) {
        this.vm = vm
        if (isRenderWatcher) {
            vm._watcher = this
        }
        vm._watchers.push(this)
        this.cb = cb; // 触发更新时调用的方法
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        this.value = this.lazy
            ? undefined
            : this.get()
    }

    get() {
        pushTarget(this)
        let value
        const vm = this.vm
        value = this.getter.call(vm, vm)
        if (this.deep) {
            traverse(value)
        }
        popTarget()
        this.cleanupDeps()
        return value
    }

    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)) {
                dep.addSub(this)
            }
        }
    }

    cleanupDeps() {
        let i = this.deps.length
        while (i--) {
            const dep = this.deps[i]
            if (!this.newDepIds.has(dep.id)) {
                dep.removeSub(this)
            }
        }
        let tmp = 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
    }


    update() {
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
            this.run()
        } else {
            queueWatcher(this)
        }
    }

   
    run() {
        if (this.active) {
            const value = this.get()
            if (value !== this.value || isObject(value) || this.deep) {
                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)
                }
            }
        }
    }

    depend() {
        let i = this.deps.length
        while (i--) {
            this.deps[i].depend()
        }
    }
}

Object数据类型响应式

最外一层key的响应式设置

使用observe()对每一个Objectkey都进行Object.defineProperty()劫持

ini 复制代码
function observe(value, asRootData) {
    ob = new Observer(value);
    return ob
}

var Observer = function Observer(value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    this.walk(value);
};
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}
vbnet 复制代码
export function defineReactive(obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            dep.notify()
        }
    })
}

深度key的响应式设置

vbnet 复制代码
export function defineReactive(obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
    const dep = new Dep()
    let childOb = !shallow && observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                    if (Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            childOb = !shallow && observe(newVal)
            dep.notify()
        }
    })
}
  • 由上面对observe()方法的分析,它会遍历Object的每一个key,进行Object.defineProperty声明
  • 对于Object每一个key对应的value,如果childOb = !shallow && observe(val)不为空,那么它会遍历value对应的每一个key,如果value[key]也是一个Object,那么会再次走到childOb = !shallow && observe(val),直到所有Object都为响应式数据为止
  • 对于obj[key]来说,会调用dep.depend(),如果obj[key]本身也是一个对象,即childOb不为空,那么它就会调用childOb.dep.depend(),因此当obj[key][key1]=xx时,也会触发dep.depend()收集的Watcher发生更新,例如
css 复制代码
data: {
  parent: {
    children: {test: "111"}
  }
}


<div>{{parent.children}}</div>

由上面的分析可以知道,当this.parent.children.test发生变化时,会触发this.parent.children收集的渲染Watcher发生变化,从而触发界面重新渲染

额外添加key

由于Object.defineProperty()的限制,无法实现对Object新增key的响应式监听,因此当我们想要为Object设置新的key的时候,需要调用Vue.set方法

kotlin 复制代码
export function set(target: Array<any> | Object, key: any, val: any): any {
    if (key in target && !(key in Object.prototype)) {
        target[key] = val;
        return val;
    }
    const ob = (target: any).__ob__;
    if (!ob) {
        target[key] = val;
        return val;
    }
    defineReactive(ob.value, key, val);
    ob.dep.notify();
    return val;
}

Vue.set()的流程可以总结为:

  • Object增加对应的keyvalue数据

  • 将新增的key加入响应式监听中,如果key对应的value也是Object,按照上面深度key的监听设置分析,会递归调用observe进行深度key的响应式设置

  • 手动触发Object收集的Watcher的刷新操作

    本质上,上面的三步流程除了第二步有略微差别之外,其它部分跟defineReactive中的set()方法流程一致

删除key

删除key也无法触发响应式的变化,需要手动调用Vue.del()方法:

  • 删除Object指定的key
  • 手动触发Object收集的Watcher的刷新操作
typescript 复制代码
function del(target: Array<any> | Object, key: any) {
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.splice(key, 1)
        return
    }
    const ob = (target: any).__ob__
    if (!hasOwn(target, key)) {
        return
    }
    delete target[key]
    if (!ob) {
        return
    }
    ob.dep.notify()
}

Array数据类型响应式

前置说明

根据官方文档说明,Vue 不能检测以下数组的变动

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

举个例子:

csharp 复制代码
var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也将在响应式系统内触发状态更新

scss 复制代码
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)

// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

为了解决第二类问题,你可以使用 splice:

scss 复制代码
vm.items.splice(newLength)

对Array[index]数据的响应式监听

如果item=Array[index]Object数据,使用observe()Array的每一个item都进行响应式的声明

scss 复制代码
function observe(value, asRootData) {
    ob = new Observer(value);
    return ob
}

var Observer = function Observer(value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
        if (hasProto) {
            protoAugment(value, arrayMethods)
        } else {
            copyAugment(value, arrayMethods, arrayKeys)
        }
        this.observeArray(value)
    }
};

observeArray(items: Array < any >) {
    for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])
    }
}

Vue.set更新Array-item

从下面代码可以看出,Vue.set()更新数组的item本质上也是调用Array.splice()方法

typescript 复制代码
export function set(target: Array<any> | Object, key: any, val: any): any {
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
    }
}

Array.splice更新Array-item

从上面的分析可以知道,一开始会触发new Observer(value)的初始化

从下面代码可以知道,大部分浏览器会触发protoAugment()方法,也就是改变Array.__proto__

javascript 复制代码
var Observer = function Observer(value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
        if (hasProto) {
            protoAugment(value, arrayMethods)
        } else {
            copyAugment(value, arrayMethods, arrayKeys)
        }
        this.observeArray(value)
    }
};

function protoAugment (target, src: Object) {
  target.__proto__ = src
}
// node_modules/vue/src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

而改变了Array.__proto__多少方法呢?

ini 复制代码
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
javascript 复制代码
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)
        // notify change
        ob.dep.notify()
        return result
    })
})
// node_modules/vue/src/core/util/lang.js
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}

从上面代码分析可以知道,Vue劫持了Array'push','pop','shift', 'unshift', 'splice', 'sort','reverse'方法,一旦运行了这些方法,会主动触发:

  • 调用Array原来的方法进行调用,然后返回Array原来的方法的返回值,如Array.push调用后的返回值
  • 进行observeArray的响应式设置,更新新设置的item(可能为Object,需要设置响应式)
  • 手动触发ob.dep.notify(),触发对应的Watcher更新,达到响应式自动更新的目的

渲染Watcher依赖收集流程分析

仅仅分析最简单的渲染Watcher依赖收集的流程,实际上并不是只有渲染Watcher一种

渲染Watcher派发更新流程分析

computed依赖收集和派发更新分析

测试代码

vbnet 复制代码
<div>{{myName}}</div>

// { [key: string]: Function | { get: Function, set: Function } }
computed: {
  myName: function() {
    // 没有set()方法,只有get()方法
    return this.firstName + this.lastName;
  }
}

依赖收集流程图分析

依赖收集代码分析

computedWatcher初始化

Vue.prototype._init初始化时,会调用initState()->initComputed(),从而进行computed数据的初始化

vbnet 复制代码
// node_modules/vue/src/core/instance/state.js
function initComputed(vm: Component, computed: Object) {
    const watchers = vm._computedWatchers = Object.create(null)

    for (const key in computed) {
        const userDef = computed[key];
        const getter = typeof userDef === 'function' ? userDef : userDef.get;
        watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            computedWatcherOptions //{ lazy: true }
        )
        defineComputed(vm, key, userDef);
    }
}

从上面代码可以知道,最终为每一个computed监听的数据建立一个Watcher,一个数据对应一个computed Watcher,传入{ lazy: true },然后调用defineComputed()方法

vbnet 复制代码
export function defineComputed(target: any, key: string, userDef: Object | Function) {
    // 为了减少分支判断,方便理解,统一假设userDef传入Function
    sharedPropertyDefinition.get = createComputedGetter(key);
    sharedPropertyDefinition.set = noop;
    Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate()
            }
            if (Dep.target) {
                watcher.depend()
            }
            return watcher.value
        }
    }
}

从上面代码可以知道,最终defineComputed是进行了Object.defineProperty的数据劫持,一般在computed中都只写get()方法,即

kotlin 复制代码
computed: {
  myName: function() {
    // 没有set()方法,只有get()方法
    return this.firstName + this.lastName;
  }
}

而回到上面代码的分析,defineComputed劫持了computedget()方法,最终返回watcher.value

渲染Watcher触发ComputedWatcher的get()方法执行

当界面上<template>{myName}</template>渲染myName的时候,会触发myNameget()方法,由于Object.defineProperty的数据劫持,会先调用

  • watcher.evaluate()->watcher.get()(从下面的代码可以得出这样的推导关系)
  • watcher.depend()
kotlin 复制代码
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
    if (watcher.dirty) {
        // evaluate () {
        //     this.value = this.get()
        //     this.dirty = false
        // }
        watcher.evaluate()
    }
    if (Dep.target) {
        // depend() {
        //     let i = this.deps.length
        //     while (i--) {
        //         this.deps[i].depend()
        //     }
        // }
        watcher.depend()
    }
    return watcher.value
}
kotlin 复制代码
// watcher.js
get() {
   //  function pushTarget (target: ?Watcher) {
   //    targetStack.push(target)
   //    Dep.target = target
        // }
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
        // this.getter = return this.firstName + this.lastName;
        value = this.getter.call(vm, vm);
    } catch (e) {} 
    finally {
        if (this.deep) { // watch类型的watcher才能配置这个参数
            traverse(value);
        }
        popTarget();
        this.cleanupDeps();
    }
    return value;
}

从上面的代码可以知道,当调用watcher.evaluate()->watcher.get()的时候,会调用:

  • pushTarget(this):将目前的Dep.target 切换到Computed Watcher
  • this.getter.call(vm, vm):触发this.firstName对应的get()方法和this.lastName对应的get()方法。由下面的依赖收集代码可以知道,此时this.firstNamethis.lastName持有的Dep会进行dep.addSub(this),收集该Computed Watcher
  • popTarget():将目前的Dep.target恢复到上一个状态
  • cleanupDeps():更新Computed Watcher的所有依赖关系,将无效的依赖关系删除(比如v-if造成的依赖关系不用再依赖)
  • 最终返回myName= return this.firstName + this.lastName;

watcher.evaluate():求值 + 更新依赖 + 将涉及到的响应式对象firstName和lastName关联到Computed Watcher

javascript 复制代码
export function defineReactive(obj: Object, key: string, val: any, ...args) {
    const dep = new Dep()
    let childOb = !shallow && observe(val)

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                    if (Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        }
    })
}

// Dep.js
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

// watcher.js
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)) {
            dep.addSub(this)
        }
    }
}

回到myNameget()方法,即下面的代码,我们刚刚分析了watcher.evaluate(),那么我们接下来还调用了myNamewatcher.depend()

我们从上面的代码知道,这个方法主要是用来收集依赖的,此时的Dep.target渲染Watchercomputed Watcher会进行自身的depend(),本质是拿出自己所有记录的Dep(为了方便理解,我们理解Dep就是一个响应式对象的代理)computed Watcher拿出自己记录的所有的deps[i],然后调用它们的depend()方法,从而完成这些响应式对象(firstNamelastName)与渲染Watcher的关联,最后返回watcher.value

scss 复制代码
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
    if (watcher.dirty) {
        // 上面分析触发了watcher.get()方法
        // 得到对应的watcher.value
        // 收集了firstName+lastName和computerWatcher的绑定
        watcher.evaluate();
        // 将目前的Dep.target切换到渲染Watcher
    }
    if (Dep.target) {
        // depend() {
        //     let i = this.deps.length
        //     while (i--) {
        //         this.deps[i].depend()
        //     }
        // }
        watcher.depend()
    }
    return watcher.value
}

// watcher.js
depend() {
    // this.deps是从cleanupDeps()中
    // this.deps = this.newDeps来的
    // this.newDeps是通过addDep()来的
    let i = this.deps.length
    while (i--) {
        this.deps[i].depend()
    }
}

// Dep.js
depend() {
    if (Dep.target) {
        Dep.target.addDep(this)
    }
}

派发更新流程图分析

派发更新代码分析

kotlin 复制代码
computed: {
  myName: function() {
    // 没有set()方法,只有get()方法
    return this.firstName + this.lastName;
  }
}

this.firstName发生改变时,会触发this.firstName.dep.subs.notify()功能,也就是触发刚刚注册的两个Watcher: 渲染WatcherComputed Watcher,首先触发的是Computed Watchernotify()方法,由下面的代码可以知道,只执行this.dirty=true

kotlin 复制代码
  update () {
    // Computed Watcher的this.lazy都为true
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

然后触发渲染Watcher,触发整个界面进行渲染,从而触发该computed[key]get()方法执行,也就是myNameget()方法执行,由依赖收集的代码可以知道,最终执行为

kotlin 复制代码
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
    if (watcher.dirty) {
        // 上面分析触发了watcher.get()方法
        // 得到对应的watcher.value
        watcher.evaluate();
    }
    if (Dep.target) {
        // depend() {
        //     let i = this.deps.length
        //     while (i--) {
        //         this.deps[i].depend()
        //     }
        // }
        watcher.depend()
    }
    return watcher.value
}

从上面的分析可以知道,computed[key]get()先收集了一波依赖:

  • watcher.evaluate():求值watcher.value + 更新依赖 + 将涉及到的响应式对象关联到Computed Watcher
  • watcher.depend():将涉及到的响应式对象关联到当前的Dep.target,即渲染Watcher

然后返回了对应的值watcher.value

computedWatcher一般无set方法,因此触发派发更新就是触发渲染Watcher/其它Watcher持有computed进行重新渲染,从而触发computed的get方法,收集最新依赖以及获取最新值

watch依赖收集和派发更新分析

watch流程图跟computed流程大同小异,因此watch只做源码分析

测试代码

watch支持多种模式的监听方式,比如传入一个回调函数,比如传入一个方法名称,比如传入一个Object,配置参数

php 复制代码
// { [key: string]: string | Function | Object | Array }
watch: {
    a: function (val, oldVal) {},
    b: 'someMethod', // 方法名
    c: {
      handler: function (val, oldVal) {}, // 值改变时的回调方法
      deep: true, // 深度遍历
      immediate: true // 马上回调一次
    },
    // 你可以传入回调数组,它们会被逐一调用
    e: [
      'handle1', // 方式1
      function handle2 (val, oldVal) {}, // 方式2
            { // 方式3
        handler: function (val, oldVal) {},
        deep: true,
        immediate: true
        },
    ],
    // watch vm.e.f's value: {g: 5}
    'e.f': function (val, oldVal) {}
}

初始化watch

scss 复制代码
export function initState(vm: Component) {
    if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch);
    }
}
function initWatch(vm: Component, watch: Object) {
    for (const key in watch) {
        const handler = watch[key];
        // 处理watch:{b: [三种形式都允许]}的形式
        if (Array.isArray(handler)) {
            for (let i = 0; i < handler.length; i++) {
                createWatcher(vm, key, handler[i]);
            }
        } else {
            createWatcher(vm, key, handler);
        }
    }
}
function createWatcher(vm: Component, expOrFn: string | Function, handler: any, options?: Object) {
    if (isPlainObject(handler)) {
        // 处理watch:{b: {handler: 处理函数, deep: true, immediate: true}}的形式
        options = handler
        handler = handler.handler
    }
    if (typeof handler === 'string') {
        // 处理watch: {b: 'someMethod'}的形式
        handler = vm[handler]
    }
    return vm.$watch(expOrFn, handler, options)
}

从上面的代码可以看出,初始化时,会进行watch中各种参数的处理,将3种不同类型的watch回调模式整理成为规范的模式,最终调用Vue.prototype.$watch进行new Watcher的构建

javascript 复制代码
Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options?: Object): Function {
    const vm: Component = this
    // cb是回调方法,如果还是对象,则使用createWatcher拆出来里面的对象
    if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
    }
    options.user = true
    // 建立一个watch类型的Watcher
    // expOrFn: getter
    // cb: 注册的回调
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
        // options={immediate:true}的分支逻辑
        pushTarget()
        invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
        popTarget()
    }
    return function unwatchFn() {
        watcher.teardown()
    }
}

依赖收集代码分析

新建Watcher的时候, 在constructor()中会触发

javascript 复制代码
class watcher {
    constructor() {
    // watch的key
    this.getter = parsePath(expOrFn);
    this.value = this.lazy?undefined:this.get();
}

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

从上面的代码可以知道,最终this.getter调用的还是传入的obj[key],从下面的get()方法可以知道,赋值this.getter后,会触发get()方法,从而触发this.getter.call(vm, vm),因此最终this.getter得到的就是vm[key]

kotlin 复制代码
get() {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    if (this.deep) {
        traverse(value); // 深度遍历数组/对象,实现
    }
    popTarget()
    this.cleanupDeps()
    return value
}

// traverse.js
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

上面代码的步骤可以概括为

  • pushTarget:修复当前的Dep.target为当前的watch类型的Watcher
  • this.getter:返回当前的vm[key],同时触发vm[key]的响应式劫持get()方法,从而触发vm[key]持有的Dep对象启动dep.depend()进行依赖收集(如下面代码所示),vm[key]持有的Dep对象将当前的watch类型的Watcher收集到vm[key]中,下次vm[key]发生变化时,会触发watch类型的Watcher进行callback的回调
  • traverse(value):深度遍历,会访问每一个Object的key,由于每一个Object的key之前在initState()的时候已经使用Object.defineProperty()进行get方法的劫持,因此触发它们对应的getter方法,进行dep.depend()收集当前的watch类型的Watcher,从而实现改变Object内部深层的某一个key的时候会回调watch类型的Watcher。没有加deep=true的时候,watch类型的Watcher只能监听Object的改变,比如watch:{curData: function(){}},只有this.curData=xxx,才会触发watch,this.curData.children=xxx是不会触发的
  • popTarget:恢复Dep.target为上一个状态
  • cleanupDeps:更新依赖关系
  • 返回值value,依赖收集结束,watch类型的Watcher初始化结束
vbnet 复制代码
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
        const value = getter ? getter.call(obj) : val
        if (Dep.target) {
            dep.depend()
            if (childOb) {
                childOb.dep.depend()
                if (Array.isArray(value)) {
                    dependArray(value)
                }
            }
        }
        return value
    }
})

派发更新代码分析

watcher的值发生改变时,会触发dep.subs.notify()方法,从上面的分析可以知道,最终会调用watcher.run()方法

kotlin 复制代码
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)
            }
        }
    }
}

由于watch类型的Watcher传入了this.user=true,因此会触发invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info),将新值和旧值一起回调,比如

css 复制代码
watch: {
  myObject: function(value, oldValue) {//新值和旧值}
}

watchOptions几种模式分析

deep=true

kotlin 复制代码
// watcher.js
get() {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
        value = this.getter.call(vm, vm)
    } catch (e) {
        if (this.user) {
            handleError(e, vm, `getter for watcher "${this.expression}"`)
        } else {
            throw e
        }
    } finally {
        // "touch" every property so they are all tracked as
        // dependencies for deep watching
        if (this.deep) {
            traverse(value)
        }
        popTarget()
        this.cleanupDeps()
    }
    return value
}

get()方法中进行对象的深度key的遍历,触发它们的getter()方法,进行依赖的收集,可以实现

javascript 复制代码
watch: {
  myObject: {
    deep: true,
    handler: function(value, oldValue) {//新值和旧值}
  }
}

this.myObject.a = 2;

虽然上面的例子只是监听了myObject,但是由于加入deep=true,因此this.myObject.a也会触发watcher.run(),如下面代码所示,由于this.deep=true,因此会回调cb(value, oldValue)

kotlin 复制代码
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)
            }
        }
    }
}

immediate=true

从下面代码可以知道,当声明immediate=true的时候,初始化Watcher,会马上调用invokeWithErrorHandling(cb, vm, [watcher.value], vm, info),即cb的回调

javascript 复制代码
Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
        const info = `callback for immediate watcher "${watcher.expression}"`
        pushTarget()
        invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
        popTarget()
    }
    return function unwatchFn() {
        watcher.teardown()
    }
}

watch: {
    myObject:
    {
        immediate: true,
          handler: function() {...初始化马上触发一次}
    }
}

sync=true

如果声明了sync=true,在dep.sub.notify()中,会马上执行,如果没有声明sync=true,会推入队列中,等到下一个nextTick周期才会执行

arduino 复制代码
update() {
    /* istanbul ignore else */
    if (this.lazy) {
        this.dirty = true
    } else if (this.sync) {
        this.run()
    } else {
        queueWatcher(this)
    }
}

export function queueWatcher(watcher: Watcher) {
    const id = watcher.id
    if (has[id] == null) {
        has[id] = true
        if (!flushing) {
            queue.push(watcher)
        } else {
            // if already flushing, splice the watcher based on its id
            // if already past its id, it will be run next immediately.
            let i = queue.length - 1
            while (i > index && queue[i].id > watcher.id) {
                i--
            }
            queue.splice(i + 1, 0, watcher)
        }
        // queue the flush
        if (!waiting) {
            waiting = true

            if (process.env.NODE_ENV !== 'production' && !config.async) {
                flushSchedulerQueue()
                return
            }
            nextTick(flushSchedulerQueue)
        }
    }
}

参考转载:Vue2源码-响应式原理浅析(白边)

相关推荐
cs_dn_Jie5 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic5 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿5 小时前
webWorker基本用法
前端·javascript·vue.js
customer086 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
getaxiosluo7 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v7 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
栈老师不回家8 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙8 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
小远yyds9 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
程序媛小果9 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot