从源码看Vue的响应式原理

超详细Vue3响应式原理介绍加源码阅读 本章介绍Vue中另一个非常重要的模块,响应式。介绍了基本原理(含图)简单实现以及如何阅读源码。 阅读详情

前段时间把 vue源码抽时间看了一遍,耐心点看再结合网上各种分析文章还是比较容易看明白的,没太大问题,唯一的问题就是

看完即忘

在这里插入图片描述

当然了,也不是说啥都不记得了,大概流程以及架构这些东西还是能留下个印象的,对于 Vue的构建算是有了个整体认知,只是具体到代码级别的细节很难记住多少,不过也情有可原嘛,又不是背代码谁能记住那么多逻辑绕来绕去的东西?

不过嘛,如果能加深对这些细节的印象那也是最好不过了,于是就决定写几篇文章吧,但不可能从头到尾把 Vue全写一遍,太多了也没那时间,想来想去,响应式这个东西几年前就已经被列入《三年前端,五年面试》考试大纲,那就它吧

本文以 vue@^2.6.6 进行分析

初始化

首先找入口,vue源码的src目录下,存放的就是未打包前的代码,这个目录下又分出几个目录:

在这里插入图片描述

compiler跟模板编译相关,将模板编译成语法树,再将 ast编译成浏览器可识别的 js代码,用于生成 DOM

core就是 Vue的核心代码了,包括内置组件(slot、transition等),内置 api的封装(nextTick、set等)、生命周期、observer、vdom等

platforms跟跨平台相关,vue目前可以运行在web和 weex上,这个目录里存在的文件用于抹平平台间的 api差异,赋予开发者无感知的开发体验

server存放跟服务器渲染(SSR)相关的逻辑

sfc,缩写来自于 Single File Components,即 单文件组件,用于配合 webpack解析 .vue文件,由于我们一般会将单个组件的 template、script、style,以及自定义的 customBlocks写在一个单 .vue文件中,而这四个都是不同的东西,肯定需要在解析的时候分别抽离出来,交给对应的处理器处理成浏览器可执行的 js文件

share定义一些客户端和服务器端公用的工具方法以及常量,例如生命周期的名称、必须的 polyfill等

其他的就废话不多说了,直接进入主题,数据的响应式肯定是跟 data 以及 props有关,所以直接从 data以及 props的初始化开始

node_modules\vue\src\core\instance\state.js文件中的 initState方法用于对 props、data、methods等的初始化工作,在 new vue的时候,会调用 _init方法,此方法位于 Vue的原型 Vue.prototype上,这个方法就会调用 initState

// node_modules\vue\src\core\instance\index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
// 往 Vue构造函数的 prototyp上挂载 _init方法
initMixin(Vue)
// node_modules\vue\src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ...
    // 初始化 props  data  watch 等
    initState(vm)
    // ...
  }

initState方法如下:

// node_modules\vue\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)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

可见,在此方法中,分别调用了 initProps、initMethods、initData、initComputed、initWatch方法,这些方法中对 props、methods、data、computed、watch进行了初始化过程,本文只是分析响应式,所以其他抛开不谈,只看 initProps 和 initData

initProps中,主要是使用了一个 for...in对 props进行遍历,调用 defineReactive方法将每个 props值变成响应式的值defineReactive正是 vue响应式的核心方法,放到后面再说;

并且又调用 proxy方法把这些 props值代理到 vue上,这样做的目的是能够让直接访问 vm.xxx 得到和访问 vm._props.xxx同样的效果(也就是代理了)

上面的意思具体点就是,你定义在 props中的东西(比如:props: { a; 1 }),首先会被附加到 vm._props对象的属性上(即 vm._props.a),然后遍历 vm._props,对其上的属性进行响应式处理(对 a响应式处理),但是我们一般访问 props并没有看到过什么 this._props.a的代码,而是直接 this.a就取到了,原因就在于 vue内部已经为我们进行了一层代理

首先附加在 vm._props上的目的是方便 vue内部的处理,只要是挂载 vm._props上的数据就都是 props而不是 data或 watch什么的,而代理到 vm上则是方便开发者书写

// node_modules\vue\src\core\instance\state.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

proxy方法的原理其实就是使用 Object.defineProperty的 get 和 set方法代理了属性的访问

