Vue设计与实现:内建组件和模块

KeepAlive组件

组件的激活与失活

"卸载"一个被 KeepAlive 的组件时,它并不会真的被卸载,而会被移动到一个隐藏容器中。当重新"挂载"该组件时, 它也不会被真的挂载,而会被从隐藏容器中取出,再"放回"原来的容器中

思路:

KeepAlive也是一个对象,用__isKeepAlive来做标识,在setup返回vnode

js 复制代码
const CompVNode = {
  type: KeepAlive,
}

const KeepAlive = {
  // KeepAlive 组件独有的属性,用作标识
  __isKeepAlive: true,
  setup() {
    return () => {}
  }
}

创建缓存对象,key为vnode.type,value为vnode,渲染的时候判断

diff 复制代码
+ const cache = new Map()
const KeepAlive = {
  // KeepAlive 组件独有的属性,用作标识
  __isKeepAlive: true,
  setup(props, { slots }) {
    // 创建一个缓存对象
    // key: vnode.type
    // value: vnode
    return () => {}
  }
}
  1. 挂载组件时判断__isKeepAlive是否为KeepAlive组件,是KeepAlive就在实例上添加 keepAliveCtx 对象
  2. move函数是将组件渲染的内容移动到指定容器中,createElement是在渲染器createRenderer的参数
diff 复制代码
function mountComponent(vnode, container, anchor) {
  // 省略部分代码

  const instance = {
    state,
    props: shallowReactive(props),
    isMounted: false,
    subTree: null,
    slots,
    mounted: [],
+    // 只有 KeepAlive 组件的实例下会有 keepAliveCtx 属性
+    keepAliveCtx: null
  }

+  // 检查当前要挂载的组件是否是 KeepAlive 组件
+  const isKeepAlive = vnode.type.__isKeepAlive
+  if (isKeepAlive) {
+    // 在 KeepAlive 组件实例上添加 keepAliveCtx 对象
+    instance.keepAliveCtx = {
+      // move 函数用来移动一段 vnode
+      move(vnode, container, anchor) {
+        // 本质上是将组件渲染的内容移动到指定容器中,即隐藏容器中
+        insert(vnode.component.subTree.el, container, anchor)
+      },
+      createElement
+    }
+  }

  // 省略部分代码
}
  1. KeepAlive有专属的_deActivate、_activate,分别在挂载、卸载时调用
  2. 卸载时将vnode移到storageContainer,挂载时将vnode插入到anchor的前面
diff 复制代码
const KeepAlive = {
  // KeepAlive 组件独有的属性,用作标识
  __isKeepAlive: true,
  setup(props, { slots }) {
    // 创建一个缓存对象
    // key: vnode.type
    // value: vnode
    const cache = new Map()
+    // 当前 KeepAlive 组件的实例
+    const instance = currentInstance
+    // 对于 KeepAlive 组件来说,它的实例上存在特殊的 keepAliveCtx 对象,该对象由渲染器注入
+    // 该对象会暴露渲染器的一些内部方法,其中 move 函数用来将一段 DOM 移动到另一个容器中
+    const { move, createElement } = instance.keepAliveCtx
+    // 创建隐藏容器
+    const storageContainer = createElement('div')
+    // KeepAlive 组件的实例上会被添加两个内部函数,分别是 _deActivate和 _activate
+    // 这两个函数会在渲染器中被调用
+    instance._deActivate = (vnode) => {
+      move(vnode, storageContainer)
+    }
+    instance._activate = (vnode, container, anchor) => {
+      move(vnode, container, anchor)
+    }
    return () => {}
  }
}
  1. slots.default默认插槽就是要被KeepAlive的组件,如果默认插槽的type不是对象也就说明不是组件就直接渲染,因为非组件的虚拟节点无法被 KeepAlive
  2. 如果默认插槽是组件,就判断组件是否有缓存到cache
  3. 如果组件有缓存,就继承组件实例,在vnode上添加 keptAlive 属性,标记为 true,避免渲染器重新挂载它
  4. 如果组件没有缓存就添加到缓存中
  5. 在组件 vnode 上添加 shouldKeepAlive 属性,并标记为 true,避免渲染器真的将组件卸载,组件的实例也添加到 vnode 上,以便在渲染器中访问
