Vue 响应式原理源码剖析

news2024/10/1 17:42:19

文章目录

  • 1. 说明
  • 2. 初始化
    • initState()
    • initProps()
    • initData()
    • observe()
    • Observer
    • defineReactive()
  • 3. 数据代理
  • 4. 模板解析
    • 4.1. 模板解析的基本流程
    • 4.2. 模板解析(1): 大括号表达式解析
    • 4.3. 模板解析(2): 事件指令解析
    • 4.4. 模板解析(3): 一般指令解析
  • 5. 数据绑定
    • 5.1. 数据绑定
    • 5.2. 数据劫持
    • 5.3. 四个重要对象
      • 5.3.1 Observer
      • 5.3.2 Dep(Depend)
      • 5.3.3 Compiler
      • 5.3.4 Watcher
      • 5.3.5 总结: dep 与 watcher 的关系: 多对多
  • 6. MVVM 原理图分析
  • 7. 双向数据绑定
  • 依赖收集过程
  • 移除订阅
  • 派发更新
    • notify()
    • update()
    • queueWatcher()
    • flushSchedulerQueue()
    • updated()
  • defineProperty 缺陷及处理
    • Vue.set()

1. 说明

在这里插入图片描述

  1. 分析 vue 作为一个 MVVM 框架的基本实现原理

    • 数据代理
    • 模板解析
    • 数据绑定
  2. 准备知识

    1. [].slice.call(lis): 将伪数组转换为真数组
    2. node.nodeType: 得到节点类型
    3. Object.defineProperty(obj, propName, {}): 给对象添加/修改属性(指定描述符)
      • configurable: true/false 是否可以重新 define
      • enumerable: true/false 是否可以枚举(for…in / keys())
      • value: 指定初始值
      • writable: true/false value 是否可以修改
      • get: 回调函数, 用来得到当前属性值
      • set: 回调函数, 用来监视当前属性值的变化
    4. Object.keys(obj): 得到对象自身可枚举的属性名的数组
    5. DocumentFragment: 文档碎片(高效批量更新多个节点)
    6. obj.hasOwnProperty(prop): 判断 prop 是否是 obj 自身的属性

2. 初始化

new Vue 初始化的时候,会对我们组件的数据 propsdata 进行初始化

源码地址:src/core/instance/init.js - 15行

export function initMixin (Vue: Class<Component>) {
  // 在原型上添加 _init 方法
  Vue.prototype._init = function (options?: Object) {
    ...
    vm._self = vm
    initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
    initEvents(vm) // 初始化事件:$on, $off, $emit, $once
    initRender(vm) // 初始化渲染: render, mixin
    callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
    initInjections(vm) // 初始化 inject
    initState(vm) // 初始化组件数据:props, data, methods, watch, computed
    initProvide(vm) // 初始化 provide
    callHook(vm, 'created') // 调用生命周期钩子函数
    ...
  }
}

initState()

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 初始化 props
  if (opts.props) initProps(vm, opts.props)
  // 初始化 methods
  if (opts.methods) initMethods(vm, opts.methods)
  // 初始化 data 
  if (opts.data) {
    initData(vm)
  } else {
    // 没有 data 的话就默认赋值为空对象,并监听
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initProps()initData()observe()

initProps()

源码地址:src/core/instance/state.js - 65行
这里主要做的是:

  • 遍历父组件传进来的 props 列表
  • 校验每个属性的命名、类型、default 属性等,都没有问题就调用 defineReactive 设置成响应式
  • 然后用 proxy() 把属性代理到当前实例上,如把 vm._props.xx 变成 vm.xx,就可以访问
function initProps (vm: Component, propsOptions: Object) {
  // 父组件传入子组件的 props
  const propsData = vm.$options.propsData || {}
  // 经过转换后最终的 props
  const props = vm._props = {}
  // 存放 props 的 key,就算 props 值空了,key 也会在里面
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // 转换非根实例的 props
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    // 校验 props 类型、default 属性等
    const value = validateProp(key, propsOptions, propsData, vm)
    // 在非生产环境中
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(`hyphenatedKey 是保留属性,不能用作组件 prop`)
      }
      // 把 props 设置成响应式的
      defineReactive(props, key, value, () => {
        // 如果用户修改 props 发出警告
        if (!isRoot && !isUpdatingChildComponent) {
          warn(`避免直接改变 prop`)
        }
      })
    } else {
      // 把 props 设置为响应式
      defineReactive(props, key, value)
    }
    // 把不在默认 vm 上的属性,代理到实例上
    // 可以让 vm._props.xx 通过 vm.xx 访问
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

