Skip to main content

8、组建的渲染和更新

组件的挂载流程

组件需要提供一个 render 函数,渲染函数需要返回虚拟 DOM

const VueComponent = {
data() {
return { age: 13 }
},
render() {
return h("p", [h(Text, "I'm Jiang sir"), h("span", this.age)])
},
}
render(h(VueComponent), document.getElementById("app"))

添加组件类型

h 方法中传入一个对象说明要渲染的是一个组件。(后续还有其他可能)

export const createVNode = (type, props, children = null) => {
const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : 0
// ... 稍后可以根据类型来进行组件的挂载
}

组件的渲染

const patch = (n1, n2, container, anchor?) => {
// 初始化和diff算法都在这里喲
if (n1 == n2) {
return
}
if (n1 && !isSameVNodeType(n1, n2)) {
// 有n1 是n1和n2不是同一个节点
unmount(n1)
n1 = null
}
const { type, shapeFlag } = n2
switch (type) {
// ...
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor)
}
}
}
const mountComponent = (n2, container, anchor) => {
const { render, data = () => ({}) } = n2.type
const state = reactive(data())
const instance = {
state, // 组件的状态
isMounted: false, // 组件是否挂载
subTree: null, // 子树
update: null,
vnode: n2,
}
const componentUpdateFn = () => {
if (!instance.isMounted) {
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 = new ReactiveEffect(componentUpdateFn, () => update())
const update = (instance.update = () => effect.run())
update()
}
const processComponent = (n1, n2, container, anchor) => {
if (n1 == null) {
mountComponent(n2, container, anchor)
} else {
// 组件更新逻辑
}
}

组件异步渲染

修改调度方法,将更新方法压入到队列中

const effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update))
const update = (instance.update = () => effect.run())

批处理操作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
resolvedPromise.then(() => {
isFlushing = false
let copy = queue.slice(0)
queue.length = 0 // 这里要先清空,防止在执行过程中在加入新的job
for (let i = 0; i < copy.length; i++) {
let job = copy[i]
job()
}
copy.length = 0
})
}
}

组件 Props、Attrs 实现

PropsAttrs关系是:没有定义在component.props中的属性将存储到attrs对象中

import { render, h, Text, Fragment } from "./runtime-dom.js"

const VueComponent = {
data() {
return { name: "zs", age: 30 }
},
props: {
address: String,
},
render() {
return h("p", [
h(Text, `${this.name}今年${this.age}岁了`),
h(Text, `${this.address}`),
h(Text, `${this.$attrs.a}${this.$attrs.b}`),
])
},
}
render(h(VueComponent, { address: "霍营", a: 1, b: 2 }), app)

initProps

const mountComponent = (vnode, container, anchor) => {
let { data = () => ({}), render, props: propsOptions = {} } = vnode.type // 这个就是用户写的内容
const state = reactive(data()) // pinia 源码就是 reactive({}) 作为组件的状态
const instance = {
// 组件的实例
state,
vnode, // vue2的源码中组件的虚拟节点叫$vnode 渲染的内容叫_vnode
subTree: null, // vnode组件的虚拟节点 subTree渲染的组件内容
isMounted: false,
update: null,
propsOptions,
attrs: {},
props: {},
}
vnode.component = instance
initProps(instance, vnode.props)
}
componentProps.ts
export function initProps(instance, rawProps) {
const props = {}
const attrs = {}
const options = instance.propsOptions || {} // 获取组件用户的配置
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
}

属性代理

shared/index.ts

const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (val, key) => hasOwnProperty.call(val, key)
const publicPropertiesMap = {
$attrs: (i) => i.attrs,
}