最后,这里面还有个 toggleObserving方法,这个方法是 vue内部对逻辑的一个优化,如果当前组件是根组件,那么根组件是不应该有 props的,但是呢,你给根组件加个 props,vue也不会报错,子组件的 props可以由父元素改变,但是根组件是没有父组件的,所以很显然根组件的 props肯定是不会改变的,也就没必要对这种 props进行依赖收集了

这里调用 toggleObserving就是禁止掉根组件 props的依赖收集

initData里做的事情跟 initProps差不多,首先,会把 data值取出放到 vm._data上,由于data的类型可以是一个对象也可以是一个函数,所以这里会判断下,如果是函数则调用 getData方法获取 data对象,否则直接取 data的值即可,不传 data的话,默认 data值是空对象 {}

let data = vm.$options.data
data = vm._data = typeof data === 'function'
  ? getData(data, vm)
  : data || {}

这个 getData其实就是执行了传入的 function类型的data,得到的值就是对象类型的 data

export function getData (data: Function, vm: Component): any {
  // ...
  // 使用 call执行 function类型的data,得到对象类型的data
  return data.call(vm, vm)
  // ...
}

另外,initData并没有直接对 data进行遍历以将 data中的值都变成是响应式的,而是另外调用 observe方法来做这件事,observe最终也调用了 defineReactive,但是在调用之前,还进行了额外的处理,这里暂时不说太多,放到后面和 defineReactive一起说;除此之外,initData也调用了 proxy进行数据代理,作用和 props调用 proxy差不多,只不过其是对 data数据进行代理

构建 Observe

现在回到上面没说的 observe 和 defineReactive,由于 observe最终还是会调用 defineReactive,所以就直接从 observe说起

observe,字面意思就是观察、观测,其主要功能就是用于检测数据的变化,由于其属于响应式,算是 vue的一个关键核心,所以其专门有一个文件夹,用于存放相关逻辑文件

// node_modules\vue\src\core\observer\index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  /// ...
  {
    ob = new Observer(value)
  }
  /// ...
}

observe方法中,主要是这一句 ob = new Observer(value),这个 Observer是一个 class类

// node_modules\vue\src\core\observer\index.js
export class Observer {
  // ...
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // ...
}

在其 constructor中,做了一些事情,这里的 new Dep()的 Dep也是跟响应式相关的一个东西,后面再说,然后调用了 def,这个方法很简单,就是调用 Object.defineProperty将当前实例(this)添加到value的 __ob__属性上:

// node_modules\vue\src\core\util\lang.js
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

vue里很多地方都用到了 Object.defineProperty,可以看出这个东西对于 vue来说还是很重要的,少了它会很麻烦,而 IE8却不支持 Object.defineProperty,所以 Vue不兼容 IE8也是有道理的

在前面的 observe方法中,也出现过 __ob__这个东西:

if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
  ob = value.__ob__
}

可以看到,__ob__在这里用于做重复校验,如果当前数据对戏 value上已经有了 __ob__属性并且此属性是由 Observer构造而来,则直接返回这个值,避免重复创建

回到 Observer类,接下里会判断 value是不是数组,如果是数组,再判断 hasProto是否为 truth值,这个 hasProto就是用于检测当前浏览器是否支持使用 __proto__的:

// node_modules\vue\src\core\util\env.js
// can we use __proto__?
export const hasProto = '__proto__' in {}

如果是就调用 protoAugment,否则调用 copyAugment,后者可以看做是前者兼容 __proto__的一个 polyfill,这两个方法的目的是一样的,都是用于改写 Array.prototype上的数组方法,以便让数组类型的数据也具备响应式的能力

换句话说,数组为什么对数组的修改,也能触发响应式呢?原因就在于 vue内部对一些常用的数组方法进行了一层代理,对这些数组方法进行了修改,关键点在于,在调用这些数组方法的时候,会同时调用 notify方法:

// node_modules\vue\src\core\observer\array.js
// notify change
ob.dep.notify()

ob就是 __ob__,即数据对象上挂载的自身的观察者,notify就是观察者的通知事件,这个后面放到 defineReactive一起说,这里调用 notify告诉 vue数据发生变化,就触发了页面的重渲染,也就相当于是数组也有了响应式的能力

完了之后,继续调用 observeArray进行深层便利,以保证所有嵌套数据都是响应式的

