Written in the beginning

  • Recently, Rain Creek released 5KB petite-Vue. Curious me, I cloned its source code to give you a wave of parsing.
  • Recently, as a result of many work things, so slowed down the original pace! Your understanding
  • Want to see my past handwritten source code + all kinds of source code analysis can pay attention to my public number to see myGitHub, the basic front-end framework source code has been analyzed

The official start of the

  • petite-vueVue is only 5KB, we first find the warehouse, clone
https://github.com/vuejs/petite-vue
Copy the code
  • When cloned, it was found to be vite + petite-Vue + multi-page startup

  • Start command:

git clone https://github.com/vuejs/petite-vue
cd /petite-vue
npm i 
npm run dev

Copy the code
  • Then open thehttp://localhost:3000/You can see the page:


Nanny teaching

  • The project has been started, so let’s first parse the project entry, since the build tool used isviteFrom the root directoryindex.htmlPopulation finding:
<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>
Copy the code
  • This is a demo project for multi-page mode + Vue +vite, and we found a simple demo pagecommits:
<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 {word-break: break-all; margin-bottom: 20px; } .author, .date { font-weight: bold; } </style>Copy the code
  • You can see the introduction at the top of the page
import { createApp, reactive } from '.. /src'Copy the code

Start with the source launcher

  • The starting function iscreateApp, 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()
}

Copy the code

The Document.currentScript property returns the

  • Create an S variable to record the currently running script element, and call the createApp and mount methods if init exists.

  • CreateApp (createApp) : createApp (createApp) : createApp (createApp) : createApp (createApp

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){ ... }Copy the code
  • The createApp method receives an initial data, which may or may not be of any type. This method is the entry function, depends on the function is also more, we want 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[] }Copy the code
  • This code creates a CTX context object and assigns it a number of properties and methods. It is then provided to the object returned by createApp
  • createContextCreate 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 }Copy the code
  • Based on the parent object passed in, do a simple inheritance and return a new onectxObject.

I almost fell into the mistake at the beginning, I write this article, is to let you understand the simple VUE principle, like the last time I wrote the nuggets editor source code parsing, write too fine, too tired. I’m going to simplify this a little bit, just to make sure everyone understands that this stuff up here doesn’t matter. The createApp 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(){}... }Copy the code
  • An object has three methods, such as a directive that uses CTX properties and methods. So the first thing I did was mount a bunch of stuff on the CTX for the following method

  • Focus on the 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 } } ... }Copy the code
  • The first thing it decides is if it’s a string, it goes back to the node, otherwise it goes back to the nodedocument
el = el || document.documentElement
Copy the code
  • defineroots, 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] }Copy the code
  • If you havev-scopeThis property, I’m going to store el in an array and assign it torootsOr go to this oneelSo let’s find all the bandsv-scopeProperty of the node, and then filter out those bandsv-scopeProperty does not containv-scopeProperty, inserted into the noderootsAn array of

If roots are still empty, put el in. There is a warning in development mode: Mounting on documentElement – This is non-optimal as petite-vue, meaning document is not the best choice.

  • In therootsWhen you’re done, start moving.
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') )Copy the code
  • thisBlockOnce the node and context are passed in, the ‘V-cloak’ property is just removed and the mount function is calledBlockThe inside.

Here’s a question: We only have the DOM node EL so far, but the VUE is full of template syntax. How does the template syntax translate into a real DOM?

  • A Block is not a function, it’s a class.

  • You can see this in the constructor function
  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)
  }
Copy the code
  • The above code can be broken down into three pieces of logic
    • Create a templatetemplate(Using the Clone node mode, due todomAfter the node is acquired, it is an object, so a clone layer is made.)
    • If it’s not the root, it inherits the recursionctxcontext
    • Call after CTX and Template are processedwalkfunction
  • walkFunction analysis:

  • Nodetype is first evaluated and then processed differently

  • If it is an Element node, different instructions are handled, such as V-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 }Copy the code
  • This function checks if the node contains an attribute of V-xx, returns the result and deletes the attribute

  • In the case of v-if, when the node is judged to have a V-if attribute, a method is called to process it and the attribute is removed (as an identifier).

Here originally I want to sleep before 12 o ‘clock, others told me only 5KB, I want to find the simplest command parsing, the result of each command code has more than 100 lines, tonight I worked overtime to more than nine o ‘clock, just transformed the micro front end of the production, or want to insist on writing it for everyone. It’s early in the morning

  • v-ifThe handler is about 60 lines
export const _if = (el: Element, exp: string, ctx: Context) => {
...
}
Copy the code
  • First the _if function takes the el node and the v-if value exp, as well as the CTX context object
if (import.meta.env.DEV && ! exp.trim()) { console.warn(`v-if expression cannot be empty.`) }Copy the code
  • Issue a warning if it is empty
  • Create a comment node based on the value of exp and insert it before EL. At the same time create an array of branches to store exp and 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
Copy the code

The Comment interface represents textual notations between the markup. Although it is not usually displayed, they are visible when viewing the source code. In HTML and XML, Comments are ‘
‘. In XML, the character sequence ‘–‘ cannot appear in comments.

  • Then create aelseElandelseExpVariable, and loop through to collect all else branches, and stored 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
    }
  }
Copy the code

Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-if Branches > V-IF Branches > V-IF Branches

  • Then, according to the triggering of side effects function, go over branches each time to find the branch that needs to be activated, insert the node into parentNode, and return nextNodev-ifThe effect of

This is HTML, so we don’t need to use the virtual DOM, but it only deals with a single node. If it is a deep dom node, we need to use the depth-first search

 // 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
  }
}

Copy the code
  • When there is no nodev-if“, this time to get their first child node to do the above action, match eachv-if v-forOr something 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)
    }
Copy the code

This is a classic example of a regular match, followed by a series of actions that return a text string. This code is pretty good, but I won’t go into detail here for the sake of time

  • applyDirectivefunction
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) } }Copy the code
  • The followingNodeType is 11Fragment means a Fragment node, so start with its first child node
} else if (type === 11) {
    walkChildren(node as DocumentFragment, ctx)
  }
Copy the code
NodeType said Ming
This property is read-only and returns a value. Valid values conform 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-NOTATIONCopy the code

Comb summary

  • Pull the code
  • Start the project
  • Find the entry createApp function
  • Define CTX and layer inheritance
  • Discover block methods
  • Do this separately depending on whether the node is element or text
  • If it’s text, go through the re match, get the data and return the string
  • If it’s element, do a recursion, parse all of themv-ifSuch as template syntax, return the real node

All dom node changes here are made by manipulating the DOM directly through JS

Interesting source code supplement

  • The nextTick implementation here is passed directlypromise.then
const p = Promise.resolve()

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

Copy the code

Write in the last

  • It is a little late, I write more than 1:00 unconsciously, if you feel good, please help me click the wave and then read/follow/like it
  • If you want to read the previous source code analysis article you can follow minegitHub– Official Number:The front-end peak