“Detailed explanation of Vue 2.X core source code, hand lu a simple version of the Vue framework (part 1)” recorded the implementation of Vue responsive system, virtual DOM generation, DOM directional update, etc.. This article summarizes the source code implementation process

Vue source code implementation process

The Vue instance is initialized

In this stage, data is mounted ($options, _data) and Vue options are initialized, such as data initialization, computed initialization, watch initialization, etc.

Data proxy

Iterate over all the data and proxy the data to the Vue instance via Object.defineProperty

Hijacking of data

Register a listener Observer for each data data object and declare an event center Dep instance for it, traversing the data and passing it into defineReactive(data[key]). Data hijacking is done in defineReactive, watcher dependencies are collected through getter, and updates through Dep through setter. Dep notifies all Watcher updates.

Rendering functions with the virtual DOM

Vue initializations call mount, declare render Watcher on mount, on mount, in mount and evaluate it. The render Watcher evaluation function will first call the render function to generate the virtual DOM, and pass the virtual DOM into the _update method to update the DOM node Diff contrast directionally. When Dep notifies Render Watcher of updates, it repeats this logic.

All code and annotation

Vue responsive system and VDOM part

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
    // 8.4 Both the initialization of computed properties and the initialization of data must be placed before the initialization of Watch, because watch can detect them only after the initialization of computed properties and data is completed.
    this.initComputed()
    this.initWatch()
    // 10.0 uses parsers and code generators to generate rendering functions
    if (this.$options.el) {
      // 10.1 Obtaining a Template String
      let html = document.querySelector("div").outerHTML
      // 10.2 Generate an abstract syntax tree
      let ast = parser(html)
      // 10.3 Generate the body of the render function
      let funCode = codegen(ast).render
      // 10.4 Generate the render function and mount it to the Vue instance
      this.$options.render = new Function(funCode)
      Call $mount to update the view
      this.$mount(this.$options.el)
    }
  }
  $mount(el) {
    16.1 Mount the container root node to the Vue instance
    this.$el = document.querySelector(el)
    // create new render Watcher
    this._watcher = new Watcher(this.() = > {
      16.3 Generating the Virtual DOM
      const vnode = this.$options.render.call(this)
      16.4 Call _update to update the view
      this._update(vnode)
    }, () = >{})}_update(vnode) {
    //17.0 has the last vnode
    if (this._vnode) {
      17.1 Call patch and pass in the last vnode and this vnode
      patch(this._vnode, vnode)
    } else {
      // 17.2 Passing the real DOM node when mounting the Vue instance for the first time
      patch(this.$el, vnode)
    }
    17.3 Save the vNode
    this._vnode = vnode
  }
  // 11.0 Generate element node
  _c(tag, attrs, children, text) {
    return new VNode(tag, attrs, children, text)
  }
  12.0 Generate a plain text node
  _v(text) {
    return new VNode(null.null.null, text)
  }
  // 13.0 Get variable content
  _s(val) {
    // 13.1 Returns an empty string if the value is null
    if (val === null || val === undefined) {
      return ' '
      // 13.2 If it is an object
    } else if (typeof val === 'object') {
      return JSON.stringify(val)
      // 13.3 If the value is a number or a string
    } else {
      return val
    }
  }
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    // Data broker
    for (let i = 0; i < keys.length; i++) {
      Object.defineProperty(this, keys[i], {
        enumerable: true.configurable: true.set: function proxySetter(newVal) {
          data[keys[i]] = newVal
        },
        get: function proxyGetter() {
          return data[keys[i]]
        },
      })
    }
    // Data hijacking
    observe(data)
  }
  initWatch() {
    const watches = this.$options.watch
    // The watch option exists
    if (watches) {
      const keys = Object.keys(watches)
      for (let index = 0; index < keys.length; index++) {
        new Watcher(this, keys[index], watches[keys[index]])
      }
    }
  }
  // 8.3 Initializes the calculated properties separately
  initComputed() {
    const computeds = this.$options.computed
    if (computeds) {
      const keys = Object.keys(computeds)
      for (let index = 0; index < keys.length; index++) {
        // 8.5 The second argument is passed to the computed attribute function
        // 8.15 The watcher that calculates attribute initialization needs to be marked lazy
        const watcher = new Watcher(this, computeds[keys[index]], function () {}, {lazy: true })
        // 8.6 mount the Watcher to the Vue instance
        Object.defineProperty(this, keys[index], {
          enumerable: true.configurable: true.// 8.7 Does not allow users to modify computing attributes
          set: function computedSetter() {
            console.warn("Please do not modify the calculated properties")},// 8.8 evaluates using watcher's get method and returns the result of the evaluation
          get: function computedGetter() {
            // 8.9 Re-evaluate if only watcher is dirty
            if (watcher.dirty) {
              watcher.get()
              // 8.10 Update the dirty status
              watcher.dirty = false
            }
            // 9.12 Determine in the getter of the calculated property whether there are more Watcher to collect
            if (Dep.target) {
              for (let i = 0; i < watcher.deps.length; i++) {
                // 9.13 Remove the DEP from watcher and continue collecting the remaining watcher
                watcher.deps[i].depend()
              }
            }
            return watcher.value
          }
        })
      }
    }
  }
  $watch(key, cb) {
    new Watcher(this, key, cb)
  }
  // 6.6 __ob__ mount, dependency collection is complete
  $set(targt, key, value) {
    constoldValue = { ... targt }// 6.7 Makes the new attribute passed in also responsive
    defineReactive(targt, key, value)
    // 6.8 Manually sending dependency updates
    targt.__ob__.dep.notify(oldValue, targt)
  }
}
// observe the data type and return an Observer instance
function observe(data) {
  const type = Object.prototype.toString.call(data)
  // 1.1 Returns if the observed data is of a basic type
  if(type ! = ='[object Object]'&& (type ! = ='[object Array]')) return
  // 1.2 Observation Data involves some complex logic to encapsulate this process as an Observer class
  / / 1.2 new Observer (data)
  // 6.3 Return the Observer instance and receive it in defineReactive.
  if (data.__ob__) return data.__ob__
  return new Observer(data)
}