diff 复制代码
const KeepAlive = {
  // KeepAlive 组件独有的属性,用作标识
  __isKeepAlive: true,
  setup(props, { slots }) {
    // 创建一个缓存对象
    // key: vnode.type
    // value: vnode
    const cache = new Map()
    // 当前 KeepAlive 组件的实例
    const instance = currentInstance
    // 对于 KeepAlive 组件来说,它的实例上存在特殊的 keepAliveCtx 对象,该对象由渲染器注入
    // 该对象会暴露渲染器的一些内部方法,其中 move 函数用来将一段 DOM 移动到另一个容器中
    const { move, createElement } = instance.keepAliveCtx
    // 创建隐藏容器
    const storageContainer = createElement('div')
    // KeepAlive 组件的实例上会被添加两个内部函数,分别是 _deActivate和 _activate
    // 这两个函数会在渲染器中被调用
    instance._deActivate = (vnode) => {
      move(vnode, storageContainer)
    }
    instance._activate = (vnode, container, anchor) => {
      move(vnode, container, anchor)
    }
    return () => {
+      // KeepAlive 的默认插槽就是要被 KeepAlive 的组件
+      let rawVNode = slots.default()
+      // 如果不是组件,直接渲染即可,因为非组件的虚拟节点无法被 KeepAlive
+      if (typeof rawVNode.type !== 'object') {
+        return rawVNode
+      }
+      // 在挂载时先获取缓存的组件 vnode
+      const cachedVNode = cache.get(rawVNode.type)
+      if (cachedVNode) {
+        // 如果有缓存的内容,则说明不应该执行挂载,而应该执行激活
+        // 继承组件实例
+        rawVNode.component = cachedVNode.component
+        // 在 vnode 上添加 keptAlive 属性,标记为 true,避免渲染器重新挂载它
+        rawVNode.keptAlive = true
+      } else {
+        // 如果没有缓存,则将其添加到缓存中,这样下次激活组件时就不会执行新的挂载动作了
+        cache.set(rawVNode.type, rawVNode)
+      }
+
+      // 在组件 vnode 上添加 shouldKeepAlive 属性,并标记为 true,避免渲染器真的将组件卸载
+      rawVNode.shouldKeepAlive = true
+      // 将 KeepAlive 组件的实例也添加到 vnode 上,以便在渲染器中访问
+      rawVNode.keepAliveInstance = instance
+
+      // 渲染组件 vnode
+      return rawVNode
    }
  }
}
  1. 在卸载组件时判断vnode.shouldKeepAlive是否为true
  2. 为true就说明是缓存组件不应该真正的卸载,调用_deActivate将组件移到其他元素上
  3. 否则正常卸载元素
diff 复制代码
function unmount(vnode) {
  if (vnode.type === Fragment) {
    vnode.children.forEach(c => unmount(c))
    return
  } else if (typeof vnode.type === 'object') {
+    // vnode.shouldKeepAlive 是一个布尔值,用来标识该组件是否应该被 KeepAlive
+    if (vnode.shouldKeepAlive) {
+      // 对于需要被 KeepAlive 的组件,我们不应该真的卸载它,而应调用该组件的父组件,
+      // 即 KeepAlive 组件的 _deActivate 函数使其失活
+      vnode.keepAliveInstance._deActivate(vnode)
+    } else {
+      unmount(vnode.component.subTree)
+    }
+    return
  }
  const parent = vnode.el.parentNode
  if (parent) {
    parent.removeChild(vnode.el)
  }
}
  1. 在挂载组件时判断keptAlive是否为true
  2. 为true说明已经KeepAlive不需要重新挂载,调用activate来激活它
