Written in the beginning

  • Recently Yuxi released 5KB of Petite-Vue. Curious me, I clone his source code, to give you a wave of analysis.
  • Recently due to the work of many things, so slow down the pace of the original! Your understanding
  • Want to see me forward handwritten source code + a variety of source code analysis can pay attention to my public number to see myGitHub, the basic front end of the framework source code has been analyzed

The official start of the

  • Petite-Vue is a 5KB Vue, so we’ll find the warehouse and clone it

    https://github.com/vuejs/petite-vue
  • After cloning, it is found that it is launched in the form of vite + petite-vue + multi-page
  • Start command:

    git clone https://github.com/vuejs/petite-vue
    cd /petite-vue
    npm i 
    npm run dev
    
  • Then open thehttp://localhost:3000/You can see the page:


Nanny teaching

  • Now that the project has started, let’s first parse the project entry. Since we are using vite as the build tool, start with the index.html population in the root directory:

    <h2>Examples</h2>
    <ul>
    <li><a href="/examples/todomvc.html">TodoMVC</a></li>
    <li><a href="/examples/commits.html">Commits</a></li>
    <li><a href="/examples/grid.html">Grid</a></li>
    <li><a href="/examples/markdown.html">Markdown</a></li>
    <li><a href="/examples/svg.html">SVG</a></li>
    <li><a href="/examples/tree.html">Tree</a></li>
    </ul>
    
    <h2>Tests</h2>
    <ul>
    <li><a href="/tests/scope.html">v-scope</a></li>
    <li><a href="/tests/effect.html">v-effect</a></li>
    <li><a href="/tests/bind.html">v-bind</a></li>
    <li><a href="/tests/on.html">v-on</a></li>
    <li><a href="/tests/if.html">v-if</a></li>
    <li><a href="/tests/for.html">v-for</a></li>
    <li><a href="/tests/model.html">v-model</a></li>
    <li><a href="/tests/once.html">v-once</a></li>
    <li><a href="/tests/multi-mount.html">Multi mount</a></li>
    </ul>
    
    <style>
    a {
      font-size: 18px;
    }
    </style>
  • This is an example of the MPS + VUE + Vite demo item, and we found a simple demo page commits:

    <script type="module"> import { createApp, reactive } from '.. /src' const API_URL = `https://api.github.com/repos/vuejs/vue-next/commits?per_page=3&sha=` createApp({ branches: ['master', 'v2-compat'], currentBranch: 'master', commits: null, truncate(v) { const newline = v.indexOf('\n') return newline > 0 ? v.slice(0, newline) : v }, formatDate(v) { return v.replace(/T|Z/g, ' ') }, fetchData() { fetch(`${API_URL}${this.currentBranch}`) .then((res) => res.json()) .then((data) => { this.commits = data }) } }).mount() </script> <div v-scope v-effect="fetchData()"> <h1>Latest Vue.js Commits</h1> <template v-for="branch in  branches"> <input type="radio" :id="branch" :value="branch" name="branch" v-model="currentBranch" /> <label :for="branch">{{ branch }}</label> </template> <p>vuejs/vue@{{ currentBranch }}</p> <ul> <li v-for="{ html_url, sha, author, commit } in commits"> <a :href="html_url" target="_blank" class="commit" >{{ sha.slice(0, 7) }}</a > - <span class="message">{{ truncate(commit.message) }}</span><br /> by <span class="author" ><a :href="author.html_url" target="_blank" >{{ commit.author.name }}</a ></span > at <span class="date">{{ formatDate(commit.author.date) }}</span> </li> </ul> </div> <style> body { font-family: 'Helvetica', Arial, sans-serif; } a { text-decoration: none; color: #f66; } li {line-height: 1.5em; margin-bottom: 20px; } .author, .date { font-weight: bold; } </style>
  • You can see the introduction at the top of the page

    import { createApp, reactive } from '.. /src'

    Start with the source boot function

  • Start with createApp and find the source code:

    //index.ts
    export { createApp } from './app'
    ...
    import { createApp } from './app'
    
    let s
    if ((s = document.currentScript) && s.hasAttribute('init')) {
    createApp().mount()
    }
    

    The document.currentScript property returns the ownership of the currently running script
    <script> Elements. The script that calls this property cannot be a JavaScript module; the module should use an import.meta object.

  • So this code up here means createsVariable records the currently running script element, if any, with specified attributesinit, then call the createApp and mount methods.
  • But here the project is actively calling the exposedcreateAppMethods. Let’s go and seecreateAppThe source code for this method is about 80 lines of code
import { reactive } from '@vue/reactivity' import { Block } from './block' import { Directive } from './directives' import { createContext } from './context' import { toDisplayString } from './directives/text' import { nextTick } from './scheduler' export default function createApp(initialData? : any){ ... }
  • The createApp method receives an initial piece of data, which may or may not be of any type. This method is the entrance function, dependent on the function is also more, we need to calm down. This function comes in and makes a bunch of stuff

    createApp(initialData? : any){ // root context const ctx = createContext() if (initialData) { ctx.scope = reactive(initialData) } // global internal helpers ctx.scope.$s = toDisplayString ctx.scope.$nextTick = nextTick ctx.scope.$refs = Object.create(null) let  rootBlocks: Block[] }
  • The above code creates a CTX context object and gives it a number of properties and methods. It is then provided to the object returned by CreateApp for use
  • CreateContext creation context:

    export const createContext = (parent? : Context): Context => { const ctx: Context = { ... parent, scope: parent ? parent.scope : reactive({}), dirs: parent ? parent.dirs : {}, effects: [], blocks: [], cleanups: [], effect: (fn) => { if (inOnce) { queueJob(fn) return fn as any } const e: ReactiveEffect = rawEffect(fn, { scheduler: () => queueJob(e) }) ctx.effects.push(e) return e } } return ctx }
  • Make a simple inheritance from the parent object passed in, and return a new onectxObject.

I almost fell into the trap at the beginning, and I’m writing this article to try to get you to understand the simplicity
vuePrinciple, like the last time I wrote the nugget editor source code parsing, write too fine, too tired. I’m simplifying it this time just to make sure everyone understands that these things up here don’t matter. this
createAppThe function returns an object:

return { directive(name: string, def? : Directive) { if (def) { ctx.dirs[name] = def return this } else { return ctx.dirs[name] } }, mount(el? : string | Element | null){}... , unmount(){}... }
  • There are three methods on the object, for exampledirectiveThe instruction will be usedctxProperties and methods of. So they start off with a bunch of stuffctxThe above is for use in the following methods
  • Mount method mount method mount method

    mount(el? : string | Element | null) { if (typeof el === 'string') { el = document.querySelector(el) if (! el) { import.meta.env.DEV && console.error(`selector ${el} has no matching element.`) return } } ... }
  • The first thing we’re going to do is say if it’s a string, then we’re going to go back and look for that node, otherwise we’re going to look for document

    el = el || document.documentElement
    • Define roots, an array of nodes

      let roots: Element[] if (el.hasAttribute('v-scope')) { roots = [el] } else { roots = [...el.querySelectorAll(`[v-scope]`)].filter( (root) => ! root.matches(`[v-scope] [v-scope]`) ) } if (! roots.length) { roots = [el] }
  • If there is a v-scope attribute, put EL in the array and assign it to roots. Otherwise, find all the nodes with v-scope attribute under this EL, then filter out the nodes without v-scope attribute under the v-scope attribute and stuff them into the roots array

    If at this time
    rootsIt’s still empty, so let’s take
    elPut it in.


    Here’s a warning in development mode:
    Mounting on documentElement - this is non-optimal as petite-vue “Means to use
    documentNot the best option.

  • After finishing the roots, start the action.

    rootBlocks = roots.map((el) => new Block(el, ctx, true)) // remove all v-cloak after mount ; [el, ...el.querySelectorAll(`[v-cloak]`)].forEach((el) => el.removeAttribute('v-cloak') )
  • thisBlockAfter passing in the node and context, the ‘v-cloak’ property is removed, and the mount function is finishedBlockThe inside.

There’s a problem here, and we’ve only got it so far
elthis
domNode, but Vue is full of template syntax, how does that template syntax translate into the DOM?

  • Block turns out not to be a function, but a class.

  • You can see this in the constructor constructor

    constructor(template: Element, parentCtx: Context, isRoot = false) {
      this.isFragment = template instanceof HTMLTemplateElement
    
      if (isRoot) {
        this.template = template
      } else if (this.isFragment) {
        this.template = (template as HTMLTemplateElement).content.cloneNode(
          true
        ) as DocumentFragment
      } else {
        this.template = template.cloneNode(true) as Element
      }
    
      if (isRoot) {
        this.ctx = parentCtx
      } else {
        // create child context
        this.parentCtx = parentCtx
        parentCtx.blocks.push(this)
        this.ctx = createContext(parentCtx)
      }
    
      walk(this.template, this.ctx)
    }
  • The above code can be broken down into three pieces of logic

    • Create a templatetemplateThe way Clone nodes are used, due todomWhen the node is retrieved, it is an object, so a clone is done.)
    • If not the root node, then recursively inheritctxcontext
    • After processing CTX and Template, callwalkfunction
  • walkFunction analysis:

  • I’m going to make a judgment based on NodeType, and then I’m going to do something different
  • If it’s aelementNodes, which process different instructions, for examplev-if

  • Here’s a utility function to look at first

    export const checkAttr = (el: Element, name: string): string | null => { const val = el.getAttribute(name) if (val ! = null) el.removeAttribute(name) return val }
  • This function checks to see if the node contains itv-xxProperty, and then return the result and delete the property
  • For example, when a node is determined to have a v-if property, call the method to handle it and remove the property (which has already been handled as an identifier).

    I wanted to go to bed before 12 o ‘clock, but I was told that there was only 5KB. I tried to find the simplest instruction to analyze, but each instruction code had more than 100 lines. I worked overtime until 9 o ‘clock tonight, and just finished the micro-front end transformation. It’s early in the morning

  • The v-if handler is about 60 lines long

    export const _if = (el: Element, exp: string, ctx: Context) => {
    ...
    }
  • First the _if function takes the value of the el node and exp, the v-if, and the CTX context object

    if (import.meta.env.DEV && ! exp.trim()) { console.warn(`v-if expression cannot be empty.`) }
  • A warning is issued if it is null
  • Then take the parent of the EL node and create a comment node based on the value of the exp and insert it in front of the EL. At the same time create an array of branches to store the exp and the EL

     const parent = el.parentElement!
    const anchor = new Comment('v-if')
    parent.insertBefore(anchor, el)
    
    const branches: Branch[] = [
      {
        exp,
        el
      }
    ]
    
    // locate else branch
    let elseEl: Element | null
    let elseExp: string | null

    Comment interface represents textual notations between markup. Although it is not usually displayed, you can see them when you look at the source code. In HTML and XML, Comments are
    '<! - 'and' - > 'Between the content. In XML, the character sequence ‘–‘ cannot appear in comments.

  • We then create the elseEl and elseExp variables and loop through all the else branches and store them in branches

    while ((elseEl = el.nextElementSibling)) {
      elseExp = null
      if (
        checkAttr(elseEl, 'v-else') === '' ||
        (elseExp = checkAttr(elseEl, 'v-else-if'))
      ) {
        parent.removeChild(elseEl)
        branches.push({ exp: elseExp, el: elseEl })
      } else {
        break
      }
    }

    This will have all of the Branches in the V-IF branch, which can be viewed as a tree traversal (breadth first search)

  • Next, according to the trigger of the side effect function, each time traverse branches to find the branch that needs to be activated, insert the node into parentNode, and return to nextNode to achieve thisv-ifThe effect of

    This is all HTML, so we don’t have to deal with the virtual DOM stuff, but we’re only dealing with a single node, if it’s deep level
    domNode, which is where the depth-first search comes in

     // process children first before self attrs
      walkChildren(el, ctx)
    
    
    const walkChildren = (node: Element | DocumentFragment, ctx: Context) => {
    let child = node.firstChild
    while (child) {
      child = walk(child, ctx) || child.nextSibling
    }
    }
    
  • When there is no one on the nodev-ifThe first child node of the node is then taken to do the above action, matching each onev-if v-forInstruction like that
If it’s a text node
else if (type === 3) {
    // Text
    const data = (node as Text).data
    if (data.includes('{{')) {
      let segments: string[] = []
      let lastIndex = 0
      let match
      while ((match = interpolationRE.exec(data))) {
        const leading = data.slice(lastIndex, match.index)
        if (leading) segments.push(JSON.stringify(leading))
        segments.push(`$s(${match[1]})`)
        lastIndex = match.index + match[0].length
      }
      if (lastIndex < data.length) {
        segments.push(JSON.stringify(data.slice(lastIndex)))
      }
      applyDirective(node, text, segments.join('+'), ctx)
    }

This is a classic place to do a regular match, then a series of operations to match, and finally return a text string. This code is pretty basic, but I won’t go into detail here because of time constraints

  • ApplyDirective function

    const applyDirective = ( el: Node, dir: Directive<any>, exp: string, ctx: Context, arg? : string, modifiers? : Record<string, true> ) => { const get = (e = exp) => evaluate(ctx.scope, e, el) const cleanup = dir({ el, get, effect: ctx.effect, ctx, exp, arg, modifiers }) if (cleanup) { ctx.cleanups.push(cleanup) } }
  • Then nodeType is 11, which means it’s a Fragment node, so start with its first child

    } else if (type === 11) {
      walkChildren(node as DocumentFragment, ctx)
    }
NodeType said Ming
This property reads only and returns a numeric value. Valid values correspond to the following types:  1-ELEMENT 2-ATTRIBUTE 3-TEXT 4-CDATA 5-ENTITY REFERENCE 6-ENTITY 7-PI (processing instruction) 8-COMMENT 9-DOCUMENT 10-DOCUMENT TYPE 11-DOCUMENT FRAGMENT 12-NOTATION

Comb summary

  • Pull the code
  • Start the project
  • Locate the entry createApp function
  • Define CTX and layers of inheritance
  • Find the block method
  • The nodes are processed separately depending on whether they are Element or Text
  • If it’s text, go through the regular match, get the data and return the string
  • If it is an Element, do a recursive process, parsing all the V-IF template syntax, and return the actual node

    All of the DOM node changes here are directly using JS to manipulate the DOM

Interesting source supplement

  • The nextTick implementation here is implemented directly through Promise. Then

    const p = Promise.resolve()
    
    export const nextTick = (fn: () => void) => p.then(fn)
    

    Write in the last

  • It’s a little late, write more than 1 o ‘clock imperceptibly, if feel write good, help me point wave again see/concern/like it
  • If you want to read future source analysis articles, you can follow minegitHub– Official account:The front-end peak