接上面,如果是对象的话就无需那么麻烦,直接调用 this.walk方法:

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

walk方法会对传入的对象进行遍历,然后对每一个遍历到的数据调用 defineReactive方法,终于到这个方法了,无论是 props的初始化还是 data的初始化最后都会调用这个方法,前面那些都是一些差异性的分别处理

大概看一眼 defineReactive这个方法,最后调用的 Object.defineProperty很显眼,原来是在这个函数中修改了属性的 get 以及 set,这两个方法很重要,分别对应所谓的 依赖收集 和 派发更新

先上个上述所有流程的简要示意图,有个大体印象,不然说得太多容易忘

在这里插入图片描述

依赖收集

先看 get

// node_modules\vue\src\core\observer\index.js
get: function reactiveGetter () {
  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
}

首先,如果当前属性以及显式定义了 get方法,则执行这个 get获取到值,接着判断 Dep.target

这里又出现了一个新的东西: Dep,这是一个 class类,比较关键,是整个依赖收集的核心

// node_modules\vue\src\core\observer\dep.js

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
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]
}

进入 Dep的定义,此类的静态属性 target初始化的值是 null,但是可以通过两个暴露出去的方法来修改这个值

另外,在 Dep.target = null的上面还有一段注释,主要是说由于同一时间只能有一个 watcher被执行(当前执行完了再进行下一个),而这个 Dep.target的指向就是这个正在执行的 watcher,所以 Dep.target就应该是全局唯一的,这也正是为什么 target是个静态属性的原因

那么现在由于 Dep.target是 null,不符合 if(Dep.target){},所以这个值肯定在什么地方被修改了,而且应该是通过 pushTarget 或 popTarget来修改的

所以什么地方会调用这两个方法?

这又得回到 get了,什么时候会调用 get?访问这个属性,也就是数据的时候就会调用这个数据的 get(如果有的话),什么时候会访问数据呢?当然是在渲染页面的时候,肯定需要拿到数据来填充模板

那么这就是生命周期的事了,这个过程应该发生在 beforeMount 和 mount中间

// node_modules\vue\src\core\instance\lifecycle.js

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

主要是 new Watcher这句代码,as we all konw,vue 使用观察者模式实现响应式逻辑,前面的 Observe是监听器,那么这里的 Watcher就是观察者,数据的变化会被通知给 Watcher,由 Watcher进行视图更新等操作

进入 Watcher方法

其构造函数 constructor的最后:

this.value = this.lazy
    ? undefined
    : this.get()

this.lazy是传入的修饰符,暂时不用管,这里可以认为直接调用 this.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
}

可以看到,在 Watcher的 get方法中,上来就调用了 pushTarget方法,所以就把当前这个 watcher push到 targetStack(位于 Dep的定义文件中)数组中去了,并且把 Dep.target的值置为这个 watcher

所以,从这里可以看出 targetStack数组的作用就是类似于一个栈,栈内的项就是 watcher

try...catch...finally的 finally语句中,首先根据 this.deep来决定是否触发当前数据子属性的 getter,这里暂时不看,然后就是调用 popTarget,这个方法就是将当前 watcher出栈,并将 Dep.target指向上一个 watcher

然后 this.cleanupDeps()其实就是依赖清空,因为已经实现了对当前 watcher的依赖收集,Dep.target已经指向了其他的 watcher,所以当前 watcher的订阅就可以取消了,腾出空间给其他的依赖收集过程使用

接着执行 value = this.getter.call(vm, vm),这里的 this.getter就是:

// node_modules\vue\src\core\instance\lifecycle.js
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

_update 和 _render都是挂载在 Vue.prototype上的方法,跟组件更新相关,vm._render方法返回一个 vnode,所以肯定涉及到数据的访问,不然怎么构建 vnode,既然访问数据,那么就会调用数据的 get方法(如果有的话)

那么就又回到前面了:

// node_modules\vue\src\core\observer\index.js
get: function reactiveGetter () {
  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
}

经过上面 Watcher的构建过程,可以知道这个时候 Dep.target其实的指向已经已经被更正为当前的 watcher了,也就是 trueth值,可以进入条件语句

首先执行 dep.depend(),dep是在 defineReactive方法中 new Dep的实例,那么看下 Dep的 depend方法