diff 复制代码
function patch(oldN, newN, container, anchor) {
  if (oldN && oldN.type !== newN.type) {
    unmount(oldN)
    oldN = null
  }
  const { type } = newN
  if (typeof type === 'string') {
    // 省略部分代码
  } else if (type === Text) {
    // 省略部分代码
  } else if (type === Fragment) {
    // 省略部分代码
  } else if (typeof type === 'object') {
    // component
    if (!oldN) {
+      // 如果该组件已经被 KeepAlive,则不会重新挂载它,而是会调用_activate 来激活它
+      if (newN.keptAlive) {
+        newN.keepAliveInstance._activate(newN, container, anchor)
+      } else {
        mountComponent(newN, container, anchor)
      }
    } else {
      patchComponent(oldN, newN, anchor)
    }
  }
}

结果:

js 复制代码
const MyComponent = {
  name: 'MyComponent',
  props: {
    title: String
  },
  setup(props, { emit, slots }) {

    const counter = ref(0)

    return () => {
      return {
        type: 'button',
        props: {
          onClick() {
            counter.value++
          }
        },
        children: `count is ${counter.value}`
      }
    }
  }
}

const CompVNode = {
  type: KeepAlive,
  children: {
    default() {
      return { type: MyComponent }
    }
  }
}

renderer.render(CompVNode, document.querySelector('#app'))
setTimeout(() => {
  renderer.render(null, document.querySelector('#app'))
}, 1000);

卸载组件时成功保存了组件

js 复制代码
renderer.render(CompVNode, document.querySelector('#app'))
setTimeout(() => {
  renderer.render(null, document.querySelector('#app'))
}, 1000);
setTimeout(() => {
  renderer.render(CompVNode, document.querySelector('#app'))
}, 2000);

挂载组件时,向cache拿缓存组件

include 和 exclude

KeepAlive 组件支持两个 props,分别是 include 和 exclude。其中,include 用来显式地配置应该被缓存组件,而 exclude 用来显式地配置不应该被缓存组件

js 复制代码
const KeepAlive = {
  __isKeepAlive: true,
  // 定义 include 和 exclude
  // 为了简化问题,我们只允许为 include 和 exclude 设置正则类 型的值
  props: {
    include: RegExp,
    exclude: RegExp
  },
  setup(props, { slots }) {
    // 省略部分代码
  }
}

思路:

  1. 通过setup的props参数拿到include、exclude
  2. 根据include、exclude判断组件的name属性,如果 name 无法被 include 匹配或者被 exclude 匹配就直接渲染组件不缓存
diff 复制代码
const KeepAlive = {
  __isKeepAlive: true,
  props: {
    include: RegExp,
    exclude: RegExp
  },
  setup(props, { slots }) {
    // 省略部分代码
    return () => {
      let rawVNode = slots.default()
      if (typeof rawVNode.type !== 'object') {
        return rawVNode
      }
+      // 获取"内部组件"的 name
+      const name = rawVNode.type.name
+      // 对 name 进行匹配
+      if (
+        name &&
+        (
+          // 如果 name 无法被 include 匹配
+          (props.include && !props.include.test(name)) ||
+          // 或者被 exclude 匹配
+          (props.exclude && props.exclude.test(name))
+        )
+      ) {
        // 则直接渲染"内部组件",不对其进行后续的缓存操作
        return rawVNode
      }

      // 省略部分代码
    }
  }
}

结果:

exclude将以My开头的组件name排除不进行缓存

js 复制代码
const CompVNode = {
  type: KeepAlive,
  props: {
    exclude: /^My/
  },
  children: {
    default() {
      return { type: MyComponent }
    }
  }
}

renderer.render(CompVNode, document.querySelector('#app'))
setTimeout(() => {
  renderer.render(null, document.querySelector('#app'))
}, 1000);

