SoFunction
Updated on 2025-04-04

A brief discussion on the first commit analysis of vue

Why write this Vue analysis article?

For the silly front-end (me), reading the source code is not easy. After all, sometimes you can’t understand the articles that read source code analysis. Every time I see the bosses who have used vue for 1 to 2 years can master the principles and even master the source code. When I see that I have been using it for several years, I am always ashamed. If you have always been satisfied with basic business development, you may have to stay at the primary level. Therefore, I hope to record knowledge points while learning the source code, so that my understanding and memory can be deeper and easier to read in the future.

Directory structure

This article uses the first commit a879ec06 of vue as the analysis version

├── build
│  └──         // `rollup` package configuration├── dist            
│  └──   
├── 
├── src            // vue source code directory│  ├── compiler        // Convert vue-template into render function│  │  ├──      // Recursive ast extraction instructions, classify attr, style, class, and generate render functions│  │  ├──    // Convert html string to ast via regular matching│  │  ├──       // compile main entrance│  │  └──    // Compile {{}}│  ├──        // Global configuration file for vue│  ├──         // Main entrance│  ├──       // Unknown (it should be the main entrance in umd format)│  ├── instance        // vue instance function│  │  └──       // Contains the initialization of vue instances, compile, data proxy, methods proxy, watch data, and rendering│  ├── observer        // Implementation of data subscription release│  │  ├──       // Implement the array mutation method, $set $remove implementation│  │  ├──      // Watch execution queue collection, execution│  │  ├──        // Subscription Center Implementation│  │  ├──       // Implementation of data hijacking to collect subscribers│  │  └──      // watch implementation, subscriber│  ├── util          // Tool function│  │  ├── 
│  │  ├── 
│  │  ├── 
│  │  ├──        // nexttick implementation│  │  ├── 
│  │  ├── 
│  │  └── 
│  └── vdom
│    ├──        // dom operation encapsulation│    ├──         // Node data analysis (element node, text node)│    ├──       // vdom main entrance│    ├── modules      // Different attribute processing functions│    │  ├──     // Normal attr attribute processing│    │  ├──     // class processing│    │  ├──    // Event processing│    │  ├──     // Props processing│    │  └──     // style processing│    ├──       // Rendering of node tree, including the addition and subtraction of node update processing, and the corresponding attr processing│    └──       // Return the final node data└──      // webpack configuration

Analysis of the process from template to html

Our code starts with new Vue(), and the constructor of Vue is as follows:

constructor (options) {
 // options is our configuration for vue this.$options = options
 this._data = 
 // Get the element html, that is, template const el = this._el = ()
 // Compile template -> render function const render = compile(getOuterHTML(el))
 this._el.innerHTML = ''
 // Instance proxy data ().forEach(key => this._proxy(key))
 // Point this method to the instance if () {
  ().forEach(key => {
   this[key] = [key].bind(this)
  })
 }
 // Data Observation this._ob = observe()
 this._watchers = []
 // watch data and update this._watcher = new Watcher(this, render, this._update)
 // Rendering function this._update(this._watcher.value)
}

When we initialize the project, the constructor will be executed, which shows us the main line of vue initialization:Compile template string => Proxy data/methods this binding => Data observation => Create watch and update rendering

1. Compile template string

const render = compile(getOuterHTML(el))

The implementation of compile is as follows:

export function compile (html) {
 html = ()
 // Cache the compilation result const hit = cache[html]
 // The parse function is defined in parse-html. Its function is to convert the html string we get into ast through regular matching, and the output is as follows {tag: 'div', attrs: {}, children: []} return hit || (cache[html] = generate(parse(html)))
}

Next, look at the generate function. Ast generates a function that builds node html through the conversion of genElement. In genElement, if for and so on will be judged and converted (the specific processing of the instruction will be analyzed later, focus on the main process code first), and finally the genData function will be executed.

