SoFunction
Updated on 2025-04-07

Detailed analysis of the principle of vue responsiveness

Preface

As the core of Vue, the responsive principle uses data hijacking to implement data-driven views. It is a knowledge point that is often tested in interviews and is also a plus for interviews.

This article will analyze the workflow of the responsive principle step by step, mainly the following structure:

  1. Analyze key members and understanding them helps understand the process
  2. Split the process and understand its role
  3. Based on the above points, understand the overall process

The article is a little longer, but most of it is implemented by code. Please watch it patiently. In order to facilitate understanding of the principles, the code in the article will be simplified. If you can, please learn it according to the source code.

Key members

In the responsive principle, the three classes Observe, Watcher, and Dep are the main members of the complete principle.

  • Observe, the entry to the responsive principle, processing observation logic according to data type
  • Watcher is used to perform update rendering. The component will have a rendering Watcher. What we often call collecting dependencies is to collect Watchers.
  • Dep, dependency collector, the attributes will have a Dep, which will facilitate the corresponding dependency trigger update when changes occur.

Let’s take a look at the implementation of these classes, which main properties and methods are included.

Observe: I will observe the data

Warm reminder: Explanation of the serial number below the code block corresponding to the serial number

// Source code location: /src/core/observer/class Observe {
 constructor(data) {
   = new Dep()
  // 1
  def(data, '__ob__', this)
  if ((data)) {
   // 2
   protoAugment(data, arrayMethods)
   // 3
   (data)
  } else {
   // 4
   (data)
  }
 }
 walk(data) {
  (data).forEach(key => {
   defineReactive(data, key, data[key])
  })
 }
 observeArray(data) {
  (item => {
   observe(item)
  })
 }
}
  1. Add the __ob__ attribute to the observed property, whose value is equal to this, that is, the current instance of Observe
  2. Adding overridden array methods to arrays, such as push, unshift, splice, etc. The purpose of rewriting is to update and render when calling these methods
  3. Observe the data in the array, and observe will call new Observe internally to form recursive observations.
  4. Observe object data, defineReactive defines get and set for data, that is, data hijacking

Dep: I will depend on data collection

// Source code location: /src/core/observer/let id = 0
class Dep{
 constructor() {
   = ++id // dep unique identifier   = [] // Storage Watcher }
 // 1
 depend() {
  (this)
 }
 // 2
 addSub(watcher) {
  (watcher)
 }
 // 3
 notify() {
  (watcher => ())
 }
}

// 4
 = null

export function pushTarget(watcher) {
  = watcher
} 

export function popTarget(){
  = null
}

export default Dep
  1. According to the main method of collecting dependencies, it is a watcher instance
  2. Add a watcher to the array, that is, add dependencies
  3. When the property changes, the notify method will be called to notify each dependency to update.
  4. Used to record watcher instances, it is globally unique, and its main purpose is to find the corresponding watcher in the process of collecting dependencies.

The two methods pushTarget and popTarget are obviously used to set. It is also a key point. This concept may be a bit difficult to understand when viewing the source code for the first time. In the subsequent process, its function will be explained in detail. Pay attention to the content of this part.

Watcher: I will trigger view update

// Source code location: /src/core/observer/let id = 0
export class Watcher {
 constructor(vm, exprOrFn, cb, options){
   = ++id // watcher unique identifier   = vm
   = cb
   = options
  // 1
   = exprOrFn
   = []
   = new Set()

  ()
 }
 run() {
  ()
 }
 get() {
  pushTarget(this)
  ()
  popTarget(this)
 }
 // 2
 addDep(dep) {
  // Prevent repeated addition of dep  if (!()) {
   ()
   (dep)
   (this)
  }
 }
 // 3
 update() {
  queueWatcher(this)
 }
}
  1. What is stored is a function that updates the view
  2. watcher stores dep, and dep also stores watcher for two-way recording
  3. Trigger update, queueWatcher is for asynchronous update, asynchronous update will call the run method to update the page

Responsive principle process

We all have a rough understanding of the functions that the above members have. Combining them below, let’s see how these functions work in the responsive principle process.

Data Observation

When data is initialized, the Observe class will be created through the observe method.