include将以My开头的组件name排除进行缓存

js 复制代码
const CompVNode = {
  type: KeepAlive,
  props: {
    include: /^My/
  },
  children: {
    default() {
      return { type: MyComponent }
    }
  }
}

renderer.render(CompVNode, document.querySelector('#app'))
setTimeout(() => {
  renderer.render(null, document.querySelector('#app'))
}, 1000);
setTimeout(() => {
  renderer.render(CompVNode, document.querySelector('#app'))
}, 2000);

Teleport组件

Teleport 包裹的元素虽然是属于 app.vue 组件,但是渲染过后它却被渲染在了 body 这个 dom 元素下面了

html 复制代码
<template>
  <div class="app">
    App组件
    <Teleport to="body">
      <div>我是被 teleport 包裹的元素</div>
    </Teleport>
  </div>
</template>

思路:

Teleport组件有__isTeleport 和 process,__isTeleport是Teleport组件标识,process是处理渲染逻辑函数

js 复制代码
const Teleport = {
  __isTeleport: true,
  process(oldN, newN, container, anchor) {
    // 在这里处理渲染逻辑
  }
}

在patch函数中判断Telepor组件时,将参数传递给process,将渲染逻辑分离

diff 复制代码
function patch(oldN, newN, container, anchor) {
  if (oldN && oldN.type !== newN.type) {
    unmount(oldN)
    oldN = null
  }
  const { type } = newN
  if (typeof type === 'string') {
    // 省略部分代码
  } else if (type === Text) {
    // 省略部分代码
  } else if (type === Fragment) {
    // 省略部分代码
+  } else if (typeof type === 'object' && type.__isTeleport) {
+    // 组件选项中如果存在 __isTeleport 标识,则它是 Teleport 组件,
+    // 调用 Teleport 组件选项中的 process 函数将控制权交接出去
+    // 传递给 process 函数的第五个参数是渲染器的一些内部方法
+    type.process(oldN, newN, container, anchor, {
+      patch,
+      patchChildren,
+      unmount,
+      move(vnode, container, anchor) {
+        insert(vnode.component ? vnode.component.subTree.el : vnode.el, container, anchor)
+      }
+    })
+  }
}
  1. 当oldN不存在时说明是新挂载
  2. 判断父组件传props的to是否是字符串类型,是字符串就通过document.querySelector获取到to对应的真实dom节点,否则就以to为真实dom节点
  3. 因为Teleport组件是包裹其他组件的,所以children是vnode数组,所以需要遍历调用patch进行挂载
diff 复制代码
const Teleport = {
  __isTeleport: true,
  process(oldN, newN, container, anchor, internals) {
+    // 通过 internals 参数取得渲染器的内部方法
+    const { patch } = internals
+    // 如果旧 VNode oldN 不存在,则是全新的挂载,否则执行更新
+    if (!oldN) {
+      // 挂载
+      // 获取容器,即挂载点
+      const target = typeof newN.props.to === 'string'
+        ? document.querySelector(newN.props.to)
+        : newN.props.to
+      // 将 newN.children 渲染到指定挂载点即可
+      newN.children.forEach(c => patch(null, c, target, anchor))
+    } else {
+      // 更新
+    }
  }
}
  1. oldN存在就说明是更新,调用patchChildren比较
  2. 更新存在新旧的to参数不同,如果新旧to参数不同,将newN.children遍历调用move移动到新的容器
diff 复制代码
const Teleport = {
  __isTeleport: true,
  process(oldN, newN, container, anchor, internals) {
    const { patch, patchChildren, move } = internals
    if (!oldN) {
      // 省略部分代码
    } else {
+      // 更新
+      patchChildren(oldN, newN, container)
+      // 如果新旧 to 参数的值不同,则需要对内容进行移动
+      if (newN.props.to !== oldN.props.to) {
+        // 获取新的容器
+        const newTarget = typeof newN.props.to === 'string'
+          ? document.querySelector(newN.props.to)
+          : newN.props.to
+        // 移动到新的容器
+        newN.children.forEach(c => move(c, newTarget))
+      }
+    }
  }
}

