头图

Introduction to the structure of the code base

  • examples Various usage examples
  • scripts package release scripts
  • tests test case
  • src

    • Directives v-if and other built-in directives
    • app.ts createApp function
    • block.ts block object
    • context.ts context object
    • eval.ts provides v-if="count === 1" and other expression operation functions
    • scheduler.ts scheduler
    • utils.ts utility functions
    • walk.ts template parsing

If you want to build your own version, just execute it on the console npm run build .

In-depth understanding of the rendering process of static views

A static view means that after the first rendering, there will be no re-rendering due to UI state changes. The view does not contain any UI state , and the state is no longer updated after the first rendering according to the UI state . This article will explain the former.

Example:

 <div v-scope="App"></div>

<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module'

  createApp({
    App: {
      $template: `
      <span> OFFLINE </span>
      <span> UNKOWN </span>
      <span> ONLINE </span>
      `
    }
  }).mount('[v-scope]')
</script>

The first thing to enter is the createApp method, its role is to create a root context object (root context) , a global scope object (root scope) and return mount , unmount directive method. Then use the mount method to find child nodes with the [v-scope] attribute (excluding children that match [v-scope] [v-scope] ), and create a root block object for them.
The source code is as follows (based on this example, I have partially cut the source code to make it easier to read):

 // 文件 ./src/app.ts

export const createApp = (initialData: any) => {
  // 创建根上下文对象
  const ctx = createContext()
  // 全局作用域对象,作用域对象其实就是一个响应式对象
  ctx.scope = reactive(initialData)
  /* 将scope的函数成员的this均绑定为scope。
   * 若采用箭头函数赋值给函数成员,则上述操作对该函数成员无效。
   */
  bindContextMethods(ctx.scope)
  
  /* 根块对象集合
   * petite-vue支持多个根块对象,但这里我们可以简化为仅支持一个根块对象。
   */
  let rootBlocks: Block[]

  return {
    // 简化为必定挂载到某个带`[v-scope]`的元素下
    mount(el: Element) {
      let roots = el.hasAttribute('v-scope') ? [el] : []
      // 创建根块对象
      rootBlocks = roots.map(el => new Block(el, ctx, true))
      return this
    },
    unmount() {
      // 当节点卸载时(removeChild)执行块对象的清理工作。注意:刷新界面时不会触发该操作。
      rootBlocks.forEach(block => block.teardown())
    }
  }
}

Although the code is very short, it leads to 3 core objects: the context object (context) , the scope (scope) and the block object (block) . Their relationship is as follows:

  • The context object (context) and the scope (scope) are 1 to 1 relationship;
  • The context object (context) and the block object (block) are in a many-to-many relationship, in which the block object (block) points to the current context object (context) through ctx parentCtx , and points to the parent through ---7b1127d871451af31dd952433b27fc4e--- context object (context) ;
  • The scope (scope) and the block object (block) are 1-to-many relationship.

The specific conclusions are:

  • The root context object (context) can be referenced by multiple root block objects through ctx ;
  • When the block object (block) is created, a new context object (context) will be created based on the current context object (context) , and point to the original context object (context) through parentCtx ;
  • During the parsing process v-scope will build a new scope object based on the current scope object, and copy the current context object (context) to form a new context object (context) for parsing and rendering of child nodes, But does not affect the context pointed to by the current block object.

Below we understand one by one.

scope

The scope here is the same as the scope we said when writing JavaScript. The function is to limit the available scope of functions and variables and reduce naming conflicts.
Has the following characteristics:

  1. There is a parent-child relationship and a sibling relationship between the scopes, forming a scope tree as a whole;
  2. A variable or property in a child scope can override the accessibility of a variable or property with the same name in the ancestor scope;
  3. If you assign a value to a variable or property that exists only in the ancestor scope, it will be assigned to the variable or property in the ancestor scope.
 // 全局作用域
var globalVariable = 'hello'
var message1 = 'there'
var message2 = 'bye'

(() => {
  // 局部作用域A
  let message1 = '局部作用域A'
  message2 = 'see you'
  console.log(globalVariable, message1, message2)
})()
// 回显:hello 局部作用域A see you

(() => {
  // 局部作用域B
  console.log(globalVariable, message1, message2)
})()
// 回显:hello there see you

