除 VNode 之外,Vue3 中还有很多其他类型的虚拟节点,如:Text, Fragment
export const Text = Symbol.for('v-text')
export const Fragment = Symbol.for('v-fgt') as any as {
__isFragment: true,
new(): {
$props: any
}
}
const patch = (n1, n2, container, anchor = null) => {
// 初始化 & diff 算法
// 1. 同一个 VNode 不用处理
if (n1 === n2) return
// 2. 两元素不同,卸载老的 VNode
if (n1 && !isSameVNodeType(n1, n2)) {
unmount(n1)
n1 = null
}
const { type, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Fragment:
processFragment(n1, n2, container, anchor)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor)
} else if (shapeFlag & ShapeFlags.COMPOENNT) {
processComponent(n1, n2, container, anchor)
}
}
}
render(h(Text, 'Hello'), document.getElementById('app'))
const processText = (n1, n2, container, anchor) => {
if (n1 === null) {
hostInsert(
n2.el = hostCreateText(n2.children as string),
container,
anchor)
} else {
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
为了让 Vue3 支持多根节点模板,Vue.js 提供 Fragment 来实现,核心就是一个无意义的标签包裹多个节点。
render(h(Fragment, [h('span', 'Hello'), h('span', ' Stella')]), document.getElementById('app'))
const processFragment = (n1,n2,container)=>{
if(n1 == null){
mountChildren(n2.children,container);
}else{
patchChildren(n1,n2,container);
}
}
卸载元素时,特殊处理下 fragment,删除其子元素
const unmount = vnode => {
if (vnode.type === Fragment) {
unmountChildren(vnode.children)
} else {
hostRemove(vnode.el)
}
}
const unmountChildren = children => {
for (let i = 0; i < children.length; i++) {
unmount(children[i])
}
}
组件需要提供一个 render 函数,渲染函数需要返回虚拟 DOM
方法列表
- h => 传入 VueComponent 对象
- data: function(){ return {...} }
- render: function(): vnode{ return h() },可接收 proxy 代理对象
- createVNode => type 参数为对象,ShapeFlags.STATEFUL_COMPONENT,返回 vnode
- render
- patch => shapeFlag 为 COMPONENT 类型
- processComponent
- mountComponent
- updateComponent
- createComponentInstance
- createAppContext
- setupRenderEffect
- updateComponentPreRender
const VueComponent = {
data() {
return { name: 'Stella' }
},
render(proxy) {
// 源码中,调用 render.call(proxy, proxy)
// proxy === this
return h('p', [h('div'), `Hello, I'm ${this.name}.`, h('div', `I love coding and dancing.`)])
},
}
render(h(VueComponent), app)
添加组件类型 h 方法中传入一个对象说明要渲染的是一个组件。(后续还有其他可能)
export const createVNode = (type, props, children = null) => {
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: 0
// ... 稍后可以根据类型来进行组件的挂载
}
组件渲染
const processComponent = (n1, n2, container, anchor) => {
if (n1 == null) {
mountComponent(n2, container, anchor)
} else {
updateComponent(n1, n2)
}
}
const mountComponent = (initialVNode, container, anchor) => {
const instance = (initialVNode.component = createComponentInstance(initialVNode))
setupRenderEffect(instance, initialVNode, container, anchor)
}
const setupRenderEffect = (instance, initialVNode, container, anchor) => {
const { render, data = () => { } } = initialVNode.type
const state = reactive(data())
const componentUpdateFn = () => {
if (!instance.isMounted) {
// render() { return h() }
const subTree = render.call(state, state)
patch(null, subTree, container, anchor)
instance.subTree = subTree
instance.isMounted = true
} else {
const subTree = render.call(state, state)
patch(instance.subTree, subTree, container, anchor)
instance.subTree = subTree
}
}
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))
const update = instance.update = effect.run.bind(effect)
update()
}
组件异步渲染 setupRenderEffect 中修改调度方法,将更新方法压入到队列中,批量处理
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update)
))
const update = instance.update = effect.run.bind(effect)
批量处理 scheduler.ts
const queue = []
let isFlushing = false
const resolvedPromise = Promise.resolve()
// 防止一更新数据就触发更新,批量处理
export function queueJob(job) {
if (!queue.includes(job)) {
// 一个组件依赖的多个数据时,多数据更新,任务队列中只会压入一次
queue.push(job)
}
// 数据更新,可能会出现多个组件更新,采用队列存储
if (!isFlushing) {
isFlushing = true
// promise 微任务,等当前同步任务执行完成后,再执行
resolvedPromise.then(() => {
isFlushing = false
const copy = queue.slice()
// 这里要先清空,防止在执行过程中在加入新的job,直到当前job都执行完,再开始新一轮任务
queue.length = 0
copy.forEach(job)
copy.length = 0 // 执行完,清空副本
})
}
}
//类似浏览器的事件环:(同步任务, 微任务, 宏任务)
// 一轮一轮,执行过程中还会新增新的任务,先缓存入队列中
组件Props、Attrs 实现
Props
和 Attrs
关系是:没有定义在 component.props
中的属性将存储到 attrs
对象中
const VueComponent = {
data() {
return { name: 'Stella', age: 18 }
},
props: {
address: String
},
render() {
return h('p', [h('div', `Hello, I'm ${this.name}, ${this.age} years old.`), h('div', `props & attrs: ${this.address}, ${this.$attrs.a}, ${this.$attrs.b}`)])
},
}
render(h(VueComponent, { address: 'Wuhan', a: 1, b: 1 }), app)
initProps
const mountComponent = (initialVNode, container, anchor) => {
// 创建实例
const instance = (initialVNode.component = createComponentInstance(initialVNode))
// 给实例赋值,data => reactive
setupComponent(instance)
// 创建 renderEffect 并更新渲染
setupRenderEffect(instance, container, anchor)
}
componentProps.ts
export function initProps(
instance,
rawProps
) {
const props = {}
const attrs = {}
// 获取组件 用户的配置
const options = instance.propsOptions || {} // VueComponent.type.props
if (rawProps) {
for (let key in rawProps) {
const value = rawProps[key]
if (key in options) {
props[key] = value
} else {
attrs[key] = value
}
}
}
instance.props = reactive(props); // 这里应该用shallowReactive,遵循单向数据流原则
instance.attrs = attrs
}
属性代理
export const PublicInstanceProxyHandlers = {
get(target, key) {
const { data, props } = target
if (data && hasOwn(data, key)) {
return data[key]
} else if (hasOwn(props, key)) {
return props[key]
}
// $attrs
const publicGetter = publicPropertiesMap[key]
if (publicGetter) {
return publicGetter(target)
}
},
set(target, key, value) {
const { data, props } = target
if (data && hasOwn(data, key)) {
data[key] = value
return true
} else if (hasOwn(props, key)) {
console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
return false
}
return true
}
}
组件流程整合
const mountComponent = (initialVNode, container, anchor) => {
// 创建实例
const instance = (initialVNode.component = createComponentInstance(initialVNode))
// 给实例赋值,data => reactive
setupComponent(instance)
// 创建 renderEffect 并更新渲染
setupRenderEffect(instance, container, anchor)
}
1)创建组件实例
export function createAppContext() {
return {
app: null,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {}
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null),
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap()
}
}
const emptyAppContext = createAppContext()
let uid = 0
export type Data = Record<string, unknown>
export function createComponentInstance(vnode) {
const type = vnode.type
const appContext = emptyAppContext
const instance = {
uid: uid++,
vnode, // 组件的虚拟节点
type,
appContext,
isMounted: false,
subTree: null!, // 要渲染的子节点
update: null!, // creation 后同步设置
// state
data: EMPTY_OBJ,
props: EMPTY_OBJ, // 父组件传入的 props
attrs: EMPTY_OBJ, // 子组件没有定义 props,会放入 $attrs中
proxy: null, // 代理对象
propsOptions: vnode.type.props // VueComponent.type 为一个对象
}
return instance
}
2)设置组件属性
export function setupComponent(instance) {
const { props, type } = instance.vnode
initProps(instance, props)
instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers)
const data = type.data
if (data) {
if (!isFunction(data)) return console.warn(`The data option must be a function`)
instance.data = reactive(data.call(instance.proxy))
}
instance.render = type.render
}
export const PublicInstanceProxyHandlers = {
get(target, key) {
const { data, props } = target
if (data && hasOwn(data, key)) {
return data[key]
} else if (hasOwn(props, key)) {
return props[key]
}
// $attrs
const publicGetter = publicPropertiesMap[key]
if (publicGetter) {
return publicGetter(target)
}
},
set(target, key, value) {
const { data, props } = target
if (data && hasOwn(data, key)) {
data[key] = value
return true
} else if (hasOwn(props, key)) {
console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
return false
}
return true
}
3)渲染effect
const setupRenderEffect = (instance, container, anchor) => {
const { render } = instance
const componentUpdateFn = () => {
if (!instance.isMounted) {
// render() { return h() }
// 返回的就是 vnode
const subTree = render.call(instance.proxy, instance.proxy)
patch(null, subTree, container, anchor)
// 方便再次挂载时,前后 vnode 进行比对
instance.subTree = subTree
// 挂载后修改 isMounted 值
instance.isMounted = true
} else {
// 用户传入的 render 方法可以接收 reactive 返回的代理对象
const subTree = render.call(instance.proxy, instance.proxy)
patch(instance.subTree, subTree, container, anchor)
instance.subTree = subTree
}
}
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update) // 控制 componentUpdateFn 执行时机,可以批处理
))
const update = instance.update = effect.run.bind(effect)
update()
}
属性更新
const cm1 = {
props: {
address: String
},
render() {
// this.address 取值时会收集依赖
// 当传入的 props 更新时,会触发 renderEffect 重新执行
return h('div', this.address)
}
}
const VueComponent = {
data() {
return { name: 'Stella', age: 18, flag: false }
},
render() {
return h(Fragment,
[h('button', { onClick: () => {
console.log(this.flag)
this.flag = !this.flag
} }, 'toggle render'),
h(cm1, { address: this.flag ? 'Wuhan' : 'Beijing' })]
)
},
}
render(h(VueComponent), app)
export const PublicInstanceProxyHandlers = {
get(target, key) {
const { data, props } = target
if (data && hasOwn(data, key)) {
return data[key]
} else if (hasOwn(props, key)) {
return props[key]
}
// $attrs
const publicGetter = publicPropertiesMap[key]
if (publicGetter) {
return publicGetter(target)
}
},
set(target, key, value) {
const { data, props } = target
if (data && hasOwn(data, key)) {
data[key] = value
return true
} else if (hasOwn(props, key)) {
// prop.key如果是子组件自己修改,则修改不成功,不会触发渲染
// updateComponent父组件修改数据,render 重新渲染
// 子组件拿到的新 prop发生变化,手动通过 instance.props.key=newValue
// 触发子组件重新渲染,取最新 prop 值
console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
return false
}
return true
}
}
- mountComponent
- createComponentInstance
- createAppContext
- setupComponent
- initProps -> reactive component.type.props
- setupRenderEffect -> ReactiveEffect -> mount vnode -> collect dep
- componentUpdateFn
- queueJob -> scheduler -> batchUpdate
- updateComponent
- updateProps -> instance.props.key=newVal -> updateComponentFn
属性更新逻辑抽离出来,slots 更新也会导致页面更新
const shouldUpdateComponent = (n1, n2) => {
const { props: prevProps, children: prevChildren } = n1
const { props: nextProps, children: nextChildren } = n2
if (prevChildren || nextChildren) return true
if (prevProps === nextProps) return false
return hasPropsChanged(prevProps, nextProps)
}
const updateComponent = (n1, n2) => {
const instance = (n2.component = n1.component)
if (shouldUpdateComponent(n1, n2)) {
instance.next = n2
instance.update()
}
}
function updateProps(prevProps, nextProps) {
if (hasPropsChanged(prevProps, nextProps)) {
for (const key in nextProps) {
// 数据更新,触发
prevProps[key] = nextProps[key]
}
for (const key in prevProps) {
if (!(key in nextProps)) {
delete prevProps[key]
}
}
}
}
function updateComponentPreRender(instance, next) {
instance.next = null
instance.vnode = next
updateProps(instance.props, next.props)
}
const componentUpdateFn = () => {
if (!instance.isMounted) {
// ...
} else {
// updateComponent 属性变化或slots变化,均手动触发 instance.update
let { next } = instance
if (next) {
updateComponentPreRender(instance, next)
}
const subTree = render.call(instance.proxy, instance.proxy)
patch(instance.subTree, subTree, container, anchor)
instance.subTree = subTree
}
}
组件的 render 函数每次更新时都会重新执行,但是 setup 函数只会在组件挂载时执行一次。
- setup 函数是 compositionAPI 的入口
- 可以在函数内部编写逻辑,解决 vue2 中反复横跳问题
- setup 返回函数时为组件的 render 函数,返回对象时对象中的数据将暴露给模板使用
- setup 中函数的参数为 props、context({ slots, emit, attrs, expose })
// 组件的render函数每次更新时都会重新执行
// 但是setup函数只会在组件挂载时执行一次。
const VueComponent = {
props: {
address: String
},
setup(props) {
const name = ref('Stella')
return {
name,
address: props.address
}
},
render() {
return h('div', `${this.name}, ${this.address}`)
},
}
render(h(VueComponent, { address: 'Wuhan' }), app)
对 setup 函数进行解析
function setupComponent(instance) {
const { props, type } = instance.vnode
initProps(instance, props)
// 解析 setup
let { setup } = type
if (setup) {
const setupContext = {}
const setupResult = setup(instance.props, setupContext)
if (isFunction(setupResult)) {
instance.render = setupResult
} else if (isObject(setupResult)) {
instance.setupState = proxyRefs(setupResult)
}
}
instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers)
const data = type.data
if (data) {
if (!isFunction(data)) return console.warn(`The data option must be a function`)
instance.data = reactive(data.call(instance.proxy))
}
if (!instance.render) {
instance.render = type.render
}
}
新增取值范围
const PublicInstanceProxyHandlers = {
get(target, key) {
const { data, props, setupState } = target
if (data && hasOwn(data, key)) {
return data[key]
} else if (hasOwn(props, key)) {
return props[key]
} else if (setupState && hasOwn(setupState, key)) {
return setupState[key]
}
// $attrs
const publicGetter = publicPropertiesMap[key]
if (publicGetter) {
return publicGetter(target)
}
},
set(target, key, value) {
const { data, props, setupState } = target
if (data && hasOwn(data, key)) {
data[key] = value
return true
} else if (hasOwn(props, key)) {
// prop.key如果是子组件自己修改,则修改不成功,不会触发渲染
// updateComponent父组件修改数据,render 重新渲染
// 子组件拿到的新 prop发生变化,手动通过 instance.props.key=newValue
// 触发子组件重新渲染,取最新 prop 值
console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
return false
} else if (setupState && hasOwn(setupState, key)) {
setupState[key] = value
}
return true
}
}
const VueComponent = {
setup(props, ctx) {
const handleClick = () => ctx.emit('tap')
return () => h('button', {
onClick: handleClick
}, 'Click me')
},
}
render(h(VueComponent, { onTap: () => alert('Bonjour!') }), app)
function setupComponent(instance) {
const { props, type } = instance.vnode
initProps(instance, props)
// 解析 setup
let { setup } = type
if (setup) {
const setupContext = {
attrs: instance.attrs,
// 实现 emit
emit: (event, ...args) => {
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
const handler = instance.vnode.props[eventName]
handler && handler(...args)
}
}
const setupResult = setup(instance.props, setupContext)
if (isFunction(setupResult)) {
instance.render = setupResult
} else if (isObject(setupResult)) {
instance.setupState = proxyRefs(setupResult)
}
}
instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers)
const data = type.data
if (data) {
if (!isFunction(data)) return console.warn(`The data option must be a function`)
instance.data = reactive(data.call(instance.proxy))
}
if (!instance.render) {
instance.render = type.render
}
}
const Component= {
render() {
return h(Fragment, [
h('div', [this.$slots.header()]),
h('div', [this.$slots.body()]),
h('div', [this.$slots.footer()])
])
}
}
const VueComponent = {
setup() {
return () =>h(Component, null, {
header: () => h('p', 'Header'),
body: () => h('p', 'Body'),
footer: () => h('p', 'Footer'),
})
},
}
render(h(VueComponent), app)
export const createVNode = (type, props, children = null) => {
if (children) {
let type = 0
if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN
} else if (isObject(children)) {// slots
type = ShapeFlags.SLOTS_CHILDREN
} else {
children = String(children)
type = ShapeFlags.TEXT_CHILDREN
}
vnode.shapeFlag |= type // 见1是1
}
return vnode
}
export const publicPropertiesMap = {
$attrs: i => i.attrs,
$slots: i => i.slots
}
function initSlots(instance, children) {
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
instance.slots = children
}
}
export function setupComponent(instance) {
const { props, type, children } = instance.vnode
initProps(instance, props)
initSlots(instance, children)
}
生命周期需要让当前实例关联对应的生命周期,这样在组件构建过程中就可以调用对应的钩子
component.ts
export const setCurrentInstance = instance => (currentInstance = instance)
export const getCurrentInstance = () => currentInstance
export const unsetCurrentInstance = (val = null) => currentInstance = null
setCurrentInstance(instance)
const setupResult = setup(instance.props, setupContext)
unsetCurrentInstance(null)
apiLifecycle.ts
import { currentInstance, setCurrentInstance } from "./component";
export const enum LifecycleHooks {
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u'
}
function createHook(type) {
return (hook, target = currentInstance) => {
if (target) {
const hooks = target[type] || (target[type] = [])
const wrappedHook = () => {
setCurrentInstance(target)
hook.call(target)
setCurrentInstance(null)
}
hooks.push(wrappedHook)
}
}
}
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const updated = createHook(LifecycleHooks.UPDATED)
const componentUpdateFn = () => {
if (!instance.isMounted) {
const { bm, m } = instance
bm && invokeArrayFns(bm)
const subTree = render.call(instance.proxy, instance.proxy)
patch(null, subTree, container, anchor)
m && invokeArrayFns(m)
instance.subTree = subTree
instance.isMounted = true
} else {
// updateComponent 属性变化或slots变化,均手动触发 instance.update
// 运行的是当前的 effect,再次触发此 effect。跳过循环操作(activeEffect !== effect)。
let { next, bu, u } = instance
if (next) {
next && updateComponentPreRender(instance, next)
}
bu && invokeArrayFns(bu)
const subTree = render.call(instance.proxy, instance.proxy)
patch(instance.subTree, subTree, container, anchor)
u && invokeArrayFns(u)
instance.subTree = subTree
}
}