1. 首页

Vue3 源码分析(3):周边特性

本文首发于我的博客 ahabhgk’s blog

Vue3 中内置组件和一些其他新特性的实现原理,作为上一篇的补充

Fragment

 js
// runtime-core/components/fragment.js
export const Fragment = {
  patch(
    { mountChildren, patchChildren, renderOptions },
    { n1, n2, container, isSVG, anchor }
  ) {
    if (n1 == null) {
      const {
        createText: hostCreateText,
        insert: hostInsert,
      } = renderOptions
      const fragmentStartAnchor = n2.node = hostCreateText('')
      const fragmentEndAnchor = n2.anchor = hostCreateText('')
      hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      mountChildren(n2, container, isSVG, fragmentEndAnchor)
    } else {
      n2.node = n1.node
      n2.anchor = n1.anchor
      patchChildren(n1, n2, container, isSVG)
    }
  },

  getNode(internals, { vnode }) { // 插入到它的前面,需要从头部拿
    return vnode.node
  },

  getNextSibling({ renderOptions }, { vnode }) { // nextSibling 需要从尾部拿
    return renderOptions.nextSibling(vnode.anchor)
  },

  move({ move, renderOptions }, { vnode, container, anchor }) {
    const { insert: hostInsert } = renderOptions
    const fragmentStartAnchor = vnode.node
    const fragmentEndAnchor = vnode.anchor
    hostInsert(fragmentStartAnchor, container, anchor)
    for (let child of vnode.children) {
      move(child, container, anchor)
    }
    hostInsert(fragmentEndAnchor, container, anchor)
  },

  unmount({ unmount, renderOptions }, { vnode, doRemove }) {
    const { remove: hostRemove } = renderOptions
    hostRemove(vnode.node)
    vnode.children.forEach(c => unmount(c, doRemove))
    hostRemove(vnode.anchor)
  },
}
//Js中文网,一个神奇的网站

这五个方法会在哪里调用可以看上一篇,有具体的讲解和代码,Fragment 就是直接将子节点进行渲染,本身可以用两个 placeholder 来标记头部和尾部,因为 Fragment 的 nextSibling 是尾部 placehoder 的 nextSibling,而 getNode 用于插入到 Fragment 前面,所以返回的是 Fragment 的头部 placeholder

Teleport

Teleport 很像 Fragment,唯一的不同就是 Teleport 把子节点渲染到 target 节点上

 js
// runtime-core/components/teleport.js
export const Teleport = {
  patch(
    { renderOptions, mountChildren, patchChildren, move },
    { n1, n2, container, isSVG, anchor },
  ) {
    if (n1 == null) {
      const teleportStartAnchor = n2.node = renderOptions.createText('')
      const teleportEndAnchor = n2.anchor = renderOptions.createText('')
      renderOptions.insert(teleportStartAnchor, container, anchor)
      renderOptions.insert(teleportEndAnchor, container, anchor)
      const target = renderOptions.querySelector(n2.props.to)
      n2.target = target
      mountChildren(n2, target, isSVG, null)
    } else {
      n2.node = n1.node
      n2.anchor = n1.anchor
      n2.target = n1.target
      patchChildren(n1, n2, n2.target, isSVG)

      if (n1.props.to !== n2.props.to) {
        const target = renderOptions.querySelector(n2.props.to)
        n2.target = target
        for (let child of n2.children) {
          move(child, container, null)
        }
      }
    }
  },

  getNode(internals, { vnode }) {
    return vnode.node
  },

  getNextSibling({ renderOptions }, { vnode }) {
    return renderOptions.nextSibling(vnode.anchor)
  },

  move({ renderOptions, move }, { vnode, container, anchor }) {
    const { insert: hostInsert } = renderOptions
    const teleportStartAnchor = vnode.node
    const teleportEndAnchor = vnode.anchor
    hostInsert(teleportStartAnchor, container, anchor)
    hostInsert(teleportEndAnchor, container, anchor)
  },

  unmount({ renderOptions, unmount }, { vnode }) {
    const { remove: hostRemove } = renderOptions
    hostRemove(vnode.node)
    vnode.children.forEach(c => unmount(c))
    hostRemove(vnode.anchor)
  },
}
//Js中文网,一个神奇的网站