// node_modules\vue\src\core\observer\dep.js
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

Dep.target此时条件成立,所以继续调用 Dep.target上的 addDep方法,Dep.target指向 Watcher,所以看 Watcher的 addDep方法

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

首先通过 id避免重复添加同一数据,最后又调用了 dep.addSub将当前 Watcher添加到 Dep中去

这里出现了几个变量,newDepIds、newDeps、depIds、deps,这几个变量其实就是在 Dep添加 watcher之前的一次校验,以及方便后续移除订阅,提升 vue的性能,算是 vue内部一种优化策略,这里不用理会

// node_modules\vue\src\core\observer\dep.js
addSub (sub: Watcher) {
  this.subs.push(sub)
}

最终,在 Dep中,会把 watcher push到 Dep的 subs数组属性中

即,最终 props和 data的响应式数据的 watcher都将放到 Dep的 subs中,这就完成了一次依赖收集的过程

继续回到 defineReactive,在调用了 dep.depend()之后,还有几行代码:

// node_modules\vue\src\core\observer\index.js
let childOb = !shallow && observe(val)
// ...
if (childOb) {
  childOb.dep.depend()
  if (Array.isArray(value)) {
    dependArray(value)
  }
}

递归调用 observe,保证子属性也是响应式的,如果当前值是数组,那么保证这个数组也是响应式的

这个依赖收集过程,简要示意图如下:

在这里插入图片描述

派发更新

依赖收集的目的就是将所有响应式数据通过 watcher收集起来统一管理,当数据发生变化的时候,就通知视图进行更新,这个更新的过程就是派发更新

继续看 defineReactive的 set方法,这个方法实现派发更新的主要逻辑

// node_modules\vue\src\core\observer\index.js
set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val
  /* eslint-disable no-self-compare */
  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
  if (getter && !setter) return
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  childOb = !shallow && observe(newVal)
  dep.notify()
}

首先是一系列的验证判断,可以不用管,然后设置数据的值为传入的值,这是一般 set函数都会执行的方法

然后到 childOb = !shallow && observe(newVal),一般情况下,shallow都是 trueth值,所以会调用 observe,经过上面的分析,我们知道这个 observe就是依赖收集相关的东西,这里的意思就是对新设置的值也进行依赖收集,加入到响应式系统中来

接下来这行代码才是关键:

dep.notify()

看下 Dep:

// node_modules\vue\src\core\observer\dep.js
notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  // ...
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

notify方法中,遍历了 subs,对每个项调用 update方法,经过前面的分析我们知道,subs的每个项其实都是依赖收集起来的 watcher,这里也就是调用了 watcher的 update方法,通过 update来触发对应的 watcher实现页面更新

所以,Dep其实就是一个 watcher管理模块,当数据变化时,会被 Observer监测到,然后由 Dep通知到 watcher

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

this.lazy 跟 computed相关,computed是惰性求值的,所以这里只是把 this.dirty设为 true,并没有做什么更新的操作;

this.sync跟 watch相关,如果 watch设置了这个值为 true,则是显式要求 watch更新需要在当前 Tick 一并执行,不必放到下一个 Tick

这两个暂时不看,不扩充太多避免逻辑太乱,正常流程会执行 queueWatcher(this)

// node_modules\vue\src\core\observer\scheduler.js
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

queueWatcher首先会根据 has[id]来避免同一 watcher的重复添加,接下来引入了队列的概念,vue并不会在每次数据改变的时候就立即执行 watcher重渲染页面,而是把这些 watcher 先推送到一个队列里,然后在nextTick 里调用 flushSchedulerQueue批量执行这些 watcher,更新 DOM

这里在 nextTick里执行 flushSchedulerQueue的目的就是为了要等到当前 Tick中所有的 watcher都加入到 queue中,再在下一 Tick中执行队列中的 watcher

看下这个 flushSchedulerQueue方法,首先对队列中的 watcher根据其 id进行排序,将 id小的 watcher放在前面(父组件 watcher的 id小于子组件的), 排序的目的也已经在注释中解释地很清楚了:

// node_modules\vue\src\core\observer\scheduler.js

// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
//    created before the child)
// 2. A component's user watchers are run before its render watcher (because
//    user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
//    its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)