// Generate the node main functionexport function generate (ast) {
 const code = genElement(ast)
 // Execute the code code and use this as the global object of the code.  So our variable in template will point to the attribute {{name}} -> return new Function (`with (this) { return $[code]}`)
}

// parse a single node -> genDatafunction genElement (el, key) {
 let exp
 // The implementation of the instruction is actually implemented when the template is compiled. if (exp = getAttr(el, 'v-for')) {
  return genFor(el, exp)
 } else if (exp = getAttr(el, 'v-if')) {
  return genIf(el, exp)
 } else if ( === 'template') {
  return genChildren(el)
 } else {
  // are the tag's own attributes and child node data respectively  return `__h__('${  }', ${ genData(el, key) }, ${ genChildren(el) })`
 }
}

We can see what is done in genData. The parse function above converts the html string into ast, while in genData further processes the attrs data of the node, such as class -> renderClass style class props attr classification. Here you can see the implementation of the bind directive, that is, by regular matching: and bind. If it matches, the corresponding value value will be converted into (value) form, while the non-match will be converted into a string ('value'). Finally, the (key-value) of attrs is output. The object obtained here is in the form of a string. For example, (value) and so on are just the variable name, and in generate, the variable value is further obtained through () through new Function.

function genData (el, key) {
 // No attribute returns empty object if (!) {
  return '{}'
 }
 // key
 let data = key ? `{key:${ key },` : `{`
 // class processing if ([':class'] || ['class']) {
  data += `class: _renderClass(${ [':class'] }, "${ ['class'] || '' }"),`
 }
 // attrs
 let attrs = `attrs:{`
 let props = `props:{`
 let hasAttrs = false
 let hasProps = false
 for (let i = 0, l = ; i < l; i++) {
  let attr = [i]
  let name = 
  // bind attribute  if ((name)) {
   name = (bindRE, '')
   if (name === 'class') {
    continue
   // style processing   } else if (name === 'style') {
    data += `style: ${  },`
   // Props property processing   } else if ((name)) {
    hasProps = true
    props += `"${ name }": (${  }),` 
   // Other properties   } else {
    hasAttrs = true
    attrs += `"${ name }": (${  }),`
   }
  // on directive, not implemented  } else if ((name)) {
   name = (onRE, '')
  // Normal attributes  } else if (name !== 'class') {
   hasAttrs = true
   attrs += `"${ name }": (${ () }),`
  }
 }
 if (hasAttrs) {
  data += (0, -1) + '},'
 }
 if (hasProps) {
  data += (0, -1) + '},'
 }
 return (/,$/, '') + '}'
}

As for genChildren, we can guess that it is to traverse the children in ast to call genElement, which actually includes processing of text nodes here.

// traversal of child nodes -> genNodefunction genChildren (el) {
 if (!) {
  return 'undefined'
 }
 // Flattening of children return '__flatten__([' + (genNode).join(',') + '])'
}

function genNode (node) {
 if () {
  return genElement(node)
 } else {
  return genText(node)
 }
}

// parse {{}}function genText (text) {
 if (text === ' ') {
  return '" "'
 } else {
  const exp = parseText(text)
  if (exp) {
   return 'String(' + escapeNewlines(exp) + ')'
  } else {
   return escapeNewlines((text))
  }
 }
}

genText processes text and line breaks, uses regular parsing {{}} in the parseText function to output strings in the form of strings (value).

Now let's take a look at the __h__ function in __h__('${ }', ${ genData(el, key) }, ${ genChildren(el) })

// h function uses the node data obtained above to obtain vNode object => Virtual domexport default function h (tag, b, c) {
 var data = {}, children, text, i
 if ( === 3) {
  data = b
  if (isArray(c)) { children = c }
  else if (isPrimitive(c)) { text = c }
 } else if ( === 2) {
  if (isArray(b)) { children = b }
  else if (isPrimitive(b)) { text = b }
  else { data = b }
 }
 if (isArray(children)) {
  // Recursive processing of child nodes  for (i = 0; i < ; ++i) {
   if (isPrimitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i])
  }
 }
 // svg processing if (tag === 'svg') {
  addNS(data, children)
 }
 // The child node is a text node return VNode(tag, data, children, text, undefined)
}

