跳转到内容

源码分析

刀刀

3/20/2025

0 字

0 分钟

主要模块

compiler

编译的过程,将代码做编译处理。

  • core:编辑器核心,做初级编译(无关任何平台)
  • dom:针对浏览器端的编译
  • sfc:针对单文件编译
  • ssr:针对服务端的优化

runtime

模块运行时做什么处理,实现什么功能。

  • core:无关平台的运行核心
  • dom:浏览器端运行
  • test:测试运行

reactivity

包含响应式系统,如reactive、ref、computed等。

size-check

查看代码体积。

响应式模块reactivity

reactive实现

响应式核心:proxy

js
// target:进行代理时代理的目标对象;handler:操作时代理对象的行为(get用来获取拦截的对象,读取其属性,set设置属性的捕获器,修改对象时会触发)
const p = new Proxy(target, handler)

原理

  1. 通过 proxy 拦截数据去代理目标对象
  2. 定义存储器 getter 接收值、 setter 设置值
  3. 通过 reflect 在对象属性上设置属性进行反射,get 获取数据、set 设置数据

初步实现

定义一个响应式函数 reactive ,形参 target 获取对象,通过 proxy 代理,get 获取值,set 设置值。

js
const reactive = target => {
  return new Proxy(target, {
    // get接收值
    get(target, key, receiver) {
    },
    // set设置值
    set(target, key, receiver) {
    }
  })
}

通过 reflectgetset 属性反射,返回。

js
const reactive = target => {
  return new Proxy(target, {
    // get接收值
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)

      console.log('get', res);
      return res
    },
    // set设置值
    set(target, key, receiver) {
      const res = Reflect.set(target, key, receiver)
      console.log('set', res);

      return res
    }
  })
}

通过传参并获取属性来触发 get 方法;通过修改属性来触发 set 方法。

js
const obj = reactive({a:10,b:20})

obj.a = 120
console.log(obj.a);

利用 node 运行查看打印。

i5vrFq.png

effect实现

原理

借助了三个函数实现。

  • effect
  • tarck:负责收集数据时对数据整理(收集阶段处理依赖)
  • trigger:负责数据更新时对数据整理(更新阶段处理依赖)

初步实现

(使用了 reactive 代码连用)

声明定义上方那三个函数,用于依赖的收集处理。

effect 是全阶段处理;tarck 是在收集阶段处理,因此在 get 方法内调用;trigger 是在更新阶段处理,因此在 set 方法内调用。

js
const reactive = target => {
  return new Proxy(target, {
    // get接收值
    get(target, key, receiver) {
      // ...

      tarck(target, key)
      return res
    },
    // set设置值
    set(target, key, receiver) {
      // ....

      trigger(target, key)
      return res
    }
  })
}

// tarck与trigger服务于effect,因此要被调用出来执行处理
const effect = effectFn => {
  effectFn()
}

const tarck = (target, key) => {}
const trigger = (target, key) => {}

tarck 收集依赖存储到本地或者某个变量中(最好存一个一一映射的关系)。vue3源码使用的是 weakMap ,为了方便这里直接使用 map 即可。

接收 target 目标对象和 key 键,然后做以下操作:

  • 定义一个 map 变量 targetMap
  • 判断当前执行的 effect 函数是否存在,不存在不做任何处理,存在则继续往下操作
  • 通过 targetMap.get(target) 方法获取 target ,判断该目标是否存在,不存在则定义为 map 对象 deepMap,通过 targetMap.set() 方法把 target 赋值给该 deepMap 变量,存在则继续往下操作
  • 通过 deepMap.get(key) 获取到映射的值,判断是否存在,不存在则 Set 设置,通过 set 方法赋值
  • 最后用 add 方法添加数据
js
// 1.建立映射
const targetMap = new Map()
let activeEffect; // 判断是否存在的全局变量

const tarck = (target, key) => {
  // 2.判断是否存在,如果不存在,直接结束函数
  if(!activeEffect) return

  // 3.
  let deepMap = targetMap.get(target)
  if(!deepMap) {
    deepMap = new Map()
    targetMap.set(target, deepMap)
  }

  // 4.
  let deps = deepMap.get(key)
  if(!deps) {
    deps = new Set()
    deepMap.set(key, deps)
  }

  // 5.
  deps.add(activeEffect)
}