结果:

js 复制代码
const CompVNode = {
  type: Teleport,
  props: {
    to: 'body'
  },
  children: [
    { type: 'h1', children: 'Header' },
    { type: 'p', children: 'content' }
  ]
}


renderer.render(CompVNode, document.querySelector('#app'))
js 复制代码
const CompVNode = {
  type: Teleport,
  props: {
    to: 'body'
  },
  children: [
    { type: 'h1', children: 'Header' },
    { type: 'p', children: 'content' }
  ]
}


renderer.render(CompVNode, document.querySelector('#app'))

const CompVNode2 = {
  type: Teleport,
  props: {
    to: '#test'
  },
  children: [
    { type: 'h1', children: 'A big Title' },
    { type: 'p', children: 'a small content' }
  ]
}

setTimeout(() => {
  renderer.render(CompVNode2, document.querySelector('#app'))
}, 1000);

Transition组件

  1. 当 DOM 元素被挂载时,将动效附加到该 DOM 元素上;
  2. 当 DOM 元素被卸载时,不要立即卸载 DOM 元素,而是等到附加 到该 DOM 元素上的动效执行完成后再卸载它。
html 复制代码
<template>
  <Transition>
   <div>我是需要过渡的元素</div>
  </Transition>
</template>

Transition组件对应的vnode,Transition组件的子节点被编译为默认插槽

js 复制代码
function render() {
  return {
    type: Transition,
    children: {
      default() {
        return { type: 'div', children: '我是需要过渡的元素' }
      }
    }
  }
}

思路:

  1. Transition组件不会渲染额外内容,只是通过默认插槽(slots.default())读取过渡元素,并渲染需要过渡的元素
  2. 在过渡元素的虚拟节点上添加transition相关的钩子函数,在挂载、卸载元素时调用
js 复制代码
const Transition = {
  name: 'Transition',
  setup(props, { slots }) {
    return () => {
      // 通过默认插槽获取需要过渡的元素
      const innerVNode = slots.default()
      // 在过渡元素的 VNode 对象上添加 transition 相应的钩子函数
      innerVNode.transition = {
        beforeEnter(el) {
          // 省略部分代码
        },
        enter(el) {
          // 省略部分代码
        },
        leave(el, performRemove) {
          // 省略部分代码
        }
      }
      // 渲染需要过渡的元素
      return innerVNode
    }
  }
}

在挂载元素前执行beforeEnter,在挂载元素后执行enter

diff 复制代码
  function mountElement(vnode, container, anchor) {
    const el = vnode.el = createElement(vnode.type)
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children)
    } else if (Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }
    if (vnode.props) {
      for (const key in vnode.props) {
        patchProps(el, key, null, vnode.props[key])
      }
    }
-    insert(el, container, anchor)
+    // 判断一个 VNode 是否需要过渡
+    const needTransition = vnode.transition
+    if (needTransition) {
+      // 调用 transition.beforeEnter 钩子,并将 DOM 元素作为参数传递
+      vnode.transition.beforeEnter(el)
+    }
+    insert(el, container, anchor)
+    if (needTransition) {
+      // 调用 transition.enter 钩子,并将 DOM 元素作为参数传递
+      vnode.transition.enter(el)
+    }
  }

如果元素需要过渡处理,那么就需要等待leave执行完后再卸载,否则直接卸载