So far, we analyzed how to deal with const render = compile(getOuterHTML(el)), from el's html string to render function.

2. Proxy data/methods this binding

// Instance proxy data().forEach(key => this._proxy(key))
// Point this method to the instanceif () {
 ().forEach(key => {
  this[key] = [key].bind(this)
 })
}

The implementation of instance proxy data is relatively simple, which is to use the object's setter and getter, return data data when reading this data, and set data data synchronously when setting this data

_proxy (key) {
 if (!isReserved(key)) {
  // need to store ref to self here
  // because these getter/setters might
  // be called by child scopes via
  // prototype inheritance.
  var self = this
  (self, key, {
   configurable: true,
   enumerable: true,
   get: function proxyGetter () {
    return self._data[key]
   },
   set: function proxySetter (val) {
    self._data[key] = val
   }
  })
 }
}

3. Implementation of Obaerve

The implementation principle of Observe is analyzed in many places, mainly using () to establish subscriptions for data changes, which is also called data hijacking in many places. Let’s learn to establish such a data subscription and publishing system from scratch.

Starting from the simplest point, we hope there is a function that can help us listen to the changes in data, and execute a specific callback function whenever the data changes.

function observe(data, callback) {
 if (!data || typeof data !== 'object') {
  return
 }

 // traverse key (data).forEach((key) => {
  let value = data[key];

  // Recursively traverse the listening depth changes  observe(value, callback);

  // Listen to individual changes  (data, key, {
   configurable: true,
   enumerable: true,
   get() {
    return value;
   },
   set(val) {
    if (val === value) {
     return
    }

    value = val;

    // Listen to new data    observe(value, callback);
    
    // Callback for data changes    callback();
   }
  });
 });
}

// Use the observe function to listen to dataconst data = {};
observe(data, () => {
 ('data modification');
})

Above we implemented a simple observe function. As long as we pass the compiled function in as a callback, the callback function will be triggered every time the data changes. However, we cannot set up listening and callback functions for separate keys, we can only listen for changes in the entire object to execute callbacks. Below we improve the function to set listening and callbacks for a certain key. At the same time, a scheduling center is established to make the entire subscription publishing model clearer.

// First of all, the subscription centerclass Dep {
 constructor() {
   = []; // Subscriber array }

 addSub(sub) {
  // Add subscriber  (sub);
 }

 notify() {
  // Notice  ((sub) => {
   ();
  });
 }
}

// Current subscriber, tagged in getter = null;

// Subscribersclass Watch {
 constructor(express, cb) {
   = cb;
  if (typeof express === 'function') {
    = express;
  } else {
    = () => {
    return new Function(express)();
   }
  }
  
  ();
 }

 get() {
  // Use the current subscriber to save   = this;
  // Execute expression -> Trigger getter -> Add subscriber in getter  ();
  // Set aside in time   = null;
 }

 update() {
  // renew  ();
 }

 addDep(dep) {
  // Add a subscription  (this);
 }
}

// Observer Establish observationclass Observe {
 constructor(data) {
  if (!data || typeof data !== 'object') {
   return
  }
 
  // traverse key  (data).forEach((key) => {
   // key => dep corresponding   const dep = new Dep();
   let value = data[key];
 
   // Recursively traverse the listening depth changes   const observe = new Observe(value);
 
   // Listen to individual changes   (data, key, {
    configurable: true,
    enumerable: true,
    get() {
     if () {
      const watch = ;
      (dep);
     }
     return value;
    },
    set(val) {
     if (val === value) {
      return
     }
 
     value = val;
 
     // Listen to new data     new Observe(value);
     
     // Callback for data changes     ();
    }
   });
  });
 }
}

