current position:Home>Vue0.11 source code reading series 3: instruction compilation

Vue0.11 source code reading series 3: instruction compilation

2021-08-23 14:25:05 Corner Kobayashi

because vue A lot of instructions , There are many functions , So there will be many special treatments for some situations , If these logics are not right vue Familiar words can't be understood for a while , So let's just look at some basic logic .

compile

establish vue Instance, when a parameter is passed el Or call it manually $mount Method to start the template compilation process ,$mount Called in the method _compile Method , After simplification, what is actually called is compile(el, options)(this, el),compile The simplified code is as follows :

function compile (el, options, partial, transcluded) {
  var nodeLinkFn = compileNode(el, options)
  var childLinkFn = el.hasChildNodes()
      ? compileNodeList(el.childNodes, options)
      : null
  
  function compositeLinkFn (vm, el) 
    var childNodes = _.toArray(el.childNodes)
    if (nodeLinkFn) nodeLinkFn(vm.$parent, el)
    if (childLinkFn) childLinkFn(vm.$parent, childNodes)
  }

  return compositeLinkFn
}

This method will determine which method to use to process a part according to some states of the instance , Because the code is greatly simplified, it is not obvious .

First look compileNode Method , This method will call different methods for ordinary nodes and text nodes , Just look at ordinary nodes :

function compileElement (el, options) {
  var linkFn, tag, component
  //  Check whether it is a custom element , That is, sub components 
  if (!el.__vue__) {
    tag = el.tagName.toLowerCase()
    component =
      tag.indexOf('-') > 0 &&
      options.components[tag]
    //  If it is a custom component, set an attribute flag for the element 
    if (component) {
      el.setAttribute(config.prefix + 'component', tag)
    }
  }
   //  If a custom component or element has attributes 
  if (component || el.hasAttributes()) {
    //  Check  terminal  Instructions 
    linkFn = checkTerminalDirectives(el, options)
    //  If not terminal, Establish normal link function 
    if (!linkFn) {
      var dirs = collectDirectives(el, options)
      linkFn = dirs.length
        ? makeNodeLinkFn(dirs)
        : null
    }
  }
  return linkFn
}

terminal There are three kinds of instructions :repeatif'component

var terminalDirectives = [
  'repeat',
  'if',
  'component'
]
function skip () {}
skip.terminal = true
function checkTerminalDirectives (el, options) {
  // v-pre Instructions are used to tell vue Skip compiling this element and all its child elements 
  if (_.attr(el, 'pre') !== null) {
    return skip
  }
  var value, dirName
  for (var i = 0; i < 3; i++) {
    dirName = terminalDirectives[i]
    if (value = _.attr(el, dirName)) {
      return makeTerminalNodeLinkFn(el, dirName, value, options)
    }
  }
}

By the way attr Method , This method is actually specifically used to obtain vue Of custom properties for , That is to say v- The properties of the beginning , Why did we write the tape in the template v- The prefix attribute is not on the final rendered element , Because it was removed in this method :

exports.attr = function (node, attr) {
  attr = config.prefix + attr
  var val = node.getAttribute(attr)
  //  If the custom directive exists , Delete it from the element 
  if (val !== null) {
    node.removeAttribute(attr)
  }
  return val
}

makeTerminalNodeLinkFn Method :

function makeTerminalNodeLinkFn (el, dirName, value, options) {
  //  Parse instruction value 
  var descriptor = dirParser.parse(value)[0]
  //  Gets the instruction method of the instruction ,vue Built in many instruction processing methods , All in /src/directives/ Under the folder 
  var def = options.directives[dirName]
  var fn = function terminalNodeLinkFn (vm, el, host) {
    //  Create and bind instructions to elements 
    vm._bindDir(dirName, el, descriptor, def, host)
  }
  fn.terminal = true
  return fn
}

parse Method is used to parse the value of the instruction , Please move to the article :vue0.11 Version source code reading series 4 : Detailed instruction value analysis function , For example, the instruction value is click: a = a + 1 | uppercase, After processing, such information will be returned :

{
    arg: 'click',
    expression: 'a = a + 1',
    filters: [
        { name: 'uppercase', args: null }
    ]
}

_bindDir Method creates an instruction instance :

exports._bindDir = function (name, node, desc, def, host) {
  this._directives.push(
    new Directive(name, node, this, desc, def, host)
  )
}

therefore linkFn as well as nodeLinkFn This is this. _bindDir The wrapper function of .