大概意思就是,在清空队列之前对队列进行排序,主要是为了以下 3点

  • 组件的更新是由父到子的(因为父组件的创建在子组件之前),所以 watcher的创建也应该是先父后子,执行顺序也应该保持先父后子

  • 用户自定义 watcher应该在 渲染 watcher之前执行(因为用户自定义 watcher的创建在 渲染watcher之前)

  • 如果一个组件在父组件的 watcher 执行期间被销毁,那么这个子组件的 watcher 都可以被跳过

排完序之后,使用了一个 for循环遍历队列,执行每个 watcher的 run方法,那么就来看下这个 run方法

// node_modules\vue\src\core\observer\watcher.js
run () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      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.active,这个 this.active的初始值是 true,那么什么时候会变成 false呢?当 watcher从所有 Dep中移除的时候,也就是这个 watcher移除掉了,所以也就没有什么派发更新的事情了

// node_modules\vue\src\core\observer\watcher.js
teardown () {
  // ...
  this.active = false
}

接着执行 const value = this.get()获取到当前值,调用 watcher的 get方法的时候会执行 watcher的 getter方法:

// node_modules\vue\src\core\observer\watcher.js
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    // ...
  }
  // ...
  return value
}

而这个 getter前面已经说了,其实就是:

// node_modules\vue\src\core\instance\lifecycle.js
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

也就是执行了 DOM更新的操作

回到 flushSchedulerQueue,在执行完 watcher.run()之后,还有些收尾工作,主要是执行了 resetSchedulerState方法

// node_modules\vue\src\core\observer\scheduler.js
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

这个方法主要是用于重置队列状态,比如最后将 waiting、flushing置为 false,这样一来,当下次调用 queueWatcher的时候,就又可以往 queue队列里堆 watcher了

回到 queueWatcher这个方法

if (!flushing) {
  queue.push(watcher)
} else {
  // if already flushing, splice the watcher based on its id
  // if already past its id, it will be run next immediately.
  let i = queue.length - 1
  while (i > index && queue[i].id > watcher.id) {
    i--
  }
  queue.splice(i + 1, 0, watcher)
}

当 flushSchedulerQueue执行,进行批量处理 watcher的时候,flushing将被置为 true,这个时候如果再次添加新的 user watcher进来,那么就会立即添加到 queue中去

这里采取改变 queue的方式是原数组修改,也就是说添加进去的 watcher会立即加入到 flushSchedulerQueue批处理的进程中,因而在 flushSchedulerQueue中对 queue的循环处理中,for循环是实时获取 queue的长度的

// node_modules\vue\src\core\observer\scheduler.js
function flushSchedulerQueue () {
  // ...
  for (index = 0; index < queue.length; index++) {
    // ...
  }
  // ...
}

另外,新加入的 watcher加到 queue的位置也是根据id进行排序的,契合上面所说的 watch执行先父后子的理念

大体流程示意图如下:

在这里插入图片描述

总结

vue的代码相比于 react的其实还是挺适合阅读的,我本来还打算打断点慢慢看,没想到根本没用到,这也表明了vue的轻量级确实是有原因的

少了各种模式和各种系统的堆砌,但同时又能满足一般业务的开发需要,代码体积小意味着会有更多的人有兴趣将其接入移动端,概念少意味着小白也能快速上手,俗话说得小白者得天下,vue能与 react这种顶级大厂团伙化规模维护的框架库分庭抗礼也不是没有道理的

工业缺陷检测深度学习方法综述 异常:点异常、 上下文异常和集群异常。点异常:又称为离群值 (outliers)[9], 描述数值上偏离正 常样本的独立数据。与异常相近的概念还包括新颖点(novelty)和分布外数据。上下文异常:同样描述数据点,其数值属于正常范围,但不符合局部上下文规律。集群异常:描述一系列相关数据的集合,集合中的每一个实例的数值在单独考察时都处于正常值域,但集 合整体的相关性特征不服从正常模式。一般的图像异常检测往往仅需区分正常与异常样本,而工业缺陷检测更关注于检测 图像中的异常像素. 阅读详情

相关推荐

TMS320F280049C 学习笔记21 交叉开关 Crossbar (X-BAR)

简单介绍了X-BAR。

whyorwhnt的专栏 2万+

Vue源码解析:Vue生命周期之从生到死(三)

