数据响应式原理

响应式处理入口

  • 通过看源码解决下面问题

    • vm.msg={count:0}, 重新给属性赋值,是否是响应式的
    • vm.arr[0]=1 给数组元素赋值,视图是否会更新
    • vm.arr.length=0,修改数组的length,视图是否会更新
    • vm.arr.push(1),视图是否会更新
  • 整个响应式处理的过程是比较复杂的,我们先从

    • src\core\instance\init.js
      • initState(vm) vm状态的初始化
      • 初始化了_data,_props,methods
    • src\core\instance\state.js
// 数据初始化
  if (opts.data) {
    // 如果有data选项
    initData(vm)
  } else {
    // 没有data选项,observe把空对象转换成响应式的对象
    observe(vm._data = {}, true /* asRootData */)
  }

Observe

  • observe函数的作用,创建observer对象
/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
/* 
  observe函数试图创建一个observer对象,并返回这个新创建的对象,如果当前的value的对象已经有了observer对象,则将这个observer对象直接返回
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 参数一:传入的对象 参数二:是否是根数据(new Vue中的options中的data数据)
  // 判断valeue是否是对象,或者value是VNode的一个实例(虚拟dom),直接返回,表示当前的对象不需要做响应式的处理
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  // ob就是Observer的实例
  let ob: Observer | void
  // 如果value存在_ob_属性(存在ob实例),直接取出返回
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    // 如果value不存在_ob_属性,且满足可响应式处理的一些条件
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue // value这个对象是否是Vue实例,如果是则不需要响应式的处理,在vm实例化的过程中(initMixin),设置了vm._isVue=true
  ) {
    // 创建一个ob实例,并且将value对象中的所有属性转换成getter/setter
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    // 如果是根数据,ob的vmCount++
    ob.vmCount++
  }
  // 返回ob实例
  return ob
}

Observer构造函数

  • 将普通对象转换成响应式对象
/* 
Observer类被附加到每个被观察的对象,一旦附加,observer这个对象就会转换目标对象的所有属性,把他们转换成getter和setter,
getter/setter的作用是收集依赖和派发更新
 */
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()
    // 初始化实例的vmCount为0
    this.vmCount = 0
    // 将实例挂载到观察对象的_ob_属性,如果value对象存在_ob_属性,则说明该对象是经过响应式处理的
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 数组的响应式处理
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 为数组中的每一个对象创建一个observer实例
      this.observeArray(value)
    } else {
      // 遍历对象中的每一个属性,转换成getter/setter
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 遍历每一个属性,设置为响应式数据
      defineReactive(obj, keys[i])
    }
  }

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

defineReactive函数

  • 为对象定义响应式的属性
/* 
  为对象定义响应式的属性
 */
export function defineReactive (
  obj: Object, // 目标对象
  key: string,  // 监听的属性
  val: any, // 属性的值
  customSetter?: ?Function, // 用户自定义的setter函数,很少用到
  shallow?: boolean // 浅,true:代表只监听对象的第一层属性   false:代表深层监听
) {
  // 创建依赖对象实例
  const dep = new Dep()
  // 获取obj的属性描述符对象
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    // 当前属性是不可配置的
    return
  }

  // cater for pre-defined getter/setters
  // 提供预定义的存取器函数
  // obj这个对象可能是用户传入的,这个对象可能已经给属性设置了get和set
  // 先将用户传入的getter和setter取出,人后
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    // 当前只传入了obj和key,没有传入val
    val = obj[key]
  }
  // 如果shallow为false,则为深层监听,递归调用observe
  // 判断是否递归观察子对象,并将子对象属性都转换成getter/setter,返回子观察对象
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 如果预定义的getter存在,则value等于getter调用的返回值
      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) {
      // 如果预定义的getter存在,则value等于getter调用的返回值
      // 否则直接赋予属性值
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 如果新值等于旧值或者新值旧值为NaN则不执行
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      // 如果用户传入的属性有getter没有setter,直接返回
      if (getter && !setter) return
      // 如果预定义setter存在则调用,否则直接更新新值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果新值是对象,把这个对象转换成getter/setter
      childOb = !shallow && observe(newVal)
      // 派发更新(发布更改通知)
      dep.notify()
    }
  })
}