// Source code location: /src/core/observer/export function observe(data) {
 // 1
 if (!isObject(data)) {
  return
 }
 let ob;
 // 2
 if (('__ob__') && data.__ob__ instanceof Observe) {
  ob = data.__ob__
 } else {
  // 3
  ob = new Observe(data)
 }
 return ob
}

During initialization, the data obtained by observe is the object we return in the data function.

  1. The observe function only observes object-type data
  2. The observed data will be added to the __ob__ attribute. By judging whether the attribute exists, it will prevent repeated observations.
  3. Create an Observe class and start processing observation logic

Object Observation

Entering the Observe internal, since the initialized data is an object, the walk method will be called:

walk(data) {
 (data).forEach(key => {
  defineReactive(data, key, data[key])
 })
}

The internal use of defineReactive method hijacking data is the most core part of implementing the responsive principle.

function defineReactive(obj, key, value) {
 // 1
 let childOb = observe(value)
 // 2
 const dep = new Dep()
 (obj, key, {
  get() {
   if () {
    // 3
    ()
    if (childOb) {
     ()
    }
   }
   return value
  },
  set(newVal) {
   if (newVal === value) {
    return
   }
   value = newVal
   // 4
   childOb = observe(newVal)
   // 5
   ()
   return value
  }
 })
}
  1. Since the value may be an object type, you need to call observe for recursive observation here
  2. The dep here means that every property mentioned above will have a dep, which exists as a closure, responsible for collecting dependencies and notifying updates.
  3. When initialized, it is the rendering watcher of the component. The dependency collected here is this watcher, which mainly collects dependencies for arrays.
  4. The new value set may be an object type, and the new value needs to be observed
  5. The value changes, notify the watcher to update, which is the trigger point when we can update the page in real time after changing the data.

After defining the attribute, the get callback triggers the get callback and the setting of the attribute triggers the set callback to achieve responsive updates.

Through the above logic, we can also find out why Vue3.0 uses Proxy instead. Only a single attribute can be defined. If the attribute is an object type, it also needs to be observed recursively, which will consume performance. Proxy is the proxy for the entire object, and a callback will be triggered as long as the attribute changes.

Array Observation

For array type observations, the observeArray method is called:

observeArray(data) {
 (item => {
  observe(item)
 })
}

Unlike objects, it performs observe to observe the object types in the array, and does not define each item in the array, that is, the terms in the array do not have dep.

Therefore, when we modify the item through the array index, the update will not be triggered. But this.$set can be used to modify and trigger updates. So the question is, why does Vue design this way?

Combined with actual scenarios, multiple items of data are usually stored in the array, such as list data. This will consume performance when observed. Another reason is that generally, modifying array elements rarely replace the entire element directly through the index. For example:

export default {
  data() {
    return {
      list: [
        {id: 1, name: 'Jack'},
        {id: 2, name: 'Mike'}
      ]
    }
  },
  cretaed() {
    // If you want to modify the value of name, this is usually used    [0].name = 'JOJO'
    // instead of the following    // [0] = {id:1, name: 'JOJO'}
    // Of course you can update this    // this.$set(, '0', {id:1, name: 'JOJO'})
  }
}

Array method rewrite

When array elements are added or deleted, the view will be updated. This is not taken for granted, but Vue internally rewritten the methods of the array. When these methods are called, the array will update the detection and trigger the view update. These methods include:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

Return to Observe's class, when the observed data type is an array, the protoAugment method is called.

if ((data)) {
 protoAugment(data, arrayMethods)
 // Observe the array (data)
} else {
 // Observe the object (data)
}

In this method, the array prototype is replaced with arrayMethods. When calling a method that changes the array, the rewrite method is preferred.

function protoAugment(data, arrayMethods) {
 data.__proto__ = arrayMethods
}

Next, let’s see how arrayMethods is implemented:

// Source code location: /src/core/observer/// 1
let arrayProto = 
// 2
export let arrayMethods = (arrayProto)

let methods = [
 'push',
 'pop',
 'shift',
 'unshift',
 'reverse',
 'sort',
 'splice'
]

