Vue3 Compat
— SourceCode, Front End Framework — 3 min read
Table of Contents
序
Vue3 中内置组件和一些其他新特性的实现原理,作为上一篇的补充
Fragment
1export const Fragment = {2 patch(3 { mountChildren, patchChildren, renderOptions },4 { n1, n2, container, isSVG, anchor }5 ) {6 if (n1 == null) {7 const {8 createText: hostCreateText,9 insert: hostInsert,10 } = renderOptions11 const fragmentStartAnchor = n2.node = hostCreateText('')12 const fragmentEndAnchor = n2.anchor = hostCreateText('')13 hostInsert(fragmentStartAnchor, container, anchor)14 hostInsert(fragmentEndAnchor, container, anchor)15 mountChildren(n2, container, isSVG, fragmentEndAnchor)16 } else {17 n2.node = n1.node18 n2.anchor = n1.anchor19 patchChildren(n1, n2, container, isSVG)20 }21 },2223 getNode(internals, { vnode }) { // 插入到它的前面,需要从头部拿24 return vnode.node25 },2627 getNextSibling({ renderOptions }, { vnode }) { // nextSibling 需要从尾部拿28 return renderOptions.nextSibling(vnode.anchor)29 },3031 move({ move, renderOptions }, { vnode, container, anchor }) {32 const { insert: hostInsert } = renderOptions33 const fragmentStartAnchor = vnode.node34 const fragmentEndAnchor = vnode.anchor35 hostInsert(fragmentStartAnchor, container, anchor)36 for (let child of vnode.children) {37 move(child, container, anchor)38 }39 hostInsert(fragmentEndAnchor, container, anchor)40 },4142 unmount({ unmount, renderOptions }, { vnode, doRemove }) {43 const { remove: hostRemove } = renderOptions44 hostRemove(vnode.node)45 vnode.children.forEach(c => unmount(c, doRemove))46 hostRemove(vnode.anchor)47 },48}
这五个方法会在哪里调用可以看上一篇,有具体的讲解和代码,Fragment 就是直接将子节点进行渲染,本身可以用两个 placeholder 来标记头部和尾部,因为 Fragment 的 nextSibling 是尾部 placehoder 的 nextSibling,而 getNode 用于插入到 Fragment 前面,所以返回的是 Fragment 的头部 placeholder
Teleport
Teleport 很像 Fragment,唯一的不同就是 Teleport 把子节点渲染到 target 节点上
1export const Teleport = {2 patch(3 { renderOptions, mountChildren, patchChildren, move },4 { n1, n2, container, isSVG, anchor },5 ) {6 if (n1 == null) {7 const teleportStartAnchor = n2.node = renderOptions.createText('')8 const teleportEndAnchor = n2.anchor = renderOptions.createText('')9 renderOptions.insert(teleportStartAnchor, container, anchor)10 renderOptions.insert(teleportEndAnchor, container, anchor)11 const target = renderOptions.querySelector(n2.props.to)12 n2.target = target13 mountChildren(n2, target, isSVG, null)14 } else {15 n2.node = n1.node16 n2.anchor = n1.anchor17 n2.target = n1.target18 patchChildren(n1, n2, n2.target, isSVG)1920 if (n1.props.to !== n2.props.to) {21 const target = renderOptions.querySelector(n2.props.to)22 n2.target = target23 for (let child of n2.children) {24 move(child, container, null)25 }26 }27 }28 },2930 getNode(internals, { vnode }) {31 return vnode.node32 },3334 getNextSibling({ renderOptions }, { vnode }) {35 return renderOptions.nextSibling(vnode.anchor)36 },3738 move({ renderOptions, move }, { vnode, container, anchor }) {39 const { insert: hostInsert } = renderOptions40 const teleportStartAnchor = vnode.node41 const teleportEndAnchor = vnode.anchor42 hostInsert(teleportStartAnchor, container, anchor)43 hostInsert(teleportEndAnchor, container, anchor)44 },4546 unmount({ renderOptions, unmount }, { vnode }) {47 const { remove: hostRemove } = renderOptions48 hostRemove(vnode.node)49 vnode.children.forEach(c => unmount(c))50 hostRemove(vnode.anchor)51 },52}
不同于 ReactDOM.createProtal 由于 ReactDOM 有一个事件的合成层,可以在这里做一些 hack,使 Portal 的父组件可以捕捉到 Portal 中的事件,Vue3、Preact 由于没有实现事件合成层,所以父组件不能捕捉到 Teleport 中的事件,但相应的减少了很多的代码量,包的体积减小很多
Inject / Provide
直接看实现
1import { isFunction } from '../shared'2import { getCurrentInstance } from './component'34export const provide = (key, value) => {5 const currentInstance = getCurrentInstance()6 if (!currentInstance) {7 console.warn(`provide() can only be used inside setup().`)8 } else {9 let { provides } = currentInstance10 const parentProvides = currentInstance.parent && currentInstance.parent.provides11 if (parentProvides === provides) {12 provides = currentInstance.provides = Object.create(parentProvides)13 }14 provides[key] = value15 }16}1718export const inject = (key, defaultValue) => {19 const currentInstance = getCurrentInstance()20 if (currentInstance) {21 const { provides } = currentInstance22 if (key in provides) {23 return provides[key]24 } else if (arguments.length > 1) { // defaultValue 可以传入 undefined25 return isFunction(defaultValue)26 ? defaultValue()27 : defaultValue28 } else {29 console.warn(`injection "${String(key)}" not found.`)30 }31 } else {32 console.warn(`inject() can only be used inside setup() or functional components.`)33 }34}
可以看出来 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
1const processComponent = (n1, n2, container, isSVG, anchor) => {2 if (n1 == null) {3 const instance = n2.instance = {4 // ...5 parent: null,6 provides: null,7 }8 const parentInstance = instance.parent = getParentInstance(n2)9 instance.provides = parentInstance ? parentInstance.provides : Object.create(null) // 没有 parentInstance 说明是根组件,它的 provides 我们初始化成空对象10 } // ...11}
1export const getParentInstance = (vnode) => {2 let parentVNode = vnode.parent3 while (parentVNode != null) {4 if (parentVNode.instance != null) return parentVNode.instance5 parentVNode = parentVNode.parent6 }7 return null8}
源码中 parentInstance 是类似于 anchor 作为一个参数一层一层传下来的,之后的 parentSuspense 也是,我们这里尽量简化,通过判断 .parent
链中是否有 instance 进行查找
onErrorCaptured
我们来实现我们唯一没有砍掉的钩子……
1import { getCurrentInstance } from './component'23export const onErrorCaptured = (errorHandler) => {4 const instance = getCurrentInstance()5 if (instance.errorCapturedHooks == null) { // 这样不用修改 renderer 中的代码了6 instance.errorCapturedHooks = []7 }8 instance.errorCapturedHooks.push(errorHandler)9}1011export const callWithErrorHandling = (fn, instance, args = []) => {12 let res13 try {14 res = fn(...args)15 } catch (e) {16 handleError(e, instance)17 }18 return res19}2021export const handleError = (error, instance) => {22 if (instance) {23 let cur = instance.parent24 while (cur) {25 const errorCapturedHooks = cur.errorCapturedHooks26 if (errorCapturedHooks) {27 for (let errorHandler of errorCapturedHooks) {28 if (errorHandler(error)) {29 return30 }31 }32 }33 cur = cur.parent34 }35 }36 console.warn('Unhandled error', error)37}
onErrorCaptured 就是添加错误处理的函数,通过 handleError 来从 instance.parent
中调用这些函数,知道返回 true 为止,而 callWithErrorHandling 是用来触发 handleError 的,我们对于用户可能出错的地方(可能有副作用的地方)调用时包裹一层 callWithErrorHandling 即可
1export const watchEffect = (cb, { onTrack, onTrigger } = {}) => {2 let cleanup3 const onInvalidate = (fn) => {4 cleanup = e.options.onStop = () => callWithErrorHandling(fn, instance)5 }6 const getter = () => {7 if (cleanup) {8 cleanup()9 }10 return callWithErrorHandling(cb, instance, [onInvalidate])11 }12 // ...13}
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 本身
1const createSuspense = (vnode, container, isSVG, anchor, internals, hiddenContainer) => {2 const suspense = {3 deps: [],4 container,5 anchor,6 isSVG,7 hiddenContainer,8 resolve() {9 internals.unmount(vnode.props.fallback)10 internals.move(vnode.props.children, suspense.container, suspense.anchor)11 vnode.node = internals.getNode(vnode.props.children)12 },13 register(instance, setupRenderEffect) {14 // ...15 },16 }17 return suspense18}1920export const Suspense = {21 patch(22 internals,23 { n1, n2, container, isSVG, anchor },24 ) {25 if (n1 == null) {26 const hiddenContainer = internals.renderOptions.createElement('div')27 const suspense = n2.suspense = createSuspense(n2, container, isSVG, anchor, internals, hiddenContainer)28 internals.mountChildren(n2, hiddenContainer, isSVG, null)29 internals.patch(null, n2.props.fallback, container, isSVG, anchor)30 n2.node = internals.getNode(n2.props.fallback)31 if (suspense.deps.length === 0) {32 suspense.resolve()33 }34 } else {35 // patchSuspense36 }37 },3839 getNode(internals, { vnode }) {40 return vnode.node41 },4243 getNextSibling({ renderOptions }, { vnode }) {44 return renderOptions.nextSibling(vnode.node)45 },4647 move({ move }, { vnode, container, anchor }) {48 if (vnode.suspense.deps.length) {49 move(vnode.props.fallback, container, anchor)50 } else {51 move(vnode.props.children, container, anchor)52 }53 vnode.suspense.container = container54 vnode.suspense.anchor = anchor55 },5657 unmount({ unmount }, { vnode, doRemove }) {58 if (vnode.suspense.deps.length) {59 unmount(vnode.props.fallback, doRemove)60 } else {61 unmount(vnode.props.children, doRemove)62 }63 },64}6566export const getParentSuspense = (vnode) => {67 vnode = vnode.parent68 while (vnode) {69 if (vnode.type === Suspense) return vnode.suspense70 vnode = vnode.parent71 }72 return null73}
我们实现的很简陋,可以看到核心逻辑就是创建一个 hiddenContainer,在这里面渲染 children,然后 createSuspense 创建实例,resolve 的时候就是把 fallback unmount 掉再把 hiddContainer 中的移动到 container 中,move 的时候 container 和 anchor 会改变,会影响 resolve,所以 suspense 实例的属性也要进行修改,这时还有很重要的一部分 patchSuspense,但是跟原理相关性较小,就不写了
1const processComponent = (n1, n2, container, isSVG, anchor) => {2 if (n1 == null) {3 // ...4 if (isPromise(render)) {5 const suspense = getParentSuspense(n2)6 const placeholder = instance.subTree = h(TextType, { nodeValue: '' })7 patch(null, placeholder, container, anchor)8 suspense.register(9 instance,10 () => setupRenderEffect(11 instance,12 internals.renderOptions.parentNode(instance.subTree.node),13 isSVG,14 internals.renderOptions.nextSibling(instance.subTree.node),15 ),16 )17 } else if (isFunction(render)) {18 setupRenderEffect(instance, container, isSVG, anchor)19 } else {20 console.warn('setup component: ', n2.type, ' need to return a render function')21 }2223 function setupRenderEffect(instance, container, isSVG, anchor) {24 instance.update = effect(() => { // component update 的入口25 const renderResult = instance.render()26 const vnode = instance.vnode27 vnode.children = [renderResult]28 renderResult.parent = vnode29 patch(instance.subTree, renderResult, container, isSVG, anchor)30 instance.subTree = renderResult31 }, {32 scheduler: queueJob,33 })34 }35 } // ...36}
接下来对于 async setup 子组件的处理就要修改 runtime 了,我们对 setup 返回结果进行判断,如果是 Promise 就找到 parentSuspense 进行注册,这里我们抽离 setupRenderEffect,注册时传入一个回调函数,用于 suspense resolve 时继续渲染该子组件使用,同时创建一个 placeholder 给组件站位,用以 setupRenderEffect 中获取 container 和 anchor,因为 async setup 组件在没有 resolve 时可能有新的节点插入,如果 container、anchor 还是旧的值时可能会出错(anchor 为 null,但是之后插入了节点,resolve 时 anchor 还是 null 的话就导致节点顺序错误)
1const createSuspense = (vnode, container, isSVG, anchor, internals, hiddenContainer) => {2 const suspense = {3 deps: [],4 // ...5 register(instance, setupRenderEffect) {6 suspense.deps.push(instance)7 instance.render8 .catch(e => {9 handleError(e, instance)10 })11 .then(renderFn => {12 instance.render = renderFn13 setupRenderEffect()14 const index = suspense.deps.indexOf(instance)15 suspense.deps.splice(index, 1)16 if (suspense.deps.length === 0) {17 suspense.resolve()18 }19 })20 },21 }22 return suspense23}
然后 register 就是将 async setup 组件实例加入到 suspense.deps 中,然后等 render resolve 时调用 setupRenderEffect 渲染该组件,并判断是否可以 resolve 了,这里 catch 后 handleError 是因为 async setup 可以执行副作用,可能会出错
defineAsyncComponent
是一个高阶组件,相当于一个增强版的 lazy,当它的上层有 Suspense 时,就返回一个 Promise,否则返回相应状态的组件
1export const defineAsyncComponent = (options) => {2 if (isFunction(options)) options = { loader: options }34 const {5 loader,6 errorComponent,7 suspensible = true,8 onError,9 } = options1011 let resolvedComponent = null1213 let retries = 014 const retry = () => {15 retries++16 return load()17 }1819 const load = () => loader()20 .catch(e => {21 if (onError) {22 return new Promise((resolve, reject) => {23 onError(24 e,25 () => resolve(retry()),26 () => reject(e),27 retries,28 )29 })30 } else {31 throw e32 }33 })34 .then((comp) => {35 if (comp && (comp.__esModule || comp[Symbol.toStringTag] === 'Module')) {36 comp = comp.default37 }38 resolvedComponent = comp39 return comp40 })4142 return defineComponent((props) => {43 const instance = getCurrentInstance()44 if (resolvedComponent) return () => h(resolvedComponent, props)45 if (suspensible && getParentSuspense(instance.vnode)) {46 return load()47 .then(comp => {48 return () => h(comp, props)49 })50 .catch(e => {51 handleError(e, instance)52 return () => errorComponent ? h(errorComponent, { error: e }) : null53 })54 }55 })56}
先来看有 Suspense 的情况,类似于 lazy 的实现,作为一个高阶组件返回 Promise,在出错的时候如果有 onError 就通过 onError 交给用户处理,没有就继续抛出 error,后面 catch 住渲染 errorComponent
再补上没有 Suspense 的情况
1export const defineAsyncComponent = (options) => {2 if (isFunction(options)) options = { loader: options }34 const {5 loader,6 loadingComponent,7 errorComponent,8 delay = 200,9 timeout,10 suspensible = true,11 onError,12 } = options13 // ...1415 return defineComponent((props) => {16 // ...1718 const error = ref()19 const loading = ref(true)20 const delaying = ref(!!delay) // 延后出现 LoadingComponent2122 if (delay) {23 setTimeout(() => delaying.value = false, delay)24 }25 if (timeout) {26 setTimeout(() => {27 // 超时28 if (loading.value && !error.value) {29 const err = new Error(`Async component timed out after ${timeout}ms.`)30 handleError(err, instance)31 error.value = err32 }33 }, timeout)34 }3536 load()37 .then(() => loading.value = false)38 .catch(e => {39 handleError(e, instance)40 error.value = e41 })4243 return () => {44 if (!loading.value && resolvedComponent) return h(resolvedComponent, props)45 // loading.value === true46 else if (error.value && errorComponent) return h(errorComponent, { error: error.value })47 else if (loadingComponent && !delaying.value) return h(loadingComponent)48 return null49 }50 })51}
这里通过判断 suspensible 为 false 或者没有 parentSuspense 返回 render function,根据相应的状态渲染相应的组件,delay 这个参数的作用是为了 delay 出现 loadingComponent 的,如果加载比较快就不用展示 loading
我们目前写的不能实现的一种情况是 suspensible 为 false 但是有 parentSuspense
1const ProfileDetails = defineAsyncComponent({2 loader: () => import('./async.jsx'),3 loadingComponent: defineComponent(() => () => <h1>Loading...</h1>),4 suspensible: false,5});67const App = {8 setup(props) {9 return () => (10 <Suspense fallback={<h1>Loading by Suspense</h1>}>11 <ProfileDetails />12 </Suspense>13 );14 },15};
这是因为 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 并进行缓存
1if (cachedVNode) {2 // copy over mounted state3 vnode.el = cachedVNode.el4 vnode.component = cachedVNode.component5 if (vnode.transition) {6 // recursively update transition hooks on subTree7 setTransitionHooks(vnode, vnode.transition!)8 }9 // avoid vnode being mounted as fresh10 vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE11 // make this key the freshest12 keys.delete(key)13 keys.add(key)14} else {15 keys.add(key)16 // prune oldest entry17 if (max && keys.size > parseInt(max as string, 10)) {18 pruneCacheEntry(keys.values().next().value)19 }20}
源码中 KeepAlive 的缓存用到了 LRU 算法,keys 是一个 Set,可以看到每次使用缓存时会刷新一下缓存,变成新鲜的,如果再来新缓存时,缓存超过了 max,就删去最陈旧的缓存,利用 Set 对 LRU 进行了简易的实现
Transition
Transition 是通过给 DOM 节点在合适时机添加移除 CSS 类名实现的,对于不同平台有不同的实现方法,Transiton 是针对浏览器平台对 BaseTransition 的封装
1// DOM Transition is a higher-order-component based on the platform-agnostic2// base Transition component, with DOM-specific logic.3export const Transition: FunctionalComponent<TransitionProps> = (4 props,5 { slots }6) => h(BaseTransition, resolveTransitionProps(props), slots)78export function resolveTransitionProps(9 rawProps: TransitionProps10): BaseTransitionProps<Element> {11 // 拿到对应的 CSS 类名12 let {13 name = 'v',14 type,15 css = true,16 duration,17 enterFromClass = `${name}-enter-from`,18 enterActiveClass = `${name}-enter-active`,19 enterToClass = `${name}-enter-to`,20 appearFromClass = enterFromClass,21 appearActiveClass = enterActiveClass,22 appearToClass = enterToClass,23 leaveFromClass = `${name}-leave-from`,24 leaveActiveClass = `${name}-leave-active`,25 leaveToClass = `${name}-leave-to`26 } = rawProps27 // ...28 // 重写 hooks 回调函数,根据对应的添加或移除 CSS 类名29 return extend(baseProps, {30 onBeforeEnter(el) {31 onBeforeEnter && onBeforeEnter(el)32 addTransitionClass(el, enterActiveClass)33 addTransitionClass(el, enterFromClass)34 },35 onBeforeAppear(el) {36 onBeforeAppear && onBeforeAppear(el)37 addTransitionClass(el, appearActiveClass)38 addTransitionClass(el, appearFromClass)39 },40 // ...41 } as BaseTransitionProps<Element>)42}
BaseTransition 做的就是从 props 传入的 hooks 通过 resolveTransitionHooks 进一步进行封装,封装成针对 diff 阶段各个时机进行调用的 hooks(beforeEnter、enter、leave、afterLeave、delayLeave、clone),setTransitionHooks 就是把这些 hooks 放到 vnode 上,以便在 diff 过程中进行调用
1const BaseTransitionImpl = {2 name: `BaseTransition`,34 props: {5 // ...6 },78 setup(props: BaseTransitionProps, { slots }: SetupContext) {9 const instance = getCurrentInstance()!10 const state = useTransitionState()11 // ...1213 return () => {14 const children =15 slots.default && getTransitionRawChildren(slots.default(), true)16 // ...1718 // at this point children has a guaranteed length of 1.19 const child = children[0]20 // ...2122 // in the case of <transition><keep-alive/></transition>, we need to23 // compare the type of the kept-alive children.24 const innerChild = getKeepAliveChild(child)25 if (!innerChild) {26 return emptyPlaceholder(child)27 }2829 const enterHooks = resolveTransitionHooks(30 innerChild,31 rawProps,32 state,33 instance34 )35 setTransitionHooks(innerChild, enterHooks)3637 const oldChild = instance.subTree38 const oldInnerChild = oldChild && getKeepAliveChild(oldChild)39 // ...40 if (41 oldInnerChild &&42 oldInnerChild.type !== Comment &&43 (!isSameVNodeType(innerChild, oldInnerChild) || transitionKeyChanged)44 ) {45 const leavingHooks = resolveTransitionHooks(46 oldInnerChild,47 rawProps,48 state,49 instance50 )51 // update old tree's hooks in case of dynamic transition52 setTransitionHooks(oldInnerChild, leavingHooks)53 // ...54 }5556 return child57 }58 }59}
调用的时机就是有关 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)是用来拿到宿主环境的节点实例或者组件实例的
1const setRef = (ref, oldRef, vnode) => {2 // unset old ref3 if (oldRef != null && oldRef !== ref) {4 if (isRef(oldRef)) oldRef.value = null5 }6 // set new ref7 const value = getRefValue(vnode)8 if (isRef(ref)) {9 ref.value = value10 } else if (isFunction(ref)) {11 callWithErrorHandling(ref, getParentInstance(vnode), [value])12 } else {13 console.warn('Invalid ref type:', value, `(${typeof value})`)14 }15}1617const getRefValue = (vnode) => {18 const { type } = vnode19 if (isSetupComponent(type)) return vnode.instance20 if (isString(type) || isTextType(type)) return vnode.node21 return type.getRefValue(internals, { vnode })22}
ref 的更新由于传入的 ref(指响应式 ref 用来接收实例)可能不同(<img ref={num % 2 ? imgRef1 : imgRef2} />
),所以要先清空 oldRef,再赋值 newRef
1const patch = (n1, n2, container, isSVG, anchor = null) => {2 // ...3 if (n2.ref != null) {4 setRef(n2.ref, n1?.ref ?? null, n2)5 }6}78const unmount = (vnode, doRemove = true) => {9 const { type, ref } = vnode10 if (ref != null) {11 setRef(ref, null, vnode)12 }13 // ...14}
ref 的更新主要在两个地方,一个是在 patch 之后,也就是更新 DOM 节点或组件实例之后,保证拿到最新的值,另一个是在 unmount 移除节点之前
Complier 优化
没有比 Vue3 Compiler 优化细节,如何手写高性能渲染函数这篇写的更好的了
这里简单说一下原理
Block Tree 和 PatchFlags
编译时生成的代码会打上 patchFlags,用来标记动态部分的信息
1export const enum PatchFlags {2 // Indicates an element with dynamic textContent (children fast path)3 TEXT = 1,45 // Indicates an element with dynamic class binding.6 CLASS = 1 << 1,78 // Indicates an element with dynamic style9 STYLE = 1 << 2,1011 // Indicates an element that has non-class/style dynamic props.12 // Can also be on a component that has any dynamic props (includes13 // class/style). when this flag is present, the vnode also has a dynamicProps14 // array that contains the keys of the props that may change so the runtime15 // can diff them faster (without having to worry about removed props)16 PROPS = 1 << 3,1718 // Indicates an element with props with dynamic keys. When keys change, a full19 // diff is always needed to remove the old key. This flag is mutually20 // exclusive with CLASS, STYLE and PROPS.21 FULL_PROPS = 1 << 4,2223 // Indicates an element with event listeners (which need to be attached during hydration)24 HYDRATE_EVENTS = 1 << 5,2526 // Indicates a fragment whose children order doesn't change.27 STABLE_FRAGMENT = 1 << 6,2829 // Indicates a fragment with keyed or partially keyed children30 KEYED_FRAGMENT = 1 << 7,3132 // Indicates a fragment with unkeyed children.33 UNKEYED_FRAGMENT = 1 << 8,3435 // ...36}创建的 Block 也会有 dynamicProps、dynamicChildren 表示动态的部分,Block 也是一个 VNode,只不过它有这些动态部分的信息
dynamicChildren 中即包含 children 中动态的部分,也包含 children 中的 Block,这样 Block 层层连接形成 Block Tree,在更新的时候只更新动态的那一部分
1const patchElement = (2 n1: VNode,3 n2: VNode,4 parentComponent: ComponentInternalInstance | null,5 parentSuspense: SuspenseBoundary | null,6 isSVG: boolean,7 optimized: boolean8) => {9 const el = (n2.el = n1.el!)10 let { patchFlag, dynamicChildren, dirs } = n211 // #1426 take the old vnode's patch flag into account since user may clone a12 // compiler-generated vnode, which de-opts to FULL_PROPS13 patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS14 const oldProps = n1.props || EMPTY_OBJ15 const newProps = n2.props || EMPTY_OBJ16 // ...1718 if (patchFlag > 0) {19 // the presence of a patchFlag means this element's render code was20 // generated by the compiler and can take the fast path.21 // in this path old node and new node are guaranteed to have the same shape22 // (i.e. at the exact same position in the source template)23 if (patchFlag & PatchFlags.FULL_PROPS) {24 // element props contain dynamic keys, full diff needed25 patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)26 } else {27 // class28 // this flag is matched when the element has dynamic class bindings.29 if (patchFlag & PatchFlags.CLASS) {30 if (oldProps.class !== newProps.class) {31 hostPatchProp(el, 'class', null, newProps.class, isSVG)32 }33 }3435 // style36 // this flag is matched when the element has dynamic style bindings37 if (patchFlag & PatchFlags.STYLE) {38 hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)39 }4041 // props42 // This flag is matched when the element has dynamic prop/attr bindings43 // other than class and style. The keys of dynamic prop/attrs are saved for44 // faster iteration.45 // Note dynamic keys like :[foo]="bar" will cause this optimization to46 // bail out and go through a full diff because we need to unset the old key47 if (patchFlag & PatchFlags.PROPS) {48 // if the flag is present then dynamicProps must be non-null49 const propsToUpdate = n2.dynamicProps!50 for (let i = 0; i < propsToUpdate.length; i++) {51 const key = propsToUpdate[i]52 const prev = oldProps[key]53 const next = newProps[key]54 if (next !== prev || (hostForcePatchProp && hostForcePatchProp(el, key))) {55 hostPatchProp(el, key, prev, next, isSVG, n1.children as VNode[], parentComponent, parentSuspense, unmountChildren)56 }57 }58 }59 }6061 // text62 // This flag is matched when the element has only dynamic text children.63 if (patchFlag & PatchFlags.TEXT) {64 if (n1.children !== n2.children) {65 hostSetElementText(el, n2.children as string)66 }67 }68 } else if (!optimized && dynamicChildren == null) {69 // unoptimized, full diff70 patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)71 }7273 if (dynamicChildren) {74 patchBlockChildren(n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense)75 } else if (!optimized) {76 // full diff77 patchChildren(n1, n2, el, null, parentComponent, parentSuspense)78 }7980 // ...81}静态提升
以下是 Vue 3 Template Explorer 选上 hoistStatic 这个选项后编译出的代码
1<div>2 <p>text</p>3</div>1import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"23const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "text", -1 /* HOISTED */)45export function render(_ctx, _cache, $props, $setup, $data, $options) {6 return (_openBlock(), _createBlock("div", null, [7 _hoisted_18 ]))9}可以看到
<p>text</p>
生成的是 _hoisted_1 变量,在 render 作用域外面,这样每次 render 函数调用是就可以服用 _hoisted_1,减少 VNode 创建的性能消耗预字符串化
1<div>2 <p>text</p>3 <p>text</p>4 <p>text</p>5 <p>text</p>6 <p>text</p>7 <p>text</p>8 <p>text</p>9 <p>text</p>10 <p>text</p>11 <p>text</p>12</div>1import { createVNode as _createVNode, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"23const _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)45export function render(_ctx, _cache, $props, $setup, $data, $options) {6 return (_openBlock(), _createBlock("div", null, [7 _hoisted_18 ]))9}当有大量连续的静态的节点时,相比静态提升,预字符串化会进一步进行优化,通过字符串创建 Static VNode
1const patch: PatchFn = (2 n1,3 n2,4 container,5 anchor = null,6 parentComponent = null,7 parentSuspense = null,8 isSVG = false,9 optimized = false10) => {11 // ...12 const { type, ref, shapeFlag } = n213 switch (type) {14 // ...15 case Static:16 if (n1 == null) {17 mountStaticNode(n2, container, anchor, isSVG)18 } else if (__DEV__) {19 patchStaticNode(n1, n2, container, isSVG)20 }21 break22 // ...23 }24 // ...25}2627const mountStaticNode = (n2: VNode, container: RendererElement, anchor: RendererNode | null, isSVG: boolean) => {28 // static nodes are only present when used with compiler-dom/runtime-dom29 // which guarantees presence of hostInsertStaticContent.30 ;[n2.el, n2.anchor] = hostInsertStaticContent!(n2.children as string, container, anchor, isSVG)31}Static VNode 会在 patch 是直接插入到 container 中,生产环节下不进行更新
预字符串化的好处有生成代码的体积减少、减少创建 VNode 的开销、减少内存占用
😃 ramble
Vue3 源码系列结束!
Vue3 目前写的只是它的响应式系统和运行时,还有很大的一个部分 complier,这一部分由于我对编译目前还没有太多的了解,而且对于理解 Vue3 核心原理影响并不大,所以就没有写,以后可能会写一写吧
之后就是 React 的源码了,至于我为什么热衷于看源码,不仅是因为自己的学习习惯,也是因为这些框架的源码相当于前端的“边界”,不仅代表着挑战也代表着我这一技术方向的深度