initData()

源码地址:src/core/instance/state.js - 113行
这里主要做的是:

  • 初始化一个 data,并拿到 keys 集合
  • 遍历 keys 集合,来判断有没有和 props 里的属性名或者 methods 里的方法名重名的
  • 没有问题就通过 proxy()data 里的每一个属性都代理到当前实例上,就可以通过 this.xx 访问了
  • 最后再调用 observe 监听整个 data
function initData (vm: Component) {
  // 获取当前实例的 data 
  let data = vm.$options.data
  // 判断 data 的类型
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(`数据函数应该返回一个对象`)
  }
  // 获取当前实例的 data 属性名集合
  const keys = Object.keys(data)
  // 获取当前实例的 props 
  const props = vm.$options.props
  // 获取当前实例的 methods 对象
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // 非生产环境下判断 methods 里的方法是否存在于 props 中
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(`Method 方法不能重复声明`)
      }
    }
    // 非生产环境下判断 data 里的属性是否存在于 props 中
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(`属性不能重复声明`)
    } else if (!isReserved(key)) {
      // 都不重名的情况下,代理到 vm 上
      // 可以让 vm._data.xx 通过 vm.xx 访问
      proxy(vm, `_data`, key)
    }
  }
  // 监听 data
  observe(data, true /* asRootData */)
}

observe()

源码地址:src/core/observer/index.js - 110行
这个方法主要就是用来给数据加上监听器
这里主要做的是:

  • 如果是 vnode 的对象类型或者不是引用类型,就直接跳出
  • 否则就给没有添加 Observer 的数据添加一个 Observer,也就是监听者
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 如果不是'object'类型 或者是 vnode 的对象类型就直接返回
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 使用缓存的对象
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 创建监听者
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

源码地址:src/core/observer/index.js - 37行
这是一个类,作用是把一个正常的数据成可观测的数据
这里主要做的是:

  • 给当前 value 打上已经是响应式属性的标记,避免重复操作
  • 然后判断数据类型
    • 如果是对象,就遍历对象,调用 defineReactive()创建响应式对象
    • 如果是数组,就遍历数组,调用 observe()对每一个元素进行监听
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // 根对象上的 vm 数量
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 给 value 添加 __ob__ 属性,值为value 的 Observe 实例
    // 表示已经变成响应式了,目的是对象遍历时就直接跳过,避免重复操作
    def(value, '__ob__', this)
    // 类型判断
    if (Array.isArray(value)) {
      // 判断数组是否有__proty__
      if (hasProto) {
        // 如果有就重写数组的方法
        protoAugment(value, arrayMethods)
      } else {
        // 没有就通过 def,也就是Object.defineProperty 去定义属性值
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // 如果是对象类型
  walk (obj: Object) {
    const keys = Object.keys(obj)
    // 遍历对象所有属性,转为响应式对象,也是动态添加 getter 和 setter,实现双向绑定
    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])
    }
  }
}

defineReactive()