// Listen to changes in a key in the dataconst data = {
 name: 'xiaoming',
 age: 26
};

const observe = new Observe(data);

const watch = new Watch('', () => {
 ('age update');
});

 = 22

Now we implement subscription center, subscriber, observer. The observer monitors the update of data, and the subscriber subscribes to the update of data through the subscription center. When the data is updated, the observer will tell the subscription center, and the subscription center will notify all subscribers to execute the update function one by one. So far, we can roughly guess the implementation principle of vue:

  1. Create new Observe to observe changes in data data (new Observe)
  2. When compiling, when a code snippet or node depends on data data, subscribers are recommended to subscribe to updates of certain data in data (new watch) for that node.
  3. When dada data is updated, the data update is notified through the subscription center, the node update function is executed, and the node is created or updated (())

The above is our guess on the basic implementation of the subscription and release mode of the vue implementation principle and the compilation to the update process. Now we will analyze the implementation of the vue source code:
Initialization of the instance

// ...
// Create data observations for datathis._ob = observe()
this._watchers = []
// Add a subscriber Execution render will trigger getter subscriber subscription updates, and data changes will trigger setter Subscription Center notifies subscribers to execute updatethis._watcher = new Watcher(this, render, this._update)
// ...

Implementation of data observation in vue

// observe functionexport function observe (value, vm) {
 if (!value || typeof value !== 'object') {
  return
 }
 if (
  hasOwn(value, '__ob__') &&
  value.__ob__ instanceof Observer
 ) {
  ob = value.__ob__
 } else if (
  shouldConvert &&
  (isArray(value) || isPlainObject(value)) &&
  (value) &&
  !value._isVue
 ) {
  // Create observers for data  ob = new Observer(value)
 }
 //Storing associated vm if (ob && vm) {
  (vm)
 }
 return ob
}

// => Observe functionexport function Observer (value) {
  = value
 // Useful in array mutation methods  = new Dep()
 // Observer instance exists in __ob__ def(value, '__ob__', this)
 if (isArray(value)) {
  var augment = hasProto
   ? protoAugment
   : copyAugment
  // Array traversal, add variant array method  augment(value, arrayMethods, arrayKeys)
  // Call the observe function for each option of the array  (value)
 } else {
  // walk -> convert -> defineReactive -> setter/getter
  (value)
 }
}

// => walk
 = function (obj) {
 var keys = (obj)
 for (var i = 0, l = ; i < l; i++) {
  (keys[i], obj[keys[i]])
 }
}

// => convert
 = function (key, val) {
 defineReactive(, key, val)
}

// Focus on defineReactiveexport function defineReactive (obj, key, val) {
 // Subscription center corresponding to the key var dep = new Dep()

 var property = (obj, key)
 if (property &&  === false) {
  return
 }

 // Compatible with original setter/getter // cater for pre-defined getter/setters
 var getter = property && 
 var setter = property && 

 // Implement recursive listening attribute val = obj[key] // Depth priority traversal. Set reactive for subproperties first var childOb = observe(val)
 // Set getter/setter (obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
   var value = getter ? (obj) : val
   // is the current watch instance   if () {
    // dep is the scheduler corresponding to obj[key] Add the current wtcher instance to the scheduler    ()
    if (childOb) {
     // Dep for the observer instance corresponding to the obj[key] value val     // Subscription of the mutation method and $set method that implement array     ()
    }

    // TODO: The function here is unknown?    if (isArray(value)) {
     for (var e, i = 0, l = ; i < l; i++) {
      e = value[i]
      e && e.__ob__ && e.__ob__.()
     }
    }
   }
   return value
  },
  set: function reactiveSetter (newVal) {
   var value = getter ? (obj) : val
   // Get val through getter to determine whether it changes   if (newVal === value) {
    return
   }
   if (setter) {
    (obj, newVal)
   } else {
    val = newVal
   }
   // Set reactive for new values   childOb = observe(newVal)
   // Notify the key corresponding to the subscription center update   ()
  }
 })
}

