This article focuses on how the underlying mechanism of Mustache template engine is implemented in VUE, which is divided into four parts: Get to know it firstWhat is a template engine
, masterMustache basic use
Analysis,Mustache's underlying core mechanism
And finally write by handRealize the mustache library
Bottom code, refuse to talk about it. The whole process is a step-by-step process, and this article is full of dry stuff to make sure you really understandVue template engine
How the bottom layer works. Finally, I hope you guys pointpraise!!!!! So without further ado, mustache Template engine!!
Knowledge reserves
- I can write some J’s
avaScript
Common algorithms, for examplerecursive
,Two dimensional array traversal
And so on; - Be familiar with
ES6
Common features, such aslet
,Arrow function
,Deconstruction assignment
And so on; - To understand
webpack
和webpack-dev-server
If you are not familiar with Webpack, you can go to the updated Webpack column Webpack To master column for detailed reading.
What is a template engine
A template engine is the most elegant solution for turning data into views. Turn the data into a DOM, and then render the DOM structure into the page.
/ / data
const data = {
array: [{name: 'Alex'.sex: 'male'.age: 18},
{name: 'Jack'.sex: 'male'.age: 20},
{name: 'the qingfeng-xiangguang fracture'.sex: 'male'.age: 19]}},Copy the code
<! -- DOM structure view -->
<ul>
<li>
<div class="hd">Basic information about Alex</div>
<div class="bd">
<p>Name: Alex</p>
<p>Gender: male</p>
<p>Age: 18</p>
</div>
</li>
<li>
<div class="hd">Jack's basic information</div>
<div class="bd">
<p>Name: Jack</p>
<p>Gender: male</p>
<p>Age: 20</p>
</div>
</li>
<li>
<div class="hd">Basic information about Alex</div>
<div class="bd">
<p>Name: Qingfeng</p>
<p>Gender: male</p>
<p>Age: 19</p>
</div>
</li>
</ul>
Copy the code
In the example above, in Vue, it is easy to iterate through it with a V-for and render it directly to the page.
<li -v-for = "(item, index) in data.array" :key="index">
Copy the code
Historically, data has been transformed into views
Pure DOM methods
Create dOM tags by iterating through the data using pure dOM methods and manually climbing the tree.
const data = {
array: [{name: 'Alex'.sex: 'male'.age: 18},
{name: 'Jack'.sex: 'male'.age: 20},
{name: 'the qingfeng-xiangguang fracture'.sex: 'male'.age: 19]}},let ul = document.querySelector('.list')
for(let i = 0; i < data.array.length; i++) {
let oli = document.createElement('li')
oli.innerText = data.array[i].name + 'Basic Information'
let divhd = document.createElement('div')
divhd.className = "hd"
let divbd = document.createElement('div')
divbd.className = "bd"
let p1 = document.createElement('p')
let p2 = document.createElement('p')
let p3 = document.createElement('p')
p1.innerText = 'Name:' + data.array[i].name
p2.innerText = 'Gender:' + data.array[i].sex
p3.innerText = 'Age:' + data.array[i].age
divbd.appendChild(p1)
divbd.appendChild(p2)
divbd.appendChild(p3)
oli.appendChild(divhd)
oli.appendChild(divbd)
ul.appendChild(oli)
}
Copy the code
I’m sure you’re all freaking out when you see how to create a pure DOM above. This isn’t a very complicated example, but if you nested a loop inside a loop, you might want to switch industries. This method is very primitive and has no practical value.
Array Join () method
Because of the complexity of the pure DOM approach above, people came up with a way to create DOM tags using strings instead of structured HTML.
const list = document.querySelector('.list')
const data = {
array: [{name: 'Alex'.sex: 'male'.age: 18},
{name: 'Jack'.sex: 'male'.age: 20},
{name: 'the qingfeng-xiangguang fracture'.sex: 'male'.age: 19]}},// Iterate over the data to add the HTNL string to the list from a string perspective
for(let i = 0; i < data.array.length; i++) {
list.innerHTML += [
'<li>'.'
'
+ data.array[i].name +'Basic info '.'
'
.' Name:'
+ data.array[i].name + '</p>'.' Gender:'
+ data.array[i].sex + '</p>'.' Age:'
+ data.array[i].age + '</p>'.' '.'</li>'
].join(' ')}Copy the code
Join () method
Isn’t it better than the topPure DOM methods
It’s a lot simpler, and the code looks a lot cleaner, the way we program.
ES6 backquotes
We can also optimize our join method using the backquoted template string of ES6 syntax.
const list = document.querySelector('.list')
const data = {
array: [{name: 'Alex'.sex: 'male'.age: 18},
{name: 'Jack'.sex: 'male'.age: 20},
{name: 'the qingfeng-xiangguang fracture'.sex: 'male'.age: 19]}},for(let i = 0; i < data.array.length; i++) {
list.innerHTML += `
<li>
<div class="hd">${data.array[i].name}</div> <div class="bd">${data.array[i].name}</p> <p>${data.array[i].sex}</p> <p>${data.array[i].age}</p>
</div>
</li>
`
}
Copy the code
Of the three methods above, the one we use more often in actual development is the third way to create tags in backquotes and render the DOM onto the view layer. So let’s go to mustache, the most elegant way to turn data into views.
The basic use of Mustache
- The introduction of
Mustache library
, either through NPM or through CDN using script. - use
Mustache. Render ()
Merge templates with data, with the first parameter representing the template and the second parameter representing the data.
Array of loop objects
In Mustache you must have {{#}} to start and {{/}} to end.
<div class="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script></script>
Copy the code
Loop through nested arrays
<div class="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script></script>
Copy the code
Loop simple array
<div class="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script>
const container = document.querySelector('.container')
const data = {
array: ['the qingfeng-xiangguang fracture'.'Alex'.'Jack']}var templateStr = ` {{#array}}
- {{.}}
{{/array}} `
let dom = Mustache.render(templateStr,data)
// Write to innerHtml
container.innerHTML = dom
</script>
Copy the code
Don’t cycle
<div class="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script>
const container = document.querySelector('.container')
const data = {
name: 'the qingfeng-xiangguang fracture'.age: 18
}
var templateStr = '< H1 > Name: {{name}}, age: {{age}}
'
let dom = Mustache.render(templateStr,data)
// Write to innerHtml
container.innerHTML = dom
</script>
Copy the code
Boolean value
Mustache also uses conditional rendering, which is very similar to the V-if we use with Vue. One caveat though: You can’t write expressions, which proves that Mustache is a weakly-typed library.
<div class="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script>
const container = document.querySelector('.container')
const data = {
isShow: true
}
var templateStr = '{{#isShow}} Qingfeng
{{/isShow}}'
let dom = Mustache.render(templateStr,data)
// Write to innerHtml
container.innerHTML = dom
</script>
Copy the code
So much for the basic use of mustache, we’ve found that with the exception of no loop, you need to use {{#}} to start and {{/}} to end.
The underlying mechanism of Mustache
When we started mustache, a lot of people probably thought that mustache library was just a regular expression idea. Of course, this is a good idea, and it is possible to implement some simple examples, but there are some complex cases where the idea of regular expressions fails, such as loop nesting. Of course, let’s see if we can do this with regular expressions.
Replace () method
Before implementing it, we need to look at how the replace () method in regular expressions is used to give us a better understanding of what the code we are writing means.
Use the replace () method to match regular expressions:
- The first parameter:
The re to match
. - The second parameter:
It can be a replacement string, or it can be a function
. - The second argument is in the form of a function: the first argument represents
The portion of the matched re
, the second parameter indicatesMatched character
The third one meansThe position of the matched character
And the fourth one isString to match
. Normally the second parameter is the data we want.
<div class="container"></div>
<script>
const container = document.querySelector('.container')
const data = {
name: 'the qingfeng-xiangguang fracture'.age: 18
}
var templateStr = '< H3 > Name: {{name}}, age: {{age}}
// Inside the re () means to capture the letter data inside
let dom = templateStr.replace(/\{\{(\w+)\}\}/g.(. args) = > data[args[1]])
console.log(dom);
</script>
Copy the code
A simple template engine implementation mechanism is written, using regular expressionsReplace () method
.Replace () method
The second argument to can be a function that provides an argument to the thing captured, combined with the data object, for intelligent substitution.
Bottom tokens
We know that Mustache isn’t implemented with regular expressions, so how does it work underneath? In fact, the mechanism of this is the picture below.Let’s interpret the basic flow of the picture above:
willTemplate characters
List firstCompiled into tokens
Tokens as intermediate forms and thenThen combine data with tokens
, andParse to a DOM string
.
So the question is, right? What are tokens? Tokens are nested arrays of JS, which are, in essence, JS representations of template strings. They are the origin of AST and virtual nodes. It may seem abstract, but let’s use code to understand what tokens are.
Simple tokens
Template string:
<h1>Name: {{name}} age: {{age}}</h1>
Copy the code
Tokens:
[["text"." Name:
],
["name"."name"],
["text".", age:],
["name"."age"],
["tetx"."</h1>"]]Copy the code
Circularly nested tokens
Template string:
var templateStr = ` < ul > < li > {{# array}} {{name}} hobby < ol > < li > {{# hobbies}} {{...}} < / li > {{/ hobbies}} < / ol > < / li > {{/ array}} < / ul > `
Copy the code
Tokens:
[["text"."<ul>"],
["#"."array"The [["text"."<li>"],
["name"."name"],
["text".< OL >], ["#","hobbies", [["text","<li>"[the],"name","."[the],"text","</li>"[the]]],"text","</ol></li>"[the]]],"text","</ul>"]]Copy the code
By printing tokens, we can see that there are three tokens on the outer layer. The tokens of the # type in the middle also contain nested tokens. After careful observation, we know that whenWith {{}}
Is wrapped into an array, the first of which represents the type (The text type
Including tags, text,Type the name
Represents a match to {{}},# type
The second is text content, and this array is one of themtoken
. All the tokens combine to form onetokens
.
Write a mustache
Handwriting implementation Scanner class
As we’ve seen before, we use mustache’s basic usage by combining the template with the data using the mustache library’s mustache.render () method to generate a DOM structure. How does the bottom layer compile our template strings into tokens? Use a Scanner Scanner to intercept and transition the {{and}} of the template string. Let’s look at the two main methods of the Scanner class:
The scan function
Action: Skips the specified content without returning a value.ScanUtil function
Allows a pointer to scan a template string until it encounters the specified content and returns the text before the specified content.
So how do you collect strings? We need to provide an identified variable tail (the tail changes with the pointer and contains the pointer) to collect strings
Now that you know the two main ways that Scanner works, let’s look at its flow chart:
The basic code implementation process is as follows:
export default class Scanner {
constructor(templateStr) {
this.templateStr = templateStr
/ / pointer
this.pos = 0
// The tail is the original length of the template string
this.tail = templateStr
}
// Skip the specified content without returning a value
scan(jump) {
// indexOf the string returns 0 to indicate that the first item is the specified one
if (this.tail.indexOf(jump) == 0) {
// Change the pointer to skip the specified content directly
this.pos += jump.length
// Update the tail
this.tail = this.templateStr.substring(this.pos)
}
}
// Let the pointer scan the template string until it encounters the specified content and returns the text before the specified content
scanUtil(stopTag) {
// Record the position of the pos pointer to which this method is executed
const pos_pack = this.pos
// If the tail does not start with the specified content, the scanner is not looking for the content
// The length of the pointer must be smaller than the length of the template string
while (!this.eos() && this.tail.indexOf(stopTag) ! = =0 ) {
// Move the pointer down if the specified content is not found
this.pos++
// The tail follows the pointer
this.tail = this.templateStr.substring(this.pos)
}
// Return the preceding text if found
return this.templateStr.substring(pos_pack, this.pos)
}
// Whether the pointer has reached the end
eos () {
return this.pos >= this.templateStr.length
}
}
Copy the code
ParseTemplelateTokens () method
The main effect of this method is to turn HTML into tokens.
import Scanner from './Scanner'
import nestTokens from './nestTokens'
export default function parseTemplateTokens(templateStr) {
// Instantiate a scanner construct that provides an argument specifically to serve the template string
// Handle template strings to generate tokens service
const scanner = new Scanner(templateStr)
// Store tokens in an array to form tokens
let tokens = []
// Collect text content as it passes by
let words
while(! scanner.eos()) {// Collect text content as it passes by
words = scanner.scanUtil('{{')
if(words ! =' ') {
// Flag bit cannot remove Spaces from class names
let isClass = false
// remove whitespace
/ / stitching
var _words = ' '
for (let i = 0; i < words.length; i++) {
// Check whether it is in the tag
if (words[i] == '<') {
isClass = true
} else if (words[i] == '>') {
isClass = false
}
// The current item is not on space concatenation
if (!/\s/.test(words[i])) {
_words += words[i]
} else {
// Yes Spaces are concatenated only within tags
if(isClass) {
_words += ' '
}
}
}
tokens.push(["text",_words])
}
// Skip the specified content
scanner.scan('{{')
words = scanner.scanUtil('}} ')
if(words ! =' ') {
if (words[0= = =The '#') {
tokens.push(["#", words.substring(1)])}else if (words[0= = ='/') {
tokens.push(["/", words.substring(1)])}else {
tokens.push(["name", words])
}
}
scanner.scan('}} ')}return nestTokens(tokens)
}
Copy the code
Ok, so we’ve wrapped a template string into a token, just a simple template string into a token, we haven’t done the loop bridge yet, so let’s go ahead and fold the token,
NestTokens () method
The nestTokens () method folds tokens, combining tokens between # and/as its subscript 2 to form a token. When writing nestTokens, we need to know the concept of stack in data structure. Stack first in first out, queue first in first out. When folding tokens, we need to use the idea of stack in data structure to fold tokens.
Given the idea of the stack, we can think of when we run through loopsThe # symbol
Will advance the stack (stack) when encountered/ symbol
It will be out of the stack. The relevant codes are as follows:
export default function nestTokens(tokens) {
// The assembled result array
const nestTokens = []
// stack is used to store #
let section = []
console.log(tokens);
for (let i = 0; i < tokens.length; i++) {
// iterate over each term
let token = tokens[i]
// Determine whether the first item of the array is of type # or/or text
switch (token[0]) {
case "#":
// store tokens after # into the item with subscript 2 at the end of the stack array.
// as a child of the current array
token[2] = []
/ / into the stack
section.push(token)
// Pushing is also storing an array in the result array
nestTokens.push(token)
break
case "/":
/ / out of the stack
let section_pop = section.pop()
// Place the stack array in the result array
nestTokens.push(section_pop)
break
default:
// If the stack is empty, place it in the result array
if (section.length == 0) {
nestTokens.push(token)
} else {
// Not null indicates that the stack has data fetched from the top of the stack and the token array after # is placed in the top item of the stack with the current subscript 2
section[section.length - 1] [2].push(token)
}
}
}
console.log(nestTokens);
}
Copy the code
Here’s a rundown of the code: First, we declare a section array as the stack and return result array, iterate over each token, and determine whether the first item in the current array is # or /, # pushed. To push the current array, / is removed, and the current array is popped. That is to say the current array first is neither # is not/can be directly deposited into the array, when the judge has an array is the first item #, will pressure into the stack, and can store the results in an array, we can know by what is said above token, will encounter # in the current array subscript 2 create an array used to store data, The # above is pushed, indicating that the section stack has data, and all subsequent array data will be stored in the array with subscript 2 at the top of the stack until the next # is reached, and then the process is repeated. When the first item of the array is /, it indicates that the unstacked array needs to be stored in the result array.
The above is the general process of nestTokens, combined with the following diagram, I believe you can better understand.
Test results:When we thought we were really going to write the nestTokens method, the printed results were not what we expected. And if you look at the code that we’ve written, you can see that we’re actually on the right track, but every time we do a stack we create an array for the top of the stack, and then the top of the stack doesn’t become a subarray for the bottom of the stack. So, we need to find a collector that can go up layer by layer. We know what the problem is, so we’re going to change the code, right
export default function nestTokens(tokens) {
// The assembled result array
const nestTokens = []
// stack is used to store #
let section = []
// The collector starts by pointing to the result array as a reference type
When # is encountered, the collector points to a new array with the token subscript 2
var collector = nestTokens
for (let i = 0; i < tokens.length; i++) {
// iterate over each term
let token = tokens[i]
// Determine whether the first item of the array is of type # or/or text
switch (token[0]) {
case "#":
// The token is placed in the collector
collector.push(token)
/ / into the stack
section.push(token)
// Change the collector
// store tokens after # into the item with subscript 2 at the end of the stack array.
// Add an item with subscript 2 to the token and have the collector point to it
collector = token[2] = []
break
case "/":
/ / out of the stack
section.pop()
// Change the collector to an array of subscripts 2 for the item at the bottom of the stack stack
collector = section.length > 0 ? section[section.length - 1] [2] : nestTokens
break
default:
collector.push(token)
}
}
console.log(nestTokens);
return nestTokens
}
Copy the code
Running results:
Once we do that, we have exactly the nested array of subarrays we want, and we have perfectly folded tokens. Now that you have compiled tokens from template strings, all that remains is to merge tokens with data and parse to generate DOM.
RenderTemplate () method
The renderTemplate () method combines tokens with data to generate DOM strings. Let’s start with a simple tokens and data.
<script src="/xuni/bundle.js"></script>
<script></script>
Copy the code
/* Merge tokens with data to generate DOM strings */
export default function renderTemplate (tokens, data) {
console.log(tokens);
console.log(data);
// Result string
let str = ' '
for(let i = 0; i < tokens.length; i++) {
let token = tokens[i]
// Determine whether the first item is text or name and #
if(token[0] = ="text") {
// Concatenate data directly into the result string
str += token[1]}else if (token[0] = ="name") {
// Find data from data
str += data[token[1]]}else if (token[0] = ="#") {
// loop recursively}}console.log(str);
}
Copy the code
We can merge a simple template string with data to generate a DOM string, but it is important to note that data[token[1]] cannot be used to retrieve data from data when the data is deeply nested. For example: data[‘ A.B.C ‘], change the ‘A.B.C’ in [] to a string, and then search for the data corresponding to the string. So we need to process the form data[‘ A.B.C ‘].
Lookup () method
When the template string has a form such as item.name, we need to process the string and retrieve the corresponding data.
<script>
const data = {
person: {
name: 'the qingfeng-xiangguang fracture'.age: 18}}var templateStr = ' Name: {{person.name}}, age: {{person.age}}
'
templateEngine.render(templateStr, data)
</script>
Copy the code
/** * process nested objects to get the data of the lowest object *@param {*} Obj The object passed in the original object *@param {*} KeyName The string passed in, such as Person.name *@returns Returns to get data in a nested object */
export default function lookup(obj, keyName) {
// Handle the normal array of data
if(keyName ! ='. ') {// Use the array redecu method for traversal
return keyName.split('. ').reduce((pre, next) = > {
return pre[next]
}, obj)
}
return data[keyName]
}
Copy the code
/* Merge tokens with data to generate DOM strings */
import lookup from "./lookup";
export default function renderTemplate (tokens, data) {
console.log(tokens);
console.log(data);
// Result string
let str = ' '
for(let i = 0; i < tokens.length; i++) {
let token = tokens[i]
// Determine whether the first item is text or name and #
if(token[0] = ="text") {
// Concatenate data directly into the result string
str += token[1]}else if (token[0] = ="name") {
// Find data from data to concatenate into the result string
str += lookup(data, token[1])}else if (token[0] = ="#") {
// loop recursively}}console.log(str);
}
Copy the code
Once you’ve dealt with the nesting of objects in data, it’s just a matter of determining the recursion of #.
ParseArray () method
The parseArray () method is mainly used to recursively traverse the array inside #. It should be noted that when data is a simple array, we directly render in the form of {{.}}, before this, we did not process the ‘. ‘attribute, so when we recurse, we need to add a’. ‘attribute, and the value is itself, so as to correctly render the data.
import lookup from "./lookup";
import renderTemplate from "./renderTemplate";
/** * Handle array structure renderTemplate to implement recursion *@param {*} Token An item of data that contains # ['#','hobbies',[]] *@param {*} The number of calls is determined by the length of the data */
export default function parseArray (token, data) {
// Get the data needed in this array
var v = lookup(data, token[1])
// Result string
var str = ' '
// Iterate over data instead of tokens Data as many times as you have in the array of Tokens Data
for(let i = 0; i < v.length; i++) {
str += renderTemplate(token[2] and {// When the data is a simple array rather than an array of objects
// We need to add a '. 'attribute for the current v[I] data itself. v[i],'. ': v[i]
})
}
return str
}
Copy the code
TemplateEngine () method
import parseTemplateTokens from './parseTemplateTokens'
import renderTemplate from './renderTemplate'
window.templateEngine = {
render(templateStr, data) {
// Make template strings into tokens
const tokens = parseTemplateTokens(templateStr)
// Combine tokens with data to generate DOM strings
let domStr = renderTemplate(tokens, data)
// Returns the DOM string
return domStr
}
}
Copy the code
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<div class="container"></div>
<script src="/xuni/bundle.js"></script>
<script></script>
</body>
</html>
Copy the code
Run the code output:
This is the main part of the Mustache source code for Vue source parsing. Some of the details are not addressed in this article, but the general function is basically implemented. After reading this article, you may find that the best part of this article is the idea of using collectors and stacks in the nestTokens method. This idea is worth learning! This is also read the charm of the source code, you will find that the algorithm is really wonderful!! The relevant source code in the article has been sent to Gitee, you can get the source code stamp me!!
Finally, after reading this article, I hope it will help you to understand how Vue performs V-for rendering. I hope you can give a thumbs-up!!