current position:Home>Vue0.11 source code reading series 4: detailed instruction value analysis function

Vue0.11 source code reading series 4: detailed instruction value analysis function

2021-08-25 22:07:23 Corner Kobayashi

demand

First of all, this version of vue The following types of instruction values are supported, as well as through dirParser.parse Data to return :

1. Instance attributes :message, After parsing, it should be :

[
    {
        "raw":"message",
        "expression":"message"
    }
]

2. expression :message === 'show', After parsing, it should be :

[
    {
        "raw":"message === 'show'",
        "expression":"message === 'show'"
    }
]

3. Ternary expression :show ? true : false, After parsing, it should be :

[
    {
        "raw":"message === 'show' ? true : false",
        "expression":"message === 'show' ? true : false"
    }
]

4. Set the element class name 、 style 、 attribute 、 The event :red:hasError,bold:isImportant,hidden:isHidden, After parsing, it should be :

[
    {"arg":"red","raw":"red: hasError","expression":"hasError"},
    {"arg":"bold","raw":"bold: isImportant","expression":"isImportant"},
    {"arg":"hidden","raw":"hidden: isHidden","expression":"isHidden"}
]

perhaps :top: top + 'px',left: left + 'px',background-color: 'rgb(0,0,' + bg + ')', After parsing, it should be :

[
    {"arg":"top","raw":"top: top + 'px'","expression":"top + 'px'"},
    {"arg":"left","raw":"left: left + 'px'","expression":"left + 'px'"},
    {"arg":"background-color","raw":"background-color: 'rgb(0,0,' + bg + ')'","expression":"'rgb(0,0,' + bg + ')'"}
]

5. Double brace interpolation :{{partialId}}, After parsing, it should be :

[
    {
        "raw":"{{partialId}}",
        "expression":"{{partialId}}"
    }
]

6. Instruction values are arrays or objects

[1, 2, 3] Should resolve to :

[
    {
        "raw":"[1, 2, 3]",
        "expression":"[1, 2, 3]"
    }
]

{arr: [1, 2, 3], value: 'abc'} Should resolve to :

[
    {
        "raw":"{arr: [1, 2, 3], value: 'abc'}",
        "expression":"{arr: [1, 2, 3], value: 'abc'}"
    }
]

7. filter :

message | capitalize Should resolve to :

[
    {
        "expression":"message",
        "raw":"message | capitalize",
        "filters":[
            {
                "name":"capitalize",
                "args":null
            }
        ]
    }
]

Parameterized message | capitalize 4 5 Should resolve to :

[
    {
        "expression":"message",
        "raw":"message | capitalize 4 5",
        "filters":[
            {
                "name":"capitalize",
                "args":["4","5"]
            }
        ]
    }
]

Use between multiple filters | separation .

To sum up , If it is a comma separated colon expression , It can be interpreted as :

[
    {
        arg: 【 The character before the colon 】,
        expression: 【 The character after the colon 】,
        raw: 【 Original value 】
    },
    ...
]

There will be one more with a filter filters Field .

All others are resolved to :

[
    {
        expression: 【 The same value as the original value 】,
        raw: 【 Original value 】
    }
]

Realization

Now let's go from 0 Start writing a parser :

let dirs = []
function parse(s) {
    return dirs
}

Simple variables

First, support the simplest first , Instance property or method , According to the above comparison , Return basically intact :

var str = ''
var dirs = []
var dir = {}
var begin = 0
var i = 0
exports.parse = function (s) {
  str = s
  for (i = 0, l = str.length; i < l; i++) {}
  pushDir()
  return dirs
}
function pushDir () {
  dir.raw = str.slice(begin, i)
  dir.expression = str.slice(begin, i)
  dirs.push(dir)
}

You can see that it's a superfluous process to get the target value , The next step is to support comma separated colon expressions .

Colon expressions

Let's look at one situation first , Such as a:b, If the current character traversed is a colon, intercept the character before the colon as arg, The character after the colon is used as expression,begin Variables are used to mark the starting point of the current expression , So to intercept the character after the colon, you need to add a variable :