Implementation of Subscription Center

let uid = 0

export default function Dep () {
  = uid++
 // Subscribe to the watch array of the dispatch center  = []
}

// Current watch instance = null

// Add subscriber = function (sub) {
 (sub)
}

// Remove subscribers = function (sub) {
 .$remove(sub)
}

// Subscribe = function () {
 // (this) => () => ()
 (this)
}

// Notification update = function () {
 // stablize the subscriber list first
 var subs = ()
 for (var i = 0, l = ; i < l; i++) {
  // subs[i].update() => ()
  subs[i].update()
 }
}

Subscriber implementation

export default function Watcher (vm, expOrFn, cb, options) {
 // mix in options
 if (options) {
  extend(this, options)
 }
 var isFn = typeof expOrFn === 'function'
  = vm
 // vm's _watchers contains all watches vm._watchers.push(this)
  = expOrFn
  = cb
  = ++uid // uid for batching
  = true
  =  // for lazy watchers
 // deps A watch instance can correspond to multiple deps  = []
  = []
  = (null)
  = null
  = null // for async error stacks
 // parse expression for getter/setter
 if (isFn) {
   = expOrFn
   = undefined
 } else {
  warn('vue-lite only supports watching functions.')
 }
  = 
  ? undefined
  : ()
  =  = false
}

 = function () {
 ()
 var scope =  || 
 var value
 try {
  // Execute expOrFn, getter => () will be triggered to add the watch instance to the dep corresponding to obj[key]  value = (scope, scope)
 }
 if () {
  // Depth watch  // The getter watch instance that triggers each key will correspond to multiple dep  traverse(value)
 }
 // ...
 ()
 return value
}

// Trigger getter to implement subscription = function () {
  = this
  = (null)
  = 0
}

// Add a subscription = function (dep) {
 var id = 
 if (![id]) {
  // Add the newly appeared dep to newDeps  [id] = true
  (dep)
  // If you are already in the dispatch center, no longer add it repeatedly  if (![id]) {
   // Add watch to the array in the dispatch center   (this)
  }
 }
}

 = function () {
 // Getter contact for excision of key  = null
 var i = 
 while (i--) {
  var dep = [i]
  if (![]) {
   // Remove subscriptions for watches in dep that are not associated in expOrFn expression   (this)
  }
 }
  = 
 var tmp = 
  = 
 // TODO: Since newDeps will eventually be empty, the meaning of assignment here is?  = tmp
}

// Subscription center notification message update = function (shallow) {
 if () {
   = true
 } else if ( || !) {
  ()
 } else {
  // if queued, only overwrite shallow with non-shallow,
  // but not the other way around.
   = 
   ? shallow
    ? 
    : false
   : !!shallow
   = true
  // record before-push error stack in debug mode
  /* istanbul ignore if */
  if (.NODE_ENV !== 'production' && ) {
    = new Error('[vue] async stack trace')
  }
  // Add to the pool to be executed  pushWatcher(this)
 }
}

// Execute update callback = function () {
 if () {
  var value = ()
  if (
   ((isObject(value) || ) && !)
  ) {
   // set new value
   var oldValue = 
    = value
   var prevError = 
   // ...
   (, value, oldValue)
  }
   =  = false
 }
}

 = function () {
 var i = 
 while (i--) {
  [i].depend()
 }
}

Watch callback execution queue

We can find above that watch performs update when it receives the update information. If pushWatcher(this) is executed in asynchronous situation and push the instance into the execution pool, then when will the callback function be executed and how? Let's take a look at the implementation of pushWatcher.

// 
var queueIndex
var queue = []
var userQueue = []
var has = {}
var circular = {}
var waiting = false
var internalQueueDepleted = false