依赖收集 Dep实例

  • 当我们访问某个属性的值的时候会进行依赖收集,
  • 依赖收集就是把依赖该属性的watcher添加到dep对象的sunbs数组中,将来数据发生变化的时候,去通知所有的watcher
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • targetStack:栈 在Vue2以后,每一个组件都有一个mountComponent,即每一个组件会对应一个watcher对象,如果组件有嵌套的话(A组件嵌套B组件),当A组建渲染的过程中,发现还有子组件B,于是先渲染子组件B,此时A组件的渲染过程就被挂载起来了,所以A组件对应的watcher对象也应该被存储到栈中,当子组件B渲染完成后,子组件B的watcher从栈中弹出,继续执行父组件A的渲染
  • 在依赖属性的get中收集依赖
    在这里插入图片描述
    **加粗样式**
    在这里插入图片描述
    在这里插入图片描述

依赖收集调试

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h1>{{msg}}</h1>
      {{msg}}
      <hr />
      {{count}}
    </div>
    <!-- 完整版本 -->
    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          msg: "Hello Vue",
          count: 100,
        },
      });
    </script>
  </body>
</html>

  • 收集依赖的过程就是通过这个属性(msg,count,在render函数中使用到了这些属性)对应的dep对象收集这个组件(watcher),new Vue可以看成是一个组件,这个组件对应的template(模板)就是div#app
  • 只要属性发生变化,就会通知对应的watcher更新视图(内部触发了updateComponent函数)
  • 将updateComponent函数传入watcher
    在这里插入图片描述
  • 在Watcher中调用updateComponent函数
    在这里插入图片描述
  • 进入updateComponnent()函数
    在这里插入图片描述
  • 进入vm_render()函数,在该函数中会调用用户传入的render函数或者有template编译成的render函数
    在这里插入图片描述
  • render函数中,调用了vm._c(模板编译成的render函数)
    在这里插入图片描述
    _s:将传入的key转换成字符串toString()
    _v:创建文本的虚拟节点
    在这里插入图片描述
  • F11=>进入访问属性的get函数之前,系统会首先进入一个辅助的判断函数hasHandler=>判断当前实例(vm),是否有_c,msg,count等成员,F10快速跳过
    在这里插入图片描述
  • 访问vm.msg就是访问vm._data.msg,做了一层代理
    在这里插入图片描述
    在这里插入图片描述
  • 第二次访问msg依赖的时候在这里插入图片描述

数据响应式原理-数组

  • 数组的响应式处理在Observer构造函数中
    在这里插入图片描述
  • protoAgument(value,arrayMethds)函数 原型增强
  • 参数
    • 参数一:数组
    • 参数二:数组相关方法
  • 作用:将第二个参数赋值给数组的原型对象=>改变数组的原型对象
    在这里插入图片描述
  • 重点分析参数二,数组和数组原型之间的中间层,通过defy函数扩展这个空对象,且触发了该对象的方法时,通知依赖
const arrayProto = Array.prototype
// 适应数组的原型创建一个新的对象,arrayMethods是响应式数组和原始数组的原型对象的中间层
// arrayMethods的原型对象就是原始数组的原型对象
// 现在的arrayMethods只是一个空对象,下面的def方法会填充这个对象的内容,属性是数组方法,属性值是回调函数
export const arrayMethods = Object.create(arrayProto)
// 修改数组元素的方法,这些方法都会修改原数组
// 数组的补丁
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  // 保存数组原方法
  const original = arrayProto[method]
  // 调用Object.defineProperty()重新定义修改数组的方法,
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    // 获取数组把绑定的ob对象
    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对象发生通知
    ob.dep.notify()
    return result
  })
})

  • 数组收集依赖
    在这里插入图片描述

数据响应式处理-数组练习

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">{{arr}}</div>
    <!-- 完整版本 -->
    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          arr: [2, 3, 5],
        },
      });
    </script>
  </body>