上俩片文章我们讲到了initLifecycle和initEvents函数,本篇是时候讲讲initInjections初始化函数了。该函数就是用来去实例化inject选项。说到inject,那就必然离不开provide,这俩是成对出现的,它们的作用是:允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。 provide:是一个对象,或者是一个返回对象的函数。 inject:是一个字符串数组,或者是一个多层对象。 这俩个东西,在组件中用的比较多。总结来说

lee前端技术站 1947

别再到处找模板了!我用这套软著申请材料(含用户手册+源代码模板)两个月搞定

本文提供了一套高效的软著申请材料模板,包括用户手册和源代码模板,帮助开发者两个月内快速完成申请。通过实战指南和常见误区解析,详细介绍了材料准备的规范与技巧,确保格式合规性,大幅提升审核通过率。

weixin_29268637的博客 548

vector 不初始化时什么状态_Vue原理解析(三):初始化时created之前做了什么?...

让我们继续this._init()的初始化之旅,接下来又会执行这样的三个初始化方法:initInjections(vm) initState(vm) initProvide(vm)5. initInjections(vm): 主要作用是初始化inject,可以访问到对应的依赖。inject和provide这里需要简单的提一下,这是vue@2.2版本添加的一对需要一起使用的API,它允许父级组件向它...

weixin_39674414的博客 328

❤️爆肝十二万字《python从零到精通教程》,从零教你变大佬❤️(建议收藏)

文章目录强烈推荐系列教程,建议学起来!!一.pycharm下载安装二.python下载安装三.pycharm上配置python四.配置镜像源让你下载嗖嗖的快4.1pycharm内部配置4.2手动添加镜像源4.3永久配置镜像源五.插件安装(比如汉化?)5.1自动补码神器第一款5.2汉化pycharm5.3其它插件六.美女背景七.自定义脚本开头八、这个前言一定要看九、python入门十、python缩进十一、Python注释1.单行注释2.多行注释十二、Python 变量1.变量定义理解2. 变量名命名3. 分

全栈川川 26万+

Vue3 + uniapp 踩坑:iOS 真机调试一直报错Maximum call stack size exceeded,和(in promise)MiniProgramError err的解决方法

不是你写错代码,是微信开发工具和 iOS 真机调试的兼容问题。开个空项目都报错,勾个设置就没问题,不是你,是工具的锅。

代码简单说 Vue、JAVA、PHP、Node.js 熟练运用,接口、架构、性能全搞定。 1265

vue响应式原理

vue响应式原理 vue响应式也叫作数据双向绑定,大致原理阐述: ​ 首先我们需要通过Object.defineProperty()方法把数据(data)设置为getter和setter的访问形式,这样我们就可以在数据被修改时在setter方法设置监视修改页面信息,也就是说每当数据被修改,就会触发对应的set方法,然后我们可以在set方法中去调用操作dom的方法。 ​ 此外,如果页面有input用v-model绑定数据,我们需要在这种绑定了data的input元素上添加监听,添加input事件监听,每.

weixin_48181168的博客 3万+

大厂校招:星宸科技嵌入式面试及参考答案(5万字长文)

当一个新的硬件设备被插入系统时,内核会发送一个设备事件通知 udev,udev 会根据设备的属性和规则,为该设备创建一个合适的设备文件。通过去除不必要的驱动程序、禁用不需要的功能、选择合适的内核配置选项、优化内核编译选项和定制内核模块等方法,可以减小内核的大小和复杂性,提高系统的性能和稳定性。上半部通常是在中断关闭的情况下执行的,以确保中断处理的原子性。处理碎片化内存是内存管理中的一个重要问题,可以采用内存压缩、内存池管理、伙伴系统、首次适应算法和最佳适应算法等方法来减少内存碎片的产生,提高内存的利用率。

大模型大数据攻城狮的专栏 2924

vue的响应式原理