var str = ''
var dirs = []
var dir = {}
var begin = 0
var argIndex = 0 // ++
var i = 0
exports.parse = function (s) {
  str = s
  for (i = 0, l = str.length; i < l; i++) {
    // ++
    c = str.charCodeAt(i)
    if (c === 0x3A) {//  The colon :
      dir.arg = str.slice(begin, i).trim()
      argIndex = i + 1
    }
  }
  pushDir()
  return dirs
}
function pushDir () {
  dir.raw = str.slice(begin, i)
  dir.expression = str.slice(argIndex, i).trim()// ++
  dirs.push(dir)
}

Next, support the presence of commas , A comma is equivalent to the end of an expression , So we have to do it once push, in addition beginargIndexdir Variables need to be reset :

exports.parse = function (s) {
  str = s
  for (i = 0, l = str.length; i < l; i++) {
    c = str.charCodeAt(i)
    if (c === 0x3A) {//  The colon :
      dir.arg = str.slice(begin, i).trim()
      argIndex = i + 1
    } else if (c === 0x2C) {//  comma , ++
      pushDir()
      dir = {}
      begin = argIndex = i + 1
    }
  }
  pushDir()
  return dirs
}

Ternary expression

Next, support ternary expressions , At present, we will separate the parts before and after the colon of the ternary expression , It will output results similar to the following :

[
    {
        "arg":"show ? true",
        "raw":"show ? true : false",
        "expression":"false"
    }
]

So when we check the colon, we have to determine whether this is a ternary expression , If so, don't intercept :

exports.parse = function (s) {
  str = s
  for (i = 0, l = str.length; i < l; i++) {
    c = str.charCodeAt(i)
    if (c === 0x3A) {//  The colon : ++
      var arg = str.slice(begin, i).trim()
      if (/^[^\?]+$/.test(arg)) {
        dir.arg = arg
        argIndex = i + 1
      }
    } else if (c === 0x2C) {
      pushDir()
      dir = {}
      begin = argIndex = i + 1
    }
  }
  pushDir()
  return dirs
}

Determine whether there is... In the character before the colon ?, If it exists, it means that it is a ternary expression , No segmentation .

Look at a special case :background-color: 'rgb(0,0,' + bg + ')'

At present, it will be resolved into :

[
    {
        "arg":"background-color",
        "raw":"background-color: 'rgb(0",
        "expression":"'rgb(0"
    },
    {
        "raw":"0",
        "expression":"0"
    },
    {
        "raw":"' + bg + ')'",
        "expression":"' + bg + ')'"
    }
]

The reason lies in the comma in the attribute value , If there is a comma in the attribute value , Then the attribute value must be enclosed in quotation marks , So in single quotation marks or double quotation marks, ignore , So let's add two variables to record whether it is in quotation marks :

var inDouble = false // ++
var inSingle = false // ++

If the first quotation mark appears , Set the flag to true, Then the middle characters are skipped directly , Until closed quotation marks appear , Just quit and continue other judgments :

exports.parse = function (s) {
  str = s
  for (i = 0, l = str.length; i < l; i++) {
    c = str.charCodeAt(i)
    if (inDouble) {//  The double quotation marks are not closed yet  ++
      if (c === 0x22) {//  Closed quotation marks appear 
        inDouble = !inDouble
      }
    } else if (inSingle) {//  The single quotation mark is not closed yet  ++
      if (c === 0x27) {//  Closed quotation marks appear 
        inSingle = !inSingle
      }
    } else if (c === 0x3A) {
      // ...
    } else if (c === 0x2C) {
      // ...
    } else {// ++
      switch (c) {
        //  The first occurrence of quotation marks sets the flag bit 
        case 0x22: inDouble = true; break // "
        case 0x27: inSingle = true; break // '      
        default:
          break;
      }
    }
  }
  pushDir()
  return dirs
}

Array or object

Arrays or objects need to be returned intact , Because the colon and comma will be cut at present , For arrays , All characters are [] Surrounded by brackets , So the comma in this interval should be ignored , Because parentheses can be nested multiple times , So add a variable to count , The left parenthesis appears with 1, A closing parenthesis appears 1, by 0 It means not in parentheses :