不同于 ReactDOM.createProtal 由于 ReactDOM 有一个事件的合成层,可以在这里做一些 hack,使 Portal 的父组件可以捕捉到 Portal 中的事件,Vue3、Preact 由于没有实现事件合成层,所以父组件不能捕捉到 Teleport 中的事件,但相应的减少了很多的代码量,包的体积减小很多

Inject / Provide

直接看实现

// runtime-core/inject.js
import { isFunction } from '../shared'
import { getCurrentInstance } from './component'

export const provide = (key, value) => {
  const currentInstance = getCurrentInstance()
  if (!currentInstance) {
    console.warn(`provide() can only be used inside setup().`)
  } else {
    let { provides } = currentInstance
    const parentProvides = currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    provides[key] = value
  }
}

export const inject = (key, defaultValue) => {
  const currentInstance = getCurrentInstance()
  if (currentInstance) {
    const { provides } = currentInstance
    if (key in provides) {
      return provides[key]
    } else if (arguments.length > 1) { // defaultValue 可以传入 undefined
      return isFunction(defaultValue)
        ? defaultValue()
        : defaultValue
    } else {
      console.warn(`injection "${String(key)}" not found.`)
    }
  } else {
    console.warn(`inject() can only be used inside setup() or functional components.`)
  }
}

可以看出来 provides 是放在 instance 上的,每个 instance 的 provides 都是通过 Object.create 继承 parentInstance 的 provides

provide 调用时就是拿到 currentInstance,然后继承 currentInstance.parent 的 provides,再像上面通过 key 添加属性;inject 就是拿到 currentInstance 的 provides,再通过 key 取值即可,比较巧妙的就是 defaultValue 对于 undefined 的处理

之前我们的 runtime 并没有再 instance 上放 provides 属性,而且怎样去拿 parentInstance,接下来我们修改之前写的 runtime

 js
// runtime-core/renderer.js
const processComponent = (n1, n2, container, isSVG, anchor) => {
  if (n1 == null) {
    const instance = n2.instance = {
      // ...
      parent: null,
      provides: null,
    }
    const parentInstance = instance.parent = getParentInstance(n2)
    instance.provides = parentInstance ? parentInstance.provides : Object.create(null) // 没有 parentInstance 说明是根组件,它的 provides 我们初始化成空对象
  } // ...
}
//Js中文网,一个神奇的网站

 js
// runtime/component.js
export const getParentInstance = (vnode) => {
  let parentVNode = vnode.parent
  while (parentVNode != null) {
    if (parentVNode.instance != null) return parentVNode.instance
    parentVNode = parentVNode.parent
  }
  return null
}
//Js中文网,一个神奇的网站

源码中 parentInstance 是类似于 anchor 作为一个参数一层一层传下来的,之后的 parentSuspense 也是,我们这里尽量简化,通过判断 .parent 链中是否有 instance 进行查找

onErrorCaptured

我们来实现我们唯一没有砍掉的钩子……

 js
// runtime-core/error-handling.js
import { getCurrentInstance } from './component'

export const onErrorCaptured = (errorHandler) => {
  const instance = getCurrentInstance()
  if (instance.errorCapturedHooks == null) { // 这样不用修改 renderer 中的代码了
    instance.errorCapturedHooks = []
  }
  instance.errorCapturedHooks.push(errorHandler)
}

export const callWithErrorHandling = (fn, instance, args = []) => {
  let res
  try {
    res = fn(...args)
  } catch (e) {
    handleError(e, instance)
  }
  return res
}

export const handleError = (error, instance) => {
  if (instance) {
    let cur = instance.parent
    while (cur) {
      const errorCapturedHooks = cur.errorCapturedHooks
      if (errorCapturedHooks) {
        for (let errorHandler of errorCapturedHooks) {
          if (errorHandler(error)) {
            return
          }
        }
      }
      cur = cur.parent
    }
  }
  console.warn('Unhandled error', error)
}
//Js中文网,一个神奇的网站

onErrorCaptured 就是添加错误处理的函数,通过 handleError 来从 instance.parent 中调用这些函数,知道返回 true 为止,而 callWithErrorHandling 是用来触发 handleError 的,我们对于用户可能出错的地方(可能有副作用的地方)调用时包裹一层 callWithErrorHandling 即可

 js
// runtime-core/api-watch.js
export const watchEffect = (cb, { onTrack, onTrigger } = {}) => {
  let cleanup
  const onInvalidate = (fn) => {
    cleanup = e.options.onStop = () => callWithErrorHandling(fn, instance)
  }
  const getter = () => {
    if (cleanup) {
      cleanup()
    }
    return callWithErrorHandling(cb, instance, [onInvalidate])
  }
  // ...
}
//Js中文网,一个神奇的网站