// 2, Observer class: Observer/listener, used to observe data, generate complex logic responsible for handling dependencies Dep instances, etc
class Observer {
  constructor(data) {
    // 6.1 Mount a Dep instance for an Observer instance (event center)
    this.dep = new Dep()
    // The 7.5 array cannot call walk because walk hijacks subscripts via defineProperty, resulting in dependency callback errors, etc
    if (Array.isArray(data)) {
      // 7.6 Overwrite the prototype object with our modified array prototype
      data.__proto__ = ArrayMethods
      // 7.7 Make all the children of the array responsive
      this.observeArray(data)
    } else {
      // 2.1 Change all attributes of data to responsive
      this.walk(data)
    }
    // 6.2 Attach an Observer instance to the non-enumerable attribute __ob__ for external $set use
    Object.defineProperty(data, "__ob__", {
      value: this.enumerable: false.configurable: true.writable: true})}walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i], data[keys[i]])
    }
  }
  // 7.8 Makes all children of the passed array responsive
  observeArray(arr) {
    for (let i = 0; i < arr.length; i++) {
      observe(arr[i])
    }
  }
}

// defineReactive utility function: used to recursively hijack data and turn it into responsive data
function defineReactive(obj, key, value) {
  // 3.1 Recursively call defineReactive to recursively hijack deep data defineReactive--observe--Observer--defineReactive
  [key] / / 3.1 observe (obj)
  // 6.4 Receive Observer instances to collect dependencies on Watcher for the attribute Dep
  let childOb = observe(obj[key])
  // create a new Dep instance for each data and maintain it through closures
  let dep = new Dep()
  // 3.2 Data hijacking the key of the current data object
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.set: function reactiveSetter(newVal) {
      if (newVal === value) return
      // 4.4 Dep assigns dependency updates
      dep.notify(newVal, value)
      value = newVal
    },
    get: function reactiveGetter() {
      Closure Dep collection relies on Watcher
      dep.depend()
      The observe function does not return an Observer instance if the data is of a simple type. If yes, collect a dependency for the Dep of the Observer instance
      if (childOb) childOb.dep.depend()
      return value
    }
  })
}
9.1 Adding a stack to store depTarget
let targetStack = []
// Dep class: event center, responsible for collecting dependencies, notifying dependency updates, etc
class Dep {
  constructor(option) {
    // 4.1 subs is used to save all subscribers
    this.subs = []
  }
  // 9.7 After collecting the DEP, call dep.addSub to collect the Watcher
  addSub(watcher) {
    this.subs.push(watcher)
  }
  The Depend method is used to collect subscriber dependencies
  depend() {
    // 5.5 if the Watcher instance is initialized
    if (Dep.target) {
      // 5.6 For each data Watcher instance, the dep. target is set first and the getter for data is triggered to complete the dependency collection
      // this.subs.push(Dep.target)
      // 9.6 Watcher collects DEP
      Dep.target.addDep(this)}}// the notify method is used to send subscribers updates
  notify(newVal, value) {
    Execute Watcher's run method for each subscriber to complete the update
    // 8.12 Depend on update Before sending an update, check whether the update is required
    this.subs.forEach(watcher= > watcher.update(newVal, value))
  }
}
let watcherId = 0
// Watcher task queue
let watcherQueue = []
// 5, Watcher class: subscriber, trigger dependency collection, processing callback
class Watcher {
  constructor(vm, exp, cb, option = {}) {
    // 8.13 watcher Added the new parameter option to set watcher by default
    this.lazy = this.dirty = !! option.lazyMount Vue instance, data attribute name, and processing callback to Watcher instance
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    // 9.8 watcher is used to save collected DEPs
    this.deps = []
    // 8.14 Lazy Watcher initialization does not require collecting dependencies
    if(! option.lazy) {// 5.2, trigger the getter for data to complete the dependency collection
      this.get()
    }
  }
  addDep(dep) {
    // 9.9 Since watcher may collect dePs multiple times per 9.0 evaluation, it terminates if it has already collected dePs
    if (this.deps.indexOf(dep) ! = = -1) return
    // 9.10 Collecting DEPs
    this.deps.push(dep)
    // 9.11 let DEP collect watcher
    dep.addSub(this)}get() {
    // 9.2 If deP collection depends on watcehr, add it to the stack first
    targetStack.push(this)
    // set the Watcher instance as the target object for Dep dependency collection
    Dep.target = this
    Check whether a function is passed when evaluating attributes before collecting dependencies
    if (typeof this.exp === 'function') {
      // 8.2 Execute the function and evaluate
      this.value = this.exp.call(this.vm)
    } else {
      Trigger the data getter interceptor to evaluate it
      this.value = this.vm[this.exp]
    }
    // 9.3 Let Watcher off the stack after evaluating and collecting dependencies
    targetStack.pop()
    // 9.4 Check whether there are uncollected watcher in the stack
    if (targetStack.length) {
      // 9.5 Get watcher at the top of the stack
      Dep.target = targetStack[targetStack.length - 1]}else {
      // Clear the dependent target object
      Dep.target = null}}8.11 Call update before run to determine whether to run directly
  update(newVal, value) {
    Do not run when the current watcher is lazy. Instead, it marks Watcher as dirty data and waits for the user to fetch the results before running
    if (this.lazy) {
      this.dirty = true
    } else {
      this.run(newVal, value)
    }
  }
  run(newVal, value) {
    // 5.8 If the task already exists in the task queue, the task is terminated
    if (watcherQueue.indexOf(this.id) ! = = -1) return
    // 5.9 Add the current watcher to the queue
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() = > {
      // 9.0 relies on updates to evaluate the watcher property to solve the problem of not triggering the calculation attribute watcher
      this.get()
      this.cb.call(this.vm, newVal, value)
      // 5.10 The task is deleted from the task queue
      watcherQueue.splice(index, 1)}}}// 7.0 gets the array prototype object
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype
// Declare an array method that needs to be modified
const methods = ['push'.'pop']
// 7.2 Modify the array method
methods.forEach(method= > {
  ArrayMethods[method] = function (. args) {
    const oldValue = [...this]
    // 7.9 Change the newly inserted data to responsive
    if (method === 'push') {
      this.__ob__.observeArray(args)
    }
    // 7.3 Pass parameters to execute the original method
    const result = Array.prototype[method].apply(this, args)
    // 7.4 Sending dependency updates
    this.__ob__.dep.notify(oldValue, this)
    return result
  }
})
The 14.0 VNode abstract class implements the virtual DOM node
class VNode {
  constructor(tag, attrs, children, text) {
    this.tag = tag
    this.attrs = attrs
    this.children = children
    this.text = text
  }
}
// 15.0 Generate the real DOM
function createEle(vnode) {
  // 15.1 is a literal node
  if(! vnode.tag) {const el = document.createTextNode(vnode.text)
    // 15.2 Save the node
    vnode.ele = el
    return el
  }
  // 15.3 is an element node
  const el = document.createElement(vnode.tag)
  vnode.ele = el
  // 15.4 Convert the child node into a real DOM and insert it into the parent node
  vnode.children.map(createEle).forEach(e= > {
    el.appendChild(e)
  })
  return el
}
// 18.6 Check whether the old and new nodes are changed
function changed(oldNode, newNode) {
  returnoldNode.tag ! == newNode.tag || oldNode.text ! == newNode.text }function patch(oldNode, newNode) {
  const isRealyElement = oldNode.nodeType
  // 18.0 When oldNode=this.$el is first mounted for the element node page
  if (isRealyElement) {
    let parent = oldNode.parentNode
    18.1 Replace the vUE container node with the new node generated by VDOM
    parent.replaceChild(createEle(newNode), oldNode)
    return
  }
  Last patch will mount ele on newNode
  let el = oldNode.ele
  // 18.3 New VDOM node exists Mount DOM to vdom.ele. Ele will be used in patch next time
  if (newNode) {
    newNode.ele = el
  }
  let parent = el.parentNode
  18.4 If the new VDOM node does not exist, delete the corresponding node in the DOM
  if(! newNode) { parent.removeChild(el)18.5 Label types or text of new and Old Nodes are Inconsistent
  } else if (changed(oldNode, newNode)) {
    18.7 Call createEle to generate a new DOM node to replace the old ONE
    parent.replaceChild(createEle(newNode), el)
    18.8 Comparing Child Nodes
  } else if (newNode.children) {
    let newLength = newNode.children.length
    let oldLength = oldNode.children.length
    18.9 Traverse all children of the old and new VDOM nodes
    for (let index = 0; index < newLength || index < oldLength; index++) {
      The old vDOM of the child node does not exist. Call createEle to generate a DOM and insert it into the parent node el
      if (index > oldLength) {
        el.appendChild(createEle(newNode.children[index]))
      } else {
        18.11 The comparison of other child nodes is achieved by calling patch
        patch(oldNode.children[index], newNode.children[index])
      }
    }
  }
}
Copy the code