var square = 0// ++
exports.parse = function (s) {
  str = s
  for (i = 0, l = str.length; i < l; i++) {
    c = str.charCodeAt(i)
    if (inDouble) {} 
    else if (inSingle) {} 
    else if (c === 0x3A) {} 
    else if (c === 0x2C && square === 0) {// ++
      pushDir()
      dir = {}
      begin = argIndex = i + 1
    } else {
      switch (c) {
        case 0x22: inDouble = true; break 
        case 0x27: inSingle = true; break   
        case 0x5B: square++; break        // [ ++
        case 0x5D: square--; break        // ] ++
        default:
          break;
      }
    }
  }
  pushDir()
  return dirs
}

Objects are similar , But the object has more colons , The colon will also be cut off , So you need to judge like a ternary expression :

var curly = 0// ++
exports.parse = function (s) {
  str = s
  for (i = 0, l = str.length; i < l; i++) {
    c = str.charCodeAt(i)
    if (inDouble) {} 
    else if (inSingle) {} 
    else if (c === 0x3A) {
      var arg = str.slice(begin, i).trim()
      if (/^[^\?\{]+$/.test(arg)) {// ++  Regular expression modification , If it does { The representative may be an object 
        dir.arg = arg
        argIndex = i + 1
      }
    } else if (c === 0x2C && square === 0 && curly === 0) {// ++
      pushDir()
      dir = {}
      begin = argIndex = i + 1
    } else {
      switch (c) {
        // ...
        case 0x7B: curly++; break         // { ++
        case 0x7D: curly--; break         // } ++
        default:
          break;
      }
    }
  }
  pushDir()
  return dirs
}

filter

Finally, let's look at the filter , The filter uses the pipe symbol |, So push the filter when traversing this character , The filter supports multiple filters , The first string represents the expression , follow-up | Separated each represents a filter , When the first | Only the value to which the filter is applied can be obtained , That is to say expression Value , You need to continue traversal to know the specific filter , How to judge whether it is the first | According to expression If there is a value :

exports.parse = function (s) {
    for (i = 0, l = str.length; i < l; i++) {
        c = str.charCodeAt(i)
        // ...
        else if (c === 0x7C) {//  Pipe, |
            if (dir.expression === undefined) {//  First appearance |
                dir.expression = str.slice(argIndex, i).trim()//  Take the first one | The first character is used as the value of the expression 
            }
        }
        // ...
    }
}
function pushDir () {
    dir.raw = str.slice(begin, i)
    if (dir.expression === undefined) {// ++  Judgment is also needed here , If there is a value, it means that it has been set by the filter branch , There's no need to set 
        dir.expression = str.slice(argIndex, i).trim()
    }
    dirs.push(dir)
}

Assuming that there is only one filter, the traversal will continue until the end , When it's over, it'll be adjusted again pushDir Method , So modify this method , Conduct a filter collection and treatment :

function pushDir () {
  dir.raw = str.slice(begin, i)
  if (dir.expression === undefined) {
    dir.expression = str.slice(argIndex, i).trim()
  } else {// ++  Add filter 
    pushFilter()
  }
  dirs.push(dir)
}
function pushFilter () {
    //  The string to be intercepted here should be | hinder ,begin and argIndex Fields don't work , So you need to add a new variable 
}

Add a variable to record the starting position of the current filter :

var lastFilterIndex = 0 // ++
exports.parse = function (s) {
    for (i = 0, l = str.length; i < l; i++) {
        c = str.charCodeAt(i)
        // ...
        else if (c === 0x7C) {
            if (dir.expression === undefined) {
                dir.expression = str.slice(argIndex, i).trim()
            }
            lastFilterIndex = i + 1// ++
        }
        // ...
    }
}

Because the filter supports parameters , Parameters and filter names are separated by spaces , So write a regular to match :/[^\s'"]+|'[^']+'|"[^"]+"/g, In addition to variables, parameters can also be strings , Therefore, the last two pairs of quotation marks are matched to ensure that the final matching result is also quoted , otherwise :capitalize 'abc' and capitalize abc The last match is :["abc"], What we need is the matching of the last two quotation marks :["'abc'"].

function pushFilter() {
  var exp = str.slice(lastFilterIndex, i).trim()
  if (exp) {
    var tokens = exp.match(/[^\s'"]+|'[^']+'|"[^"]+"/g)
    var filter = {}
    filter.name = tokens[0]
    filter.args = tokens.length > 1 ? tokens.slice(1) : null
    dir.filters = dir.filters || []
    dir.filters.push(filter)
  }
}

give the result as follows :

image-20210108154353418

Next, support the case of multiple filters , Multiple filters , There will be many |, So I'll go to | Of if Branch , If it does not appear for the first time, it does not need to be modified expression Value , direct push The filter currently traversed can :

exports.parse = function (s) {
    for (i = 0, l = str.length; i < l; i++) {
        c = str.charCodeAt(i)
        // ...
        else if (c === 0x7C) {
            if (dir.expression === undefined) {
                dir.expression = str.slice(argIndex, i).trim()
            } else {//  Not for the first time push ++ 
                pushFilter()
            }
            lastFilterIndex = i + 1
        }
        // ...
    }
}

give the result as follows :

image-20210108154842736

Finally, let's look at a special case , Namely || The situation of , This means or , So it can't be resolved to a filter , stay if Add judgment to the conditions , Exclude the currently traversed | The previous or next character is also | The situation of :

exports.parse = function (s) {
    for (i = 0, l = str.length; i < l; i++) {
        c = str.charCodeAt(i)
        // ...
        else if (c === 0x7C && str.charCodeAt(i - 1) !== 0x7C && str.charCodeAt(i + 1) !== 0x7C) {// ++ 
            // ...
        }
        // ...
    }
}

complete

It's almost done here , The complete code is as follows :

var str = ''
var dirs = []
var dir = {}
var begin = 0
var argIndex = 0 
var i = 0
var inDouble = false
var inSingle = false
var square = 0
var curly = 0
var lastFilterIndex = 0
function reset() {
  str = ''
  dirs = []
  dir = {}
  begin = 0
  argIndex = 0 
  i = 0
  inDouble = false
  inSingle = false
  square = 0
  curly = 0
  lastFilterIndex = 0
}
exports.parse = function (s) {
  reset()
  str = s
  for (i = 0, l = str.length; i < l; i++) {
    c = str.charCodeAt(i)
    if (inDouble) {//  The double quotation marks are not closed yet 
      if (c === 0x22) {//  Closed quotation marks appear 
        inDouble = !inDouble
      }
    } else if (inSingle) {//  The single quotation mark is not closed yet 
      if (c === 0x27) {//  Closed quotation marks appear 
        inSingle = !inSingle
      }
    } else if (c === 0x3A) {//  The colon :
      var arg = str.slice(begin, i).trim()
      if (/^[^\?\{]+$/.test(arg)) {
        dir.arg = arg
        argIndex = i + 1
      }
    } else if (c === 0x2C && square === 0 && curly === 0) {//  comma ,
      pushDir()
      dir = {}
      begin = argIndex = i + 1
    } else if (c === 0x7C && str.charCodeAt(i - 1) !== 0x7C && str.charCodeAt(i + 1) !== 0x7C) {//  Pipe, |
      if (dir.expression === undefined) {//  First appearance |
        dir.expression = str.slice(argIndex, i).trim()
      } else {//  Not for the first time push
        pushFilter()
      }
      lastFilterIndex = i + 1
    } else {
      switch (c) {
        case 0x22: inDouble = true; break // "
        case 0x27: inSingle = true; break // '  
        case 0x5B: square++; break        // [
        case 0x5D: square--; break        // ]
        case 0x7B: curly++; break         // {
        case 0x7D: curly--; break         // }
        default:
          break;
      }
    }
  }
  pushDir()
  return dirs
}
function pushDir () {
  dir.raw = str.slice(begin, i)
  if (dir.expression === undefined) {// ++  Judgment is also needed here , If there is a value, it means that it has been set by the filter branch , There's no need to set 
    dir.expression = str.slice(argIndex, i).trim()
  } else {// ++  Add filter 
    pushFilter()
  }
  dirs.push(dir)
}
function pushFilter() {
  var exp = str.slice(lastFilterIndex, i).trim()
  if (exp) {
    var tokens = exp.match(/[^\s'"]+|'[^']+'|"[^"]+"/g)
    var filter = {}
    filter.name = tokens[0]
    filter.args = tokens.length > 1 ? tokens.slice(1) : null
    dir.filters = dir.filters || []
    dir.filters.push(filter)
  }
}

Replace the above code vue The relevant code in the source code , Tested that the basic use case can run through , But there may be other special scenes that are not taken care of , For more perfect code, please read it yourself vue Source code .

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

Random recommended