trigger

  • 获取定义的 targetMap.get() 的键值,如果没有,不执行后续操作
  • depsMap.get(key) 获取键值,没有不执行后续操作
  • 遍历键值执行函数
js
const trigger = (target, key) => {
  const depsMap = targetMap.get(target)
  if(!depsMap) return

  const deps = depsMap.get(key)
  if(!deps) return

  deps.forEach(effectFn => effectFn());
}

effect 处理相应的函数对象,返回函数让外部可以执行,然后自调用执行一次。

js
const effect = fn => {
  const effectFn = () => {
    activeEffect = fn
    return fn()
  }

  // 先执行一次
  effectFn()

  return effectFn
}

调用 effect ,传入一个函数,定义一个对象调用并修改值,例如:

js
const a = reactive({
  count: 0
})

effect(() => {
  console.log('执行effect,a:' + a.count);
})

a.count = 10

最后运行效果如下所示:

i51bmt.png

bug修复

如果定义一个多属性的方法,一个属性使用嵌套的形式输出,一个属性直接输出,如下所示:

js
const a = reactive({
  count: 0,
  num: 10
})

effect(() => {
  effect(() => {
    console.log('count:' + a.count);
  })
  console.log('num:' + a.num);
})

a.num = 200
a.num = 250
a.num = 500

此时控制台打印的效果如下所示:

iS3gYt.png

可以发现,只输出了嵌套调用的 count ,直接输出的 num 没有了。

这是因为它们是同名函数,内部的函数会覆盖外部的函数。实质上,我们调用的是外部的函数,而运行的是内部的函数,因此就会出现上面的bug。

解决方案:把每一次调用的函数都保存到数组内,存储更变之前的函数,执行完毕后再清除。

js
const arr = []

// tarck与trigger服务于effect,因此要被调用出来
const effect = fn => {
  const effectFn = () => {
    try {
      arr.push(effectFn)
      activeEffect = fn
      return fn()
    } finally {
      arr.pop()
      activeEffect = arr[arr.length - 1]
    }
  }

  // 先执行一次
  effectFn()

  return effectFn
}

ref实现

原理

通过调用并返回一个类,其中,类的 get 方法接收 value 并赋值,set 方法修改 value 值。

使用 tarcktrigger 收集依赖。

js
import {effect, reactive, trigger, tarck} from './effect.js'

export const ref = value => {
  return new RefImpl(value)
}

// 声明类,做获取、赋值操作
class RefImpl {
  constructor(value) {
    this._value = value
  }

  get value() {
    tarck(this, 'value')
    return this._value
  }

  set value(newValue) {
    this._value = newValue
    trigger(this, 'value')
  }
}

修改值,查看控制台的输出

js
const count = ref(1)

effect(() => {
  console.log('count:' + count.value);
})

count.value = 10

iSmRck.png

答疑解惑

为什么返回 this._value 而调用时是 count.value

getset 实际上是一个代理,代理括号前面的那个变量,_value 是它们内部操作的变量。

computed实现

初步实现

照猫画虎,照 ref 实现 computed

computed 接收的参数是一个函数,因此要去调整参数,把 value 换为 getter ,类中不是通过 this. 直接赋值,而是通过 effect 函数来收集依赖实现。set 方法也不需要了,通过 effect 来赋值操作。

js
import {effect, reactive, trigger, tarck} from './effect.js'
import { ref } from "./ref.js";

export const computed = getter => {
  return new ComputedRefImpl(getter)
}

class ComputedRefImpl {
  constructor(getter) {
    this.effect = effect(getter, () => {
      trigger(this, 'value')
    })
  }

  get value() {
    this._value = this.effect()
    tarck(this, 'value')
    return this._value
  }
}

现在只能接收函数,没有处理函数,因此回到 effect 函数处理函数实现调度。

目前 effect 函数只接收一个函数,声明第二个形参调度器,默认值为 null