(method => {
 arrayMethods[method] = function(...args) {
  // 3
  let res = arrayProto[method].apply(this, args)
  let ob = this.__ob__
  let inserted = ''
  switch(method){
   case 'push':
   case 'unshift':
    inserted = args
    break;
   case 'splice':
    inserted = (2)
    break;
  }
  // 4
  inserted && (inserted)
  // 5
  ()
  return res
 }
})
  1. Save the prototype of the array, because the rewrited array method still needs to call the native array method
  2. arrayMethods is an object that saves overridden methods. The object is created using (arrayProto) so that users can inherit and use native methods when calling non-rewrite methods.
  3. Call native methods to store the return value, and set the return value of the rewrite function
  4. inserted stores the newly added value. If inserted exists, observe the new value
  5. Trigger view update

Dependency collection

Dependency collection is a prerequisite for view updates and a crucial link in the responsive principle.

Pseudocode flow

For easy understanding, here is a piece of pseudo-code to roughly understand the process of dependency collection:

// data datalet data = {
  name: 'joe'
}

// Render watcherlet watcher = {
  run() {
     = watcher
    ()
  }
}

// dep
let dep = [] // Storage dependencies = null // Record watcher
// Data hijacking(data, 'name', {
  get(){
    // Collect dependencies    ()
  },
  set(newVal){
     = newVal
    (watcher => {
      ()
    })
  }
})

initialization:

  1. First, get and set will be defined for the name attribute
  2. Then the initialization will be performed once.
  3. At this time, get , trigger the get function to collect dependencies.

renew:

Modify , trigger the set function, and call run to update the view.

Real process

Let’s take a look at how the real dependency collection process is carried out.

function defineReactive(obj, key, value) {
 let childOb = observe(value)
 const dep = new Dep()
 (obj, key, {
  get() {
   if () {
    () // Collect dependencies    if (childOb) {
     ()
    }
   }
   return value
  },
  set(newVal) {
   if (newVal === value) {
    return
   }
   value = newVal
   childOb = observe(newVal)
   ()
   return value
  }
 })
}

First, initialize the data and call defineReactive function to hijack the data.

export class Watcher {
 constructor(vm, exprOrFn, cb, options){
   = exprOrFn
  ()
 }
 get() {
  pushTarget(this)
  ()
  popTarget(this)
 }
}

Initialize the watcher to start rendering the page. Rendering the page requires data value, triggering a get callback, and collecting dependencies.

class Dep{
 constructor() {
   = id++
   = []
 }
 depend() {
  (this)
 }
}

For watcher, call the addDep method and pass in the dep instance.

export class Watcher {
 constructor(vm, exprOrFn, cb, options){
   = []
   = new Set()
 }
 addDep(dep) {
  if (!()) {
   ()
   (dep)
   (this)
  }
 }
}

After adding dep to addDep, call and pass in the current watcher instance.

class Dep{
 constructor() {
   = id++
   = []
 }
 addSub(watcher) {
  (watcher)
 }
}

Collect the incoming watchers, and the dependency collection process is completed.

To add, usually many attribute variables are bound to the page, and rendering will take values ​​for attributes. At this time, the dependencies collected by each attribute are the same watcher, that is, the component's rendering watcher.

Array dependency collection

(method => {
 arrayMethods[method] = function(...args) {
  let res = arrayProto[method].apply(this, args)
  let ob = this.__ob__
  let inserted = ''
  switch(method){
   case 'push':
   case 'unshift':
    inserted = args
    break;
   case 'splice':
    inserted = (2)
    break;
  }
  // Observation of new values  inserted && (inserted)
  // Update the view  ()
  return res
 }
})

Remember that in the rewritten method, the update view will be called. __ob__ is the identifier we defined for the observation data in Observe, and the value is an Observe instance. So where are the dependencies collected?

function defineReactive(obj, key, value) {
 // 1
 let childOb = observe(value)
 const dep = new Dep()
 (obj, key, {
  get() {
   if () {
    ()
    // 2
    if (childOb) {
     ()
    }
   }
   return value
  },
  set(newVal) {
   if (newVal === value) {
    return
   }
   value = newVal
   childOb = observe(newVal)
   ()
   return value
  }
 })
}
  1. The return value of the observe function is an instance of Observe
  2. Execute, add dependencies to the dep of the Observe instance

So when the array is updated, the dependencies have been collected.

Overall process

