• vue中watch原理浅析


    watch 是vue提供的侦听器, 用于对 data 的属性进行监听 

    Vue 通过 watch选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的

    用法

    1. <template>
    2. <div>
    3. <button @click="add">点击</button>
    4. </div>
    5. </template>
    6. <script>
    7. export default {
    8. data() {
    9. return {
    10. i: 0
    11. };
    12. },
    13. watch: {
    14. i(newVal, oldVal) {
    15. console.log(newVal, oldVal);
    16. }
    17. },
    18. methods: {
    19. add() {
    20. this.i++;
    21. }
    22. }
    23. }
    24. </script>

    上面的例子, 使用 watch 对 data.i 进行监听, 当 data.i 发生变化时, 便会触发 watch 中的监听函数, 打印出 newVal 和 oldVal ;

    当然还有许多种用法,:

    1. watch: {
    2. // 函数
    3. a: function (val, oldVal) {
    4. console.log('new: %s, old: %s', val, oldVal)
    5. },
    6. // 方法名
    7. b: 'someMethod',
    8. // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
    9. c: {
    10. handler: function (val, oldVal) { /* ... */ },
    11. deep: true
    12. },
    13. // 该回调将会在侦听开始之后被立即调用
    14. d: {
    15. handler: 'someMethod',
    16. immediate: true
    17. },
    18. // 你可以传入回调数组,它们会被逐一调用
    19. e: [
    20. 'handle1',
    21. function handle2 (val, oldVal) { /* ... */ },
    22. {
    23. handler: function handle3 (val, oldVal) { /* ... */ },
    24. /* ... */
    25. }
    26. ],
    27. // watch vm.e.f's value: {g: 5}
    28. 'e.f': function (val, oldVal) { /* ... */ }
    29. }

    源码解析

    vue在 initState 中执行 initWatch 方法注册 watch:

    1. function initState (vm: Component) {
    2. vm._watchers = []
    3. const opts = vm.$options
    4. if (opts.props) initProps(vm, opts.props)
    5. if (opts.methods) initMethods(vm, opts.methods)
    6. if (opts.data) {
    7. initData(vm)
    8. } else {
    9. observe(vm._data = {}, true /* asRootData */)
    10. }
    11. if (opts.computed) initComputed(vm, opts.computed)
    12. if (opts.watch && opts.watch !== nativeWatch) {
    13. initWatch(vm, opts.watch)
    14. }
    15. }

    顺着 initWatch 往下看:

    1. function initWatch (vm: Component, watch: Object) {
    2. for (const key in watch) {
    3. const handler = watch[key]
    4. if (Array.isArray(handler)) {
    5. for (let i = 0; i < handler.length; i++) {
    6. createWatcher(vm, key, handler[i])
    7. }
    8. } else {
    9. createWatcher(vm, key, handler)
    10. }
    11. }
    12. }

    initWatch 函数对 watch 对象进行遍历, 当对象的属性值为数组时, 对数组进行遍历执行 createWatcher 方法, 如果对象的属性值不为数组, 则直接执行 createWatcher 方法:

    1. function createWatcher (
    2. vm: Component,
    3. expOrFn: string | Function,
    4. handler: any,
    5. options?: Object
    6. ) {
    7. if (isPlainObject(handler)) {
    8. options = handler
    9. handler = handler.handler
    10. }
    11. if (typeof handler === 'string') {
    12. handler = vm[handler]
    13. }
    14. return vm.$watch(expOrFn, handler, options)
    15. }

    isPlainObject 方法用于判断参数是否为 "object":

    1. function isPlainObject (obj: any): boolean {
    2. return _toString.call(obj) === '[object Object]'
    3. }

    当 handler 为对象时(指 object), 便执行: options = handler , handler = handler.handler; 即这种情况:

    1. watch: {
    2. c: {
    3. handler: function (val, oldVal) { /* ... */ },
    4. deep: true
    5. }
    6. }

    若是 handler 为字符串, 便执行 handler = vm[handler]; 即这种情况:

    1. watch: {
    2. b: 'someMethod'
    3. }

    经过以上两步操作, 这时候的 handler 为我们的监听函数, 当然有特殊情况, 也就是 handler 原先是一个对象, 对象的 handler 也是对象的情况, 这里我们先不讨论, 接着往下看; createWatch 最后返回 vm.$watch(expOrFn, handler, options) , 我们再追踪一下 $watch :

    1. Vue.prototype.$watch = function (
    2. expOrFn: string | Function,
    3. cb: any,
    4. options?: Object
    5. ): Function {
    6. const vm: Component = this
    7. if (isPlainObject(cb)) {
    8. return createWatcher(vm, expOrFn, cb, options)
    9. }
    10. options = options || {}
    11. options.user = true
    12. const watcher = new Watcher(vm, expOrFn, cb, options)
    13. if (options.immediate) {
    14. try {
    15. cb.call(vm, watcher.value)
    16. } catch (error) {
    17. handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    18. }
    19. }
    20. return function unwatchFn () {
    21. watcher.teardown()
    22. }
    23. }
    24. }

    $watch 是 Vue 原型链上的一个方法, 首先判断传入的 cb 参数, 也就是上面的 handler , 当 cb 为一个 对象 (指Object, 而非Function)时, 重新执行 createWatcher , 这里也就解决了前面刚刚说到的: handler 是对象的问题; 接着将 options.user 设置为 true , 并创建一个 watcher ; 接着, 根据 options.immediate 是否为 true 决定是否立即执行 cb 函数, 并将 watcher.value 作为 cb 的参数传入, 这便是以下的 watch 语法的具体实现:

    1. // 该回调将会在侦听开始之后被立即调用
    2. {
    3. watch: {
    4. d: {
    5. handler: 'someMethod',
    6. immediate: true
    7. }
    8. }
    9. }

    回到 watcher 上面来, 这是实现数据监听的核心部分; watcher 的构造函数为 Watcher , 先从 Watcher 的构造函数进行解析, 以下省略了无关代码:

    1. constructor (
    2. vm: Component,
    3. expOrFn: string | Function,
    4. cb: Function,
    5. options?: ?Object,
    6. isRenderWatcher?: boolean
    7. ) {
    8. this.vm = vm
    9. vm._watchers.push(this)
    10. // options
    11. if (options) {
    12. this.deep = !!options.deep
    13. this.user = !!options.user
    14. }
    15. this.cb = cb
    16. this.active = true
    17. this.expression = process.env.NODE_ENV !== 'production'
    18. ? expOrFn.toString()
    19. : ''
    20. // parse expression for getter
    21. if (typeof expOrFn === 'function') {
    22. this.getter = expOrFn
    23. } else {
    24. this.getter = parsePath(expOrFn)
    25. if (!this.getter) {
    26. this.getter = noop
    27. }
    28. }
    29. this.value = this.lazy
    30. ? undefined
    31. : this.get()
    32. }

    分别将 options.deep 和 options.user 赋值给 this.deep 和 this.user, cb 函数赋值给 this.cb ; 判断 expOrFn 的类型, expOrFn 是watch 的 key , 因此我们默认为字符串类型, 使用 parsePath 进行转换后再赋值给 this.getter; 以下为 parsePath :

    1. const unicodeLetters = 'a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD'
    2. const bailRE = new RegExp(`[^${unicodeLetters}.$_\\d]`)
    3. function parsePath (path: string): any {
    4. if (bailRE.test(path)) {
    5. return
    6. }
    7. const segments = path.split('.')
    8. return function (obj) {
    9. for (let i = 0; i < segments.length; i++) {
    10. if (!obj) return
    11. obj = obj[segments[i]]
    12. }
    13. return obj
    14. }
    15. }

    parsePath 传入一个参数 path , 使用 String.prototype.split 对 path 进行处理, 以 . 为分隔点生成一个数组 segments , 最后返回一个函数, 函数执行时会对传入参数 obj 进行多层级属性访问, 最后返回一个属性值; 举个例子, 假设 path 为 "a.b.c", 那么函数执行时会先访问 obj.a , 再访问 obj.a.b , 最后访问 obj.a.b.c ,并返回 obj.a.b.c , 这是一个非常巧妙的设计, 后面会讲到; 回到 Watcher 的构造函数, 经过前面的折腾, 此时 this.getter 得到一个函数作为值; 接着执行以下代码:

    1. this.value = this.lazy
    2. ? undefined
    3. : this.get()

    this.lazy 为 false , 执行 this.get() 获取值, 并将值缓存在 this.value 中; so, 接着看 get 方法:

    1. get () {
    2. pushTarget(this)
    3. let value
    4. const vm = this.vm
    5. try {
    6. value = this.getter.call(vm, vm)
    7. } catch (e) {
    8. if (this.user) {
    9. handleError(e, vm, `getter for watcher "${this.expression}"`)
    10. } else {
    11. throw e
    12. }
    13. } finally {
    14. // "touch" every property so they are all tracked as
    15. // dependencies for deep watching
    16. if (this.deep) {
    17. traverse(value)
    18. }
    19. popTarget()
    20. this.cleanupDeps()
    21. }
    22. return value
    23. }

    pushTarget(this) 的作用是将当前 watcher 设置为 Dep.target ; Dep.target 是一个储存 watcher 的全局变量, 这里不作细讲, 只需要知道就好; 接着执行 this.getter.call(vm, vm) , 对 vm 的属性进行层级访问, 触发 data 中目标属性的 get 方法, 触发属性对应的 dep.depend 方法, 进行依赖收集;

    1. depend () {
    2. if (Dep.target) {
    3. Dep.target.addDep(this)
    4. }
    5. }

    Dep.target 为当前的 watcher , 因此代码可以理解为: watcher.addDep(this) :

    1. addDep (dep: Dep) {
    2. const id = dep.id
    3. if (!this.newDepIds.has(id)) {
    4. this.newDepIds.add(id)
    5. this.newDeps.push(dep)
    6. if (!this.depIds.has(id)) {
    7. dep.addSub(this)
    8. }
    9. }
    10. }

    反复横跳, 执行 dep.addSub(this) , 将 watcher 加入 dep.subs 列表中:

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

    上面便是依赖收集的全过程, 接着回到前面的代码中: 如果 this.deep 为 true , 也就是 watch 中设置深层监听, 会执行 traverse 对 value 进行更加深层的判断:

    1. if (this.deep) {
    2. traverse(value)
    3. }

    traverse的代码如下:

    1. function traverse (val: any) {
    2. _traverse(val, seenObjects)
    3. seenObjects.clear()
    4. }
    5. function _traverse (val: any, seen: SimpleSet) {
    6. let i, keys
    7. const isA = Array.isArray(val)
    8. if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    9. return
    10. }
    11. if (val.__ob__) {
    12. const depId = val.__ob__.dep.id
    13. if (seen.has(depId)) {
    14. return
    15. }
    16. seen.add(depId)
    17. }
    18. if (isA) {
    19. i = val.length
    20. while (i--) _traverse(val[i], seen)
    21. } else {
    22. keys = Object.keys(val)
    23. i = keys.length
    24. while (i--) _traverse(val[keys[i]], seen)
    25. }
    26. }

    当 data 的属性发生变动时, 触发属性的 set 方法, 执行属性对应的 dep.notify 方法, 通知收集的所有 watcher , 执行 watcher.update 方法进行更新:

    1. update () {
    2. /* istanbul ignore else */
    3. if (this.lazy) {
    4. this.dirty = true
    5. } else if (this.sync) {
    6. this.run()
    7. } else {
    8. queueWatcher(this)
    9. }
    10. }

    执行 queueWatcher 方法, 进行 dom 更新, 但这里的重点不在于 dom 更新, 顺着代码往下看:

    1. function queueWatcher (watcher: Watcher) {
    2. const id = watcher.id
    3. if (has[id] == null) {
    4. has[id] = true
    5. if (!flushing) {
    6. queue.push(watcher)
    7. } else {
    8. // if already flushing, splice the watcher based on its id
    9. // if already past its id, it will be run next immediately.
    10. let i = queue.length - 1
    11. while (i > index && queue[i].id > watcher.id) {
    12. i--
    13. }
    14. queue.splice(i + 1, 0, watcher)
    15. }
    16. // queue the flush
    17. if (!waiting) {
    18. waiting = true
    19. if (process.env.NODE_ENV !== 'production' && !config.async) {
    20. flushSchedulerQueue()
    21. return
    22. }
    23. nextTick(flushSchedulerQueue)
    24. }
    25. }
    26. }

    最终执行 nextTick(flushSchedulerQueue) , 这里不对 nextTick 细化了, 只需要理解为在当前事件循环结束调用了 flushSchedulerQueue 方法, 所以我们看一下 flushSchedulerQueue :

    1. function flushSchedulerQueue () {
    2. // ...省略
    3. queue.sort((a, b) => a.id - b.id)
    4. for (index = 0; index < queue.length; index++) {
    5. watcher = queue[index]
    6. watcher.run()
    7. }
    8. // ...省略
    9. }

    关键的一句: watcher.run() , 是的, 我们再横跳回 watcher.run 中看看:

    1. run () {
    2. if (this.active) {
    3. const value = this.get()
    4. if (
    5. value !== this.value ||
    6. // Deep watchers and watchers on Object/Arrays should fire even
    7. // when the value is the same, because the value may
    8. // have mutated.
    9. isObject(value) ||
    10. this.deep
    11. ) {
    12. // set new value
    13. const oldValue = this.value
    14. this.value = value
    15. if (this.user) {
    16. try {
    17. this.cb.call(this.vm, value, oldValue)
    18. } catch (e) {
    19. handleError(e, this.vm, `callback for watcher "${this.expression}"`)
    20. }
    21. } else {
    22. this.cb.call(this.vm, value, oldValue)
    23. }
    24. }
    25. }
    26. }

    执行 this.get() 获取监听属性的值, 判断值是否和缓存的值相等, 不同的话执行 this.cb.call(this.vm, value, oldValue) , 也就是 watch 设置的 handler 函数; 这便是 watch 实现 监听的原理~

    值得一提的是, parsePath 中返回的函数对 data 属性进行层级访问:

    假设 path 为 "a.b.c", 那么函数执行时会先访问 obj.a , 再访问 obj.a.b , 最后访问 obj.a.b.c

    也就是当前的 watcher 被 data.a 、data.a.b 、data.a.b.c 进行依赖收集, 当其中一个属性发生变化时都会触发 watch 设置的监听函数, 这是个非常巧妙的设计!

    总结

    vue 中 watch 对数据进行监听的原理为: 对 watch 每个属性创建一个 watcher , watcher 在初始化时会将监听的目标值缓存到 watcher.value 中, 因此触发 data[key] 的 get 方法, 被对应的 dep 进行依赖收集; 当 data[key] 发生变动时触发 set 方法, 执行 dep.notify 方法, 通知所有收集的依赖 watcher , 触发收集的 watch watcher , 执行 watcher.cb , 也就是 watch 中的监听函数 (* ̄︶ ̄)

  • 相关阅读:
    【面经】2022社招软件测试面试(6)-德科
    竞争性谈判文件
    C++实现高性能内存池(二)
    Python性能测试框架Locust实战教程
    SSM进阶-Duubo入门demo整合MyBatis
    安防监控系统/视频云存储EasyCVR平台视频无法播放是什么原因?
    ChatGPT 上线 Canva 插件,可生成图片和视频内容
    macOS - 获取硬件设备信息
    【面试】虚拟机栈面试题
    如何编写整洁的代码
  • 原文地址:https://blog.csdn.net/weixin_44786530/article/details/127090846