挂载调度函数,判断当前调度器是否为空,为空则为 effectFn 赋值挂载。

目前为止调度器不为空,去到 trigger 执行调度器,判断当前是否存在调度函数,如果存在则先执行调度函数,没有则执行 effect 本身。

js
export const effect = (fn, scheduler = null) => {
  const effectFn = () => {
    try {
      arr.push(effectFn)
      activeEffect = effectFn
      return fn()
    } finally {
      // ......
    }
  }

  // 先执行一次
  effectFn()

  if(scheduler) effectFn.scheduler = scheduler

  return effectFn
}

export const trigger = (target, key) => {
  // ......

  deps.forEach(effectFn => {
    // 如果有调度函数,优先执行调度函数。没有才执行effectFn函数本身
    effectFn.scheduler ? effectFn.scheduler(effectFn) : effectFn()
  });
}

调用该方法

js
const b = ref(1)
const c = computed(() => b.value + 5)
const fn = effect(() => {
  console.log('b:' + b.value);
  console.log('c:' + c.value);
})

fn()

执行结果如下所示

ioItWo.png

缓存实现

原理

设置一个锁,如果当前值没有更改,则锁为 true 表示锁上状态,通过 get 返回保存的值;如果值发生更改,即触发更新后,锁的状态改为 false ,此时调用调度函数获取新值。

js
// ......

class ComputedRefImpl {
  constructor(getter) {
    this._dirty = true
    this.effect = effect(getter, () => {
      if(!this._dirty) {
        this._dirty = false
        trigger(this, 'value')
      }
    })
  }

  get value() {
    if(this._dirty) {
      this._value = this.effect()
      this._dirty = false
      tarck(this, 'value')
    }

    return this._value
  }
}

模块渲染

vue运用虚拟dom技术来提高性能。虚拟dom技术简而言之指的是先将需要渲染的dom节点在内存中创建好,再通过渲染函数一次性渲染到页面中。

每次dom修改都会引起重绘或回流,大量的dom操作会极大影响性能。使用虚拟dom技术,就可以先在内存中创建好dom节点,操作内存中的dom节点比起直接操作页面dom节点性能要好不少。

原理

位运算符

利用位运算符的操作判断节点类型。

js
// 判断是属于什么类型的节点
const SHAPEFLAG = {
  ELEMENT: 1,
  TEXT: 1 << 1,
  COMPONENT: 1 << 2,
  TEXT_CHILDREN: 1 << 3,
  APPY_CHILDREN: 1 << 4
}

实现

创建虚拟节点

通过函数获取传参返回对象的形式创建一个 vnode 虚拟节点。

  1. 判断是属于什么类型的节点
    • 如果类型为字符串,则是元素类型
    • 如果类型为 Text ,则是文本类型
    • 否则是子节点
  2. 判断子元素属于什么类型节点
    • 如果类型为字符串,则是元素类型
    • 如果类型为 Text ,则是文本子类型
    • 如果类型为 Array ,则是数组子类型
  3. 返回包装的对象,包含类型,属性,子元素,标记,空 elkey
js
/**
 * 创建一个vnode虚拟节点
 * @type 虚拟don类型
 * @props 属性
 * @children 子元素
 */
const h = (type, props, children) => {
  let shapeFlag = 0 // 标记:区分类型

  // 根据type处理对应的节点类型,把记录的类型保存在shapeFlag中,后续包装成对象返回
  if(typeof type === 'string') {
    shapeFlag |= SHAPEFLAG.ELEMENT
  } else if(type === Text) {
    shapeFlag |= SHAPEFLAG.TEXT
  } else {
    shapeFlag |= SHAPEFLAG.COMPONENT
  }

  // 根据children处理对应的节点类型,把记录的类型保存在shapeFlag中,后续包装成对象返回
  if(typeof children === 'string') {
    shapeFlag |= SHAPEFLAG.ELEMENT
  } else if(children === 'number') {
    shapeFlag |= SHAPEFLAG.TEXT_CHILDREN
  } else if(Array.isArray(children)) {
    shapeFlag |= SHAPEFLAG.APPY_CHILDREN
  }

  return {
    type,
    props,
    children,
    shapeFlag,
    el: null,
    key: props && props.key
  }
}