Let’s review the initialization process and update process. If you are looking at the source code for the first time and don’t know where to start, you can also refer to the following order. Since the source code is implemented more, the source code shown below will slightly delete some code

Initialization process

Entry file:

// Source code location: /src/core/instance/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:

// Source code location: /src/core/instance/export function initMixin (Vue: Class<Component>) {
 ._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 merges the mixin option and the passed options option   // The $options here can be understood as an object passed in when new Vue   vm.$options = mergeOptions(
    resolveConstructorOptions(),
    options || {},
    vm
   )
  }

  // expose real self
  vm._self = vm
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  // Initialize the data  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')

  if (vm.$) {
   // Initialize the rendering page Mount the component   vm.$mount(vm.$)
  }
 }
}

The above focuses on two functions, initState initializes data, and vm.$mount(vm.$) initializes the rendering page.

Enter initState first:

// Source code location: /src/core/instance/export function initState (vm: Component) {
 vm._watchers = []
 const opts = vm.$options
 if () initProps(vm, )
 if () initMethods(vm, )
 if () {
  // data initialization  initData(vm)
 } else {
  observe(vm._data = {}, true /* asRootData */)
 }
 if () initComputed(vm, )
 if ( &&  !== nativeWatch) {
  initWatch(vm, )
 }
}

function initData (vm: Component) {
 let data = vm.$
 // When data is a function, execute the data function and take out the return value data = vm._data = typeof data === 'function'
  ? getData(data, vm)
  : data || {}
 // proxy data on instance
 const keys = (data)
 const props = vm.$
 const methods = vm.$
 let i = 
 while (i--) {
  const key = keys[i]
  if (props && hasOwn(props, key)) {
   .NODE_ENV !== 'production' && warn(
    `The data property "${key}" is already declared as a prop. ` +
    `Use prop default value instead.`,
    vm
   )
  } else if (!isReserved(key)) {
   proxy(vm, `_data`, key)
  }
 }
 // observe data
 // Here we start to follow the logic of observing data observe(data, true /* asRootData */)
}

The internal process of observe has been discussed above, so I will go through it briefly here:

  1. new Observe Observation data
  2. defineReactive hijacking of data

The initState logic is executed, go back to the beginning, and then execute vm.$mount(vm.$) rendering page:

$mount:

// Source code location: /src/platforms/web/runtime/.$mount = function (
 el?: string | Element,
 hydrating?: boolean
): Component {
 el = el && inBrowser ? query(el) : undefined
 return mountComponent(this, el, hydrating)
}

mountComponent:

// Source code location: /src/core/instance/export function mountComponent (
 vm: Component,
 el: ?Element,
 hydrating?: boolean
): Component {
 vm.$el = el
 callHook(vm, 'beforeMount')

 let updateComponent
 /* istanbul ignore if */
 if (.NODE_ENV !== 'production' &&  && mark) {
  updateComponent = () => {
   const name = vm._name
   const id = vm._uid
   const startTag = `vue-perf-start:${id}`
   const endTag = `vue-perf-end:${id}`

   mark(startTag)
   const vnode = vm._render()
   mark(endTag)
   measure(`vue ${name} render`, startTag, endTag)

   mark(startTag)
   vm._update(vnode, hydrating)
   mark(endTag)
   measure(`vue ${name} patch`, startTag, endTag)
  }
 } else {
  // This method will be called when the data changes  updateComponent = () => {
   // vm._render() returns vnode, where the data data will be valued   // vm._update converts vnode to real dom and renders it to the page   vm._update(vm._render(), hydrating)
  }
 }
 
 // Execute Watcher, this is what is mentioned above to render wacther new Watcher(vm, updateComponent, noop, {
  before () {
   if (vm._isMounted && !vm._isDestroyed) {
    callHook(vm, 'beforeUpdate')
   }
  }
 }, true /* isRenderWatcher */)
 hydrating = false

 // manually mounted instance, call mounted on self
 // mounted is called for render-created child components in its inserted hook
 if (vm.$vnode == null) {
  vm._isMounted = true
  callHook(vm, 'mounted')
 }
 return vm
}

Watcher:

// Source code location: /src/core/observer/let uid = 0