Suspense

Vue3 在更新时遇到 Suspense 是在内存中创建一个 hiddenContianer,在内存中继续渲染 children,渲染 children 时如果遇到 async setup 会隐式的返回一个 Promise,Suspense 通过 register 接收这个 Promise,渲染完 children 后判断是否有接收 Promise,如果没有则把 hiddenContainer 中的 children 移动到 container 中,有则渲染 fallback 作为子节点,之后所有接收到的 Promise 在 resolve 之后再把 hiddenContainer 中的 children 移动到 container 中

在内存中创建 hiddenContainer 去渲染 children 是因为 Suspense 必须要根据是否有接收到 Promise 判断渲染 fallback 还是 children,而 Promise 只来自执行 children 中的 async setup

Suspense 的处理主要分为两部分,一部分是 Suspense 本身的处理,另一部分是对 async setup 子组件的处理,首先来看 Suspense 本身

 js
// runtime-core/components/suspense.js
const createSuspense = (vnode, container, isSVG, anchor, internals, hiddenContainer) => {
  const suspense = {
    deps: [],
    container,
    anchor,
    isSVG,
    hiddenContainer,
    resolve() {
      internals.unmount(vnode.props.fallback)
      internals.move(vnode.props.children, suspense.container, suspense.anchor)
      vnode.node = internals.getNode(vnode.props.children)
    },
    register(instance, setupRenderEffect) {
      // ...
    },
  }
  return suspense
}

export const Suspense = {
  patch(
    internals,
    { n1, n2, container, isSVG, anchor },
  ) {
    if (n1 == null) {
      const hiddenContainer = internals.renderOptions.createElement('div')
      const suspense = n2.suspense = createSuspense(n2, container, isSVG, anchor, internals, hiddenContainer)
      internals.mountChildren(n2, hiddenContainer, isSVG, null)
      internals.patch(null, n2.props.fallback, container, isSVG, anchor)
      n2.node = internals.getNode(n2.props.fallback)
      if (suspense.deps.length === 0) {
        suspense.resolve()
      }
    } else {
      // patchSuspense
    }
  },

  getNode(internals, { vnode }) {
    return vnode.node
  },

  getNextSibling({ renderOptions }, { vnode }) {
    return renderOptions.nextSibling(vnode.node)
  },

  move({ move }, { vnode, container, anchor }) {
    if (vnode.suspense.deps.length) {
      move(vnode.props.fallback, container, anchor)
    } else {
      move(vnode.props.children, container, anchor)
    }
    vnode.suspense.container = container
    vnode.suspense.anchor = anchor
  },

  unmount({ unmount }, { vnode, doRemove }) {
    if (vnode.suspense.deps.length) {
      unmount(vnode.props.fallback, doRemove)
    } else {
      unmount(vnode.props.children, doRemove)
    }
  },
}

export const getParentSuspense = (vnode) => {
  vnode = vnode.parent
  while (vnode) {
    if (vnode.type === Suspense) return vnode.suspense
    vnode = vnode.parent
  }
  return null
}
//Js中文网,一个神奇的网站

我们实现的很简陋,可以看到核心逻辑就是创建一个 hiddenContainer,在这里面渲染 children,然后 createSuspense 创建实例,resolve 的时候就是把 fallback unmount 掉再把 hiddContainer 中的移动到 container 中,move 的时候 container 和 anchor 会改变,会影响 resolve,所以 suspense 实例的属性也要进行修改,这时还有很重要的一部分 patchSuspense,但是跟原理相关性较小,就不写了

 js
// runtime-core/renderer.js
const processComponent = (n1, n2, container, isSVG, anchor) => {
  if (n1 == null) {
    // ...
    if (isPromise(render)) {
      const suspense = getParentSuspense(n2)
      const placeholder = instance.subTree = h(TextType, { nodeValue: '' })
      patch(null, placeholder, container, anchor)
      suspense.register(
        instance,
        () => setupRenderEffect(
          instance,
          internals.renderOptions.parentNode(instance.subTree.node),
          isSVG,
          internals.renderOptions.nextSibling(instance.subTree.node),
        ),
      )
    } else if (isFunction(render)) {
      setupRenderEffect(instance, container, isSVG, anchor)
    } else {
      console.warn('setup component: ', n2.type, ' need to return a render function')
    }

    function setupRenderEffect(instance, container, isSVG, anchor) {
      instance.update = effect(() => { // component update 的入口
        const renderResult = instance.render()
        const vnode = instance.vnode
        vnode.children = [renderResult]
        renderResult.parent = vnode
        patch(instance.subTree, renderResult, container, isSVG, anchor)
        instance.subTree = renderResult
      }, {
        scheduler: queueJob,
      })
    }
  } // ...
}
//Js中文网,一个神奇的网站