For non terminal Instructions , It's called collectDirectives Method , This method will traverse all the attributes of the element attributes, If it is v- Prefixed vue The instruction will be defined as an object in the following format :

{
    name: dirName,//  In addition to the v- The instruction name of the prefix 
    descriptors: dirParser.parse(attr.value),//  Data after instruction value parsing 
    def: options.directives[dirName],//  Processing method corresponding to the instruction 
    transcluded: transcluded
}

Not vue If there is a dynamic binding for the attribute of the instruction , It will also be processed , In this version vue The dynamic binding in is interpolated using double braces , and 2.x Use v-bind Dissimilarity .

Such as :<div class="{{error}}"></div>, Therefore, regular matching will be used to determine whether there is dynamic binding , Finally, the data in the following format is returned :

{
    def: options.directives.attr,
    _link: allOneTime//  Whether all attributes are one-time differences 
    ? function (vm, el) {//  If it's one-time, it doesn't need to be updated later 
        el.setAttribute(name, vm.$interpolate(value))
    }
    : function (vm, el) {//  If the dependent response data changes, it also needs to be changed 
        var value = textParser.tokensToExp(tokens, vm)
        var desc = dirParser.parse(name + ':' + value)[0]
        vm._bindDir('attr', el, desc, def)
    }
}

collectDirectives Method will eventually return an array of the above objects , And then call makeNodeLinkFn Create a binding function for each instruction :

function makeNodeLinkFn (directives) {
  return function nodeLinkFn (vm, el, host) {
    var i = directives.length
    var dir, j, k, target
    while (i--) {
      dir = directives[i]
      if (dir._link) {
        dir._link(vm, el)
      } else {// v- Prefixed instructions 
        k = dir.descriptors.length
        for (j = 0; j < k; j++) {
          vm._bindDir(dir.name, el,
            dir.descriptors[j], dir.def, host)
        }
      }
    }
  }
}

To sum up compileNode The function of is to traverse the attributes on the element , Create an instruction binding function for each , This instruction function will create a Directive example , See this class later .

If the element has child elements, it will call compileNodeList Method , If the child element has a child element, it will continue to call , In fact, all child elements are called recursively compileNode Method .

compile Method finally returns compositeLinkFn Method , This method is executed immediately , This method calls the just generated nodeLinkFn and childLinkFn Method , The result of execution is to bind the instructions of all elements and child elements , That is, an attribute or instruction on an element is created Directive example .

Directive

The main thing this class does is put DOM And data binding , The instruction will be called when instantiating bind Method , One will be instantiated at the same time Watcher example , Instructions will be called during subsequent data updates update Method .

function Directive (name, el, vm, descriptor, def, host) {
  this.name = name
  this.el = el
  this.vm = vm
  this.raw = descriptor.raw
  this.expression = descriptor.expression
  this.arg = descriptor.arg
  this.filters = _.resolveFilters(vm, descriptor.filters)
  this._host = host
  this._locked = false
  this._bound = false
  this._bind(def)
}

The constructor defines some properties and calls _bind Method ,resolveFilters The method will put the filter getter and setter Collected into an array , Facilitate subsequent loop calls :

exports.resolveFilters = function (vm, filters, target) {
  var res = target || {}
  filters.forEach(function (f) {
    var def = vm.$options.filters[f.name]
    if (!def) return
    var args = f.args
    var reader, writer
    if (typeof def === 'function') {
      reader = def
    } else {
      reader = def.read
      writer = def.write
    }
    if (reader) {
      if (!res.read) res.read = []
      res.read.push(function (value) {
        return args
          ? reader.apply(vm, [value].concat(args))
          : reader.call(vm, value)
      })
    }
    if (writer) {
      if (!res.write) res.write = []
      res.write.push(function (value, oldVal) {
        return args
          ? writer.apply(vm, [value, oldVal].concat(args))
          : writer.call(vm, value, oldVal)
      })
    }
  })
  return res
}

_bind Method :

