Follow me on my blog shymean.com

Recently, Vue has added a petite-Vue repository. It’s a mini version of Vue without virtual DOM. It was originally called Vue-Lite, and it’s mainly used to “sprinkle” some Vue-like interactions with server-side RENDERED HTML pages. Quite interesting, so I looked at the source code (V0.2.3), sorted out this article.

start

Development and debugging environment

The development environment for the entire project is very simple

git clone [email protected]:vuejs/petite-vue.git

yarn 

Use vite to boot
npm run dev

# visit http://localhost:3000/
Copy the code

I have to say, using Vite for the development environment is pretty cool

Create a new test file, exmaples/demo.html, and write some code

<script type="module">
  import { createApp, reactive } from '.. /src'

  createApp({
    msg: "hello"
  }).mount("#app")
</script>

<div id="app">
    <h1>{{msg}}</h1>
</div>
Copy the code

Then visit http://localhost:3000/demo.html

The directory structure

From the Readme, you can see some differences between the project and the standard VUE

  • It is Only 5.8 KB and has a small size
  • Vue-compatible template syntax, vUe-compatible template syntax
  • Dom-based, mutates in place, DOM driven, in place conversion
  • Driven by @vue/reactivity, used@vue/reactivitydrive

The directory structure is also simple, written in TS, and the external dependencies are basically @vue/reactivity.

The core to realize

createContext

As you can see from the demo code above, the entire project starts with createApp.

export const createApp = (initialData? :any) = > {
  // root context
  const ctx = createContext()
  if (initialData) {
    ctx.scope = reactive(initialData) // Proxy the initialization data into responsiveness
  }
  // Some interfaces for the app
  return {
    directive(name: string, def? : Directive) {},
    mount(el? :string | Element | null) {},
    unmount(){}}}Copy the code

Check reactive in Vue3. Check reactive in Vue3.

CreateApp creates the root context using createContext. This context is now familiar. Look at createContext

export constcreateContext = (parent? : Context):Context= > {
  constctx: Context = { ... parent,scope: parent ? parent.scope : reactive({}),
    dirs: parent ? parent.dirs : {}, // Support directives
    effects: [].blocks: [].cleanups: [].// Provides an interface to register effect callbacks, mainly using the scheduler to control when they are called
    effect: (fn) = > {
      if (inOnce) {
        queueJob(fn)
        return fn as any
      }
      // @vue/ effect method in reactivity
      const e: ReactiveEffect = rawEffect(fn, {
        scheduler: () = > queueJob(e)
      })
      ctx.effects.push(e)
      return e
    }
  }
  return ctx
}
Copy the code

A quick look at queueJob shows that the nextTick implementation is familiar from Vue,

  • The callback is saved through a global variable queue
  • In the next microtask phase, execute each callback in the queue in turn, and then clear the queue

mount

The basic use

createApp().mount("#app")
Copy the code

The main purpose of the mount method is to process the EL parameters, find the root DOM node to which the application is mounted, and then perform the initialization process

mount(el? :string | Element | null) {
    let roots: Element[]
    / /... Initialize roots according to EL parameters
    // Create a Block instance based on el
    rootBlocks = roots.map((el) = > new Block(el, ctx, true))
    return this
}
Copy the code

A Block is an abstract concept used to unify DOM node rendering, insertion, removal, and destruction.

Here are the dependencies on this Block, which you can see are mainly used for initialization, if, and for

Take a look at the implementation of Block

// src/block.ts
export class Block {
  template: Element | DocumentFragment
  ctx: Context key? :anyparentCtx? : ContextisFragment: booleanstart? : Text end? : Textget el() {
    return this.start || (this.template as Element)
  }

  constructor(template: Element, parentCtx: Context, isRoot = false) {
    // Initialize this.template
    // Initialize this.ctx
    
    // Build the application
    walk(this.template, this.ctx)
  }
  // It is mainly used when adding or removing something
  insert(parent: Element, anchor: Node | null = null) {}
  remove() {}
  teardown(){}}Copy the code

