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.valueh("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 初始化// 初始为 falseisMounted: false,// 使用 instance.subTree 存储 subTree, 在更新时候需要用, 需要在 component 初始化 subTree// 定义 subTree 存储上一次更新的 VNodesubTree: {},}}
使用 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 初始化 subTreeconst 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 的值// 在更新时候,获取到 上一个 subTreeconst { proxy } = instance// 拿到更新后的 subTreeconst subTree = instance.render.call(proxy)// 取出 之前保存上一次组件的 subTreeconst prevSubTree = instance.subTree// 实现 patch() 的更新逻辑// 添加 n1 n2; 老的虚拟节点 & 新的虚拟节点 -- 在这里进行赋值patch(prevSubTree, subTree, container, instance)}})}
处理初始化 patch() 传入的 老虚拟节点 & 新的虚拟节点 ; n1, n2
/*renderer.ts*/function render(vnode, container) {// 调用 path// 初始化逻辑 n1 -> nullpatch(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) {}// 处理Elementfunction 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 为 nullpatch(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"})// 修改 propsconst onChangePropsDemo1 = () => {props.value.foo = "new-foo"}// 修改 barconst 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_OBJconst newProps = n2.props || EMPTY_OBJ// 取到 n1的el, 赋值给 n2.elconst 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 的 propsconst prevProp = oldProps[key]// 取到 newProps 的 propsconst nextProp = newProps[key]// 判断 二者props是否一样if (prevProp !== nextProp) {// 不等时, 进行更新逻辑// 调用 hostPatchProp() 接口 , 因为就是在 hostPatchProp 这里实现props的赋值的hostPatchProp(el, key, prevProp, nextProp)}}// 优化2: 如果 oldProps 为空,不用进行对比if (oldProps !== EMPTY_OBJ) {// 实现3. bar 更新后这个属性在更新后没有了, 删除 bar 属性// 遍历 oldPropsfor (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 , 新的是Texth(ArrayToText),// 老的是 Text , 新的是 Text// h(TextToText),// 老的是 Text , 新的是 Array// h(TextToArray),// 老的是 Array , 新的是 Array// h(ArrayToArray)])}}
/* example/update/patchChildren/ArrayToText.js */// 新的节点 -> Textconst nextChildren = "newChildren"// 老的节点 -> Arrayconst prevChildren = [h('div', {}, "A"),h("div", {}, "B")]export default {name: "ArrayToText",setup() {// 定义响应式数据const isChange = ref(false)// 全局变量window.isChange = isChangereturn {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 还是 Arrayconst { shapeFlag } = n2// 老节点的状态 是 Text 还是 Arrayconst prevShapeFlag = n1.shapeFlag// 获取新老节点的 childrenconst c1 = n1.childrenconst c2 = n2.children// 2. 判断 n2 的 shapeFlag 新节点的 ShapeFlagif (shapeFlag & ShapeFlags.TEXT_CHILDREN) {// 如果新的节点 是 Text 节点// 如果老的节点 是 Array 节点if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// Array -> Text// 清空老节点的 ArrayunmountChildren(c1)// 设置新节点的文本// 传入 div -> 也就是 老的节点的 el , 和新的节点的 children// 写在runtime-dom 的接口上 -> 与 Element 渲染一致hostSetElementText(el, c2)}} else {// 新的节点 是 Array 节点, 因为只有这两种状态}}// 清空 children -> Arrayfunction unmountChildren(children) {// 遍历 childrenfor (let i = 0; i < children.length; i++) {// 获取每一个 children 的 el// 也就是它的父标签 divconst 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.parentNodeconst parent = el.parentNode// 判断是否存在if (parent) {// 执行删除, 使用el的父节点删除 elparent.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// 老的是 textconst 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 还是 Arrayconst { shapeFlag } = n2// 老节点的状态 是 Text 还是 Arrayconst prevShapeFlag = n1.shapeFlag// 获取新老节点的 childrenconst c1 = n1.childrenconst c2 = n2.children// 2. 判断n2 的 shapeFlagif (shapeFlag & ShapeFlags.TEXT_CHILDREN) {// 如果新的节点 是 Text 节点// 如果老的节点 是 Array 节点if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 清空老节点的 ArrayunmountChildren(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 } = n2const prevShapeFlag = n1.shapeFlagconst c1 = n1.childrenconst c2 = n2.childrenif (shapeFlag & ShapeFlags.TEXT_CHILDREN) {// 如果老的节点 是 Array 节点if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 清空老节点的 ArrayunmountChildren(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// 老的是 textconst 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 还是 Arrayconst prevShapeFlag = n1.shapeFlag// 获取新老节点的 childrenconst c1 = n1.childrenconst c2 = n2.children// 2. 判断n2 的 shapeFlagif (shapeFlag & ShapeFlags.TEXT_CHILDREN) {// 如果新的节点 是 Text 节点// 如果老的节点 是 Array 节点if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 清空老节点的 ArrayunmountChildren(c1)}if (c1 !== c2) {// 更新文本hostSetElementText(container, c2)}} else {// 新的节点 是 Array 节点, 因为只有这两种状态// 判断 n1 的 shapeFlag, 判断之前的是不是Text节点if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {// Text -> Array// 1. 清空 TexthostSetElementText(container, "")// 2. 渲染 ArraymountChildren(c2, container, parentComponent)} else {// Array -> Array}}}
/* renderer.ts */// 初始化渲染时 - 修改 vnode -> vnode.children// 3. childrenconst { children, shapeFlag } = vnodeif (shapeFlag & ShapeFlags.TEXT_CHILDREN) {// if (typeof children === 'string') {el.textContent = children} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 改为传入 VNode 的 childrenmountChildren(vnode.children, el, parentComponent)}// 修改一些代码function mountChildren(children, container, parentComponent) {// 修改为 childrenchildren.forEach((v) => {// 组件逻辑, n1 为 nullpatch(null, v, container, parentComponent)})}

Array -> Array
涉及到 diff 算法,写在 第六章结