一、reduce 1.数值的累加 作用:将****前一项*和*后一项****的值进行运算,返回累积的结果 格式:数组.reduce(function(prev,next){…}) 其中,prev表示前一项,next表示后一项。 运算规则: 默认情况下,会把数组的第一个元素作为prev的初始值。 每循环一次,把累积的结果赋给prev,next就变为下一个数组元素 var arr3 = [10,22,23,25,50]; const total = arr3.reduce(function(pre,ne

垂耳兔的博客 9169

手把手教你用 Cursor 写一个完整的 Web 应用(含前后端 + 部署)

本文介绍如何利用Cursor AI工具从零开发一个完整的全栈待办事项Web应用。文章针对开发者常见痛点——无法独立完成项目闭环,提供了一套AI驱动的全栈开发范式。通过Vue3+Node.js+MySQL技术栈,全程由Cursor自动生成项目架构、前后端代码、数据库设计,并解决联调、部署等关键问题。重点展示了如何用AI完成:项目规划与架构设计、后端接口开发、前端页面实现、跨域联调、打包优化及云端部署全流程。该方法将传统需要3-5天的开发工作压缩至1小时内完成,产出规范可上线的项目,特别适合想突破"碎

远程部署调试 运行安装 项目调试 二次开发 项目技术新 持续迭代 部分源码免费分享 633

为YOLOv11引入自适应特征融合模块(ASFF)

本文探讨了在YOLOv11中引入ASFF(自适应空间特征融合)模块以解决多尺度目标检测性能不均衡的问题。作者分析了传统FPN固定融合策略的缺陷,详细介绍了ASFF模块的实现原理,包括动态权重生成网络设计和特征融合机制。通过将ASFF集成到YOLOv11颈部网络,实现了特征在双向流动时的自适应融合,在复杂场景下mAP提升1.2%而推理时间仅增加3-5ms。文章还分享了训练调参技巧和部署注意事项,指出ASFF特别适合交通监控等目标尺度变化大的场景,但强调需根据实际需求平衡模型复杂度与性能。最后提供了工程实践建议

as1225120794的博客 320

Vue 源码分析:深入响应式原理

简单来说,响应式就是当数据发生变化时,视图能够自动更新。在 Vue 中,当我们修改一个响应式数据时,所有依赖于该数据的视图都会自动重新渲染。在创建 Vue 实例时,Vue 会遍历 data 对象的所有属性,并使用observe函数将它们转换为响应式属性。当我们在模板中使用响应式数据时,Vue 会创建一个 Watcher 对象来监听该数据的变化。在 Watcher 对象的get()方法中,会触发响应式数据的 getter 函数。在 getter 函数中,会将当前的 Watcher 对象添加到该数据的。

q68686的博客 1262

【好玩的游戏项目】使用Docker快速部署属于你的RPG网页小游戏

【好玩的游戏项目】使用Docker快速部署属于你的RPG网页小游戏

jks212454的博客 5572

vue2源码解析——响应式原理

Vue2的响应式原理核心是通过数据劫持、依赖收集和视图更新实现数据驱动视图。首先使用Object.defineProperty对数据进行递归劫持,在getter中收集依赖(Watcher实例),在setter中触发更新。当数据变化时,会通知相关Watcher执行回调函数,从而实现精准的局部更新。整个过程通过observe、Observer、defineReactive三个函数相互调用形成递归,确保嵌套对象也能被深度监听。这种机制避免了数据变化时全量更新视图的性能问题,实现了高效精准的响应式更新。

2202_75345049的博客 1056

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

源码解析 深入vue响应式原理

深入响应式原理 大家应该都知道响应式原理的核心API是Object.defineProperty。如果不了解请点击: Object.defineProperty 去MDN了解下这个API的用途。 那这个API是如何做到响应式对象的呢。 接下来我们从源码的角度来进行分析: 我们知道在组件的data中定义的数据,最终都能响应式,看下Vue2.x的源码是如何初始化data的: export function initState (vm: Component) { vm._watchers = [] con

m0_52544128的博客 1691

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

从原理和源码理解Vue3的响应式机制

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。

发果叁 540

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

vue源码解析和vue响应式原理

源码的获取项目地址:为什么分析? 新的版本发布后,现有项目不会升级到,还有很长的一段过渡期。项目地址源码目录结构(在目录下面定义的就是源码内容): ​ 在目录是的核心库,在目录下面,也定义了很多的文件夹,下面我们先简单来看一下。目录下面定义的是组件。:定义的是中的静态方法。,,,等。:创建的实例,定义了的构造函数,初始化,以及生命周期的钩子函数等。:定义响应式机制的位置,:定义公共成员。:定义虚拟这里我们来介绍一下,关于源码中使用的打包方式。打包工具 所使用的打包工具为,比更加轻量,是把所有的文件(

panerchao的博客 1098

vue源码学习一之响应式原理及虚拟DOM

@TOC 文章内容输出来源:拉勾教育大前端高薪训练营 在拉钩训练营学习已经有一段时间了,感觉这段时间的收获远比自己独自学习强的多,自己学习的时候经常会因为惰性,无法坚持,在这里有班主任时刻关注你的学习进度(感觉对我这种懒人蛮好的),最重要的是有了一个学习的计划,不用无头苍蝇一样东一点西一点,最后什么都没记住。训练营的课程基本都是 准备工作 Vue 源码的获取 项目地址:https://github.com/vuejs/vue Fork 一份到自己仓库,克隆到本地,可以自己写注释提交到 github 为

weixin_44213825的博客 1084

Vue3 源码解析(六):响应式原理与 reactive

在今天的文章中,笔者先带大家回顾了 Vue2 的响应式原理,又开始介绍 Vue3 的响应式原理,通过比较 Vue2 和 Vue3 的响应式系统的区别引出 Vue3 响应式系统的提升之处,尤其是其中最主要的调整将 Object.defineProperty 替换为 Proxy 代理对象。为了让大家属性 Proxy 对响应式系统的影响,笔者着重介绍了响应式基础 API:reactive。

rita_0567的博客 1398

Vue.set 响应式原理详解:源码级逐行带入实战解析

Vue2的响应式系统通过Vue.set实现动态添加响应式属性,核心流程如下:1) 检查目标是否为数组并处理;2) 若属性已存在直接赋值;3) 判断是否为响应式对象(含__ob__);4) 调用defineReactive添加getter/setter,创建Dep依赖收集器;5) 通过ob.dep.notify()触发视图更新。整个过程通过Object.defineProperty将新属性转为响应式,并建立属性与依赖的关联机制,确保数据变化时可自动更新视图。