p._bind = function (def) {
  if (typeof def === 'function') {
    this.update = def
  } else {//  This version of vue The instruction has these hook methods :bind、update、unbind
    _.extend(this, def)
  }
  this._watcherExp = this.expression
  //  If the instruction exists bind Method , Call at this time 
  if (this.bind) {
    this.bind()
  }
  if (this._watcherExp && this.update){
    var dir = this
    var update = this._update = function (val, oldVal) {
        dir.update(val, oldVal)
    }
    //  Use the original expression as the identifier , Because the filter will make the same arg Become different observers 
    var watcher = this.vm._watchers[this.raw]
    if (!watcher) {
      //  The expression has not been created watcher, Instantiate a 
      watcher = this.vm._watchers[this.raw] = new Watcher(
        this.vm,
        this._watcherExp,
        update,
        {
          filters: this.filters
        }
      )
    } else {//  If it exists, the update function is added into 
      watcher.addCb(update)
    }
    this._watcher = watcher
    if (this._initValue != null) {//  Case with initial value , See in v-model The situation of 
      watcher.set(this._initValue)
    } else if (this.update) {//  Others will call update Method , therefore bind Method calls are followed by update Method 
      this.update(watcher.value)
    }
  }
  this._bound = true
}

Here you can know the instantiation Directive The command will be called when the bind Hook function , Usually do some initialization , The instruction is then initialized with a Watcher example , This instance will be used for dependency collection , Last but not least v-model The instruction will be called immediately update Method ,watcher When instantiating, the value of the expression will be calculated , So what you get at this time value It's the latest .

Watcher

Watcher Instance is used to parse expressions and collect dependencies , And trigger the callback update when the value of the expression changes . Mentioned in the first article $watch Method is also implemented using this class .

function Watcher (vm, expression, cb, options) {
  this.vm = vm
  this.expression = expression
  this.cbs = [cb]
  this.id = ++uid
  this.active = true
  options = options || {}
  this.deep = !!options.deep
  this.user = !!options.user
  this.deps = Object.create(null)
  if (options.filters) {
    this.readFilters = options.filters.read
    this.writeFilters = options.filters.write
  }
  //  Resolve the expression to getter/setter
  var res = expParser.parse(expression, options.twoWay)
  this.getter = res.get
  this.setter = res.set
  this.value = this.get()
}

The logic of the constructor is simple , Declare some variables 、 Resolve the expression to getter and setter The type of , such as :a.b The resolved get by :

function anonymous(o){
    return o.a.b
}

set by :

function set(obj, val){
    Path.se(obj, path, val)
}

In short, it is to generate two functions , An instance for this Set the value , An instance to get this Value on , The specific parsing logic is complex , Have the opportunity to analyze in detail or read the source code by yourself :/src/parsers/path.js.

Finally called. get Method :

p.get = function () {
  this.beforeGet()
  var vm = this.vm
  var value
  //  Call value method 
  value = this.getter.call(vm, vm)
  // “ touch ” Each attribute , So that they are all tracked as dependencies , For in-depth observation 
  if (this.deep) {
    traverse(value)
  }
  //  Apply filter function 
  value = _.applyFilters(value, this.readFilters, vm)
  this.afterGet()
  return value
}

Called before calling the value function. beforeGet Method :

p.beforeGet = function () {
  Observer.target = this
  this.newDeps = {}
}

Here we know the second article vue0.11 Version source code reading Series II : Data observation Mentioned in Observer.target What is it , Logic can also be concatenated ,vue Each attribute is intercepted during data observation , stay getter I can judge Observer.target Whether there is , If it exists, it will Observer.target Corresponding watcher Instance collects the dependent object instance of this property dep in :

if (Observer.target) {
    Observer.target.addDep(dep)
}

beforeGet Then the value function of the expression is called , The corresponding attribute will be triggered getter.

addDep Method :

p.addDep = function (dep) {
  var id = dep.id
  if (!this.newDeps[id]) {
    this.newDeps[id] = dep
    if (!this.deps[id]) {
      this.deps[id] = dep
      //  Collect this watcher Instance to the dependent object of the attribute 
      dep.addSub(this)
    }
  }
}

afterGet Used to do some reset and cleaning work :

p.afterGet = function () {
  Observer.target = null
  for (var id in this.deps) {
    if (!this.newDeps[id]) {//  Delete the attributes that are no longer dependent during this dependency collection 
      this.deps[id].removeSub(this)
    }
  }
  this.deps = this.newDeps
}

traverse Method is used to deeply traverse all nested properties , In this way, all nested properties that have been converted are collected as dependencies , That is, the of the expression watcher This attribute and all its descendants will be dep Object collection , In this way, a change in the value of a descendant attribute will also trigger an update :

function traverse (obj) {
  var key, val, i
  for (key in obj) {
    val = obj[key]//  This is it , Get this property to trigger getter, here Observer.target Attribute or this watcher
    if (_.isArray(val)) {
      i = val.length
      while (i--) traverse(val[i])
    } else if (_.isObject(val)) {
      traverse(val)
    }
  }
}