接下来对于 async setup 子组件的处理就要修改 runtime 了,我们对 setup 返回结果进行判断,如果是 Promise 就找到 parentSuspense 进行注册,这里我们抽离 setupRenderEffect,注册时传入一个回调函数,用于 suspense resolve 时继续渲染该子组件使用,同时创建一个 placeholder 给组件站位,用以 setupRenderEffect 中获取 container 和 anchor,因为 async setup 组件在没有 resolve 时可能有新的节点插入,如果 container、anchor 还是旧的值时可能会出错(anchor 为 null,但是之后插入了节点,resolve 时 anchor 还是 null 的话就导致节点顺序错误)

 js
// runtime-core/components/suspense.js
const createSuspense = (vnode, container, isSVG, anchor, internals, hiddenContainer) => {
  const suspense = {
    deps: [],
    // ...
    register(instance, setupRenderEffect) {
      suspense.deps.push(instance)
      instance.render
        .catch(e => {
          handleError(e, instance)
        })
        .then(renderFn => {
          instance.render = renderFn
          setupRenderEffect()
          const index = suspense.deps.indexOf(instance)
          suspense.deps.splice(index, 1)
          if (suspense.deps.length === 0) {
            suspense.resolve()
          }
        })
    },
  }
  return suspense
}
//Js中文网,一个神奇的网站

然后 register 就是将 async setup 组件实例加入到 suspense.deps 中,然后等 render resolve 时调用 setupRenderEffect 渲染该组件,并判断是否可以 resolve 了,这里 catch 后 handleError 是因为 async setup 可以执行副作用,可能会出错

defineAsyncComponent

是一个高阶组件,相当于一个增强版的 lazy,当它的上层有 Suspense 时,就返回一个 Promise,否则返回相应状态的组件

 js
// runtime-core/api-define-component.js
export const defineAsyncComponent = (options) => {
  if (isFunction(options)) options = { loader: options }

  const {
    loader,
    errorComponent,
    suspensible = true,
    onError,
  } = options

  let resolvedComponent = null

  let retries = 0
  const retry = () => {
    retries++
    return load()
  }

  const load = () => loader()
    .catch(e => {
      if (onError) {
        return new Promise((resolve, reject) => {
          onError(
            e,
            () => resolve(retry()),
            () => reject(e),
            retries,
          )
        })
      } else {
        throw e
      }
    })
    .then((comp) => {
      if (comp && (comp.__esModule || comp[Symbol.toStringTag] === 'Module')) {
        comp = comp.default
      }
      resolvedComponent = comp
      return comp
    })

  return defineComponent((props) => {
    const instance = getCurrentInstance()
    if (resolvedComponent) return () => h(resolvedComponent, props)
    if (suspensible && getParentSuspense(instance.vnode)) {
      return load()
        .then(comp => {
          return () => h(comp, props)
        })
        .catch(e => {
          handleError(e, instance)
          return () => errorComponent ? h(errorComponent, { error: e }) : null
        })
    }
  })
}
//Js中文网,一个神奇的网站

先来看有 Suspense 的情况,类似于 lazy 的实现,作为一个高阶组件返回 Promise,在出错的时候如果有 onError 就通过 onError 交给用户处理,没有就继续抛出 error,后面 catch 住渲染 errorComponent

再补上没有 Suspense 的情况