apple_70049717的博客 1567

Vue源码解析-响应式原理

Vue源码解析-响应式原理课程目标 课程目标 Vue.js的静态成员和实例成员的初始化过程 初次渲染过程 数据响应式原理

shenzhenya的博客 877

Vue源码详解(一)- 响应式原理

原文链接:Vue源码详解(一)- 响应式原理

NovenQingTang的博客 211

源码分析vue响应式原理

源码分析vue响应式原理 经过上篇简单实现vue响应式原理之后, 我们已经简略地了解了响应式是怎么通过发布-订阅模式来实现的,这篇主要从源码分析Vue对响应式原理的实现。 找到initData 在vue源码入口文件vue/src/core/index.js中,可以看到import Vue from './instance/index',导入了Vue这个对象。 在vue/src/core/instance/index.js中, import { initMixin } from './init' //..

Jiuto_crx的博客 445

【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher

我们在使用Vue时,只需要修改数据,视图就会自动更新,这就是数据响应 今天来学习Vue实现数据响应式的原理~

YK菌的博客 5682

Vue.js 框架源码与进阶 - Vue.js 源码剖析 - 响应式原理

1.3 了解 Flow 官网:https://flow.org/ JavaScript 的静态类型检查器 Flow 的静态类型检查错误是通过静态类型推断实现的 文件开头通过 // @flow 或者 /* @flow */ 声明 具体的使用方式请参考博客 JavaScript 深度剖析 - 类型系统 + TypeScript 1.4 调试设置 打包工具 Rollup安装依赖 设置 sourcemap(代码地图) 执行 dev调试术语Runtime + Compiler(完整版) vs Run

每天进步一点点 1878

Vue2.x源码解读-响应式原理剖析

准备工作 下载vue源码,可以先将vue项目fork到自己的github仓库,然后在clone自己仓库的vue,这样在解读源码的时候可以随时添加注释,并将注释提交到自己的仓库。 源码代码主要结构说明: dist:打包生成的文件 examples:实例代码目录 src:源码文件目录 compiler:编译器相关代码,把template模板转化成render函数 core:核心代码 components:定义vue自带的keep-alive组件 global-api:定义vue中的今天方法,包括m

wsq_bsdn的博客 425
上一篇: 跟混乱的页面弹窗说再见
下一篇: canvas&&CSS&&SVG三种实现仪表盘的方式
Quiet-Night
博客等级 码龄10年 159粉丝 84原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值