zoukankan      html  css  js  c++  java
  • 理解Vue的Watch原理

    前言

    watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用。在面试时,也是必问知识点,一般会用作和 computed 进行比较。

    那么本文就来带大家从源码理解 watch 的工作流程,以及依赖收集和深度监听的实现。在此之前,希望你能对响应式原理流程、依赖收集流程有一些了解,这样理解起来会更加轻松。

    watch 用法

    “知己知彼,才能百战百胜”,分析源码之前,先要知道它如何使用。这对于后面理解有一定的辅助作用。

    第一种,字符串声明:

    var vm = new vue({
      el: '#example',
      data: {
        message: 'Hello'
      },
      watch: {
        message: 'handler'
      },
      methods: {
        handler (newVal, oldVal) { /* ... */ }
      }
    })
    

    第二种,函数声明:

    var vm = new vue({
      el: '#example',
      data: {
        message: 'Hello'
      },
      watch: {
        message: function (newVal, oldVal) { /* ... */ }
      }
    })
    

    第三种,对象声明:

    var vm = new Vue({
      el: '#example',
      data: {
        peopel: {
          name: 'jojo',
          age: 15
        }
      },
      watch: {
        // 字段可使用点操作符 监听对象的某个属性
        'people.name': {
          handler: function (newVal, oldVal) { /* ... */ }
        }
      }
    })
    watch: {
      people: {
        handler: function (newVal, oldVal) { /* ... */ },
        // 回调会在监听开始之后被立即调用
        immediate: true,
        // 对象深度监听  对象内任意一个属性改变都会触发回调
        deep: true
      }
    }
    

    第四种,数组声明:

    var vm = new Vue({
      el: '#example',
      data: {
        peopel: {
          name: 'jojo',
          age: 15
        }
      },
      // 传入回调数组,它们会被逐一调用
      watch: {
        'people.name': [
          'handle',
          function handle2 (newVal, oldVal) { /* ... */ },
          {
            handler: function handle3 (newVal, oldVal) { /* ... */ },
          }
        ],  
      },
      methods: {
        handler (newVal, oldVal) { /* ... */ }
      }
    })
     

    工作流程

    入口文件:

    // 源码位置:/src/core/instance/index.js
    import { initMixin } from './init'
    import { stateMixin } from './state'
    import { renderMixin } from './render'
    import { eventsMixin } from './events'
    import { lifecycleMixin } from './lifecycle'
    import { warn } from '../util/index'
    
    function Vue (options) {
      this._init(options)
    }
    
    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    
    export default Vue
    

    _init:

    // 源码位置:/src/core/instance/init.js
    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++
    
        // merge options
        if (options && options._isComponent) {
          // optimize internal component instantiation
          // since dynamic options merging is pretty slow, and none of the
          // internal component options needs special treatment.
          initInternalComponent(vm, options)
        } else {
          // mergeOptions 对 mixin 选项和 new Vue 传入的 options 选项进行合并
          vm.$options = mergeOptions(
            resolveconstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
    
        // expose real self
        vm._self = vm
        initLifecycle(vm)
        initEvents(vm)
        initRender(vm)
        callHook(vm, 'beforeCreate')
        initInjections(vm) // resolve injections before data/props
        // 初始化数据
        initState(vm)
        initProvide(vm) // resolve provide after data/props
        callHook(vm, 'created')
    
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    }
    

    initState:

    // 源码位置:/src/core/instance/state.js 
    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      if (opts.computed) initComputed(vm, opts.computed)
      // 这里会初始化 watch
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }
    

    initWatch:

    // 源码位置:/src/core/instance/state.js 
    function initWatch (vm: Component, watch: Object) {
      for (const key in watch) {
        const handler = watch[key]
        if (Array.isArray(handler)) {
          // 1
          for (let i = 0; i < handler.length; i++) {
            createWatcher(vm, key, handler[i])
          }
        } else {
          // 2
          createWatcher(vm, key, handler)
        }
      }
    }
    
    1. 数组声明的 watch 有多个回调,需要循环创建监听
    2. 其他声明方式直接创建

    createWatcher:

    // 源码位置:/src/core/instance/state.js 
    function createWatcher (
      vm: Component,
      expOrFn: string | Function,
      handler: any,
      options?: Object
    ) {
      // 1
      if (isPlainObject(handler)) {
        options = handler
        handler = handler.handler
      }
      // 2
      if (typeof handler === 'string') {
        handler = vm[handler]
      }
      // 3
      return vm.$watch(expOrFn, handler, options)
    }
    
    1. 对象声明的 watch,从对象中取出对应回调
    2. 字符串声明的 watch,直接取实例上的方法(注:methods 中声明的方法,可以在实例上直接获取)
    3. expOrFn 是 watch 的 key 值,$watch 用于创建一个“用户Watcher”

    所以在创建数据监听时,除了 watch 配置外,也可以调用实例的 $watch 方法实现同样的效果。

    $watch:

    // 源码位置:/src/core/instance/state.js
    export function stateMixin (Vue: Class<Component>) {
      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)
        }
        // 1
        options = options || {}
        options.user = true
        // 2
        const watcher = new Watcher(vm, expOrFn, cb, options)
        // 3
        if (options.immediate) {
          try {
            cb.call(vm, watcher.value)
          } catch (error) {
            handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
          }
        }
        // 4
        return function unwatchFn () {
          watcher.teardown()
        }
      }
    }
    

    stateMixin 在入口文件就已经调用了,为 Vue 的原型添加 $watch 方法。

    1. 所有“用户Watcher”的 options,都会带有 user 标识
    2. 创建 watcher,进行依赖收集
    3. immediate 为 true 时,立即调用回调
    4. 返回的函数可以用于取消 watch 监听
     

    依赖收集及更新流程

    经过上面的流程后,最终会进入 new Watcher 的逻辑,这里面也是依赖收集和更新的触发点。接下来看看这里面会有哪些操作。

    依赖收集

    // 源码位置:/src/core/observer/watcher.js
    export default class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm
        // options
        if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
          this.before = options.before
        } else {
          this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        this.dirty = this.lazy // for lazy watchers
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        // parse expression for getter
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)
        }
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    }
    

    在 Watcher 构造函数内,对传入的回调和 options 都进行保存,这不是重点。让我们来关注下这段代码:

    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    

    传进来的 expOrFn 是 watch 的键值,因为键值可能是 obj.a.b,需要调用 parsePath 对键值解析,这一步也是依赖收集的关键点。它执行后返回的是一个函数,先不着急 parsePath 做的是什么,先接着流程继续走。

    下一步就是调用 get:

    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
    }
    

    pushTarget 将当前的“用户Watcher”(即当前实例this) 挂到 Dep.target 上,在收集依赖时,找的就是 Dep.target。然后调用 getter 函数,这里就进入 parsePath 的逻辑。

    // 源码位置:/src/core/util/lang.js
    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
      }
    }
    

    参数 obj 是 vm 实例,segments 是解析后的键值数组,循环去获取每项键值的值,触发它们的“数据劫持get”。接着触发 dep.depend 收集依赖(依赖就是挂在 Dep.target 的 Watcher)。

    到这里依赖收集就完成了,从上面我们也得知,每一项键值都会被触发依赖收集,也就是说上面的任何一项键值的值发生改变都会触发 watch 回调。例如:

    watch: {
        'obj.a.b.c': function(){}
    }
    

    不仅修改 c 会触发回调,修改 b、a 以及 obj 同样触发回调。这个设计也是很妙,通过简单的循环去为每一项都收集到了依赖。

    更新

    在更新时首先触发的是“数据劫持set”,调用 dep.notify 通知每一个 watcher 的 update 方法。

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

    接着就走 queueWatcher 进行异步更新,这里先不讲异步更新。只需要知道它最后会调用的是 run 方法。

    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) {
            try {
              this.cb.call(this.vm, value, oldValue)
            } catch (e) {
              handleError(e, this.vm, `callback for watcher "${this.expression}"`)
            }
          } else {
            this.cb.call(this.vm, value, oldValue)
          }
        }
      }
    }
    

    this.get 获取新值,调用 this.cb,将新值旧值传入。

    深度监听

    深度监听是 watch 监听中一项很重要的配置,它能为我们观察对象中任何一个属性的变化。

    目光再拉回到 get 函数,其中有一段代码是这样的:

    if (this.deep) {
      traverse(value)
    }
    

    判断是否需要深度监听,调用 traverse 并将值传入

    // 源码位置:/src/core/observer/traverse.js
    const seenObjects  = new Set()
    
    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__) {
        // 1
        const depId = val.__ob__.dep.id
        // 2
        if (seen.has(depId)) {
          return
        }
        seen.add(depId)
      }
      // 3
      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)
      }
    }
    
    1. depId 是每一个被观察属性都会有的唯一标识
    2. 去重,防止相同属性重复执行逻辑
    3. 根据数组和对象使用不同的策略,最终目的是递归获取每一项属性,触发它们的“数据劫持get”收集依赖,和 parsePath 的效果是异曲同工

    从这里能得出,深度监听利用递归进行监听,肯定会有性能损耗。因为每一项属性都要走一遍依赖收集流程,所以在业务中尽量避免这类操作。

    卸载监听

    这种手段在业务中基本很少用,也不算是重点,属于那种少用但很有用的方法。它作为 watch 的一部分,这里也讲下它的原理。

    使用

    先来看看它的用法:

    data(){
      return {
        name: 'jojo'
      }
    }
    mounted() {
      let unwatchFn = this.$watch('name', () => {})
      setTimeout(()=>{
        unwatchFn()
      }, 10000)
    }
    

    使用 $watch 监听数据后,会返回一个对应的卸载监听函数。顾名思义,调用它当然就是不会再监听数据。

    原理

    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) {
        try {
          // 立即调用 watch
          cb.call(vm, watcher.value)
        } catch (error) {
          handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
        }
      }
      return function unwatchFn () {
        watcher.teardown()
      }
    }
    

    可以看到返回的 unwatchFn 里实际执行的是 teardown。

    teardown () {
      if (this.active) {
        if (!this.vm._isBeingDestroyed) {
          remove(this.vm._watchers, this)
        }
        let i = this.deps.length
        while (i--) {
          this.deps[i].removeSub(this)
        }
        this.active = false
      }
    }
    

    teardown 里的操作也很简单,遍历 deps 调用 removeSub 方法,移除当前 watcher 实例。在下一次属性更新时,也不会通知 watcher 更新了。deps 存储的是属性的 dep。

    奇怪的地方

    在看源码时,我发现 watch 有个奇怪的地方,导致它的用法是可以这样的:

    watch:{
      name:{
        handler: {
          handler: {
            handler: {
              handler: {
                handler: {
                  handler: {
                    handler: ()=>{console.log(123)},
                    immediate: true
                  }
                }
              }
            }
          }
        }
      }
    }
    

    一般 handler 是传递一个函数作为回调,但是对于对象类型,内部会进行递归去获取,直到值为函数。所以你可以无限套娃传对象。

    递归的点在 $watch 中的这段代码:

    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    

    如果你知道这段代码的实际应用场景麻烦告诉我一下,嘿嘿~

    vi设计http://www.maiqicn.com 办公资源网站大全https://www.wode007.com

    总结

    watch 监听实现利用遍历获取属性,触发“数据劫持get”逐个收集依赖,这样做的好处是其上级的属性发生修改也能执行回调。

    与 data 和 computed 不同,watch 收集依赖的流程是发生在页面渲染之前,而前两者是在页面渲染时进行取值才会收集依赖。

    在面试时,如果被问到 computed 和 watch 的异同,我们可以从下面这些点进行回答:

    • 一是 computed 要依赖 data 上的属性变化返回一个值,watch 则是观察数据触发回调;
    • 二是 computed 和 watch 依赖收集的发生点不同;
    • 三是 computed 的更新需要“渲染Watcher”的辅助,watch 不需要,这点在我的上一篇文章有提到。
  • 相关阅读:
    关于 HTML5 的 11 个让人难以接受的事实
    【转】zookeeper 的监控工具
    arm+linux 裸机环境搭建之安装工具篇(eclipse)
    Unity 利用NGUI2.6.3做技能冷却的CD效果
    spring boot打包文件后,报错No such file or directory
    机器学习实战读书笔记(3)朴素贝叶斯
    机器学习实战读书笔记(2)决策树
    机器学习实战读书笔记(1)
    No operations allowed after connection closed--转
    简单理解Socket
  • 原文地址:https://www.cnblogs.com/xiaonian8/p/13705374.html
Copyright © 2011-2022 走看看