源码地址:src/core/observer/index.js - 135行
这个方法的作用是定义响应式对象
这里主要做的是:

  • 先初始化一个 dep 实例
  • 如果是对象就调用 observe,递归监听,以保证不管结构嵌套多深,都能变成响应式对象
  • 然后调用 Object.defineProperty() 劫持对象属性的 gettergetter
  • 如果获取时,触发 getter 会调用 dep.depend() 把观察者 push 到依赖的数组 subs 里去,也就是依赖收集
  • 如果更新时,触发 setter 会做以下操作
    • 新值没有变化或者没有 setter 属性的直接跳出
    • 如果新值是对象就调用 observe() 递归监听
    • 然后调用 dep.notify() 派发更新
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  // 创建 dep 实例
  const dep = new Dep()
  // 拿到对象的属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // 获取自定义的 getter 和 setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 如果 val 是对象的话就递归监听
  // 递归调用 observe 就可以保证不管对象结构嵌套有多深,都能变成响应式对象
  let childOb = !shallow && observe(val)
  // 截持对象属性的 getter 和 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 拦截 getter,当取值时会触发该函数
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 进行依赖收集
      // 初始化渲染 watcher 时访问到需要双向绑定的对象,从而触发 get 函数
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // 拦截 setter,当值改变时会触发该函数
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // 判断是否发生变化
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // 没有 setter 的访问器属性
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果新值是对象的话递归监听
      childOb = !shallow && observe(newVal)
      // 派发更新
      dep.notify()
    }
  })
}

上面说了通过 dep.depend 来做依赖收集,可以说 Dep 就是整个 getter 依赖收集的核心了

3. 数据代理

  1. 数据代理: 通过一个对象代理对另一个对象(在前一个对象内部)中属性的操作(读/写)
  2. vue 数据代理: 通过 vm 对象来代理 data 对象中所有属性的操作
  3. 好处: 更方便的操作 data 中的数据
  4. 基本实现流程 a. 通过 Object.defineProperty()vm 添加与 data 对象的属性对应的属性描述符 b. 所有添加的属性都包含 getter/setter c. getter/setter 内部去操作 data 中对应的属性数据

4. 模板解析

4.1. 模板解析的基本流程

  1. el 的所有子节点取出, 添加到一个新建的文档 fragment 对象中
  2. fragment 中的所有层次子节点递归进行编译解析处理
    • 对大括号表达式文本节点进行解析
    • 对元素节点的指令属性进行解析
      • 事件指令解析
      • 一般指令解析
  3. 将解析后的 fragment 添加到 el 中显示

4.2. 模板解析(1): 大括号表达式解析

  1. 根据正则对象得到匹配出的表达式字符串: 子匹配/RegExp.\$1 name
  2. data 中取出表达式对应的属性值
  3. 将属性值设置为文本节点的 textContent

4.3. 模板解析(2): 事件指令解析

  1. 从指令名中取出事件名
  2. 根据指令的值(表达式)从 methods 中得到对应的事件处理函数对象
  3. 给当前元素节点绑定指定事件名和回调函数的 dom 事件监听
  4. 指令解析完后, 移除此指令属性

4.4. 模板解析(3): 一般指令解析

  1. 得到指令名和指令值(表达式) text/html/class msg/myClass
  2. data 中根据表达式得到对应的值
  3. 根据指令名确定需要操作元素节点的什么属性
    • v-texttextContent 属性
    • v-htmlinnerHTML 属性
    • v-classclassName 属性
  4. 将得到的表达式的值设置到对应的属性上
  5. 移除元素的指令属性

5. 数据绑定

5.1. 数据绑定

一旦更新了 data 中的某个属性数据, 所有界面上直接使用或间接使用了此属性的节点都会 更新

5.2. 数据劫持

  1. 数据劫持是 vue 中用来实现数据绑定的一种技术
  2. 基本思想: 通过 defineProperty()来监视 data 中所有属性(任意层次)数据的变化, 一旦变化就去更新界面

5.3. 四个重要对象

5.3.1 Observer

a. 用来对 data 所有属性数据进行劫持的构造函数
b. 给 data 中所有属性重新定义属性描述(get/set)
c. 为 data 中的每个属性创建对应的 dep 对象

5.3.2 Dep(Depend)