// runtime-core/api-define-component.js
export const defineAsyncComponent = (options) => {
  if (isFunction(options)) options = { loader: options }

  const {
    loader,
    loadingComponent,
    errorComponent,
    delay = 200,
    timeout,
    suspensible = true,
    onError,
  } = options
  // ...

  return defineComponent((props) => {
    // ...

    const error = ref()
    const loading = ref(true)
    const delaying = ref(!!delay) // 延后出现 LoadingComponent

    if (delay) {
      setTimeout(() => delaying.value = false, delay)
    }
    if (timeout) {
      setTimeout(() => {
        // 超时
        if (loading.value && !error.value) {
          const err = new Error(`Async component timed out after ${timeout}ms.`)
          handleError(err, instance)
          error.value = err
        }
      }, timeout)
    }

    load()
      .then(() => loading.value = false)
      .catch(e => {
        handleError(e, instance)
        error.value = e
      })

    return () => {
      if (!loading.value && resolvedComponent) return h(resolvedComponent, props)
      // loading.value === true
      else if (error.value && errorComponent) return h(errorComponent, { error: error.value })
      else if (loadingComponent && !delaying.value) return h(loadingComponent)
      return null
    }
  })
}
/*https://www.javascriptc.com Js中文资源网,一个帮助开发者成长的社区*/

这里通过判断 suspensible 为 false 或者没有 parentSuspense 返回 render function,根据相应的状态渲染相应的组件,delay 这个参数的作用是为了 delay 出现 loadingComponent 的,如果加载比较快就不用展示 loading

我们目前写的不能实现的一种情况是 suspensible 为 false 但是有 parentSuspense

const ProfileDetails = defineAsyncComponent({
  loader: () => import('./async.jsx'),
  loadingComponent: defineComponent(() => () => <h1>Loading...</h1>),
  suspensible: false,
});

const App = {
  setup(props) {
    return () => (
      <Suspense fallback={<h1>Loading by Suspense</h1>}>
        <ProfileDetails />
      </Suspense>
    );
  },
};

这是因为 setupRenderEffect 传入的 container、anchor 是不变的,通过闭包存起来了,ProfileDetails 一开始渲染时是在 Suspense 中的,它的 container 是 hiddenContainer,之后渲染也是 hiddenContainer,所以导致页面空白,我们可以把 container、anchor 放到 instance 实例上,让这两个值可以改变,通过 instance 上的 container、anchor 进行渲染

KeepAlive

建立一个 Map 作为缓存,以子节点的 key 或 type 作为缓存的 key(const key = vnode.key == null ? vnode.type : vnode.key);KeepAlive 的 render function 被调用时,也就是 KeepAlive 被渲染时,会根据 props 的 includes 和 excludes 规则判断 children 是否可以被缓存,不可以就直接渲染,可以就在缓存里找,如果缓存里有就用缓存中的进行渲染,children 的状态都是旧的在缓存中的,否则用新的 children 并进行缓存

 js
if (cachedVNode) {
  // copy over mounted state
  vnode.el = cachedVNode.el
  vnode.component = cachedVNode.component
  if (vnode.transition) {
    // recursively update transition hooks on subTree
    setTransitionHooks(vnode, vnode.transition!)
  }
  // avoid vnode being mounted as fresh
  vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
  // make this key the freshest
  keys.delete(key)
  keys.add(key)
} else {
  keys.add(key)
  // prune oldest entry
  if (max && keys.size > parseInt(max as string, 10)) {
    pruneCacheEntry(keys.values().next().value)
  }
}
//Js中文网,一个神奇的网站

源码中 KeepAlive 的缓存用到了 LRU 算法,keys 是一个 Set,可以看到每次使用缓存时会刷新一下缓存,变成新鲜的,如果再来新缓存时,缓存超过了 max,就删去最陈旧的缓存,利用 Set 对 LRU 进行了简易的实现

Transition

Transition 是通过给 DOM 节点在合适时机添加移除 CSS 类名实现的,对于不同平台有不同的实现方法,Transiton 是针对浏览器平台对 BaseTransition 的封装

// DOM Transition is a higher-order-component based on the platform-agnostic
// base Transition component, with DOM-specific logic.
export const Transition: FunctionalComponent<TransitionProps> = (
  props,
  { slots }
) => h(BaseTransition, resolveTransitionProps(props), slots)