The walk method, which is used for recursive nodes and children, should be familiar if you’ve seen recursive diff before. However, there is no virtual DOM in Petite-Vue, so the DOM is directly updated in walk.

export const walk = (node: Node, ctx: Context): ChildNode | null | void= > {
  const type = node.nodeType
  if (type= = =1) {
    // Element node
    const el = node as Element
    / /... Handle such as V-if and V-for
    / /... The inspection property executes the corresponding directive processing applyDirective, such as V-scoped, ref, and so on

    // Handle the child node first, then handle the node's own attributes
    walkChildren(el, ctx)

    // Handle customization related to node attributes, including built-in directives and custom directives
  } else if (type= = =3) {
    // Text node
    const data = (node as Text).data
    if (data.includes('{{')) {
      // Match the text to be replaced, then applyDirective(text)
      applyDirective(node, text, segments.join('+'), ctx)
    }
  } else if (type= = =11) {
    walkChildren(node as DocumentFragment, ctx)
  }
}

const walkChildren = (node: Element | DocumentFragment, ctx: Context) = > {
  let child = node.firstChild
  while (child) {
    child = walk(child, ctx) || child.nextSibling
  }
}
Copy the code

As you can see, node.nodeType is treated differently

  • For element nodes, some instructions on the node are processed and then passedwalkChildrenProcess child nodes.
    • V-if determines whether a Block needs to be created and then inserted or removed based on the expression
    • V-for, the loop builds the Block and then performs the insert
  • For text nodes, replace{{}}Expression, and then replace the text content

v-if

The implementation of if stores all branch judgments using branches. ActiveBranchIndex stores the branch index values currently located via closures.

During initialization or update, if a branch expression resolves correctly and does not match the last activeBranchIndex, a new Block is created and the walk in the Block constructor is followed.

export const _if = (el: Element, exp: string, ctx: Context) = > {
  const parent = el.parentElement!
  const anchor = new Comment('v-if')
  parent.insertBefore(anchor, el)

  // Store various branches of conditional judgment
  const branches: Branch[] = [{ exp,el }]

  / / positioning the if... else if ... Else, etc., in an array of branches

  let block: Block | undefined
  let activeBranchIndex: number = -1 // Use closures to store the index value of the currently located branch

  const removeActiveBlock = () = > {
    if (block) {
      parent.insertBefore(anchor, block.el)
      block.remove()
      block = undefined}}// Collect dependencies
  ctx.effect(() = > {
    for (let i = 0; i < branches.length; i++) {
      const { exp, el } = branches[i]
      if(! exp || evaluate(ctx.scope, exp)) {// A new block is generated when a branch switch is judged
        if(i ! == activeBranchIndex) { removeActiveBlock() block =new Block(el, ctx)
          block.insert(parent, anchor)
          parent.removeChild(anchor)
          activeBranchIndex = i
        }
        return}}// no matched branch.
    activeBranchIndex = -1
    removeActiveBlock()
  })

  return nextNode
}
Copy the code

v-for

The main function of the for directive is to create multiple nodes in a loop, and it also implements a function similar to the Diff algorithm to reuse blocks based on keys

