加载中...
  • Vuejs设计与实现 loading

    模板的工作原理

    1
    2
    3
    4
    渲染器: 把虚拟DOM渲染为真实的DOM
    组件: 一组DOM元素的封装, 这组DOM元素就是要渲染的内容
    编译器: 将模板编译为渲染函数
    Vue3的渲染器和编译器可以进行信息传递(通过虚拟DOM)

    响应式系统的作用与实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    响应式数据: 当值变化后, 副作用函数自动重新执行

    // 简易实现
    const bucket = new WeakMap()

    let activeEffect

    const data = { text: 'hello world' }

    function track(target, key) {
    if (!activeEffect) return
    let depsMap = bucket.get(target)
    if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if (!deps) {
    depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)
    }

    function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)
    if (!effects) return
    effects.forEach((fn) => fn())
    }

    const obj = new Proxy(data, {
    get(target, key) {
    track(target, key)
    return target[key]
    },
    set(target, key, value) {
    target[key] = value
    trigger(target, key)
    return true
    }
    })

    function effect(fn) {
    activeEffect = fn
    fn()
    }

    effect(() => {
    console.log('effect run')
    document.body.innerHTML = obj.text
    })

    setTimeout(() => {
    obj.notExist = 'error'
    obj.text = 'text'
    }, 1000)

    分支切换和 cleanup

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    每次副作用函数执行时, 我们可以先把它从所有与之相关联的依赖集合中删除, 当副作用函数执行完毕后, 会重新建立联系, 在新的联系中, 不会包含遗留的副作用函数

    function effect(fn) {
    // activeEffect = fn
    // fn()
    const effectFn = () => {
    cleanup(effectFn)
    activeEffect = fn
    fn()
    }
    effectFn.deps = []
    effectFn()
    }

    function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
    }
    effectFn.deps.length = 0
    }

    在调用forEach遍历Set集合时, 如果一个值已经被访问过了, 但该值被删除重新添加到集合, 如果此时forEach遍历没有结束, 那么改值会重新被访问.
    {
    set.delete(1)
    set.add(1)
    }

    嵌套的 effect 与 effect 栈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    我们用全局变量activeEffect来存储effect函数注册的副作用函数, 这意味着同一时刻, activeEffect所存储的副作用函数只能有一个, 当副作用函数发生嵌套时, 内层副作用函数的执行会覆盖activeEffect的值, 并且永远不会恢复

    function effect(fn) {
    const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn) // add
    fn()
    effectStack.pop() // add
    activeEffect = effectStack.at(-1) // add
    }
    effectFn.deps = []
    effectFn()
    }

    避免无限递归循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    effect(() => obj.foo++) // 既读取obj.foo的值, 又会设置obj.foo的值

    解决方法
    在trigger动作发生时增加守卫条件: 如果trigger触发执行的副作用函数与当前正在执行的副作用函数相同, 则不触发执行

    const effectsToRun = new Set()
    effects.forEach((effectFn) => {
    if (effectFn !== activeEffect) {
    effectsToRun.add(effectFn)
    }
    })

    调度执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    调度执行是指当trigger动作触发副作用函数重新执行时, 有能力决定副作用函数执行的时机, 次数, 以及方式

    用户在调用effect函数注册副作用函数时, 可以传递第二个参数options, 它是一个对象, 其中允许指定scheduler调度函数

    effectsToRun.forEach((effectFn) => {
    const { scheduler } = effectFn.options
    if (scheduler) {
    scheduler(effectFn)
    } else {
    effectFn()
    }
    })

    计算属性 computed 和 lazy

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    export function effect(fn, options = {}) {
    // activeEffect = fn
    // fn()
    const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    const res = fn()
    effectStack.pop()
    activeEffect = effectStack.at(-1)
    return res
    }
    effectFn.options = options
    effectFn.deps = []
    if (!options.lazy) {
    effectFn()
    }
    return effectFn
    }

    export const computed = (getter) => {
    let value
    let dirty = true
    const effectFn = effect(getter, {
    scheduler() {
    dirty = true
    trigger(obj, 'value')
    },
    lazy: true
    })
    const obj = {
    get value() {
    if (dirty) {
    value = effectFn()
    dirty = false
    }
    track(obj, 'value')
    return value
    }
    }
    return obj
    }

    watch 的实现原理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    所谓watch本质就是观测一个响应式数据, 当数据发生变化时通知并执行响应的回调函数, 利用了effect以及options.scheduler选项
    newValue, oldValue, 使用了lazy选项
    竟态问题: watch函数的回调函数接受第三个参数, onInvalidate, 它是一个函数, 在当前副作用函数过期后执行, 按序发送a, b两个请求, b先返回, 则a过期

    export const watch = (source, cb, options = {}) => {
    let getter
    if (typeof source === 'function') {
    getter = source
    } else {
    getter = () => traverse(source)
    }
    let newValue, oldValue
    let cleanup

    const onInvalidate = (fn) => {
    cleanup = fn
    }

    const job = () => {
    newValue = effectFn()
    if (cleanup) {
    cleanup()
    }
    cb(newValue, oldValue, onInvalidate)
    oldValue = newValue
    }

    const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: () => {
    if (options.flush === 'post') {
    const p = Promise.then()
    p.then(job)
    } else {
    job()
    }
    }
    })

    if (options.immediate) {
    job()
    } else {
    oldValue = effectFn()
    }
    }

    理解 Proxy 和 Reflect

    1
    2
    3
    4
    Proxy只能代理对象, 且只能拦截对一个对象的基本操作
    复合操作: obj.fn(), 第一个基本语义为get, 第二个基本语义为函数调用

    Reflect是一个全局对象, Reflect的第三个参数receiver可以认为是函数调用过程中的this, 代表着谁在读取属性

    代理 Object

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    对一个普通对象的所有可能的读取操作
    obj.foo (get)
    key in obj (has)
    for (const key in obj) {} (ownKeys)

    const reactive = (data) => {
    return new Proxy(data, {
    get(target, key, receiver) {
    // 代理对象的raw属性可以读取原始属性
    // child.raw = obj parent.raw = proto
    if (key === 'raw') {
    return target
    }
    if (!isReadonly) {
    track(target, key)
    }
    track(target, key)
    const res = Reflect.get(target, key, receiver)
    if (typeof res === 'object' && res !== null) {
    return isReadonly ? readonly(res) : reactive(res)
    }


    return res
    },
    set(target, key, value, receiver) {
    // target是原对象, 当自身对象不存在属性时, 会遍历原型链
    // receiver是代理对象
    const oldValue = target[key]
    // 判断for...in循环是否修改了值
    const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
    const res = Reflect.set(target, key, value, receiver)
    if (target === receiver.raw) {
    if (value !== oldValue) {
    trigger(target, key, type)
    }
    }
    return res
    },
    has(target, key, receiver) {
    track(target, key)
    return Reflect.has(target, key, receiver)
    },
    ownKeys(target) {
    track(target, ITERATE_KEY)
    return Reflect.ownKeys(target)
    },
    deleteProperty(target, key) {
    const res = Reflect.defineProperty(target, key)
    if (res) {
    trigger(target, key)
    }
    return res
    }
    })
    }

    只读与浅只读

    1
    2
    3
    如果一个数据是可读的
    set, delete失效
    没必要为只读数据建立响应联系

    原始值的响应式方案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    js中的proxy无法提供对原始值得代理, 因此想要将原始值变成响应式数据, 就必须对其做一层包裹, 也就是ref

    区分ref
    function ref(val) {
    const wrapper = {
    value: val
    }
    Object.prototype.defineProperty(wrapper, '__v_isRef', {
    value: true
    })
    return reactive(wrapper)
    }

    ref除了能够用于原始值的响应式方案外, 还能用来解决响应式丢失的问题
    (解构一个响应式对象后, 会得到一个普通对象)
    function toRef(obj, key) {
    const wrapper = {
    get value() {
    return obj[key]
    },
    set value(val) {
    obj[key] = val
    }
    }
    Object.prototype.defineProperty(wrapper, '__v_isRef', {
    value: true
    })
    return wrapper
    }
    function toRefs(obj) {
    const res = {}
    for (let ket in obj) {
    res[key] = toRef(obj, key)
    }
    return res
    }

    return { ...toRefs(reactiveData) }

    自动脱ref
    function isRef(ref) {
    return !!ref.__v_isRef
    }

    function unref(ref) {
    return isRef(ref) ? ref.value : ref
    }

    function proxyRefs(target) {
    return new Proxy(target, {
    get(target, key, receiver) {
    const value = Reflect.get(target, key, receiver)
    return unref(value)
    },
    set(target, key, newValue, receiver) {
    const value = target[key]
    if (isRef(value)) {
    value.value = newValue
    return true
    }
    return Reflect.set(target, key, newValue, receiver)
    }
    })
    }

    渲染器与响应式系统的结合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    渲染器不仅能够渲染真实的DOM, 它还是框架跨平台能力的关键
    <script src="http://unpkg.com/@vue/reactivity#3.0.5/dist/reactivity.global.js"></script>

    const { effect, ref } = VueReactivity


    渲染器把虚拟DOM节点渲染为真实DOM节点的过程叫做挂载 mount


    新增, 更新, 删除节点
    const renderer = createRenderer()
    renderer.render(vnode1, app)
    renderer.redner(vnode2, app)
    renderer.render(null, app)
    首次渲染时, 渲染器会将vnode1渲染为真实的DOM, 渲染完成后, vnode1会存储到容器元素的container._vnode属性中, 它在后续渲染中作为旧vnode使用
    第二次渲染时, 旧vnode存在, 此时渲染器会把vnode2作为新node, 并将新旧node一同传递给patch函数进行补丁
    第三次渲染, 什么都不渲染, 清空容器

    自定义渲染器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    createApp => renderer.createApp => createRenderer(options).createApp

    function createRenderer(options = {}) {

    const { createElement, insert, setElementText } = options

    function patch(n1, n2, container) {
    if (!n1) {
    mountElement(n2, container)
    } else {

    }
    }
    function render(vnode, container) {
    if (vnode) {
    patch(container._vnode, vnode, container)
    } else {
    if (container._vnode) {
    container.innerHTML = ''
    }
    }
    container._vnode = vnode
    }
    function hydrate(vnode, container) { }
    function mountElement(vnode, container) {
    const el = createElement(vnode.type)
    if (typeof vnode.children === 'string') {
    setElementText(el, vnode.children)
    }
    insert(el, container)
    }
    return {
    render,
    hydrate
    }
    }
    const options = {
    createElement(tag) {
    return document.createElement(tag)
    },
    setElementText(el, text) {
    el.textContent = text
    },
    insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)
    }
    }
    const renderer = createRenderer(options)
    const vnode = { type: 'div', props: {}, children: 'hello' }
    renderer.render(vnode, app)

    正确的设置元素属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    HTML attributes的作用是设置与之对应的DOM properties的初始值
    如果设置的attribute不合法, 浏览器会使用内建合法值作为与之对应DOM property的默认值

    优先设置元素的DOM properties, 但当值为空字符串时, 要手动将值矫正为true, 如果不存在DOM property, 使用setAttribute

    shouldSetAsProps(el, key, value) {
    if (key === 'form' && el, tagName === 'INPUT') return false
    return key in el
    },
    patchProps(el, key, preValue, nextValue) {
    // 使用el.className 代替 setAttribute 性能更好
    if (key === 'class') {
    el.className = nextValue || ''
    } else if (options.shouldSetAsProps(el, key, nextValue)) {
    const type = typeof el[key]
    if (type === 'boolean' && nextValue === '') {
    el[key] = true
    } else {
    el[key] = nextValue
    }
    } else {
    el.setAttribute(key, nextValue)
    }
    }

    卸载操作

    1
    2
    3
    4
    5
    6
    7
    8
    直接通过innerHTML = ‘’ 缺点
    1. 容器的内容可能是由某个或多个组件渲染的, 当卸载操作发生时, 应该正确地调用这些组件的beforeUnmount, unmounted生命周期
    2. 执行自定义指令钩子
    3. 不会移除绑定在DOM元素上的事件处理函数

    const el = container._vnode.el
    const parent = el.parentNode
    if (parent) parent.removeChild(el)

    区分 vnode 类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    当vnode的type不同时, 卸载旧vnode, 挂载新的vnode

    function patch(n1, n2, container) {
    // 当vnode的type不同时, 卸载旧vnode
    if (n1 && n1.type !== n2.type) {
    unmount(n1)
    n1 = null
    }
    const type = typeof n2.type
    switch (type) {
    case 'fragment':
    break
    case 'string':
    if (!n1) {
    // 新增
    mountElement(n2, container)
    } else {
    // 更新
    patchElement(n1, n2)
    }
    break
    case 'object':
    break
    }
    }

    事件的处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    绑定一个伪造的事件处理函数invoker, 然后把真正的事件处理函数设置为invoker.value属性的值, 这样当更新事件的时候, 我们将不再需要调用removeEventListener函数来移除上一次绑定的事件, 只需要更新invoker.value的值即可, 同时处理了事件与更新时间间的差异

    if (/^on/.test(key)) {
    const invokers = el._vei || (el._vei = {})
    let invoker = invokers[key]
    const name = key.slice(2).toLowerCase()
    if (nextValue) {
    if (!invoker) {
    invoker = el._vei[key] = (e) => {
    console.log('e: ', e)
    // e.timeStamp 时间发生的时间
    // 如果时间发生的时间早于事件处理函数绑定的时间, 则不执行事件处理函数
    if (e.timeStamp < invoker.attached) return
    if (Array.isArray(invoker.value)) {
    invoker.value.forEach((fn) => fn(e))
    } else {
    invoker.value(e)
    }
    }
    invoker.value = nextValue
    invoker.attached = performance.now()
    el.addEventListener(name, invoker)
    } else {
    invoker.value = nextValue
    }
    } else if (invoker) {
    el.removeEventListener(name, invoker)
    }
    }

    更新子节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    vnode.children的三种情况: 没有子节点, 文本子节点, 一组子节点

    function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
    // 旧节点的类型有三种可能, 没有子节点, 文本节点以及一组子节点
    // 只有当旧节点为一组节点时, 才需要逐个卸载, 其他情况下什么都不需要做
    if (Array.isArray(n1.children)) {
    n1.children.forEach((c) => unmount(c))
    }
    setElementText(container, n2.children)
    } else if (Array.isArray(n2.children)) {
    if (Array.isArray(n1.children)) {
    //TODO diff
    n1.children.forEach((c) => unmount(c))
    n2.children.forEach((c) => patch(null, c, container))
    } else {
    setElementText(container, '')
    n2.children.forEach((c) => patch(null, c, container))
    }
    } else {
    // 代码运行到这里说明新子节点不存在
    if (Array.isArray(n1.children)) {
    n1.children.forEach((c) => unmount(c))
    } else if (typeof n1.children === 'string') {
    setElementText(container, '')
    }
    }
    }

    文本节点和注释节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    认为创建文本节点和注释节点
    cosnt TEXT = Symbol()
    const COMMENT = Symbol()

    case TEXT:
    if (!n1) {
    const el = (n2.el = createText(n2.children))
    insert(el, container)
    } else {
    const el = (n2.el = n1.el)
    if (n2.children !== n1.children) {
    setText(el, n2.children)
    }
    }
    break

    Fragment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    渲染器渲染Fragment时, 只会渲染Fragment的子节点

    unmount(vnode) {
    if (vnode.type === FRAGMENT) {
    vnode.children.forEach((c) => renderOptions.unmount(c))
    return
    }
    const parent = vnode.el.parentNode
    if (parent) {
    parent.removeChild(vnode.el)
    }
    },

    简单 diff 算法

    1
    在进行对比时, 应该遍历新旧节点中长度较短的一组, 执行patch函数每项比对更新, 接着再对比新旧两组节点的长度, 如果新的一组节点更长, 则说明有新节点需要挂载, 否则说明有旧节点需要卸载

    DOM 复用与 key 的作用

    1
    想要通过移动DOM移动完成更新, 必须要保证一个前提: 新旧两组节点中确实存在可以服用的节点 vnode.type && vnode.key

    找到需要移动的元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    当新旧两个子节点的节点顺序不变时, 不需要额外的移动操作

    每一次寻找可复用节点时, 都会记录该可复用节点在旧的一组节点中的位置索引, 如果把这些位置索引按照先后排序, 则可以得到一个序列, 如果是一个递增序列, 就不需要移动节点

    在旧children中寻找具有相同key值节点的过程中, 遇到的最大索引值
    如果在后续寻找中, 存在索引值比当前遇到的最大索引值还要小的节点, 则意味着该节点需要移动

    if (Array.isArray(n1.children)) {
    //TODO diff
    const oldChildren = n1.children
    const newChildren = n2.children
    const oldLen = oldChildren.length
    const newLen = newChildren.length
    // 用来存储寻找过程中遇到的最大索引值
    let lastIndex = 0
    for (let i = 0; i < newLen; i++) {
    const newVNode = newChildren[i]
    for (let j = 0; j < oldLen; j++) {
    const oldVNode = oldChildren[j]
    if (newVNode.key === oldVNode.key) {
    patch(oldVNode, newVNode, container)
    if (j < lastIndex) {
    // 如果当前索引小于最大索引, 意味着该节点对应的真实DOM需要移动
    } else {
    lastIndex = j
    }
    break
    }
    }
    }
    }

    如何移动元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    要移动的并不是虚拟节点本身, 而是真实的DOM节点 => vnode.el

    if (j < lastIndex) {
    // 如果当前索引小于最大索引, 意味着该节点对应的真实DOM需要移动
    const preVNode = newChildren[i - 1]
    if (preVNode) {
    const anchor = preVNode.el.nextSibling
    insert(newVNode.el, container, anchor)
    }
    } else {
    lastIndex = j
    }

    新增节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    找到新增节点: 在旧节点组中没有对应的key
    挂载到正确的位置上

    const newVNode = newChildren[i]
    let j = 0
    // find代表是否在旧的一组节点中找到可以复用的节点
    let find = false
    for (j; j < oldLen; j++) {
    const oldVNode = oldChildren[j]
    if (newVNode.key === oldVNode.key) {
    find = true
    patch(oldVNode, newVNode, container)
    if (j < lastIndex) {
    // 如果当前索引小于最大索引, 意味着该节点对应的真实DOM需要移动
    const preVNode = newChildren[i - 1]
    if (preVNode) {
    const anchor = preVNode.el.nextSibling
    insert(newVNode.el, container, anchor)
    }
    } else {
    lastIndex = j
    }
    break
    }
    }
    if (!find) {
    const preVNode = newChildren[i - 1]
    let anchor = null
    if (preVNode) {
    anchor = preVNode.el.nextSibling
    } else {
    anchor = container.firstChild
    }
    patch(null, newVNode, container, anchor)
    }

    移除不存在节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    当基本的更新结束时, 我们需要遍历一遍旧节点, 然后去新的节点中寻找具有相同key值的节点, 找不到则删除

    for (let i = 0; i < oldLen; i++) {
    const oldVNode = oldChildren[i]
    const has = newChildren.find((vnode) => vnode.key === oldVNode.key)
    if (!has) {
    unmount(oldVNode)
    }
    }

    双端 diff 算法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    双端diff算法是一种同时对新旧两组节点的两个端点进行比较的算法
    因此, 我们需要四个索引值, 分别指向新旧两组子节点的端点

    比较旧的一组子节点中的第一个子节点与新的一组子节点中的第一个子节点
    比较旧的一组子节点中的最后一个子节点与新的一组子节点中的最后一个子节点
    比较旧的一组子节点中的第一个子节点于新的一组子节点中的最后一个子节点
    比较旧的一组子节点中的最后一个子节点与新的一组子节点中的第一个子节点

    将索引oldEndIdx指向的虚拟节点所对应的真实DOM移动到索引oldStartIdx指向的虚拟节点所对应的真实DOM前面

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isSameKey(oldStartVNode, newStartVNode)) {
    patch(oldStartVNode, newStartVNode, container)
    oldStartVNode = oldChildren[++oldStartIdx]
    newStartVNode = newChildren[++newStartIdx]
    } else if (isSameKey(oldEndVNode, newEndVNode)) {
    patch(oldEndVNode, newEndVNode, container)
    oldEndVNode = oldChildren[--oldEndIdx]
    newEndVNode = newChildren[--newEndIdx]
    } else if (isSameKey(oldStartVNode, newEndVNode)) {
    patch(oldStartVNode, newEndVNode, container)
    insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)
    oldStartVNode = oldChildren[++oldStartIdx]
    newEndVNode = newChildren[--newEndIdx]
    } else if (isSameKey(oldEndVNode, newStartVNode)) {
    patch(oldEndVNode, newStartVNode, container)
    insert(oldEndVNode.el, container, oldStartVNode.el)
    oldEndVNode = oldChildren[--oldEndIdx]
    newStartVNode = newChildren[++newStartIdx]
    }
    }

    非理性情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    1, 2, 3, 4
    2, 4, 3, 1
    我们只能通过增加额外的处理逻辑来处理
    拿新的一组子节点的头部节点去旧的一组子节点中寻找, 如果找到, 意味着找到的节点时新节点中的头部

    const idxInOld = oldChildren.findIndex((node) => node.key === newStartVNode.key)
    if (idxInOld > 0) {
    const vnodeToMove = oldChildren[idxInOld]
    patch(vnodeToMove, newStartVNode, container)
    insert(vnodeToMove.el, container, oldStartVNode.el)
    oldChildren[idxInOld] = undefined
    } else {
    patch(null, newStartVNode,container, oldStartVNode.el)
    }
    newStartVNode = newChildren[++newStartIdx]


    1, 2, 3
    4, 1, 2, 3

    最后, oldEndIdx< oldStartIdx, 会有遗漏的节点没有更新, 需要补充逻辑
    if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
    for (let i = newStartIdx; i <= newEndIdx; i++) {
    patch(null, newChildren[i], container, oldStartVNode.el)
    }
    }


    移除不存在节点
    1, 2, 3, 5
    4, 1, 2, 3
    if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
    for (let i = newStartIdx; i <= newEndIdx; i++) {
    patch(null, newChildren[i], container, oldStartVNode.el)
    }
    } else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
    unmount(oldChildren[i])
    }
    }

    快速 dif 算法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    快速diff算法包含了预处理: 找出相同的前置元素和后置元素
    1, 2, 3
    1, 4, 2, 3
    对于相同的前置节点和后置节点, 由于它们在新旧两组节点中的相对位置不变, 所以无需移动它们

    新增
    oldEnd < j: 说明在预处理过程中, 所有旧子节点都处理完了
    newEnd >=j :说明在预处理过后, 在新的一组子节点中, 仍然有未被处理的节点, 而这些遗留的节点将被视为新增节点

    删除
    newEnd < j: 说明在预处理过程中, 所有新子节点都处理完了
    oldEnd >= j: 说明在预处理过后, 在新的一组子节点中, 仍然有未被处理的节点, 而这些遗留的节点将被视为删除节点

    function patchKeyedChildren(n1, n2, container) {
    const oldChildren = n1.children
    const newChildren = n2.children

    // 处理相同的前置节点, 索引j指向新旧两组子节点的开头
    let j = 0
    let oldVNode = oldChildren[j]
    let newVNode = newChildren[j]
    while (oldVNode.key === newVNode.key) {
    patch(oldVNode, newVNode, container)
    j++
    oldVNode = oldChildren[j]
    newVNode = newChildren[j]
    }
    // 处理相同的后置节点
    let oldEnd = oldChildren.length - 1
    let newEnd = newChildren.length - 1
    oldVNode = oldChildren[oldEnd]
    newVNode = newChildren[newEnd]
    while (oldVNode.key === newVNode.key) {
    patch(oldVNode, newVNode, container)
    oldVNode = oldChildren[--oldEnd]
    newVNode = newChildren[--newEnd]
    }
    // 旧子节点处理完毕, 新子节点还有未被处理的节点
    if (j > oldEnd && j <= newEnd) {
    const anchorIndex = newEnd + 1
    const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null
    while (j <= newEnd) {
    patch(null, newChildren[j++], container, anchor)
    }
    } else if (j > newEnd && j < oldEnd) {
    while (j <= oldEnd) {
    unmount(oldChildren[j++])
    }
    }
    }

    判断是否需要进行 DOM 移动操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    1,2,3,4,6,5
    1,3,4,2,7,5

    判断是否有节点要移动, 以及应该如何移动
    找出那些需要被添加或移除的节点

    构造一个数组source, 它的长度=新的一组子节点在经过预处理之后剩余未处理子节点的数量, 并且source中每个元素的初始值都是-1
    source用来存储新的一组子节点中的节点再旧的一组子节点中的位置索引, 后面会使用它计算出一个最长递增子序列, 用于辅助完成DOM移动的操作

    // 处理非理想情况
    const count = newEnd - j + 1
    const source = new Array(count).fill(-1)

    const oldStart = j
    const newStart = j
    let moved = false
    let pos = 0

    // 构建索引表
    const keyIndex = new Map()
    for (let i = newStart; i <= newEnd; i++) {
    keyIndex.set(newChildren[i].key, i)
    }

    // 更新过的节点数量
    let patched = 0

    for (let i = oldStart; i <= oldEnd; i++) {
    oldVNode = oldChildren[i]
    // 如果更新过的节点数量大于需要更新的节点数量, 则卸载多余的节点
    if (patched >= count) {
    unmount(oldVNode)
    continue
    }
    // 通过索引表快速找到新的一组子节点中具有相同key值得节点位置
    const k = keyIndex.get(oldVNode.key)
    if (typeof k !== 'undefined') {
    newVNode = newChildren[k]
    patch(oldVNode, newVNode, container)
    patched++
    source[k - newStart] = i
    // 判断是否需要移动
    if (k < pos) {
    moved = true
    } else {
    pos = k
    }
    } else {
    unmount(oldVNode)
    }
    }

    如何移动元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    lis函数: 最长递增子序列中的元素在source数组中的位置索引
    含义: 在新的一组子节点中, 重新编号后索引值为0和1的这两个节点在更新前后顺序没有发生变化. 换句话说, 索引值为0和1的节点不需要移动

    // 移动元素
    if (moved) {
    // 最长递增子序列中的元素在source数组中的位置索引
    const seq = lis(source)

    // s指向最长递增子序列的最后一个元素
    let s = seq.length - 1
    // i 指向新的一组子节点的最后一个元素
    let i = count - 1

    for (i; i >= 0; i--) {
    const pos = i + newStart
    const newVNode = newChildren[pos]
    const nextPos = pos + 1
    const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null
    if (source[i] === -1) {
    // 新增子节点
    patch(null, newVNode, container, anchor)
    } else if (i !== seq[s]) {
    // 如果节点的索引i不等于seq[s]的值, 说明节点需要移动
    insert(newVNode.el, container, anchor)
    } else {
    // 不需要移动
    s--
    }
    }
    }

    渲染组件

    1
    2
    3
    4
    5
    6
    7
    8
    判断组件: vnode.type === ‘object’

    function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type
    const { render } = componentOptions
    const subTree = render()
    patch(null, subTree, container, anchor)
    }

    组件状态与自更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type
    const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } =
    componentOptions

    beforeCreate && beforeCreate()

    const state = reactive(data())

    // 定义组件实例, 一个组件实力本质上就是一个对象, 它包含与组件有关的状态信息
    const instance = {
    state,
    isMounted: false,
    subTree: null
    }

    vnode.component = instance

    created && created.call(state)

    effect(
    () => {
    // 组件自身响应式数据发生变化, 组件自动重新执行渲染函数
    const subTree = render.call(state, state)
    if (!instance.isMounted) {
    beforeMount && beforeMount.call(state)
    patch(null, subTree, container, anchor)
    instance.isMounted = true
    mounted && mounted.call(state)
    } else {
    beforeUpdate && beforeUpdate(state)
    patch(instance.subTree, subTree, container, anchor)
    updated && updated(state)
    }
    instance.subTree = subTree
    },
    {
    scheduler: queueJob
    }
    )
    }

    nextTick

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const queue = new Set()

    let isFlushing = false

    const p = Promise.resolve()

    export function queueJob(job) {
    queue.add(job)
    if (!isFlushing) {
    isFlushing = true
    p.then(() => {
    try {
    queue.forEach((job) => job())
    } finally {
    isFlushing = true
    queue.length = 0
    }
    })
    }
    }

    props 与组件的被动更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    <MyComponent title=‘my title’ :other=‘val’ />
    const App = {
    name: 'App',
    type: MyComponent,
    props: {
    title: 'my title',
    other: this.val
    },
    children: []
    }

    为组件传递的props数据, 即组件的vnode.props对象
    组件选项对象定义的props选项, 即MyComponent.props对象

    export function resolveProps(options, propsData) {
    const props = {}
    const attrs = {}

    for (const key in propsData) {
    if (key in options) {
    // 如果传递的props为自检自身定义的props, 则视为合法
    props[key] = propsData[key]
    } else {
    // 否则将其视为attrs
    attrs[key] = propsData[key]
    }
    }
    return [props, attrs]
    }

    props本质是父组件的数据, 当props发生变化时, 会触发父组件的重新渲染
    我们把由父组件更新引起的子组件更新叫做子组件的被动更新

    当子组件被动更新时, 我们需要做
    检测子组件是否真的需要更新, 因为子组件的props可能是不变的
    如果需要更新, 则更新子组件的props, slots等内容
    const instance = (n2.component = n1.component)
    const { props } = instance
    // 调用hasPropsChanged检测子组件传递的props是否发生变化, 如果没有变化, 不更新
    if (hasPropsChanged(n1.props, n2.props)) {
    const [nextProps] = resolveProps(n2.type.props, n2.props)
    for (const k in nextProps) {
    props[k] = nextProps[k]
    }
    for (const k in props) {
    if (!(k in nextProps)) delete props[k]
    }
    }

    代理组件实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const renderContext = new Proxy(instance, {
    get(t, k, r) {
    const { state, props } = t
    if (state && k in state) {
    return state[k]
    } else if (k in props) {
    return props[k]
    } else {
    console.log(`${key} 未定义`)
    }
    },
    set(t, k, v, r) {
    const { state, props } = t
    if (state && k in state) {
    state[k] = v
    } else if (k in props) {
    props[k] = v
    } else {
    console.log(`${key} 未定义`)
    }
    }
    })

    setup 函数的作用与实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    setup函数只会在被挂载的时候执行一次, 它的返回值可以有两种情况
    1. 返回一个函数, 改函数将作为组件的render函数
    2. 返回一个对象, 该对象中包含的数据将暴露给模板使用

    setup接受两个参数
    setup(props, { slots, emit, attrs, expose }) {},

    const setupContext = { attrs }
    const setupResult = setup(shallowReadonly(instance.props), { setupContext })

    let setupState = null
    if (typeof setupResult === 'function') {
    if (render) {
    console.error('setup函数返回渲染函数, render选项将被忽略')
    }
    render = setupResult
    } else {
    setupState = setupResult
    }

    组件事件与 emit 的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <MyComponent @change=“handler” />
    const Comp = {
    type: MyComponent,
    props: {
    onChange: handler
    }
    }

    function emit(event, ...payload) {
    const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
    const handler = instance.props[eventName]
    if (handler) {
    handler(...payload)
    } else {
    console.log('事件不存在')
    }
    }

    插槽的工作原理与实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    当在父组件中使用插槽
    <MyComponent>
    <template #header>
    <h1>Hello</h1>
    </tmeplate>
    </MyComponent>

    function render() {
    return {
    type: MyComponent,
    // 组件的children会被编译成一个对象, $slots => children
    children: {
    // 插槽函数
    header() {
    return { type: ‘h1’, children: ‘hello’ }
    }
    }
    }
    }
    用户可以通过this.$slots访问插槽

    注册生命周期

    1
    2
    3
    currnetInstance
    每当初始化组件并执行组件的setup之前, 先将currentInstance设置为当前组件实例,载执行setup函数
    onMounted函数只能在setup中调用

    异步组件与函数式组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    提供的能力
    允许用户指定加载出错时要渲染得组件
    允许用户指定loading组件, 以及展示该组件的延迟时间
    允许用户设置加载组件的超时时长
    组件加载失败时, 为用户提供重试功能

    AsyncComp: defineAsyncComponent({
    loader: () => import(‘CompA’),
    timeout: 2000,
    errorComponent: ErrorComp
    })

    export function defineAsyncComponent(options) {
    if (typeof options === 'function') {
    options = {
    loader: options
    }
    }
    const { loader } = options
    // 存储异步加载的组件
    let InnerComp = null
    return {
    name: 'AsyncComponentWrapper',
    setup() {
    const loaded = ref(false)
    const timeout = ref(false)
    loader().then((c) => {
    InnerComp = c
    loaded.value = true
    })
    let timer = null
    if (options.timeout) {
    timer = setTimeout(() => {
    timeout.value = true
    }, options.timeout)
    }
    onUnmounted(() => {
    clearTimeout(timer)
    })
    const placeholder = { type: Text, children }
    return () => {
    if (loaded.value) {
    return { type: InnerComp }
    } else if (timeout.value) {
    return options.errorComponent ? { type: options.errorComponent } : placeholder
    }
    return placeholder
    }
    }
    }
    }

    展示loading, 当超过200ms没有完成加载, 才展示loading

    函数式组件

    1
    2
    函数式组件没有自身状态, 可以接收外部传入的props
    无需初始化data以及生命周期

    keepAlive

    1
    2
    keep-alive的本质是缓存管理, 再加上特殊的挂载/卸载逻辑

    模板 DSL 的编译器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    源代码-词法分析-语法分析-语义分析-中间代码生成-优化-目标代码生成

    const template = `<div>
    <h1 v-if="ok">Vue Template</h1>
    </div>`

    const templateAST = parse(template)
    const jsAST = transform(templateAST)
    const code = generate(jsAST)

    tokenzie

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    正则表达式的本质就是有限自动机

    function tokenzie(str) {
    let currentState = State.initial
    const chars = []
    const tokens = []
    while (str) {
    const char = str[0]
    switch (currentState) {
    case State.initial:
    if (char === '<') {
    currentState = State.tagOpen
    str = str.slice(1)
    } else if (isAlpha(char)) {
    currentState = State.text
    chars.push(char)
    str = str.slice(1)
    }
    break

    case State.tagOpen:
    if (isAlpha(char)) {
    currentState = State.tagName
    chars.push(char)
    str = str.slice(1)
    } else if (char === '/') {
    currentState = State.tagEnd
    str = str.slice(1)
    }
    break

    case State.tagName:
    if (isAlpha(char)) {
    chars.push(char)
    str = str.slice(1)
    } else if (char === '>') {
    currentState = State.initial
    tokens.push({
    type: 'tag',
    name: chars.join('')
    })
    chars.length = 0
    str = str.slice(1)
    }
    break

    case State.text:
    if (isAlpha(char)) {
    chars.push(char)
    str = str.slice(1)
    } else if (char === '<') {
    currentState = State.tagOpen
    tokens.push({
    type: 'text',
    content: chars.join('')
    })
    chars.length = 0
    str = str.slice(1)
    }
    break

    case State.tagEnd:
    if (isAlpha(char)) {
    currentState = State.tagEndName
    chars.push(char)
    str = str.slice(1)
    }
    break

    case State.tagEndName:
    if (isAlpha(char)) {
    chars.push(char)
    str = str.slice(1)
    } else if (char === '>') {
    currentState = State.initial
    tokens.push({
    type: 'tagEnd',
    name: chars.join('')
    })
    chars.length = 0
    str = str.slice(1)
    }
    break
    }
    }
    return tokens
    }
    上一篇:
    react+ts仿jira笔记
    下一篇:
    现代js库开发
    本文目录
    本文目录