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 :repeat
、if
、'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 :
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)
}
}
}
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
:
The next in watcher
Li Gei Observer.target
Assignment and value operation , Triggered show
Attribute getter
:
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
:
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 :
{
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 .