a. data 中的每个属性(所有层次)都对应一个 dep 对象
b. 创建的时机:

  • 在初始化 data 中各个属性时创建对应的 dep 对象
  • data 中的某个属性值被设置为新的对象时

c. 对象的结构

{ 
	id, // 每个 dep 都有一个唯一的 id 
	subs //包含 n 个对应 watcher 的数组(subscribes 的简写) 
}

d. subs 属性说明

  • watcher 被创建时, 内部将当前 watcher 对象添加到对应的 dep 对象的 subs
  • 当此 data 属性的值发生改变时, subs 中所有的 watcher 都会收到更新的通知,从而最终更新对应的界面

Dep源码地址:src/core/observer/dep.js
这是一个类,它实际上就是对 Watcher 的一种管理
这里首先初始化一个 subs 数组,用来存放依赖,也就是观察者,谁依赖这个数据,谁就在这个数组里,然后定义几个方法来对依赖添加、删除、通知更新等
另外它有一个静态属性 target,这是一个全局的 Watcher,也表示同一时间只能存在一个全局的 Watcher

let uid = 0
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) {
      // 调用 Watcher 的 addDep 函数
      Dep.target.addDep(this)
    }
  }
  // 派发更新(下一章节介绍)
  notify () {
    ...
  }
}
// 同一时间只有一个观察者使用,赋值观察者
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

5.3.3 Compiler

a. 用来解析模板页面的对象的构造函数(一个实例)
b. 利用 compile 对象解析模板页面
c. 每解析一个表达式(非事件指令)都会创建一个对应的 watcher 对象, 并建立 watcherdep 的关系
d. compliewatcher 关系: 一对多的关系

5.3.4 Watcher

a. 模板中每个非事件指令或表达式都对应一个 watcher 对象
b. 监视当前表达式数据的变化
c. 创建的时机: 在初始化编译模板时
d. 对象的组成

{ 
	vm, //vm 对象 
	exp, //对应指令的表达式 
	cb, //当表达式所对应的数据发生改变的回调函数 
	value, //表达式当前的值 
	depIds //表达式中各级属性所对应的 dep 对象的集合对象 //属性名为 dep 的 id, 属性值为 dep 
}

源码地址:src/core/observer/watcher.js

Watcher 也是一个类,也叫观察者(订阅者),这里干的活还挺复杂的,而且还串连了渲染编译