</html>

  • vm.arr.push(100)
    在这里插入图片描述
  • 观察数组的原型对象-arrayMethods
    在这里插入图片描述
  • arrayMethods的原型对象才是数组原生的原型对象
  • vm.arr._proto=arrayMethods
  • arrayMethods.proto=Array.prototype
    在这里插入图片描述
  • 当调试vm.arr[0=100时候,视图没有更新,说明没有触发observer.depde notify()方法
  • 我们只有给数组的方法设置了响应式处理,没有给数组的属性设置响应式处理,length也是同理
  • 以数组的push方法为例
  // 第三个参数是val,相当于arr.push(...args)就是触发了第三个参数函数=>派发更新
      def(arraymethods, "push", function (...args) {
        // 调用原始方法
        const result = original.apply(this, args);
        // 获取数组把绑定的ob对象
        const ob = this.__ob__;
        // 派发更新(发布更改通知)
        ob.dep.notify();
        // 返回处理后的结果
        return result;
      });

在这里插入图片描述

在这里插入图片描述

  • 再通过this.observeArray(value)=>为数组中的每一个对象创建一个observer实例=>收集依赖=>完整实现了数组的响应式处理

数组的依赖收集是在哪实现的呢?

  • 在最开始的$options.data对象响应式处理过程中,数组是该对象的属性,数组是在这个过程中收集的依赖,同理对象也是如此

数据响应式原理-Watcher上(首次渲染)

Watcher类

  • watcher分为三种
    • computed Watcher 计算属性
    • 用户Watcher(侦听器)
    • 渲染 Watcher
      在这里插入图片描述

数据响应式原理-Watcher上(数据更新)

  • 当数据发生变化的时候,除了dep.notify()方法
  // 发布通知
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    // 调用每一个watcher实例的updata()方法进行更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
  • 再观察watcher的update方法内部的代码
  update () {
    /* istanbul ignore else */
    // 渲染watcher的lazy和sync均为false,执行queueWatcher函数
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

数据响应式原理-调试

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">{{arr}}</div>
    <!-- 完整版本 -->
    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          arr: [2, 3, 5],
        },
      });
    </script>
  </body>
</html>

  • 打断点
    在这里插入图片描述
  • 刷新,当前传入的value就是data对象,访问了arr数组属性,收集arr属性对应的依赖
    在这里插入图片描述

数据响应式原理-总结

在这里插入图片描述
在这里插入图片描述

动态添加一个响应式属性

  • 思考:当我们给一个响应式对象动态添加一个属性的时候,此时这个属性是否是响应式的?,通过下面代码演示
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      {{obj.title}}
      <hr />
      {{obj.name}}
      <hr />
      {{arr}}
    </div>
    <!-- 完整版本 -->
    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          obj: {
            title: "Hello Vue",
          },
          arr: [2, 3, 5],
        },
      });
    </script>
  </body>
</html>

  • 效果
    在这里插入图片描述
  • 在控制台动态的给vm.ob对象添加name属性
    在这里插入图片描述
  • 如果我们想为响应式对象新增的属性也是响应式的,可以适应set方法 Vue.set或者vm.$set,这两个方底层引用的是同一个函数set
  • 我们使用更多的是通过vm实例的$set方法来实现,因为在组件中很难获取到Vue的构造函数
    在这里插入图片描述
  • 数组同样的原理,第二个参数树数组的索引index
  • Vue官方文档查看vm.$set
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

set源码

  • set方法有两个 构造函数方法Vue.set() / 实例方法 vm.$set()
  • 定义位置
    • Vue.set()
      • global-api/index.js
// 静态方法
Vue.set = set;
Vue.delete = del;
Vue.nextTick = nextTick;
  • vm.$set()
  • instance/index.js
// 注册vm的$data/$props/$set/$delete/$watcher
// instance/state.js
stateMixin(Vue)

// instance/state.js
Vue.prototype.$set=set
Vue.prototype.$delete=del
  • 我们首先来观察Vue构造函数的set静态方法
    在这里插入图片描述
    set方法是在src\core\observer\index.js文件中定义的
  • 再观察vm实例的$set方法
    在这里插入图片描述
    set方法是在src\core\observer\index.js文件中定义的
  • 结论:Vue.set和vm.$set方法是相同的方法

set函数源码

  • 当目标对象是数组的时候,内部调用target.splice(key,1,val),此时的splice是arrayMethods中的增强方法
  • 当目标对象是响应式对象时,内部调用defineReactive(ob.value, key, val)
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    //  如果目标对象是undefined或者是原始值(不是响应式对象)
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 判断target是否是数组,并且key是一个合法的数组索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 判断传入的key和数组的length哪个大,会将两者最大值传给数组length赋值给数组length属性
    target.length = Math.max(target.length, key)
    // 通过splice对key位置的元素进行替换
    // splice在array.js进行了响应式的处理
    target.splice(key, 1, val)
    return val
  }
  // 如果key在对象中已经存在直接赋值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 获取target中的observer对象
  const ob = (target: any).__ob__
  // 如果target是Vue实例或者$data直接返回
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 如果ob不存在,target不是响应式对象,直接赋值
  if (!ob) {
    target[key] = val
    return val
  }
  // 把key设置为响应式属性
  defineReactive(ob.value, key, val)
  // 发送通知
  ob.dep.notify()
  return val
}