传递参数

js
render(
  h(
    // 标签,{属性(id,类名,样式,事件)}
    'ul', {
      id: 'ulId',
      class: 'active',
      style: {
        border: 'none',
        color: '#ccc'
      },
      onClick: () => console.log('click ul')
    },
    [
      h('li', null, 1),
      h('li', null, [h(Text, null, '2')]),
      h('li', null, 3),
    ]
  )
)

渲染函数执行

  • 定义一个 render 函数,接收 vnode 节点和 container 内容渲染父节点。

    一个节点可能会有很多子节点,因此使用递归操作挂载会更好。

    1. container 内容先清空,方便后续挂载
    2. 定义一个 mount 函数用于处理
    js
    const render = (vnode, container) => {
      container.innerHTML = ''
      mount(vnode, container)
    }
  • mount 函数接收一样的参数

    1. vnode 中解构获取 shapeFlag 标记,判断处理
    2. 如果是文本节点,调用文本节点渲染的函数
    3. 如果是元素节点,调用元素节点对应的函数
    4. 如果是子节点,调用子节点对应的函数

    都传入 vnodecontainer

    js
    const mount = (vnode, container) => {
      const { shapeFlag } = vnode
    
      if (shapeFlag & SHAPEFLAG.TEXT) {
        // 单独调用文本节点的渲染方法
        mountTextNode(vnode, container)
      } else if (shapeFlag & SHAPEFLAG.ELEMENT) {
        // 元素节点的挂载方法
        mountELEMENT(vnode, container)
      } else if (shapeFlag & SHAPEFLAG.COMPONENT) {
        // 子节点
        mountCOMPONENT(vnode, container)
      }
    }
  • 文本节点挂载函数

    1. 通过 document.createTextNode 创建文本节点
    2. container 上挂载刚刚创建的文本节点
    3. vnodeel 属性赋值对应属性
    js
    const mountTextNode = (vnode, container) => {
      const textNode = document.createTextNode(vnode.children)
    
      container.appendChild(textNode)
    
      vnode.el = textNode
    }
  • 元素节点挂载函数

    1. 解构 vnode 获取类型、属性、子元素与标记
    2. 创建一个盒子,类型为第一步解构获取到的 type
    3. container 上挂载元素
    4. 挂载元素(单独封装为一个函数,传递 propsel 参数)
    5. 根据不同的子节点类型调用不同的方法来挂载子节点
      • 文本节点继续调用文本节点函数渲染
      • 子节点则封装一个子节点渲染的函数,接收属性 children 和挂载父节点 el
    6. 挂载元素到 container
    7. el 赋值到 vnodeel
    js
    const mountELEMENT = (vnode, container) => {
      // 拿数据
      const {
        type,
        props,
        children,
        shapeFlag,
      } = vnode
    
      const el = document.createElement(type)
    
      // 挂在属性
      mountProps(props, el)
    
      // 子节点类型不同,调用不同方法挂载子节点
      if (shapeFlag & SHAPEFLAG.TEXT_CHILDREN) {
        mountTextNode(vnode, el)
      } else if (shapeFlag & SHAPEFLAG.APPY_CHILDREN) {
        mountChildren(children, el)
      }
    
      container.appendChild(el)
    
      vnode.el = el
    }
  • 子节点渲染的函数

    js
    const mountChildren = (children, container) => {
      children.forEach(element => {
        mount(element, container)
      });
    }
  • 属性函数渲染

    遍历属性对象,通过 switch 判断:

    • class:类名添加
    • style:样式设置
    • on:事件绑定
    js
    // 属性遍历
    const mountProps = (props, el) => {
      for (const key in props) {
        const value = props[key]
        switch (key) {
          case 'class':
            el.className = value
            break;
          case 'style':
            for (const styleName in value) {
              el.style[styleName] = value[styleName]
            }
            break;
          default:
            // 判断事件,事件都是以on开头
            if(/^on[A-Z]/.test(key)){
              // 把事件名的on去掉,转为小写。如:onClick转为click
              const eventName = key.slice(2).toLowerCase()
              el.addEventListener(eventName, value)
            } else {
              el.setAttribute(key, value)
            }
            break;
        }
      }
    }