let uid = 0
export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // Watcher 实例持有的 Dep 实例的数组
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.value = this.lazy
      ? undefined
      : this.get()
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
  }
  get () 
    // 该函数用于缓存 Watcher
    // 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用回调函数,也就是upcateComponent,对需要双向绑定的对象求值,从而触发依赖收集
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
    } finally {
      // 深度监听
      if (this.deep) {
        traverse(value)
      }
      // 恢复Watcher
      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)) {
        // 把当前 Watcher push 进数组
        dep.addSub(this)
      }
    }
  }
  // 清理不需要的依赖(下面有)
  cleanupDeps () {
    ...
  }
  // 派发更新时调用(下面有)
  update () {
    ...
  }
  // 执行 watcher 的回调
  run () {
    ...
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

5.3.5 总结: dep 与 watcher 的关系: 多对多

a. data 中的一个属性对应一个 dep, 一个 dep 中可能包含多个 watcher(模板中有几个表达式使用到了同一个属性)
b. 模板中一个非事件表达式对应一个 watcher, 一个 watcher 中可能包含多个 dep(表达式是多层: a.b)
c. 数据绑定使用到 2 个核心技术

  • defineProperty()
  • 消息订阅与发布

6. MVVM 原理图分析

在这里插入图片描述

7. 双向数据绑定

  1. 双向数据绑定是建立在单向数据绑定(model==>View)的基础之上的
  2. 双向数据绑定的实现流程:
    a. 在解析 v-model 指令时, 给当前元素添加 input 监听
    b. 当 input 的 value 发生改变时, 将最新的值赋值给当前表达式所对应的 data 属性

依赖收集过程

在首次渲染挂载的时候,还会有这样一段逻辑

mountComponent 源码地址:src/core/instance/lifecycle.js - 141行

export function mountComponent (...): Component {
  // 调用生命周期钩子函数
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
    // 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染
    vm._update(vm._render(), hydrating)
  }
  // 为当前组件实例设置观察者,监控 updateComponent 函数得到的数据,下面有介绍
  new Watcher(vm, updateComponent, noop, {
    // 当触发更新的时候,会在更新之前调用
    before () {
      // 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不会执行
      if (vm._isMounted && !vm._isDestroyed) {
        // 调用生命周期钩子函数
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // 没有老的 vnode,说明是首次渲染
  if (vm.$vnode == null) {
    vm._isMounted = true
    // 调用生命周期钩子函数
    callHook(vm, 'mounted')
  }
  return vm
}

依赖收集:

  1. 挂载之前会实例化一个渲染 watcher ,进入 watcher 构造函数里就会执行 this.get() 方法
  2. 然后就会执行 pushTarget(this),就是把 Dep.target 赋值为当前渲染 watcher 并压入栈(为了恢复用)
  3. 然后执行 this.getter.call(vm, vm),也就是上面的 updateComponent() 函数,里面就执行了 vm._update(vm._render(), hydrating)
  4. 接着执行 vm._render() 就会生成渲染 vnode,这个过程中会访问 vm 上的数据,就触发了数据对象的 getter
  5. 每一个对象值的 getter 都有一个 dep,在触发 getter 的时候就会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)
  6. 然后这里会做一些判断,以确保同一数据不会被多次添加,接着把符合条件的数据 pushsubs 里,到这就已经完成了依赖的收集,不过到这里还没执行完,如果是对象还会递归对象触发所有子项的getter,还要恢复 Dep.target 状态

移除订阅

移除订阅就是调用 cleanupDeps() 方法。比如在模板中有 v-if 我们收集了符合条件的模板 a 里的依赖。当条件改变时,模板 b 显示出来,模板 a 隐藏。这时就需要移除 a 的依赖
这里主要做的是:

  • 先遍历上一次添加的实例数组 deps,移除 dep.subs 数组中的 Watcher 的订阅
  • 然后把 newDepIdsdepIds 交换,newDepsdeps 交换
  • 再把 newDepIdsnewDeps 清空
// 清理不需要的依赖
  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
  }

派发更新

notify()

触发 setter 的时候会调用 dep.notify() 通知所有订阅者进行派发更新

notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 如果不是异步,需要排序以确保正确触发
      subs.sort((a, b) => a.id - b.id)
    }
    // 遍历所有 watcher 实例数组
    for (let i = 0, l = subs.length; i < l; i++) {
      // 触发更新
      subs[i].update()
    }
  }

update()

触发更新时调用

  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 组件数据更新会走这里
      queueWatcher(this)
    }
  }

queueWatcher()

源码地址:src/core/observer/scheduler.js - 164行

这是一个队列,也是 Vue 在做派发更新时的一个优化点。就是说在每次数据改变的时候不会都触发 watcher 回调,而是把这些 watcher 都添加到一个队列里,然后在 nextTick 后才执行

这里和下一小节 flushSchedulerQueue() 的逻辑有交叉的地方,所以要联合起来理解

主要做的是:

  • 先用 has 对象查找 id,保证同一个 watcher 只会 push 一次
  • else 如果在执行 watcher 期间又有新的 watcher 插入进来就会到这里,然后从后往前找,找到第一个待插入的 id 比当前队列中的 id 大的位置,插入到队列中,这样队列的长度就发生了变化
  • 最后通过 waiting 保证 nextTick 只会调用一次
export function queueWatcher (watcher: Watcher) {
  // 获得 watcher 的 id
  const id = watcher.id
  // 判断当前 id 的 watcher 有没有被 push 过
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // 最开始会进入这里
      queue.push(watcher)
    } else {
      // 在执行下面 flushSchedulerQueue 的时候,如果有新派发的更新会进入这里,插入新的 watcher,下面有介绍
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // 最开始会进入这里
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      // 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用
      nextTick(flushSchedulerQueue)
    }
  }
}