// Reset the execution poolfunction resetBatcherState () {
 queue = []
 userQueue = []
 // has avoid duplication has = {}
 circular = {}
 waiting = internalQueueDepleted = false
}

// Execution execution queuefunction flushBatcherQueue () {
 runBatcherQueue(queue)
 internalQueueDepleted = true
 runBatcherQueue(userQueue)
 resetBatcherState()
}

// Batch executionfunction runBatcherQueue (queue) {
 for (queueIndex = 0; queueIndex < ; queueIndex++) {
  var watcher = queue[queueIndex]
  var id = 
  // After execution is set to null  has[id] = null
  ()
  // in dev build, check and stop circular updates.
  if (.NODE_ENV !== 'production' && has[id] != null) {
   circular[id] = (circular[id] || 0) + 1
   if (circular[id] > config._maxUpdateCount) {
    warn(
     'You may have an infinite update loop for watcher ' +
     'with expression "' +  + '"',
     
    )
    break
   }
  }
 }
}

// Add to execution poolexport function pushWatcher (watcher) {
 var id = 
 if (has[id] == null) {
  if (internalQueueDepleted && !) {
   // an internal watcher triggered by a user watcher...
   // let's run it immediately after current user watcher is done.
   (queueIndex + 1, 0, watcher)
  } else {
   // push watcher into appropriate queue
   var q = 
    ? userQueue
    : queue
   has[id] = 
   (watcher)
   // queue the flush
   if (!waiting) {
    waiting = true
    // Execute in nextick    nextTick(flushBatcherQueue)
   }
  }
 }
}

4. patch implementation

The above is the implementation principle of data-driven in vue. Next, we will return to the main process. After executing the watch, we will execute this._update(this._watcher.value) to start node rendering.

// _update => createPatchFunction => patch => patchVnode => (dom api)

// vtree is the result of the execution of the render function compiled by the compile function, returning the object currently representing the current dom structure (virtual node tree)_update (vtree) {
 if (!this._tree) {
  // First rendering  patch(this._el, vtree)
 } else {
  patch(this._tree, vtree)
 }
 this._tree = vtree
}

// When processing nodes, different processing needs to be done for class, props, style, attrs, events, and different processing needs// Inject here the processing functions for different attributesconst patch = createPatchFunction([
 _class, // makes it easy to toggle classes
 props,
 style,
 attrs,
 events
])

// => createPatchFunction returns the patch function. The patch function adds, deletes and updates the nodes by comparing the differences between virtual nodes.// Finally, call the native dom API to update htmlreturn function patch (oldVnode, vnode) {
 var i, elm, parent
 var insertedVnodeQueue = []
 // pre hook
 for (i = 0; i < ; ++i) [i]()

 if (isUndef()) {
  oldVnode = emptyNodeAt(oldVnode)
 }

 if (sameVnode(oldVnode, vnode)) {
  // someNode can patch
  patchVnode(oldVnode, vnode, insertedVnodeQueue)
 } else {
  // Normally, no reuse remove insert  elm = 
  parent = (elm)

  createElm(vnode, insertedVnodeQueue)

  if (parent !== null) {
   (parent, , (elm))
   removeVnodes(parent, [oldVnode], 0, 0)
  }
 }

 for (i = 0; i < ; ++i) {
  insertedVnodeQueue[i].(insertedVnodeQueue[i])
 }

 // hook post
 for (i = 0; i < ; ++i) [i]()
 return vnode
}

Ending

The above analyzes the general implementation of vue from template to node rendering. Of course, there are some places where there is no comprehensive analysis. The template resolution ast is mainly implemented through regular matching, and the patch process of node rendering and update is mainly implemented through node operation comparison. However, we have a relatively complete understanding of the general process of compiling template strings => proxy data/methods this binding => data observation => establishing watch and updating rendering.

This is all about this article about the first commit analysis of vue. For more related vue commit content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!