运行

index.html 引入 runtime.js 文件,通过 yarn vite 运行查看页面效果。

注意:

如果出现 'vite' 不是内部或外部命令,也不是可运行的 程序 或批处理文件。 error Command failed with exit code 1 错误,说明你没有装依赖。先 yarn 安装依赖。

页面更新实现

如果 render 函数多次执行更新,则重复清空 cantainer 再创建节点是很消耗性能的。

思路

把旧节点称为 v1,新节点称为 v2.想象以下几点:

  1. 对比v1和v2,我们需要传入v2,获取v1
  2. 不能只增不减,因此需要 unmount 删除不需要的节点
  3. 不同类型节点需要的对比操作不同,如文本节点对比文本内容;元素节点对比标签类型、节点属性、子节点等,因此需要用到不同的函数事件。
  4. 如果类型相同,只需要去操作节点更新即可,不需要整个替换

步骤

  1. 获取旧节点

  2. 判断新旧节点是否一样,一样则不需要更新节点,不一样则考虑以下情况:

    • 存在节点,更新(关键在于对比新旧节点的类型 type )判断,如果类型不一致,销毁节点。获取标记,判断其类型,做不同操作

      • 如果存在且为文本类型,判断是否存在新节点。存在则更新文本节点的内容,不存在则调用前面封装好的函数新建文本节点。

      • 如果存在且为元素类型,判断旧节点与新节点是否相同,一样则直接返回,不一样则遍历旧节点的属性对象。

        如果对应的新节点该属性为空,更新方法;遍历新节点的属性对象,获取其旧属性的值与新属性的值。

        再看看两者是否不等,不等则更新方法。

        更新方法的函数接收新旧节点,对应属性的 key ,渲染父节点 el 。判断类名、样式、事件的动态修改或移除。

        js
        const patchDomProps = (prev, next, key, el) => {
          switch (key) {
            case 'class':
              el.className = next || ''
              break;
            case 'style':
              if(next === null) {
                el.removeAttribute('style')
              } else {
                if(prev) {
                  for (const styleName in object) {
                    if(next[styleName] === null) {
                      el.style[styleName] = ''
                    }
                  }
                }
        
                for (const styleName in next) {
                  el.style[styleName] = next[styleName]
                }
              }
              break;
            default:
              if (/^on[A-Z]/.test(key)) {
                // 把事件名的on去掉,转为小写。如:onClick转为click
                const eventName = key.slice(2).toLowerCase()
                if(prev) el.removeEventListener(eventName)
                if(next) el.addEventListener(eventName)
              } else {
                if(next === null || next === false)
                el.removeAttribute(key)
        
                if(next) el.setAttribute(key)
              }
              break;
          }
        }
      • 更新子节点,子节点是数组形式,因此需要获取旧子节点的长度,新子节点的长度和二者最小的值,如果新子节点长度大于旧子节点,说明有新增操作,反之是删除操作

    • 不存在节点,移除旧节点,做销毁操作

    js
    const render = (vnode, container) => {
      const prevNode = container._vnode
    
      // 判断旧节点是否存在
      if (vnode) {
        patch(prevNode, vnode, container)
      } else {
        prevNode && unmount(prevNode)
      }
    
      container.innerHTML = ''
      mount(vnode, container)
    }
    
    const patch = (v1, v2, container) => {
      // 如果类型不一致,直接销毁
      if (v1 && v1.type !== v2.type) {
        unmount(v1)
        v1 = null
      }
    
      const { shapeFlag } = v2
      if (shapeFlag & SHAPEFLAG.TEXT) {
        processTextNode(v1, v2, container)
      } else if (shapeFlag & SHAPEFLAG.ELEMENT) {
        processElement(v1, v2, container)
      }
    }
    
    const processTextNode = (v1, v2, container) => {
      if (v1) {
        v2.el = v1.el
        v1.el.textContent = v2.children
      } else {
        mountTextNode(v2, container)
      }
    }
    
    const processElement = (v1, v2, container) => {
      if (v1) {
        v2.el = v1.el
    
        patchProps(v1.props, v2.props, el)
        patchChildren(v1, v2, v2.el)
      } else {
        mountELEMENT(v2, container)
      }
    }
    
    const patchProps = (old, newProps, el) => {
      if (old === newProps) {
        return
      }
    
      for (const key in old) {
        if (newProps[key] === null) {
          patchDomProps(old[key], null, key, el)
        }
      }
    
      for (const key in newProps) {
        const prew = old[key]
        const next = newProps[key]
    
        if (prew !== next) {
          patchDomProps(prev, next, key, el)
        }
      }
    }
    
    const patchChildren = (v1, v2, el) => {
      const {shapeFlag: prevShap, children: prevChildren} = v1
      const {shapeFlag: nextShap, children: nextChildren} = v2
    
      if(prevShap & SHAPEFLAG.TEXT_CHILDREN) {
        if(nextShap & SHAPEFLAG.TEXT_CHILDREN) {
          el.textContent = nextChildren
        } else if(nextShap & SHAPEFLAG.APPY_CHILDREN) {
          el.textContent = ''
          mountChildren(nextChildren, el)
        } else {
          el.textContent = ''
        }
      } else if(prevShap & SHAPEFLAG.APPY_CHILDREN) {
        if(nextShap&SHAPEFLAG.TEXT_CHILDREN) {
          el.textContent = nextChildren
        } else if(nextShap & SHAPEFLAG.APPY_CHILDREN) {
          const oldLength = prevChildren.length
          const newLength = nextChildren.length
          const commonLength = Math.min(oldLength, newLength)
    
          for (let i = 0; i < commonLength; i++) {
            patch(prevChildren[i], nextChildren[i], el)
          }
          if(oldLength > newLength) {} else if(oldLength < newLength) {
            mountChildren(nextChildren.slice(commonLength), el)
          }
        }
      }
    }
    
    const patchDomProps = (prev, next, key, el) => {
      switch (key) {
        case 'class':
          el.className = next || ''
          break;
        case 'style':
          if(next === null) {
            el.removeAttribute('style')
          } else {
            if(prev) {
              for (const styleName in object) {
                if(next[styleName] === null) {
                  el.style[styleName] = ''
                }
              }
            }
    
            for (const styleName in next) {
              el.style[styleName] = next[styleName]
            }
          }
          break;
        default:
          if (/^on[A-Z]/.test(key)) {
            // 把事件名的on去掉,转为小写。如:onClick转为click
            const eventName = key.slice(2).toLowerCase()
            if(prev) el.removeEventListener(eventName)
            if(next) el.addEventListener(eventName)
          } else {
            if(next === null || next === false)
            el.removeAttribute(key)
    
            if(next) el.setAttribute(key)
          }
          break;
      }
    }