export default class Watcher {
 constructor(vm, exprOrFn, cb, options){
   = ++id
   = vm
   = cb
   = options
  // exprOrFn is the updateComponent passed on above   = exprOrFn

   = []
   = new Set()

  ()
 }
 get() {
  // 1. pushTarget records the current watcher to , which is globally unique  pushTarget(this)
  let value
  const vm = 
  try {
  // 2. Calling is equivalent to executing the vm._render function, taking values ​​for attributes on the instance,  //The get method triggered by this performs dependency collection () within the get method. Here, dependency collection requires   value = (vm, vm)
  } catch (e) {
   if () {
    handleError(e, vm, `getter for watcher "${}"`)
   } else {
    throw e
   }
  } finally {
   // "touch" every property so they are all tracked as
   // dependencies for deep watching
   if () {
    traverse(value)
   }
   // 3. popTarget will be empty   popTarget()
   ()
  }
  return value
 }
}

At this point, the initialization process has been completed. The main tasks of the initialization process are data hijacking, rendering pages and collecting dependencies.

Update process

The data changes, trigger set, execute

// Source code location: /src/core/observer/let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
 static target: ?Watcher;
 id: number;
 subs: Array<Watcher>;

 constructor () {
   = uid++
   = []
 }

 addSub (sub: Watcher) {
  (sub)
 }

 removeSub (sub: Watcher) {
  remove(, sub)
 }

 depend () {
  if () {
   (this)
  }
 }

 notify () {
  // stabilize the subscriber list first
  const subs = ()
  if (.NODE_ENV !== 'production' && !) {
   // subs aren't sorted in scheduler if not running async
   // we need to sort them now to make sure they fire in correct
   // order
   ((a, b) =>  - )
  }
  for (let i = 0, l = ; i < l; i++) {
   // Execute the update method of watcher   subs[i].update()
  }
 }
}

// Source code location: /src/core/observer//**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
update () {
 /* istanbul ignore else */
 if () { // Compute attribute update   = true
 } else if () { // Synchronous update  ()
 } else {
  // Generally, data will be updated asynchronously  queueWatcher(this)
 }
}

queueWatcher:

// Source code location: /src/core/observer/
// Used to store watchersconst queue: Array<Watcher> = []
// Used for watcher deduplicationlet has: { [key: number]: ?true } = {}
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
 let watcher, id

 // Sort the watcher ((a, b) =>  - )

 // do not cache length because more watchers might be pushed
 // as we run existing watchers
 for (index = 0; index < ; index++) {
  watcher = queue[index]
  id = 
  has[id] = null
  // The run method updates the view  ()
 }
}
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
 const id = 
 if (has[id] == null) {
  has[id] = true
  // watcher joins the array  (watcher)
  // Asynchronous update  nextTick(flushSchedulerQueue)
 }
}

nextTick:

// Source code location: /src/core/util/
const callbacks = []
let pending = false

function flushCallbacks () {
 pending = false
 const copies = (0)
  = 0
 // traversal callback function execution for (let i = 0; i < ; i++) {
  copies[i]()
 }
}

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
 const p = ()
 timerFunc = () => {
  (flushCallbacks)
 }
}

export function nextTick (cb?: Function, ctx?: Object) {
 let _resolve
 // Add callback function to array (() => {
  if (cb) {
   (ctx)
  }
 })
 if (!pending) {
  pending = true
  // traversal callback function execution  timerFunc()
 }
 // $flow-disable-line
 if (!cb && typeof Promise !== 'undefined') {
  return new Promise(resolve => {
   _resolve = resolve
  })
 }
}

This step is to use microtasks to execute the callback function asynchronously, which is the above. Eventually, the update page will be called.

The update process is now completed.

Written at the end

If you have not been exposed to the source code, I believe you may still be a little confused after reading it. This is normal. It is recommended to compare the source code and read it a few more times by yourself to know the process. For students with basics, it is considered a review.

If you want to become stronger, learning to read the source code is the only way to go. In this process, you can not only learn the design ideas of the framework, but also cultivate your own logical thinking. Everything is difficult at the beginning, and sooner or later we will take this step. It is better to start today.

I have put the simplified code ingithub, if you need it, you can take a look.

The above is the detailed content of the detailed analysis of the principle of vue responsiveness. For more information about the principle of Vue responsiveness, please pay attention to my other related articles!