petite-vue源码剖析-从静态视图开始

代码库结构介绍

  • examples 各种使用示例
  • scripts 打包发布脚本
  • tests 测试用例
  • src
    • directives v-if等内置指令的实现
    • app.ts createApp函数
    • block.ts 块对象
    • context.ts 上下文对象
    • eval.ts 提供v-if="count === 1"等表达式运算功能
    • scheduler.ts 调度器
    • utils.ts 工具函数
    • walk.ts 模板解析

若想构建自己的版本只需在控制台执行npm run build即可。

深入理解静态视图的渲染过程

静态视图是指首次渲染后,不会因UI状态变化引发重新渲染。其中视图不包含任何UI状态,和根据UI状态首次渲染后状态不再更新两种情况,本篇将针对前者进行讲解。

示例:

首先进入的就是createApp方法,它的作用就是创建根上下文对象(root context)全局作用域对象(root scope)并返回mount,unmountdirective方法。然后通过mount方法寻找附带[v-scope]属性的孩子节点(排除匹配[v-scope] [v-scope]的子孙节点),并为它们创建根块对象
源码如下(基于这个例子,我对源码进行部分删减以便更容易阅读):

// 文件 ./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())
    }
  }
}

代码虽然很短,但引出了3个核心对象:上下文对象(context)作用域(scope)块对象(block)。他们三的关系是:

  • 上下文对象(context)作用域(scope) 是 1 对 1 关系;
  • 上下文对象(context)块对象(block) 是 多 对 多 关系,其中块对象(block)通过ctx指向当前上下文对象(context),并通过parentCtx指向父上下文对象(context)
  • 作用域(scope)块对象(block) 是 1 对 多 关系。

具体结论是:

  • 根上下文对象(context) 可被多个根块对象通过ctx引用;
  • 块对象(block)创建时会基于当前的上下文对象(context)创建新的上下文对象(context),并通过parentCtx指向原来的上下文对象(context)
  • 解析过程中v-scope就会基于当前作用域对象构建新的作用域对象,并复制当前上下文对象(context)组成一个新的上下文对象(context)用于子节点的解析和渲染,但不会影响当前块对象指向的上下文。

下面我们逐一理解。

作用域(scope)

这里的作用域和我们编写JavaScript时说的作用域是一致的,作用是限定函数和变量的可用范围,减少命名冲突。
具有如下特点:

  1. 作用域之间存在父子关系和兄弟关系,整体构成一颗作用域树;
  2. 子作用域的变量或属性可覆盖祖先作用域同名变量或属性的访问性;
  3. 若对仅祖先作用域存在的变量或属性赋值,将赋值给祖先作用域的变量或属性。
// 全局作用域
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

而且作用域是依附上下文存在的,所以作用域的创建和销毁自然而然都位于上下文的实现中(./src/context.ts)。
另外,petite-vue中的作用域并不是一个普通的JavaScript对象,而是一个经过@vue/reactivity处理的响应式对象,目的是一旦作用域成员被修改,则触发相关副作用函数执行,从而重新渲染界面。

块对象(block)

作用域(scope)是用于管理JavaScript的变量和函数可用范围,而块对象(block)则用于管理DOM对象。

// 文件 ./src/block.ts

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

  // 基于上述例子没有采用