Diff算法

思路

有一个场景,我们有5个元素:a、b、c,想要变更为a、d、b、c,通过 diff 算法实现转化。a节点是一样的,不变,直接使用旧节点即可,头指针往后;从后看c节点也是一样的,不变,直接使用旧节点,尾指针往前。

diff 算法核心是先做预处理,处理头指针和尾指针的绑定,中间部分后续做更新处理。

步骤

假设旧节点的 key 和新节点的 key 都存在,定义头指针 i = 0 和旧节点 c1 尾指针 e1 = c1.length - 1 、新节点 c2 尾指针 e2 = c2.length - 1

接下来做预处理,判断头指针与尾指针的索引位置:

  • 如果头指针小于等于 e1 与小于等于 e2 ,且 c1 的第 i 项指针与 c2 的第 i 项指针相同,则做渲染处理,i 自增1
  • 如果尾指针小于等于 e1 与小于等于 e2 ,且 c1 的第 e1 项指针与 c2 的第 e2 项指针相同,则做渲染处理,e1e2 自减1

预处理结束,接下来判断新旧节点渲染的情况:如果 i 大于 e1 且小于等于 e2 ,这个时候可以认为 ie2 中间部分是需要挂载的新节点;如果 i 大于 e2 且小于等于 e1 ,这个时候可以认为 ie1 中间部分是需要卸载的旧节点。

  • 如果 i 大于 e1 且小于等于 e2 ,遍历 e2 ,起始索引为 i ,挂载新节点
  • 如果 i 大于 e2 且小于等于 e1 ,遍历 e1 ,起始索引为 i ,卸载旧节点