flushSchedulerQueue()

源码地址:src/core/observer/scheduler.js - 71行
这里主要做的是:

  • 先排序队列,排序条件有三点,看注释
  • 然后遍历队列,执行对应 watcher.run()。需要注意的是,遍历的时候每次都会对队列长度进行求值,因为在 run 之后,很可能又会有新的 watcher 添加进来,这时就会再次执行到上面的 queueWatcher
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // 根据 id 排序,有如下条件
  // 1.组件更新需要按从父到子的顺序,因为创建过程中也是先父后子
  // 2.组件内我们自己写的 watcher 优先于渲染 watcher
  // 3.如果某组件在父组件的 watcher 运行期间销毁了,就跳过这个 watcher
  queue.sort((a, b) => a.id - b.id)

  // 不要缓存队列长度,因为遍历过程中可能队列的长度发生变化
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // 执行 beforeUpdate 生命周期钩子函数
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 执行组件内我们自己写的 watch 的回调函数并渲染组件
    watcher.run()
    // 检查并停止循环更新,比如在 watcher 的过程中又重新给对象赋值了,就会进入无限循环
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(`无限循环了`)
        break
      }
    }
  }
  // 重置状态之前,先保留一份队列备份
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  resetSchedulerState()
  // 调用组件激活的钩子  activated
  callActivatedHooks(activatedQueue)
  // 调用组件更新的钩子  updated
  callUpdatedHooks(updatedQueue)
}

updated()

终于可以更新了,updated 大家都熟悉了,就是生命周期钩子函数

上面调用 callUpdatedHooks() 的时候就会进入这里, 执行 updated

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

至此 Vue2 的响应式原理流程的源码基本就分析完毕了,接下来就介绍一下上面流程中的不足之处

defineProperty 缺陷及处理

使用 Object.defineProperty 实现响应式对象,还是有一些问题的

  • 比如给对象中添加新属性时,是无法触发 setter
  • 比如不能检测到数组元素的变化

Vue.set()

给对象添加新的响应式属性时,可以使用一个全局的 API,就是 Vue.set() 方法

源码地址:src/core/observer/index.js - 201行

set 方法接收三个参数:

  • target:数组或普通对象
  • key:表示数组下标或对象的 key 名
  • val:表示要替换的新值

这里主要做的是:

  • 先判断如果是数组,并且下标合法,就直接使用重写过的 splice 替换
  • 如果是对象,并且 key 存在于 target 里,就替换值
  • 如果没有 __ob__,说明不是一个响应式对象,直接赋值返回
  • 最后再把新属性变成响应式,并派发更新
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 如果是数组 而且 是合法的下标
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    // 直接使用 splice 就替换,注意这里的 splice 不是原生的,所以才可以监测到,具体看下面
    target.splice(key, 1, val)
    return val
  }
  // 到这说明是对象
  // 如果 key 存在于 target 里,就直接赋值,也是可以监测到的
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 获取 target.__ob__
  const ob = (target: any).__ob__
  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
  }
  // 在 Observer 里介绍过,如果没有这个属性,就说明不是一个响应式对象
  if (!ob) {
    target[key] = val
    return val
  }
  // 然后把新添加的属性变成响应式
  defineReactive(ob.value, key, val)
  // 手动派发更新
  ob.dep.notify()
  return val
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1413191.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实体识别与分类方法综述

目录 前言1 实体识别简介2 基于模板和规则的方法3 基于序列标注的方法3.1 常见序列标注模型3.2 模型参数估计和学习问题3.3 常见序列预测模型 4. 基于深度学习的实体识别方法5 基于预训练语言模型的实体识别5.1 BERT、GPT等预训练语言模型5.2 解码策略 6 特殊问题与挑战6.1 标签…

