5.1 实现 Element 更新的主流程
Element 更新的主流程实现, 之前在实现 reactivity
响应式时, 使用了 effect
监视器,用于监视响应式数据的变化, 进行收集依赖 & 触发依赖 的动作。
在实现 Element
更新时,在setup
函数中使用了 响应式数据,在初始化 Element
中的 patch()
时, 使用effct
进行监控 虚拟节点的变化 。
happy path
导出响应式函数
/* reactivity/index.ts */
export { ref } from "./ref"
实现在 render 模板中 ref 定义的数据,不使用 .value
形式访问 , 在 setup()
的返回值中用 proxyRefs()
包裹
/* component.ts*/
function handlerSetupResult(instance, setupResult) {
if (isObject(setupResult)) {
// 将 setup 的结果 注入 instance
// 使用 proxyRefs 函数包裹,可以 让ref 数据在 render() 中不再使用 .value 方式访问数据
instance.setupState = proxyRefs(setupResult)
}
}
/* update/App.js */
// 实现组件的更新流程
import { h, ref } from "../../lib/guide-mini-vue.esm.js"
export const App = {
name: "App",
setup() {
const count = ref(0)
const onClick = () => {
// 响应式数据发生变化
count.value++
}
return { count, onClick }
},
render() {
return h(
"div",
{},
[
// 这里不再使用 this.count.value
h("div", {}, `count: ${this.count}`),
h("button", { onClick: this.onClick}, "Click me")
]
)
},
}
初始时, 模板展示
使用 effect
监视器,监视 element
模板内容
定义初始化数据
/* component.ts */
export function createComponentInstance(vnode, parent) {
const component = {
// isMounted, 使用 isMounted 判断是否 init 初始化
// 初始为 false
isMounted: false,
// 使用 instance.subTree 存储 subTree, 在更新时候需要用, 需要在 component 初始化 subTree
// 定义 subTree 存储上一次更新的 VNode
subTree: {},
}
}
使用 effect
监视 element变化
/* renderer.ts */
function setupRenderEffect(instance, initialVNode, container) {
// 使用 effect监视模板中响应式数据的变化
effect(() => {
// 然后实现依赖收集 & 触发依赖的实现
// 把 渲染的逻辑 写在这里, 收集依赖
// 使用 isMounted 判断是否 init 初始化
if (!instance.isMounted) {
// 初始化
console.log("init")
const { proxy } = instance
// call render()
// 使用 instance.subTree 存储 subTree, 在更新时候需要用, 需要在 component 初始化 subTree
const subTree = (instance.subTree = instance.render.call(proxy)) // 将proxy 注入到 render() 中
// 初始化 传入 n1, 初始化为 null -> 没有老的虚拟节点
patch(null, subTree, container, instance)
initialVNode.el = subTree.el
// 改变 isMounted 状态
instance.isMounted = true
} else {
// 更新逻辑
console.log("update")
// 实现: 在初始化时 instance 中用一个变量 subTree , 保存当前实例的 subTree 的值
// 在更新时候,获取到 上一个 subTree
const { proxy } = instance
// 拿到更新后的 subTree
const subTree = instance.render.call(proxy)
// 取出 之前保存上一次组件的 subTree
const prevSubTree = instance.subTree
// 实现 patch() 的更新逻辑
// 添加 n1 n2; 老的虚拟节点 & 新的虚拟节点 -- 在这里进行赋值
patch(prevSubTree, subTree, container, instance)
}
})
}
处理初始化 patch()
传入的 老虚拟节点 & 新的虚拟节点 ; n1, n2
/*renderer.ts*/
function render(vnode, container) {
// 调用 path
// 初始化逻辑 n1 -> null
patch(null, vnode, container, null)
}
// patch() 接收新的参数
// n1 是旧的虚拟节点
// n2 是新的虚拟节点
// 如果 n1 不存在 --- 初始化 , n1 存在那就是 更新逻辑
function patch(n1, n2, container, parentComponent) {
const { type, shapeFlag } = n2 // type 组件的类型
switch (type) {
case Fragment:
processFragment(n1, n2, container, parentComponent)
case Text:
processText(n1, n2, container)
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, parentComponent)
} else if (shapeFlag & ShapeFlags.STATIC_COMPONENT) {
processComponent(n1, n2, container, parentComponent)
}
}
}
// 用于处理 Text 文本节点
function processText(n1, n2, container) {
}
// 处理Element
function processElement(n1, n2, container, parentComponent) {
// 判断 n1 是否存在
if (!n1) {
// 如果不存在 表示执行初始化逻辑
// 初始化
mountElement(n2, container, parentComponent)
} else {
// n1 具有数据 , 进行更新逻辑
patchElement(n1, n2, container)
}
}
// 实现 Element 更新的逻辑
function patchElement(n1, n2, container) {
console.log("patchElement")
console.log("n1", n1)
console.log("n2", n2)
}
// 修改
function mountChildren(vnode, container, parentComponent) {
vnode.children.forEach((v) => {
// 组件逻辑, n1 为 null
patch(null, v, container, parentComponent)
})
}
此时已经实现 Element
更新的主流程
5.2 实现 props 的更新
实现 Props 的更新
props的更新要求
1. foo 之前的值 与 之后的值不一样了, 修改 props 属性
2. foo 更新后变为了 null | undefined ; 删除了 foo 属性
3. bar 更新后这个属性在更新后没有了, 删除 bar 属性
测试 props 的App 组件
/* update/updateProps/App.js */
export const App = {
name: "App",
setup() {
// 定义响应式数据
const count = ref(0)
// 定义事件,修改响应式数据
const onClick = () => {
count.value++
}
const props = ref({
foo: "foo",
bar: "bar"
})
// 修改 props
const onChangePropsDemo1 = () => {
props.value.foo = "new-foo"
}
// 修改 bar
const onChangePropsDemo2 = () => {
props.value.foo = undefined
}
// 删除 bar 属性
const onChangePropsDemo3 = () => {
props.value = {
foo: "foo"
}
}
return {
count,
onClick,
props,
onChangePropsDemo1,
onChangePropsDemo2,
onChangePropsDemo3,
}
},
// 渲染函数
render() {
// console.log(this.count)
return h(
"div",
{
id: "root",
// 设置props
...this.props
},
[
h('div', {}, `count: ${this.count}`),
h('button',
{
// 点击事件
onClick: this.onClick
},
'click me'
),
h(
'button',
{ onClick: this.onChangePropsDemo1 },
// 直接修改 props.foo 的值
'foo-值改变了-修改'
),
h(
'button',
{ onClick: this.onChangePropsDemo2 },
'foo值变成了undefined - 删除'
),
h(
'button',
{ onClick: this.onChangePropsDemo3 },
'bar - 删除'
)
]
)
}
}
实现 Props
更新的逻辑
/* renderer.ts */
// 实现 Element 更新的逻辑
function patchElement(n1, n2, container) {
console.log("patchElement")
console.log("n1", n1)
console.log("n2", n2)
// 执行Props的更新
// 取到 n1 和 n2 的 props , 如果 props 为空 , 赋值为 EMPTY_OBJ 空对象
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
// 取到 n1的el, 赋值给 n2.el
const el = (n2.el = n1.el)
// 定义 更新 patchProps 的函数
patchProps(el, oldProps, newProps)
}
// 实现 patchProps 更新Props的函数
function patchProps(el, oldProps, newProps) {
/**
* 实现 Props 的更新
*
* props的更新要求
* 1. foo 之前的值 与 之后的值不一样了, 修改 props 属性
* 2. foo 更新后变为了 null | undefined ; 删除了 foo 属性
* 3. bar 更新后这个属性在更新后没有了, 删除 bar 属性
*/
// 优化1. 当 oldProps 与 newProps 不相同时,需要更新
if (oldProps !== newProps) {
// 实现1. 循环 newProps 与 oldProps 做对比
for (const key in newProps) {
// 取到 oldProps 的 props
const prevProp = oldProps[key]
// 取到 newProps 的 props
const nextProp = newProps[key]
// 判断 二者props是否一样
if (prevProp !== nextProp) {
// 不等时, 进行更新逻辑
// 调用 hostPatchProp() 接口 , 因为就是在 hostPatchProp 这里实现props的赋值的
hostPatchProp(el, key, prevProp, nextProp)
}
}
// 优化2: 如果 oldProps 为空,不用进行对比
if (oldProps !== EMPTY_OBJ) {
// 实现3. bar 更新后这个属性在更新后没有了, 删除 bar 属性
// 遍历 oldProps
for (const key in oldProps) {
// 如果 key 不在 newProps 中
if (!(key in newProps)) {
// 删除这个 key
// 调用 hostPatchProp() 接口
hostPatchProp(el, key, oldProps[key], null)
}
}
}
}
}
修改一些重构代码
/* runtime-dom/index.ts */
// 给Element 添加属性
function patchProp(el, key, prevProp, nextVal) { // 添加 prevProps 属性
const isOn = () => /^on[A-Z]/.test(key)
if (isOn()) {
// 这里实现注册事件
el.addEventListener(key.slice(2).toLowerCase(), nextVal)
} else {
// 实现2: foo 更新后变为了 null | undefined ; 删除了 foo 属性
// 新增判断: 如果 nextVal 为 undefined || null 时
if (nextVal === undefined || nextVal === null) {
// 执行删除
el.removeAttribute(key)
} else {
// 赋值属性
el.setAttribute(key, nextVal)
}
}
}
/* shared/index.ts */
// 导出使用同一个 EMPTY_OBJ 空对象
export const EMPTY_OBJ = {}
实现
5.3 实现 children 的更新
实现 Element
中的 chlidren
的更新逻辑
测试组件 , 都存入updateChilren
文件夹
Array -> Text
/* App.js */
// 实现 Element - Children 更新流程
import { h } from "../../../lib/guide-mini-vue.esm.js"
import ArrayToText from "./ArrayToText.js"
import TextToText from "./TextToText.js"
import TextToArray from "./TextToArray.js"
import ArrayToArray from "./ArrayToArray.js"
export const App = {
name: "App",
setup() {
},
render() {
return h("div", { tTd: 1 },
[
h('p', {}, "主页"),
// 1. 老的是 Array , 新的是Text
h(ArrayToText),
// 老的是 Text , 新的是 Text
// h(TextToText),
// 老的是 Text , 新的是 Array
// h(TextToArray),
// 老的是 Array , 新的是 Array
// h(ArrayToArray)
])
}
}
/* example/update/patchChildren/ArrayToText.js */
// 新的节点 -> Text
const nextChildren = "newChildren"
// 老的节点 -> Array
const prevChildren = [
h('div', {}, "A"),
h("div", {}, "B")
]
export default {
name: "ArrayToText",
setup() {
// 定义响应式数据
const isChange = ref(false)
// 全局变量
window.isChange = isChange
return {
isChange
}
},
render() {
const self = this
// 判断响应式数据变化
return self.isChange === true
? h('div', {}, nextChildren) // 展示新的节点
: h("div", {}, prevChildren) // 展示旧的节点
},
}
具体实现
/**
* 实现 Children 的更新逻辑
*
* 1. 先判断当前节点 和 老的节点 的 children 的状态,看看他是 text 还是 Array
* 2. 实现更新内容, 基于 shapeFlags 判断 children 的类型,
* 2.1 如果是 Array -> Text 节点
* - 实现: 1. 先清空掉数组, 2. 设置文本
* 2.2 如果是 Text -> Text
* - 实现: 判断老的Text 和 新的 Text 是否相同,如果不同,则更新文本
* 2.3 如果是 Text -> Array
* - 实现:1. 把Text清空,让后获取到Array的children , 重新进行mountChildren 渲染
*/
/* renderer.ts */
// 实现 Element 更新的逻辑
function patchElement(n1, n2, container) {
/* 其他代码 */
// 定义 更新 patchChildren 的函数
patchChildren(n1, n2, el)
}
// 实现 Children 的更新
function patchChildren(n1, n2, el) { // 传入 el -> 当前标签的父元素 父标签
// 1. 获取 n1 和 n2 的 children 的状态
// 新节点的状态 是 Text 还是 Array
const { shapeFlag } = n2
// 老节点的状态 是 Text 还是 Array
const prevShapeFlag = n1.shapeFlag
// 获取新老节点的 children
const c1 = n1.children
const c2 = n2.children
// 2. 判断 n2 的 shapeFlag 新节点的 ShapeFlag
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 如果新的节点 是 Text 节点
// 如果老的节点 是 Array 节点
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// Array -> Text
// 清空老节点的 Array
unmountChildren(c1)
// 设置新节点的文本
// 传入 div -> 也就是 老的节点的 el , 和新的节点的 children
// 写在runtime-dom 的接口上 -> 与 Element 渲染一致
hostSetElementText(el, c2)
}
} else {
// 新的节点 是 Array 节点, 因为只有这两种状态
}
}
// 清空 children -> Array
function unmountChildren(children) {
// 遍历 children
for (let i = 0; i < children.length; i++) {
// 获取每一个 children 的 el
// 也就是它的父标签 div
const child = children[i].el
// 执行删除的逻辑
// 写在runtime-dom 的接口上 -> 与 Element 渲染一致
// 传入 获取到的 el 标签
hostRemove(child)
}
}
在runtime-dom
实现删除Array
和 添加 Text
的接口方法
/* runtime-dom/index.ts */
// 执行删除 children 的逻辑
function remove(el) {
// 1. 拿到 child 父级的 节点 -> div = el.parentNode
const parent = el.parentNode
// 判断是否存在
if (parent) {
// 执行删除, 使用el的父节点删除 el
parent.removeChild(el)
}
}
// SetElementText
// 实现添加文本节点的接口
function setElementText(el, text) {
el.textContent = text
}
const renderer: any = createRender({
createElement,
patchProp,
insert,
// 导出
remove,
setElementText
})
使用接口方法
/* render.ts */
const {
createElement: hostCreateElement,
patchProp: hostPatchProp,
insert: hostInsert,
remove: hostRemove,
setElementText: hostSetElementText
} = options
Text -> Text
/* TextToText.js */
// 新的是 text
// 老的是 text
const prevChildren = "oldChild";
const nextChildren = "newChild";
export default {
name: "TextToText",
setup() {
/* 其他代码 */
},
render() {
const self = this;
return self.isChange === true
? h("div", {}, nextChildren)
: h("div", {}, prevChildren);
},
};
实现 Text -> Text
的逻辑
/* renderer.ts */
// 实现 Children 的更新
function patchChildren(n1, n2, el) { // 传入 el -> 当前标签的父元素 父标签
// 1. 获取 n1 和 n2 的 children 的状态
// 新节点的状态 是 Text 还是 Array
const { shapeFlag } = n2
// 老节点的状态 是 Text 还是 Array
const prevShapeFlag = n1.shapeFlag
// 获取新老节点的 children
const c1 = n1.children
const c2 = n2.children
// 2. 判断n2 的 shapeFlag
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 如果新的节点 是 Text 节点
// 如果老的节点 是 Array 节点
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 清空老节点的 Array
unmountChildren(c1)
// 设置新节点的文本
// 传入 div -> 也就是 老的节点的 el , 和新的节点的 children
// 写在runtime-dom 的接口上 -> 与 Element 渲染一致
hostSetElementText(el, c2)
} else {
// 如果老的节点 是 Text 节点
// Text -> Text
// 当两个节点不相等时才取更新
if (c1 !== c2) {
// 更新文本
hostSetElementText(el, c2)
}
}
} else {
// 新的节点 是 Array 节点, 因为只有这两种状态
}
}
实现设置文本代码重构
/* renderer.ts */
// 实现 Children 的更新
function patchChildren(n1, n2, el) {
const { shapeFlag } = n2
const prevShapeFlag = n1.shapeFlag
const c1 = n1.children
const c2 = n2.children
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 如果老的节点 是 Array 节点
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 清空老节点的 Array
unmountChildren(c1)
}
// 重构 因为以上代码使用了两次 hostSetElementText
// 设置新节点的文本
// 传入 div -> 也就是 老的节点的 el , 和新的节点的 children
// 写在runtime-dom 的接口上 -> 与 Element 渲染一致
// hostSetElementText(el, c2)
// 2. 如果老的节点 是 Text 节点
// 当两个节点不相等时才取更新
if (c1 !== c2) {
// 更新文本
hostSetElementText(el, c2)
}
} else {
// 新的节点 是 Array 节点, 因为只有这两种状态
}
}
Text -> Array
/* TextToArray.js */
import { h, ref } from "../../../lib/guide-mini-vue-esm.js"
// 新的是 array
// 老的是 text
const prevChildren = "oldChild";
const nextChildren = [h("div", {}, "A"), h("div", {}, "B")];
export default {
name: "TextToArray",
setup() {
// 气态代码
},
render() {
const self = this;
return self.isChange === true
? h("div", {}, nextChildren)
: h("div", {}, prevChildren);
},
};
实现 Text -> Array
的逻辑
/* renderer.ts */
// 这里往上层传入 parentComponent , 因为mountChildren 需要
function patchChildren(n1, n2, container, parentComponent) {
const { shapeFlag } = n2
// 老节点的状态 是 Text 还是 Array
const prevShapeFlag = n1.shapeFlag
// 获取新老节点的 children
const c1 = n1.children
const c2 = n2.children
// 2. 判断n2 的 shapeFlag
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 如果新的节点 是 Text 节点
// 如果老的节点 是 Array 节点
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 清空老节点的 Array
unmountChildren(c1)
}
if (c1 !== c2) {
// 更新文本
hostSetElementText(container, c2)
}
} else {
// 新的节点 是 Array 节点, 因为只有这两种状态
// 判断 n1 的 shapeFlag, 判断之前的是不是Text节点
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
// Text -> Array
// 1. 清空 Text
hostSetElementText(container, "")
// 2. 渲染 Array
mountChildren(c2, container, parentComponent)
} else {
// Array -> Array
}
}
}
/* renderer.ts */
// 初始化渲染时 - 修改 vnode -> vnode.children
// 3. children
const { children, shapeFlag } = vnode
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// if (typeof children === 'string') {
el.textContent = children
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 改为传入 VNode 的 children
mountChildren(vnode.children, el, parentComponent)
}
// 修改一些代码
function mountChildren(children, container, parentComponent) {
// 修改为 children
children.forEach((v) => {
// 组件逻辑, n1 为 null
patch(null, v, container, parentComponent)
})
}
Array -> Array
涉及到 diff 算法,写在 第六章结