If the value of an attribute changes later, according to the first article, we know that in the attribute setter The subscriber will be called in the function update Method , This subscriber is Watcher example , Take a look at this method :

p.update = function () {
  if (!config.async || config.debug) {
    this.run()
  } else {
    batcher.push(this)
  }
}

It's normal to go else The branch ,batcher It will be updated asynchronously and in batch , But it was also called in the end run Method , So let's look at this method first :

p.run = function () {
  if (this.active) {
    //  Gets the latest value of the expression 
    var value = this.get()
    if (
      value !== this.value ||
      Array.isArray(value) ||
      this.deep
    ) {
      var oldValue = this.value
      this.value = value
      var cbs = this.cbs
      for (var i = 0, l = cbs.length; i < l; i++) {
        cbs[i](value, oldValue)
        //  When a callback deletes other callbacks , It's true at present. I don't know 
        var removed = l - cbs.length
        if (removed) {
          i -= removed
          l -= removed
        }
      }
    }
  }
}

The logic is simple , Traversal calls the watcher Instance all instructions update Method , The command will update the page .

Batch update, please move the article vue0.11 Version source code reading series 5 : How is batch update done .

Here, the template compilation process is over , Next, let's look at the specific process from the perspective of an instruction .

With if Take a look at the whole process

The template is as follows :

<div id="app">
    <div v-if="show"> I came out </div>
</div>

JavaScript The code is as follows :

window.vm = new Vue({
    el: '#app',
    data: {
        show: false
    }
})

Input at the console window.vm.show = true This div It will show .

Based on the above analysis , We know that v-if This instruction must eventually be called _bindDir Method :

image-20210111194832587

Get into Directive In the after _bind In the call if The directive bind Method , The simplified method is as follows :

{
    bind: function () {
        var el = this.el
        if (!el.__vue__) {
            //  We created two annotation elements to show and hide div Replaced with , See the figure below for the effect 
            this.start = document.createComment('v-if-start')
            this.end = document.createComment('v-if-end')
            _.replace(el, this.end)
            _.before(this.start, this.end)
        }
    }
}

image-20210111200014709

You can see bind What the method does is replace this element from the page with two annotation elements . bind Method is then created for this instruction watcher

image-20210111201925172

The next in watcher Li Gei Observer.target Assignment and value operation , Triggered show Attribute getter

image-20210112093317760

Called after dependency collection if The directive update Method , Take a look at this method :

{
    update: function (value) {
        if (value) {
            if (!this.unlink) {
                var frag = templateParser.clone(this.template)
                this.compile(frag)
            }
        } else {
            this.teardown()
        }
    }
}

Because our initial value is false, So go else Branch called teardown Method :

{
    teardown: function () {
        if (!this.unlink) return
        transition.blockRemove(this.start, this.end, this.vm)
        this.unlink()
        this.unlink = null
    }
}

This time unlink It's not worth it , So I went straight back , But if it's worth it ,teardown Method will be used first transition Class to remove the element , Then unbind the instruction .

Now let's type in the console window.vm.show = true, This triggers show Of setter

image-20210112101502685

And then call show Attribute dep Of notify Method ,dep Of our subscribers, only if The directive watcher, So I call watcher Of update Method , Finally call to if The directive update Method , The value at this point is true, So I will go to if In the branch ,unlink It's not worth it , So I call compile Method :

image-20210112102302256

{
    compile: function (frag) {
        var vm = this.vm
        transition.blockAppend(frag, this.end, vm)
    }
}

Part of the compilation process is ignored , You can see how to use it transition Class to display elements . This transition class we will vue0.11 Version source code reading series 6 : Transition principle Learn more in .

summary

It can be found that there is no so-called virtual in this early version DOM, No, diff Algorithm , Template compilation is to traverse elements and attributes on elements , Create an instruction instance for each attribute , Create a for the same instruction expression watcher example , Instruction instance provision update Methods watcher,watcher Will trigger all observed properties in the expression getter, then watcher Instances will be collected by the dependencies of these properties dep Gather up , Triggered when the attribute value changes setter, stay setter Will traverse dep All in watcher, Call update method , That is, provided by the instruction instance update Method , That is, the of the final instruction object update Method to complete the page update .

Of course , This part of the code is still relatively complex , It's far from as simple as this article says , Various recursive calls , Various function overloading , Call again and again , It's so beautiful , If you are interested, please read by yourself .

copyright notice
author[Corner Kobayashi],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2021/08/20210823142334506Z.html

Random recommended