diff 复制代码
function unmount(vnode) {
  // 判断 VNode 是否需要过渡处理
  const needTransition = vnode.transition
  if (vnode.type === Fragment) {
    vnode.children.forEach(c => unmount(c))
    return
  } else if (typeof vnode.type === 'object') {
    if (vnode.shouldKeepAlive) {
      vnode.keepAliveInstance._deActivate(vnode)
    } else {
      unmount(vnode.component.subTree)
    }
    return
  }
  const parent = vnode.el.parentNode
+  if (parent) {
+    // 将卸载动作封装到 performRemove 函数中
+    const performRemove = () => parent.removeChild(vnode.el)
+    if (needTransition) {
+      // 如果需要过渡处理,则调用 transition.leave 钩子,
+      // 同时将 DOM 元素和 performRemove 函数作为参数传递
+      vnode.transition.leave(vnode.el, performRemove)
+    } else {
+      // 如果不需要过渡处理,则直接执行卸载操作
+      performRemove()
+    }
+  }
}
  1. 在挂载前添加enter-from和enter-active类
  2. 在挂载后移除enter-from类,添加enter-to类,使元素移动到最左边
  3. 在卸载时添加leave-from和leave-active类,在下一帧移除leave-from 类,添加leave-to类,使元素向右移动200px,过渡效果执行完最终执行卸载元素
js 复制代码
const Transition = {
  name: 'Transition',
  setup(props, { slots }) {
    return () => {
      const innerVNode = slots.default()
      innerVNode.transition = {
        beforeEnter(el) {
          // 设置初始状态:添加 enter-from 和 enter-active 类
          el.classList.add('enter-from')
          el.classList.add('enter-active')
        },
        enter(el) {
          // 在下一帧切换到结束状态
          nextFrame(() => {
            // 移除 enter-from 类,添加 enter-to 类
            el.classList.remove('enter-from')
            el.classList.add('enter-to')
            // 监听 transitionend 事件完成收尾工作
            el.addEventListener('transitionend', () => {
              el.classList.remove('enter-to')
              el.classList.remove('enter-active')
            })
          })
        },
        leave(el, performRemove) {
          // 设置离场过渡的初始状态:添加 leave-from 和 leave-active类
          el.classList.add('leave-from')
          el.classList.add('leave-active')
          // 在下一帧修改状态
          nextFrame(() => {
            // 移除 leave-from 类,添加 leave-to 类
            el.classList.remove('leave-from')
            el.classList.add('leave-to')
            // 监听 transitionend 事件完成收尾工作
            el.addEventListener('transitionend', () => {
              el.classList.remove('leave-to')
              el.classList.remove('leave-active')
              // 调用 transition.leave 钩子函数的第二个参数,完成 DOM元素的卸载
              performRemove()
            })
          })
        }
      }
      return innerVNode
    }
  }
}
js 复制代码
function nextFrame(cb) {
  requestAnimationFrame(() => {
    requestAnimationFrame(cb)
  })
}
css 复制代码
 .box {
  width: 100px;
  height: 100px;
  background-color: red;
 }

.enter-from {
  transform: translateX(200px);
}

 .enter-to {
  transform: translateX(0);
 }

 .enter-active {
  transition: transform 1s ease-in-out;
 }

/* 初始状态 */
.leave-from {
  transform: translateX(0);
}
/* 结束状态 */
.leave-to {
  transform: translateX(200px);
}
/* 过渡过程 */
.leave-active {
  transition: transform 2s ease-out;
}

结果:

js 复制代码
const App = {
  name: 'App',
  setup() {
    const toggle = ref(true)

    setTimeout(() => {
      toggle.value = false
    }, 2000);

    return () => {
      return {
        type: Transition,
        children: {
          default() {
            return toggle.value ? { type: 'div', props: { class: 'box' } } : { type: Text, chidlren: '' }
          }
        }
      }
    }
  }
}
renderer.render({ type: App }, document.querySelector('#app'))
相关推荐
qq_3901617710 分钟前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test39 分钟前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫1 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.1 小时前
Chrome调试工具(查看CSS属性)
前端·chrome
栈老师不回家2 小时前
Vue 计算属性和监听器
前端·javascript·vue.js