delete使用

  • set方法有两个 构造函数方法Vue.deletet() / 实例方法 vm.$delete()
  • Vue.delete
  • vm.$delete
    • 功能
      删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到 property 被删除的限制(也就数属性被删除的时候,我们检测不到这个属性被删除了,所以没法发送通知,所以包装了下该方法),但是你应该很少会使用它。
      • 注意:目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象。
  • 示例
// 对象
vm.$delete(vm.obj,"msg")
// 数组
vm.$delete(vm.arr,1)

del方法是在src\core\observer\index.js文件中定义的

  • 结论:Vue.delete和vm.$delete方法是相同的方法
    在这里插入图片描述

delete源码

  • 位置:src\core\observer\index.js
  • 当目标对象是数组的时候,内部调用target.splice(key,1),此时的splice是arrayMethods中的增强方法
  • 当目标对象是响应式对象时,内部调用delete target[key]

Watcher回顾

vm.$watcher
vm.$watcher(expOrFn,callback,[options])

  • 功能
    观察Vue示例变化的一个表达式或计算属性函数,回调函数得到的参数为新值和旧值,表达式只接受监督的键路径,对于更复杂的表达式,用一个函数取代
  • 参数
    • expOrFn:要监视$data中的属性,可以是表达式或者函数

    • callback:数据变化后执行的函数

      • 函数:回调函数
      • 对象:具有handler属性(字符串或者函数),如果该属性为字符串则methods中相应的定义
        在这里插入图片描述
    • options:可选的选项

      • deep:布尔类型,深度监听
      • immediate:布尔类型,是否立即执行一次回调函数
    • 示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">{{user.fullName}}</div>
    <!-- 完整版本 -->
    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          user: {
            firstName: "诸葛",
            lastName: "亮",
            fullName: "",
          },
        },
      });
      vm.$watch("user", function (newValue, oldVlaue) {
        this.user.fullName = newValue.firstName + "" + newValue.lastName;
      });
    </script>
  </body>
</html>
  • 效果-视图上是空白,因为不会立即执行这个回调函数,只有当属性发生变化的时候才会调用这个回调函数
  • 如果想立即在视图上看到效果(立即执行这个回调函数),增加$watch的第三个参数options
 vm.$watch(
        "user",
        function (newValue, oldVlaue) {
          this.user.fullName = newValue.firstName + "" + newValue.lastName;
        },
        {
          immediate: true,
        }
      );
  • 效果
    在这里插入图片描述
  • 直接修改user对象=> vm.user={},发现视图更新了,为空白区域,说明$watch回调函数触发了,监听成功
  • 修改user对象的某一个属性=> vm.user.firstName=“张”,发现视图没有更新,说明回调函数没有触发,因为我们我们上面是监听user属性的变化,至于user对象的内部属性,是没有监听的,所以user,firstName监听失败
    • 方法一:监听user.firstName
  vm.$watch(
        "user.firstName",
        function (newValue, oldVlaue) {
          this.user.fullName = newValue.firstName + "" + newValue.lastName;
        },
        {
          immediate: true,
        }
      );
  • 方法二:还是监听user属性,但是在第三个参数(选项)中配置深层监听,它不仅监听user这个属性的变化,还监听user这个对象子属性的变化
vm.$watch(
        "user",
        function (newValue, oldVlaue) {
          this.user.fullName = newValue.firstName + "" + newValue.lastName;
        },
        {
          deep: true,
          immediate: true,
        }
      );

三种类型的Watcher

  • 三种类型的Watcher对象
    • 没有静态方法,因为$watch方法中要使用vm实例
    • Watcher分为三种:计算属性Watcher,用户Watcher(侦听器),渲染Watcher
      • 创建顺序:计算属性Watcher,用户Watcher(侦听器),渲染Watcher
    • vm.$watch()
      • src\core\instance\state.js
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">{{user.fullName}}</div>
    <!-- 完整版本 -->
    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          message: "Hello Vue",
          user: {
            firstName: "诸葛",
            lastName: "亮",
            fullName: "",
          },
        },
        computed: {
          reversedMessage: function () {
            return this.message.split("").reverse().join("v");
          },
        },
        watch: {
          user: {
            handler: function (newValue, oldVlaue) {
              this.user.fullName = newValue.firstName + "" + newValue.lastName;
            },
            deep: true,
            immediate: true,
          },
        },
      });
    </script>
  </body>