export function resolveTransitionProps(
  rawProps: TransitionProps
): BaseTransitionProps<Element> {
  // 拿到对应的 CSS 类名
  let {
    name = 'v',
    type,
    css = true,
    duration,
    enterFromClass = `${name}-enter-from`,
    enterActiveClass = `${name}-enter-active`,
    enterToClass = `${name}-enter-to`,
    appearFromClass = enterFromClass,
    appearActiveClass = enterActiveClass,
    appearToClass = enterToClass,
    leaveFromClass = `${name}-leave-from`,
    leaveActiveClass = `${name}-leave-active`,
    leaveToClass = `${name}-leave-to`
  } = rawProps
  // ...
  // 重写 hooks 回调函数,根据对应的添加或移除 CSS 类名
  return extend(baseProps, {
    onBeforeEnter(el) {
      onBeforeEnter && onBeforeEnter(el)
      addTransitionClass(el, enterActiveClass)
      addTransitionClass(el, enterFromClass)
    },
    onBeforeAppear(el) {
      onBeforeAppear && onBeforeAppear(el)
      addTransitionClass(el, appearActiveClass)
      addTransitionClass(el, appearFromClass)
    },
    // ...
  } as BaseTransitionProps<Element>)
}
/*https://www.javascriptc.com Js中文资源网,一个帮助开发者成长的社区*/

BaseTransition 做的就是从 props 传入的 hooks 通过 resolveTransitionHooks 进一步进行封装,封装成针对 diff 阶段各个时机进行调用的 hooks(beforeEnter、enter、leave、afterLeave、delayLeave、clone),setTransitionHooks 就是把这些 hooks 放到 vnode 上,以便在 diff 过程中进行调用

const BaseTransitionImpl = {
  name: `BaseTransition`,

  props: {
    // ...
  },

  setup(props: BaseTransitionProps, { slots }: SetupContext) {
    const instance = getCurrentInstance()!
    const state = useTransitionState()
    // ...

    return () => {
      const children =
        slots.default && getTransitionRawChildren(slots.default(), true)
      // ...

      // at this point children has a guaranteed length of 1.
      const child = children[0]
      // ...

      // in the case of <transition><keep-alive/></transition>, we need to
      // compare the type of the kept-alive children.
      const innerChild = getKeepAliveChild(child)
      if (!innerChild) {
        return emptyPlaceholder(child)
      }

      const enterHooks = resolveTransitionHooks(
        innerChild,
        rawProps,
        state,
        instance
      )
      setTransitionHooks(innerChild, enterHooks)

      const oldChild = instance.subTree
      const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
      // ...
      if (
        oldInnerChild &&
        oldInnerChild.type !== Comment &&
        (!isSameVNodeType(innerChild, oldInnerChild) || transitionKeyChanged)
      ) {
        const leavingHooks = resolveTransitionHooks(
          oldInnerChild,
          rawProps,
          state,
          instance
        )
        // update old tree's hooks in case of dynamic transition
        setTransitionHooks(oldInnerChild, leavingHooks)
        // ...
      }

      return child
    }
  }
}
/*https://www.javascriptc.com Js中文资源网,一个帮助开发者成长的社区*/

调用的时机就是有关 vnode 节点位置改变的时候,分别是 mount、move 和 unmount。mount 时就调用 BeforeEnter,并注册 enter 到 post 任务队列中;unmount 时就调用 leave 和 afterLeave,并注册 delayLeave 到 post 任务队列中;move 根据 moveType 的不同调用的也不同,比如 Suspense 中 resolve 时是把 children 从 hiddContainer 移到 container 中,相当于 mount,KeepAlive 的 activate 相当于 mount,deactivate 相当于 unmount

Ref

ref(指 runtime 的 ref)是用来拿到宿主环境的节点实例或者组件实例的

// runtime-core/renderer.js
const setRef = (ref, oldRef, vnode) => {
  // unset old ref
  if (oldRef != null && oldRef !== ref) {
    if (isRef(oldRef)) oldRef.value = null
  }
  // set new ref
  const value = getRefValue(vnode)
  if (isRef(ref)) {
    ref.value = value
  } else if (isFunction(ref)) {
    callWithErrorHandling(ref, getParentInstance(vnode), [value])
  } else {
    console.warn('Invalid ref type:', value, `(${typeof value})`)
  }
}

const getRefValue = (vnode) => {
  const { type } = vnode
  if (isSetupComponent(type)) return vnode.instance
  if (isString(type) || isTextType(type)) return vnode.node
  return type.getRefValue(internals, { vnode })
}

ref 的更新由于传入的 ref(指响应式 ref 用来接收实例)可能不同(<img ref={num % 2 ? imgRef1 : imgRef2} />),所以要先清空 oldRef,再赋值 newRef

 js
// runtime-core/renderer.js
const patch = (n1, n2, container, isSVG, anchor = null) => {
  // ...
  if (n2.ref != null) {
    setRef(n2.ref, n1?.ref ?? null, n2)
  }
}