Ultraleap 3Di新建项目之给所有的Joint挂载物体

工程文件 Ultraleap 3Di给所有的Joint挂载物体 前期准备 参考上一期文章&#xff0c;进行正确配置 Ultraleap 3Di配置以及在 Unity 中使用 Ultraleap 3Di手部跟踪 新建项目 初始项目如下&#xff1a; 新建Create Empty 将新建的Create Empty&#xff0c;重命名为LeapPro…

10-微服务Nacos Config的通用配置

一、解决不同环境相同配置问题-自定义Data ID配置 在实际的开发过程中&#xff0c;我们的项目所用到的配置参数有的时候并不需要根据不同的环境进行区分&#xff0c;生产、测试、开发环境所用到的参数值是相同的。那么解决同一服务在多环境中&#xff0c;引用相同的配置的问题…

HTTP中POST、GET、PUT、DELETE方式的区别

GET请求会向数据库发索取数据的请求&#xff0c;从而来获取信息&#xff0c;该请求就像数据库的select操作一样&#xff0c;只是用来查询一下数据&#xff0c;不会修改、增加数据&#xff0c;不会影响资源的内容&#xff0c;即该请求不会产生副作用。无论进行多少次操作&#x…

uni-app 微信小程序之红包雨活动

文章目录 1. 页面效果2. 页面样式代码 1. 页面效果 GIF录屏有点卡&#xff0c;实际比较丝滑 每0.5s掉落一个红包控制4s后自动移除红包点击红包消除红包&#xff08;或者自行1&#xff0c;或者弹窗需求&#xff09; 2. 页面样式代码 <!-- 红包雨活动 --> <template>…

【TCP】重传与超时机制

前言 在网络通信的世界里&#xff0c;传输控制协议&#xff08;TCP&#xff09;扮演着一个至关重要的角色。它确保了数据的可靠传输&#xff0c;就像邮差确保每一封信都能准确无误地送达收件人手中一样。但是&#xff0c;网络环境充满了不确定性&#xff0c;数据包可能会因为各…

(大众金融)SQL server面试题(3)-客户已用额度总和

今天&#xff0c;面试了一家公司&#xff0c;什么也不说先来三道面试题做做&#xff0c;第三题。 那么&#xff0c;我们就开始做题吧&#xff0c;谁叫我们是打工人呢。 题目是这样的&#xff1a; DEALER_INFO经销商授信协议号码经销商名称经销商证件号注册地址员工人数信息维…

web3d-three.js场景设计器-mesh网格添加多模型-模型描述随动

给场景中的模型加上广告牌描述&#xff0c;可以在模型的MESH里添加Sprite&#xff0c;配上相应的文字&#xff0c; 描述Sprite的位置则是在mesh中的相对位置&#xff0c;比如模型高10&#xff0c;那么我们可以给一个y等于10 来进行适配&#xff0c;这样在移动模型mesh网格时可…

重磅!讯飞星火V3.5马上发布!AI写作、AI编程、AI绘画等功能全面提升!

讯飞星火大模型相信很多友友已经不陌生了&#xff0c;可以说是国内GPT相关领域的龙头标杆&#xff0c;而对于1月30日即将在讯飞星火发布会发出的V3.5新版本来说&#xff0c;讯飞星火V3.5与之前版本相比&#xff0c;性能提升方面相当明显&#xff0c;在提示语义理解、内容生成、…

常见の算法链表问题

