vue 变化侦测原理

关于我

刘博文

360导航 - 高级前端工程师

前奇舞团成员

95后

源码中比较重要的几个部分

整体逻辑

变化侦测

模板编译

virtualDOM

How Changes Are Tracked

Workflow

变化侦测

Observer

Dep

Watcher

如何侦测变化

  • defineProperty
  • proxy

Observer

注入getter和setter

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

function defineReactive (data, key, val) {
    // 递归观测子属性
    observer(val)

    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            return val
        },
        set: function (newVal) {
            if(val === newVal){
                return
            }
            // 对新值进行观测
            observer(newVal)
            val = newVal
        }
    })
}

观察所有数据

怎么观察?

getter时,收集依赖

setter时,触发依赖

收集的依赖存哪?

存 Dep 中

Dep

专门用来存储依赖用的

每个属性都有一个Dep

Dep 实现

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 () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
function defineReactive (data, key, val) {
    observer(val)
    let dep = new Dep()        // 新增
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.addSub()    // 新增
            return val
        },
        set: function (newVal) {
            if(val === newVal){
                return
            }
            observer(newVal)
            dep.notify()    // 新增
            val = newVal
        }
    })
}

收集谁????

当数据发生变化通知谁??

Watcher

watcher 是变化侦测和更新view的一个纽带

function defineReactive (data, key, val) {
    observer(val)
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            if (Dep.target) { // 新增
                dep.depend()  // 修改
            }
            return val
        },
        set: function (newVal) {
            if(val === newVal){
                return
            }
            observer(newVal)
            dep.notify()
            val = newVal
        }
    })
}

Dep.target 就是watcher实例

watcher

// keypath
vm.$watch('a.b.c', function (newVal, oldVal) {
  // do something
})

watcher

class Watch {
    constructor (expOrFn, cb) {
        this.getter = parsePath(expOrFn)
        this.cb = cb
        this.value = this.get()
    }

    get () {
        pushTarget(this)
        value = this.getter.call(vm, vm)
        popTarget()
    }

    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)
            }
        }
    }

    update () {
        const value = this.get()
        this.cb.call(this.vm, value, oldValue)
    }
}
export function pushTarget (_target: Watcher) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
}
export function popTarget () {
    Dep.target = targetStack.pop()
}

结论

watcher主动将自己 push 到属性的依赖中

// keypath
vm.$watch('a.b.c', function (newVal, oldVal) {
  // do something
})

主动将 watcher push到 a.b.c 的依赖中

结论2

只有通过watcher读取属性的值才会收集依赖

模板是通过watcher读取属性的值的~

watcher 是变化侦测和更新view的一个纽带

Array怎么进行变化侦测?

defineProperty 侦测不了Array

monkey patch

  1. 在运行时替换方法、属性等
  2. 在不修改第三方代码的情况下增加原来不支持的功能
  3. 在运行时为内存中的对象增加patch而不是在磁盘的源代码中增加

vue 对Array的补丁

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

/**
 * Intercept mutating methods and emit events
 */
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original 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
  })
})

arrayMethods

  • 使用数组方法时会触发 ob.dep.notify() 来告诉watcher 数据变啦
  • push unshift splice (这种可以新增数组元素的方法),需要把新增的元素进行 new Observer(item) 来进行变化检测

arrayMethods 是怎么生效的?

我们平时都是直接操作数组原生方法的

array.__proto__ = arrayMethods

当数据丢进 Observer 时,如果发现是Array

const keys = Object.getOwnPropertyNames(arrayMethods)

for (let i = 0, l = keys.length; i < l; i++) {
  const key = keys[i]
  Object.defineProperty(target, key, {
    value: arrayMethods[key],
    enumerable: false,
    writable: true,
    configurable: true
  })
}

如果数据没有 __proto__ 属性

总结

Title Text

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin urna odio, aliquam vulputate faucibus id, elementum lobortis felis. Mauris urna dolor, placerat ac sagittis quis.

Title Text

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin urna odio, aliquam vulputate faucibus id, elementum lobortis felis. Mauris urna dolor, placerat ac sagittis quis.

Thanks

vue 变化侦测原理

By 刘博文

vue 变化侦测原理

关于 vue 的变化检测原理的分享~

  • 5,218