const unmount = (vnode, doRemove = true) => {
  const { type, ref } = vnode
  if (ref != null) {
    setRef(ref, null, vnode)
  }
  // ...
}
//Js中文网,一个神奇的网站

ref 的更新主要在两个地方,一个是在 patch 之后,也就是更新 DOM 节点或组件实例之后,保证拿到最新的值,另一个是在 unmount 移除节点之前

Complier 优化

JS中文网 – 前端进阶资源教程 www.javascriptC.com
一个致力于帮助开发者用代码改变世界为使命的平台,每天都可以在这里找到技术世界的头条内容

没有比 Vue3 Compiler 优化细节,如何手写高性能渲染函数这篇写的更好的了

这里简单说一下原理

  1. Block Tree 和 PatchFlags

    编译时生成的代码会打上 patchFlags,用来标记动态部分的信息

 ts
    export const enum PatchFlags {
      // Indicates an element with dynamic textContent (children fast path)
      TEXT = 1,

      // Indicates an element with dynamic class binding.
      CLASS = 1 << 1,

      // Indicates an element with dynamic style
      STYLE = 1 << 2,

      // Indicates an element that has non-class/style dynamic props.
      // Can also be on a component that has any dynamic props (includes
      // class/style). when this flag is present, the vnode also has a dynamicProps
      // array that contains the keys of the props that may change so the runtime
      // can diff them faster (without having to worry about removed props)
      PROPS = 1 << 3,

      // Indicates an element with props with dynamic keys. When keys change, a full
      // diff is always needed to remove the old key. This flag is mutually
      // exclusive with CLASS, STYLE and PROPS.
      FULL_PROPS = 1 << 4,

      // Indicates an element with event listeners (which need to be attached during hydration)
      HYDRATE_EVENTS = 1 << 5,

      // Indicates a fragment whose children order doesn't change.
      STABLE_FRAGMENT = 1 << 6,

      // Indicates a fragment with keyed or partially keyed children
      KEYED_FRAGMENT = 1 << 7,

      // Indicates a fragment with unkeyed children.
      UNKEYED_FRAGMENT = 1 << 8,

      // ...
    }
    //Js中文网,一个神奇的网站

创建的 Block 也会有 dynamicProps、dynamicChildren 表示动态的部分,Block 也是一个 VNode,只不过它有这些动态部分的信息

dynamicChildren 中即包含 children 中动态的部分,也包含 children 中的 Block,这样 Block 层层连接形成 Block Tree,在更新的时候只更新动态的那一部分
 ts
    const patchElement = (
      n1: VNode,
      n2: VNode,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      isSVG: boolean,
      optimized: boolean
    ) => {
      const el = (n2.el = n1.el!)
      let { patchFlag, dynamicChildren, dirs } = n2
      // #1426 take the old vnode's patch flag into account since user may clone a
      // compiler-generated vnode, which de-opts to FULL_PROPS
      patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
      const oldProps = n1.props || EMPTY_OBJ
      const newProps = n2.props || EMPTY_OBJ
      // ...

      if (patchFlag > 0) {
        // the presence of a patchFlag means this element's render code was
        // generated by the compiler and can take the fast path.
        // in this path old node and new node are guaranteed to have the same shape
        // (i.e. at the exact same position in the source template)
        if (patchFlag & PatchFlags.FULL_PROPS) {
          // element props contain dynamic keys, full diff needed
          patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
        } else {
          // class
          // this flag is matched when the element has dynamic class bindings.
          if (patchFlag & PatchFlags.CLASS) {
            if (oldProps.class !== newProps.class) {
              hostPatchProp(el, 'class', null, newProps.class, isSVG)
            }
          }

          // style
          // this flag is matched when the element has dynamic style bindings
          if (patchFlag & PatchFlags.STYLE) {
            hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
          }

          // props
          // This flag is matched when the element has dynamic prop/attr bindings
          // other than class and style. The keys of dynamic prop/attrs are saved for
          // faster iteration.
          // Note dynamic keys like :[foo]="bar" will cause this optimization to
          // bail out and go through a full diff because we need to unset the old key
          if (patchFlag & PatchFlags.PROPS) {
            // if the flag is present then dynamicProps must be non-null
            const propsToUpdate = n2.dynamicProps!
            for (let i = 0; i < propsToUpdate.length; i++) {
              const key = propsToUpdate[i]
              const prev = oldProps[key]
              const next = newProps[key]
              if (next !== prev || (hostForcePatchProp && hostForcePatchProp(el, key))) {
                hostPatchProp(el, key, prev, next, isSVG, n1.children as VNode[], parentComponent, parentSuspense, unmountChildren)
              }
            }
          }
        }

        // text
        // This flag is matched when the element has only dynamic text children.
        if (patchFlag & PatchFlags.TEXT) {
          if (n1.children !== n2.children) {
            hostSetElementText(el, n2.children as string)
          }
        }
      } else if (!optimized && dynamicChildren == null) {
        // unoptimized, full diff
        patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
      }

      if (dynamicChildren) {
        patchBlockChildren(n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense)
      } else if (!optimized) {
        // full diff
        patchChildren(n1, n2, el, null, parentComponent, parentSuspense)
      }

      // ...
    }
    //Js中文网,一个神奇的网站

  1. 静态提升

    以下是 Vue 3 Template Explorer 选上 hoistStatic 这个选项后编译出的代码

 html
    <div>
      <p>text</p>
    </div>
    //Js中文网,一个神奇的网站

 js
    import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

    const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "text", -1 /* HOISTED */)

    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createBlock("div", null, [
        _hoisted_1
      ]))
    }
    //Js中文网,一个神奇的网站

