Write at the beginning
- You Yuxi recently released a 5kb petite-vue. Curious, I cloned his source code and gave you an analysis.
- Recently, due to a lot of work, the pace of originality has been slowed down! Everyone understand
- If you want to see my handwritten source code + various source code analysis, you can follow my public
GitHub
see my 060ee453515e86. Basically, the front-end framework source code has been parsed.
Officially begin
petite-vue
is a vue with only 5kb, let’s find the warehouse first and clone ithttps://github.com/vuejs/petite-vue
- After cloning, I found that it was started with vite + petite-vue + multi-page form
Start command:
git clone https://github.com/vuejs/petite-vue cd /petite-vue npm i npm run dev
- Then open
http://localhost:3000/
to see the page:
Nanny teaching
The project has been started. Next, let's analyze the entry of the project first. Since the build tool used is
vite
, we can find it from the populationindex.html
<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 a demo project of multi-page mode+vue+vite, 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 from the source code to start the function
The startup function is
createApp
, 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
<script>
element that the currently running script belongs to. The script that calls this attribute cannot be a JavaScript module, and the module should use the import.meta object.s
code means to create a 060ee453516455 variable to record the currently running script element. If there is a specified attributeinit
, then call the createApp and mount methods.- But in this project, the exposed
createApp
method is actively called. Let’s take a look atcreateApp
method. There are 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 data, which can be of any type or not. This method is an entry function, and it depends on many functions, so we have to calm down. This function came in and made a lot of things
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 assigns many attributes and methods to it. Then provide it to the object returned by createApp to use
createContext
Create 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 }
- According to the parent object passed in, a simple inheritance is made, and then a new
ctx
object is returned.
I almost fell into a misunderstanding at the beginning. I wrote this article to let everyone understand the simplevue
, like the source code analysis of the Nuggets editor I wrote last time, the writing is too detailed and too tired. Simplify this time so that everyone can understand that these things are not important. ThecreateApp
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 example, the
directive
instruction will use the attributes and methods ofctx
So at the beginning, a lot of things were mounted toctx
in order to use the following methods 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 } } ... }
First, it will judge if the incoming string is string, then go back to find this node, otherwise it will find
document
el = el || document.documentElement
Define
roots
, a node arraylet 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
v-scope
, store el in the array and assign it toroots
. Otherwise, go to thisel
find allv-scope
attribute, and then filter out these nodes with thev-scope
attribute without thev-scope
attribute.roots
arrayroots
is still empty at this timeel
it.
Here is a warning in development mode:Mounting on documentElement - this is non-optimal as petite-vue
, which means that usingdocument
is not the best choice.After
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') )
- The
Block
constructor is the key point. After passing in the node and context, the outside just removes the'v-cloak' attribute, and the call to the mount function is over, so the principle is hidden inBlock
.
There is a question here. We currently only get theel
dom
, but there are template grammars in vue. How do those template grammars become real dom?
- Block turned out to be not a function, but a class.
Can be seen 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 divided into three logics
- Create the template
template
(using the clone node method, because thedom
node is an object after it is obtained, a layer of clone is made) - If it is not the root node, recursively inherit the
ctx
context - After processing ctx and Template, call
walk
function
- Create the template
walk
function analysis:
- Will judge according to nodetype first, and then do different processing
- If it is a
element
node, different instructions must be processed, such asv-if
Here is 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 means to check whether the node contains
v-xx
, then return the result and delete the attribute Take
v-if
an example. When it is judged that this node has thev-if
attribute, then call the method to process it, and delete this attribute (it has been processed as an identifier)I wanted to go to bed before 12 o’clock. Others told me that it’s only 5kb. I wanted to find the simplest instruction to parse. As a result, each instruction code has more than 100 lines. I worked overtime until 9 o’clock tonight. After transforming the micro front end into production, I still want to persevere and write it for everyone. It's early morning now
v-if
processing function is about 60 linesexport const _if = (el: Element, exp: string, ctx: Context) => { ... }
First, the _if function first gets the value of the v-if of the el node and exp, and the ctx context object
if (import.meta.env.DEV && !exp.trim()) { console.warn(`v-if expression cannot be empty.`) }
- If it is empty, report a warning
Then get the parent node of the el node, and create a comment node (temporary storage) based on the value of exp and insert it before el, and create a branches array 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
The Comment interface represents textual notations between markups. Although it is usually not displayed, you can see them when viewing the source code. In HTML and XML, comments are the content between
'<!--' and'-->'. In XML, the character sequence'--' cannot appear in comments.
Then create
elseEl
andelseExp
, and loop through the collection of all the else branches, and store them in the brancheswhile ((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 } }
In this way, there are all branches of v-if in Branches, which can be regarded as a tree traversal (breadth first search)
Next, according to the triggering of the side-effect function, each time to traverse the branches to find the branch that needs to be activated, insert the node into the parentNode, and return to the nextNode to achieve the effect of
v-if
Since it is all html here, we have omitted the virtual dom, but the above only deals with a single node. If it is a deep-level
dom
node, the depth-first search will be used later.// 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 are no
v-if
the node, then take their first child node to do the above actions, matching each instruction such asv-if v-for
If it is 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 place is very classic, it is through regular matching, and then a series of operations to match, and finally a text string is returned. This code is very essential, but due to time constraints, I won’t go into details here.
applyDirective
functionconst 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) } }
Next, if the
nodeType is 11, it means it is a Fragment node, so start directly from its first child node
} else if (type === 11) { walkChildren(node as DocumentFragment, ctx) }
nodeType description
此属性只读且传回一个数值。
有效的数值符合以下的型别:
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
Combing summary
- Pull code
- Startup project
- Find the entry createApp function
- Define ctx and inheritance
- Discover the block method
- Separate processing according to whether the node is element or text
- If it is text, go through regular matching, get the data and return the string
If it is an element, do a recursive process, parse all
v-if
, and return the real nodeAll dom node changes here are directly operated dom through js
Interesting source code supplement
The nextTick implementation here is directly through
promise.then
const p = Promise.resolve() export const nextTick = (fn: () => void) => p.then(fn)
Write at the end
- It’s a bit late, I don’t know it until 1 o’clock, if I feel that I’ve written well, please help me to read/follow/like it
- If you want to read previous source code analysis articles, you can follow my
gitHub
-public number:front-end peak
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。