export const _for = (el: Element, exp: string, ctx: Context) = > {
  / /... Some tool methods include Createchildcontext and mountBlock

  ctx.effect(() = > {
    const source = evaluate(ctx.scope, sourceExp)
    const prevKeyToIndexMap = keyToIndexMap
    // Create a context with multiple child nodes based on the loop item; [childCtxs, keyToIndexMap] = createChildContexts(source)if(! mounted) {// First render, create a new Block and then insert
      blocks = childCtxs.map((s) = > mountBlock(s, anchor))
      mounted = true
    } else {
      / / update
      const nextBlocks: Block[] = []
      // Remove blocks that do not exist
      for (let i = 0; i < blocks.length; i++) {
        if(! keyToIndexMap.has(blocks[i].key)) { blocks[i].remove() } }// process by key
      let i = childCtxs.length
      while (i--) {
        const childCtx = childCtxs[i]
        const oldIndex = prevKeyToIndexMap.get(childCtx.key)
        const next = childCtxs[i + 1]
        const nextBlockOldIndex = next && prevKeyToIndexMap.get(next.key)
        const nextBlock =
          nextBlockOldIndex == null ? undefined : blocks[nextBlockOldIndex]
        // There is no old block, create it directly
        if (oldIndex == null) {
          // new
          nextBlocks[i] = mountBlock(
            childCtx,
            nextBlock ? nextBlock.el : anchor
          )
        } else {
          // There are old blocks, reuse, check whether need to move position
          const block = (nextBlocks[i] = blocks[oldIndex])
          Object.assign(block.ctx.scope, childCtx.scope)
          if(oldIndex ! == i) {if (blocks[oldIndex + 1] !== nextBlock) {
              block.insert(parent, nextBlock ? nextBlock.el : anchor)
            }
          }
        }
      }
      blocks = nextBlocks
    }
  })

  return nextNode
}
Copy the code

A processing instruction

All directives are handled by applyDirective and processDirective, the latter is based on a secondary wrapper of the former and mainly deals with some builtInDirectives shortcuts,

export const builtInDirectives: Record<string, Directive<any>> = {
  bind,
  on,
  show,
  text,
  html,
  model,
  effect
}
Copy the code

Each instruction is based on CTX and EL to quickly achieve some logic, specific implementation can refer to the corresponding source code.

When calling app.directive to register a custom directive,

directive(name: string, def? : Directive) {
    if (def) {
        ctx.dirs[name] = def
        return this
    } else {
        return ctx.dirs[name]
    }
},
Copy the code

You are actually adding a property to contenx’s DIRs, and when applyDirective is called, you get the corresponding handler

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)
  // Execute the instruction method
  const cleanup = dir({
    el,
    get,
    effect: ctx.effect,
    ctx,
    exp,
    arg,
    modifiers
  })
  // Collect those side effects that need to be removed during uninstallation
  if (cleanup) {
    ctx.cleanups.push(cleanup)
  }
}
Copy the code

Therefore, you can use these parameters passed in above to build custom directives

app.directive("auto-focus".({el}) = >{
    el.focus()
})
Copy the code

summary

The whole code looks very lean indeed

  • Without the virtual DOM, there is no need to build the render function through the Template, directly recursively traversing the DOM node, and processing various instructions through the re
  • With the help of@vue/reactivity, the whole responsive system implementation is very natural, except in the use of parsing instructions throughctx.effect()Collecting dependencies basically eliminates the need to care about the logic of data changes

The main role of petite-Vue is to “sprinkling” some VUe-like interactions in the HTML pages rendered on the server.

For most of the server-side RENDERING HTML projects I’ve worked on so far, if you want to implement some DOM interaction, it’s generally used

  • JQuery operates DOM and YYDS
  • Of course a Vue can be written using script + template, but it’s a bit of an overkill to access a Vue for a div interaction
  • Others such as the React framework are equivalent

Petite-vue uses the same template syntax and responsive functionality as Vue and should be a great development experience. It does not need to consider the cross-platform functions of the virtual DOM, and directly uses browser-related APIS to operate the DOM in the source code, reducing the cost of the framework Runtime runtime, and should be good in terms of performance.

In summary, it seems that petite-Vue combines the development experience of the vue standard version with a very small code volume, good development experience, and decent performance, and may be used to manipulate the DOM in a more modern way than jQuery.

This project is the first version submitted on June 30th. At present, relevant functions and interfaces are not particularly stable, and there may be adjustments. But for the example in the Exmples directory, it should suffice for a few simple requirements scenarios, which you might try to use in smaller historical projects.