const mountComponent = (vnode, container, anchor) => {
// ...
const instance = {
// 组件的实例
// ...
proxy: null,
}
vnode.component = instance
initProps(instance, vnode.props)
instance.proxy = new Proxy(instance, {
get(target, key) {
const { data, props } = target
if (data && hasOwn(data, key)) {
return data[key]
} else if (hasOwn(props, key)) {
return props[key]
}
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 = (vnode, container, anchor) => {
// 1) 创建实例
const instance = (vnode.component = createComponentInstance(vnode))
// 2) 给实例赋值
setupComponent(instance)
// 3) 创建渲染effect及更新
setupRenderEffect(instance, container, anchor)
}

1)创建组件实例#

component.ts
export function createComponentInstance(vnode) {
const instance = {
// 组件的实例
data: null,
vnode, // vue2的源码中组件的虚拟节点叫$vnode 渲染的内容叫_vnode
subTree: null, // vnode组件的虚拟节点 subTree渲染的组件内容
isMounted: false,
update: null,
attrs: {},
props: {},
proxy: null,
propsOptions: vnode.type.props,
}
return instance
}

2)设置组件属性

const publicPropertiesMap = {
$attrs: (i) => i.attrs,
}
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]
}
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
},
}
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
}

3)渲染 effect

const setupRenderEffect = (instance, container, anchor) => {
const { render } = instance
const componentUpdateFn = () => {
// 区分是初始化 还是要更新
if (!instance.isMounted) {
// 初始化
const subTree = render.call(instance.proxy, instance.proxy) // 作为this,后续this会改
patch(null, subTree, container, anchor) // 创造了subTree的真实节点并且插入了
instance.subTree = subTree
instance.isMounted = true
} else {
// 组件内部更新
const subTree = render.call(instance.proxy, instance.proxy)
patch(instance.subTree, subTree, container, anchor)
instance.subTree = subTree
}
}
// 组件的异步更新
const effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update))
// 我们将组件强制更新的逻辑保存到了组件的实例上,后续可以使用
const update = (instance.update = () => effect.run())
update()
}

属性更新

const My = {
props: { address: String },
render() {
return h("div", this.address)
},
}
const VueComponent = {
data() {
return { name: "zs", age: 30, flag: false }
},
render() {
return h(Fragment, [
h("button", { onClick: () => (this.flag = !this.flag) }, "切换渲染"),
h(My, { address: this.flag ? "霍营" : "回龙观" }),
])
},
}
render(h(VueComponent), app)
const updateComponent = (n1, n2) => {
const instance = (n2.component = n1.component)
const { props: prevProps } = n1
const { props: nextProps } = n2
updateProps(instance, prevProps, nextProps)
}
const processComponent = (n1, n2, container, anchor) => {
if (n1 == null) {
mountComponent(n2, container, anchor)
} else {
// 组件更新逻辑
updateComponent(n1, n2)
}
}
props.ts
export const hasPropsChanged = (prevProps = {}, nextProps = {}) => {
const nextKeys = Object.keys(nextProps)
if (nextKeys.length !== Object.keys(prevProps).length) {
return true
}
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
if (nextProps[key] !== prevProps[key]) {
return true
}
}
return false
}
export function updateProps(instance, prevProps, nextProps) {
if (hasPropsChanged(prevProps, nextProps)) {
// 比较前后属性是否一致
for (const key in nextProps) {
// 循环props
instance.props[key] = nextProps[key] // 响应式属性更新后会重新渲染
}
for (const key in instance.props) {
// 循环props
if (!(key in nextProps)) {
delete instance.props[key]
}
}
}
}

这里我们将更新逻辑放到componentFn中,因为除了属性更新之外,插槽也会导致页面更新

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 // 将新的虚拟节点放到next属性上
instance.update() // 属性变化手动调用更新方法
}
}
export function updateProps(prevProps, nextProps) {
for (const key in nextProps) {
// 循环props
prevProps[key] = nextProps[key] // 响应式属性更新后会重新渲染
}
for (const key in prevProps) {
// 循环props
if (!(key in nextProps)) {
delete prevProps[key]
}
}
}
function updateComponentPreRender(instance, next) {
instance.next = null
instance.vnode = next
updateProps(instance, instance.props, next.props)
}
const componentUpdateFn = () => {
if (!instance.isMounted) {
// ...
} else {
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
}
}