Parser part

ElementASTs (ElementASTs)
{/ * * * * children: [{...}], * parent: {}, * tag: "div", * type: 1, //1- element node 2- text node with variable 3- plain text node, * expression:'_s(name)', //type if 2 returns _s(variable) * text:'{{name}}' // Text node before compilation string *} */
function parser(html) {
  // Stack: records the level of the current element
  let stack = []
  // The root element node
  let root = null
  // The parent node of the current element
  let currentParent = null
  // 1.0 continuously parses the template string
  while (html) {
    let index = html.indexOf("<")
    / / 2.1 if the element has a text node example: before the HTML = "{{name}} < div > 1 < / div > < / root >"
    if (index > 0) {
      // 2.2 Intercept the text before the label
      let text = html.slice(0, index)
      5.4 Call the parseText utility function to parse the text
      let element = parseText(text)
      // 5.5 Add parent attributes to text nodes
      element.parent = currentParent
      2.3 Push the literal node into the children of the parent element
      currentParent.children.push(element)
      // 2.4 Truncate the part that has been processed
      html = html.slice(index)
      / / 1.0 if to start tag Example: the HTML = "< root > {{name}} < div > 1 < / div > < / root >"
    } else if (html[index + 1]! = ='/') {
      // 1.1 Get the element type
      let gtIndex = html.indexOf(">")
      let eleType = html.slice(index + 1, gtIndex).trim()
      EleType = 'div id="app"' after processing: eleType = 'div'
      let emptyIndex = eleType.indexOf("")
      let attrs = {}
      if(emptyIndex ! = = -1) {
        // 1.3 Get the element tag attribute
        attrs = parseAttr(eleType.slice(emptyIndex + 1))
        eleType = eleType.slice(0, emptyIndex)
      }
      1.4 Creating an AST Node
      const element = {
        children: [],
        attrs,
        parent: currentParent,
        tag: eleType,
        type: 1
      }
      // 1.5 has no root element node
      if(! root) { root = element }else {
        // 1.6 Pushes the current element node into children of the parent element
        currentParent.children.push(element)
      }
      // 1.7 Parsing to the start of the element tag pushes the element up the stack
      stack.push(element)
      // 1.8 Updates the current parent element
      currentParent = element
      // 1.9 truncates the part that has been processed
      html = html.slice(gtIndex + 1)
      HTML = ""
    } else {
      let gtIndex = html.indexOf(">")
      // 3.1 Parse to the element's end tag hierarchy and unstack one
      stack.pop()
      // 3.2 Update the current parent element
      currentParent = stack[stack.length - 1]
      // 3.3 Cut off the part that has been processed
      html = html.slice(gtIndex + 1)}}return root
}
// Parse text nodes
function parseText(text) {
  Unparsed text
  let originText = text
  // May be plain text or variable text default: plain text
  let type = 3
  // The text node of the element node may be composed of multiple segments
  / / example: < p > I {{name}}, I {{age}} < / p > token = [' my '{{name}},' my ', {{age}}]
  let token = []
  while (text) {
    let start = text.indexOf({{" ")
    let end = text.indexOf("}}")
    //4.0 If interpolation exists
    if(start ! = = -1&& end ! = = -1) {
      // 4.1 Marks the text node type as text with variables
      type = 2
      // 4.2 There is plain text before interpolation
      if (start > 0) {
        // 4.3 Advance token before interpolation in plain text
        token.push(JSON.stringify(text.slice(0, start)))
      }
      // 4.4 Get the expression in the interpolation
      let exp = text.slice(start + 2, end)
      // 4.5 Parse expressions and advance tokens
      token.push(`_s(${exp}) `)
      // 4.6 Cut off the part that has been processed
      text = text.slice(end + 2)
      // 5.0 There is no interpolation
    } else {
      // 5.1 Terminates text to push tokens directly
      token.push(JSON.stringify(text))
      text = ' '}}let element = {
    text: originText,
    type
  }
  // 5.3 If type is 2, the variable text node requires expression
  if (type === 2) {
    element.expression = token.join("+")}return element
}
// Parse the tag attributes
function parseAttr(eleAttrs) {
  let attrs = {}
  attrString = eleAttrs.split("")
  attrString.forEach(e= > {
    if (e && e.indexOf("=")! = = -1) {
      const attrsArr = e.split("=")
      attrs[attrsArr[0]] = attrsArr[1]}else {
      attrs[e] = true}});return attrs
}
Copy the code

Code generator section

// Convert AST to render function body
/**{children: [{...}], parent: {}, tag: "div", type: 1, //1- element node 2- text node 3- text node, expression: '_s (name)', / / the type if is 2, it returns _s (variable) text: '{{name}} / / text string} * / node before compilation
function codegen(ast) {
  // The first layer of the 1.0 AST must be an element node
  let code = genElement(ast)
  return {
    // When the rendering function is executed, passing this to change the direction of this in the function body.
    render: `with(this){return ${code}} `}}// Convert the element node
function genElement(el) {
  // 2.1 Obtaining child nodes
  let children = genChildren(el)
  // 2.0 returns _c(tag name, tag attribute object, tag child node array), and changes the tag name to JSON string
  return `_c(The ${JSON.stringify(el.tag)}.The ${JSON.stringify(el.attrs)}.${children}) `
}
// Convert the text node
function genText(node) {
  // 5.0 Text nodes with variables
  if (node.type === 2) {
    // node.expression Any variable is evaluated by this.[node.expression]!!!!
    return `_v(${node.expression}) `
  }
  // 5.1 Plain text nodes must become JSON strings or they will be treated as variables
  return `_v(The ${JSON.stringify(node.text)}) `
}
// Determine the node to which the type is transferred
function genNode(node) {
  // 4.0 Check the node type
  if (node.type === 1) {
    return genElement(node)
  } else {
    return genText(node)
  }
}
// Convert the child node
function genChildren(node) {
  // 3.0 Checks whether there are child nodes
  if (node.children && node.children.length > 0) {
    // 3.1 Convert all child nodes [child node 1, child node 2... , recursively convert all child nodes genNode--genElement--genChildren--genNode
    return ` [${node.children.map(node => genNode(node))}] `}}Copy the code