可以看到 `<p>text</p>` 生成的是 \_hoisted\_1 变量,在 render 作用域外面,这样每次 render 函数调用是就可以服用 \_hoisted\_1,减少 VNode 创建的性能消耗
  1. 预字符串化
 html
    <div>
      <p>text</p>
      <p>text</p>
      <p>text</p>
      <p>text</p>
      <p>text</p>
      <p>text</p>
      <p>text</p>
      <p>text</p>
      <p>text</p>
      <p>text</p>
    </div>
    //Js中文网,一个神奇的网站

 js
    import { createVNode as _createVNode, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

    const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<p>text</p><p>text</p><p>text</p><p>text</p><p>text</p><p>text</p><p>text</p><p>text</p><p>text</p><p>text</p>", 10)

    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createBlock("div", null, [
        _hoisted_1
      ]))
    }
    //Js中文网,一个神奇的网站

当有大量连续的静态的节点时,相比静态提升,预字符串化会进一步进行优化,通过字符串创建 Static VNode
 ts
    const patch: PatchFn = (
      n1,
      n2,
      container,
      anchor = null,
      parentComponent = null,
      parentSuspense = null,
      isSVG = false,
      optimized = false
    ) => {
      // ...
      const { type, ref, shapeFlag } = n2
      switch (type) {
        // ...
        case Static:
          if (n1 == null) {
            mountStaticNode(n2, container, anchor, isSVG)
          } else if (__DEV__) {
            patchStaticNode(n1, n2, container, isSVG)
          }
          break
        // ...
      }
      // ...
    }

    const mountStaticNode = (n2: VNode, container: RendererElement, anchor: RendererNode | null, isSVG: boolean) => {
      // static nodes are only present when used with compiler-dom/runtime-dom
      // which guarantees presence of hostInsertStaticContent.
      ;[n2.el, n2.anchor] = hostInsertStaticContent!(n2.children as string, container, anchor, isSVG)
    }
    //Js中文网,一个神奇的网站

Static VNode 会在 patch 是直接插入到 container 中,生产环节下不进行更新

预字符串化的好处有**生成代码的体积减少、减少创建 VNode 的开销、减少内存占用**

😃 ramble

Vue3 源码系列结束!

Vue3 目前写的只是它的响应式系统和运行时,还有很大的一个部分 complier,这一部分由于我对编译目前还没有太多的了解,而且对于理解 Vue3 核心原理影响并不大,所以就没有写,以后可能会写一写吧

之后就是 React 的源码了,至于我为什么热衷于看源码,不仅是因为自己的学习习惯,也是因为这些框架的源码相当于前端的“边界”,不仅代表着挑战也代表着我这一技术方向的深度

simple-vue 实现完整代码

作者:ahabhgk
链接:https://juejin.im/post/6881632044963938311

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:Vue3 源码分析(3):周边特性

链接:https://www.javascriptc.com/4130.html

« GitHub 上适合新手的开源项目——Python 篇
JavaScript 中 10 个需要掌握基础的问题»
Flutter 中文教程资源

相关推荐

QR code