And the scope is attached to the context, so the creation and destruction of the scope are naturally located in the context implementation ( ./src/context.ts ).
In addition, the scope in petite-vue is not an ordinary JavaScript object, but a responsive object that has been processed by @vue/reactivity . The purpose is to trigger the execution of related side effects functions once the scope members are modified. thereby re-rendering the interface.

block object

The scope (scope) is used to manage the available scope of JavaScript variables and functions, while the block object (block) is used to manage DOM objects.

 // 文件 ./src/block.ts

// 基于示例,我对代码进行了删减
export class Block {
  template: Element | DocumentFragment // 不是指向$template,而是当前解析的模板元素
  ctx: Context // 有块对象创建的上下文对象
  parentCtx?: Context // 当前块对象所属的上下文对象,根块对象没有归属的上下文对象

  // 基于上述例子没有采用<template>元素,并且静态视图不包含任何UI状态,因此我对代码进行了简化
  construct(template: Element, parentCtx: Context, isRoot = false) {
    if (isRoot) {
      // 对于根块对象直接以挂载点元素作为模板元素
      this.template = template
    }
    if (isRoot) {
      this.ctx = parentCtx
    }

    // 采用深度优先策略解析元素(解析过程会向异步任务队列压入渲染任务)
    walk(this.template, this.ctx)
  }
}
 // 文件 ./src/walk.ts

// 基于上述例子为静态视图不包含任何UI状态,因此我对代码进行了简化
export const walk = (node: Node, ctx: Context): ChildNode | null | void => {
  const type= node.nodeType
  if (type === 1) {
    // node为Element类型
    const el = node as Element

    let exp: string | null
    if ((exp = checkAttr(el, 'v-scope')) || exp === '') {
      // 元素带`v-scope`则计算出最新的作用对象。若`v-scope`的值为空,则最新的作用域对象为空对象
      const scope = exp ? evaluate(ctx.scope, exp) : {}
      // 更新当前上下文的作用域
      ctx = createScopedContext(ctx, scope)
      // 若当前作用域存在`$template`渲染到DOM树上作为在线模板,后续会递归解析处理
      // 注意:这里不会读取父作用域的`$template`属性,必须是当前作用域的
      if (scope.$template) {
        resolveTemplate(el, scope.$template)
      }
    }

    walkChildren(el, ctx)
  }
}

// 首先解析第一个孩子节点,若没有孩子则解析兄弟节点
const walkChildren = (node: Element | DocumentFragment, ctx: Context) => {
  let child = node.firstChild
  while (child) {
    child = walk(child, ctx) || child.nextSibling
  }
}

// 基于上述例子我对代码进行了简化
const resolveTemplate = (el: Element, template: string) => {
  // 得益于Vue采用的模板完全符合HTML规范,所以这么直接简单地渲染为HTML元素后,`@click`和`:value`等属性名称依然不会丢失
  el.innerHTML = template
}

In order to make it easier to read, I have simplified the code of the expression operation (remove the hint and cache mechanism in the development stage)

 // 文件 ./src/eval.ts

export const evaluate = (scope: any, exp: string, el? Node) => execute(scope, exp, el)

const execute = (scope: any, exp: string, el? Node) => {
  const fn = toFunction(exp)
  return fn(scope, el)
}

const toFunction = (exp: string): Function => {
  try {
    return new Function('$data', '$el', `with($data){return(${exp})}`)
  }
  catch(e) {
    return () => {}
  }
}

context object

Above we learned that the scope (scope) is used to manage the available scope of JavaScript variables and functions, while the block object (block) is used to manage the DOM object, then the context object (context) is the connection scope (scope) and block The carrier of the object (block) is also the connection point to form a tree structure of multiple block objects ( [根块对象.ctx] -> [根上下文对象, 根上下文对象.blocks] -> [子块对象] -> [子上下文对象] ).

 // 文件 ./src/context.ts

export interface Context {
  scope: Record<string, any> // 当前上下文对应的作用域对象
  cleanups: (()=>void)[] // 当前上下文指令的清理函数
  blocks: Block[] // 归属于当前上下文的块对象
  effect: typeof rawEffect // 类似于@vue/reactivity的effect方法,但可根据条件选择调度方式
  effects: ReativeEffectRunner[] // 当前上下文持有副作用方法,用于上下文销毁时回收副作用方法释放资源
}