</html>

调试代码-三个Watcher的创建顺序

在这里插入图片描述

计算属性watcher
  • 刷新,进入Watcher构造函数,发现该Watcher构造函数在initComputed函数中执行了,而initComputed函数在initstate中执行
    在这里插入图片描述
  • 进入(点击)initComputed函数,创建计算属性watcher
    在这里插入图片描述
  • 进入(点击)initState函数,初始化opts.computed
    在这里插入图片描述
  • 计算属性watcher的id为1
    在这里插入图片描述
用户watcher(侦听器)
  • 用户watcher是$watch创建的
    在这里插入图片描述
    在这里插入图片描述
  • 侦听器watcher的id为2
    在这里插入图片描述
渲染watcher

在这里插入图片描述

  • 渲染watcher的id为3
    在这里插入图片描述
  • 以上是三种watcher的创建顺序,那么watcher执行的顺序呢,会根据watcher的id大小顺序依次执行,所以执行的顺序和创建watcher的顺序一样

watcher源码

用户watcher(侦听器)

  • src\core\instance\state.js

  • 在initState函数中初始化opts.watch
    在这里插入图片描述

  • initWatch函数中
    在这里插入图片描述
    如果属性的值为数组,如果传递的是一个数组,它会把当前监听的属性创建多个处理函数,也就是当前这个属性发生变化的时候,会执行多个回调函数

  • createWatcher函数
    在这里插入图片描述

function createWatcher (
  vm: Component, // vm实例
  expOrFn: string | Function, //当前监听的属性
  handler: any, // 监听属性对应的值
  options?: Object // 选项
) {
  // 判断该属性对应的值是否是原生对象,
  if (isPlainObject(handler)) {
    options = handler
    // handler变量就是回调函数
    handler = handler.handler
  }
  // 判断handler是否是字符串,如果是字符串会找到metheds中同名的函数作为回调函数
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 如果handler是函数,则不作任何处理
  return vm.$watch(expOrFn, handler, options)
}
  • 小结:hander有三种情况

    • handler为数组:元素可以是对象或者字符串,遍历调用createWatcher(vm, key, handler[i])
    • handler为非数组,调用createWatcher(vm, key, handler)
      • handler为对象,在createWatcher函数内部处理hander
      • handler为字符串,在createWatcher函数内部处理hander
  • $watch函数
    在这里插入图片描述

  // 给原型上挂载$watcher方法,监控数据变化
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    // $watch方法是一个实例方法,它没有对应的静态方法,原因在于$watch方法内部用到了vm的实例
    const vm: Component = this
    // 传入过来的cb回调函数是一个对象,需要将对象中的回调函数取出,调用createWatcher方法
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    // 如果options曾经解析过,直接赋值,否则为空对象
    options = options || {}
    // 标记为用户watcher
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 判断immediate,如果为true
    if (options.immediate) {
      try {
        // 立即执行一次cb回调函数,并且把当前值传入
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    // 返回取消监听的方法
    return function unwatchFn () {
      watcher.teardown()
    }
  }
  • 什么地方用到了 options.user = true属性呢=>在Watcher构造函数中
    在这里插入图片描述
  • 在Watcher构造函数中,渲染watcher的lazy属性是false,那么什么时候为true呢,我们观察创建计算属性的位置
    在这里插入图片描述

nextTick回顾

  • 异步更新队列-nextTick()

    • Vue更新DOM是异步执行的,批量的
      • 在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的DOM
    • vm. n e x t T i c k ( f u n c t i o n ( ) / ∗ 操 作 d o m ∗ / ) , 在 nextTick(function(){/* 操作dom*/}),在 nextTick(function()/dom/),nextTick函数内部,视图已经更新完成,所以可以获取到视图(dom)的最新数据
  • bm.$nextTick()代码演示

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <p ref="P1">{{msg}}</p>
    </div>
    <!-- 完整版本 -->
    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          message: "Hello Vue",
        },
        mounted(){
          // 数据改变后不会立即更新dom,不能获取到dom相关的最新数据,只能使用$nextTick()方法
          this.msg="Hello World"
          this.$nextTick(()=>{
            console.log(this.$refs.p1.textContent)
          })
        }
      });
    </script>
  </body>
</html>

nextTick源码

  • nextTick既有静态方法,也有实例方法
  • 位置
    • src\core\util\next-tick.js
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值