时间复杂度 1.链表逆序 package class04;import java.util.ArrayList; import java.util.List;public class Code01_ReverseList {public static class Node {public int value;public Node next;public Node(int data) {value data;}}public static class DoubleNode {publi…

零基础学习数学建模——(四)备战美赛

本篇博客将讲解如何备战美赛。 什么是美赛 美赛&#xff0c;全称是美国大学生数学建模竞赛&#xff08;MCM/ICM&#xff09;&#xff0c;由美国数学及其应用联合会主办&#xff0c;是最高的国际性数学建模竞赛&#xff0c;也是世界范围内最具影响力的数学建模竞赛。 赛题内容…

Unity3D学习之UI系统——NGUI

文章目录 1. 前言2 NGUI下载和导入3. NGUI三大组件3.1 Root组件3.1.1 分辨率概念3.1.2 Root的作用3.1.3 root脚本各组件3.1.4 总结 3.2 Panel 组件3.2.1 Panel的作用3.2.2 Panel的控件3.2.3 总结 3.3 EventSystem组件3.3.1 作用3.3.2 组件3.3.3 总结 4 图集制作4.1 图集的作用4…

华为和苹果手机迁移备忘录数据方法

在数字时代&#xff0c;手机已成为我们生活的重要组成部分&#xff0c;而备忘录更是我们日常不可或缺的小助手。但当我们从华为切换到苹果&#xff0c;或从苹果转向华为时&#xff0c;如何确保那些重要的备忘信息不丢失&#xff0c;顺利迁移到新手机中呢&#xff1f; 我曾亲身…

Java实现加权平均分计算程序WeightedAverageCalculator

成绩加权平均分计算程序&#xff0c;带UI界面和输入保存功能。 因为本人对成绩的加权均分有所关注&#xff0c;但学校的教务系统查分时往往又不显示个人的加权均分&#xff0c;加之每次手动敲计算器计算很麻烦就花了点时间写了一个加权均分计算程序自用&#xff0c;顺便开源。…

Vue组件化-插槽Slot和非父子通信

一、认识插槽Slot作用 1.认识插槽Slot 在开发中&#xff0c;我们会经常封装一个个可复用的组件&#xff1a; 前面我们会通过props传递给组件一些数据&#xff0c;让组件来进行展示&#xff1b;但是为了让这个组件具备更强的通用性&#xff0c;我们不能将组件中的内容限制为固…

鸿蒙开发第2篇__装饰器

在ArkTS中&#xff0c; 有装饰器语法。 装饰器用于装饰类、结构、方法、变量&#xff0c;赋予其特殊的含义。 1. Component 表示自定义组件&#xff0c; 用此装饰器的组件是可重用的&#xff0c;可以与其他组件重合 此装饰器装饰的 struct 表示该结构体具有组件化能力&#…

3. MATLAB中Plot绘制放大特定的区域

在MATLAB中&#xff0c;我们经常需要绘制图形并进行一些自定义的操作。在本示例中&#xff0c;我们将演示如何在MATLAB中绘制一个图形&#xff0c;并通过放大某个特定的区域来突出显示。 ## 原始图形 首先&#xff0c;我们绘制了一个包含正弦和余弦函数的图形。 % MATLAB 代…

【STM32】STM32学习笔记-BKP备份寄存器和RTC实时时钟(42)

00. 目录 文章目录 00. 目录01. BKP简介02. BKP特性03. BKP基本结构04. RTC简介05. RTC主要特性06. RTC框图07. RTC基本结构08. 硬件电路09. RTC操作注意事项10. 附录 01. BKP简介 备份寄存器是42个16位的寄存器&#xff0c;可用来存储84个字节的用户应用程序数据。他们处在备…

设计模式_组合模式_Composite

案例引入 学校院系展示 编写程序展示一个学校院系结构: 需求是这样&#xff0c;要在一个页面中展示出学校的院系组成&#xff0c;一个学校有多个学院&#xff0c;一个学院有多个系 【传统方式】 将学院看做是学校的子类&#xff0c;系是学院的子类&#xff0c;小的组织继承大…

【数据结构与算法】队列(Queue)

文章目录 1. 队列介绍2.队列代码实现&#xff08;C语言&#xff09; 往期相关文章推荐&#xff1a; 1.栈之浅谈数组和链表实现栈各自的优缺点 2.线性表之链表 3.线性表之顺序表 1. 队列介绍 队列是一种特殊的线性表&#xff0c;只允许在队尾插入数据&#xff0c;在队头删除数据…