/**
 * 由Block构造函数调用创建新上下文对象,特性如下:
 * 1. 新上下文对象作用域与父上下文对象一致
 * 2. 新上下文对象拥有全新的effects、blocks和cleanups成员
 * 结论:由Block构造函数发起的上下文对象创建,不影响作用域对象,但该上下文对象会独立管理旗下的副作用方法、块对象和指令
 */
export const createContext = (parent? Context): Context => {
  const ctx: Context = {
    ...parent,
    scope: parent ? parent.scope : reactive({}), // 指向父上下文作用域对象
    effects: [],
    blocks: [],
    cleanups: [],
    effect: fn => {
      // 当解析遇到`v-once`属性,`inOnce`即被设置为`true`,而副作用函数`fn`即直接压入异步任务队列执行一次,即使其依赖的状态发生变化副作用函数也不会被触发。
      if (inOnce) {
        queueJob(fn)
        return fn as any
      }
      // 生成状态发生变化时自动触发的副作用函数
      const e: ReactiveEffectRunner = rawEffect(fn, {
        scheduler: () => queueJob(e)
      })
      ctx.effects.push(e)
      return e
    }
  }
  return ctx
}

/**
 * 当解析时遇到`v-scope`属性并存在有效值时,便会调用该方法基于当前作用域创建新的作用域对象,并复制当前上下文属性构建新的上下文对象用于子节点的解析和渲染。
 */
export const createScopedContext = (ctx: Context, data = {}): Context => {
  const parentScope = ctx.scope
  /* 构造作用域对象原型链 
   * 此时若当设置的属性不存在于当前作用域,则会在当前作用域创建该属性并赋值。
   */
  cosnt mergeScope = Object.create(parentScope)
  Object.defineProperties(mergeScope, Object.getOwnPropertyDescriptors(data))
  // 构造ref对象原型链
  mergeScope.$ref = Object.create(parentScope.$refs)
  // 构造作用域链
  const reactiveProxy = reactive(
    new Proxy(mergeScope, {
      set(target, key, val, receiver) {
        // 若当设置的属性不存在于当前作用域则将值设置到父作用域上,由于父作用域以同样方式创建,因此递归找到拥有该属性的祖先作用域并赋值
        if (receiver === reactiveProxy && !target.hasOwnProperty(key)) {
          return Reflect.set(parentScope, key, val)
        }
        return Reflect.set(target, key, val, receiver)
      }
    })
  )

  /* 将scope的函数成员的this均绑定为scope。
   * 若采用箭头函数赋值给函数成员,则上述操作对该函数成员无效。
   */
  bindContextMethods(reactiveProxy)
  return {
    ...ctx,
    scope: reactiveProxy
  }
}

Human flesh single-step debugging

  1. Call createApp generate the global scope according to the input parameters rootScope , and create the root context rootCtx ;
  2. Call mount to <div v-scope="App"></div> build a root block object rootBlock and use it as a template to perform parsing processing;
  3. v-scope attribute is recognized during parsing, and the global scope rootScope is used as the basis to obtain the local scope scope , and the root context rootCtx Build a new context with the blueprint ctx for parsing and rendering of child nodes;
  4. Get $template attribute value and generate HTML element;
  5. Depth-first traversal resolves child nodes.

to be continued

Through a simple example, we have a certain degree of understanding of the parsing, scheduling and rendering process of petite-vue. In the next article, we will look again through the static view v-if and v-for Yes How to change the DOM tree structure according to the state.
In addition, some friends may have the following questions

  1. What is the receiver of Proxy?
  2. Difference between new Function and eval ?

These follow-up will be introduced in a dedicated article, so stay tuned :)

"Anatomy of Petite-Vue Source Code" booklet

"Petite-Vue Source Code Analysis" combines examples to interpret the source code line by line from online rendering, responsive system and sandbox model, and also makes a detailed analysis of the SMI optimization dependency cleaning algorithm using the JS engine in the responsive system. It is definitely an excellent stepping stone before getting started with Vue3 source code. If you like it, remember to forward it and appreciate it!


肥仔John
2.8k 声望1.8k 粉丝

《Petite-Vue源码剖析》作者