再有一个场景:c1 为a、b、c、d、e;c2 为a、d、f、g、e,只有首尾相同,其他都不等。

  1. 首先获取 c1c2 的起始索引 s1s2 等于 i

  2. 初始化 keyToNewIndex = new Map()

    遍历 e2 ,初始值为 s2 ,把索引和对应索引的 c2key 值作为映射关系保存。

    定义一个 patched 为0,用于记录当前已经 patched 更新的节点数量。

    定义一个 movedfalse ,用于判断是否需要移动位置。

    定义一个 maxNewIndexSoFar 为0 ,用于判断是否是上升趋势。

    定义一个 toBePatche2 - s2 + 1 ,用于记录需要被 patch 更新的节点数量。

  3. 根据存的 keyToNewIndex 获取新索引 newIndex

  4. 根据 newIndex 更新 newIndexToOldIndexMap

    初始化 newIndexToOldIndexMap 的数据结构 new Array(toBePatch).fill(-1)

  5. 遍历 e1 ,起始值为 s1 ,判断 patched 是否大于等于 toBePatch ,符合要求,则卸载节点。

    获取该节点的新索引 keyToNewIndexMap.get(c1[i].key),如果该索引不存在,说明没有该节点了,销毁;存在,把当前索引 i 保存给 newIndexToOldIndexMap[newIndex - s2] ,判断其是否为上升趋势 newIndex >= maxNewIndexSoFar,符合要求则把 newIndex 赋值给 maxNewIndexSoFar ,否则把需要移动的变量 moved 改为 true 。挂载节点,patched 自增1。

  6. 移动。根据 newIndexToOldIndexMap 获取其第0项的数据并变为数组的形式 res = [newIndexToOldIndexMap[0]] ,定义对应索引 pos = [0]

    遍历 newIndexToOldIndexMap ,如果它第 i 项为 -1,说明其并没有导入东西,直接 pos.push(-1) 即可。

    如果它第 i 项大于 res[res.length - 1]res 追加 newIndexToOldIndexMap[i]pos 追加 res.length - 1 ;否则遍历 res ,判断 res[j] 是否大于 newIndexToOldIndexMap[i] ,把 newIndexToOldIndexMap[i] 赋值给 res[j]

  7. 获取处理完的 res 数组最后一项,

组件实现

  • 获取节点类型,节点类型只能是组件节点类型。

  • 创建实例对象 instance ,包含参数 propsattrs ,状态 setupStatectx

  • 初始化 props

    定义 props 为当前实例对象中的 propsattrs 为当前实例对象中的 attrs

    遍历 props 实例对象,如果组件存在,赋值,不存在,保存。

    最后通过前面封装好的 reactive 方法转为响应式。

  • instance 实例对象通过 effect 函数设置 update 更新函数,

调度器

定义一个变量 a = ref(0) ,定义一个方法

js
add = () => {
    a.value++
    a.value++
}

此时执行操作时会让 a 自增两次1,也就是有两次更新操作,控制台查看效果,发现 log 两次更新,如果变化多的话这性能肯定不合理,最好的方法是把方法封存,等待更新结束后再执行。

使用调度函数,先定义一个队列数组,把所有任务 .push() 进去执行入队操作。

是用一个变量控制是否执行,判断其是否为假。如果为假,改为真,执行回调函数。

createApp

接收一个参数,判断其类型。如果是字符串,则通过 document.querySelector() 获取元素,通过 render(h()) 渲染挂载

编译

穷举语法树

设置一个节点类型穷举对象

js
const NodeTypes = {
    ROOT: 'ROOT',
    TEXT: 'TEXT',
    // ...
}

设置一个元素类型穷举对象

js
const ElementTypes = {
    ELEMENT: 'ELEMENT',
    COMPONENT: 'COMPONENT',
    // ...
}